ulpwise 0.3.0__tar.gz → 0.3.2__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 (41) hide show
  1. ulpwise-0.3.2/.github/dependabot.yml +23 -0
  2. {ulpwise-0.3.0 → ulpwise-0.3.2}/.github/workflows/ci.yml +16 -9
  3. {ulpwise-0.3.0 → ulpwise-0.3.2}/.github/workflows/release.yml +31 -10
  4. {ulpwise-0.3.0 → ulpwise-0.3.2}/.gitignore +1 -0
  5. ulpwise-0.3.2/CHANGELOG.md +245 -0
  6. {ulpwise-0.3.0 → ulpwise-0.3.2}/Cargo.lock +11 -11
  7. {ulpwise-0.3.0 → ulpwise-0.3.2}/Cargo.toml +1 -1
  8. {ulpwise-0.3.0 → ulpwise-0.3.2}/PKG-INFO +82 -14
  9. {ulpwise-0.3.0 → ulpwise-0.3.2}/README.md +80 -13
  10. {ulpwise-0.3.0 → ulpwise-0.3.2}/pyproject.toml +3 -2
  11. {ulpwise-0.3.0 → ulpwise-0.3.2}/python/ulpwise/__init__.py +115 -8
  12. {ulpwise-0.3.0 → ulpwise-0.3.2}/python/ulpwise/__main__.py +1 -1
  13. ulpwise-0.3.2/python/ulpwise/corpus/cases.json +1108 -0
  14. {ulpwise-0.3.0 → ulpwise-0.3.2}/python/ulpwise/scan.py +20 -7
  15. {ulpwise-0.3.0 → ulpwise-0.3.2}/python/ulpwise/survey.py +155 -50
  16. {ulpwise-0.3.0 → ulpwise-0.3.2}/studies/accuracy-survey-2026-09/README.md +13 -1
  17. {ulpwise-0.3.0 → ulpwise-0.3.2}/studies/accuracy-survey-2026-09/results.csv +2 -2
  18. {ulpwise-0.3.0 → ulpwise-0.3.2}/studies/accuracy-survey-2026-09/results.md +2 -2
  19. ulpwise-0.3.2/studies/accuracy-survey-2026-10-half/README.md +133 -0
  20. ulpwise-0.3.2/studies/accuracy-survey-2026-10-half/results.csv +91 -0
  21. ulpwise-0.3.2/studies/accuracy-survey-2026-10-half/results.md +105 -0
  22. {ulpwise-0.3.0 → ulpwise-0.3.2}/tests/test_core.py +64 -0
  23. {ulpwise-0.3.0 → ulpwise-0.3.2}/tests/test_corpus.py +23 -4
  24. {ulpwise-0.3.0 → ulpwise-0.3.2}/tests/test_scan.py +71 -0
  25. ulpwise-0.3.2/tests/test_survey.py +684 -0
  26. ulpwise-0.3.0/CHANGELOG.md +0 -80
  27. ulpwise-0.3.0/python/ulpwise/corpus/cases.json +0 -437
  28. ulpwise-0.3.0/tests/test_survey.py +0 -53
  29. {ulpwise-0.3.0 → ulpwise-0.3.2}/CONTRIBUTING.md +0 -0
  30. {ulpwise-0.3.0 → ulpwise-0.3.2}/LICENSE-APACHE +0 -0
  31. {ulpwise-0.3.0 → ulpwise-0.3.2}/LICENSE-MIT +0 -0
  32. {ulpwise-0.3.0 → ulpwise-0.3.2}/examples/sqrt_knife_values.py +0 -0
  33. {ulpwise-0.3.0 → ulpwise-0.3.2}/examples/torch_sqrt_conformance.py +0 -0
  34. {ulpwise-0.3.0 → ulpwise-0.3.2}/python/ulpwise/corpus/__init__.py +0 -0
  35. {ulpwise-0.3.0 → ulpwise-0.3.2}/python/ulpwise/plugin.py +0 -0
  36. {ulpwise-0.3.0 → ulpwise-0.3.2}/src/edge.rs +0 -0
  37. {ulpwise-0.3.0 → ulpwise-0.3.2}/src/exact.rs +0 -0
  38. {ulpwise-0.3.0 → ulpwise-0.3.2}/src/knife.rs +0 -0
  39. {ulpwise-0.3.0 → ulpwise-0.3.2}/src/lib.rs +0 -0
  40. {ulpwise-0.3.0 → ulpwise-0.3.2}/src/py.rs +0 -0
  41. {ulpwise-0.3.0 → ulpwise-0.3.2}/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,245 @@
