From b2bcfa38584e86ddbbacadd41b5ee014362d702e Mon Sep 17 00:00:00 2001 From: Lauri Himanen <lauri.himanen@gmail.com> Date: Mon, 29 Aug 2022 12:32:11 +0000 Subject: [PATCH] Resolve "Unit issue with delta degrees" --- gui/src/units.js | 16 ++++++++++--- gui/src/units.spec.js | 10 +++++++-- nomad/cli/dev.py | 40 ++++++++++++++++++++------------- nomad/metainfo/metainfo.py | 14 ++++++++++-- tests/metainfo/test_metainfo.py | 10 +++++++++ 5 files changed, 68 insertions(+), 22 deletions(-) diff --git a/gui/src/units.js b/gui/src/units.js index f07f1bbcd1..68e8e2ef5a 100644 --- a/gui/src/units.js +++ b/gui/src/units.js @@ -236,15 +236,25 @@ export class Unit { /** * Normalizes the given expression into a format that can be parsed by MathJS. + * + * This function will replace the Pint power symbol of '**' with the symbol + * '^' used by MathJS. In addition, we convert any 'delta'-units (see: + * https://pint.readthedocs.io/en/stable/nonmult.html) into their regular + * counterparts: MathJS will automatically ignore the offset when using + * non-multiplicative units in expressions. + * * @param {str} expression Expression * @returns string Expression in normalized form */ normalizeExpression(expression) { - return expression.replace('**', '^') + let normalized = expression.replace(/\*\*/g, '^') + normalized = normalized.replace(/delta_/g, '') + normalized = normalized.replace(/Δ/g, '') + return normalized } /** - * Checks if the given unit has the same based dimensions as this one. + * Checks if the given unit has the same base dimensions as this one. * @param {str | Unit} unit Unit to compare to * @returns boolean Whether the units have the same base dimensions. */ @@ -385,7 +395,7 @@ export class Unit { * Gets the dimension of this unit as a string. The order of the dimensions is * fixed (determined at unit registration time). * - * @param {boolean} base Whether to return dimension in base units. Othwerwise + * @param {boolean} base Whether to return dimension in base units. Otherwise * the original unit dimensions are used. * @returns The dimensionality as a string, e.g. 'time^2 energy mass^-2' */ diff --git a/gui/src/units.spec.js b/gui/src/units.spec.js index ea982fb8c5..ca9044c026 100644 --- a/gui/src/units.spec.js +++ b/gui/src/units.spec.js @@ -89,7 +89,12 @@ test.each([ ['division', 'm/s', 'angstrom/femtosecond', 'Å / fs'], ['multiplication', 'm*s', 'angstrom*femtosecond', 'Å fs'], ['power with hat', 'm^2', 'angstrom^2', 'Å^2'], - ['power with double asterisk', 'm**2', 'angstrom**2', 'Å^2'], + ['power with double asterisk (single)', 'm**2', 'angstrom**2', 'Å^2'], + ['power with double asterisk (multiple)', 'm**2 / s**2', 'angstrom**2 / ms**2', 'Å^2 / ms^2'], + ['explicit delta (single)', 'delta_celsius', 'delta_K', 'K'], + ['explicit delta (multiple)', 'delta_celsius / delta_celsius', 'delta_K / delta_K', 'K / K'], + ['explicit delta symbol (single)', 'Δcelsius', 'ΔK', 'K'], + ['explicit delta symbol (multiple)', 'Δcelsius / Δcelsius', 'ΔK / ΔK', 'K / K'], ['combined', 'm*m/s^2', 'angstrom^2/femtosecond^2', 'Å^2 / fs^2'], ['negative exponent', 's^-2', 'femtosecond^-2', 'fs^-2'], ['simple to complex with one unit', 'N', 'kg*m/s^2', '(kg m) / s^2'], @@ -137,7 +142,8 @@ test.each([ ['celsius to kelvin', 'celsius', 'kelvin', 5, 278.15], ['fahrenheit to kelvin', 'fahrenheit', 'kelvin', 5, 258.15], ['celsius to fahrenheit', 'celsius', 'fahrenheit', 5, 41], - ['celsius to kelvin: derived unit (offset not applied)', 'joule/celsius', 'joule/kelvin', 5, 5], + ['celsius to kelvin: derived unit (implicit delta)', 'joule/celsius', 'joule/kelvin', 5, 5], + ['celsius to kelvin: derived unit (explicit delta)', 'joule/delta_celsius', 'joule/kelvin', 5, 5], ['fahrenheit to kelvin: derived unit (offset not applied)', 'joule/fahrenheit', 'joule/kelvin', 5, 9 / 5 * 5], ['celsius to fahrenheit: derived unit (offset not applied)', 'joule/celsius', 'joule/fahrenheit', 5, 5 / 9 * 5] ] diff --git a/nomad/cli/dev.py b/nomad/cli/dev.py index ccae421dde..13c8d296f4 100644 --- a/nomad/cli/dev.py +++ b/nomad/cli/dev.py @@ -380,6 +380,19 @@ def units(ctx): aliases[unit_long_name].append(unit_name) # For each defined dimension, get the available units if there are any. + def get_unit_data(unit, dimension): + unit_long_name = ureg.get_name(unit_name) + unit_abbreviation = ureg.get_symbol(unit_name) + unit_label = unit_long_name.replace('_', ' ') + unit_label = unit_label[0].upper() + unit_label[1:] + + return { + 'name': unit_long_name, + 'dimension': dimension[1:-1], + 'label': unit_label, + 'abbreviation': unit_abbreviation, + 'aliases': aliases[unit_long_name] + } dimensions = list(ureg._dimensions.keys()) unit_list = [] for dimension in dimensions: @@ -389,19 +402,8 @@ def units(ctx): continue else: for unit in units: - key = str(unit) - unit_long_name = ureg.get_name(key) - unit_abbreviation = ureg.get_symbol(key) - unit_label = unit_long_name.replace('_', ' ') - unit_label = unit_label[0].upper() + unit_label[1:] - - unit_list.append({ - 'name': unit_long_name, - 'dimension': dimension[1:-1], - 'label': unit_label, - 'abbreviation': unit_abbreviation, - 'aliases': aliases[unit_long_name] - }) + unit_name = str(unit) + unit_list.append(get_unit_data(unit_name, dimension)) # Some units need to be added manually. unit_list.extend([ @@ -503,8 +505,16 @@ def units(ctx): is_number = False if not is_operator and not is_number: units.add(part) - # for unit in units: - # assert unit in unit_names, 'The unit "{}" is not defined in the unit definitions.'.format(unit) + + # Check that the defined units do not contain 'delta_' or 'Δ' in them. This is + # reserved to indicate that a quantity should be treated without offset. + # MathJS does not have explicit support for these delta-units, but instead + # uses them implicitly when non-multiplicative units appear in expressions. + for unit in unit_names: + assert 'delta_' not in unit and 'Δ' not in unit, ( + f'Invalid unit name {unit}. "delta_" and "Δ" are reserved for unit variants ' + 'with no offset, but MathJS does not have support for these units.' + ) # Print unit conversion table and unit systems as a Javascript source file output = '''/* diff --git a/nomad/metainfo/metainfo.py b/nomad/metainfo/metainfo.py index aa288097f8..e98573a7b4 100644 --- a/nomad/metainfo/metainfo.py +++ b/nomad/metainfo/metainfo.py @@ -423,10 +423,20 @@ class _Dimension(DataType): class _Unit(DataType): def set_normalize(self, section, quantity_def: 'Quantity', value): + # Explicitly providing a Pint delta-unit is not currently allowed. + # Implicit conversions are fine as MathJS on the frontend supports them. + def has_delta(unit_string): + return 'delta_' in unit_string or 'Δ' in unit_string + + delta_error = TypeError('Explicit Pint "delta"-units are not yet supported.') if isinstance(value, str): + if has_delta(value): + raise delta_error value = units.parse_units(value) - - elif not isinstance(value, pint.unit._Unit): + elif isinstance(value, pint.unit._Unit): + if has_delta(str(value)): + raise delta_error + else: raise TypeError('Units must be given as str or pint Unit instances.') return value diff --git a/tests/metainfo/test_metainfo.py b/tests/metainfo/test_metainfo.py index 7a2b46dfa4..c300d33508 100644 --- a/tests/metainfo/test_metainfo.py +++ b/tests/metainfo/test_metainfo.py @@ -182,6 +182,16 @@ class TestM2: def test_unit(self): assert System.lattice_vectors.unit is not None + def test_unit_explicit_delta(self): + with pytest.raises(TypeError): + Quantity(type=np.dtype(np.float64), unit='delta_degC / hr') + with pytest.raises(TypeError): + Quantity(type=np.dtype(np.float64), unit='ΔdegC / hr') + Quantity(type=np.dtype(np.float64), unit='degC / hr') + with pytest.raises(TypeError): + Quantity(type=np.dtype(np.float64), unit=ureg.delta_degC / ureg.hour) + Quantity(type=np.dtype(np.float64), unit=ureg.degC / ureg.hour) + def test_extension(self): assert getattr(Run, 'x_vasp_raw_format', None) is not None assert 'x_vasp_raw_format' in Run.m_def.all_quantities -- GitLab