feat(rate): R3 compound-unit primitive — the dimensional substrate (R3a)
generate/rate_comprehension/units.py: BaseUnit + RateUnit (numerator/denominator) + the three single-rate compositions — rate_times_time (mile/hour × hour = mile; refuses mile/hour × minute), rate_from_quantity_over_time (mile ÷ hour = mile/hour), time_from_quantity_over_rate (mile ÷ mile/hour = hour; refuses dollar ÷ mile/hour). A non-composing op raises UnitError (the wrong=0 dimensional gate), never fabricates a unit. Deliberately tiny: bare-name units, no generic dimensional universe, no semantic unit-class typing, no unit conversion, no compound-of-compound. Fresh off-serving organ (generate/rate_comprehension/). 10 tests incl. all three refusals + consistent composition.
This commit is contained in:
parent
a00e87b297
commit
d9ebec7b70
3 changed files with 197 additions and 0 deletions
31
generate/rate_comprehension/__init__.py
Normal file
31
generate/rate_comprehension/__init__.py
Normal file
|
|
@ -0,0 +1,31 @@
|
|||
"""R3 single-rate comprehension organ (off-serving).
|
||||
|
||||
A fresh, isolated organ for the R3 capability family: explicit single-rate integer problems with
|
||||
exact compound-unit composition — ``quantity = rate × time`` and its two inverses. Its new
|
||||
substrate is the compound-unit algebra (``units``); the gold/oracle/solver/reader build on it the
|
||||
same disciplined way R2 did. Disjoint from the GSM8K serving path (imports no
|
||||
``generate.derivation`` / ``core.reliability_gate``), so it cannot regress the serving metric.
|
||||
|
||||
R3 v1 supports ONLY single-rate problems — no combined rates, no temporal state, no unit
|
||||
conversion, no multi-equation systems. Those are later R3 slices.
|
||||
"""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
from generate.rate_comprehension.units import (
|
||||
BaseUnit,
|
||||
RateUnit,
|
||||
UnitError,
|
||||
rate_from_quantity_over_time,
|
||||
rate_times_time,
|
||||
time_from_quantity_over_rate,
|
||||
)
|
||||
|
||||
__all__ = [
|
||||
"BaseUnit",
|
||||
"RateUnit",
|
||||
"UnitError",
|
||||
"rate_from_quantity_over_time",
|
||||
"rate_times_time",
|
||||
"time_from_quantity_over_rate",
|
||||
]
|
||||
90
generate/rate_comprehension/units.py
Normal file
90
generate/rate_comprehension/units.py
Normal file
|
|
@ -0,0 +1,90 @@
|
|||
"""Compound-unit algebra for the R3 single-rate organ (R3a).
|
||||
|
||||
The genuinely new substrate of R3: a **rate carries a compound unit** — ``mile/hour`` is
|
||||
``quantity / time`` — and the three single-rate operations must verify that units *compose*:
|
||||
|
||||
```text
|
||||
rate × time -> quantity (mile/hour × hour = mile) [time must be the rate's denominator]
|
||||
quantity ÷ time -> rate (mile ÷ hour = mile/hour)
|
||||
quantity ÷ rate -> time (mile ÷ mile/hour = hour) [quantity must be the rate's numerator]
|
||||
```
|
||||
|
||||
A non-composing operation REFUSES (``UnitError``) rather than producing a nonsense unit —
|
||||
``mile/hour × minute``, ``students/hour × mile``, ``dollar ÷ mile/hour``. This is the wrong=0
|
||||
boundary for R3: the dimensional check refuses before any arithmetic runs.
|
||||
|
||||
Deliberately tiny. Units are bare names; there is **no** generic dimensional-analysis universe
|
||||
and **no** semantic unit-class typing (v1 cannot itself know "dollar is not a time" — the gold and
|
||||
reader only admit sensible single-rate problems). Compound-of-compound and rate/rate are simply
|
||||
not representable (``RateUnit`` is one numerator over one denominator). No unit conversion (v1
|
||||
never turns minutes into hours). Deterministic; no clock, no randomness.
|
||||
"""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
from dataclasses import dataclass
|
||||
|
||||
|
||||
class UnitError(ValueError):
|
||||
"""A non-composing unit operation — refuse, never fabricate a unit."""
|
||||
|
||||
|
||||
@dataclass(frozen=True, slots=True)
|
||||
class BaseUnit:
|
||||
"""A simple unit of quantity or time — ``mile``, ``hour``, ``dollar``, ``widget``."""
|
||||
|
||||
name: str
|
||||
|
||||
def __post_init__(self) -> None:
|
||||
if not self.name or not isinstance(self.name, str):
|
||||
raise UnitError(f"BaseUnit needs a non-empty name; got {self.name!r}")
|
||||
|
||||
|
||||
@dataclass(frozen=True, slots=True)
|
||||
class RateUnit:
|
||||
"""A compound rate unit ``numerator / denominator`` — ``mile/hour``, ``dollar/hour``."""
|
||||
|
||||
numerator: str
|
||||
denominator: str
|
||||
|
||||
def __post_init__(self) -> None:
|
||||
if not self.numerator or not self.denominator:
|
||||
raise UnitError(f"RateUnit needs non-empty units; got {self!r}")
|
||||
if self.numerator == self.denominator:
|
||||
raise UnitError(f"a rate's numerator and denominator must differ; got {self}")
|
||||
|
||||
def __str__(self) -> str:
|
||||
return f"{self.numerator}/{self.denominator}"
|
||||
|
||||
|
||||
def rate_times_time(rate: RateUnit, time: BaseUnit) -> BaseUnit:
|
||||
"""``rate × time -> quantity`` (``mile/hour × hour = mile``). The time unit MUST be the rate's
|
||||
denominator, else REFUSE (``mile/hour × minute``)."""
|
||||
if time.name != rate.denominator:
|
||||
raise UnitError(f"{rate} × {time.name}: the time unit must be {rate.denominator!r}")
|
||||
return BaseUnit(rate.numerator)
|
||||
|
||||
|
||||
def rate_from_quantity_over_time(quantity: BaseUnit, time: BaseUnit) -> RateUnit:
|
||||
"""``quantity ÷ time -> rate`` (``mile ÷ hour = mile/hour``)."""
|
||||
if quantity.name == time.name:
|
||||
raise UnitError(f"{quantity.name} ÷ {time.name}: quantity and time units must differ")
|
||||
return RateUnit(quantity.name, time.name)
|
||||
|
||||
|
||||
def time_from_quantity_over_rate(quantity: BaseUnit, rate: RateUnit) -> BaseUnit:
|
||||
"""``quantity ÷ rate -> time`` (``mile ÷ mile/hour = hour``). The quantity unit MUST be the
|
||||
rate's numerator, else REFUSE (``dollar ÷ mile/hour``)."""
|
||||
if quantity.name != rate.numerator:
|
||||
raise UnitError(f"{quantity.name} ÷ {rate}: the quantity unit must be {rate.numerator!r}")
|
||||
return BaseUnit(rate.denominator)
|
||||
|
||||
|
||||
__all__ = [
|
||||
"BaseUnit",
|
||||
"RateUnit",
|
||||
"UnitError",
|
||||
"rate_from_quantity_over_time",
|
||||
"rate_times_time",
|
||||
"time_from_quantity_over_rate",
|
||||
]
|
||||
76
tests/test_rate_units.py
Normal file
76
tests/test_rate_units.py
Normal file
|
|
@ -0,0 +1,76 @@
|
|||
"""Tests for the R3 compound-unit algebra (R3a).
|
||||
|
||||
Pins the three single-rate compositions and — load-bearing — that a non-composing operation
|
||||
REFUSES (``UnitError``) rather than fabricating a unit. The dimensional check is the wrong=0 gate.
|
||||
"""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import dataclasses
|
||||
|
||||
import pytest
|
||||
|
||||
from generate.rate_comprehension.units import (
|
||||
BaseUnit,
|
||||
RateUnit,
|
||||
UnitError,
|
||||
rate_from_quantity_over_time,
|
||||
rate_times_time,
|
||||
time_from_quantity_over_rate,
|
||||
)
|
||||
|
||||
_MPH = RateUnit("mile", "hour")
|
||||
|
||||
|
||||
def test_rate_times_time_composes() -> None:
|
||||
assert rate_times_time(_MPH, BaseUnit("hour")) == BaseUnit("mile")
|
||||
|
||||
|
||||
def test_rate_times_wrong_time_refuses() -> None:
|
||||
# mile/hour × minute — the time unit is not the rate's denominator.
|
||||
with pytest.raises(UnitError):
|
||||
rate_times_time(_MPH, BaseUnit("minute"))
|
||||
# students/hour × mile.
|
||||
with pytest.raises(UnitError):
|
||||
rate_times_time(RateUnit("student", "hour"), BaseUnit("mile"))
|
||||
|
||||
|
||||
def test_quantity_over_time_makes_rate() -> None:
|
||||
assert rate_from_quantity_over_time(BaseUnit("mile"), BaseUnit("hour")) == _MPH
|
||||
|
||||
|
||||
def test_quantity_over_same_unit_refuses() -> None:
|
||||
with pytest.raises(UnitError):
|
||||
rate_from_quantity_over_time(BaseUnit("mile"), BaseUnit("mile"))
|
||||
|
||||
|
||||
def test_quantity_over_rate_makes_time() -> None:
|
||||
assert time_from_quantity_over_rate(BaseUnit("mile"), _MPH) == BaseUnit("hour")
|
||||
|
||||
|
||||
def test_quantity_over_rate_wrong_numerator_refuses() -> None:
|
||||
# dollar ÷ mile/hour — the quantity unit is not the rate's numerator.
|
||||
with pytest.raises(UnitError):
|
||||
time_from_quantity_over_rate(BaseUnit("dollar"), _MPH)
|
||||
|
||||
|
||||
def test_rate_unit_numerator_equals_denominator_refuses() -> None:
|
||||
with pytest.raises(UnitError):
|
||||
RateUnit("mile", "mile")
|
||||
|
||||
|
||||
def test_three_operations_compose_consistently() -> None:
|
||||
# mile/hour × hour = mile; mile ÷ hour = mile/hour; mile ÷ mile/hour = hour.
|
||||
quantity = rate_times_time(_MPH, BaseUnit("hour"))
|
||||
assert rate_from_quantity_over_time(quantity, BaseUnit("hour")) == _MPH
|
||||
assert time_from_quantity_over_rate(quantity, _MPH) == BaseUnit("hour")
|
||||
|
||||
|
||||
def test_units_are_frozen() -> None:
|
||||
with pytest.raises(dataclasses.FrozenInstanceError):
|
||||
_MPH.numerator = "kilometer" # type: ignore[misc]
|
||||
|
||||
|
||||
def test_empty_unit_name_refuses() -> None:
|
||||
with pytest.raises(UnitError):
|
||||
BaseUnit("")
|
||||
Loading…
Reference in a new issue