fix(python): q16_mul/q16_div pure integer toward-zero truncation

Replaced float division + banker's rounding with pure integer
division truncating toward zero, matching FixedPoint.lean:298 and
FixedPoint.lean:302 exactly.

Added _div_toward_zero helper for Python's // vs Lean Int /
semantics mismatch.

Updated docstring: 'CANONICAL ROUNDING MODE' → 'ARITHMETIC MODE:
truncation toward zero matches Lean Int /'.

No float in compute paths. float_to_q16 (I/O boundary) unchanged.
This commit is contained in:
allaun 2026-07-07 09:31:05 -05:00
parent c3a5e6b60b
commit 68844c92c4

View file

@ -1,15 +1,16 @@
"""Canonical Q16_16 fixed-point arithmetic. """Canonical Q16_16 fixed-point arithmetic.
Single source of truth: CoreFormalism/Q16_16_Spec.lean Single source of truth: Core/SilverSight/FixedPoint.lean
All operations MUST produce identical results to the Lean implementation. All operations MUST produce identical results to the Lean implementation.
Q16_16 represents fixed-point numbers with 16 integer bits and 16 fractional bits. Q16_16 represents fixed-point numbers with 16 integer bits and 16 fractional bits.
Range: [-32768.0, 32767.9999847412109375] Range: [-32768.0, 32767.9999847412109375]
Resolution: 1/65536 0.0000152587890625 Resolution: 1/65536 0.0000152587890625
CANONICAL ROUNDING MODE: round-half-up (banker's rounding) ARITHMETIC MODE: truncation toward zero (matches Lean Int / division)
- Values exactly at half-LSB round to nearest even - Lean mul (FixedPoint.lean:298): (a.toInt * b.toInt) / q16Scale
- All other values round to nearest - Lean div (FixedPoint.lean:302): (a.toInt * q16Scale) / b.toInt
- No rounding: Q16_16 is a quotient type with floor semantics
""" """
import math import math
@ -126,45 +127,47 @@ def q16_sub(a: int, b: int) -> int:
return max(Q16_MIN_RAW, min(Q16_MAX_RAW, result)) return max(Q16_MIN_RAW, min(Q16_MAX_RAW, result))
def q16_mul(a: int, b: int) -> int: def _div_toward_zero(num: int, den: int) -> int:
"""Multiply two Q16_16 values with canonical rounding. """Integer division truncating toward zero.
result = canonical_round((a * b) / 65536) Python's // floors toward negative infinity.
Lean's Int / truncates toward zero.
Uses 64-bit intermediate, then applies banker's rounding. This helper matches Lean's semantics.
""" """
# Use Python's arbitrary precision integers (no overflow issue) if num >= 0:
prod_64 = a * b return num // den
else:
# Divide by scale with banker's rounding return -(-num // den)
# prod_64 / 65536 with half-to-even
scaled = prod_64 / Q16_SCALE # This is a float division for correct rounding
rounded = round(scaled) # Banker's rounding def q16_mul(a: int, b: int) -> int:
"""Multiply two Q16_16 values with truncation toward zero.
return max(Q16_MIN_RAW, min(Q16_MAX_RAW, rounded))
Matches FixedPoint.lean:298: (a.toInt * b.toInt) / q16Scale
Uses Python's arbitrary precision integers (no overflow).
"""
prod = a * b
return max(Q16_MIN_RAW, min(Q16_MAX_RAW, _div_toward_zero(prod, Q16_SCALE)))
def q16_div(a: int, b: int) -> int: def q16_div(a: int, b: int) -> int:
"""Divide two Q16_16 values with canonical rounding. """Divide two Q16_16 values with truncation toward zero.
result = canonical_round((a * 65536) / b) Matches FixedPoint.lean:302: (a.toInt * q16Scale) / b.toInt
Args: Args:
a: Dividend (Q16_16 raw value) a: Dividend (Q16_16 raw value)
b: Divisor (Q16_16 raw value), must not be zero b: Divisor (Q16_16 raw value), must not be zero
Raises: Raises:
ZeroDivisionError: If b is zero ZeroDivisionError: If b is zero
""" """
if b == 0: if b == 0:
raise ZeroDivisionError("Q16_16 division by zero") raise ZeroDivisionError("Q16_16 division by zero")
# (a * 65536) / b with banker's rounding
num = a * Q16_SCALE num = a * Q16_SCALE
scaled = num / b # Float division for correct rounding return max(Q16_MIN_RAW, min(Q16_MAX_RAW, _div_toward_zero(num, b)))
rounded = round(scaled)
return max(Q16_MIN_RAW, min(Q16_MAX_RAW, rounded))
# ============================================================ # ============================================================