Merge pull request #642 from AssetOverflow/feat/r3-c
feat(rate): R3c — exact integer single-rate solver [merge after b]
This commit is contained in:
commit
54c9f60442
3 changed files with 111 additions and 0 deletions
|
|
@ -13,6 +13,7 @@ conversion, no multi-equation systems. Those are later R3 slices.
|
|||
from __future__ import annotations
|
||||
|
||||
from generate.rate_comprehension.model import RateProblem, RateRole
|
||||
from generate.rate_comprehension.solver import answer_unit, solve_rate
|
||||
from generate.rate_comprehension.units import (
|
||||
BaseUnit,
|
||||
RateUnit,
|
||||
|
|
@ -28,7 +29,9 @@ __all__ = [
|
|||
"RateRole",
|
||||
"RateUnit",
|
||||
"UnitError",
|
||||
"answer_unit",
|
||||
"rate_from_quantity_over_time",
|
||||
"rate_times_time",
|
||||
"solve_rate",
|
||||
"time_from_quantity_over_rate",
|
||||
]
|
||||
|
|
|
|||
65
generate/rate_comprehension/solver.py
Normal file
65
generate/rate_comprehension/solver.py
Normal file
|
|
@ -0,0 +1,65 @@
|
|||
"""Exact integer single-rate solver (R3c).
|
||||
|
||||
Solves the one unknown of a ``RateProblem`` — ``quantity = rate × time`` and its two inverses —
|
||||
with **exact integer arithmetic** and a **unit-composition** confirmation:
|
||||
|
||||
```text
|
||||
query quantity: quantity = rate × time (always integer)
|
||||
query rate: rate = quantity ÷ time (exact division or REFUSE)
|
||||
query time: time = quantity ÷ rate (exact division or REFUSE)
|
||||
```
|
||||
|
||||
No floats, no rounding. A non-exact inverse REFUSES (``non_integer_solution``) rather than
|
||||
flooring to a wrong integer — the wrong=0 boundary. The compound-unit composition is confirmed via
|
||||
the unit algebra (R3a); since a ``RateProblem``'s units are consistent by construction, that check
|
||||
is defensive (a non-composing unit is caught earlier, at the reader). Off-serving; deterministic.
|
||||
"""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
from generate.meaning_graph.reader import Refusal
|
||||
from generate.rate_comprehension.model import RateProblem
|
||||
from generate.rate_comprehension.units import (
|
||||
BaseUnit,
|
||||
RateUnit,
|
||||
UnitError,
|
||||
rate_from_quantity_over_time,
|
||||
rate_times_time,
|
||||
time_from_quantity_over_rate,
|
||||
)
|
||||
|
||||
|
||||
def solve_rate(problem: RateProblem) -> int | Refusal:
|
||||
"""Solve the unknown slot exactly, or refuse a non-integer inverse."""
|
||||
ru = problem.rate_unit
|
||||
try:
|
||||
if problem.query == "quantity":
|
||||
rate_times_time(ru, BaseUnit(problem.time_unit)) # confirm mile/hour × hour = mile
|
||||
assert problem.rate is not None and problem.time is not None
|
||||
return problem.rate * problem.time
|
||||
if problem.query == "rate":
|
||||
rate_from_quantity_over_time(BaseUnit(problem.quantity_unit), BaseUnit(problem.time_unit))
|
||||
assert problem.quantity is not None and problem.time is not None
|
||||
if problem.quantity % problem.time != 0:
|
||||
return Refusal("non_integer_solution", f"{problem.quantity} ÷ {problem.time} (rate)")
|
||||
return problem.quantity // problem.time
|
||||
# query == "time"
|
||||
time_from_quantity_over_rate(BaseUnit(problem.quantity_unit), ru)
|
||||
assert problem.quantity is not None and problem.rate is not None
|
||||
if problem.quantity % problem.rate != 0:
|
||||
return Refusal("non_integer_solution", f"{problem.quantity} ÷ {problem.rate} (time)")
|
||||
return problem.quantity // problem.rate
|
||||
except UnitError as exc: # pragma: no cover - a RateProblem's units compose by construction
|
||||
return Refusal("rate_unit_mismatch", str(exc))
|
||||
|
||||
|
||||
def answer_unit(problem: RateProblem) -> str | RateUnit:
|
||||
"""The unit of the answer to *problem* — a base unit string, or a ``RateUnit`` when asking rate."""
|
||||
if problem.query == "quantity":
|
||||
return problem.quantity_unit
|
||||
if problem.query == "time":
|
||||
return problem.time_unit
|
||||
return problem.rate_unit
|
||||
|
||||
|
||||
__all__ = ["answer_unit", "solve_rate"]
|
||||
43
tests/test_rate_solver.py
Normal file
43
tests/test_rate_solver.py
Normal file
|
|
@ -0,0 +1,43 @@
|
|||
"""Tests for the exact integer single-rate solver (R3c).
|
||||
|
||||
Ties the solver to the R3 gold: every ``solved`` fixture computes its ``gold``; every
|
||||
``solver_refuses`` fixture refuses with the gold-claimed reason. Exact-or-refuse, never rounds.
|
||||
"""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
from evals.rate_oracle.runner import _load_rate_gold, gold_to_problem
|
||||
from generate.meaning_graph.reader import Refusal
|
||||
from generate.rate_comprehension.model import RateProblem
|
||||
from generate.rate_comprehension.solver import answer_unit, solve_rate
|
||||
from generate.rate_comprehension.units import RateUnit
|
||||
|
||||
|
||||
def test_solver_solves_every_solved_fixture() -> None:
|
||||
for fx in (f for f in _load_rate_gold() if f["expect"] == "solved"):
|
||||
assert solve_rate(gold_to_problem(fx)) == fx["gold"], fx["id"]
|
||||
|
||||
|
||||
def test_solver_refuses_every_solver_refuse_fixture_with_reason() -> None:
|
||||
for fx in (f for f in _load_rate_gold() if f["expect"] == "solver_refuses"):
|
||||
out = solve_rate(gold_to_problem(fx))
|
||||
assert isinstance(out, Refusal) and out.reason == fx["solver_reason"], fx["id"]
|
||||
|
||||
|
||||
def test_forward_product_is_always_integer() -> None:
|
||||
assert solve_rate(RateProblem(RateUnit("mile", "hour"), 7, 9, None, "quantity")) == 63
|
||||
|
||||
|
||||
def test_non_exact_inverse_refuses_never_rounds() -> None:
|
||||
# 100 mile ÷ 3 hour = 33.3… → refuse (not 33).
|
||||
out = solve_rate(RateProblem(RateUnit("mile", "hour"), None, 3, 100, "rate"))
|
||||
assert isinstance(out, Refusal) and out.reason == "non_integer_solution"
|
||||
# one less mile is exact → 99 ÷ 3 = 33.
|
||||
assert solve_rate(RateProblem(RateUnit("mile", "hour"), None, 3, 99, "rate")) == 33
|
||||
|
||||
|
||||
def test_answer_unit_by_query() -> None:
|
||||
rp = lambda q, **kw: RateProblem(RateUnit("mile", "hour"), kw.get("rate"), kw.get("time"), kw.get("quantity"), q) # noqa: E731
|
||||
assert answer_unit(rp("quantity", rate=60, time=3)) == "mile"
|
||||
assert answer_unit(rp("time", rate=60, quantity=180)) == "hour"
|
||||
assert answer_unit(rp("rate", time=3, quantity=180)) == RateUnit("mile", "hour")
|
||||
Loading…
Reference in a new issue