Skip to content

Commit

Permalink
Use pint instead of cf_units
Browse files Browse the repository at this point in the history
  • Loading branch information
ocefpaf committed Jun 20, 2024
1 parent b406f22 commit f276028
Show file tree
Hide file tree
Showing 4 changed files with 134 additions and 7 deletions.
15 changes: 8 additions & 7 deletions compliance_checker/cfutil.py
Original file line number Diff line number Diff line change
Expand Up @@ -8,9 +8,10 @@
from collections import defaultdict
from functools import lru_cache, partial

from cf_units import Unit
from importlib_resources import files

from compliance_checker.units import UndefinedUnitError, units

_UNITLESS_DB = None
_SEA_NAMES = None

Expand Down Expand Up @@ -111,8 +112,8 @@ def is_dimensionless_standard_name(standard_name_table, standard_name):
f".//entry[@id='{standard_name}']",
)
if found_standard_name is not None:
canonical_units = Unit(found_standard_name.find("canonical_units").text)
return canonical_units.is_dimensionless()
canonical_units = units(found_standard_name.find("canonical_units").text)
return canonical_units.dimensionless
# if the standard name is not found, assume we need units for the time being
else:
return False
Expand Down Expand Up @@ -2037,8 +2038,8 @@ def units_convertible(units1, units2, reftimeistime=True):
:param str units2: A string representing the units
"""
try:
u1 = Unit(units1)
u2 = Unit(units2)
except ValueError:
u1 = units(units1)
u2 = units(units2)
except UndefinedUnitError:
return False
return u1.is_convertible(u2)
return u1.is_compatible_with(u2)
1 change: 1 addition & 0 deletions compliance_checker/tests/test_cf.py
Original file line number Diff line number Diff line change
Expand Up @@ -1211,6 +1211,7 @@ def test_dimensionless_vertical(self):
# Check negative compliance -- 3 out of 4 pass

dataset = self.load_dataset(STATIC_FILES["bad"])
print(f"{STATIC_FILES["bad"]=}")
results = self.cf.check_dimensionless_vertical_coordinates(dataset)
scored, out_of, messages = get_results(results)
assert len(results) == 4
Expand Down
124 changes: 124 additions & 0 deletions compliance_checker/units.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,124 @@
"""Module to provide unit support via pint approximating UDUNITS/CF."""

import functools
import re

import pint
from pint import ( # noqa: F401
DimensionalityError,
UndefinedUnitError,
UnitStrippedWarning,
)

# from `xclim`'s unit support module with permission of the maintainers
try:

@pint.register_unit_format("cf")
def short_formatter(unit, registry, **options):
"""Return a CF-compliant unit string from a `pint` unit.
Parameters
----------
unit : pint.UnitContainer
Input unit.
registry : pint.UnitRegistry
the associated registry
**options
Additional options (may be ignored)
Returns
-------
out : str
Units following CF-Convention, using symbols.
"""
import re

Check warning on line 34 in compliance_checker/units.py

View check run for this annotation

Codecov / codecov/patch

compliance_checker/units.py#L34

Added line #L34 was not covered by tests

# convert UnitContainer back to Unit
unit = registry.Unit(unit)

Check warning on line 37 in compliance_checker/units.py

View check run for this annotation

Codecov / codecov/patch

compliance_checker/units.py#L37

Added line #L37 was not covered by tests
# Print units using abbreviations (millimeter -> mm)
s = f"{unit:~D}"

Check warning on line 39 in compliance_checker/units.py

View check run for this annotation

Codecov / codecov/patch

compliance_checker/units.py#L39

Added line #L39 was not covered by tests

# Search and replace patterns
pat = r"(?P<inverse>(?:1 )?/ )?(?P<unit>\w+)(?: \*\* (?P<pow>\d))?"

Check warning on line 42 in compliance_checker/units.py

View check run for this annotation

Codecov / codecov/patch

compliance_checker/units.py#L42

Added line #L42 was not covered by tests

def repl(m):
i, u, p = m.groups()
p = p or (1 if i else "")
neg = "-" if i else ""

Check warning on line 47 in compliance_checker/units.py

View check run for this annotation

Codecov / codecov/patch

compliance_checker/units.py#L44-L47

Added lines #L44 - L47 were not covered by tests

return f"{u}{neg}{p}"

Check warning on line 49 in compliance_checker/units.py

View check run for this annotation

Codecov / codecov/patch

compliance_checker/units.py#L49

Added line #L49 was not covered by tests

out, n = re.subn(pat, repl, s)

Check warning on line 51 in compliance_checker/units.py

View check run for this annotation

Codecov / codecov/patch

compliance_checker/units.py#L51

Added line #L51 was not covered by tests

# Remove multiplications
out = out.replace(" * ", " ")

Check warning on line 54 in compliance_checker/units.py

View check run for this annotation

Codecov / codecov/patch

compliance_checker/units.py#L54

Added line #L54 was not covered by tests
# Delta degrees:
out = out.replace("Δ°", "delta_deg")
return out.replace("percent", "%")

Check warning on line 57 in compliance_checker/units.py

View check run for this annotation

Codecov / codecov/patch

compliance_checker/units.py#L56-L57

Added lines #L56 - L57 were not covered by tests

except ImportError:
pass

Check warning on line 60 in compliance_checker/units.py

View check run for this annotation

Codecov / codecov/patch

compliance_checker/units.py#L59-L60

Added lines #L59 - L60 were not covered by tests

# ------
# Reused with modification from MetPy under the terms of the BSD 3-Clause License.
# Copyright (c) 2015,2017,2019 MetPy Developers.
# Create registry, with preprocessors for UDUNITS-style powers (m2 s-2) and percent signs
units = pint.UnitRegistry(
autoconvert_offset_to_baseunit=True,
preprocessors=[
lambda x: (
"count" if x == "1" else x
), # Should be salinity as well but all we can is that it is integer and dimensionless
lambda x: "S_K" if x.lower() in ["0.001", "1e-3"] else x,
functools.partial(
re.compile(
r"(?<=[A-Za-z])(?![A-Za-z])(?<![0-9\-][eE])(?<![0-9\-])(?=[0-9\-])",
).sub,
"**",
),
lambda string: string.replace("%", "percent"),
],
force_ndarray_like=True,
)
# ----- end block copied from metpy

# need to insert to make sure this is the first preprocessor
# This ensures we convert integer `1` to string `"1"`, as needed by pint.
units.preprocessors.insert(0, str)

# -----
units.define("percent = 0.01 = %")

# Define commonly encountered units (both CF and non-CF) not defined by pint
units.define("@alias meter = gpm")
# ----- end block copied from metpy

# -----
# The following redefinitions were copied from xclim under the terms of their Apache-2 license
# In pint, the default symbol for year is "a" which is not CF-compliant (stands for "are")
units.define("year = 365.25 * day = yr")

# Define commonly encountered units not defined by pint
units.define("@alias degC = C = deg_C = Celsius = degrees_Celsius")
units.define("@alias degK = deg_K")
units.define("@alias day = d")
units.define("@alias hour = h") # Not the Planck constant...
units.define(
"degrees_north = degree = degrees_north = degrees_N = degreesN = degree_north = degree_N = Degrees_N = degreeN",
)
units.define(
"degrees_east = degree = degrees_east = degrees_E = degreesE = degree_east = degree_E = Degrees_E = degreeE",
)
# degrees for grid_longitude / grid_latitude for grid_mappings
units.define("degrees = degree = degrees")
units.define("[speed] = [length] / [time]")
# ----- end block copied from xclim

# Add other specific aliases (by cf_xarray developers)
units.define("practical_salinity_unit = [] = psu = PSU")
units.define("parts_per_thousand = 0.001 = sea_water_knudsen_salinity = S_K")

# end of vendored code from MetPy

# Set as application registry
pint.set_application_registry(units)
1 change: 1 addition & 0 deletions requirements.txt
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@ netcdf4>=1.6.4
owslib>=0.8.3
packaging
pendulum>=1.2.4
pint
pygeoif>=0.6
pyproj>=2.2.1
regex>=2017.07.28
Expand Down

0 comments on commit f276028

Please sign in to comment.