Skip to content

Convert schema-like objects to dataclasses #943

Description

@sadielbartholomew

As well as incorporating enumerations (see #942), there are objects in the codebase where we could convert to further data classes in order to better encode and handle their schema-like nature. Though we should only do so if a genuinely useful case for the logic using them, to avoid over-engineering.

An immediate example I see is from the parallel dictionaries in the constants module, e.g. the formula term system, where the same key space for the CF-supported Parametric Vertical Coordinates are spread across four parallel dicts which each define constraints set out in Appendix D of the CF Conventions:

cf-python/cf/constants.py

Lines 212 to 494 in 8c760f8

# --------------------------------------------------------------------
# Define the standard names that are allowed for each formula term, as
# described in Appendix D: Parametric Vertical Coordinates of the CF
# conventions.
#
# A domain ancillary or coordinate construct may have any of the
# specified names. A value of None means that no standard name has
# been defined for that term.
# --------------------------------------------------------------------
formula_terms_standard_names = {
"atmosphere_ln_pressure_coordinate": {
"p0": ("reference_air_pressure_for_atmosphere_vertical_coordinate",),
"lev": ("atmosphere_ln_pressure_coordinate",),
},
"atmosphere_sigma_coordinate": {
"sigma": ("atmosphere_sigma_coordinate",),
"ptop": ("air_pressure_at_top_of_atmosphere_model",),
"ps": ("surface_air_pressure",),
},
"atmosphere_hybrid_sigma_pressure_coordinate": {
"p0": ("reference_air_pressure_for_atmosphere_vertical_coordinate",),
"ps": ("surface_air_pressure",),
"a": (None,),
"ap": (None,),
"b": (None,),
},
"atmosphere_hybrid_height_coordinate": {
"a": ("atmosphere_hybrid_height_coordinate",),
"b": (None,),
"orog": (
"surface_altitude",
"surface_height_above_geopotential_datum",
),
},
"atmosphere_sleve_coordinate": {
"ztop": (
"altitude_at_top_of_atmosphere_model",
"height_above_geopotential_datum_at_top_of_atmosphere_model",
),
"a": (None,),
"b1": (None,),
"b2": (None,),
"zsurf1": (None,),
"zsurf2": (None,),
},
"ocean_sigma_coordinate": {
"eta": formula_terms_D1["eta"],
"depth": formula_terms_D1["depth"],
"sigma": ("ocean_sigma_coordinate",),
},
"ocean_s_coordinate": {
"eta": formula_terms_D1["eta"],
"depth": formula_terms_D1["depth"],
"a": (None,),
"b": (None,),
"depth_c": (None,),
"C": (None,),
"s": ("ocean_s_coordinate",),
},
"ocean_s_coordinate_g1": {
"eta": formula_terms_D1["eta"],
"depth": formula_terms_D1["depth"],
"depth_c": (None,),
"C": (None,),
"s": ("ocean_s_coordinate_g1",),
},
"ocean_s_coordinate_g2": {
"eta": formula_terms_D1["eta"],
"depth": formula_terms_D1["depth"],
"depth_c": (None,),
"C": (None,),
"s": ("ocean_s_coordinate_g2",),
},
"ocean_sigma_z_coordinate": {
"eta": formula_terms_D1["eta"],
"depth": formula_terms_D1["depth"],
"zlev": formula_terms_D1["zlev"],
"nsigma": (None,),
"depth_c": (None,),
"sigma": ("ocean_sigma_z_coordinate",),
},
"ocean_double_sigma_coordinate": {
"depth": formula_terms_D1["depth"],
"a": (None,),
"href": (None,),
"k_c": (None,),
"z1": (None,),
"z2": (None,),
"sigma": ("ocean_double_sigma_coordinate",),
},
}
# --------------------------------------------------------------------
# Set the maximum number of dimensions allowed for each formula term,
# as described in Appendix D: Parametric Vertical Coordinates of the
# CF conventions.
#
# A given domain ancillary construct may have this number of
# dimensions or fewer.
# --------------------------------------------------------------------
formula_terms_max_dimensions = {
"atmosphere_ln_pressure_coordinate": {"p0": 0, "lev": 1}, # (k)
"atmosphere_sigma_coordinate": {
"sigma": 1, # (k)
"ptop": 0,
"ps": 3, # (n,j,i)
},
"atmosphere_hybrid_sigma_pressure_coordinate": {
"p0": 0,
"ps": 3, # (n,j,i)
"a": 1, # (k)
"ap": 1, # (k)
"b": 1, # (k)
},
"atmosphere_hybrid_height_coordinate": {
"a": 1, # (k)
"b": 1, # (k)
"orog": 3, # (n,j,i)
},
"atmosphere_sleve_coordinate": {
"ztop": 0,
"a": 1, # (k)
"b1": 1, # (k)
"b2": 1, # (k)
"zsurf1": 3, # (n,j,i)
"zsurf2": 3, # (n,j,i)
},
"ocean_sigma_coordinate": {
"eta": 3, # (n,j,i)
"depth": 2, # (j,i)
"sigma": 1, # (k)
},
"ocean_s_coordinate": {
"eta": 3, # (n,j,i)
"depth": 2, # (j,i)
"a": 0,
"b": 0,
"depth_c": 0,
"C": 1, # (k)
"s": 1, # (k)
},
"ocean_s_coordinate_g1": {
"eta": 3, # (n,j,i)
"depth": 2, # (j,i)
"depth_c": 0,
"C": 1, # (k)
"s": 1, # (k)
},
"ocean_s_coordinate_g2": {
"eta": 3, # (n,j,i)
"depth": 2, # (j,i)
"depth_c": 0,
"C": 1, # (k)
"s": 1, # (k)
},
"ocean_sigma_z_coordinate": {
"eta": 3, # (n,j,i)
"depth": 2, # (j,i)
"zlev": 1, # (k)
"nsigma": 0,
"depth_c": 0,
"sigma": 1, # (k)
},
"ocean_double_sigma_coordinate": {
"depth": 2, # (j,i)
"a": 0,
"href": 0,
"k_c": 0,
"z1": 0,
"z2": 0,
"sigma": 1, # (k)
},
}
# --------------------------------------------------------------------
# Define the computed standard name of the computed vertical
# coordinate values, as described in Appendix D: Parametric Vertical
# Coordinates of the CF conventions.
#
# A string value means that there can only be one computed standard
# name.
#
# A dictionary value means that the computed standard name depends on
# the standard name of the given term. For example, the computed
# standard name for 'atmosphere_sleve_coordinate' depends on the
# standard name of the 'ztop' term.
# --------------------------------------------------------------------
_D1_depth_mapping = {
"sea_floor_depth_below_geoid": "altitude",
"sea_floor_depth_below_geopotential_datum": "height_above_geopotential_ datum",
"sea_floor_depth_below_reference_ellipsoid": "height_above_reference_ ellipsoid",
"sea_floor_depth_below_mean_sea_level": "height_above_mean_sea_ level",
}
formula_terms_computed_standard_names = {
"atmosphere_ln_pressure_coordinate": "air_pressure",
"atmosphere_sigma_coordinate": "air_pressure",
"atmosphere_hybrid_sigma_pressure_coordinate": "air_pressure",
"atmosphere_hybrid_height_coordinate": {
"orog": {
"surface_altitude": "altitude",
"surface_height_above_geopotential_datum": "height_above_geopotential_datum",
}
},
"atmosphere_sleve_coordinate": {
"ztop": {
"altitude_at_top_of_atmosphere_model": "altitude",
"height_above_geopotential_datum_at_top_of_atmosphere_model": "height_above_geopotential_datum",
}
},
"ocean_sigma_coordinate": {"depth": _D1_depth_mapping},
"ocean_s_coordinate": {"depth": _D1_depth_mapping},
"ocean_s_coordinate_g1": {"depth": _D1_depth_mapping},
"ocean_s_coordinate_g2": {"depth": _D1_depth_mapping},
"ocean_sigma_z_coordinate": {"depth": _D1_depth_mapping},
"ocean_double_sigma_coordinate": {"depth": _D1_depth_mapping},
}
# --------------------------------------------------------------------
# Define the canonical units of formula terms, as described in
# Appendix D: Parametric Vertical Coordinates of the CF conventions.
# --------------------------------------------------------------------
formula_terms_units = {
"atmosphere_ln_pressure_coordinate": {"p0": "Pa", "lev": ""},
"atmosphere_sigma_coordinate": {"sigma": "", "ptop": "Pa", "ps": "Pa"},
"atmosphere_hybrid_sigma_pressure_coordinate": {
"p0": "Pa",
"ps": "Pa",
"ap": "Pa",
"a": "",
"b": "",
},
"atmosphere_hybrid_height_coordinate": {"a": "m", "b": "", "orog": "m"},
"atmosphere_sleve_coordinate": {
"ztop": "m",
"a": "",
"b1": "",
"b2": "",
"zsurf1": "m",
"zsurf2": "m",
},
"ocean_sigma_coordinate": {"eta": "m", "depth": "m", "sigma": ""},
"ocean_s_coordinate": {
"eta": "m",
"depth": "m",
"a": "",
"b": "",
"depth_c": "m",
"C": "",
"s": "",
},
"ocean_s_coordinate_g1": {
"eta": "m",
"depth": "m",
"depth_c": "m",
"C": "",
"s": "",
},
"ocean_s_coordinate_g2": {
"eta": "m",
"depth": "m",
"depth_c": "m",
"C": "",
"s": "",
},
"ocean_sigma_z_coordinate": {
"eta": "m",
"depth": "m",
"zlev": "m",
"nsigma": "",
"depth_c": "m",
"sigma": "",
},
"ocean_double_sigma_coordinate": {
"depth": "m",
"href": "m",
"k_c": "",
"a": "m",
"z1": "m",
"z2": "m",
"sigma": "",
},
}

and where conversion to data classes of a template similar to the following would make the underlying schema explicit and improve readability/maintainability:

from dataclasses import dataclass

@dataclass(frozen=True)
class FormulaDefinition:
    standard_names: dict
    units: dict
    max_dimensions: dict
    computed_standard_names: dict

But we can systematically survey the codebase for further candidates, ideally at the same time as we plan #942.

Activity

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Labels

    Type

    No type

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions