From 346d4d59808d30403fbd3a7e183c5a8ee73e143a Mon Sep 17 00:00:00 2001 From: ***** <721466+soodoku@users.noreply.github.com> Date: Tue, 27 Oct 2026 05:23:29 -0700 Subject: [PATCH] Fix simcheck source and portable interpolation checks --- pyproject.toml | 6 +++++- src/streamcal/calibrators.py | 9 +++++---- tests/test_kernels.py | 14 +++++++++----- 3 files changed, 19 insertions(+), 10 deletions(-) diff --git a/pyproject.toml b/pyproject.toml index d5f8826..3eec518 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -53,7 +53,7 @@ test = [ "pytest-cov>=6", # Monte Carlo test machinery. A dependency-group, not an extra: PyPI rejects # direct URL references in published project metadata. - "simcheck @ git+https://github.com/finite-sample/simcheck", + "simcheck", ] docs = [ "sphinx>=8", @@ -134,3 +134,7 @@ source = ["src"] [tool.uv.build-backend] module-name = "streamcal" + +[tool.uv.sources] +# The statistical test helper is unrelated to the simcheck package on PyPI. +simcheck = { git = "https://github.com/finite-sample/simcheck" } diff --git a/src/streamcal/calibrators.py b/src/streamcal/calibrators.py index 8c36db0..a4aac08 100644 --- a/src/streamcal/calibrators.py +++ b/src/streamcal/calibrators.py @@ -51,8 +51,9 @@ def _grid_interp( """``np.interp(values, grid, fitted)`` for an evenly spaced ``grid``. Finds each interval with :func:`_grid_index` instead of a binary search, - then evaluates the same expression ``np.interp`` does, so the result is - the same to the last bit. + then evaluates the same expression as ``np.interp``. For nondecreasing + probability values, results differ by at most one floating-point step: + NumPy may fuse the multiply-add while separate ufuncs round twice. Args: values: Points to interpolate at. @@ -71,8 +72,8 @@ def _grid_interp( # Batch sizes from which the grid kernels beat NumPy's binary searches. Below -# them, NumPy's lower fixed cost wins. Both paths give identical results, so -# the switch changes speed only. Measured with ``benchmarks.speed``. +# them, NumPy's lower fixed cost wins. Index choices are identical; interpolation +# can differ by one floating-point step. Measured with ``benchmarks.speed``. _GRID_INDEX_FROM = 1024 _GRID_INTERP_FROM = 2048 diff --git a/tests/test_kernels.py b/tests/test_kernels.py index ff1cc53..15e881d 100644 --- a/tests/test_kernels.py +++ b/tests/test_kernels.py @@ -1,4 +1,4 @@ -"""Faster code paths must reproduce the straightforward ones exactly.""" +"""Grid kernels preserve bin choices and interpolation within one float step.""" import subprocess import sys @@ -30,7 +30,7 @@ def _adversarial_points(n_bins, rng): @pytest.mark.parametrize("n_bins", [*range(1, 70), 100, 997, 1000, 4096]) -def test_grid_kernels_match_numpy_bit_for_bit(n_bins): +def test_grid_kernels_match_numpy(n_bins): rng = np.random.default_rng(n_bins) edges, centers, points = _adversarial_points(n_bins, rng) reference = np.clip(np.digitize(points, edges) - 1, 0, n_bins - 1) @@ -39,7 +39,9 @@ def test_grid_kernels_match_numpy_bit_for_bit(n_bins): fitted = np.sort(rng.random(n_bins)) fast = _grid_interp(points, centers, fitted) slow = np.interp(points, centers, fitted) - assert np.array_equal(fast.view(np.int64), slow.view(np.int64)) + # NumPy may fuse its multiply-add; separate ufuncs round twice. + np.testing.assert_array_max_ulp(fast, slow, maxulp=1) + assert np.array_equal(_grid_interp(centers, centers, fitted), fitted) @pytest.mark.parametrize("n_bins", [1, 2, 20, 1000]) @@ -54,8 +56,10 @@ def test_size_switches_agree_on_both_sides_of_the_threshold(n_bins): _bin_index(points, edges), np.clip(np.digitize(points, edges) - 1, 0, n_bins - 1), ) - assert np.array_equal( - _interpolate(points, centers, fitted), np.interp(points, centers, fitted) + np.testing.assert_array_max_ulp( + _interpolate(points, centers, fitted), + np.interp(points, centers, fitted), + maxulp=1, )