Functions

Expression Functions

The following functions are available in expr fields within slot derivations. See the Expression Language guide for full details on syntax and null propagation.

Function Description
str(x) Convert to string (distributes over lists)
int(x) Convert to integer (distributes over lists)
float(x) Convert to float (distributes over lists)
bool(x) Convert to boolean (distributes over lists)
abs(x) Absolute value (distributes over lists)
round(x) Round number (distributes over lists)
strlen(x) String length (distributes over lists)
max(items) Maximum of a list
min(items) Minimum of a list
len(items) Length of a list
case(pairs...) Conditional — first matching (condition, value) pair
uuid5(namespace, name) Deterministic UUID v5 generation
slugify(s, separator="_") ASCII-fold + lowercase + collapse non-alphanumerics; None on no extractable content
to_snake(s) Convert to snake_case
to_camel(s) Convert to camelCase
to_pascal(s) Convert to PascalCase

For functions not in this list, see Extension Functions to register your own.

Unit Conversion

Basic unit conversion functions.

Currently only native pint units or UCUM units are supported.

For UCUM, the ucumvert library is used to convert UCUM units to pint units, see <https://github.com/dalito/ucumvert>_.

DimensionalityError

Bases: Exception

Raised when a unit conversion cannot be performed.

Note: equivalent to the pint error, but the pint dependency is optional

Source code in src/linkml_map/functions/unit_conversion.py
47
48
49
50
51
52
53
class DimensionalityError(Exception):
    """
    Raised when a unit conversion cannot be performed.

    Note: equivalent to the pint error, but the
    pint dependency is optional
    """

UndefinedUnitError

Bases: Exception

Raised when a unit is not defined.

Note: equivalent to the pint error, but the pint dependency is optional

Source code in src/linkml_map/functions/unit_conversion.py
38
39
40
41
42
43
44
class UndefinedUnitError(Exception):
    """
    Raised when a unit is not defined.

    Note: equivalent to the pint error, but the
    pint dependency is optional
    """

UnitSystem

Bases: str, Enum

Enumeration of supported unit systems.

Source code in src/linkml_map/functions/unit_conversion.py
25
26
27
28
29
30
class UnitSystem(str, Enum):
    """Enumeration of supported unit systems."""

    UCUM = "ucum"
    IEC61360 = "iec61360"
    SI = "SI"

convert_units(magnitude, from_unit, to_unit, system=None)

Convert a quantity between units.

convert_units(1, "m", "cm") 100.0 convert_units(1, "m", "cm", system=UnitSystem.UCUM) 100.0 convert_units(1, "m", "cm", system=UnitSystem.SI) 100.0 convert_units(1.0, "hectare", "m^2", system=UnitSystem.SI) 10000.0 convert_units(1.0, "hectare", "m ** 2", system=UnitSystem.SI) 10000.0 convert_units(1.0, "hectare", "m ** 2", system=None) 10000.0 convert_units(1.0, "km2", "m2", system=UnitSystem.UCUM) 1000000.0 convert_units("100", "m", "cm") 10000.0 convert_units("3.14", "m", "cm") 314.0

:param magnitude: :param from_unit: :param to_unit: :return: converted magnitude

Source code in src/linkml_map/functions/unit_conversion.py
 56
 57
 58
 59
 60
 61
 62
 63
 64
 65
 66
 67
 68
 69
 70
 71
 72
 73
 74
 75
 76
 77
 78
 79
 80
 81
 82
 83
 84
 85
 86
 87
 88
 89
 90
 91
 92
 93
 94
 95
 96
 97
 98
 99
