mirror of
https://github.com/allaunthefox/SilverSight.git
synced 2026-07-31 01:25:21 +00:00
feat(python): Q16 fraction module for exact-arithmetic mutation exploration
q16_fraction.py uses Python's Fraction for the entire compute path: - No float, no truncation during intermediate operations - Quantize to Lean-compatible Q16_16 raw int only at to_raw() - from_float is the sole float boundary (I/O entry point) Provides Q16 subclass of Fraction with: - Exact q16_mul/q16_div/q16_add/q16_sub (no rounding) - to_raw: truncation toward zero (matches FixedPoint.lean:298) - to_raw_rounded: round-half-up (alternative output) Designed for value-set mutation sweeps: explore the full rational space, quantize only at the final output boundary.
This commit is contained in:
parent
68844c92c4
commit
d709136e17
1 changed files with 193 additions and 0 deletions
193
python/q16_fraction.py
Normal file
193
python/q16_fraction.py
Normal file
|
|
@ -0,0 +1,193 @@
|
|||
"""
|
||||
Q16_16 Fraction Arithmetic — exact rational exploration (no float in compute path)
|
||||
|
||||
Uses Python's Fraction for exact intermediate arithmetic. Float only at the
|
||||
outer I/O boundary (JSON/sensor input). All compute-path operations are
|
||||
pure Fraction arithmetic.
|
||||
|
||||
Designed for mutation value-set exploration:
|
||||
- No rounding, no truncation, no float error during sweeps
|
||||
- Quantize to Q16_16 raw int only at the final output boundary
|
||||
|
||||
Usage:
|
||||
from q16_fraction import Q16, q16_mul, q16_div, to_raw
|
||||
|
||||
a = Q16.from_float(3.14) # I/O boundary: float → Fraction
|
||||
b = Q16(1, 2) # exact 0.5: Fraction(1, 2) * 65536
|
||||
c = q16_mul(a, b) # exact 1.57: no truncation
|
||||
raw = to_raw(c) # → 102893 (int, Lean-compatible)
|
||||
|
||||
Reference: FixedPoint.lean:298 — Lean truncates toward zero at each op.
|
||||
This module TRUNCATES only at the final to_raw() boundary.
|
||||
Exploration path uses exact arithmetic throughout.
|
||||
"""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import math
|
||||
import struct
|
||||
from fractions import Fraction
|
||||
from typing import Union
|
||||
|
||||
# ── Constants ──────────────────────────────────────────────────────────────
|
||||
|
||||
Q16_SCALE = 65536
|
||||
INT32_MAX = 2147483647
|
||||
INT32_MIN = -2147483648
|
||||
|
||||
|
||||
# ── Q16 type: a Fraction scaled by 65536 in value, but exact in operation ──
|
||||
|
||||
class Q16(Fraction):
|
||||
"""Q16_16 value as an exact Fraction.
|
||||
|
||||
All arithmetic is exact rational (no float, no truncation).
|
||||
The value is the real number it represents (not the raw int).
|
||||
|
||||
Construction:
|
||||
Q16(num, den) — Fraction(num, den) (exact rational)
|
||||
Q16.from_float(f) — I/O boundary (float → Fraction)
|
||||
Q16.from_raw(i) — int raw value (raw → Fraction)
|
||||
|
||||
Conversion:
|
||||
to_raw(q) — Fraction → int (output boundary)
|
||||
"""
|
||||
|
||||
__slots__ = ()
|
||||
|
||||
def __repr__(self) -> str:
|
||||
return f"Q16({float(self):.6f})"
|
||||
|
||||
@classmethod
|
||||
def from_float(cls, f: float) -> Q16:
|
||||
"""I/O boundary: float → Q16 via exact Fraction conversion.
|
||||
|
||||
Uses Fraction.from_float for exact binary→rational conversion,
|
||||
then limits denominator to 65536 for Q16_16 compatibility.
|
||||
Raises ValueError if f is NaN or infinite.
|
||||
"""
|
||||
if math.isnan(f) or math.isinf(f):
|
||||
raise ValueError(f"Non-finite float: {f}")
|
||||
return cls(Fraction.from_float(f).limit_denominator(Q16_SCALE))
|
||||
|
||||
@classmethod
|
||||
def from_raw(cls, raw: int) -> Q16:
|
||||
"""Convert a Q16_16 raw integer back to Q16.
|
||||
|
||||
The raw int represents value * 65536 with truncation.
|
||||
This reverses the truncation: the Fraction captures the
|
||||
exact value that the raw int encodes.
|
||||
"""
|
||||
return cls(raw, Q16_SCALE)
|
||||
|
||||
def to_raw(self) -> int:
|
||||
"""Quantize to Q16_16 raw int (truncation toward zero).
|
||||
|
||||
Matches FixedPoint.lean:298 semantics at the output boundary.
|
||||
Use this when emitting Lean-compatible values.
|
||||
"""
|
||||
num = self.numerator * Q16_SCALE
|
||||
den = self.denominator
|
||||
if num >= 0:
|
||||
return max(INT32_MIN, min(INT32_MAX, num // den))
|
||||
else:
|
||||
return max(INT32_MIN, min(INT32_MAX, -(-num // den)))
|
||||
|
||||
def to_raw_rounded(self) -> int:
|
||||
"""Quantize to Q16_16 raw int with round-half-up.
|
||||
|
||||
Alternative output boundary for cases where rounding is preferred
|
||||
over truncation. Not Lean-compatible by default.
|
||||
"""
|
||||
return max(
|
||||
INT32_MIN,
|
||||
min(INT32_MAX, int(round(self.numerator * Q16_SCALE / self.denominator))),
|
||||
)
|
||||
|
||||
def to_float(self) -> float:
|
||||
"""Debug/convenience: convert to float.
|
||||
|
||||
Not part of the compute path — use only for logging/display.
|
||||
"""
|
||||
return float(self)
|
||||
|
||||
|
||||
# ── Arithmetic (exact Fraction, no float, no truncation) ──────────────────
|
||||
|
||||
def q16_mul(a: Union[Q16, Fraction], b: Union[Q16, Fraction]) -> Q16:
|
||||
"""Exact Q16 multiplication. No truncation, no float.
|
||||
|
||||
Contrast with Lean's truncating mul (FixedPoint.lean:298).
|
||||
This path preserves full precision for mutation exploration.
|
||||
"""
|
||||
return Q16(a * b)
|
||||
|
||||
|
||||
def q16_div(a: Union[Q16, Fraction], b: Union[Q16, Fraction]) -> Q16:
|
||||
"""Exact Q16 division. No truncation, no float.
|
||||
|
||||
Raises ZeroDivisionError if b is zero.
|
||||
"""
|
||||
if b == 0:
|
||||
raise ZeroDivisionError("Q16 division by zero")
|
||||
return Q16(a / b)
|
||||
|
||||
|
||||
def q16_add(a: Union[Q16, Fraction], b: Union[Q16, Fraction]) -> Q16:
|
||||
return Q16(a + b)
|
||||
|
||||
|
||||
def q16_sub(a: Union[Q16, Fraction], b: Union[Q16, Fraction]) -> Q16:
|
||||
return Q16(a - b)
|
||||
|
||||
|
||||
# ── Convenience constructors ───────────────────────────────────────────────
|
||||
|
||||
def to_raw(a: Union[Q16, Fraction]) -> int:
|
||||
"""Convert any Q16/Fraction to Q16_16 raw int.
|
||||
|
||||
One-shot boundary function for the common case.
|
||||
"""
|
||||
if isinstance(a, Fraction) and not isinstance(a, Q16):
|
||||
a = Q16(a)
|
||||
return a.to_raw()
|
||||
|
||||
|
||||
def from_raw(raw: int) -> Q16:
|
||||
return Q16.from_raw(raw)
|
||||
|
||||
|
||||
def from_float(f: float) -> Q16:
|
||||
return Q16.from_float(f)
|
||||
|
||||
|
||||
# ── Serialization ──────────────────────────────────────────────────────────
|
||||
|
||||
def q16_from_bytes(raw_bytes: bytes) -> Q16:
|
||||
return Q16.from_raw(struct.unpack("<i", raw_bytes)[0])
|
||||
|
||||
|
||||
def q16_to_bytes(a: Union[Q16, Fraction]) -> bytes:
|
||||
return struct.pack("<i", to_raw(a))
|
||||
|
||||
|
||||
# ── Eval witnesses ────────────────────────────────────────────────────────
|
||||
|
||||
if __name__ == "__main__":
|
||||
# Mutation exploration: all arithmetic is exact
|
||||
a = Q16.from_float(3.0)
|
||||
b = Q16(1, 3) # exact 1/3
|
||||
|
||||
c = q16_mul(a, b) # exact 1.0
|
||||
d = q16_div(a, b) # exact 9.0
|
||||
e = q16_add(c, d) # exact 10.0
|
||||
|
||||
print(f" 3.0 × 1/3 = {c.to_float():.6f} (raw={to_raw(c)})")
|
||||
print(f" 3.0 ÷ 1/3 = {d.to_float():.6f} (raw={to_raw(d)})")
|
||||
print(f" 1.0 + 9.0 = {e.to_float():.6f} (raw={to_raw(e)})")
|
||||
|
||||
# Compare with Lean-compatible truncation at boundary
|
||||
a_raw = from_float(3.14)
|
||||
b_raw = from_raw(102893)
|
||||
print(f" 3.14 as Q16: {a_raw} (raw={to_raw(a_raw)})")
|
||||
print(f" raw 102893 → {b_raw.to_float():.6f}")
|
||||
Loading…
Add table
Reference in a new issue