core/generate/combined_rate_comprehension/model.py
Shay 2a7e94c011 feat(combined-rate): CMB-b — exact combined-rate solver (int-or-refuse)
solve_combined_rate(CombinedRateProblem) -> int | Refusal over effective_rate = rate_a +/- rate_b.
effective_rate query returns the net (even <=0); quantity/time queries need a positive net rate
(non_positive_net_rate) and exact division (non_integer_solution). Pure integer arithmetic, no
float/Fraction (CMB v1 crosses no units). Off-serving. Graded by a new run_solver lane in
evals/combined_rate_oracle against the committed gold.

Adversarial 5-lens(+adjudicator) verification returned fix_first; both real hazards fixed:
- wrong=0 breach: negative known-slot inputs (time<0/quantity<0) bypassed the eff<=0 guard and
  produced negative answers. FIXED UPSTREAM in model.py __post_init__ — rate_a/rate_b and the known
  time/quantity are now positive ints, so the illegal state is unrepresentable and the solver can
  never receive a negative-yielding path. Added model + gold (cmb-07d eff<0/time) + lane coverage.
- doc overclaim: the solver lane does NOT grade 'two independent paths' (both solver and the
  oracle's _canonical_outcome delegate to model.effective_rate). Corrected both docstrings to name
  the true anchor (committed gold + inline-computed literal tests) and added a difference-mode
  inline-computed test.

R3-vac came back SOLID (separate PR). gold 18/18 (6/5/7); solver lane 6/0 + 5/0; 36 CMB tests;
router-hygiene + serving unchanged.
2026-06-08 07:59:45 -07:00

108 lines
5.1 KiB
Python
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

"""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).
The two rates and the known time/quantity are **positive ints** — a non-positive rate or a
non-positive duration/quantity is nonsensical and cannot be represented (so the solver can never
receive a path that yields a negative answer). The *net* rate, by contrast, MAY be ``<= 0``:
``effective_rate`` is derived (a property), and for ``difference`` mode with ``rate_a <= rate_b``
it is ``<= 0``. 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) or value <= 0:
raise ValueError(f"{role} must be a positive 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) or value <= 0:
raise ValueError(f"{role} value must be a positive 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"]