# units { #tvbo.utils.units }

`utils.units`

Unit and Dimension Utilities.

Central mapping between TVBO's ``UnitEnum`` (QUDT-backed), a backend's own unit vocabulary, SymPy units, and legacy free-text unit strings found in older model YAMLs.

The ``UnitEnum`` values use conventional abbreviations (ms, mV, nA, etc.) as defined in the LinkML schema (``schema/tvbo_datamodel.yaml``).

* ``unit_to_lems_dimension(unit)`` — map a UnitEnum value to a LEMS dimension name
* ``unit_to_lems_symbol(unit)`` — map a UnitEnum value to the LEMS unit symbol
* ``unit_has_time_dimension(unit)`` — whether the unit carries a time component
* ``normalize_unit(raw)`` — convert legacy free-text strings to UnitEnum values

## Attributes

| Name | Description |
| --- | --- |
| [DEFAULT_TIME_UNIT](#tvbo.utils.units.DEFAULT_TIME_UNIT) |  |

## Functions

| Name | Description |
| --- | --- |
| [normalize_unit](#tvbo.utils.units.normalize_unit) | Convert a legacy free-text unit string to a UnitEnum value name. |
| [time_unit_factor](#tvbo.utils.units.time_unit_factor) | The exact factor converting a time-valued number from one scope's unit to another's. |
| [time_unit_of](#tvbo.utils.units.time_unit_of) | The time unit governing *scopes*, resolved outwards, `ms` if nobody declares one. |
| [unit_dimensions](#tvbo.utils.units.unit_dimensions) | Base-dimension exponents of *unit* as `{base unit name: Fraction}`. |
| [unit_expression](#tvbo.utils.units.unit_expression) | *unit* as a SymPy expression over SI base quantities, or `None` if uncurated. |
| [unit_facts](#tvbo.utils.units.unit_facts) | The vendored QUDT record for *unit*, or `None` if it is not curated. |
| [unit_has_time_dimension](#tvbo.utils.units.unit_has_time_dimension) | Return True if the unit carries a time component (T or T⁻¹). |
| [unit_multiplier](#tvbo.utils.units.unit_multiplier) | The exact `Fraction` scaling *unit* onto SI base units, or `None` if uncurated. |
| [unit_named](#tvbo.utils.units.unit_named) | The one curated unit whose expression is exactly *expression*, or `None`. |
| [unit_to_latex](#tvbo.utils.units.unit_to_latex) | Return a LaTeX string for the unit, suitable for wrapping in ``$...$``. |
| [unit_to_lems_dimension](#tvbo.utils.units.unit_to_lems_dimension) | Return the LEMS dimension name for a UnitEnum value (or string). |
| [unit_to_lems_symbol](#tvbo.utils.units.unit_to_lems_symbol) | Return the LEMS unit symbol string for appending to numeric values. |
| [unit_to_si_factor](#tvbo.utils.units.unit_to_si_factor) | Return the multiplicative factor to convert from *unit* to SI base units. |
| [unit_to_symbol](#tvbo.utils.units.unit_to_symbol) | Return the conventional display symbol for a UnitEnum value. |

### normalize_unit { #tvbo.utils.units.normalize_unit }

```python
utils.units.normalize_unit(raw)
```

Convert a legacy free-text unit string to a UnitEnum value name.

Accepts abbreviations, full names, slash notation, and exponent notation.
Returns ``None`` if the string cannot be mapped — which is the answer for a unit that is real but uncurated, such as ``kg/ms^2``: the slot records it as written rather than rounding it to the nearest curated name it is not equal to.

>>> normalize_unit("mV")
'mV'
>>> normalize_unit("millivolt")
'mV'
>>> normalize_unit("ms^-1")
'per_ms'
>>> normalize_unit("kg/s^2")
'N_per_m'
>>> normalize_unit(None)

### time_unit_factor { #tvbo.utils.units.time_unit_factor }

```python
utils.units.time_unit_factor(from_scope, to_scope)
```

The exact factor converting a time-valued number from one scope's unit to another's.

Either side may be a single scope or a tuple of them resolved innermost-first, so a subnetwork can be asked *with* its enclosing scopes: one that declares no unit of its own inherits the parent's, and the factor is then exactly 1 rather than a conversion onto the fallback.

`Fraction`, not float: a subnetwork in `s` folding into a parent in `ms` is exactly 1000, and a factor of 1000 living only in a modeller's head is invisible in every backend's output — which is the error this exists to catch.

#### Raises {.doc-section .doc-section-raises}

| Name   | Type       | Description                                                                                                                            |
|--------|------------|----------------------------------------------------------------------------------------------------------------------------------------|
|        | ValueError | If either unit is not time-valued, or they are dimensionally incompatible — a silent conversion between them would be worse than none. |

### time_unit_of { #tvbo.utils.units.time_unit_of }

```python
utils.units.time_unit_of(*scopes)
```

The time unit governing *scopes*, resolved outwards, `ms` if nobody declares one.

Every time-valued number in a scope — `step_size`, `period`, `downsample_period`, `simulation_period`, `sampling_period` — is in the unit this returns. Pass the scopes innermost first (`time_unit_of(node.subnetwork, network, integrator, experiment)`); the first that declares a unit wins, and `None` entries are skipped so callers need not pre-filter. A scope's own `integration` is consulted before moving outwards, so passing a Network is enough to reach the clock it declares.

This is the only place the `ms` fallback is stated. It used to be stated in every scope at once through `ifabsent: ms`, which made an unset unit indistinguishable from a written one and so could not be inherited through — and separately at each call site, where the defaults had already drifted apart (`"s"` in one branch of the NeuroML adapter, `"ms"` in the others, and a LEMS emitter that appended `ms` unconditionally while its sibling read the declaration). A model in the database declares `s`, so those disagreed on real data.

### unit_dimensions { #tvbo.utils.units.unit_dimensions }

```python
utils.units.unit_dimensions(unit)
```

Base-dimension exponents of *unit* as `{base unit name: Fraction}`.

The SI base dimensions of ISO 80000-1, straight from QUDT's dimension vector:
`mV` is `{kilogram: 1, meter: 2, second: -3, ampere: -1}`. An empty mapping is dimensionless; `None` means uncurated, which is not the same claim.

### unit_expression { #tvbo.utils.units.unit_expression }

```python
utils.units.unit_expression(unit)
```

*unit* as a SymPy expression over SI base quantities, or `None` if uncurated.

`Q = {Q}·[Q]`: the numeric multiplier times the base-unit product, so `mV` is `kilogram*meter**2/(1000*ampere*second**3)`. Built from the vendored multiplier and dimension vector rather than from a name, because no naming convention resolves `mm_per_ms` — `sympy.physics.units` has no such quantity, and looking one up by name yields a free symbol that compares equal to nothing and raises nothing. Deriving it also keeps the expression and the dimension vector beside it from ever disagreeing.

Affine units are refused: `degC` carries a conversion offset, so it is not a multiple of kelvin and no single expression states it.

### unit_facts { #tvbo.utils.units.unit_facts }

```python
utils.units.unit_facts(unit)
```

The vendored QUDT record for *unit*, or `None` if it is not curated.

`None` covers both an unknown spelling and a value carried in the enum without unit facts (`per_unit`), because neither can be reasoned about dimensionally — the caller's fallback is the same for both.

### unit_has_time_dimension { #tvbo.utils.units.unit_has_time_dimension }

```python
utils.units.unit_has_time_dimension(unit)
```

Return True if the unit carries a time component (T or T⁻¹).

This is the key signal for NeuroML LEMS export: if any parameter in the RHS equation has a time dimension, the equation already carries time normalisation and ``/ SEC`` is not needed.

### unit_multiplier { #tvbo.utils.units.unit_multiplier }

```python
utils.units.unit_multiplier(unit)
```

The exact `Fraction` scaling *unit* onto SI base units, or `None` if uncurated.

Exact because ratios of it are the thing being checked: `mm/ms` over `m/s` is `Fraction(1)`, where the float route gives 0.9999999999999999 and a dimensional check that should read "identical" reads "off by 1e-16".

### unit_named { #tvbo.utils.units.unit_named }

```python
utils.units.unit_named(expression)
```

The one curated unit whose expression is exactly *expression*, or `None`.

The inverse of `unit_expression`, used to give a *derived* unit back its ordinary name: propagation produces `kilogram*meter**2/(1000*ampere*second**3)`, which is `mV` and reads far better said that way. Exact because the expressions are built from `Fraction` multipliers, so `mm/ms` and `m/s` are the same key rather than two that differ by 1e-16.

`None` when several units share the expression, which is not a gap in the vocabulary but a limit on what dimensional analysis can tell you: `Hz`, `per_s` and `rad_per_s` are all exactly `1/second`, as are `dimensionless`, `arbitrary_unit` and `rad` all exactly `1`. Five such groups exist among the curated units. Naming a derived quantity `Hz` would assert it is a frequency on evidence that says only "per second", so the base expression is printed instead — a weaker claim, and the true one.

### unit_to_latex { #tvbo.utils.units.unit_to_latex }

```python
utils.units.unit_to_latex(unit)
```

Return a LaTeX string for the unit, suitable for wrapping in ``$...$``.

Converts enum values like ``per_ms`` → ``\\mathrm{ms}^{-1}``, ``rad_per_ms`` → ``\\mathrm{rad}\\,\\mathrm{ms}^{-1}``.
Returns an empty string for dimensionless / unknown units.

### unit_to_lems_dimension { #tvbo.utils.units.unit_to_lems_dimension }

```python
utils.units.unit_to_lems_dimension(unit)
```

Return the LEMS dimension name for a UnitEnum value (or string).

Read from the LEMS entry in the software database, which is where a claim about what LEMS calls things belongs. Units LEMS has no dimension for — including every one it simply has no name for — come back ``"none"``, its own spelling of dimensionless.

### unit_to_lems_symbol { #tvbo.utils.units.unit_to_lems_symbol }

```python
utils.units.unit_to_lems_symbol(unit)
```

Return the LEMS unit symbol string for appending to numeric values.

For dimensioned parameters (e.g. ``pF``, ``nS``, ``mV``), returns the matching LEMS unit symbol.  For dimensionless or unknown units, returns ``""``.

### unit_to_si_factor { #tvbo.utils.units.unit_to_si_factor }

```python
utils.units.unit_to_si_factor(unit)
```

Return the multiplicative factor to convert from *unit* to SI base units.

Read from the vendored QUDT record, not from a local table. The table this replaced held 19 of the 62 curated units and returned `1.0` for the rest, so `mm` converted as though it were metres — silently, which is how it survived.

>>> unit_to_si_factor("mV")
0.001
>>> unit_to_si_factor("ms")
0.001
>>> unit_to_si_factor(None)
1.0

### unit_to_symbol { #tvbo.utils.units.unit_to_symbol }

```python
utils.units.unit_to_symbol(unit)
```

Return the conventional display symbol for a UnitEnum value.

For simple abbreviations (ms, mV, nA), the enum value IS the symbol.
For compound forms (per_ms, mV_per_ms), returns conventional notation.

>>> unit_to_symbol("ms")
'ms'
>>> unit_to_symbol("per_ms")
'1/ms'
>>> unit_to_symbol(None)
''