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.
Files changed (61) hide show
  1. levy_stable-2.0.0/CHANGELOG.md +200 -0
  2. levy_stable-2.0.0/LICENSE +674 -0
  3. levy_stable-2.0.0/MANIFEST.in +17 -0
  4. levy_stable-2.0.0/PKG-INFO +374 -0
  5. levy_stable-2.0.0/README.md +326 -0
  6. levy_stable-2.0.0/pyproject.toml +210 -0
  7. levy_stable-2.0.0/setup.cfg +4 -0
  8. levy_stable-2.0.0/src/levy/__init__.py +277 -0
  9. levy_stable-2.0.0/src/levy/__main__.py +29 -0
  10. levy_stable-2.0.0/src/levy/_build/__init__.py +27 -0
  11. levy_stable-2.0.0/src/levy/_build/cli.py +299 -0
  12. levy_stable-2.0.0/src/levy/_build/quadrature.py +144 -0
  13. levy_stable-2.0.0/src/levy/_build/tables.py +420 -0
  14. levy_stable-2.0.0/src/levy/_compat.py +142 -0
  15. levy_stable-2.0.0/src/levy/_logging.py +31 -0
  16. levy_stable-2.0.0/src/levy/_pandas.py +129 -0
  17. levy_stable-2.0.0/src/levy/_typing.py +69 -0
  18. levy_stable-2.0.0/src/levy/api.py +995 -0
  19. levy_stable-2.0.0/src/levy/backends/__init__.py +186 -0
  20. levy_stable-2.0.0/src/levy/backends/_numpy.py +84 -0
  21. levy_stable-2.0.0/src/levy/backends/_torch.py +377 -0
  22. levy_stable-2.0.0/src/levy/constants.py +92 -0
  23. levy_stable-2.0.0/src/levy/data/cdf.npz +0 -0
  24. levy_stable-2.0.0/src/levy/data/limits.npz +0 -0
  25. levy_stable-2.0.0/src/levy/data/manifest.json +53 -0
  26. levy_stable-2.0.0/src/levy/data/pdf.npz +0 -0
  27. levy_stable-2.0.0/src/levy/distribution.py +377 -0
  28. levy_stable-2.0.0/src/levy/fitting.py +376 -0
  29. levy_stable-2.0.0/src/levy/interpolation.py +162 -0
  30. levy_stable-2.0.0/src/levy/parametrization.py +331 -0
  31. levy_stable-2.0.0/src/levy/py.typed +0 -0
  32. levy_stable-2.0.0/src/levy/sampling.py +153 -0
  33. levy_stable-2.0.0/src/levy/tables.py +414 -0
  34. levy_stable-2.0.0/src/levy_stable.egg-info/PKG-INFO +374 -0
  35. levy_stable-2.0.0/src/levy_stable.egg-info/SOURCES.txt +59 -0
  36. levy_stable-2.0.0/src/levy_stable.egg-info/dependency_links.txt +1 -0
  37. levy_stable-2.0.0/src/levy_stable.egg-info/entry_points.txt +2 -0
  38. levy_stable-2.0.0/src/levy_stable.egg-info/requires.txt +25 -0
  39. levy_stable-2.0.0/src/levy_stable.egg-info/top_level.txt +1 -0
  40. levy_stable-2.0.0/tests/_cases.py +381 -0
  41. levy_stable-2.0.0/tests/_compare.py +108 -0
  42. levy_stable-2.0.0/tests/_encode.py +61 -0
  43. levy_stable-2.0.0/tests/conftest.py +95 -0
  44. levy_stable-2.0.0/tests/golden/generate.py +151 -0
  45. levy_stable-2.0.0/tests/golden/golden_v1.jsonl +280 -0
  46. levy_stable-2.0.0/tests/test_api.py +501 -0
  47. levy_stable-2.0.0/tests/test_build_cli.py +450 -0
  48. levy_stable-2.0.0/tests/test_characterization.py +52 -0
  49. levy_stable-2.0.0/tests/test_compat.py +28 -0
  50. levy_stable-2.0.0/tests/test_deprecations.py +242 -0
  51. levy_stable-2.0.0/tests/test_domain_tolerance.py +238 -0
  52. levy_stable-2.0.0/tests/test_fit_starting_scale.py +322 -0
  53. levy_stable-2.0.0/tests/test_hot_loop.py +113 -0
  54. levy_stable-2.0.0/tests/test_invariants.py +275 -0
  55. levy_stable-2.0.0/tests/test_known_bugs.py +129 -0
  56. levy_stable-2.0.0/tests/test_no_pandas.py +123 -0
  57. levy_stable-2.0.0/tests/test_no_torch.py +156 -0
  58. levy_stable-2.0.0/tests/test_pandas.py +210 -0
  59. levy_stable-2.0.0/tests/test_precision.py +123 -0
  60. levy_stable-2.0.0/tests/test_regressions.py +529 -0
  61. 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.