ulpwise 0.3.0__tar.gz → 0.3.1__tar.gz

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (38) hide show
  1. ulpwise-0.3.1/.github/dependabot.yml +23 -0
  2. {ulpwise-0.3.0 → ulpwise-0.3.1}/.github/workflows/ci.yml +16 -9
  3. {ulpwise-0.3.0 → ulpwise-0.3.1}/.github/workflows/release.yml +31 -10
  4. {ulpwise-0.3.0 → ulpwise-0.3.1}/.gitignore +1 -0
  5. ulpwise-0.3.1/CHANGELOG.md +222 -0
  6. {ulpwise-0.3.0 → ulpwise-0.3.1}/Cargo.lock +11 -11
  7. {ulpwise-0.3.0 → ulpwise-0.3.1}/Cargo.toml +1 -1
  8. {ulpwise-0.3.0 → ulpwise-0.3.1}/PKG-INFO +64 -13
  9. {ulpwise-0.3.0 → ulpwise-0.3.1}/README.md +62 -12
  10. {ulpwise-0.3.0 → ulpwise-0.3.1}/pyproject.toml +3 -2
  11. {ulpwise-0.3.0 → ulpwise-0.3.1}/python/ulpwise/__init__.py +115 -8
  12. {ulpwise-0.3.0 → ulpwise-0.3.1}/python/ulpwise/__main__.py +1 -1
  13. ulpwise-0.3.1/python/ulpwise/corpus/cases.json +1024 -0
  14. {ulpwise-0.3.0 → ulpwise-0.3.1}/python/ulpwise/scan.py +20 -7
  15. {ulpwise-0.3.0 → ulpwise-0.3.1}/python/ulpwise/survey.py +152 -49
  16. {ulpwise-0.3.0 → ulpwise-0.3.1}/studies/accuracy-survey-2026-09/README.md +13 -1
  17. {ulpwise-0.3.0 → ulpwise-0.3.1}/studies/accuracy-survey-2026-09/results.csv +2 -2
  18. {ulpwise-0.3.0 → ulpwise-0.3.1}/studies/accuracy-survey-2026-09/results.md +2 -2
  19. {ulpwise-0.3.0 → ulpwise-0.3.1}/tests/test_core.py +64 -0
  20. {ulpwise-0.3.0 → ulpwise-0.3.1}/tests/test_corpus.py +23 -4
  21. {ulpwise-0.3.0 → ulpwise-0.3.1}/tests/test_scan.py +71 -0
  22. ulpwise-0.3.1/tests/test_survey.py +681 -0
  23. ulpwise-0.3.0/CHANGELOG.md +0 -80
  24. ulpwise-0.3.0/python/ulpwise/corpus/cases.json +0 -437
  25. ulpwise-0.3.0/tests/test_survey.py +0 -53
  26. {ulpwise-0.3.0 → ulpwise-0.3.1}/CONTRIBUTING.md +0 -0
  27. {ulpwise-0.3.0 → ulpwise-0.3.1}/LICENSE-APACHE +0 -0
  28. {ulpwise-0.3.0 → ulpwise-0.3.1}/LICENSE-MIT +0 -0
  29. {ulpwise-0.3.0 → ulpwise-0.3.1}/examples/sqrt_knife_values.py +0 -0
  30. {ulpwise-0.3.0 → ulpwise-0.3.1}/examples/torch_sqrt_conformance.py +0 -0
  31. {ulpwise-0.3.0 → ulpwise-0.3.1}/python/ulpwise/corpus/__init__.py +0 -0
  32. {ulpwise-0.3.0 → ulpwise-0.3.1}/python/ulpwise/plugin.py +0 -0
  33. {ulpwise-0.3.0 → ulpwise-0.3.1}/src/edge.rs +0 -0
  34. {ulpwise-0.3.0 → ulpwise-0.3.1}/src/exact.rs +0 -0
  35. {ulpwise-0.3.0 → ulpwise-0.3.1}/src/knife.rs +0 -0
  36. {ulpwise-0.3.0 → ulpwise-0.3.1}/src/lib.rs +0 -0
  37. {ulpwise-0.3.0 → ulpwise-0.3.1}/src/py.rs +0 -0
  38. {ulpwise-0.3.0 → ulpwise-0.3.1}/src/ulp.rs +0 -0
@@ -0,0 +1,23 @@
1
+ # Dependabot keeps Cargo.lock and the workflow actions current through pull requests that CI
2
+ # tests, so a pyo3 patch release (0.29.2 -> 0.29.3 at the time of writing) is picked up without
3
+ # anyone running cargo by hand. Python dependencies stay unpinned in pyproject.toml on purpose:
4
+ # the corpus is meant to run against whatever torch, kornia, timm, peft, torchrl and
5
+ # ultralytics the user has installed.
6
+ version: 2
7
+ updates:
8
+ - package-ecosystem: cargo
9
+ directory: /
10
+ schedule:
11
+ interval: weekly
12
+ day: monday
13
+ open-pull-requests-limit: 3
14
+ commit-message:
15
+ prefix: "deps(cargo)"
16
+ - package-ecosystem: github-actions
17
+ directory: /
18
+ schedule:
19
+ interval: weekly
20
+ day: monday
21
+ open-pull-requests-limit: 3
22
+ commit-message:
23
+ prefix: "deps(actions)"
@@ -4,6 +4,9 @@ on:
4
4
  push:
5
5
  branches: [main]
6
6
  pull_request:
7
+ schedule:
8
+ - cron: "17 6 * * 1" # Monday 06:17 UTC: the corpus against the latest releases, strict
9
+ workflow_dispatch:
7
10
 
8
11
  jobs:
9
12
  rust:
@@ -14,7 +17,7 @@ jobs:
14
17
  matrix:
15
18
  os: [ubuntu-latest, macos-latest, windows-latest]
16
19
  steps:
17
- - uses: actions/checkout@v4
20
+ - uses: actions/checkout@v7
18
21
  - uses: dtolnay/rust-toolchain@stable
19
22
  with:
20
23
  components: rustfmt, clippy
@@ -34,29 +37,33 @@ jobs:
34
37
  - os: ubuntu-latest
35
38
  python: "3.9"
36
39
  steps:
37
- - uses: actions/checkout@v4
40
+ - uses: actions/checkout@v7
38
41
  - uses: dtolnay/rust-toolchain@stable
39
- - uses: actions/setup-python@v5
42
+ - uses: actions/setup-python@v7
40
43
  with:
41
44
  python-version: ${{ matrix.python }}
42
- - run: pip install pytest numpy packaging
45
+ - run: pip install pytest numpy packaging mpmath
43
46
  - run: pip install .
