Unit-Aware Type Annotations#
brainunit provides a comprehensive type annotation system that lets you express physical-unit constraints directly in Python type hints. This enables:
Self-documenting code — function signatures show expected units
Runtime validation — catch unit mismatches at the function boundary
isinstance support — check if a quantity has the right dimension at runtime
IDE support — type checkers see
Annotated[Quantity, ...]metadata
The system is built on PEP 593 (typing.Annotated) and follows the same patterns as astropy’s unit-aware type hints.
import brainunit as u
import jax.numpy as jnp
1. The Quantity[...] Subscript Syntax#
The simplest way to annotate unit-aware code is with Quantity[unit]. This produces a typing.Annotated type that records the unit constraint as metadata.
Annotating with a specific Unit#
Pass any Unit object to Quantity[...]:
# Annotate with a specific unit
u.Quantity[u.meter]
Quantity[m]
# Compound units work too
u.Quantity[u.meter / u.second]
Quantity[m / s]
u.Quantity[u.kilogram * u.meter / u.second ** 2]
Quantity[N]
Annotating with a physical type (dimension name)#
Instead of a specific unit, you can annotate with a physical type string. This is more flexible — it accepts any unit of that dimension:
# Annotate with a physical type string
u.Quantity["length"]
Quantity['length']
u.Quantity["speed"]
Quantity['speed']
u.Quantity["voltage"]
Quantity['voltage']
Available physical types#
The following physical type strings are recognized:
Base dimensions |
Derived dimensions |
Compound dimensions |
|---|---|---|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
||
|
||
|
||
|
||
|
2. isinstance Support#
Quantity[...] and PhysicalType(...) both support Python’s isinstance for runtime dimension checking. This lets you check whether a quantity has the right physical type:
# Check with Quantity[unit] — dimension-based matching
x = 2.0 * u.kmeter
print(f"isinstance(x, Quantity[u.meter]) = {isinstance(x, u.Quantity[u.meter])}") # True (km has length dim)
print(f"isinstance(x, Quantity[u.second]) = {isinstance(x, u.Quantity[u.second])}") # False (not time)
print(f"isinstance(x, Quantity['length']) = {isinstance(x, u.Quantity['length'])}") # True
print(f"isinstance(x, Quantity['mass']) = {isinstance(x, u.Quantity['mass'])}") # False
isinstance(x, Quantity[u.meter]) = True
isinstance(x, Quantity[u.second]) = False
isinstance(x, Quantity['length']) = True
isinstance(x, Quantity['mass']) = False
from brainunit.typing import PhysicalType
# PhysicalType also works directly with isinstance
v = 10.0 * u.meter / u.second
print(f"isinstance(v, PhysicalType('speed')) = {isinstance(v, PhysicalType('speed'))}") # True
print(f"isinstance(v, PhysicalType('length')) = {isinstance(v, PhysicalType('length'))}") # False
# Non-Quantity values always return False
print(f"isinstance(42, PhysicalType('length')) = {isinstance(42, PhysicalType('length'))}") # False
isinstance(v, PhysicalType('speed')) = True
isinstance(v, PhysicalType('length')) = False
isinstance(42, PhysicalType('length')) = False
3. Using Annotations in Function Signatures#
Annotated functions are self-documenting — the signature tells you exactly what units are expected:
def kinetic_energy(
m: u.Quantity[u.kilogram],
v: u.Quantity[u.meter / u.second],
) -> u.Quantity[u.joule]:
"""Calculate kinetic energy: KE = 0.5 * m * v^2."""
return 0.5 * m * v ** 2
result = kinetic_energy(10.0 * u.kilogram, 3.0 * u.meter / u.second)
print(f"KE = {result}")
print(f"Dimension matches joule? {result.dim == u.joule.dim}")
KE = 45. J
Dimension matches joule? True
With physical type strings (more flexible — allows any unit of that dimension):
def travel_time(
distance: u.Quantity["length"],
speed: u.Quantity["speed"],
) -> u.Quantity["time"]:
"""Calculate travel time = distance / speed."""
return distance / speed
# Works with any length/speed units
t = travel_time(100.0 * u.kmeter, 50.0 * u.kmeter / u.second)
print(f"Time = {t}")
Time = 2. s
4. Pre-built Type Aliases#
The brainunit.typing module provides ready-made aliases for common physical types. These also support isinstance checks.
from brainunit.typing import LENGTH, MASS, TIME, SPEED, ENERGY, VOLTAGE, FORCE
def displacement(v: SPEED, t: TIME) -> LENGTH:
"""Calculate displacement = velocity * time."""
return v * t
d = displacement(10.0 * u.meter / u.second, 5.0 * u.second)
print(f"Displacement = {d}")
Displacement = 50. m
def newton_second_law(m: MASS, a: u.Quantity["acceleration"]) -> FORCE:
"""F = m * a."""
return m * a
force = newton_second_law(5.0 * u.kilogram, 9.8 * u.meter / u.second ** 2)
print(f"Force = {force}")
print(f"Dimension matches newton? {force.dim == u.newton.dim}")
Force = 49. N
Dimension matches newton? True
Full list of pre-built aliases#
Alias |
Physical Type |
|---|---|
|
Any |
|
Dimensionless |
|
Length |
|
Mass |
|
Time |
|
Electric current |
|
Temperature |
|
Amount of substance |
|
Luminous intensity |
|
Frequency |
|
Force |
|
Energy |
|
Power |
|
Pressure |
|
Electric charge |
|
Voltage / Electric potential |
|
Resistance |
|
Capacitance |
|
Conductance |
|
Magnetic flux |
|
Magnetic field |
|
Inductance |
|
Speed |
|
Acceleration |
|
Area |
|
Volume |
|
Density |
5. Core Type Aliases#
For general-purpose typing, brainunit.typing also provides:
QuantityLike— any value that can be converted to aQuantity(numbers, arrays, existing Quantities)UnitLike— any value interpretable as aUnit(Unit objects, strings like"mV", orNone)DimensionLike— any value interpretable as aDimension(Dimension objects or strings)
from brainunit.typing import QuantityLike, UnitLike
def make_quantity(value: QuantityLike, unit: UnitLike = None) -> u.Quantity:
"""Create a Quantity from flexible input types."""
if unit is not None:
return u.Quantity(value, unit=unit)
return u.Quantity(value)
# All of these work
print(make_quantity(3.14, u.meter))
print(make_quantity(jnp.array([1.0, 2.0]), "mV"))
print(make_quantity(5.0 * u.second))
3.14 m
[1. 2.] mV
5. s
6. Runtime Validation with @validate_units#
The @validate_units decorator checks that Quantity arguments match their annotated units/dimensions at call time. It inspects Quantity[...] metadata and checks every annotated argument on each call.
from brainunit.typing import validate_units
@validate_units
def ohms_law(
V: u.Quantity[u.volt],
R: u.Quantity[u.ohm],
) -> u.Quantity[u.amp]:
"""Calculate current: I = V / R."""
return V / R
# Correct units — works fine
current = ohms_law(12.0 * u.volt, 4.0 * u.ohm)
print(f"Current = {current}")
Current = 3. A
# Compatible units also work (millivolt is still a voltage)
current2 = ohms_law(500.0 * u.mvolt, 100.0 * u.ohm)
print(f"Current = {current2}")
Current = 5. mA
# Wrong units — raises an error!
try:
ohms_law(12.0 * u.meter, 4.0 * u.ohm) # meter is not a voltage
except Exception as e:
print(f"Caught: {type(e).__name__}: {e}")
Caught: DimensionMismatchError: Argument 'V' of 'ohms_law' expected dimension compatible with V, got m.
# Non-Quantity argument — also caught
try:
ohms_law(12.0, 4.0 * u.ohm) # plain float is not a Quantity
except TypeError as e:
print(f"Caught: {e}")
Caught: Argument 'V' of 'ohms_law' expected a Quantity, got float.
Strict mode#
By default, @validate_units only checks dimensional compatibility. Use strict=True to require exact unit match (same scale):
@validate_units(strict=True)
def precise_voltage(V: u.Quantity[u.volt]) -> u.Quantity[u.volt]:
"""Only accepts values in volts, not millivolts or kilovolts."""
return V
# Exact unit match — OK
print(precise_voltage(5.0 * u.volt))
# Different scale — rejected in strict mode
try:
precise_voltage(500.0 * u.mvolt)
except Exception as e:
print(f"Strict mode caught: {type(e).__name__}: {e}")
5. V
Strict mode caught: UnitMismatchError: Argument 'V' of 'precise_voltage' expected unit V, got mV.
Physical type validation#
@validate_units also works with physical type string annotations:
@validate_units
def momentum(
m: u.Quantity["mass"],
v: u.Quantity["speed"],
) -> u.Quantity["momentum"]:
"""Calculate momentum: p = m * v."""
return m * v
p = momentum(2.0 * u.kilogram, 5.0 * u.meter / u.second)
print(f"Momentum = {p}")
# Wrong dimension
try:
momentum(2.0 * u.kilogram, 5.0 * u.meter) # length, not speed
except Exception as e:
print(f"Caught: {type(e).__name__}: {e}")
Momentum = 10. kg * m / s
Caught: DimensionMismatchError: Argument 'v' of 'momentum' expected physical type 'speed', got dimension m.
7. The PhysicalType Class#
PhysicalType is the annotation marker that records dimension constraints. You usually don’t need to create it directly — Quantity["speed"] does it for you — but it’s available for advanced use:
from brainunit.typing import PhysicalType
pt = PhysicalType("speed")
print(f"Physical type: {pt}")
print(f"Dimension: {pt.dimension}")
print(f"Same as meter/second dim? {pt.dimension == (u.meter / u.second).dim}")
Physical type: PhysicalType('speed')
Dimension: m s^-1
Same as meter/second dim? True
# PhysicalType objects are hashable and comparable
print(PhysicalType("length") == PhysicalType("length")) # True
print(PhysicalType("length") == PhysicalType("mass")) # False
# Case-insensitive
print(PhysicalType("Length") == PhysicalType("length")) # True
True
False
True
8. Composing with typing.Optional and typing.Union#
Unit-annotated Quantities work with standard typing constructs:
from typing import Optional
def apply_force(
mass: u.Quantity[u.kilogram],
acceleration: u.Quantity[u.meter / u.second ** 2],
friction: Optional[u.Quantity[u.newton]] = None,
) -> u.Quantity[u.newton]:
"""Calculate net force, optionally subtracting friction."""
force = mass * acceleration
if friction is not None:
force = force - friction
return force
# Without friction
print(apply_force(5.0 * u.kilogram, 2.0 * u.meter / u.second ** 2))
# With friction
print(apply_force(5.0 * u.kilogram, 2.0 * u.meter / u.second ** 2, 3.0 * u.newton))
10. N
7. N
9. Introspecting Annotations#
Unit annotations can be inspected programmatically via the _metadata attribute:
# Introspect the metadata stored in Quantity[...] types
length_type = u.Quantity[u.meter]
speed_type = u.Quantity["speed"]
print(f"Quantity[u.meter] metadata: {length_type._metadata}")
print(f"Quantity['speed'] metadata: {speed_type._metadata}")
Quantity[u.meter] metadata: m
Quantity['speed'] metadata: PhysicalType('speed')
10. Comparison: Quantity[unit] vs @check_dims vs @check_units#
brainunit provides three approaches to unit validation. Here’s when to use each:
Feature |
|
|
|
|---|---|---|---|
Style |
PEP 593 type hints |
Decorator kwargs |
Decorator kwargs |
isinstance |
Yes |
No |
No |
Validates |
Input arguments |
Input + return |
Input + return |
Granularity |
Unit or dimension |
Dimension only |
Unit (exact) |
Best for |
New code, libraries |
Legacy code, quick checks |
Strict unit matching |
# Approach 1: Type annotations (recommended for new code)
@validate_units
def approach1(V: u.Quantity[u.volt], I: u.Quantity[u.amp]) -> u.Quantity[u.watt]:
return V * I
# Approach 2: check_dims decorator
@u.check_dims(V=u.volt.dim, I=u.amp.dim, result=u.watt.dim)
def approach2(V, I):
return V * I
# Approach 3: check_units decorator
@u.check_units(V=u.volt, I=u.amp, result=u.watt)
def approach3(V, I):
return V * I
# All three produce the same result
V, I = 12.0 * u.volt, 2.0 * u.amp
print(f"Approach 1: {approach1(V, I)}")
print(f"Approach 2: {approach2(V, I)}")
print(f"Approach 3: {approach3(V, I)}")
Approach 1: 24. W
Approach 2: 24. W
Approach 3: 24. W
11. Best Practices#
Use physical type strings for public APIs#
Physical type strings ("length", "speed") are more flexible than specific units. They accept any unit of the right dimension:
# Preferred: accepts meter, kilometer, mile, etc.
def distance(x: Quantity["length"]) -> Quantity["length"]: ...
# More restrictive: conceptually expects meters
def distance(x: Quantity[u.meter]) -> Quantity[u.meter]: ...
Use specific units for internal functions#
When a function’s implementation depends on a specific unit (e.g., for numerical constants), annotate with that unit:
@validate_units(strict=True)
def _internal_calc(voltage_mV: Quantity[u.mvolt]) -> Quantity[u.mvolt]:
# Implementation uses millivolt-specific constants
return voltage_mV * 0.5
Use isinstance for runtime branching#
def process(quantity):
if isinstance(quantity, Quantity["length"]):
return quantity.in_unit(u.meter)
elif isinstance(quantity, Quantity["time"]):
return quantity.in_unit(u.second)
else:
raise TypeError(f"Unexpected dimension: {quantity.dim}")
Combine with @validate_units for safety-critical code#
@validate_units
def drug_dosage(
concentration: Quantity["density"],
volume: Quantity["volume"],
patient_mass: Quantity["mass"],
) -> Quantity["dimensionless"]:
return (concentration * volume) / patient_mass
Use QuantityLike and UnitLike for flexible inputs#
from brainunit.typing import QuantityLike, UnitLike
def convert(value: QuantityLike, target: UnitLike) -> Quantity:
q = Quantity(value)
if target is not None:
return q.in_unit(target)
return q