Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 5 additions & 1 deletion pyproject.toml
Original file line number Diff line number Diff line change
Expand Up @@ -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",
Expand Down Expand Up @@ -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" }
9 changes: 5 additions & 4 deletions src/streamcal/calibrators.py
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Expand All @@ -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

Expand Down
14 changes: 9 additions & 5 deletions tests/test_kernels.py
Original file line number Diff line number Diff line change
@@ -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
Expand Down Expand Up @@ -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)
Expand All @@ -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])
Expand All @@ -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,
)


Expand Down
Loading