Skip to content

Design Principles

Core philosophy and architectural tradeoffs behind ucon.


Correctness Over Performance

ucon prioritizes correctness over raw speed. Every operation validates dimensional consistency before executing.

# Dimension check happens before arithmetic
length + time  # raises immediately, not after computation

Tradeoff: This adds overhead to every operation. For hot paths with millions of conversions, consider batch operations (planned for v0.10.x) or extracting raw values after validation.

Rationale: Unit errors in scientific and engineering contexts can have severe consequences. A 10x slower library that catches errors is more valuable than a fast library that silently propagates mistakes.


Injectable Over Global

ucon avoids global mutable state. The ConversionGraph is injectable via context managers rather than a singleton.

from ucon.graph import using_graph, ConversionGraph

# Default graph used implicitly
distance.to(units.mile)

# Custom graph injected explicitly
custom = ConversionGraph()
custom.add_edge(src=my_unit, dst=units.meter, map=LinearMap(1.5))

with using_graph(custom):
    # All operations use custom graph
    value.to(my_unit)

Implementation: ContextVar provides thread-safe, async-safe scoping:

_graph_context: ContextVar[ConversionGraph | None] = ContextVar("graph", default=None)
_parsing_graph: ContextVar[ConversionGraph | None] = ContextVar("parsing_graph", default=None)

Tradeoff: Slightly more verbose than a global registry. Users must pass graphs explicitly or use context managers.

Rationale: - Libraries embedding ucon can maintain isolated graphs - Tests run in parallel without interference - No hidden action-at-a-distance from global state mutation


Explicit Dimensional Algebra

Dimensions are algebraic objects, not string labels. Operations produce new dimensions following physical laws.

Dimension.length / Dimension.time      # → Dimension.velocity
Dimension.mass * Dimension.acceleration  # → Dimension.force
Dimension.energy ** 0.5                # → derived dimension

Implementation: Each dimension has a Vector of 8 exponents (T, L, M, I, Θ, J, N, B). Arithmetic operates on vectors:

class Dimension(Enum):
    length = Vector(0, 1, 0, 0, 0, 0, 0, 0)
    time = Vector(1, 0, 0, 0, 0, 0, 0, 0)
    velocity = Vector(-1, 1, 0, 0, 0, 0, 0, 0)  # T⁻¹L¹

Rationale: This enables ucon to verify that length / time produces something compatible with velocity, catching errors that string-based systems miss.


Parse, Don't Validate

Unit strings are parsed into structured types immediately, not validated later during computation.

# Parsing happens once, at the boundary
unit = get_unit_by_name("kg*m/s^2")  # Returns UnitProduct

# All subsequent operations work with typed objects
number = unit(10)
result = number.to(units.newton)

Tradeoff: Parsing errors surface early, which may feel strict. But this prevents silent failures deep in computation chains.

Rationale: Borrowed from Parse, don't validate. Structured types carry invariants that eliminate whole classes of bugs.


Composable Unit Systems

Custom unit systems compose without modifying the core library.

from ucon.packages import load_package

# Load domain-specific units from TOML
aerospace = load_package("aerospace.ucon.toml")
medical = load_package("medical.ucon.toml")

# Compose into a new graph
graph = get_default_graph().with_package(aerospace).with_package(medical)

Implementation: ConversionGraph.with_package() returns a new graph, leaving the original unchanged:

def with_package(self, package: UnitPackage) -> ConversionGraph:
    new = self.copy()
    for unit_def in package.units:
        new.register_unit(unit_def.materialize())
    for edge_def in package.edges:
        edge_def.materialize(new)
    return new

Rationale: - Domain experts define units in config files, not Python code - Composition is explicit and traceable - No global mutation means packages can't conflict


Pseudo-Dimension Isolation

Angles, ratios, solid angles, and counts share a zero-dimensional vector but are semantically distinct. ucon prevents cross-family conversions.

# These all have dimension vector = (0,0,0,0,0,0,0,0)
units.radian(1)    # angle
units.percent(50)  # ratio
units.steradian(1) # solid_angle
units.each(10)     # count

# But cross-conversion is blocked
units.radian(1).to(units.percent)  # raises ConversionNotFound

Implementation: Pseudo-dimensions use tuple values to prevent Python's Enum from aliasing them:

class Dimension(Enum):
    none = Vector()
    angle = (Vector(), "angle")       # tuple prevents aliasing
    ratio = (Vector(), "ratio")
    solid_angle = (Vector(), "solid_angle")
    count = (Vector(), "count")

Rationale: Mathematically, these are all dimensionless. But converting radians to percent is nonsensical. Semantic isolation catches this class of error.


Map Composition

Conversion functions are first-class objects that compose via @ (matrix-style composition).

from ucon.maps import LinearMap, AffineMap, LogMap

# Linear: y = a * x
meter_to_foot = LinearMap(3.28084)

# Affine: y = a * x + b (for temperature)
celsius_to_kelvin = AffineMap(1, 273.15)

# Logarithmic: y = scale * log_base(x) + offset
fraction_to_nines = LogMap(scale=-1) @ AffineMap(a=-1, b=1)

# Composition
path = meter_to_foot @ foot_to_inch  # meter → inch

Implementation: Each Map implements __call__, inverse(), and __matmul__:

class LinearMap:
    def __call__(self, x): return self.a * x
    def inverse(self): return LinearMap(1 / self.a)
    def __matmul__(self, other): return ComposedMap(self, other)

Rationale: Conversion paths are composed at graph-traversal time, not hardcoded. This enables: - Multi-hop conversions (meter → foot → inch) - Uncertainty propagation via derivative() - Nonlinear conversions (temperature, logarithmic scales)


Structured Errors for AI Agents

MCP tools return structured ConversionError objects with likely_fix for mechanical corrections and hints for exploratory suggestions.

ConversionError(
    error="Unknown unit: 'kilgoram'",
    error_type="unknown_unit",
    likely_fix="kilogram (kg)",  # High confidence → apply directly
    hints=["Similar: gram (g)"],  # Lower confidence → reason about
)

Implementation: Fuzzy matching with confidence tiers:

def _suggest_units(bad_name: str) -> tuple[str | None, list[str]]:
    matches = get_close_matches(bad_name, corpus, cutoff=0.6)
    top_score = similarity(bad_name, matches[0])

    # High confidence (≥0.7) with clear gap → likely_fix
    if top_score >= 0.7 and gap_to_second >= 0.1:
        return format_unit(matches[0]), []

    # Otherwise → hints only
    return None, [format_unit(m) for m in matches]

Rationale: AI agents can self-correct typos without escalating to users, while ambiguous cases require human judgment.


Summary

Principle Tradeoff Benefit
Correctness over performance Slower operations Catches errors early
Injectable over global More verbose Thread-safe, testable
Explicit dimensional algebra Learning curve Physics-correct operations
Parse, don't validate Strict boundaries No silent failures
Composable unit systems Config files needed Domain isolation
Pseudo-dimension isolation Can't convert rad→% Semantic correctness
Map composition More abstractions Flexible conversions
Structured errors More response fields AI self-correction