44
- - run: pytest tests/test_core.py -q -p no:cacheprovider
47
+ - run: pytest tests/test_core.py tests/test_survey.py tests/test_scan.py -q -p no:cacheprovider
45
48
  - name: sqrt conformance of numpy on this platform
46
49
  run: python examples/torch_sqrt_conformance.py && python examples/sqrt_knife_values.py
47
50
 
48
51
  corpus:
49
52
  name: regression corpus (ubuntu, torch cpu)
50
53
  runs-on: ubuntu-latest
54
+ env:
55
+ # On the weekly and the manual runs an unexpected pass is a failure: a release fixed a case,
56
+ # update cases.json. Push and pull request runs stay lenient.
57
+ ULPWISE_CORPUS_STRICT: ${{ (github.event_name == 'schedule' || github.event_name == 'workflow_dispatch') && '1' || '0' }}
51
58
  steps:
52
- - uses: actions/checkout@v4
59
+ - uses: actions/checkout@v7
53
60
  - uses: dtolnay/rust-toolchain@stable
54
- - uses: actions/setup-python@v5
61
+ - uses: actions/setup-python@v7
55
62
  with:
56
63
  python-version: "3.12"
57
- - run: pip install pytest numpy packaging scipy
64
+ - run: pip install pytest numpy packaging scipy mpmath expecttest
58
65
  - run: pip install torch torchvision --index-url https://download.pytorch.org/whl/cpu
59
- - run: pip install kornia timm peft torchrl
66
+ - run: pip install kornia timm peft torchrl ultralytics
60
67
  - run: pip install .
61
68
  - run: pytest tests -q -p no:cacheprovider -rxXs
62
69
  - name: sqrt conformance of torch and numpy on this platform
@@ -1,9 +1,10 @@
1
1
  # Builds wheels for every supported platform plus an sdist. On a tag push (v*) it also
2
2
  # uploads them to PyPI through trusted publishing (no API token stored anywhere):
3
3
  # the PyPI project "ulpwise" must list this repository, this workflow file name and the
4
- # "pypi" environment as its publisher. A manual run only builds and checks the wheels,
5
- # unless its "publish" input is set, in which case it also uploads, tags the commit and
6
- # creates the GitHub release with the CHANGELOG section as notes and the wheels attached.
4
+ # "pypi" environment as its publisher. A manual run only builds and checks the wheels and
5
+ # gathers them the way the publish job would (the "collect" job), unless its "publish" input
6
+ # is set, in which case it instead uploads, tags the commit and creates the GitHub release
7
+ # with the CHANGELOG section as notes and the wheels attached.
7
8
  name: release
8
9
 
9
10
  on:
@@ -48,8 +49,8 @@ jobs:
48
49
  target: x64
49
50
  smoke: true
50
51
  steps:
51
- - uses: actions/checkout@v4
52
- - uses: actions/setup-python@v5
52
+ - uses: actions/checkout@v7
53
+ - uses: actions/setup-python@v7
53
54
  with:
54
55
  python-version: "3.12"
55
56
  - uses: PyO3/maturin-action@v1
@@ -66,7 +67,7 @@ jobs:
66
67
  python -c "import ulpwise; print('ulpwise', ulpwise.__version__)"
67
68
  python -m ulpwise ulp 1.0 1.0000001 --dtype f32
68
69
  python -m ulpwise midpoint sqrt 0.8528626561164856 --dtype f32
69
- - uses: actions/upload-artifact@v4
70
+ - uses: actions/upload-artifact@v7
70
71
  with:
71
72
  name: wheels-${{ matrix.name }}
72
73
  path: dist
@@ -74,7 +75,7 @@ jobs:
74
75
  sdist:
75
76
  runs-on: ubuntu-latest
76
77
  steps:
77
- - uses: actions/checkout@v4
78
+ - uses: actions/checkout@v7
78
79
  - uses: PyO3/maturin-action@v1
79
80
  with:
80
81
  command: sdist
@@ -84,11 +85,31 @@ jobs:
84
85
  pip install pytest numpy
