API Reference¶
Core Python API for ucon.
parse¶
Parse human-readable quantity strings into Number objects.
Basic Usage¶
# Simple quantities
parse("60 mi/h") # <60 mi/h>
parse("9.81 m/s^2") # <9.81 m/s²>
parse("1.5 kg") # <1.5 kg>
# Pure numbers (dimensionless)
parse("100") # <100>
parse("3.14159") # <3.14159>
# Scientific notation
parse("1.5e3 m") # <1500 m>
parse("6.022e23") # <6.022e23>
# Negative values
parse("-273.15 degC") # <-273.15 °C>
Uncertainty¶
# Plus-minus notation
parse("1.234 ± 0.005 m") # <1.234 ± 0.005 m>
parse("1.234 +/- 0.005 m") # ASCII alternative
# Parenthetical (metrology convention)
parse("1.234(5) m") # means 1.234 ± 0.005
parse("1.234(56) m") # means 1.234 ± 0.056
# Uncertainty with unit
parse("1.234 m ± 0.005 m") # <1.234 ± 0.005 m>
Error Handling¶
from ucon.parsing import ParseError
# Unknown unit
parse("60 foobar") # raises UnknownUnitError
# Invalid number
parse("abc m") # raises ParseError (subclass of ValueError)
# Empty string
parse("") # raises ValueError
ParseError¶
Raised when a quantity string cannot be parsed. Subclass of ValueError.
from ucon.parsing import ParseError
try:
parse("abc m")
except ParseError as e:
print(e) # parse error details
Number¶
The primary type for representing dimensional quantities.
Creating Numbers¶
# Via unit callable
distance = units.meter(5) # <5 m>
mass = units.gram(250) # <250 g>
# With scale prefix
km = Scale.kilo * units.meter
mg = Scale.milli * units.gram
d = km(10) # <10 km>
dose = mg(500) # <500 mg>
# Directly
n = Number(quantity=9.8, unit=units.meter / units.second ** 2)
Properties¶
| Property | Type | Description |
|---|---|---|
quantity |
float | The numeric magnitude |
unit |
Unit | UnitProduct | The unit of measurement |
dimension |
Dimension | Physical dimension |
uncertainty |
float | None | Optional uncertainty value |
Methods¶
to(target, graph=None, propagate_factor_uncertainty=False)¶
Convert to a different unit.
d = units.meter(1000)
d.to(Scale.kilo * units.meter) # <1.0 km>
weight = units.pound(154)
weight.to(Scale.kilo * units.gram) # <69.85... kg>
When propagate_factor_uncertainty=True, the uncertainty of the conversion
factor itself (from measured physical constants like Eₕ, mₚ, a₀) is
included in the result via GUM quadrature:
# Exact conversion — no uncertainty even with flag
units.meter(1).to(units.foot, propagate_factor_uncertainty=True)
# <3.28084 ft> (meter→foot is exact by definition)
# Measured conversion — factor uncertainty propagated
units.joule(1).to(units.hartree, propagate_factor_uncertainty=True)
# <2.2937e17 ± ~2.5e5 Eh> (reflects Hartree energy uncertainty)
# Combined with measurement uncertainty
n = Number(quantity=1.0, unit=units.joule, uncertainty=1e-10)
n.to(units.hartree, propagate_factor_uncertainty=True)
# Both measurement and factor uncertainties combined in quadrature
The default False preserves backward compatibility — only measurement
uncertainty (from Number.uncertainty) is propagated through Map.derivative().
simplify()¶
Express in base scale (no prefix).
Arithmetic¶
Numbers support arithmetic with dimensional consistency:
# Addition/subtraction (same dimension required)
a = units.meter(10)
b = units.meter(5)
a + b # <15 m>
a - b # <5 m>
# Multiplication/division (dimensions combine)
distance = units.meter(100)
time = units.second(10)
velocity = distance / time # <10.0 m/s>
# Scalar multiplication
dose = units.gram(5)
dose * 3 # <15 g>
Unit¶
Atomic representation of a base or derived unit.
Predefined Units¶
from ucon import units
units.meter # length
units.gram # mass
units.second # time
units.ampere # current
units.kelvin # temperature
units.mole # amount of substance
units.candela # luminous intensity
units.newton # force
units.joule # energy
units.watt # power
# ... see Units & Dimensions reference
Properties¶
| Property | Type | Description |
|---|---|---|
name |
str | Canonical name |
shorthand |
str | Primary symbol |
aliases |
tuple[str] | Alternative symbols |
dimension |
Dimension | Physical dimension |
basis |
Basis | Dimensional basis (e.g., SI, CGS) |
Methods¶
is_compatible(other, basis_graph=None)¶
Check if conversion to another unit is possible.
# Same dimension — always compatible
units.meter.is_compatible(units.foot) # True
# Different dimensions — incompatible
units.meter.is_compatible(units.second) # False
# Cross-basis — requires BasisGraph
from ucon.basis import BasisGraph
from ucon.basis.transforms import SI_TO_CGS_ESU
bg = BasisGraph().with_transform(SI_TO_CGS_ESU)
units.ampere.is_compatible(statampere, basis_graph=bg) # True
Creating Custom Units¶
from ucon.core import Unit, Dimension
drop = Unit(
name="drop",
dimension=Dimension.count,
aliases=("gtt", "drop")
)
Scale¶
SI and binary scale prefixes.
Available Scales¶
| Scale | Prefix | Factor |
|---|---|---|
Scale.peta |
P | 10^15 |
Scale.tera |
T | 10^12 |
Scale.giga |
G | 10^9 |
Scale.mega |
M | 10^6 |
Scale.kilo |
k | 10^3 |
Scale.hecto |
h | 10^2 |
Scale.deca |
da | 10^1 |
Scale.one |
- | 10^0 |
Scale.deci |
d | 10^-1 |
Scale.centi |
c | 10^-2 |
Scale.milli |
m | 10^-3 |
Scale.micro |
u | 10^-6 |
Scale.nano |
n | 10^-9 |
Scale.pico |
p | 10^-12 |
Scale.femto |
f | 10^-15 |
Scale.gibi |
Gi | 2^30 |
Scale.mebi |
Mi | 2^20 |
Scale.kibi |
Ki | 2^10 |
Usage¶
# Apply to unit
km = Scale.kilo * units.meter
mg = Scale.milli * units.gram
GiB = Scale.gibi * units.byte
# Scale composition
Scale.kilo * Scale.milli # → Scale.one
Dimension¶
Physical dimension enumeration.
Base Dimensions¶
Dimension.length
Dimension.mass
Dimension.time
Dimension.current
Dimension.temperature
Dimension.luminous_intensity
Dimension.amount_of_substance
Dimension.information
Derived Dimensions¶
Dimension.velocity # length / time
Dimension.acceleration # length / time^2
Dimension.force # mass * length / time^2
Dimension.energy # mass * length^2 / time^2
Dimension.power # energy / time
Dimension.pressure # force / area
# ... see Units & Dimensions reference
Pseudo-Dimensions¶
Dimension.angle # radian, degree
Dimension.solid_angle # steradian
Dimension.ratio # percent, ppm
Dimension.count # discrete items
Arithmetic¶
Dimension.length / Dimension.time # velocity
Dimension.mass * Dimension.acceleration # force
Dimension.energy ** 0.5 # sqrt(energy)
Constant¶
Physical constants with CODATA 2022 uncertainties.
Built-in Constants¶
SI Defining Constants (exact):
| Constant | Symbol | Value | Unit |
|---|---|---|---|
speed_of_light |
c | 299792458 | m/s |
planck_constant |
h | 6.62607015e-34 | J·s |
elementary_charge |
e | 1.602176634e-19 | C |
boltzmann_constant |
k_B | 1.380649e-23 | J/K |
avogadro_constant |
N_A | 6.02214076e23 | 1/mol |
luminous_efficacy |
K_cd | 683 | lm/W |
hyperfine_transition_frequency |
ΔνCs | 9192631770 | Hz |
Derived Constants (exact):
| Constant | Symbol | Derivation |
|---|---|---|
reduced_planck_constant |
ℏ | h / 2π |
molar_gas_constant |
R | k_B × N_A |
stefan_boltzmann_constant |
σ | (derived) |
Measured Constants (with uncertainty):
| Constant | Symbol | Uncertainty |
|---|---|---|
gravitational_constant |
G | 1.5e-15 |
fine_structure_constant |
α | 1.1e-12 |
electron_mass |
m_e | 2.8e-40 |
proton_mass |
m_p | 5.1e-37 |
neutron_mass |
m_n | 9.5e-37 |
vacuum_permittivity |
ε₀ | 1.3e-21 |
vacuum_permeability |
μ₀ | 1.9e-16 |
Usage¶
from ucon import constants, units
# Access by name
c = constants.speed_of_light
c.value # 299792458
c.unit # m/s
c.is_exact # True
c.category # "exact"
# Access by symbol (Unicode or ASCII)
constants.c # speed_of_light
constants.h # planck_constant
constants.G # gravitational_constant
constants.hbar # reduced_planck_constant (ASCII)
constants.ℏ # reduced_planck_constant (Unicode)
# Arithmetic with constants (returns Number)
mass = units.kilogram(1)
energy = mass * c ** 2 # E = mc²
energy.quantity # ~8.99e16
energy.dimension # energy
# Measured constants propagate uncertainty
F = G * mass * mass / units.meter(1) ** 2
F.uncertainty is not None # True
Enumeration¶
from ucon.constants import all_constants, get_constant_by_symbol
# List all constants
for const in all_constants():
print(f"{const.symbol}: {const.name}")
# Lookup by symbol
c = get_constant_by_symbol("c") # speed_of_light
h = get_constant_by_symbol("hbar") # reduced_planck_constant
Constant Properties¶
| Property | Type | Description |
|---|---|---|
symbol |
str | Standard symbol (e.g., "c", "h") |
name |
str | Full name (e.g., "speed of light in vacuum") |
value |
float | Numeric value in SI units |
unit |
Unit | UnitProduct | SI unit of the constant |
uncertainty |
float | None | Standard uncertainty (None if exact) |
source |
str | Data source (default: "CODATA 2022") |
category |
str | "exact", "derived", "measured", or "session" |
is_exact |
bool | True if uncertainty is None |
dimension |
Dimension | Physical dimension |
Methods¶
as_number()¶
Convert to a Number for calculations.
Creating Custom Constants¶
from ucon.constants import Constant
from ucon import units
# Domain-specific constant
custom_k = Constant(
symbol="k_custom",
name="custom spring constant",
value=250.0,
unit=units.newton / units.meter,
uncertainty=0.5, # or None for exact
source="Lab measurement",
category="session",
)
# Use in calculations
x = units.meter(0.1)
F = custom_k.as_number() * x # Hooke's law
ConversionGraph¶
Registry of conversion edges between units.
Default Graph¶
Custom Graph¶
from ucon.graph import ConversionGraph
from ucon.maps import LinearMap
graph = ConversionGraph()
# Register a unit
graph.register_unit(custom_unit)
# Add conversion edge
graph.add_edge(
src=units.foot,
dst=units.meter,
map=LinearMap(0.3048)
)
# Convert
result = graph.convert(units.foot, units.meter)
result(12) # 3.6576
Cross-Basis Edges¶
For units in different dimensional bases, use basis_transform:
from ucon.basis.transforms import SI_TO_CGS_ESU
graph.add_edge(
src=units.ampere,
dst=statampere,
map=LinearMap(2.998e9),
basis_transform=SI_TO_CGS_ESU,
)
Bulk registration with connect_systems():
graph.connect_systems(
basis_transform=SI_TO_CGS,
edges={
(units.meter, centimeter_cgs): LinearMap(100),
(units.gram, gram_cgs): LinearMap(1),
},
)
Introspection¶
# List rebased units (cross-basis bridges)
graph.list_rebased_units() # {ampere: [RebasedUnit(...), ...], ...}
# List registered transforms
graph.list_transforms() # [SI_TO_CGS_ESU, ...]
Context Manager¶
from ucon.graph import using_conversion_graph
with using_conversion_graph(my_graph):
# Unit parsing uses my_graph for resolution
value = convert(1, "custom_unit", "kg")
ConversionContext¶
Scoped cross-dimensional conversions for physical relationships that connect different dimensions (e.g., wavelength and frequency).
from ucon.contexts import ConversionContext, ContextEdge, using_context
from ucon.contexts import spectroscopy, boltzmann
Built-in Contexts¶
| Context | Edges | Physical Laws |
|---|---|---|
spectroscopy |
meter↔hertz, hertz→joule, meter→joule, joule→reciprocal_meter | f=c/λ, E=hf, E=hc/λ, k=E/(hc) |
boltzmann |
kelvin→joule | E=k_B·T |
Usage¶
from ucon import units
from ucon.contexts import spectroscopy, boltzmann, using_context
# Spectroscopy: wavelength to frequency
with using_context(spectroscopy):
result = units.meter(500e-9).to(units.hertz)
print(f"{result.quantity:.3e} Hz") # 5.996e+14 Hz
# Energy from frequency
result = units.hertz(5e14).to(units.joule)
print(f"{result.quantity:.3e} J") # 3.313e-19 J
# Boltzmann: temperature to energy
with using_context(boltzmann):
result = units.kelvin(300).to(units.joule)
print(f"{result.quantity:.3e} J") # 4.142e-21 J
# Multiple contexts
with using_context(spectroscopy, boltzmann):
# Both spectroscopy and Boltzmann edges available
pass
Cross-dimensional conversions only work inside using_context() blocks. Outside the block, DimensionMismatch is raised as expected.
Custom Contexts¶
from ucon.contexts import ConversionContext, ContextEdge
from ucon.maps import LinearMap
my_context = ConversionContext(
name="custom",
edges=(
ContextEdge(
src=units.kelvin,
dst=units.joule,
map=LinearMap(1.380649e-23),
),
),
description="Custom thermal context.",
)
with using_context(my_context):
result = units.kelvin(300).to(units.joule)
Maps¶
Composable conversion morphisms used as edges in the ConversionGraph.
Class Hierarchy¶
| Class | Formula | Use Case |
|---|---|---|
LinearMap(a, rel_uncertainty=0.0) |
y = a * x | Most unit conversions (m -> ft, kg -> lb) |
AffineMap(a, b, rel_uncertainty=0.0) |
y = a * x + b | Temperature conversions (C -> F) |
ReciprocalMap(a, rel_uncertainty=0.0) |
y = a / x | Inversely proportional (wavelength -> frequency) |
LogMap(scale, base, reference) |
y = scale * log(x/ref) + offset | Decibels, pH |
ExpMap(scale, base, reference) |
y = ref * base^(scale*x + offset) | Inverse of LogMap |
ComposedMap(outer, inner) |
y = outer(inner(x)) | Heterogeneous composition fallback |
All maps are @dataclass(frozen=True) --- immutable and hashable.
Factor Uncertainty¶
Maps carry an optional rel_uncertainty field (relative uncertainty of the
conversion factor). This is 0.0 for exact conversions and nonzero for
edges derived from measured physical constants (e.g., Hartree energy,
Planck mass).
# Exact conversion factor
m_to_ft = LinearMap(3.28084)
m_to_ft.rel_uncertainty # 0.0
# Factor from a measured constant
j_to_hartree = LinearMap(2.2937e17, rel_uncertainty=1.1e-12)
j_to_hartree.rel_uncertainty # 1.1e-12
Uncertainty composes through the standard operations:
- Composition (
f @ g):sqrt(f.rel_uncertainty**2 + g.rel_uncertainty**2) - Inverse: preserved (inversion doesn't change relative uncertainty)
- Power (
f ** n):abs(n) * f.rel_uncertainty
ComposedMap computes rel_uncertainty as a property from its inner and
outer maps. LogMap and ExpMap inherit the default 0.0 from the ABC
(they represent definitional transforms, not measured factors).
Creating Maps¶
from ucon.maps import LinearMap, AffineMap, ReciprocalMap, LogMap
# Linear: meters to feet
m_to_ft = LinearMap(3.28084)
m_to_ft(1.0) # 3.28084
# Affine: Celsius to Fahrenheit
c_to_f = AffineMap(1.8, 32)
c_to_f(100) # 212.0
# Reciprocal: wavelength to frequency (f = c / λ)
c = 299792458.0
lambda_to_freq = ReciprocalMap(c)
lambda_to_freq(500e-9) # ~5.996e14 Hz
# Logarithmic: watts to dBm
w_to_dbm = LogMap(scale=10, reference=0.001)
w_to_dbm(1.0) # 30.0 (1 W = 30 dBm)
# pH: concentration to pH
conc_to_ph = LogMap(scale=-1)
conc_to_ph(1e-7) # 7.0
Inverse¶
Every map has an inverse() that returns the reverse conversion:
ft_to_m = m_to_ft.inverse()
ft_to_m(3.28084) # ~1.0
f_to_c = c_to_f.inverse()
f_to_c(212) # 100.0
# ReciprocalMap is self-inverse: if f = c/λ, then λ = c/f
freq_to_lambda = lambda_to_freq.inverse()
freq_to_lambda(5.996e14) # ~500e-9 (back to 500 nm)
dbm_to_w = w_to_dbm.inverse() # returns ExpMap
dbm_to_w(30) # 1.0
Composition¶
Maps compose via @ (outer @ inner = apply inner first, then outer):
# Chain: meters -> feet -> inches
m_to_ft = LinearMap(3.28084)
ft_to_in = LinearMap(12)
m_to_in = ft_to_in @ m_to_ft
m_to_in(1.0) # ~39.37
# Linear @ Linear stays Linear
isinstance(m_to_in, LinearMap) # True
# Mixed types fall back to ComposedMap
mixed = LogMap(scale=10) @ AffineMap(1, 5)
isinstance(mixed, ComposedMap) # True
Derivative (Uncertainty Propagation)¶
Maps provide derivative(x) for uncertainty propagation: delta_y = |f'(x)| * delta_x.
m = LinearMap(3.28084)
m.derivative(1.0) # 3.28084 (constant for linear)
log_m = LogMap(scale=10)
log_m.derivative(100) # 10 / (100 * ln(10)) ≈ 0.0434
Worked Example: ReciprocalMap in Spectroscopy¶
ReciprocalMap models inversely proportional physical relationships
where y = constant / x. The canonical example is the wavelength–frequency
relation f = c / λ.
from ucon.maps import ReciprocalMap, LinearMap
c = 299792458.0 # speed of light (m/s)
h = 6.62607015e-34 # Planck constant (J·s)
# Wavelength (m) → frequency (Hz): f = c / λ
wl_to_freq = ReciprocalMap(c)
# Green light at 532 nm (Nd:YAG laser second harmonic)
freq = wl_to_freq(532e-9) # 5.635e14 Hz
# Self-inverse: frequency → wavelength uses the same constant
freq_to_wl = wl_to_freq.inverse()
freq_to_wl(freq) # 532e-9 m (round-trips exactly)
# Chain with LinearMap for frequency → energy: E = h * f
freq_to_energy = LinearMap(h)
energy = freq_to_energy(freq) # 3.734e-19 J (one green photon)
# Compose: wavelength → energy in one step via E = hc / λ
wl_to_energy = ReciprocalMap(c * h)
wl_to_energy(532e-9) # 3.734e-19 J
# Uncertainty propagation: d/dx[a/x] = -a/x²
wl_to_freq.derivative(532e-9) # -1.059e+21 Hz/m
These maps are what the built-in spectroscopy context uses internally.
For end-user conversions, prefer using_context(spectroscopy) over
constructing maps directly — see the
Conversion Contexts guide.
Packages¶
Load domain-specific units and conversions from TOML files.
TOML Schema¶
[package]
name = "aerospace"
version = "1.0.0"
description = "Aerospace and aviation units"
requires = [] # Names of required packages
[[units]]
name = "slug"
dimension = "mass"
aliases = ["slug"]
[[units]]
name = "nautical_mile"
dimension = "length"
shorthand = "nmi" # Explicit display symbol
aliases = ["NM"]
[[edges]]
src = "slug"
dst = "kilogram"
factor = 14.5939
[[edges]]
src = "nautical_mile"
dst = "meter"
factor = 1852
Edges support affine conversions for temperature-like units:
Edges derived from measured constants can carry a relative uncertainty:
For non-linear conversions, use the explicit map inline table with a
type key. Supported types: linear, affine, log, reciprocal.
# Logarithmic (e.g., decibels)
[[edges]]
src = "decibel_power"
dst = "ratio"
map = { type = "log", scale = 10, base = 10 }
# Reciprocal (e.g., wavelength ↔ frequency)
[[edges]]
src = "wavelength_unit"
dst = "frequency_unit"
map = { type = "reciprocal", a = 299792458.0 }
Constants can be defined for domain-specific values:
[[constants]]
symbol = "vs"
name = "speed of sound in dry air at 20C"
value = 343.0
unit = "m/s"
source = "ISA standard atmosphere"
category = "exact"
Schema: [[aspects]]¶
[[aspects]]
name = "weighting_standard"
applies_to = ["dose_equivalent"] # root-only
join_policy = "refuse" # default for aspects
[[aspects]]
name = "icrp103"
parent = "weighting_standard"
| Key | Type | Required | Description |
|---|---|---|---|
name |
string | yes | Canonical aspect name |
parent |
string | no | Name of another entry (declaration order is free); absent marks a family root |
join_policy |
string | no | "refuse" (default) or "lca" |
applies_to |
array of strings | no | Root-only. Kind names the family attaches to; ["*"] = wildcard |
multiplication_policy |
string | no | Root-only. "carry" (default) |
Root-only keys on a child entry are a load-time AspectError. Schema
faults (missing name, wrong types, unknown policies) raise
ValueError.
Loading¶
from ucon.packages import load_package
from ucon import get_default_graph
package = load_package("aerospace.ucon.toml")
print(package.name) # "aerospace"
print(package.units) # (UnitDef(...), ...)
print(package.edges) # (EdgeDef(...), ...)
print(package.constants) # (ConstantDef(...), ...)
# Apply to graph
graph = get_default_graph().with_package(package)
# Access materialized constants
for const in graph.package_constants:
print(f"{const.symbol} = {const.value} {const.unit}")
UnitDef, EdgeDef, and ConstantDef¶
# UnitDef holds the declaration before materialization
for unit_def in package.units:
print(unit_def.name) # "slug"
print(unit_def.dimension) # "mass"
print(unit_def.aliases) # ("slug",)
print(unit_def.shorthand) # None or explicit symbol
# EdgeDef holds the conversion specification
for edge_def in package.edges:
print(edge_def.src) # "slug"
print(edge_def.dst) # "kilogram"
print(edge_def.factor) # 14.5939
print(edge_def.offset) # 0.0 (non-zero for affine)
print(edge_def.rel_uncertainty) # 0.0 (nonzero for measured factors)
# ConstantDef holds the constant specification
for const_def in package.constants:
print(const_def.symbol) # "vs"
print(const_def.value) # 343.0
print(const_def.unit) # "m/s"
print(const_def.uncertainty) # None or float
Error Handling¶
from ucon.packages import load_package, PackageLoadError
try:
load_package("nonexistent.toml")
except PackageLoadError as e:
print(e) # file not found or invalid schema
enforce_dimensions¶
Decorator for runtime dimension checking.
Usage¶
@enforce_dimensions
def speed(
distance: Number[Dimension.length],
time: Number[Dimension.time],
) -> Number:
return distance / time
# Valid
speed(units.meter(100), units.second(10)) # <10.0 m/s>
# Raises ValueError
speed(units.second(100), units.second(10))
# ValueError: distance: expected dimension 'length', got 'time'
# Raises TypeError
speed(100, units.second(10))
# TypeError: distance: expected Number, got int
UnitProduct¶
Composite unit from multiplication/division of units.
Creation¶
# Via arithmetic
velocity = units.meter / units.second
acceleration = units.meter / units.second ** 2
# Via Scale
km = Scale.kilo * units.meter # Returns UnitProduct
# Explicit
up = UnitProduct({
UnitFactor(units.meter, Scale.one): 1,
UnitFactor(units.second, Scale.one): -2,
})
Properties¶
| Property | Type | Description |
|---|---|---|
factors |
dict[UnitFactor, float] | Factor-to-exponent mapping |
dimension |
Dimension | Combined dimension |
shorthand |
str | Formatted string (e.g., "m/s") |
Exceptions¶
from ucon.graph import (
ConversionNotFound,
DimensionMismatch,
CyclicInconsistency,
)
from ucon.basis import (
NoTransformPath,
LossyProjection,
)
from ucon.units import UnknownUnitError
| Exception | When Raised |
|---|---|
ConversionNotFound |
No path between units in graph |
DimensionMismatch |
Operation on incompatible dimensions |
CyclicInconsistency |
Adding edge creates inconsistent cycle |
NoTransformPath |
No BasisTransform connects the two bases |
LossyProjection |
Dimension cannot be represented in target basis |
UnknownUnitError |
Unit string cannot be parsed |
Basis System¶
For cross-basis conversions (e.g., SI ↔ CGS).
from ucon.basis import Basis, BasisGraph, BasisTransform
from ucon.basis.builtin import SI, CGS, CGS_ESU
from ucon.basis.transforms import SI_TO_CGS, SI_TO_CGS_ESU, SI_TO_CGS_EMU
Standard Bases¶
| Basis | Components | Description |
|---|---|---|
SI |
T, L, M, I, Θ, J, N, B | International System (8 dimensions) |
CGS |
L, M, T | Centimetre-gram-second (mechanical) |
CGS_ESU |
L, M, T, Q | CGS electrostatic (charge is fundamental) |
Standard Transforms¶
| Transform | Source | Target | Notes |
|---|---|---|---|
SI_TO_CGS |
SI | CGS | Mechanical dimensions (L, M, T) |
CGS_TO_SI |
CGS | SI | Inverse of SI_TO_CGS |
SI_TO_CGS_ESU |
SI | CGS_ESU | Current → L^(3/2)·M^(1/2)·T^(-2) |
SI_TO_CGS_EMU |
SI | CGS | Current → L^(1/2)·M^(1/2)·T^(-1) |
SI_TO_NATURAL |
SI | NATURAL | All dimensions → powers of energy |
NATURAL_TO_SI |
NATURAL | SI | Inverse with constant bindings |
BasisGraph¶
Track connectivity between bases:
from ucon.basis import BasisGraph
from ucon.basis.transforms import SI_TO_CGS_ESU
bg = BasisGraph()
bg = bg.with_transform(SI_TO_CGS_ESU)
bg.are_connected(SI, CGS_ESU) # True
BasisTransform¶
Matrix mapping dimension vectors between bases:
from ucon.basis.transforms import SI_TO_CGS_ESU
# Transform a dimension vector
si_current = units.ampere.dimension.vector
cgs_current = SI_TO_CGS_ESU(si_current)
# Result: L^(3/2) M^(1/2) T^(-2) in CGS-ESU
See Dual-Graph Architecture for details.
Basis Context Scoping¶
Basis and BasisGraph are scoped through the active UnitSystem. Use use()
with a derived system to override them:
from ucon import active_system, use, CGS, Dimension
# Override the active basis
cgs_system = active_system().with_basis(CGS)
with use(cgs_system):
dim = Dimension.from_components(L=1, T=-1)
dim.basis # CGS
from ucon import active_system, use
from ucon.basis import BasisGraph
# Override the active BasisGraph
custom_bg = BasisGraph()
custom_system = active_system().with_basis_graph(custom_bg)
with use(custom_system):
# All operations see custom_bg
pass
The active system's basis and basis graph are accessible via the UnitSystem
fields:
system = active_system()
system.basis # Current basis (SI by default)
system.basis_graph # Current BasisGraph
Natural Units¶
Support for natural units where physical constants c = h_bar = k_B = 1.
from ucon import NATURAL, SI_TO_NATURAL, NATURAL_TO_SI
from ucon.basis import ConstantBinding, ConstantBoundBasisTransform
NATURAL Basis¶
Single-dimensional basis where all quantities reduce to powers of energy:
from ucon import NATURAL, BasisVector
from fractions import Fraction
# NATURAL has one component: energy (E)
len(NATURAL) # 1
NATURAL[0].name # "energy"
NATURAL[0].symbol # "E"
# Create natural unit vectors
energy = BasisVector(NATURAL, (Fraction(1),)) # E^1
inverse_energy = BasisVector(NATURAL, (Fraction(-1),)) # E^-1
dimensionless = BasisVector(NATURAL, (Fraction(0),)) # E^0
Dimension Mappings¶
With c = h_bar = k_B = 1:
| SI Dimension | Natural Units | Physical Meaning |
|---|---|---|
| Length (L) | E^-1 | L = h_bar*c / E |
| Time (T) | E^-1 | T = h_bar / E |
| Mass (M) | E | E = mc^2 |
| Temperature (Theta) | E | E = k_B * T |
| Velocity (L/T) | E^0 | Dimensionless (c=1) |
| Action (ML^2/T) | E^0 | Dimensionless (h_bar=1) |
| Energy (ML^2/T^2) | E | Energy is fundamental |
SI_TO_NATURAL Transform¶
Transform SI dimensions to natural units:
from ucon import SI, NATURAL, SI_TO_NATURAL, BasisVector
from fractions import Fraction
# Velocity: L^1 T^-1 in SI
si_velocity = BasisVector(SI, (
Fraction(-1), # T^-1
Fraction(1), # L^1
Fraction(0), Fraction(0), Fraction(0),
Fraction(0), Fraction(0), Fraction(0),
))
natural_velocity = SI_TO_NATURAL(si_velocity)
natural_velocity.is_dimensionless() # True! (c = 1)
# Energy: M^1 L^2 T^-2 in SI
si_energy = BasisVector(SI, (
Fraction(-2), # T^-2
Fraction(2), # L^2
Fraction(1), # M^1
Fraction(0), Fraction(0),
Fraction(0), Fraction(0), Fraction(0),
))
natural_energy = SI_TO_NATURAL(si_energy)
natural_energy["E"] # Fraction(1) - pure energy
Lossy Projections¶
Electromagnetic dimensions (current, luminous intensity, etc.) are not representable in pure natural units:
from ucon import SI, SI_TO_NATURAL, BasisVector, LossyProjection
from fractions import Fraction
# Current has no representation in natural units
si_current = BasisVector(SI, (
Fraction(0), Fraction(0), Fraction(0), Fraction(1),
Fraction(0), Fraction(0), Fraction(0), Fraction(0),
))
# Raises LossyProjection
try:
SI_TO_NATURAL(si_current)
except LossyProjection as e:
print(e) # "current has no representation in natural"
# Use allow_projection=True to drop
result = SI_TO_NATURAL(si_current, allow_projection=True)
result.is_dimensionless() # True (projected to zero)
NATURAL_TO_SI (Inverse)¶
Transform natural unit dimensions back to SI using constant bindings:
from ucon import NATURAL, NATURAL_TO_SI, BasisVector
from fractions import Fraction
# Energy in natural units
natural_energy = BasisVector(NATURAL, (Fraction(1),))
# Transform back to SI
si_result = NATURAL_TO_SI(natural_energy)
si_result.basis.name # "SI"
ConstantBinding¶
Records the relationship between source and target dimensions via physical constants:
from ucon import ConstantBinding, NATURAL, BasisVector
from ucon.basis import BasisComponent
from fractions import Fraction
# Length relates to inverse energy via h_bar*c
binding = ConstantBinding(
source_component=BasisComponent("length", "L"),
target_expression=BasisVector(NATURAL, (Fraction(-1),)),
constant_symbol="h_bar_c", # h_bar * c
exponent=Fraction(1),
)
binding.constant_symbol # "h_bar_c"
binding.exponent # Fraction(1)
ConstantBoundBasisTransform¶
Basis transform with bindings that enable inversion of non-square matrices:
from ucon import ConstantBoundBasisTransform
# SI_TO_NATURAL is a ConstantBoundBasisTransform (8x1 matrix)
isinstance(SI_TO_NATURAL, ConstantBoundBasisTransform) # True
# Inverse works despite non-square matrix
NATURAL_TO_SI = SI_TO_NATURAL.inverse()
NATURAL_TO_SI.source.name # "natural"
NATURAL_TO_SI.target.name # "SI"
# Convert to plain BasisTransform (loses binding info)
plain = SI_TO_NATURAL.as_basis_transform()
UnitSystem (system= kwarg)¶
A UnitSystem is a frozen value type that bundles a Basis, the unit / dimension / conversion / basis-graph registries, a BaseUnits mapping, and a per-instance AlgebraCache.
Activation¶
Resolution Priority¶
Entry points that accept system= resolve their basis-graph and conversion-graph in this order:
explicit kwarg (graph=, basis_graph=, …)
> system.basis_graph / system.conversion_graph
> active() snapshot
> module-level default
Threaded Entry Points¶
The system= kwarg is accepted by:
| Entry point | Module |
|---|---|
Number.to(target, *, system=None, graph=None, ...) |
ucon.quantity |
parse(text, *, system=None, ...) |
ucon.parsing |
parse_unit(text, *, system=None, ...) |
ucon.parsing |
parse_dimension(text, *, system=None, ...) |
ucon.parsing |
enforce_dimensions(*, system=None) |
ucon.checking |
Each entry point falls back to active() (and then to module-level globals) when system= is omitted, preserving v1.7 call sites verbatim.
AlgebraCache¶
UnitSystem owns a per-instance AlgebraCache (sub-caches mul / div / pow). Dimension algebra routes through the active system's cache.
See ucon.system for the full reference and ucon.basis.ops for the explicit cross-basis helpers that the new value type composes with.
Particle Physics Applications¶
Common particle physics dimensions:
from ucon import SI, SI_TO_NATURAL, BasisVector
from fractions import Fraction
# Cross-section (area): L^2 -> E^-2
si_cross_section = BasisVector(SI, (
Fraction(0), Fraction(2), Fraction(0), Fraction(0),
Fraction(0), Fraction(0), Fraction(0), Fraction(0),
))
SI_TO_NATURAL(si_cross_section)["E"] # Fraction(-2)
# Decay width (inverse time): T^-1 -> E
si_decay_width = BasisVector(SI, (
Fraction(-1), Fraction(0), Fraction(0), Fraction(0),
Fraction(0), Fraction(0), Fraction(0), Fraction(0),
))
SI_TO_NATURAL(si_decay_width)["E"] # Fraction(1)
# Momentum: M*L/T -> E
si_momentum = BasisVector(SI, (
Fraction(-1), Fraction(1), Fraction(1), Fraction(0),
Fraction(0), Fraction(0), Fraction(0), Fraction(0),
))
SI_TO_NATURAL(si_momentum)["E"] # Fraction(1)
Kinds¶
A kind is a sortal refinement within a single dimension. Where
pseudo-dimensions and basis abstraction distinguish quantities by changing
their dimensional signature, a KindLattice records a partial order over
named kinds that all share the same dimension (e.g.,
kinetic_energy ≤ energy, equivalent_dose ≤ absorbed_dose).
Kind and KindLattice are re-exported from the top-level ucon package.
Number carries an optional kind field; arithmetic dispatch consults the
active KindLattice and FormulaRegistry.
See Kind-of-Quantity Problem for the
conceptual framing.
Kind¶
Frozen dataclass representing a single kind:
from ucon.dimension import LENGTH, MASS, TIME
from ucon.kinds import Kind, JoinPolicy
ENERGY_DIM = (LENGTH ** 2) * MASS / (TIME ** 2)
energy = Kind("energy", dimension=ENERGY_DIM)
kinetic = Kind(
name="kinetic_energy",
dimension=ENERGY_DIM,
parent=energy,
aliases=("KE",),
join_policy=JoinPolicy.LCA,
)
| Field | Type | Default | Description |
|---|---|---|---|
name |
str | required | Canonical name (must be unique within a lattice) |
dimension |
Dimension | required | Physical dimension carried by this kind |
parent |
Kind | None | None |
Parent in the refinement lattice; must share dimension |
aliases |
tuple[str, ...] | () |
Alternative names for lookup |
join_policy |
JoinPolicy | LCA |
What happens when two distinct sibling kinds combine |
Equality and hash key off name only — two Kind instances with the same
name are considered the same kind regardless of other fields.
JoinPolicy¶
Enum that controls operations between distinct sibling kinds:
| Member | Behavior |
|---|---|
JoinPolicy.LCA |
Result takes the least-common-ancestor kind |
JoinPolicy.REFUSE |
Operation is rejected at the kind layer |
LCA is the default and is appropriate for energies, doses, and other
refinements where the union is physically meaningful. REFUSE is used by
ratio and dimensionless, where mixing yields no sensible interpretation.
KindLattice¶
Container that validates structural invariants and provides LCA-based lookup:
from ucon.kinds import KindLattice
lat = KindLattice([energy, kinetic])
lat.get("kinetic_energy") # Kind('kinetic_energy')
lat.get("KE") # resolves via alias
"KE" in lat # True
lat.names() # ('energy', 'kinetic_energy')
lat.lca(kinetic, kinetic) # (kinetic, JoinPolicy.LCA)
Adding a Kind whose parent is unknown, whose parent has a different
dimension, or that creates a cycle is rejected when the lattice is
constructed (see Exceptions below).
Module-level lca()¶
ucon.kinds.lca(a, b, lattice) is a thin functional wrapper around
KindLattice.lca for code that prefers a free function.
Exceptions¶
from ucon.kinds import (
KindError, # base class
KindCycle, # parent edges form a cycle
OrphanParent, # parent name does not resolve
CrossDimensionParent,# parent has a different dimension
NameCollision, # two distinct Kinds share a name
AliasCollision, # an alias overlaps another kind's name/alias
KindNotFound, # lookup by name/alias missed
JoinRefused, # JoinPolicy.REFUSE triggered
)
| Exception | When raised |
|---|---|
KindCycle |
Adding a kind would create a cycle in parent edges |
OrphanParent |
A parent name does not resolve in the lattice |
CrossDimensionParent |
A child kind's dimension differs from its parent's |
NameCollision |
Two distinct Kind instances are registered under the same name |
AliasCollision |
An alias overlaps the name or alias of another kind |
KindNotFound |
lat.get(...) is called with an unknown name or alias |
JoinRefused |
An operation is refused by JoinPolicy.REFUSE |
Formulas¶
Kind-formula dispatch for multiplication and division. Formulas do kind work only; aspect propagation is handled by the aspect stratum's carry rule (see Aspects below).
A FormulaRegistry records named relationships between input kinds and an
output kind. Given a sequence of Kind instances, the registry resolves
the formula that produces the corresponding output.
KindFormula¶
Frozen dataclass describing a single relationship:
from ucon.kinds import Kind
from ucon.formulas import KindFormula
from ucon.dimension import LENGTH, MASS, TIME
ABSORBED_DOSE_DIM = (LENGTH ** 2) / (TIME ** 2)
DIMENSIONLESS = LENGTH / LENGTH
D = Kind("absorbed_dose", dimension=ABSORBED_DOSE_DIM)
wR = Kind("radiation_weighting_factor", dimension=DIMENSIONLESS)
H = Kind("equivalent_dose", dimension=ABSORBED_DOSE_DIM)
f = KindFormula(
name="radiation_weighting",
expression="D * w_R",
input_kinds={"D": D, "w_R": wR},
output_kind=H,
commutative=True,
notes="w_R per ICRP 103; caller selects.",
)
| Field | Type | Default | Description |
|---|---|---|---|
name |
str | required | Unique formula identifier within a registry |
expression |
str | required | Free-form expression string (e.g. "D * w_R") |
input_kinds |
dict[str, Kind] | required | Named inputs in declaration order |
output_kind |
Kind | required | Kind of the resulting quantity |
generalizes |
bool | False |
Reserved; effective in v1.9.2 |
commutative |
bool | True |
Two-input formulas are mirrored on registration |
notes |
str | "" |
Free-form annotation |
Equality and hash key off name only. input_kind_tuple() returns the
kinds in insertion order.
FormulaRegistry¶
Indexed collection of KindFormula instances supporting name lookup and
input-tuple lookup:
from ucon.formulas import FormulaRegistry
reg = FormulaRegistry([f])
reg.get("radiation_weighting") # the KindFormula
reg.lookup(D, wR).name # "radiation_weighting"
reg.lookup(wR, D).name # "radiation_weighting" (commutative)
"radiation_weighting" in reg # True
len(reg) # 1
reg.names() # ('radiation_weighting',)
| Method | Purpose |
|---|---|
register(formula) |
Add a formula; raises DuplicateFormula on name reuse |
get(name) |
Return the named formula or raise FormulaNotFound |
lookup(*kinds) |
Resolve a formula by exact input-kind tuple |
resolve(*kinds) |
Tiered formula resolution returning LookupResult (v1.9.2) |
apply(inputs) |
Resolve a formula from named kind bindings in one step |
names() |
Tuple of registered names |
Commutative formulas are indexed under both the original key and a canonical sorted key, so permuted orderings resolve at any arity.
resolve(*input_kinds, lattice=None, dimension_fallback=False)¶
New in v1.9.2. Tiered formula resolution through successive match tiers, checked in strict priority order — the first to produce a match wins:
- EXACT — direct input-kind tuple match.
- COMMUTATIVE — canonical sorted-key match (any arity).
- GENERALIZED — ancestor-walk via
latticeat increasing L1 distance (only formulas withgeneralizes=True). - DIMENSIONAL — dimension-tuple match ignoring kind identity
(only when
dimension_fallback=True).
from ucon.formulas import MatchKind
result = reg.resolve(D, wR)
result.formula.name # "radiation_weighting"
result.match_kind # MatchKind.EXACT
result.distance # 0
# Generalized: child kind climbs to parent
result = reg.resolve(child_kind, wR, lattice=lat)
result.match_kind # MatchKind.GENERALIZED
result.distance # 1 (L1 distance in ancestor walk)
| Parameter | Type | Description |
|---|---|---|
*input_kinds |
Kind | Kinds to match, in caller-supplied order |
lattice |
KindLattice | None | Enables GENERALIZED matching via ancestor walk |
dimension_fallback |
bool | Enables DIMENSIONAL matching as a last resort |
Returns LookupResult. Raises FormulaNotFound if no formula matched
at any enabled tier, or AmbiguousFormula if multiple formulas matched
at the same GENERALIZED distance.
apply(inputs, *, lattice=None, dimension_fallback=False)¶
Single entry point that resolves a formula from named kind bindings. As of v2.2.0 formulas do kind work only — aspect propagation is the aspect stratum's carry rule (see Aspects):
from ucon.formulas import MatchKind
formula, out_kind, match_kind = reg.apply({"D": D, "w_R": wR})
# formula == <KindFormula "radiation_weighting">
# out_kind == H (equivalent_dose)
# match_kind == MatchKind.EXACT
| Parameter | Type | Description |
|---|---|---|
inputs |
Mapping[str, Kind] | Map of binding names to kinds |
lattice |
KindLattice | None | Enables GENERALIZED matching via ancestor walk |
dimension_fallback |
bool | Enables DIMENSIONAL matching as a last resort |
Returns tuple[KindFormula, Kind, MatchKind]. Raises FormulaNotFound
if no formula matches the input kinds, or AmbiguousFormula if multiple
formulas match at the same GENERALIZED distance.
Types¶
New in v1.9.2.
| Type | Description |
|---|---|
MatchKind |
Enum: EXACT, COMMUTATIVE, GENERALIZED, DIMENSIONAL |
LookupResult |
Frozen dataclass with formula, match_kind, and distance (L1 distance for GENERALIZED; 0 for others) |
Exceptions¶
from ucon.formulas import (
FormulaError, # base class
FormulaNotFound, # name or kind-tuple did not resolve
DuplicateFormula, # name already registered
AmbiguousFormula, # multiple formulas match at same distance
)
| Exception | When raised |
|---|---|
FormulaNotFound |
get(name), lookup(*kinds), or resolve(*kinds) did not match |
DuplicateFormula |
A formula with the same name is already registered |
AmbiguousFormula |
Multiple formulas matched at the same GENERALIZED distance; carries candidates and distance |
Aspects¶
An aspect qualifies a quantity on terms its kind cannot express: two dose equivalents — same dimension, same unit, same kind — may still be weighted per different standards, and their sum conforms to neither. Aspects gate combination below the kind layer. (The flat string-set model that previously occupied this package was removed in v2.2.0; an unkeyed set cannot distinguish conflict from absence, which is the defect the aspect stratum exists to fix.)
from ucon import (
Aspect,
AspectError,
AspectRefused,
AspectNotApplicable,
)
from ucon.aspects import AspectForest, MultPolicy
Aspect¶
A frozen tree node, peer of Kind. The root of a tree is the family;
subtrees group related positions (standards bodies, procedures) under
it.
from ucon import Aspect
family = Aspect("weighting_standard",
applies_to=frozenset({"dose_equivalent"}))
icrp60 = Aspect("icrp60", parent=family)
icrp103 = Aspect("icrp103", parent=family)
| Field | Type | Default | Description |
|---|---|---|---|
name |
str | required | Unique aspect name |
parent |
Aspect | None | None |
Tree edge; None marks a family root |
join_policy |
JoinPolicy | REFUSE |
Consulted when this node is the LCA of a join (kinds default to LCA; aspects default to REFUSE) |
applies_to |
frozenset[str] | frozenset() |
Root-only. Kind names the family may attach to; empty = unrestricted, {"*"} = wildcard |
multiplication_policy |
MultPolicy | CARRY |
Root-only. How partial presence resolves under ×/÷ |
Number.aspects¶
Number carries an additive aspects: frozenset[Aspect] (default
empty). applies_to is enforced at construction — the aspect layer's
one sanctioned kind-read — so a false claim refuses at the moment it is
made:
from ucon import Number, units
dose = Number(2.0, units.gray, kind=dose_eq, aspects=[icrp103])
repr(dose) # '<2.0 Gy [dose_equivalent] #icrp103>'
Number(1.5, units.gray, kind=absorbed, aspects=[icrp103])
# AspectNotApplicable — weighting standards do not apply to absorbed dose
Arithmetic Resolution¶
Resolution is family-wise: both operands' aspects group by family root, and each family resolves independently. Cross-family comparison never happens.
| Configuration | + / − |
× / ÷ |
|---|---|---|
| Equal positions | carry | carry |
| Differing positions | join at LCA under the ancestor's join_policy (refuse raises AspectRefused) |
same |
| Partial (one side silent) | strict: AspectRefused; permissive: inherit + warning |
per the family's multiplication_policy (carry threads the present positions onto the product) |
There is no drop: no resolution path silently discards a position.
The carry rule survives kind degradation — a weighting standard rides
dose × time even when the product's kind degrades — which is what
keeps refusals alive through round trips:
a = Number(2.0, units.gray, kind=dose_eq, aspects=[icrp103])
b = Number(3.0, units.gray, kind=dose_eq, aspects=[icrp60])
a + b # AspectRefused — same kind, different standard
(a * t) / t # <2.0 Gy #icrp103> — carried and restored
Single-operand operations (.to(), scalar *//, **, simplify(),
to_base(), Ratio.evaluate(), UnitSystem.adopt(), Bridge.apply())
thread aspects unchanged.
The resolvers are importable as pure functions —
resolve_add_aspects(left, right, *, strict) and
resolve_mul_aspects(left, right) via ucon.aspects — reading nothing
but their operands and the strict bit.
AspectForest¶
Declaration-time container: validates a set of aspect trees (passing
leaves is enough — construction closes over parents), groups them by
family, and answers lca/join queries. Each family runs on a private,
mirrored kind-lattice engine, so join semantics are the same vetted code
kinds use.
from ucon.aspects import AspectForest
forest = AspectForest([icrp60, icrp103])
forest.families() # (weighting_standard,)
forest.join(icrp60, icrp103) # AspectRefused
Exceptions¶
| Exception | When raised |
|---|---|
AspectError |
Base class; structural failures (duplicate names, orphan parents, root-only violations) |
AspectRefused |
Two positions cannot be reconciled — carries family, left, right, policy; a None side marks the partial case |
AspectNotApplicable |
A restricted family attached to the wrong kind (or an unkinded Number) — carries family, kind |
No Kind-named exception ever surfaces from an aspect operation: engine
errors are rewrapped as AspectError with the original chained as
__cause__.
TOML¶
Aspects are authored in [[aspects]] sections and loaded with
parse_aspects_payload / load_aspects_file (via ucon.parsing); see
TOML Loaders below and the
Serialization Format reference. There are no
builtin aspects: core ships mechanism, domains ship vocabulary.
TOML Loaders for Kinds & Formulas¶
Kinds, formulas, and aspects can be authored in TOML and loaded into a
KindLattice, FormulaRegistry, and AspectForest. The loaders are
independent of the ConversionGraph TOML schema documented in
Serialization Format — they read the
[[kinds]], [[formulas]], and [[aspects]] sections from any TOML
file, and the sections can share a file.
from ucon.parsing import (
parse_kinds_payload,
load_kinds_file,
parse_formulas_payload,
load_formulas_file,
parse_aspects_payload,
load_aspects_file,
)
Schema: [[kinds]]¶
[[kinds]]
name = "energy"
dimension = "L^2 * M / T^2"
aliases = ["E"]
join_policy = "lca"
[[kinds]]
name = "kinetic_energy"
dimension = "L^2 * M / T^2"
parent = "energy"
aliases = ["KE"]
| Key | Type | Required | Description |
|---|---|---|---|
name |
string | yes | Canonical kind name |
dimension |
string | yes | Dimension expression (e.g. "L^2 * M / T^2", "1") |
parent |
string | no | Name of an existing kind (forward references resolved) |
aliases |
array of strings | no | Alternative names |
join_policy |
string | no | "lca" (default) or "refuse" |
Schema: [[formulas]]¶
[[formulas]]
name = "radiation_weighting"
expression = "D * w_R"
output_kind = "equivalent_dose"
commutative = true
notes = "w_R per ICRP 103."
[formulas.inputs]
D = { kind = "absorbed_dose" }
w_R = { kind = "radiation_weighting_factor" }
| Key | Type | Required | Description |
|---|---|---|---|
name |
string | yes | Formula name |
expression |
string | yes | Free-form expression text |
output_kind |
string | yes | Name of an existing kind |
inputs |
table | yes | Map of input names to { kind = "..." } |
commutative |
bool | no | Defaults to true |
generalizes |
bool | no | Defaults to false |
notes |
string | no | Free-form notes |
Loading¶
from pathlib import Path
from ucon.parsing import load_aspects_file, load_formulas_file, load_kinds_file
lat = load_kinds_file(Path("radiation.ucon.toml"))
reg = load_formulas_file(Path("radiation.ucon.toml"), lattice=lat)
forest = load_aspects_file(Path("radiation.ucon.toml"))
reg.get("radiation_weighting").output_kind.name # "equivalent_dose"
load_formulas_file requires a KindLattice because formula inputs and
outputs reference kinds by name. Unknown kind references raise
KindNotFound.
Payload-Level Helpers¶
parse_kinds_payload(payload) and parse_formulas_payload(payload, lattice=...)
accept already-decoded dictionaries and are useful for testing or for
embedding kind definitions inside a larger TOML document.
from ucon.parsing import parse_kinds_payload
lat = parse_kinds_payload({
"kinds": [
{"name": "energy", "dimension": "L^2 * M / T^2"},
{"name": "kinetic_energy", "dimension": "L^2 * M / T^2", "parent": "energy"},
]
})
lat.get("kinetic_energy").parent.name # "energy"
Parser-level validation surfaces the same exceptions as the lattice itself
(KindCycle, OrphanParent, CrossDimensionParent, AliasCollision) plus
ValueError for malformed payloads (missing name, unknown join_policy,
non-string aliases, etc.).