Skip to content

Commit 61e7e7e

Browse files
committed
fix(parallel): preserve observed evidence through Rust f64
1 parent 266cf27 commit 61e7e7e

1 file changed

Lines changed: 43 additions & 10 deletions

File tree

python/fast_mlsirm/parallel_analysis.py

Lines changed: 43 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -121,7 +121,10 @@ def _validate_trusted_real_scalar(value: object) -> None:
121121
raise ValueError("data must be real-valued")
122122
if value_type is np.bool_ or any(
123123
value_type is scalar_type
124-
for scalar_type in (*_TRUSTED_NUMPY_INTEGER_TYPES, *_TRUSTED_NUMPY_FLOAT_TYPES)
124+
for scalar_type in (
125+
*_TRUSTED_NUMPY_INTEGER_TYPES,
126+
*_TRUSTED_NUMPY_FLOAT_TYPES,
127+
)
125128
):
126129
return
127130
raise ValueError("data must be numeric and convertible to float64")
@@ -148,13 +151,45 @@ def _preflight_real_matrix(data: object) -> None:
148151
if row_type is list or row_type is tuple:
149152
for column_index in range(len(row)):
150153
cell = row[column_index]
151-
if type(cell) is list or type(cell) is tuple or type(cell) is np.ndarray:
154+
if (
155+
type(cell) is list
156+
or type(cell) is tuple
157+
or type(cell) is np.ndarray
158+
):
152159
raise ValueError("data must be a 2-D persons x items array")
153160
_validate_trusted_real_scalar(cell)
154161
continue
155162
_validate_trusted_real_scalar(row)
156163

157164

165+
def _lossless_float64_matrix(raw: np.ndarray) -> np.ndarray:
166+
"""Narrow trusted evidence only when every finite numeric identity survives."""
167+
try:
168+
with np.errstate(over="ignore", invalid="ignore"):
169+
narrowed = np.ascontiguousarray(raw, dtype=np.float64)
170+
except (TypeError, ValueError, OverflowError):
171+
raise ValueError("data must be numeric and convertible to float64") from None
172+
173+
if raw.dtype.kind in ("i", "u"):
174+
info = np.iinfo(raw.dtype)
175+
bits = raw.dtype.itemsize * 8
176+
lower = 0.0 if raw.dtype.kind == "u" else float(-(1 << (bits - 1)))
177+
upper = float(1 << bits) if raw.dtype.kind == "u" else float(1 << (bits - 1))
178+
if np.any(narrowed < lower) or np.any(narrowed >= upper):
179+
raise ValueError("data must be exactly representable as float64")
180+
restored = narrowed.astype(raw.dtype)
181+
if not np.array_equal(restored, raw):
182+
raise ValueError("data must be exactly representable as float64")
183+
del info
184+
elif raw.dtype.kind == "f" and raw.dtype.itemsize > np.dtype(np.float64).itemsize:
185+
restored = narrowed.astype(raw.dtype)
186+
finite = np.isfinite(raw)
187+
if np.any(restored[finite] != raw[finite]):
188+
raise ValueError("data must be exactly representable as float64")
189+
190+
return narrowed
191+
192+
158193
def _real_numeric_matrix(data: object) -> np.ndarray:
159194
"""Validate inert real evidence before narrowing it to contiguous ``float64``."""
160195
_preflight_real_matrix(data)
@@ -168,10 +203,7 @@ def _real_numeric_matrix(data: object) -> np.ndarray:
168203
raise ValueError("data must be real-valued")
169204
if raw.dtype.kind not in ("b", "i", "u", "f"):
170205
raise ValueError("data must be numeric and convertible to float64")
171-
try:
172-
return np.ascontiguousarray(raw, dtype=np.float64)
173-
except (TypeError, ValueError, OverflowError):
174-
raise ValueError("data must be numeric and convertible to float64") from None
206+
return _lossless_float64_matrix(raw)
175207

176208

177209
def parallel_analysis(
@@ -204,10 +236,11 @@ def parallel_analysis(
204236
scalar evidence; arbitrary array/container/numeric subclasses and
205237
conversion providers are rejected before NumPy protocols execute. The
206238
known 2-D carrier structure is preflighted without unbounded recursive
207-
container traversal. Complex and non-real storage is rejected before the
208-
accepted matrix is marshalled to contiguous ``float64``. The random-
209-
eigenvalue benchmark workspace is bounded to 128 MiB before compiled
210-
dispatch.
239+
container traversal. Finite integer and extended-precision floating
240+
observations must preserve their numeric identity through the Rust `f64`
241+
boundary. Complex and non-real storage is rejected before the accepted
242+
matrix is marshalled to contiguous ``float64``. The random-eigenvalue
243+
benchmark workspace is bounded to 128 MiB before compiled dispatch.
211244
212245
"""
213246
explicit_iterations = (

0 commit comments

Comments
 (0)