85
86
  pip install dist/*.tar.gz
86
87
  pytest tests/test_core.py -q -p no:cacheprovider
87
- - uses: actions/upload-artifact@v4
88
+ - uses: actions/upload-artifact@v7
88
89
  with:
89
90
  name: wheels-sdist
90
91
  path: dist
91
92
 
93
+ # A manual run without "publish" ends here: gather the artifacts exactly the way the publish
94
+ # job does and count them, so a dry run exercises the whole pipeline short of the upload.
95
+ collect:
96
+ if: ${{ !(startsWith(github.ref, 'refs/tags/v') || inputs.publish == true) }}
97
+ needs: [wheels, sdist]
98
+ runs-on: ubuntu-latest
99
+ steps:
100
+ - uses: actions/download-artifact@v8
101
+ with:
102
+ pattern: wheels-*
103
+ merge-multiple: true
104
+ path: dist
105
+ - name: one abi3 wheel per matrix entry plus the sdist
106
+ run: |
107
+ ls -l dist
108
+ wheels=$(ls dist/*.whl | wc -l)
109
+ sdists=$(ls dist/*.tar.gz | wc -l)
110
+ echo "$wheels wheels, $sdists sdist"
111
+ test "$wheels" -eq 5 && test "$sdists" -eq 1
112
+
92
113
  publish:
93
114
  if: startsWith(github.ref, 'refs/tags/v') || inputs.publish == true
94
115
  needs: [wheels, sdist]
@@ -98,8 +119,8 @@ jobs:
98
119
  id-token: write
99
120
  contents: write
100
121
  steps:
101
- - uses: actions/checkout@v4
102
- - uses: actions/download-artifact@v4
122
+ - uses: actions/checkout@v7
123
+ - uses: actions/download-artifact@v8
103
124
  with:
104
125
  pattern: wheels-*
105
126
  merge-multiple: true
@@ -8,3 +8,4 @@ __pycache__/
8
8
  *.egg-info/
9
9
  .pytest_cache/
10
10
  .venv/
11
+ .coverage
@@ -0,0 +1,222 @@
1
+ # Changelog
2
+
3
+ ## 0.3.1 (2026-10-05)
4
+
5
+ - `ulpwise survey` takes `f16` and `bf16` in `--dtypes` (the default stays `f32,f64`): torch and jax get half
6
+ precision rows, measured in float16 or bfloat16 ulps against the same mpmath reference; numpy has no
7
+ bfloat16 and scipy.special computes a float16 input in float32, so those two have no `bf16` rows and scipy
8
+ has no `f16` rows. A backend without a kernel for a function in a dtype (torch's Bessel and Airy functions,
9
+ `erfcx`, `ndtri`, `log_ndtr` and `zeta` in half precision) is logged and skipped before the reference is
10
+ computed. The torch default tolerances of the two dtypes are `(1.6e-2, 1e-5)` and `(1e-3, 1e-5)`, as in
11
+ `torch.testing`. A log spaced domain wider than the dtype is now clipped to the dtype's finite range before
12
+ the points are spaced: the float16 grid of `sqrt` and `log` over `(1e-300, 1e300)` kept 32 of 600 points and
13
+ `acosh` 24, they have about 570 and 490 now; the float32 grids of the same functions go from about 100 points
14
+ to about 600, the float64 grids are unchanged. The `(0, 1)` and `(-1, 1)` grids of `ndtri`, `logit`, `asin`,
15
+ `acos`, `atanh` and `erfinv` stop at the float below 1 in half precision too, where `1 - 1e-4` and `1 - 1e-3`
16
+ rounded to 1. Two corrections to the ulp metric for every dtype: the threshold from which an exact result
17
+ counts as a correct overflow to infinity was `fmax * (1 + eps / 2)`, about one ulp above the largest finite
18
+ value, and is now `fmax + ulp / 2`, the rounding threshold; and the spacing at the largest finite value was
19
+ numpy's `inf`, which turned every finite result there into a 0 ulp error, it is `ulpwise.spacing` now.
20
+ An unknown dtype raises `ValueError` instead of a `KeyError` from the grid.
21
+ - `next_up`, `next_down`, `spacing`, `neighbours`, `binade_edges` and `all_floats` accept `f16` and `bf16` (and the
22
+ `float16`, `half`, `bfloat16`, `torch.float16`, `torch.bfloat16` aliases) like `ulp_distance` and `special` already
23
+ did; they raised `unsupported dtype` before. The 16 bit versions are pure Python on the bit patterns, with the
24
+ conventions of the Rust ones: the largest float steps to infinity, both zeros to the smallest subnormal, `spacing`
25
+ is the distance from `|x|` to the next float and stays finite at the largest float, `neighbours` returns finite
26
+ values only. A test walks every finite float16 and bfloat16 and checks the six functions against each other, against
27
+ numpy's float16 `nextafter` and `spacing` and against torch's bfloat16 `nextafter`.
28
+ - `ulpwise scan` read the first positional argument of a method call as the value the method is applied
29
+ to, so `log(exp(x).sum(-1))` was not a `logsumexp-by-hand` finding while `log(exp(x).sum())` and
30
+ `log(exp(x).sum(dim=-1))` were. A method call whose first argument is a dimension (an integer, `None`
31
+ or a tuple of them) is now read as applying to its receiver, like the argument-less form.
32
+ - `ulpwise scan` printed a `SyntaxWarning` (a `DeprecationWarning` before Python 3.12) for every invalid
33
+ escape sequence in the files it scanned, four lines on kornia. The parse now ignores those warnings;
34
+ a file that does not parse is skipped as before.
35
+ - `ulpwise scan` reported Windows paths with backslashes (`kornia\geometry\conversions.py`), so the same
36
+ report read differently from the one made on Linux or macOS. Paths are now written with forward
37
+ slashes on every platform; `--run` still imports the module from either form. The CI pytest job on
38
+ ubuntu, macOS and windows runs `tests/test_scan.py` now, and that file also covers the reasons
39
+ `--run` gives for a function it does not run (a class, a constant, an instance method, a builtin
40
+ without a signature, a nested name, a module that fails to import) and the ulp comparison of the
41
+ results across infinities, NaNs and an all zero reference.
42
+ - `ulpwise survey` without scipy installed died at `ndtri` with a `ModuleNotFoundError` and lost every
43
+ row computed before it: the reference started its Newton iteration from `scipy.special.ndtri`. It now
44
+ starts from a bisection on `math.erfc`, returns an exact zero at `p = 0.5` (a Newton step there leaves a
45
+ rounding residual, which the ulp metric would read as 1e16 ulps against the exact 0.0 of torch and scipy)
46
+ and gives the same rows as before on the 600 point float64 and float32 grids. The survey also evaluates
47
+ the reference only when some requested backend implements the function; a numpy only run no longer
48
+ spends its time on references for the torch and scipy only entries. The pytest job on ubuntu, macOS and
49
+ windows (Python 3.12 and 3.9) runs `tests/test_survey.py` now, with numpy as the only backend.
50
+ - `ulpwise survey` printed a numpy `RuntimeWarning` for every backend call that divided by zero or
51
+ overflowed at the edge values of the grid (`reciprocal` and `log` at 0, `reciprocal` at the overflow
52
+ edge), hundreds of lines on a full run. Those points are counted in the `nonfinite` column already, so
53
+ the survey now evaluates the backends under `np.errstate(all="ignore")`, and a test runs `reciprocal` and
54
+ `log` with warnings turned into errors.
55
+ - The README said that two kornia fixes in the corpus were merged but not released; the corpus has 31
56
+ kornia cases now, and the pytorch/rl, peft and pytorch (#198006) fixes are in the same state (kornia
57
+ 0.8.3 and 0.9.0rc1, torchrl 0.14.0, peft 0.21.2 and torch 2.14.1 predate them or cherry-pick other
58
+ changes), while the timm and ultralytics fixes are released. The sentence says so now, and
59
+ `tests/test_corpus.py` checks that the README table has one row per case with the merge date from
60
+ `cases.json`, so a case added without its row, or a row with a stale date, fails the suite.
61
+ - `tests/test_survey.py` checks every reference of the survey registry against the libraries it measures:
62
+ each entry is surveyed on a 24 point float64 grid against torch, numpy and scipy and its median error must
63
+ stay under 32 ulp, which a reference that is another function fails by fifteen orders of magnitude (torch's
64
+ `polygamma_1` is the worst true median at 9 ulp), and the piecewise references (`selu`, `elu`, `entr`,
65
+ `gelu_tanh`, `log_ndtr`, `lgamma`, `digamma`, `erfinv`, `ndtri`, `zeta`, `sinc`, `spherical_bessel_j0`)
66
+ are pinned once per branch, since a slip in one branch moves a third of the grid and keeps the median.
67
+ `survey.py` goes from 77 to 94 percent covered.
68
+ - With torch installed but `expecttest` missing, `torch.testing._internal` does not import and
69
+ `ulpwise survey` silently used the dtype default as the `OpInfo` tolerance, so the `op tol` columns
70
+ looked like an override that was never read. The survey now logs one line with the import error and
71
+ the `survey` extra installs `expecttest`. The regression corpus CI job, which has torch but had no
72
+ `expecttest`, failed on the two `op_db` tests for the same reason; it installs `expecttest` now and the
73
+ tests skip with the reason where `op_db` is not importable.
74
+ - `ulpwise survey` looked up torch's `OpInfo` tolerance by op name and took the first `op_db` entry
75
+ with that name. `polygamma` has one entry per order, and the first, `polygamma_n_0`, has no
76
+ override, so the `op fail` column of `polygamma_1` and `polygamma_2` in float32 was computed with
77
+ the dtype default (`rtol 1.3e-6`, `atol 1e-5`) instead of torch's `polygamma_n_1` and `_n_2`
78
+ override (`rtol 0.01`, `atol 1e-4`). `Entry` gains `opinfo_variant`, `torch_opinfo_tolerance`
79
+ takes a `variant` and otherwise prefers the base variant, and the two registry entries name
80
+ theirs. In `studies/accuracy-survey-2026-09` the `polygamma_1` f32 row goes from 86 to 7 inputs
81
+ failing the op's tolerance; the measured errors are unchanged (erratum in its README).
82
+ - `tests/test_survey.py` covers the survey itself now: `survey()` on `sqrt` against numpy and torch
83
+ (numpy within half an ulp, torch under one), an inexact backend, a backend that raises or returns
84
+ the wrong shape, the backend resolution, the `op_db` tolerance lookup including the variant case,
85
+ the csv and markdown writers, `versions()`, `main()` and the `survey` subcommand. CI installs
86
+ `mpmath` for the Python job so these tests run there instead of being skipped.
87
+ - Six more kornia cases, each present on kornia 0.8.3 and fixed on main: the Hessian of `So3.exp`
88
+ at the identity (#4972, nine nan for `-I / 4` and zeros), `sampson_epipolar_distance` of a point on
89
+ its epiline with `squared=False` (#5116, `sqrt(eps)` = 1e-4 for 0) and of the same `F` scaled by
90
+ 1e-4 (#5116, a third lower), `RandomHue` on a float64 image (#5131, 8.7e-8 rad past a half turn,
91
+ the float32 rounding error of pi), `MS_SSIMLoss(sigmas=(0.5, 1.3), reduction="none")` (#5143, an
92
+ even 6-pixel window and a `(1, 15, 19)` map for a `(1, 3, 16, 20)` input) and `MS_SSIMLoss` on a
93
+ uint8 pair (#5353, `expected scalar type Byte but found Float`; with `data_range=255` it now scores
94
+ the pair divided by 255 at the default to 1e-6). Six more: `So3.right_jacobian` of a float16
95
+ 45 rad rotation (#4967, the identity for a matrix of 0.2 to 0.4), `average_quaternions` with a member
96
+ stored as `3 q` (#4980, 41.8 degrees for the 22.5 degree bisector), `Hyperplane.through` a float16
97
+ triangle with legs of 300 (#5104, the normal `(0, 0, 1)` for `(0, 0, -1)`), `filter2d` with a
98
+ per-sample kernel on a channels-last image (#5301, torch's view error), `lovasz_hinge_loss` of a
99
+ float16 prediction (#5303, a float32 loss) and `get_box_kernel1d` (#5357, a stride-0 view where
100
+ one write zeroed all three taps). The kornia block is 31 cases, all `present` on 0.8.3 and all
101
+ `fixed` on main. One more ultralytics case, `scale_masks` with the dataloader's `ratio_pad` and an
102
+ odd letterbox padding (#26379, a padded row survived the crop and the bottom rows of every mask
103
+ faded to 0) and `verify_image_label` on two triangles that tile one square (#26377, the second
104
+ polygon was dropped as a duplicate of the first because only the class and the box were compared);
105
+ both carry `fixed_in_release` 8.4.165, the first tag with the fixes), and `verify_image_label` on a
106
+ pose row for a detect task (#26357, the row was read as a polygon, its box was out of bounds and
107
+ the image was dropped as corrupt; `fixed_in_release` 8.4.164). The corpus is 50 cases.
108
+ - The manual `workflow_dispatch` run of CI is now strict about the corpus like the Monday run (the
109
+ changelog said so already, the workflow set the variable for `schedule` only).
110
+ - The release workflow's manual dry run (`workflow_dispatch` without `publish`) now ends in a
111
+ `collect` job that downloads the artifacts the way the publish job does and checks that there
112
+ are five wheels and one sdist, so a dry run covers the whole pipeline short of the upload. The
113
+ workflow actions moved to `actions/checkout@v7`, `setup-python@v7`, `upload-artifact@v7` and
114
+ `download-artifact@v8` (the first Dependabot pull requests), and `Cargo.lock` to pyo3 0.29.3.
115
+ - Dependabot (`.github/dependabot.yml`) opens weekly pull requests for Cargo.lock and the
116
+ workflow actions, so pyo3 patch releases land through CI-tested pull requests instead of a
117
+ hand-run `cargo update`; Python dependencies stay unpinned on purpose.
118
+ - Eight more kornia cases, each present on kornia 0.8.3 and fixed on main: `Se2.exp`/`Se2.log` at
119
+ theta = 1e-8 (#4960, the translation came back as (1, 2) from exp and (1e-8, -5e-9) from log),
120
+ `point_line_distance` of a homogeneous point with w = 2 (#4975, 4.0 for 1.5), `Quaternion.__pow__`
121
+ of -1 (#5003, the zero quaternion), `So2` from a `(B, 1)` angle times `(B, 2)` points (#5005,
122
+ `(B, B, 2)`), `solve_cubic` of `1e-30 x^3 + 2x - 6` (#5024, roots `[0, 0, 0]` for 3),
123
+ `RgbToGrayscale` on uint8 (#5111, all zeros), `conv_soft_argmax2d` with a far-away peak (#5134,
124
+ the weak window's coordinates moved from 0.8834 to 1.0) and `get_gaussian_discrete_kernel1d(1,
125
+ sigma)` (#5376, 3 taps). Six more in the same shape: `Se3.exp`'s `d t / d omega` at the identity
126
+ (#4963, nan on 0.8.3, zero on main before the fix), `Quaternion.polar_angle`'s gradient at the
127
+ identity (#4981, nan), `decompose_essential_matrix` of a `(3, 3)` input (#4998, `(1, 3, 3)`),
128
+ `Hyperplane.through`'s gradients for orthogonal equal-length edges (#5058, nan),
129
+ `Vector3.normalized` of a float16 zero vector (#5084, NaN) and `otsu_threshold`'s mask below
130
+ zero (#5182, all False). The whole kornia block of the corpus, 19 cases, reads `present` on 0.8.3;
131
+ the mean_average_precision recall thresholds (#5101) were checked and left out because 0.8.3
132
+ already scores the exact-tenth case right.
133
+ - CI runs the regression corpus every Monday (and on `workflow_dispatch`) with
134
+ `ULPWISE_CORPUS_STRICT=1`, which turns an unexpected pass into a failure: a release fixed a case
135
+ and `fixed_in_release` is stale. Push and pull request runs stay non strict. The corpus job also
136
+ installs `ultralytics`, so the ultralytics case runs there instead of being skipped.
137
+ - Corpus metadata: kornia #4838 is fixed by kornia #5124 (merged 2026-09-30) and kornia #4897 by
138
+ kornia #4941 (merged 2026-09-26); both cases now carry the `pr` and `merged_at`, and stay expected
139
+ failures because no kornia release after 0.9.0rc1 (2026-07-19) exists yet. ultralytics #26330 sets
140
+ `fixed_in_release` to `8.4.164`: the tag `v8.4.164` (2026-09-27, the first release after the merge)
141
+ carries the `use_obb` check in `verify_labels` and `v8.4.163` does not, so the case is a hard
142
+ failure, not an expected one, from 8.4.164 on. peft #3777 keeps `fixed_in_release: null`: the
143
+ `v0.21.1` and `v0.21.2` tags still carry the `weight.size()[2:4]` shortcut in `lora/layer.py`.
144
+
145
+ ## 0.3.0 (2026-09-26)
146
+
147
+ - `ulpwise scan`: static scan of a repository (a directory, a GitHub URL or `owner/repo`, cloned
148
+ with depth 1) for the floating point patterns behind the corpus bugs. Twelve rules with severity,
149
+ reason, replacement and upstream example: `exp-of-square`, `sin-of-pi-times`, `softplus-by-hand`,
150
+ `logsumexp-by-hand`, `hypot-by-hand`, `sqrt-of-difference`, `one-minus-cos`, `log1p-by-hand`,
151
+ `expm1-by-hand`, `atan-of-quotient`, `small-angle-division`, `acos-for-angle`. Findings carry
152
+ file, line, function and the source line; the report ends with the functions that do the most
153
+ elementary math. `--report` writes Markdown, `--rules` filters, `--fail-on` gates CI,
154
+ `--include-tests` widens the walk. On kornia main the four `small-angle-division` and
155
+ `one-minus-cos` lines are the ones kornia #4897 fixes.
156
+ - `ulpwise scan --run`: imports the math-heavy module level functions and calls them on the same
157
+ grid in float32 and float64, reporting the largest error in ulps of the largest output and the
158
+ worst elementwise ulp distance with its input. Static methods run, instance methods and other
159
+ skips carry their reason. `--run-limit` bounds it, `--run-installed` imports the installed package
160
+ instead of the scanned tree.
161
+ - `ulp_distance`, `ulp_distances`, `ordered`, `max_ulp` and `assert_max_ulp` take `f16` and
162
+ `bf16`, and `flatten` reads the dtype off float16 and bfloat16 numpy arrays and torch tensors.
163
+ Inputs that are not representable are rounded to nearest even first, so a bfloat16 tensor can be
164
+ measured against a float64 reference; without an explicit dtype the less precise of the two
165
+ inputs decides. Cross-checked against the numpy int16 view for float16 and the torch view for
166
+ bfloat16.
167
+ - `special("f16")` and `special("bf16")`: the same 29 named edge values as f32 and f64, so
168
+ `edge_values`, the `edge_f16` and `edge_bf16` pytest fixtures and `ulpwise special f16` work.
169
+ Every value satisfies the same checks as the f32 and f64 tables, run through numpy for float16
170
+ and torch for bfloat16.
171
+ - `ulpwise corpus`: runs the regression corpus against the installed packages without pytest and
172
+ prints one line per case, present, fixed or skipped, with the installed version and the upstream
173
+ reference. `--repo` filters by repository or case id, `--fail-if-present` makes a present bug exit 1.
174
+ Any exception from a repro counts as present, like the pytest run: several corpus bugs are crashes.
175
+
176
+ ## 0.2.0 (2026-09-26)
177
+
178
+ - `ulpwise survey` (`ulpwise.survey`): accuracy survey of 61 elementary and special functions of
179
+ torch, numpy, scipy and jax against a 200 bit mpmath reference, in ulps of the dtype, with the
180
+ worst input per row. For torch it also counts inputs where the vectorized kernel and the scalar
181
+ tail disagree and inputs that would fail the `OpInfo` reference test tolerance, default and per op
182
+ override, read from `op_db`. Writes `results.csv` and `results.md`. Optional extra
183
+ `ulpwise[survey]` pulls in mpmath.
184
+ - `studies/accuracy-survey-2026-09`: the first run and its reading notes.
185
+ - Corpus: `max_ulp` check type, and five open cases with the complete patch attached to the issue:
186
+ pytorch #198448 (`torch.sqrt` float64 rounding), pytorch #198583 (Bessel and Airy `p1evl` leading
187
+ 1), kornia #4838 and #4897 (small angle series), torchvision #9676 (rotated box clamp). Open cases
188
+ carry `issue`, `pr: null` and `fixed_in_release: null` and stay expected failures until a release
189
+ contains the fix.
190
+ - Corpus: three more open cases, pytorch #198663 (`polygamma(1, x)`: float64 series truncation and
191
+ float32 reflection argument) and pytorch #198664 (`erfcx` negative branch, `exp` at the rounded
192
+ square), both with the complete patch attached to the issue.
193
+ - Corpus: kornia #4768, `ellipse_to_laf` described the wrong ellipse whenever `b != 0`.
194
+ - Corpus: ultralytics #26330, OBB datasets with plain box labels are rejected at load time; the repro
195
+ writes a one-image dataset to a temporary directory, so it needs ultralytics but no weights.
196
+
197
+ ## 0.1.2 (2026-09-24)
198
+
199
+ - `ulpwise` console script, so `uvx ulpwise midpoint sqrt 0.85`, `pipx run ulpwise ...` and a plain
200
+ `ulpwise ...` inside a virtualenv work without `python -m`.
201
+
202
+ ## 0.1.1 (2026-09-24)
203
+
204
+ No code changes. The AI disclosure in the README was shortened and the package was republished
205
+ from a repository with a fresh history.
206
+
207
+ ## 0.1.0 (2026-09-24)
208
+
209
+ First release.
210
+
211
+ - Rust core: ordered float views and ulp distances (`ulp`), named edge values computed from the
212
+ format (`edge`), exact rounding oracles for `sqrt`, reciprocal and division with correctly rounded
213
+ results that do not depend on the platform libm, f64 referenced midpoint reports for 15 more
214
+ `f32` functions (`exact`), and knife-edge scans (`knife`).
215
+ - Python package built with maturin: `ulp_distance`, `assert_max_ulp` and `max_ulp` for floats,
216
+ lists, numpy arrays and torch tensors, `special`, `neighbours`, `binade_edges`, `all_floats`,
217
+ `midpoint`, `knife_edges`, `sqrt_cr`, a CLI (`python -m ulpwise`) and a pytest plugin
218
+ (`edge_f32` / `edge_f64` parametrization, `assert_max_ulp` fixture).
219
+ - Regression corpus of 11 upstream bugs with runnable repros (kornia, pytorch, pytorch/rl, timm,
220
+ peft), run by `tests/test_corpus.py` against whatever is installed.
221
+ - `examples/torch_sqrt_conformance.py`: measures how often a platform's `torch.sqrt` and
222
+ `numpy.sqrt` disagree with correct rounding at knife-edge inputs.
@@ -37,9 +37,9 @@ dependencies = [
37
37
 
38
38
  [[package]]
39
39
  name = "pyo3"
40
- version = "0.29.2"
40
+ version = "0.29.3"
41
41
  source = "registry+https://github.com/rust-lang/crates.io-index"
42
- checksum = "4688ddedf473e32662b9b067670129a8afb8c18e351482c70d62ba4a88171e8b"
42
+ checksum = "700d18fa267b73b9b521fd7e13580e2f446916f176cacee1ab63fcc8191f1655"
43
43
  dependencies = [
44
44
  "libc",
45
45
  "once_cell",
@@ -51,18 +51,18 @@ dependencies = [
51
51
 
52
52
  [[package]]
53
53
  name = "pyo3-build-config"
54
- version = "0.29.2"
54
+ version = "0.29.3"
55
55
  source = "registry+https://github.com/rust-lang/crates.io-index"
56
- checksum = "f41027e41b4bd03f6e60f9f417fe24a6341a6bb744edd62b6f709f2a52ea30e9"
56
+ checksum = "7b3fc0c4d08f6bb10e71fe39dfb9e2f59c6eb6854e22ec8092f50c69a4499adb"
57
57
  dependencies = [
58
58
  "target-lexicon",
59
59
  ]
60
60
 
61
61
  [[package]]
62
62
  name = "pyo3-ffi"
63
- version = "0.29.2"
63
+ version = "0.29.3"
64
64
  source = "registry+https://github.com/rust-lang/crates.io-index"
65
- checksum = "e591a95526fead067432c3b3a33fc74770b87b1e04e73671090d9c2055a2b327"
65
+ checksum = "dfc0b8e19df29aad7086cf977bb0c2a2f143e30567eb113e9cf72b62ca698330"
66
66
  dependencies = [
67
67
  "libc",
68
68
  "pyo3-build-config",
@@ -70,9 +70,9 @@ dependencies = [
70
70
 
71
71
  [[package]]
72
72
  name = "pyo3-macros"
73
- version = "0.29.2"
73
+ version = "0.29.3"
74
74
  source = "registry+https://github.com/rust-lang/crates.io-index"
75
- checksum = "73225868fc1cd84eef2c3c230ddb91273bf1de46aeb8a4248da76d32a0924a1c"
75
+ checksum = "6100e8a4b5eba53afaa5ed078364851a0b2499c553a44c31026b929049b49dc6"
76
76
  dependencies = [
77
77
  "proc-macro2",
78
78
  "pyo3-macros-backend",
@@ -82,9 +82,9 @@ dependencies = [
82
82
 
83
83
  [[package]]
84
84
  name = "pyo3-macros-backend"
85
- version = "0.29.2"
85
+ version = "0.29.3"
86
86
  source = "registry+https://github.com/rust-lang/crates.io-index"
87
- checksum = "571575aa3749fa6216757dd47d2a3e7ef360f329a40f0666a9fbd14889024952"
87
+ checksum = "6143877a16e82b5a727b7127ff4cd86858a24a28d745d72f43e6f227c7b1bdb3"
88
88
  dependencies = [
89
89
  "heck",
90
90
  "proc-macro2",
@@ -120,7 +120,7 @@ checksum = "adb6935a6f5c20170eeceb1a3835a49e12e19d792f6dd344ccc76a985ca5a6ca"
120
120
 
121
121
  [[package]]
122
122
  name = "ulpwise"
123
- version = "0.3.0"
123
+ version = "0.3.1"
124
124
  dependencies = [
125
125
  "pyo3",
126
126
  ]
@@ -1,6 +1,6 @@
1
1
  [package]
2
2
  name = "ulpwise"
3
- version = "0.3.0"
3
+ version = "0.3.1"
4
4
  edition = "2021"
5
5
  rust-version = "1.86"
6
6
  license = "MIT OR Apache-2.0"
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: ulpwise
3
- Version: 0.3.0
3
+ Version: 0.3.1
4
4
  Classifier: Development Status :: 3 - Alpha
5
5
  Classifier: Framework :: Pytest
6
6
  Classifier: Intended Audience :: Developers
@@ -11,6 +11,7 @@ Classifier: Topic :: Software Development :: Testing
11
11
  Classifier: Topic :: Scientific/Engineering :: Mathematics
12
12
  Requires-Dist: numpy ; extra == 'survey'
13
13
  Requires-Dist: mpmath>=1.3 ; extra == 'survey'
14
+ Requires-Dist: expecttest ; extra == 'survey'
14
15
  Requires-Dist: pytest>=7 ; extra == 'test'
15
16
  Requires-Dist: numpy ; extra == 'test'
16
17
  Provides-Extra: survey
@@ -123,6 +124,11 @@ ulpwise.assert_max_ulp(torch_out, reference, max_ulp_=2) # floats,
123
124
  dict(ulpwise.special("f32"))["square_underflows_to_zero"] # 2.6469779601696886e-23, the largest x with x * x == 0
124
125
  dict(ulpwise.special("bf16"))["square_overflows"] # 1.8446744073709552e+19, 2 ** 64
125
126
 
127
+ # the floats around a value in any of the four dtypes
128
+ ulpwise.next_up(1.0, "bf16") # 1.0078125
129
+ ulpwise.spacing(65504.0, "f16") # 32.0, the ulp of the largest float16
130
+ ulpwise.neighbours(1.0, 2, "f16") # [0.99902, 0.99951, 1.0, 1.00098, 1.00195]
131
+
126
132
  # exact placement of a result relative to its rounding midpoint
127
133
  ulpwise.midpoint("sqrt", 0.8528626561164856, "f32") # (0.9235056042671204, 0.000377, True, False)
128
134
  ulpwise.midpoint("div", 1.0, "f64", 3.0) # (0.3333333333333333, 0.1666..., True, False)
@@ -160,7 +166,15 @@ whose packages are installed; a case is an expected failure while the installed
160
166
  known to contain the fix, so the run tells you which bugs are present in your environment.
161
167
  `ulpwise corpus` does the same without pytest, one line per case with the installed version and
162
168
  `present`, `fixed` or `skipped`; `ulpwise corpus --repo pytorch --fail-if-present` is the CI gate
163
- form.
169
+ form. `ULPWISE_CORPUS_STRICT=1 pytest tests/test_corpus.py` also fails on an unexpected pass, which
170
+ is how the weekly scheduled run of this repository (and a manual `workflow_dispatch` run) notices
171
+ that a release fixed a case before `fixed_in_release` says so. To run the corpus against an older release without touching your
172
+ environment, side-install it and put it first on the path:
173
+
174
+ ```sh
175
+ pip install --no-deps --target /tmp/kornia-0.8.3 kornia==0.8.3
176
+ PYTHONPATH=/tmp/kornia-0.8.3 ulpwise corpus --repo kornia # 31 present, 0 fixed on kornia 0.8.3
177
+ ```
164
178
 
165
179
  | case | kind | merged |
166
180
  |---|---|---|
@@ -177,18 +191,48 @@ form.
177
191
  | timm #2792 Kron `__setstate__` shadowed | crash | 2026-09-18 |
178
192
  | peft #3777 pointwise Conv3d took the conv2d 1x1 shortcut | shape | 2026-09-21 |
179
193
  | ultralytics #26330 OBB train and val on plain box labels crashed in the validator or the loss instead of at load time | crash | 2026-09-25 |
194
+ | ultralytics #26379 `scale_masks` kept a padded row when the letterbox padding was odd, the bottom of every mask faded to 0 | rounding | 2026-09-28 |
195
+ | ultralytics #26377 `verify_image_label` dropped a polygon that shared its class and box with another | aliasing | 2026-09-28 |
196
+ | ultralytics #26357 `verify_image_label` read a pose row as a polygon for a detect task and dropped the image as corrupt | crash | 2026-09-27 |
197
+ | kornia #4941 `So3.log`, the `So3` Jacobians and `Se3.exp/log` lost all digits for small angles (issue #4897) | series | 2026-09-26 |
198
+ | kornia #5124 `axis_angle_to_rotation_matrix` dropped the `theta^2` terms below 1e-3 rad (issue #4838) | series | 2026-09-30 |
199
+ | kornia #4960 `Se2.exp` and `Se2.log` lost the translation at small angles | cancellation | 2026-09-26 |
200
+ | kornia #4975 `point_line_distance` ignored the weight of a homogeneous point | scale | 2026-09-27 |
201
+ | kornia #5003 `Quaternion.__pow__` returned the zero quaternion on the negative real axis | branch cut | 2026-09-27 |
202
+ | kornia #5005 `So2` from a `(B, 1)` angle rotated `(B, 2)` points into `(B, B, 2)` | shape | 2026-09-27 |
203
+ | kornia #5024 `solve_cubic` lost the root of a cubic with a tiny leading coefficient | scale | 2026-09-28 |
204
+ | kornia #5111 `RgbToGrayscale` turned a uint8 image into zeros | dtype | 2026-09-30 |
205
+ | kornia #5134 `conv_soft_argmax2d` shifted every window's exponent by the maximum of the whole map | normalisation | 2026-09-30 |
206
+ | kornia #5376 `get_gaussian_discrete_kernel1d(1, sigma)` returned 3 taps | shape | 2026-10-03 |
207
+ | kornia #4963 `Se3.exp` at `omega = 0`: `d t / d omega` was nan, then zero, instead of `-0.5 [upsilon]_x` | gradient | 2026-09-26 |
208
+ | kornia #4981 `Quaternion.polar_angle` had a nan gradient at the identity | gradient | 2026-09-27 |
209
+ | kornia #4998 `decompose_essential_matrix` added a batch dimension to a `(3, 3)` input | shape | 2026-09-27 |
210
+ | kornia #5058 `Hyperplane.through` had nan gradients for orthogonal edges of equal length | gradient | 2026-09-29 |
211
+ | kornia #5084 `Vector3.normalized` turned a float16 zero vector into NaN | dtype | 2026-09-29 |
212
+ | kornia #5182 `otsu_threshold(return_mask=True)` compared the thresholded image with 0 | sign | 2026-10-01 |
213
+ | kornia #4972 the Hessian of `So3.exp` at the identity was nan (issue #4966) | gradient | 2026-09-27 |
214
+ | kornia #5116 `sampson_epipolar_distance` scored an exact match `sqrt(eps)` and changed with the scale of `F` (issue #4881) | eps, scale | 2026-09-30 |
215
+ | kornia #5131 `RandomHue` shifted a float64 image by the float32 pi, 8.7e-8 past a half turn (issue #5127) | constant | 2026-09-30 |
216
+ | kornia #5143 `MS_SSIMLoss` built an even window for sigma 1.3 and lost a row and a column (issue #5126) | shape | 2026-10-01 |
217
+ | kornia #5353 `MS_SSIMLoss` raised on a uint8 image (issue #5351) | crash | 2026-10-02 |
218
+ | kornia #4967 `So3.right_jacobian` in float16 was off by 0.79 above 41 rad (issue #4965) | overflow | 2026-09-26 |
219
+ | kornia #4980 `average_quaternions` counted a member stored as `3 q` nine times (issue #4974) | normalisation | 2026-09-27 |
220
+ | kornia #5104 `Hyperplane.through` flipped the normal of a large float16 triangle | overflow | 2026-09-29 |
221
+ | kornia #5301 `filter2d` with one kernel per sample raised on a channels-last input (issue #5292) | crash | 2026-10-02 |
222
+ | kornia #5303 `lovasz_hinge_loss` returned a float32 loss for a float16 prediction (issue #5289) | dtype | 2026-10-02 |
223
+ | kornia #5357 `get_box_kernel1d` returned a stride-0 view, editing one tap rewrote them all (issue #5160) | aliasing | 2026-10-02 |
180
224
  | pytorch #198448 `torch.sqrt` float64 not correctly rounded at 27 of 64 knife edges | rounding | open |
181
225
  | pytorch #198583 `bessel_j0/j1/y0/y1`, `airy_ai` float64 lose up to 12 digits (`p1evl` leading 1) | digits | open |
182
- | kornia #4838 `axis_angle_to_rotation_matrix` drops the `theta^2` terms below 1e-3 rad | series | open |
183
- | kornia #4897 `So3.log`, the `So3` Jacobians and `Se3.exp/log` lose all digits for small angles | series | open |
184
226
  | torchvision #9676 `clamp_bounding_boxes` collapses slightly tilted rotated boxes to a point | geometry | open |
185
227
  | pytorch #198663 `polygamma(1, x)` float64 keeps 9 digits (series stops at `1/42`), float32 loses all for large negative `x` | truncation, rounding | open |
186
228
  | pytorch #198664 `erfcx` off by `x*x/2` ulps for negative `x` (`exp` at the rounded square) | rounding | open |
187
229
 
188
- Cases marked `open` have an issue with the complete patch attached and no merged fix yet; they are
189
- expected failures until a release contains the fix (`fixed_in_release` in `cases.json`), and the
190
- `max_ulp` check type measures the digits directly. The ultralytics case builds its one-image dataset
191
- in a temporary directory and needs no weights; two more ultralytics fixes (#26240, #26246) are not in
230
+ Cases marked `open` have an issue with the complete patch attached and no merged fix yet. Every case
231
+ is an expected failure until the installed release contains the fix (`fixed_in_release` in
232
+ `cases.json`; the timm and ultralytics fixes above are released, the kornia, pytorch/rl, peft and
233
+ pytorch ones are merged but not in a release yet), and the `max_ulp` check type measures the digits
234
+ directly. The ultralytics dataset and label cases build their one-image
235
+ dataset in a temporary directory and need no weights; two more ultralytics fixes (#26240, #26246) are not in
192
236
  the corpus yet because their repros need model weights or the COCO evaluator.
193
237
 
194
238
  ## Repository scan
@@ -208,7 +252,7 @@ overflows, the usual replacement and, where one exists, the upstream bug it come
208
252
  | `exp-of-square` | high | `exp(x * x)`, `exp(x ** 2)`, `(-x.pow(2)).exp()`: the rounding error of the square is multiplied by `x * x / 2` ulps (pytorch #198664) |
209
253
  | `sin-of-pi-times` | high | `sin(pi * x)`, `cos(pi * x)`: the product is rounded before the argument reduction (pytorch #198663) |
210
254
  | `softplus-by-hand` | high | `log(1 + exp(x))` |
211
- | `logsumexp-by-hand` | high | `log(exp(a) + exp(b))`, `log(sum(exp(x)))` |
255
+ | `logsumexp-by-hand` | high | `log(exp(a) + exp(b))`, `log(sum(exp(x)))`, `log(exp(x).sum(-1))` |
212
256
  | `hypot-by-hand` | high | `sqrt(a * a + b * b)` |
213
257
  | `sqrt-of-difference` | medium | `sqrt(a - b)` |
214
258
  | `one-minus-cos` | medium | `1 - cos(x)` (kornia #4897) |
@@ -260,16 +304,23 @@ the reading list is where that judgement happens.
260
304
  pip install 'ulpwise[survey]' torch scipy jax # mpmath is the reference, the rest are backends
261
305
  ulpwise survey --out survey # results.csv and results.md, about 3 minutes
262
306
  ulpwise survey --functions bessel_j0,polygamma_1 --backends torch,scipy --dtypes f64 --points 2000
307
+ ulpwise survey --functions erf,exp --backends torch --dtypes f16,bf16 # half precision rows
263
308
  ```
264
309
 
265
310
  For every function in `ulpwise.survey.REGISTRY` (exp, log, trig and hyperbolic functions, erf and
266
311
  friends, gamma family, torch.special Bessel and Airy functions, the activation functions), every
267
- dtype and every installed backend, the survey evaluates a log spaced grid over the function's domain
312
+ dtype (`f64`, `f32`, `f16` and `bf16`; the default is `f32,f64`) and every installed backend, the
313
+ survey evaluates a log spaced grid over the function's domain, clipped to the finite range of the dtype,
268
314
  plus the named edge values of the dtype, computes the exact value with mpmath at the rounded input,
269
315
  and reports max, p99 and median error in ulps, the fraction of inputs beyond 1 and 10 ulps, non
270
316
  finite mismatches and the worst input. For torch it also reports how many inputs the vectorized
271
317
  kernel and the scalar tail disagree on, and how many inputs would fail torch's reference test under
272
- the dtype default tolerance and under the op's `OpInfo` override, read from `op_db`.
318
+ the dtype default tolerance and under the op's `OpInfo` override, read from `op_db`. Reading `op_db` needs
319
+ `expecttest`, a test-only dependency of torch that the `survey` extra installs; without it the survey logs
320
+ one line and uses the default. torch and jax have `f16` and `bf16` rows; numpy has no bfloat16 and
321
+ scipy.special computes a float16 input in float32, so neither has `bf16` rows and scipy has no `f16` rows.
322
+ A backend without a kernel for a function in a dtype (torch's Bessel functions in half precision) is
323
+ logged and skipped.
273
324
 
274
325
  [`studies/accuracy-survey-2026-09`](https://github.com/Nicholas022400701/ulpwise/blob/main/studies/accuracy-survey-2026-09/README.md) is the first run
275
326
  (torch 2.14.0+cpu, numpy 2.2.6, scipy 1.18.1, jax 0.11.2, Linux x86_64 AVX512). The short version:
@@ -307,10 +358,10 @@ cross-checks both against `fractions.Fraction` and `decimal.Decimal` at 80 digit
307
358
  docstring and the checks for the shapes, and an mpmath reference for the scalar functions.
308
359
  - Mutation scoring for numerical tests: single token mutants of the code under test (`abs`, a
309
360
  dropped `sqrt`, `/ 4` for `/ 16`) run against the test suite, reporting which survive.
310
- - `float16` and `bfloat16` `spacing`, `next_up`, knife edges and exact oracles (ulp distances and edge values are done).
361
+ - `float16` and `bfloat16` knife edges and exact oracles (ulp distances, edge values, `spacing` and the neighbour functions are done).
311
362
  - Exact references for transcendental functions in Rust (correctly rounded `exp`, `log`, ...) so
312
363
  the `f64` knife-edge scans do not need mpmath.
313
- - Survey backends for CUDA and MPS, and `float16` / `bfloat16` rows.
364
+ - Survey backends for CUDA and MPS.
314
365
  - Zero copy paths for numpy arrays and torch tensors.
315
366
  - More corpus entries, with fixtures for the cases that need data.
316
367