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.
108 lines
5.1 KiB
Python
108 lines
5.1 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).
|
||
|
||
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"]
|