First rung of the combined-rate ladder, off-serving (imports no generate.derivation / core.reliability_gate). Claims exactly: the combined-rate setup ruler is defined and gold-valid. No reader/solver/wiring yet — no capability claim. - generate/combined_rate_comprehension/: local RateUnit (decoupled from R3), CombinedRateProblem (two-explicit-rates + per-query-slot guard; effective_rate = rate_a +/- rate_b; non-positive net left to the solver, not the model). - evals/combined_rate_oracle/: span-free signature (sum commutative / difference ordered) + a NON-VACUOUS validator. 17 gold fixtures: 6 solved (full combine_mode x query grid), 4 solver_refuses, 7 reader_refuses (the complete refusal taxonomy + the 2x2 domain-entry grid). Adversarial 5-lens verification returned fix_first; the validator now cross-checks every solved gold and solver_refuses reason against the canonical arithmetic (_canonical_outcome) so a mislabelled / arithmetically-impossible fixture is rejected (meaningful-fail per CLAUDE.md), with dedicated tests. Added eff<0 and eff=0/time coverage fixtures; removed a dead determinism guard. gold 17/17 valid; 25 oracle tests; R1/R2/R3 + router-hygiene + serving all unchanged. Doc: docs/analysis/cmb-a-combined-rate-ruler-2026-06-08.md
105 lines
4.8 KiB
Python
105 lines
4.8 KiB
Python
"""Typed model for a combined-rate problem (CMB-a).
|
||
|
||
A combined-rate problem is two explicit rates over one shared unit, combined by an explicit mode,
|
||
then single-rate algebra over the result:
|
||
|
||
```text
|
||
effective_rate = rate_a + rate_b (combine_mode == "sum")
|
||
effective_rate = rate_a - rate_b (combine_mode == "difference")
|
||
quantity = effective_rate × time (query == "quantity")
|
||
time = quantity ÷ effective_rate (query == "time")
|
||
effective_rate (query == "effective_rate")
|
||
```
|
||
|
||
The two rates are **always known** (that is what makes it a combined-rate problem — two explicit
|
||
rates); ``rate_unit`` is the single source of unit truth for both. The query selects which derived
|
||
slot is asked:
|
||
|
||
- ``quantity`` — ``time`` is the known, ``quantity`` is the unknown (``None``).
|
||
- ``time`` — ``quantity`` is the known, ``time`` is the unknown (``None``).
|
||
- ``effective_rate`` — neither ``time`` nor ``quantity`` is needed; both are ``None``.
|
||
|
||
Pure data with a structural guard: the two rates must be present, and exactly the slots the query
|
||
licenses are known/unknown (illegal states — a missing rate, the wrong slot unknown, an
|
||
over-specified ``effective_rate`` query — cannot be represented).
|
||
|
||
``effective_rate`` is derived (a property), and for ``difference`` mode it MAY be ``<= 0``
|
||
(``rate_a <= rate_b``). The model does NOT refuse that — a non-positive net rate is the *solver's*
|
||
boundary (``non_positive_net_rate``, CMB-b), not a malformed setup. Off-serving; deterministic.
|
||
No unit conversion in v1 (``time_unit`` defaults to the rate denominator and the v1 gold never
|
||
crosses units).
|
||
"""
|
||
|
||
from __future__ import annotations
|
||
|
||
from dataclasses import dataclass
|
||
from typing import Literal
|
||
|
||
from generate.combined_rate_comprehension.units import RateUnit
|
||
|
||
CombineMode = Literal["sum", "difference"]
|
||
CombinedRateQuery = Literal["quantity", "time", "effective_rate"]
|
||
|
||
#: The slots each query licenses as (known, unknown). ``effective_rate`` needs neither time nor
|
||
#: quantity; the other two trade exactly one known for one unknown.
|
||
_QUERY_SLOTS: dict[str, tuple[frozenset[str], frozenset[str]]] = {
|
||
"quantity": (frozenset({"time"}), frozenset({"quantity"})),
|
||
"time": (frozenset({"quantity"}), frozenset({"time"})),
|
||
"effective_rate": (frozenset(), frozenset({"time", "quantity"})),
|
||
}
|
||
|
||
|
||
@dataclass(frozen=True, slots=True)
|
||
class CombinedRateProblem:
|
||
"""Two explicit rates over one unit, combined by ``combine_mode``, with one queried slot."""
|
||
|
||
rate_a: int
|
||
rate_b: int
|
||
rate_unit: RateUnit
|
||
combine_mode: CombineMode
|
||
time: int | None
|
||
quantity: int | None
|
||
query: CombinedRateQuery
|
||
#: The duration's time unit (forward-compat with conversion). Defaults to the rate denominator;
|
||
#: v1 never crosses units, so it always equals ``rate_unit.denominator`` here.
|
||
time_unit: str | None = None
|
||
|
||
def __post_init__(self) -> None:
|
||
if self.time_unit is None:
|
||
object.__setattr__(self, "time_unit", self.rate_unit.denominator)
|
||
for role, value in (("rate_a", self.rate_a), ("rate_b", self.rate_b)):
|
||
if not isinstance(value, int) or isinstance(value, bool):
|
||
raise ValueError(f"{role} must be a known int (two explicit rates); got {value!r}")
|
||
if self.combine_mode not in ("sum", "difference"):
|
||
raise ValueError(f"combine_mode must be 'sum' or 'difference'; got {self.combine_mode!r}")
|
||
if self.query not in _QUERY_SLOTS:
|
||
raise ValueError(f"query must be one of {sorted(_QUERY_SLOTS)}; got {self.query!r}")
|
||
known_slots, unknown_slots = _QUERY_SLOTS[self.query]
|
||
slots: dict[str, int | None] = {"time": self.time, "quantity": self.quantity}
|
||
known = {role for role, value in slots.items() if value is not None}
|
||
if known != known_slots:
|
||
raise ValueError(
|
||
f"query={self.query!r} licenses knowns {sorted(known_slots)}; got {sorted(known)}"
|
||
)
|
||
for role in unknown_slots:
|
||
if slots[role] is not None:
|
||
raise ValueError(f"query={self.query!r}: slot {role!r} must be the unknown (None)")
|
||
for role in known_slots:
|
||
value = slots[role]
|
||
if not isinstance(value, int) or isinstance(value, bool):
|
||
raise ValueError(f"{role} value must be int; got {value!r}")
|
||
|
||
@property
|
||
def effective_rate(self) -> int:
|
||
"""The combined rate (pure derivation). MAY be ``<= 0`` for ``difference``; the solver,
|
||
not the model, owns the ``non_positive_net_rate`` refusal."""
|
||
if self.combine_mode == "sum":
|
||
return self.rate_a + self.rate_b
|
||
return self.rate_a - self.rate_b
|
||
|
||
@property
|
||
def quantity_unit(self) -> str:
|
||
return self.rate_unit.numerator
|
||
|
||
|
||
__all__ = ["CombineMode", "CombinedRateProblem", "CombinedRateQuery"]
|