ulpwise 0.2.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 (42) hide show
  1. ulpwise-0.3.1/.github/dependabot.yml +23 -0
  2. {ulpwise-0.2.0 → ulpwise-0.3.1}/.github/workflows/ci.yml +16 -9
  3. {ulpwise-0.2.0 → ulpwise-0.3.1}/.github/workflows/release.yml +48 -9
  4. {ulpwise-0.2.0 → ulpwise-0.3.1}/.gitignore +1 -0
  5. ulpwise-0.3.1/CHANGELOG.md +222 -0
  6. {ulpwise-0.2.0 → ulpwise-0.3.1}/Cargo.lock +11 -11
  7. {ulpwise-0.2.0 → ulpwise-0.3.1}/Cargo.toml +1 -1
  8. {ulpwise-0.2.0 → ulpwise-0.3.1}/PKG-INFO +152 -22
  9. ulpwise-0.3.1/README.md +355 -0
  10. {ulpwise-0.2.0 → ulpwise-0.3.1}/pyproject.toml +3 -2
  11. ulpwise-0.3.1/python/ulpwise/__init__.py +447 -0
  12. {ulpwise-0.2.0 → ulpwise-0.3.1}/python/ulpwise/__main__.py +37 -1
  13. {ulpwise-0.2.0 → ulpwise-0.3.1}/python/ulpwise/corpus/__init__.py +36 -1
  14. ulpwise-0.3.1/python/ulpwise/corpus/cases.json +1024 -0
  15. {ulpwise-0.2.0 → ulpwise-0.3.1}/python/ulpwise/plugin.py +4 -3
  16. ulpwise-0.3.1/python/ulpwise/scan.py +635 -0
  17. {ulpwise-0.2.0 → ulpwise-0.3.1}/python/ulpwise/survey.py +152 -49
  18. {ulpwise-0.2.0 → ulpwise-0.3.1}/studies/accuracy-survey-2026-09/README.md +13 -1
  19. {ulpwise-0.2.0 → ulpwise-0.3.1}/studies/accuracy-survey-2026-09/results.csv +2 -2
  20. {ulpwise-0.2.0 → ulpwise-0.3.1}/studies/accuracy-survey-2026-09/results.md +2 -2
  21. ulpwise-0.3.1/tests/test_core.py +361 -0
  22. ulpwise-0.3.1/tests/test_corpus.py +96 -0
  23. ulpwise-0.3.1/tests/test_scan.py +262 -0
  24. ulpwise-0.3.1/tests/test_survey.py +681 -0
  25. ulpwise-0.2.0/CHANGELOG.md +0 -49
  26. ulpwise-0.2.0/README.md +0 -226
  27. ulpwise-0.2.0/python/ulpwise/__init__.py +0 -184
  28. ulpwise-0.2.0/python/ulpwise/corpus/cases.json +0 -436
  29. ulpwise-0.2.0/tests/test_core.py +0 -207
  30. ulpwise-0.2.0/tests/test_corpus.py +0 -41
  31. ulpwise-0.2.0/tests/test_survey.py +0 -53
  32. {ulpwise-0.2.0 → ulpwise-0.3.1}/CONTRIBUTING.md +0 -0
  33. {ulpwise-0.2.0 → ulpwise-0.3.1}/LICENSE-APACHE +0 -0
  34. {ulpwise-0.2.0 → ulpwise-0.3.1}/LICENSE-MIT +0 -0
  35. {ulpwise-0.2.0 → ulpwise-0.3.1}/examples/sqrt_knife_values.py +0 -0
  36. {ulpwise-0.2.0 → ulpwise-0.3.1}/examples/torch_sqrt_conformance.py +0 -0
  37. {ulpwise-0.2.0 → ulpwise-0.3.1}/src/edge.rs +0 -0
  38. {ulpwise-0.2.0 → ulpwise-0.3.1}/src/exact.rs +0 -0
  39. {ulpwise-0.2.0 → ulpwise-0.3.1}/src/knife.rs +0 -0
  40. {ulpwise-0.2.0 → ulpwise-0.3.1}/src/lib.rs +0 -0
  41. {ulpwise-0.2.0 → ulpwise-0.3.1}/src/py.rs +0 -0
  42. {ulpwise-0.2.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,8 +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 and tags the commit.
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.
6
8
  name: release
7
9
 
8
10
  on:
@@ -47,8 +49,8 @@ jobs:
47
49
  target: x64
48
50
  smoke: true
49
51
  steps:
50
- - uses: actions/checkout@v4
51
- - uses: actions/setup-python@v5
52
+ - uses: actions/checkout@v7
53
+ - uses: actions/setup-python@v7
52
54
  with:
53
55
  python-version: "3.12"
54
56
  - uses: PyO3/maturin-action@v1
@@ -65,7 +67,7 @@ jobs:
65
67
  python -c "import ulpwise; print('ulpwise', ulpwise.__version__)"
66
68
  python -m ulpwise ulp 1.0 1.0000001 --dtype f32
67
69
  python -m ulpwise midpoint sqrt 0.8528626561164856 --dtype f32
68
- - uses: actions/upload-artifact@v4
70
+ - uses: actions/upload-artifact@v7
69
71
  with:
70
72
  name: wheels-${{ matrix.name }}
71
73
  path: dist
@@ -73,7 +75,7 @@ jobs:
73
75
  sdist:
74
76
  runs-on: ubuntu-latest
75
77
  steps:
76
- - uses: actions/checkout@v4
78
+ - uses: actions/checkout@v7
77
79
  - uses: PyO3/maturin-action@v1
78
80
  with:
79
81
  command: sdist
@@ -83,11 +85,31 @@ jobs:
83
85
  pip install pytest numpy
84
86
  pip install dist/*.tar.gz
85
87
  pytest tests/test_core.py -q -p no:cacheprovider
86
- - uses: actions/upload-artifact@v4
88
+ - uses: actions/upload-artifact@v7
87
89
  with:
88
90
  name: wheels-sdist
89
91
  path: dist
90
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
+
91
113
  publish:
92
114
  if: startsWith(github.ref, 'refs/tags/v') || inputs.publish == true
93
115
  needs: [wheels, sdist]
@@ -97,8 +119,8 @@ jobs:
97
119
  id-token: write
98
120
  contents: write
99
121
  steps:
100
- - uses: actions/checkout@v4
101
- - uses: actions/download-artifact@v4
122
+ - uses: actions/checkout@v7
123
+ - uses: actions/download-artifact@v8
102
124
  with:
103
125
  pattern: wheels-*
104
126
  merge-multiple: true
@@ -119,3 +141,20 @@ jobs:
119
141
  run: |
120
142
  git tag "v${{ steps.version.outputs.version }}"
121
143
  git push origin "v${{ steps.version.outputs.version }}"
144
+ - name: GitHub release with the CHANGELOG section as notes and the wheels attached
145
+ env:
146
+ GH_TOKEN: ${{ github.token }}
147
+ VERSION: ${{ steps.version.outputs.version }}
148
+ run: |
149
+ python3 - > notes.md <<'EOF'
150
+ import os, re
151
+ version = os.environ["VERSION"]
152
+ text = open("CHANGELOG.md", encoding="utf-8").read()
153
+ m = re.search(r"^## " + re.escape(version) + r"\b.*?\n(.*?)(?=^## |\Z)", text, re.M | re.S)
154
+ print(m.group(1).strip() if m else f"ulpwise {version}, see CHANGELOG.md")
155
+ EOF
156
+ if gh release view "v$VERSION" > /dev/null 2>&1; then
157
+ gh release upload "v$VERSION" dist/* --clobber
158
+ else
159
+ gh release create "v$VERSION" dist/* --title "ulpwise $VERSION" --notes-file notes.md
160
+ fi
@@ -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.2.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.2.0"
3
+ version = "0.3.1"
4
4
  edition = "2021"
5
5
  rust-version = "1.86"
6
6
  license = "MIT OR Apache-2.0"