core/language_packs/unit_dimensions.py
Shay 58a94c8e4b
feat: implement kernel substrate tranche 1 base-layer foundations (#829)
* feat: implement kernel substrate tranche 1 base-layer foundations

* fix(kernel): ground scalar spans and tighten morphology labels

Add extract_scalar_candidates() with exact source spans and problem_text
provenance while keeping canonicalize_scalar() as the detached pack helper.
Morphology labels now emit missing_* only when substrate frame/unit lookups
actually fail, not on mere trigger-surface presence.
2026-06-18 18:46:20 -07:00

166 lines
5 KiB
Python

"""Unit and dimension facade — exact unit/dimension facts from ADR-0127.
Tranche 1 — broad base-layer foundations.
Facade over ``loader.py`` (en_units_v1) exposing dimension compatibility
checks, exact conversions, and provenance-tagged unit facts.
Non-goals:
- Fuzzy month/year conversions
- Unsupported world conversions
- Unit guessing
- Solving a problem by unit conversion alone
"""
from __future__ import annotations
from dataclasses import dataclass
from fractions import Fraction
from language_packs.loader import (
lookup_dimension,
lookup_unit,
get_conversion_graph,
)
# ---------------------------------------------------------------------------
# Rate dimension component mappings.
# ---------------------------------------------------------------------------
_RATE_COMPONENTS: dict[str, tuple[str, str]] = {
"wage": ("money", "time"),
"speed": ("length", "time"),
"unit_price": ("money", "count"),
"frequency": ("count", "time"),
"density": ("mass", "volume"),
"items_per_container": ("count", "container"),
}
# ---------------------------------------------------------------------------
# Dataclasses — frozen, slots=True.
# ---------------------------------------------------------------------------
@dataclass(frozen=True, slots=True)
class DimensionFact:
"""A classified dimension fact for a unit token."""
surface: str
dimension: str
singular: str
plural: str
is_derived: bool
provenance_kind: str # always "kernel_unit"
@dataclass(frozen=True, slots=True)
class ConversionFact:
"""An exact conversion fact between two compatible units."""
from_unit: str
to_unit: str
ratio: Fraction
dimension: str
provenance_kind: str # always "kernel_unit"
# ---------------------------------------------------------------------------
# Public API
# ---------------------------------------------------------------------------
def classify_dimension(token: str) -> DimensionFact | None:
"""Classify a token's dimension via the en_units_v1 pack.
Looks up the token as a unit and maps it to a DimensionFact.
Returns None if unsupported or not found in the pack.
"""
if not token:
return None
unit = lookup_unit(token)
if unit is None:
return None
dim = lookup_dimension(unit.dimension)
if dim is None:
return None
return DimensionFact(
surface=token,
dimension=unit.dimension,
singular=unit.singular,
plural=unit.plural,
is_derived=dim.is_derived,
provenance_kind="kernel_unit",
)
def are_dimensions_compatible(dim_a: str, dim_b: str) -> bool:
"""Return True if two dimensions are compatible (same base dimension)."""
dim_a_clean = dim_a.strip().lower()
dim_b_clean = dim_b.strip().lower()
if dim_a_clean == dim_b_clean:
return True
# Check if both are rate dimensions with compatible components.
if dim_a_clean in _RATE_COMPONENTS and dim_b_clean in _RATE_COMPONENTS:
num_a, den_a = _RATE_COMPONENTS[dim_a_clean]
num_b, den_b = _RATE_COMPONENTS[dim_b_clean]
return (
are_dimensions_compatible(num_a, num_b)
and are_dimensions_compatible(den_a, den_b)
)
return False
def exact_conversion(from_unit: str, to_unit: str) -> ConversionFact | None:
"""Return an exact conversion between two units if one exists in the pack.
Does not perform multi-hop conversions (returns None if no direct edge exists).
Ratios are converted to exact Fraction values.
"""
if not from_unit or not to_unit:
return None
u_from = lookup_unit(from_unit)
u_to = lookup_unit(to_unit)
if u_from is None or u_to is None:
return None
if u_from.dimension != u_to.dimension:
return None
graph = get_conversion_graph(u_from.dimension)
if not graph or not graph.edges:
return None
from_singular = u_from.singular.lower()
to_singular = u_to.singular.lower()
# Look for direct edge in the conversion graph.
for edge in graph.edges:
if edge.from_unit.lower() == from_singular and edge.to_unit.lower() == to_singular:
exact_ratio = Fraction(edge.ratio).limit_denominator(1000000)
return ConversionFact(
from_unit=u_from.singular,
to_unit=u_to.singular,
ratio=exact_ratio,
dimension=u_from.dimension,
provenance_kind="kernel_unit",
)
return None
def classify_rate_dimension(numerator_dim: str, denominator_dim: str) -> str | None:
"""Classify a rate dimension from its components."""
num = numerator_dim.strip().lower()
den = denominator_dim.strip().lower()
for rate_dim, (n, d) in _RATE_COMPONENTS.items():
if n == num and d == den:
return rate_dim
return None
def supported_dimension_families() -> tuple[str, ...]:
"""Return all supported dimension family names, sorted."""
return ("count", "length", "money", "time")