levy-stable 2.0.0__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.
- levy_stable-2.0.0/CHANGELOG.md +200 -0
- levy_stable-2.0.0/LICENSE +674 -0
- levy_stable-2.0.0/MANIFEST.in +17 -0
- levy_stable-2.0.0/PKG-INFO +374 -0
- levy_stable-2.0.0/README.md +326 -0
- levy_stable-2.0.0/pyproject.toml +210 -0
- levy_stable-2.0.0/setup.cfg +4 -0
- levy_stable-2.0.0/src/levy/__init__.py +277 -0
- levy_stable-2.0.0/src/levy/__main__.py +29 -0
- levy_stable-2.0.0/src/levy/_build/__init__.py +27 -0
- levy_stable-2.0.0/src/levy/_build/cli.py +299 -0
- levy_stable-2.0.0/src/levy/_build/quadrature.py +144 -0
- levy_stable-2.0.0/src/levy/_build/tables.py +420 -0
- levy_stable-2.0.0/src/levy/_compat.py +142 -0
- levy_stable-2.0.0/src/levy/_logging.py +31 -0
- levy_stable-2.0.0/src/levy/_pandas.py +129 -0
- levy_stable-2.0.0/src/levy/_typing.py +69 -0
- levy_stable-2.0.0/src/levy/api.py +995 -0
- levy_stable-2.0.0/src/levy/backends/__init__.py +186 -0
- levy_stable-2.0.0/src/levy/backends/_numpy.py +84 -0
- levy_stable-2.0.0/src/levy/backends/_torch.py +377 -0
- levy_stable-2.0.0/src/levy/constants.py +92 -0
- levy_stable-2.0.0/src/levy/data/cdf.npz +0 -0
- levy_stable-2.0.0/src/levy/data/limits.npz +0 -0
- levy_stable-2.0.0/src/levy/data/manifest.json +53 -0
- levy_stable-2.0.0/src/levy/data/pdf.npz +0 -0
- levy_stable-2.0.0/src/levy/distribution.py +377 -0
- levy_stable-2.0.0/src/levy/fitting.py +376 -0
- levy_stable-2.0.0/src/levy/interpolation.py +162 -0
- levy_stable-2.0.0/src/levy/parametrization.py +331 -0
- levy_stable-2.0.0/src/levy/py.typed +0 -0
- levy_stable-2.0.0/src/levy/sampling.py +153 -0
- levy_stable-2.0.0/src/levy/tables.py +414 -0
- levy_stable-2.0.0/src/levy_stable.egg-info/PKG-INFO +374 -0
- levy_stable-2.0.0/src/levy_stable.egg-info/SOURCES.txt +59 -0
- levy_stable-2.0.0/src/levy_stable.egg-info/dependency_links.txt +1 -0
- levy_stable-2.0.0/src/levy_stable.egg-info/entry_points.txt +2 -0
- levy_stable-2.0.0/src/levy_stable.egg-info/requires.txt +25 -0
- levy_stable-2.0.0/src/levy_stable.egg-info/top_level.txt +1 -0
- levy_stable-2.0.0/tests/_cases.py +381 -0
- levy_stable-2.0.0/tests/_compare.py +108 -0
- levy_stable-2.0.0/tests/_encode.py +61 -0
- levy_stable-2.0.0/tests/conftest.py +95 -0
- levy_stable-2.0.0/tests/golden/generate.py +151 -0
- levy_stable-2.0.0/tests/golden/golden_v1.jsonl +280 -0
- levy_stable-2.0.0/tests/test_api.py +501 -0
- levy_stable-2.0.0/tests/test_build_cli.py +450 -0
- levy_stable-2.0.0/tests/test_characterization.py +52 -0
- levy_stable-2.0.0/tests/test_compat.py +28 -0
- levy_stable-2.0.0/tests/test_deprecations.py +242 -0
- levy_stable-2.0.0/tests/test_domain_tolerance.py +238 -0
- levy_stable-2.0.0/tests/test_fit_starting_scale.py +322 -0
- levy_stable-2.0.0/tests/test_hot_loop.py +113 -0
- levy_stable-2.0.0/tests/test_invariants.py +275 -0
- levy_stable-2.0.0/tests/test_known_bugs.py +129 -0
- levy_stable-2.0.0/tests/test_no_pandas.py +123 -0
- levy_stable-2.0.0/tests/test_no_torch.py +156 -0
- levy_stable-2.0.0/tests/test_pandas.py +210 -0
- levy_stable-2.0.0/tests/test_precision.py +123 -0
- levy_stable-2.0.0/tests/test_regressions.py +529 -0
- levy_stable-2.0.0/tests/test_torch.py +331 -0
|
@@ -0,0 +1,200 @@
|
|
|
1
|
+
# Changelog
|
|
2
|
+
|
|
3
|
+
All notable changes to this project are documented here.
|
|
4
|
+
|
|
5
|
+
The format follows [Keep a Changelog](https://keepachangelog.com/en/1.1.0/),
|
|
6
|
+
and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
|
|
7
|
+
|
|
8
|
+
## [2.0.0] - 2026-09-21
|
|
9
|
+
|
|
10
|
+
The package could not be built on Python 3.12, had no tests and no CI, and
|
|
11
|
+
carried several defects that returned wrong numbers silently. 2.0 fixes that
|
|
12
|
+
and adds a typed, validated API. **Every 1.x name still works**, and away
|
|
13
|
+
from the defects listed under *Fixed* below -- calls that returned a wrong
|
|
14
|
+
number, a NaN, or an error -- it returns the same floats, bit for bit.
|
|
15
|
+
|
|
16
|
+
This is the first release on PyPI, as **`levy-stable`**. The import name is
|
|
17
|
+
still `levy`. The `pylevy` name on PyPI belongs to the 2005 package this one
|
|
18
|
+
descends from, and its author declined to transfer it; see
|
|
19
|
+
`docs/proposals/pypi-name.md`.
|
|
20
|
+
|
|
21
|
+
### Added
|
|
22
|
+
|
|
23
|
+
- A typed, keyword-only API: `levy.pdf`, `levy.cdf`, `levy.logpdf`, `levy.rvs`
|
|
24
|
+
and `levy.fit`, with a frozen `levy.StableParams` carrying validated
|
|
25
|
+
parameters. Defined in `levy.api`, and reachable there too; resolved lazily
|
|
26
|
+
at the top level, so `import levy` still does not import pydantic. Out-of-range
|
|
27
|
+
values are rejected where they are written, naming the field. Pydantic is used
|
|
28
|
+
only at this boundary; `tests/test_hot_loop.py` counts model constructions
|
|
29
|
+
during a fit and fails if the number grows with the data.
|
|
30
|
+
- `py.typed`, so the annotations are visible to a downstream type checker.
|
|
31
|
+
`mypy --strict` runs in CI over the typed surface.
|
|
32
|
+
- A characterization test suite: 251 golden records, stored as exact hex floats,
|
|
33
|
+
covering `levy`, `neglog_levy`, `random` and `fit_levy` in all five
|
|
34
|
+
parametrizations. Every change since is measured against it.
|
|
35
|
+
- CI on Linux × Python 3.9–3.13 plus macOS and Windows at both ends of the
|
|
36
|
+
range, pinned NumPy 1.x and 2.x legs, and separate legs for table generation,
|
|
37
|
+
golden reproducibility, lint and docstrings, doctests, the optional extras,
|
|
38
|
+
and the documentation build.
|
|
39
|
+
- Project documentation and automation: `CONTRIBUTING.md`, `AGENTS.md`,
|
|
40
|
+
`CODE_OF_CONDUCT.md`, `CITATION.cff`, issue and pull-request templates,
|
|
41
|
+
Dependabot for pip and GitHub Actions, and `.pre-commit-config.yaml`.
|
|
42
|
+
- A release workflow: a `vX.Y.Z` tag is refused unless it matches
|
|
43
|
+
`levy.__version__`, the built wheel is smoke-tested in a clean environment
|
|
44
|
+
before anything is published, publishing goes through PyPI Trusted Publishing
|
|
45
|
+
(OIDC, no stored secret), and the lookup tables plus their manifest are
|
|
46
|
+
attached to the GitHub release, for use through `$LEVY_DATA_DIR` or for
|
|
47
|
+
checking a local build against their checksums.
|
|
48
|
+
- `levy-tables`, a console script that regenerates the lookup tables into a user
|
|
49
|
+
cache directory, with a `manifest.json` recording grid size, library versions
|
|
50
|
+
and per-array SHA256.
|
|
51
|
+
- `levy/__main__.py`, so the documented `python -m levy build` works. It never
|
|
52
|
+
did: for a package, `-m` requires `__main__.py`.
|
|
53
|
+
- NumPy-style docstrings throughout, enforced by `ruff` (pydocstyle, numpy
|
|
54
|
+
convention) and `numpydoc`, both gated in CI.
|
|
55
|
+
- Documentation that builds from a checkout, built on every pull request
|
|
56
|
+
with `-W` and uploaded as an artifact, so a docstring that does not parse,
|
|
57
|
+
a page missing from the toctree or an unreachable intersphinx inventory
|
|
58
|
+
fails where it was introduced; the examples on the front and migration
|
|
59
|
+
pages are executed by the doctest builder. New narrative pages: **How it works** (the tan-space
|
|
60
|
+
grid, Catmull-Rom interpolation, the tail crossover and its known
|
|
61
|
+
discontinuity, why float32 suffices) and **Migrating from 1.x**. A committed
|
|
62
|
+
`.readthedocs.yaml` replaces build settings that existed only in the Read the
|
|
63
|
+
Docs web interface, where nobody but the account owner could see them.
|
|
64
|
+
- An optional `pandas` extra. `pdf`, `cdf` and `logpdf` accept a `Series` or a
|
|
65
|
+
`DataFrame` and return one carrying the same index; `fit` accepts a `Series`
|
|
66
|
+
or a single-column `DataFrame`, and `FitResult.to_series()` reports the
|
|
67
|
+
parameters under that parametrization's own names. The core never sees a
|
|
68
|
+
pandas object, and an install without the extra never imports pandas --
|
|
69
|
+
support is detected by looking in `sys.modules`, so the fast path is one
|
|
70
|
+
dictionary lookup. `tests/test_no_pandas.py` runs the whole API in a
|
|
71
|
+
subprocess with pandas blocked at the import system.
|
|
72
|
+
- An optional `torch` extra: `levy.backends`, with a torch implementation of
|
|
73
|
+
the interpolation written to be differentiable. Hand `pdf`, `cdf` or `logpdf`
|
|
74
|
+
a tensor -- or select the backend with `levy.set_backend('torch')` or
|
|
75
|
+
`levy.using('torch')` -- and gradients flow to `alpha`, `beta`, `mu` and
|
|
76
|
+
`sigma`, so a stable distribution can sit inside a larger torch model and be
|
|
77
|
+
fitted by gradient descent. `torch.autograd.gradcheck` passes for all four
|
|
78
|
+
parameters, for pdf, cdf and the negative log density, in float64. NumPy
|
|
79
|
+
remains the default, and an install without the extra never imports torch.
|
|
80
|
+
- `weights=` on `fit` and on `fit_levy`: one non-negative weight per
|
|
81
|
+
observation, minimizing the weighted negative log likelihood. A weight of 2
|
|
82
|
+
counts an observation twice, 0 drops it; a wrong length, a negative or
|
|
83
|
+
non-finite weight, or all-zero weights raise `ValueError`. Contributed as
|
|
84
|
+
[#16](https://github.com/josemiotto/pylevy/pull/16) by Wesley Tansey against
|
|
85
|
+
1.x; ported to 2.0. The unweighted path is unchanged, expression for
|
|
86
|
+
expression, so no golden moved.
|
|
87
|
+
|
|
88
|
+
### Changed
|
|
89
|
+
|
|
90
|
+
- Packaging moved from `distutils` to PEP 621 `pyproject.toml`. The package
|
|
91
|
+
builds and installs on 3.9–3.13 again; it could not be built on 3.12+ at all.
|
|
92
|
+
- The 800-line `__init__.py` was split by concern into `constants`,
|
|
93
|
+
`interpolation`, `tables`, `parametrization`, `distribution`, `fitting`,
|
|
94
|
+
`sampling` and `_build`, under a `src/` layout. Verified as a pure move:
|
|
95
|
+
99,312 values bit-identical across wheels built before and after.
|
|
96
|
+
- The lookup tables are stored as `float32` and the two crossover-limit tables
|
|
97
|
+
are merged: **24.7 MB → 10.3 MB**. Measured cost, worst case 1.7e-07 relative
|
|
98
|
+
— three orders of magnitude below the interpolation error that already
|
|
99
|
+
dominates.
|
|
100
|
+
- `print()` on error paths replaced with a module logger carrying a
|
|
101
|
+
`NullHandler`.
|
|
102
|
+
|
|
103
|
+
### Deprecated
|
|
104
|
+
|
|
105
|
+
Every name below still works and still returns exactly what it returned in 1.1.
|
|
106
|
+
Accessing one through `levy.` emits a `DeprecationWarning` naming its
|
|
107
|
+
replacement; the warning fires on *access*, not on `import levy`. They will
|
|
108
|
+
be removed in a future major release.
|
|
109
|
+
|
|
110
|
+
| 1.x | 2.0 | note |
|
|
111
|
+
|---|---|---|
|
|
112
|
+
| `levy.levy(x, a, b, cdf=False)` | `levy.pdf(x, alpha=a, beta=b)` | |
|
|
113
|
+
| `levy.levy(x, a, b, cdf=True)` | `levy.cdf(x, alpha=a, beta=b)` | the `cdf=` flag is gone |
|
|
114
|
+
| `levy.neglog_levy(...)` | `levy.logpdf(...)` | **opposite sign** — `logpdf` returns `log(pdf)` |
|
|
115
|
+
| `levy.fit_levy(x)` | `levy.fit(x)` | returns a `FitResult`; rejects a misspelt parameter name |
|
|
116
|
+
| `levy.random(..., shape=)` | `levy.rvs(..., size=)` | |
|
|
117
|
+
| `levy.Parameters` | `levy.StableParams` | or `levy.parametrization.Parameters` for the fitting wrapper |
|
|
118
|
+
| `levy.convert_to_par0` | `levy.StableParams.from_par` | validates the result |
|
|
119
|
+
| `levy.convert_from_par0` | `levy.StableParams.to_par` | |
|
|
120
|
+
| `levy.size` | `levy.constants.size` | |
|
|
121
|
+
| `levy.par_bounds` | `levy.constants.par_bounds` | |
|
|
122
|
+
| `levy.par_names` | `levy.constants.par_names` | |
|
|
123
|
+
| `levy.default` | `levy.constants.default` | |
|
|
124
|
+
| `levy.f_bounds` | `levy.constants.f_bounds` | |
|
|
125
|
+
|
|
126
|
+
Each 1.x name also remains importable from its own module without a warning, if
|
|
127
|
+
you want the old behavior and no deprecation noise.
|
|
128
|
+
|
|
129
|
+
### Fixed
|
|
130
|
+
|
|
131
|
+
- `random(alpha=2.0, mu=..., sigma=...)` ignored `mu` and `sigma`. The Gaussian
|
|
132
|
+
branch returned before reaching the line that applied them, so
|
|
133
|
+
`random(2.0, 0.0, mu=100, sigma=5)` came back centered on zero.
|
|
134
|
+
- `random(1.0, ±1.0)` produced **NaN for about 0.9% of draws**, and the
|
|
135
|
+
surviving samples were from the wrong distribution (Kolmogorov–Smirnov against
|
|
136
|
+
this package's own CDF: p = 3e-07 over 200k draws). The α = 1 nudge sat 1e-15
|
|
137
|
+
from the pole of the tangent, where rounding the argument becomes an ~11%
|
|
138
|
+
error in the result. Moved to 1e-8, in the middle of a plateau where every
|
|
139
|
+
radius from 1e-10 to 1e-6 behaves identically.
|
|
140
|
+
- `alpha` below 0.5 produced a *negative* grid index — a perfectly valid Python
|
|
141
|
+
index — and silently returned values for `alpha ≈ 1.94`. `alpha` and `beta`
|
|
142
|
+
are now validated against the tabulated range.
|
|
143
|
+
- Four cells of the shipped `cdf.npz` held `5.72e+307` instead of a probability:
|
|
144
|
+
quadrature failures baked into the table. `levy(0.0, 0.58, 0.74, cdf=True)`
|
|
145
|
+
returned `6.44e+307`. They are repaired at load time by interpolating along x,
|
|
146
|
+
logged once at `WARNING`.
|
|
147
|
+
- `Parameters.x = [...]` raised `UnboundLocalError` instead of `TypeError`; the
|
|
148
|
+
setter dispatched on `__class__.__name__`.
|
|
149
|
+
- `_reflect` was an unbounded `while 1:` loop. With the σ bounds `(1e-6, 1e10)`,
|
|
150
|
+
folding `1e30` needs ~1e20 iterations and never returns. Replaced by the
|
|
151
|
+
closed-form fold, identical output in the covered range.
|
|
152
|
+
- `np.Inf` (removed in NumPy 2.0) made table generation fail on modern NumPy.
|
|
153
|
+
- `levy.size` was hardcoded into the index arithmetic, so tables at any other
|
|
154
|
+
resolution raised `IndexError` — which defeated the point of `--size`.
|
|
155
|
+
- The doctests: 4 of the 21 had been failing, and CI now runs them as a gate.
|
|
156
|
+
- Fits could stop at a point that was not even stationary, returning parameters
|
|
157
|
+
pinned near a boundary. Reported upstream as
|
|
158
|
+
[#20](https://github.com/josemiotto/pylevy/issues/20) for `alpha=1.6`,
|
|
159
|
+
`sigma=0.005`. The search always started at `sigma = 1`, whatever the scale of
|
|
160
|
+
the data; 200 times too wide, and L-BFGS-B did not recover. Measured over 400
|
|
161
|
+
samples of 10,000 points, **2.25% of fits failed**, leaving up to 9,054
|
|
162
|
+
log-likelihood units unclaimed; the same sweep with the fix in place fails
|
|
163
|
+
**0 of 400**. A second starting point derived from the
|
|
164
|
+
data's median and interquartile range is now tried alongside the historical
|
|
165
|
+
one, and the better optimum wins. Because the old start is still a candidate,
|
|
166
|
+
the returned likelihood can only improve: over the ten golden fit cases,
|
|
167
|
+
five optima are unchanged, five improve (by up to 4.5e-08 in negative log
|
|
168
|
+
likelihood) and none get worse.
|
|
169
|
+
- `fit_levy(x, par='B')` aborted with `ValueError: beta must be in [-1.0, 1.0],
|
|
170
|
+
got 1.0000000000000004` on 9 of 36 measured samples. Converting Zolotarev's
|
|
171
|
+
B into parametrization 0 evaluates two tangents whose rounding does not
|
|
172
|
+
cancel, so `beta_0` can land up to 44 ULP outside `[-1, 1]`. The domain check
|
|
173
|
+
added earlier in this release was exact, and turned that rounding into a lost
|
|
174
|
+
fit. Values within 1e-12 of an endpoint are now snapped to it; anything
|
|
175
|
+
further out is still rejected.
|
|
176
|
+
|
|
177
|
+
### Known issues
|
|
178
|
+
|
|
179
|
+
- The CDF is discontinuous at the tail crossover, stepping down by up to
|
|
180
|
+
2.6e-03. Fixing it means regenerating the crossover limits or reconciling the
|
|
181
|
+
interpolated and asymptotic branches; tracked separately.
|
|
182
|
+
- Maximum likelihood for stable distributions is not convex, and L-BFGS-B can
|
|
183
|
+
still settle on a local optimum even from the improved starting point. The
|
|
184
|
+
two-start search above removes the scale-mismatch failures but not every
|
|
185
|
+
case: fitting in `'B'` on badly scaled data can still stop short of the
|
|
186
|
+
optimum reached in `'0'`. Fitting in more than one parametrization and
|
|
187
|
+
keeping the best likelihood remains a sound workaround; more starting points
|
|
188
|
+
would reduce it further, at proportional cost.
|
|
189
|
+
|
|
190
|
+
## Versioning
|
|
191
|
+
|
|
192
|
+
`levy.__version__` is the single source of truth, and tags are `vX.Y.Z` from
|
|
193
|
+
here on. The existing tags `v0.5`, `1.1` and `1.2` are inconsistent with that
|
|
194
|
+
and are left as they are — note that `1.2` was tagged while `__version__` still
|
|
195
|
+
read `"1.1"`. Release automation asserts that a tag matches `__version__` before
|
|
196
|
+
publishing.
|
|
197
|
+
|
|
198
|
+
## [1.1] - 2020-08
|
|
199
|
+
|
|
200
|
+
The last release of the 1.x line. See the git history; there was no changelog.
|