SilverSight/python/q16_canonical.py
allaun 68844c92c4 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.
2026-07-07 09:31:05 -05:00

225 lines
6.5 KiB
Python

"""Canonical Q16_16 fixed-point arithmetic.
Single source of truth: Core/SilverSight/FixedPoint.lean
All operations MUST produce identical results to the Lean implementation.
Q16_16 represents fixed-point numbers with 16 integer bits and 16 fractional bits.
Range: [-32768.0, 32767.9999847412109375]
Resolution: 1/65536 ≈ 0.0000152587890625
ARITHMETIC MODE: truncation toward zero (matches Lean Int / division)
- Lean mul (FixedPoint.lean:298): (a.toInt * b.toInt) / q16Scale
- Lean div (FixedPoint.lean:302): (a.toInt * q16Scale) / b.toInt
- No rounding: Q16_16 is a quotient type with floor semantics
"""
import math
import struct
# ============================================================
# §1 CONSTANTS
# ============================================================
Q16_SCALE: int = 65536 # 2^16
Q16_MAX_RAW: int = 2147483647 # INT32_MAX
Q16_MIN_RAW: int = -2147483648 # INT32_MIN
Q16_MAX_FLOAT: float = 32767.9999847412109375 # Max representable
Q16_MIN_FLOAT: float = -32768.0 # Min representable
Q16_RESOLUTION: float = 1.0 / Q16_SCALE # ≈ 0.0000152587890625
# ============================================================
# §2 CONVERSIONS
# ============================================================
def float_to_q16(f: float) -> int:
"""Convert float to Q16_16 raw value with canonical round-half-up.
Uses banker's rounding: round(x * 65536) with ties to nearest even.
Result is clamped to [INT32_MIN, INT32_MAX].
Args:
f: Float value in range [-32768.0, 32767.9999847412109375]
Returns:
32-bit signed integer representing the Q16_16 value
Raises:
ValueError: If f is NaN or infinite
"""
if math.isnan(f) or math.isinf(f):
raise ValueError(f"Cannot convert non-finite float to Q16_16: {f}")
# Python's round() implements banker's rounding (round-half-to-even)
# round(x) = nearest integer, ties go to nearest even integer
scaled = f * Q16_SCALE
rounded = round(scaled) # Banker's rounding: ties to even
# Clamp to 32-bit signed range with saturation
return max(Q16_MIN_RAW, min(Q16_MAX_RAW, rounded))
def q16_to_float(q: int) -> float:
"""Convert Q16_16 raw value to float.
This is exact: no rounding occurs.
Args:
q: 32-bit signed integer Q16_16 raw value
Returns:
Float value = q / 65536.0
"""
return q / Q16_SCALE
def int_to_q16(i: int) -> int:
"""Convert integer to Q16_16 raw value (exact, no rounding).
The integer is scaled by 65536. Clamped to valid range.
Args:
i: Integer in range [-32768, 32767]
Returns:
Q16_16 raw value = clamp(i * 65536)
"""
scaled = i * Q16_SCALE
return max(Q16_MIN_RAW, min(Q16_MAX_RAW, scaled))
def q16_to_int(q: int) -> int:
"""Convert Q16_16 to integer (truncates toward zero).
Args:
q: Q16_16 raw value
Returns:
Integer part = q // 65536 (toward zero)
"""
# Python's // truncates toward negative infinity, so we need
# to handle negative values correctly for toward-zero truncation
if q >= 0:
return q // Q16_SCALE
else:
return -(-q // Q16_SCALE)
# ============================================================
# §3 ARITHMETIC OPERATIONS (all with saturation)
# ============================================================
def q16_add(a: int, b: int) -> int:
"""Add two Q16_16 values with saturation.
result = clamp(a + b)
"""
result = a + b
return max(Q16_MIN_RAW, min(Q16_MAX_RAW, result))
def q16_sub(a: int, b: int) -> int:
"""Subtract two Q16_16 values with saturation.
result = clamp(a - b)
"""
result = a - b
return max(Q16_MIN_RAW, min(Q16_MAX_RAW, result))
def _div_toward_zero(num: int, den: int) -> int:
"""Integer division truncating toward zero.
Python's // floors toward negative infinity.
Lean's Int / truncates toward zero.
This helper matches Lean's semantics.
"""
if num >= 0:
return num // den
else:
return -(-num // den)
def q16_mul(a: int, b: int) -> int:
"""Multiply two Q16_16 values with truncation toward zero.
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:
"""Divide two Q16_16 values with truncation toward zero.
Matches FixedPoint.lean:302: (a.toInt * q16Scale) / b.toInt
Args:
a: Dividend (Q16_16 raw value)
b: Divisor (Q16_16 raw value), must not be zero
Raises:
ZeroDivisionError: If b is zero
"""
if b == 0:
raise ZeroDivisionError("Q16_16 division by zero")
num = a * Q16_SCALE
return max(Q16_MIN_RAW, min(Q16_MAX_RAW, _div_toward_zero(num, b)))
# ============================================================
# §4 COMPARISON OPERATIONS
# ============================================================
def q16_eq(a: int, b: int) -> bool:
return a == b
def q16_lt(a: int, b: int) -> bool:
return a < b
def q16_le(a: int, b: int) -> bool:
return a <= b
# ============================================================
# §5 UTILITY FUNCTIONS
# ============================================================
def q16_from_bytes(raw_bytes: bytes) -> int:
"""Convert 4 bytes (little-endian int32) to Q16_16 raw value."""
return struct.unpack('<i', raw_bytes)[0]
def q16_to_bytes(q: int) -> bytes:
"""Convert Q16_16 raw value to 4 bytes (little-endian int32)."""
# Clamp first to ensure valid int32
clamped = max(Q16_MIN_RAW, min(Q16_MAX_RAW, q))
return struct.pack('<i', clamped)
def q16_is_valid(q: int) -> bool:
"""Check if a raw value is in the valid Q16_16 range."""
return Q16_MIN_RAW <= q <= Q16_MAX_RAW
def q16_repr(q: int) -> str:
"""Return human-readable representation of Q16_16 value."""
return f"Q16_16({q} / 65536 = {q16_to_float(q)})"
# ============================================================
# §6 EXPORTS FOR C INTEROP
# ============================================================
# These functions provide the C-compatible interface for the roundtrip test
def c_float_to_q16(f: float) -> int:
"""C-compatible wrapper for float_to_q16."""
return float_to_q16(f)
def c_q16_to_float(q: int) -> float:
"""C-compatible wrapper for q16_to_float."""
return q16_to_float(q)