100
101
102
103
104
105
106
def convert_units(
    magnitude: float | int | str, from_unit: str, to_unit: str, system: UnitSystem | None = None
) -> float:
    """
    Convert a quantity between units.

    >>> convert_units(1, "m", "cm")
    100.0
    >>> convert_units(1, "m", "cm", system=UnitSystem.UCUM)
    100.0
    >>> convert_units(1, "m", "cm", system=UnitSystem.SI)
    100.0
    >>> convert_units(1.0, "hectare", "m^2", system=UnitSystem.SI)
    10000.0
    >>> convert_units(1.0, "hectare", "m ** 2", system=UnitSystem.SI)
    10000.0
    >>> convert_units(1.0, "hectare", "m ** 2", system=None)
    10000.0
    >>> convert_units(1.0, "km2", "m2", system=UnitSystem.UCUM)
    1000000.0
    >>> convert_units("100", "m", "cm")
    10000.0
    >>> convert_units("3.14", "m", "cm")
    314.0

    :param magnitude:
    :param from_unit:
    :param to_unit:
    :return: converted magnitude
    """
    magnitude = float(magnitude)
    ureg: pint.UnitRegistry = get_unit_registry(system)
    from_unit = normalize_unit(from_unit, system)
    to_unit = normalize_unit(to_unit, system)
    try:
        from_unit_q = ureg.parse_units(from_unit)
    except lark.exceptions.UnexpectedCharacters as err:
        msg = f"Cannot parse unit: {from_unit}"
        raise UndefinedUnitError(msg) from err
    except pint.errors.UndefinedUnitError as err:
        msg = f"Unknown source unit: {from_unit}"
        raise UndefinedUnitError(msg) from err
    quantity = magnitude * from_unit_q
    try:
        return quantity.to(to_unit).magnitude
    except pint.errors.UndefinedUnitError as err:
        msg = f"Unknown target unit: {from_unit}"
        raise UndefinedUnitError(msg) from err
    except pint.errors.DimensionalityError as err:
        msg = f"Cannot convert from {from_unit} to {to_unit}"
        raise DimensionalityError(msg) from err

get_unit_registry(system=None) cached

Get a unit registry.

ureg = get_unit_registry(UnitSystem.UCUM) ureg.from_ucum("m/s2.kg") str() '1.0 kilogram * meter / second ** 2' ureg.from_ucum("m[H2O]{35Cel}") # annotated UCUM code; ucumvert <0.3 names it m_H2O, >=0.3 meter_H2O .to("mbar") ureg("degC") # a standard pint unit ureg.from_ucum("g/m2") _.to(ureg.from_ucum("kg/m2")) ureg.from_ucum("nmol/mmol{Cre}") sireg = get_unit_registry(UnitSystem.SI) sireg("degC") sireg("ha")

:param system: currently only supported values are None or UnitSystem.UCUM :return:

Source code in src/linkml_map/functions/unit_conversion.py
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
@lru_cache
def get_unit_registry(
    system: UnitSystem | None = None,
) -> pint.UnitRegistry | PintUcumRegistry:
    """
    Get a unit registry.

    >>> ureg = get_unit_registry(UnitSystem.UCUM)
    >>> ureg.from_ucum("m/s2.kg")
    <Quantity(1.0, 'meter * kilogram / second ** 2')>
    >>> str(_)
    '1.0 kilogram * meter / second ** 2'
    >>> ureg.from_ucum("m[H2O]{35Cel}")  # annotated UCUM code; ucumvert <0.3 names it m_H2O, >=0.3 meter_H2O
    <Quantity(1, '..._H2O')>
    >>> _.to("mbar")
    <Quantity(98.0665, 'millibar')>
    >>> ureg("degC")   # a standard pint unit
    <Quantity(1, 'degree_Celsius')>
    >>> ureg.from_ucum("g/m2")
    <Quantity(1.0, 'gram / meter ** 2')>
    >>> _.to(ureg.from_ucum("kg/m2"))
    <Quantity(0.001, 'kilogram / meter ** 2')>
    >>> ureg.from_ucum("nmol/mmol{Cre}")
    <Quantity(1.0, 'nanomole / millimole')>
    >>> sireg = get_unit_registry(UnitSystem.SI)
    >>> sireg("degC")
    <Quantity(1, 'degree_Celsius')>
    >>> sireg("ha")
    <Quantity(1, 'hectare')>

    :param system: currently only supported values are None or UnitSystem.UCUM
    :return:
    """
    import pint

    ureg = pint.UnitRegistry()
    if not system:
        return ureg
    if system in REGISTRIES:
        return REGISTRIES[system]
    if system.value in dir(ureg.sys):
        ureg.default_system = system.value
        return ureg
    msg = f"Unknown unit system: {system}"
    raise NotImplementedError(msg)

normalize_unit(unit, system=None)

Normalize the unit to UnitSystem.UCUM, if possible.

Source code in src/linkml_map/functions/unit_conversion.py
156
157
158
159
160
161
162
163
164
165
166
167
168
169
def normalize_unit(unit: str, system: UnitSystem | None = None) -> str:
    """Normalize the unit to UnitSystem.UCUM, if possible."""
    if system is None or system != UnitSystem.UCUM:
        return unit

    # this is UnitSystem.UCUM
    try:
        return str(get_unit_registry(system).from_ucum(unit))
    except pint.errors.UndefinedUnitError as err:
        msg = f"Unknown unit: {unit}"
        raise UndefinedUnitError(msg) from err
    except lark.exceptions.UnexpectedCharacters as err:
        msg = f"Cannot parse unit: {unit}"
        raise UndefinedUnitError(msg) from err

