core/generate/rate_comprehension/solver.py
Shay 1c59f331f4 feat(rate): exact time-unit conversion for single-rate (R3.2b-e)
Option A (text-faithful): RateProblem gains time_unit (the duration's ORIGINAL unit; time stays int; defaults to the rate denominator). The SOLVER converts via convert_time -> exact Fraction -> int-or-refuse(non_integer); Fraction never leaves the solver, no floats. The reader accepts a convertible (minute<->hour) duration mismatch (keeps original time_unit); a non-convertible one still refuses rate_unit_mismatch. Signature includes time_unit ('30 minutes' != '30 hours').

Gold: r3-09 flips reader_refuses -> SOLVED (60 mph for 30 min = 30 mile, exact 1/2 hour; distractor 1800 rejected); new r3-13 non-convertible (3 gallons) stays rate_unit_mismatch. R3 gold 13/13; reader 9/0/0 -> answers 7/0/6 (setup_correct 8->9). Failure-family: rate_unit_mismatch now non-convertible-only -> still unsupported_rate_duration proposal surface; router-hygiene invariant stays green; R1 7/0/3, R2 10/0/0 unchanged. 119-test smoke green incl. invariants. Off-serving, no float path.
2026-06-08 05:43:51 -07:00

64 lines
2.9 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.

"""Exact integer single-rate solver (R3c) with exact-rational unit conversion (R3.2c).
Solves the one unknown of a ``RateProblem`` — ``quantity = rate × time`` and its two inverses —
normalizing the duration to the rate's denominator unit by **exact rational conversion** (R3.2):
```text
query quantity: quantity = rate × convert(time) (exact int or REFUSE)
query rate: rate = quantity ÷ convert(time) (exact int or REFUSE)
query time: time = quantity ÷ rate (in the rate denominator unit) (exact int or REFUSE)
```
``Fraction`` is confined here: ``convert_time`` returns an exact rational, the arithmetic is
exact, and a non-whole result REFUSES (``non_integer_solution``) — **no float, no rounding**. A
duration unit that does not convert to the rate denominator (``…/hour`` for ``3 gallons``) raises
``ConversionError`` and REFUSES (``rate_unit_mismatch``). Off-serving; deterministic.
"""
from __future__ import annotations
from fractions import Fraction
from generate.meaning_graph.reader import Refusal
from generate.rate_comprehension.conversion import ConversionError, convert_time
from generate.rate_comprehension.model import RateProblem
from generate.rate_comprehension.units import RateUnit
def _exact_int(value: Fraction) -> int | Refusal:
"""An exact integer, or a typed refusal — never a rounded/floored approximation."""
if value.denominator != 1:
return Refusal("non_integer_solution", str(value))
return int(value)
def solve_rate(problem: RateProblem) -> int | Refusal:
"""Solve the unknown slot exactly (converting the duration as needed), or refuse."""
ru = problem.rate_unit
try:
if problem.query == "quantity":
assert problem.rate is not None and problem.time is not None
time = convert_time(problem.time, problem.time_unit, ru.denominator)
return _exact_int(Fraction(problem.rate) * time)
if problem.query == "rate":
assert problem.quantity is not None and problem.time is not None
time = convert_time(problem.time, problem.time_unit, ru.denominator)
return _exact_int(Fraction(problem.quantity) / time)
# query == "time" — answered in the rate's denominator unit; the duration is the unknown.
assert problem.quantity is not None and problem.rate is not None
return _exact_int(Fraction(problem.quantity) / Fraction(problem.rate))
except ConversionError as exc:
return Refusal("rate_unit_mismatch", str(exc))
def answer_unit(problem: RateProblem) -> str | RateUnit:
"""The unit of the answer — the rate numerator (quantity), the rate denominator (time), or the
full ``RateUnit`` (rate)."""
if problem.query == "quantity":
return problem.quantity_unit
if problem.query == "time":
return problem.rate_unit.denominator
return problem.rate_unit
__all__ = ["answer_unit", "solve_rate"]