1
+ # Changelog
2
+
3
+ ## 0.3.2 (2026-10-06)
4
+
5
+ - Two regression corpus cases for pytorch #199850: `torch.erf` in bfloat16 and float16 on CPU returns 0 at and
6
+ below 1.8e-7 and loses relative accuracy below 1e-3 (13404 bfloat16 ulps, 5 float16 ulps at worst), found with
7
+ the half precision rows of `ulpwise survey --functions erf --backends torch --dtypes f16,bf16`.
8
+ - Two regression corpus cases for pytorch #199867: `torch.special.logit` in float16 and bfloat16 on CPU rounds `1 - x`
9
+ and `x / (1 - x)` to the input dtype before the log, so logit(0.499756) in float16 is -0.000488 for an exact -0.000977
10
+ (512 ulp, 64 bfloat16 ulp at worst), found with `ulpwise survey --functions logit --backends torch --dtypes f16,bf16`.
11
+ - `studies/accuracy-survey-2026-10-half`: the second accuracy survey, torch 2.14.0+cpu in float16 and bfloat16 for the
12
+ 45 functions with half precision CPU kernels. 33 float16 and 34 bfloat16 rows of 45 are correctly rounded; `erf`
13
+ (pytorch #199850) and `logit` (pytorch #199867) lose their digits before the rounding step, `polygamma(2, x)` in
14
+ float16 is off at every half integer in `(-1024, -256)` because the Hurwitz zeta sum accumulates in float for the
15
+ reduced types, and the activation tails and the `rsqrt` and `i0e` vector versus scalar disagreements of September
16
+ show again.
17
+ - `ulpwise survey`: the `polygamma_1` and `polygamma_2` references at a non positive integer are now the signed
18
+ infinity of the pole, `+inf` for an odd order (the limit from both sides) and `-inf` for an even one (the sign of
19
+ `(-1) ** (n + 1) * n! * zeta(n + 1, x)`, which is what scipy, torch and jax return), instead of a domain error
20
+ that expected `nan`. The f16 and bf16 grids reach the integers from 2048 and 256 up, so every negative point
21
+ past there was a pole and counted as a nonfinite mismatch: on torch 2.14 the two functions had 354 of 3546
22
+ float16 points and 492 of 4016 bfloat16 points each, `polygamma_2` has 0 now in every dtype and `polygamma_1`
23
+ keeps 491 bfloat16, 4 float32 and 2 float16 points where torch returns a large finite value instead of `inf`
24
+ at a negative integer (pytorch #198663).
25
+
26
+ ## 0.3.1 (2026-10-05)
27
+
28
+ - `ulpwise survey` takes `f16` and `bf16` in `--dtypes` (the default stays `f32,f64`): torch and jax get half
29
+ precision rows, measured in float16 or bfloat16 ulps against the same mpmath reference; numpy has no
30
+ bfloat16 and scipy.special computes a float16 input in float32, so those two have no `bf16` rows and scipy
31
+ has no `f16` rows. A backend without a kernel for a function in a dtype (torch's Bessel and Airy functions,
32
+ `erfcx`, `ndtri`, `log_ndtr` and `zeta` in half precision) is logged and skipped before the reference is
33
+ computed. The torch default tolerances of the two dtypes are `(1.6e-2, 1e-5)` and `(1e-3, 1e-5)`, as in
34
+ `torch.testing`. A log spaced domain wider than the dtype is now clipped to the dtype's finite range before
35
+ the points are spaced: the float16 grid of `sqrt` and `log` over `(1e-300, 1e300)` kept 32 of 600 points and
36
+ `acosh` 24, they have about 570 and 490 now; the float32 grids of the same functions go from about 100 points
37
+ to about 600, the float64 grids are unchanged. The `(0, 1)` and `(-1, 1)` grids of `ndtri`, `logit`, `asin`,
38
+ `acos`, `atanh` and `erfinv` stop at the float below 1 in half precision too, where `1 - 1e-4` and `1 - 1e-3`
39
+ rounded to 1. Two corrections to the ulp metric for every dtype: the threshold from which an exact result
40
+ counts as a correct overflow to infinity was `fmax * (1 + eps / 2)`, about one ulp above the largest finite
41
+ value, and is now `fmax + ulp / 2`, the rounding threshold; and the spacing at the largest finite value was
42
+ numpy's `inf`, which turned every finite result there into a 0 ulp error, it is `ulpwise.spacing` now.
43
+ An unknown dtype raises `ValueError` instead of a `KeyError` from the grid.
44
+ - `next_up`, `next_down`, `spacing`, `neighbours`, `binade_edges` and `all_floats` accept `f16` and `bf16` (and the
45
+ `float16`, `half`, `bfloat16`, `torch.float16`, `torch.bfloat16` aliases) like `ulp_distance` and `special` already
46
+ did; they raised `unsupported dtype` before. The 16 bit versions are pure Python on the bit patterns, with the
47
+ conventions of the Rust ones: the largest float steps to infinity, both zeros to the smallest subnormal, `spacing`
48
+ is the distance from `|x|` to the next float and stays finite at the largest float, `neighbours` returns finite
49
+ values only. A test walks every finite float16 and bfloat16 and checks the six functions against each other, against
50
+ numpy's float16 `nextafter` and `spacing` and against torch's bfloat16 `nextafter`.
51
+ - `ulpwise scan` read the first positional argument of a method call as the value the method is applied
52
+ to, so `log(exp(x).sum(-1))` was not a `logsumexp-by-hand` finding while `log(exp(x).sum())` and
53
+ `log(exp(x).sum(dim=-1))` were. A method call whose first argument is a dimension (an integer, `None`
54
+ or a tuple of them) is now read as applying to its receiver, like the argument-less form.
55
+ - `ulpwise scan` printed a `SyntaxWarning` (a `DeprecationWarning` before Python 3.12) for every invalid
56
+ escape sequence in the files it scanned, four lines on kornia. The parse now ignores those warnings;
57
+ a file that does not parse is skipped as before.
58
+ - `ulpwise scan` reported Windows paths with backslashes (`kornia\geometry\conversions.py`), so the same
59
+ report read differently from the one made on Linux or macOS. Paths are now written with forward
60
+ slashes on every platform; `--run` still imports the module from either form. The CI pytest job on
61
+ ubuntu, macOS and windows runs `tests/test_scan.py` now, and that file also covers the reasons
62
+ `--run` gives for a function it does not run (a class, a constant, an instance method, a builtin
63
+ without a signature, a nested name, a module that fails to import) and the ulp comparison of the
64
+ results across infinities, NaNs and an all zero reference.
65
+ - `ulpwise survey` without scipy installed died at `ndtri` with a `ModuleNotFoundError` and lost every
66
+ row computed before it: the reference started its Newton iteration from `scipy.special.ndtri`. It now
67
+ starts from a bisection on `math.erfc`, returns an exact zero at `p = 0.5` (a Newton step there leaves a
68
+ rounding residual, which the ulp metric would read as 1e16 ulps against the exact 0.0 of torch and scipy)
69
+ and gives the same rows as before on the 600 point float64 and float32 grids. The survey also evaluates
70
+ the reference only when some requested backend implements the function; a numpy only run no longer
71
+ spends its time on references for the torch and scipy only entries. The pytest job on ubuntu, macOS and
72
+ windows (Python 3.12 and 3.9) runs `tests/test_survey.py` now, with numpy as the only backend.
73
+ - `ulpwise survey` printed a numpy `RuntimeWarning` for every backend call that divided by zero or
74
+ overflowed at the edge values of the grid (`reciprocal` and `log` at 0, `reciprocal` at the overflow
75
+ edge), hundreds of lines on a full run. Those points are counted in the `nonfinite` column already, so
76
+ the survey now evaluates the backends under `np.errstate(all="ignore")`, and a test runs `reciprocal` and
77
+ `log` with warnings turned into errors.
78
+ - The README said that two kornia fixes in the corpus were merged but not released; the corpus has 31
79
+ kornia cases now, and the pytorch/rl, peft and pytorch (#198006) fixes are in the same state (kornia
80
+ 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
81
+ changes), while the timm and ultralytics fixes are released. The sentence says so now, and
82
+ `tests/test_corpus.py` checks that the README table has one row per case with the merge date from
83
+ `cases.json`, so a case added without its row, or a row with a stale date, fails the suite.
84
+ - `tests/test_survey.py` checks every reference of the survey registry against the libraries it measures:
85
+ each entry is surveyed on a 24 point float64 grid against torch, numpy and scipy and its median error must
86
+ stay under 32 ulp, which a reference that is another function fails by fifteen orders of magnitude (torch's
87
+ `polygamma_1` is the worst true median at 9 ulp), and the piecewise references (`selu`, `elu`, `entr`,
88
+ `gelu_tanh`, `log_ndtr`, `lgamma`, `digamma`, `erfinv`, `ndtri`, `zeta`, `sinc`, `spherical_bessel_j0`)
89
+ are pinned once per branch, since a slip in one branch moves a third of the grid and keeps the median.
90
+ `survey.py` goes from 77 to 94 percent covered.
91
+ - With torch installed but `expecttest` missing, `torch.testing._internal` does not import and
92
+ `ulpwise survey` silently used the dtype default as the `OpInfo` tolerance, so the `op tol` columns
93
+ looked like an override that was never read. The survey now logs one line with the import error and
94
+ the `survey` extra installs `expecttest`. The regression corpus CI job, which has torch but had no
95
+ `expecttest`, failed on the two `op_db` tests for the same reason; it installs `expecttest` now and the
96
+ tests skip with the reason where `op_db` is not importable.
97
+ - `ulpwise survey` looked up torch's `OpInfo` tolerance by op name and took the first `op_db` entry
98
+ with that name. `polygamma` has one entry per order, and the first, `polygamma_n_0`, has no
99
+ override, so the `op fail` column of `polygamma_1` and `polygamma_2` in float32 was computed with
100
+ the dtype default (`rtol 1.3e-6`, `atol 1e-5`) instead of torch's `polygamma_n_1` and `_n_2`
101
+ override (`rtol 0.01`, `atol 1e-4`). `Entry` gains `opinfo_variant`, `torch_opinfo_tolerance`
102
+ takes a `variant` and otherwise prefers the base variant, and the two registry entries name
103
+ theirs. In `studies/accuracy-survey-2026-09` the `polygamma_1` f32 row goes from 86 to 7 inputs
104
+ failing the op's tolerance; the measured errors are unchanged (erratum in its README).
105
+ - `tests/test_survey.py` covers the survey itself now: `survey()` on `sqrt` against numpy and torch
106
+ (numpy within half an ulp, torch under one), an inexact backend, a backend that raises or returns
107
+ the wrong shape, the backend resolution, the `op_db` tolerance lookup including the variant case,
108
+ the csv and markdown writers, `versions()`, `main()` and the `survey` subcommand. CI installs
109
+ `mpmath` for the Python job so these tests run there instead of being skipped.
110
+ - Six more kornia cases, each present on kornia 0.8.3 and fixed on main: the Hessian of `So3.exp`
111
+ at the identity (#4972, nine nan for `-I / 4` and zeros), `sampson_epipolar_distance` of a point on
112
+ its epiline with `squared=False` (#5116, `sqrt(eps)` = 1e-4 for 0) and of the same `F` scaled by
113
+ 1e-4 (#5116, a third lower), `RandomHue` on a float64 image (#5131, 8.7e-8 rad past a half turn,
114
+ the float32 rounding error of pi), `MS_SSIMLoss(sigmas=(0.5, 1.3), reduction="none")` (#5143, an
115
+ even 6-pixel window and a `(1, 15, 19)` map for a `(1, 3, 16, 20)` input) and `MS_SSIMLoss` on a
116
+ uint8 pair (#5353, `expected scalar type Byte but found Float`; with `data_range=255` it now scores
117
+ the pair divided by 255 at the default to 1e-6). Six more: `So3.right_jacobian` of a float16
118
+ 45 rad rotation (#4967, the identity for a matrix of 0.2 to 0.4), `average_quaternions` with a member
119
+ stored as `3 q` (#4980, 41.8 degrees for the 22.5 degree bisector), `Hyperplane.through` a float16
120
+ triangle with legs of 300 (#5104, the normal `(0, 0, 1)` for `(0, 0, -1)`), `filter2d` with a
121
+ per-sample kernel on a channels-last image (#5301, torch's view error), `lovasz_hinge_loss` of a
122
+ float16 prediction (#5303, a float32 loss) and `get_box_kernel1d` (#5357, a stride-0 view where
123
+ one write zeroed all three taps). The kornia block is 31 cases, all `present` on 0.8.3 and all
124
+ `fixed` on main. One more ultralytics case, `scale_masks` with the dataloader's `ratio_pad` and an
125
+ odd letterbox padding (#26379, a padded row survived the crop and the bottom rows of every mask
126
+ faded to 0) and `verify_image_label` on two triangles that tile one square (#26377, the second
127
+ polygon was dropped as a duplicate of the first because only the class and the box were compared);
128
+ both carry `fixed_in_release` 8.4.165, the first tag with the fixes), and `verify_image_label` on a
129
+ pose row for a detect task (#26357, the row was read as a polygon, its box was out of bounds and
130
+ the image was dropped as corrupt; `fixed_in_release` 8.4.164). The corpus is 50 cases.
131
+ - The manual `workflow_dispatch` run of CI is now strict about the corpus like the Monday run (the
132
+ changelog said so already, the workflow set the variable for `schedule` only).
133
+ - The release workflow's manual dry run (`workflow_dispatch` without `publish`) now ends in a
134
+ `collect` job that downloads the artifacts the way the publish job does and checks that there
135
+ are five wheels and one sdist, so a dry run covers the whole pipeline short of the upload. The
136
+ workflow actions moved to `actions/checkout@v7`, `setup-python@v7`, `upload-artifact@v7` and
137
+ `download-artifact@v8` (the first Dependabot pull requests), and `Cargo.lock` to pyo3 0.29.3.
138
+ - Dependabot (`.github/dependabot.yml`) opens weekly pull requests for Cargo.lock and the
139
+ workflow actions, so pyo3 patch releases land through CI-tested pull requests instead of a
140
+ hand-run `cargo update`; Python dependencies stay unpinned on purpose.
141
+ - Eight more kornia cases, each present on kornia 0.8.3 and fixed on main: `Se2.exp`/`Se2.log` at
142
+ theta = 1e-8 (#4960, the translation came back as (1, 2) from exp and (1e-8, -5e-9) from log),
143
+ `point_line_distance` of a homogeneous point with w = 2 (#4975, 4.0 for 1.5), `Quaternion.__pow__`
144
+ of -1 (#5003, the zero quaternion), `So2` from a `(B, 1)` angle times `(B, 2)` points (#5005,
145
+ `(B, B, 2)`), `solve_cubic` of `1e-30 x^3 + 2x - 6` (#5024, roots `[0, 0, 0]` for 3),
146
+ `RgbToGrayscale` on uint8 (#5111, all zeros), `conv_soft_argmax2d` with a far-away peak (#5134,
147
+ the weak window's coordinates moved from 0.8834 to 1.0) and `get_gaussian_discrete_kernel1d(1,
148
+ sigma)` (#5376, 3 taps). Six more in the same shape: `Se3.exp`'s `d t / d omega` at the identity
149
+ (#4963, nan on 0.8.3, zero on main before the fix), `Quaternion.polar_angle`'s gradient at the
150
+ identity (#4981, nan), `decompose_essential_matrix` of a `(3, 3)` input (#4998, `(1, 3, 3)`),
151
+ `Hyperplane.through`'s gradients for orthogonal equal-length edges (#5058, nan),
152
+ `Vector3.normalized` of a float16 zero vector (#5084, NaN) and `otsu_threshold`'s mask below
153
+ zero (#5182, all False). The whole kornia block of the corpus, 19 cases, reads `present` on 0.8.3;
154
+ the mean_average_precision recall thresholds (#5101) were checked and left out because 0.8.3
155
+ already scores the exact-tenth case right.
156
+ - CI runs the regression corpus every Monday (and on `workflow_dispatch`) with
157
+ `ULPWISE_CORPUS_STRICT=1`, which turns an unexpected pass into a failure: a release fixed a case
158
+ and `fixed_in_release` is stale. Push and pull request runs stay non strict. The corpus job also
159
+ installs `ultralytics`, so the ultralytics case runs there instead of being skipped.
160
+ - Corpus metadata: kornia #4838 is fixed by kornia #5124 (merged 2026-09-30) and kornia #4897 by
161
+ kornia #4941 (merged 2026-09-26); both cases now carry the `pr` and `merged_at`, and stay expected
162
+ failures because no kornia release after 0.9.0rc1 (2026-07-19) exists yet. ultralytics #26330 sets
163
+ `fixed_in_release` to `8.4.164`: the tag `v8.4.164` (2026-09-27, the first release after the merge)
164
+ carries the `use_obb` check in `verify_labels` and `v8.4.163` does not, so the case is a hard
165
+ failure, not an expected one, from 8.4.164 on. peft #3777 keeps `fixed_in_release: null`: the
166
+ `v0.21.1` and `v0.21.2` tags still carry the `weight.size()[2:4]` shortcut in `lora/layer.py`.
167
+
168
+ ## 0.3.0 (2026-09-26)
169
+
170
+ - `ulpwise scan`: static scan of a repository (a directory, a GitHub URL or `owner/repo`, cloned
171
+ with depth 1) for the floating point patterns behind the corpus bugs. Twelve rules with severity,
172
+ reason, replacement and upstream example: `exp-of-square`, `sin-of-pi-times`, `softplus-by-hand`,
173
+ `logsumexp-by-hand`, `hypot-by-hand`, `sqrt-of-difference`, `one-minus-cos`, `log1p-by-hand`,
174
+ `expm1-by-hand`, `atan-of-quotient`, `small-angle-division`, `acos-for-angle`. Findings carry
175
+ file, line, function and the source line; the report ends with the functions that do the most
176
+ elementary math. `--report` writes Markdown, `--rules` filters, `--fail-on` gates CI,
177
+ `--include-tests` widens the walk. On kornia main the four `small-angle-division` and
178
+ `one-minus-cos` lines are the ones kornia #4897 fixes.
179
+ - `ulpwise scan --run`: imports the math-heavy module level functions and calls them on the same
180
+ grid in float32 and float64, reporting the largest error in ulps of the largest output and the
181
+ worst elementwise ulp distance with its input. Static methods run, instance methods and other
182
+ skips carry their reason. `--run-limit` bounds it, `--run-installed` imports the installed package
183
+ instead of the scanned tree.
184
+ - `ulp_distance`, `ulp_distances`, `ordered`, `max_ulp` and `assert_max_ulp` take `f16` and
185
+ `bf16`, and `flatten` reads the dtype off float16 and bfloat16 numpy arrays and torch tensors.
186
+ Inputs that are not representable are rounded to nearest even first, so a bfloat16 tensor can be
187
+ measured against a float64 reference; without an explicit dtype the less precise of the two
188
+ inputs decides. Cross-checked against the numpy int16 view for float16 and the torch view for
189
+ bfloat16.
190
+ - `special("f16")` and `special("bf16")`: the same 29 named edge values as f32 and f64, so
191
+ `edge_values`, the `edge_f16` and `edge_bf16` pytest fixtures and `ulpwise special f16` work.
192
+ Every value satisfies the same checks as the f32 and f64 tables, run through numpy for float16
193
+ and torch for bfloat16.
194
+ - `ulpwise corpus`: runs the regression corpus against the installed packages without pytest and
195
+ prints one line per case, present, fixed or skipped, with the installed version and the upstream
196
+ reference. `--repo` filters by repository or case id, `--fail-if-present` makes a present bug exit 1.
197
+ Any exception from a repro counts as present, like the pytest run: several corpus bugs are crashes.
198
+
199
+ ## 0.2.0 (2026-09-26)
200
+
201
+ - `ulpwise survey` (`ulpwise.survey`): accuracy survey of 61 elementary and special functions of
202
+ torch, numpy, scipy and jax against a 200 bit mpmath reference, in ulps of the dtype, with the
203
+ worst input per row. For torch it also counts inputs where the vectorized kernel and the scalar
204
+ tail disagree and inputs that would fail the `OpInfo` reference test tolerance, default and per op
205
+ override, read from `op_db`. Writes `results.csv` and `results.md`. Optional extra
206
+ `ulpwise[survey]` pulls in mpmath.
207
+ - `studies/accuracy-survey-2026-09`: the first run and its reading notes.
208
+ - Corpus: `max_ulp` check type, and five open cases with the complete patch attached to the issue:
209
+ pytorch #198448 (`torch.sqrt` float64 rounding), pytorch #198583 (Bessel and Airy `p1evl` leading
210
+ 1), kornia #4838 and #4897 (small angle series), torchvision #9676 (rotated box clamp). Open cases
211
+ carry `issue`, `pr: null` and `fixed_in_release: null` and stay expected failures until a release
212
+ contains the fix.
213
+ - Corpus: three more open cases, pytorch #198663 (`polygamma(1, x)`: float64 series truncation and
214
+ float32 reflection argument) and pytorch #198664 (`erfcx` negative branch, `exp` at the rounded
215
+ square), both with the complete patch attached to the issue.
216
+ - Corpus: kornia #4768, `ellipse_to_laf` described the wrong ellipse whenever `b != 0`.
217
+ - Corpus: ultralytics #26330, OBB datasets with plain box labels are rejected at load time; the repro
218
+ writes a one-image dataset to a temporary directory, so it needs ultralytics but no weights.
219
+
220
+ ## 0.1.2 (2026-09-24)
221
+
222
+ - `ulpwise` console script, so `uvx ulpwise midpoint sqrt 0.85`, `pipx run ulpwise ...` and a plain
223
+ `ulpwise ...` inside a virtualenv work without `python -m`.
224
+
225
+ ## 0.1.1 (2026-09-24)
226
+
227
+ No code changes. The AI disclosure in the README was shortened and the package was republished
228
+ from a repository with a fresh history.
229
+
230
+ ## 0.1.0 (2026-09-24)
231
+
232
+ First release.
233
+
234
+ - Rust core: ordered float views and ulp distances (`ulp`), named edge values computed from the
235
+ format (`edge`), exact rounding oracles for `sqrt`, reciprocal and division with correctly rounded
236
+ results that do not depend on the platform libm, f64 referenced midpoint reports for 15 more
237
+ `f32` functions (`exact`), and knife-edge scans (`knife`).
238
+ - Python package built with maturin: `ulp_distance`, `assert_max_ulp` and `max_ulp` for floats,
239
+ lists, numpy arrays and torch tensors, `special`, `neighbours`, `binade_edges`, `all_floats`,
240
+ `midpoint`, `knife_edges`, `sqrt_cr`, a CLI (`python -m ulpwise`) and a pytest plugin
241
+ (`edge_f32` / `edge_f64` parametrization, `assert_max_ulp` fixture).
242
+ - Regression corpus of 11 upstream bugs with runnable repros (kornia, pytorch, pytorch/rl, timm,
243
+ peft), run by `tests/test_corpus.py` against whatever is installed.
244
+ - `examples/torch_sqrt_conformance.py`: measures how often a platform's `torch.sqrt` and
245
+ `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.2"
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.2"
4
4
  edition = "2021"
5
5
  rust-version = "1.86"
6
6
  license = "MIT OR Apache-2.0"