perform_unit_conversion(slot_derivation, source_obj, sv, source_type)

Convert a slot's value between units, per its unit_conversion config.

Takes the source row, schemaview and source type explicitly rather than a DerivationContext: that type lives in object_transformer, which imports this module, so depending on it here would be circular.

:param slot_derivation: the derivation carrying the unit_conversion block :param source_obj: the source row :param sv: source schema view, for resolving the slot's declared unit :param source_type: source class name :return: the converted magnitude, a structured value, or None :rtype: float | dict | None

Source code in src/linkml_map/functions/unit_conversion.py
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
def perform_unit_conversion(
    slot_derivation: SlotDerivation,
    source_obj: dict[str, Any],
    sv: SchemaView,
    source_type: str,
) -> float | dict | None:
    """Convert a slot's value between units, per its ``unit_conversion`` config.

    Takes the source row, schemaview and source type explicitly rather than a
    ``DerivationContext``: that type lives in ``object_transformer``, which imports
    this module, so depending on it here would be circular.

    :param slot_derivation: the derivation carrying the ``unit_conversion`` block
    :param source_obj: the source row
    :param sv: source schema view, for resolving the slot's declared unit
    :param source_type: source class name
    :return: the converted magnitude, a structured value, or None
    :rtype: float | dict | None
    """
    uc = slot_derivation.unit_conversion
    curr_v = source_obj.get(slot_derivation.populated_from, None)

    if curr_v is None:
        logger.debug(f"No value found for slot '{slot_derivation.populated_from}'; skipping conversion")
        return None

    slot = sv.induced_slot(slot_derivation.populated_from, source_type)
    schema_unit = None
    from_unit = None
    system = UnitSystem.UCUM

    if slot.unit:
        if slot.unit.ucum_code:
            schema_unit = slot.unit.ucum_code
        elif slot.unit.iec61360code:
            schema_unit = slot.unit.iec61360code
            system = UnitSystem.IEC61360
        elif slot.unit.symbol:
            schema_unit = slot.unit.symbol
            system = None
        elif slot.unit.abbreviation:
            schema_unit = slot.unit.abbreviation
            system = None
        elif slot.unit.descriptive_name:
            schema_unit = slot.unit.descriptive_name
            system = None
        else:
            raise NotImplementedError(f"Cannot determine unit system for slot '{slot.name}' — all unit fields are None")

    spec_unit = uc.source_unit if uc.source_unit else None

    if schema_unit and spec_unit:
        if schema_unit != spec_unit:
            raise ValueError(
                f"Mismatch in source units for slot '{slot_derivation.populated_from}': "
                f"schema unit '{schema_unit}' vs. transformation spec '{spec_unit}'"
            )
        from_unit = schema_unit
    elif schema_unit:
        from_unit = schema_unit
    elif spec_unit:
        from_unit = spec_unit
    else:
        if uc.source_unit_slot:
            from_unit = None
        else:
            slot_name = slot_derivation.populated_from
            raise ValueError(f"No source unit provided in schema or transformation spec for slot '{slot_name}'")

    if uc.source_unit_slot:
        # Structured input, e.g., {"value": 120, "unit": "cm"}
        from_unit_val = curr_v.get(uc.source_unit_slot)
        if from_unit_val:
            if from_unit and from_unit_val != from_unit:
                slot_name = slot_derivation.populated_from
                raise ValueError(
                    f"Value unit '{from_unit_val}' does not match expected '{from_unit}' for slot '{slot_name}'"
                )
            from_unit = from_unit_val
        else:
            raise ValueError(f"Missing unit in structured value for slot '{slot_derivation.populated_from}': {curr_v}")

        magnitude = curr_v.get(uc.source_magnitude_slot)
        if magnitude is None:
            raise ValueError(
                f"Missing magnitude in structured value for slot '{slot_derivation.populated_from}': {curr_v}"
            )
    else:
        magnitude = curr_v

    try:
        magnitude = float(magnitude)
    except (TypeError, ValueError):
        if uc.none_if_non_numeric:
            return None
        raise

    to_unit = uc.target_unit or from_unit
    if from_unit == to_unit:
        result = magnitude
    else:
        result = convert_units(magnitude, from_unit=from_unit, to_unit=to_unit, system=system)

    if uc.target_magnitude_slot:
        return {uc.target_magnitude_slot: result, uc.target_unit_slot: to_unit}
    return result