levy-stable 2.0.0__py3-none-any.whl
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/__init__.py +277 -0
- levy/__main__.py +29 -0
- levy/_build/__init__.py +27 -0
- levy/_build/cli.py +299 -0
- levy/_build/quadrature.py +144 -0
- levy/_build/tables.py +420 -0
- levy/_compat.py +142 -0
- levy/_logging.py +31 -0
- levy/_pandas.py +129 -0
- levy/_typing.py +69 -0
- levy/api.py +995 -0
- levy/backends/__init__.py +186 -0
- levy/backends/_numpy.py +84 -0
- levy/backends/_torch.py +377 -0
- levy/constants.py +92 -0
- levy/data/cdf.npz +0 -0
- levy/data/limits.npz +0 -0
- levy/data/manifest.json +53 -0
- levy/data/pdf.npz +0 -0
- levy/distribution.py +377 -0
- levy/fitting.py +376 -0
- levy/interpolation.py +162 -0
- levy/parametrization.py +331 -0
- levy/py.typed +0 -0
- levy/sampling.py +153 -0
- levy/tables.py +414 -0
- levy_stable-2.0.0.dist-info/METADATA +374 -0
- levy_stable-2.0.0.dist-info/RECORD +32 -0
- levy_stable-2.0.0.dist-info/WHEEL +5 -0
- levy_stable-2.0.0.dist-info/entry_points.txt +2 -0
- levy_stable-2.0.0.dist-info/licenses/LICENSE +674 -0
- levy_stable-2.0.0.dist-info/top_level.txt +1 -0
levy/__init__.py
ADDED
|
@@ -0,0 +1,277 @@
|
|
|
1
|
+
# Copyright (C) 2005 Paul Harrison
|
|
2
|
+
# Copyright (C) 2017 José M. Miotto
|
|
3
|
+
# This program is free software; you can redistribute it and/or modify
|
|
4
|
+
# it under the terms of the GNU General Public License as published by
|
|
5
|
+
# the Free Software Foundation; either version 3 of the License, or
|
|
6
|
+
# (at your option) any later version.
|
|
7
|
+
#
|
|
8
|
+
# This program is distributed in the hope that it will be useful,
|
|
9
|
+
# but WITHOUT ANY WARRANTY; without even the implied warranty of
|
|
10
|
+
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
|
11
|
+
# GNU General Public License for more details.
|
|
12
|
+
#
|
|
13
|
+
# You should have received a copy of the GNU General Public License
|
|
14
|
+
# along with this program; if not, write to the Free Software
|
|
15
|
+
# Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA
|
|
16
|
+
|
|
17
|
+
r"""Calculation and maximum-likelihood fitting of Levy alpha-stable distributions.
|
|
18
|
+
|
|
19
|
+
Direct computation of these distributions requires a lengthy numerical
|
|
20
|
+
integration, so the package interpolates values from a precomputed table
|
|
21
|
+
instead. That is what makes fitting by maximum likelihood fast enough to be
|
|
22
|
+
practical.
|
|
23
|
+
|
|
24
|
+
Notes
|
|
25
|
+
-----
|
|
26
|
+
**Parametrizations.** The parameters of a Levy stable distribution can be
|
|
27
|
+
written down in several ways. Available here are 0 and 1 in the notation of
|
|
28
|
+
Nolan [Nolan2020]_, and M, A and B from Zolotarev [Zolotarev1986]_.
|
|
29
|
+
|
|
30
|
+
Nolan's are the easier two to reason about. Parametrization 0 is typically
|
|
31
|
+
preferred for numerical calculations and has
|
|
32
|
+
:math:`E(X)=\delta_0-\beta\gamma\tan(\pi\alpha/2)`, while 1 is preferred for
|
|
33
|
+
intuition, since :math:`E(X)=\delta_1`.
|
|
34
|
+
|
|
35
|
+
Parametrizations are handled by the module; you only say which one you are
|
|
36
|
+
using. :meth:`~levy.parametrization.Parameters.convert` moves a parameter array
|
|
37
|
+
between any two of them. Internally everything runs in parametrization 0, which
|
|
38
|
+
is what the lookup tables are built in.
|
|
39
|
+
|
|
40
|
+
``alpha`` below 0.5 is not supported: the tables do not cover it, and passing a
|
|
41
|
+
smaller value raises :exc:`ValueError`.
|
|
42
|
+
|
|
43
|
+
**Module layout.** The implementation used to be a single 800-line
|
|
44
|
+
``__init__.py``. It is now split by concern, and this module re-exports the
|
|
45
|
+
whole public surface, so ``import levy`` behaves exactly as before:
|
|
46
|
+
|
|
47
|
+
===================== =========================================================
|
|
48
|
+
``constants`` grid geometry, fit bounds, parametrization metadata
|
|
49
|
+
``interpolation`` Catmull-Rom interpolation and bound folding
|
|
50
|
+
``tables`` locating, loading, caching and repairing the tables
|
|
51
|
+
``parametrization`` the five parametrizations and the ``Parameters`` wrapper
|
|
52
|
+
``distribution`` ``levy`` and ``neglog_levy``
|
|
53
|
+
``fitting`` ``fit_levy``
|
|
54
|
+
``sampling`` ``random``
|
|
55
|
+
``api`` the typed API: ``pdf``/``cdf``/``logpdf``/``rvs``/``fit``
|
|
56
|
+
``backends`` which array library evaluates: NumPy, or optional torch
|
|
57
|
+
``_build`` offline table generation (only the CLI imports it)
|
|
58
|
+
===================== =========================================================
|
|
59
|
+
|
|
60
|
+
References
|
|
61
|
+
----------
|
|
62
|
+
.. [Nolan2020] J. P. Nolan, "Univariate Stable Distributions", Springer, 2020.
|
|
63
|
+
https://edspace.american.edu/jpnolan/stable/
|
|
64
|
+
.. [Zolotarev1986] V. M. Zolotarev, "One-dimensional Stable Distributions",
|
|
65
|
+
AMS, 1986.
|
|
66
|
+
|
|
67
|
+
Examples
|
|
68
|
+
--------
|
|
69
|
+
>>> import numpy as np
|
|
70
|
+
>>> import levy
|
|
71
|
+
>>> np.round(levy.cdf(np.array([1.0, 2.0]), alpha=1.5, beta=0.0), 6)
|
|
72
|
+
array([0.756342, 0.89496 ])
|
|
73
|
+
>>> x = levy.rvs(alpha=1.5, beta=0.0, size=200, random_state=0)
|
|
74
|
+
>>> bool(np.allclose(levy.fit(x).params.as_tuple(), [1.525, -0.078, 0.048, 0.986], atol=5e-3))
|
|
75
|
+
True
|
|
76
|
+
"""
|
|
77
|
+
|
|
78
|
+
import importlib
|
|
79
|
+
import warnings
|
|
80
|
+
|
|
81
|
+
from levy._logging import logger # noqa: F401 (re-exported)
|
|
82
|
+
from levy.constants import _lower, _upper # noqa: F401 (re-exported)
|
|
83
|
+
from levy.distribution import ( # noqa: F401 (re-exported)
|
|
84
|
+
_approximate,
|
|
85
|
+
_check_alpha_beta,
|
|
86
|
+
_grid_index,
|
|
87
|
+
_grid_shape,
|
|
88
|
+
)
|
|
89
|
+
from levy.interpolation import _interpolate, _reflect # noqa: F401 (re-exported)
|
|
90
|
+
from levy.parametrization import _phi, _psi # noqa: F401 (re-exported)
|
|
91
|
+
from levy.sampling import _ALPHA_1_RADIUS # noqa: F401 (re-exported)
|
|
92
|
+
from levy.tables import ( # noqa: F401 (re-exported)
|
|
93
|
+
_CDF_TOLERANCE,
|
|
94
|
+
_TABLE_NAMES,
|
|
95
|
+
PACKAGED_DATA,
|
|
96
|
+
ROOT,
|
|
97
|
+
_data_cache,
|
|
98
|
+
_has_complete_tables,
|
|
99
|
+
_load_table,
|
|
100
|
+
_read_from_cache,
|
|
101
|
+
_repair_table,
|
|
102
|
+
data_dir,
|
|
103
|
+
user_cache_dir,
|
|
104
|
+
)
|
|
105
|
+
|
|
106
|
+
__version__ = "2.0.0"
|
|
107
|
+
|
|
108
|
+
#: The 2.0 surface.
|
|
109
|
+
_CURRENT = [
|
|
110
|
+
'pdf',
|
|
111
|
+
'cdf',
|
|
112
|
+
'logpdf',
|
|
113
|
+
'rvs',
|
|
114
|
+
'fit',
|
|
115
|
+
'StableParams',
|
|
116
|
+
'FitResult',
|
|
117
|
+
'api',
|
|
118
|
+
'backends',
|
|
119
|
+
'set_backend',
|
|
120
|
+
'using',
|
|
121
|
+
'data_dir',
|
|
122
|
+
'user_cache_dir',
|
|
123
|
+
'PACKAGED_DATA',
|
|
124
|
+
'ROOT',
|
|
125
|
+
'logger',
|
|
126
|
+
'__version__',
|
|
127
|
+
]
|
|
128
|
+
|
|
129
|
+
# The 1.x names. Every one of them still works and still returns exactly what
|
|
130
|
+
# it returned in 1.1; each maps to (module, attribute, what to use instead).
|
|
131
|
+
#
|
|
132
|
+
# They are resolved through the module __getattr__ below rather than imported
|
|
133
|
+
# up here, which is the whole point: the warning fires when a name is *used*,
|
|
134
|
+
# not when levy is imported. An import-time warning storm -- one line for every
|
|
135
|
+
# deprecated name, on every `import levy`, whether or not the caller touches
|
|
136
|
+
# any of them -- is the fastest way to get a deprecation reverted.
|
|
137
|
+
_DEPRECATED = {
|
|
138
|
+
'levy': (
|
|
139
|
+
'levy.distribution', 'levy',
|
|
140
|
+
'levy.pdf() for a density and levy.cdf() for a distribution '
|
|
141
|
+
'function; the cdf= flag is gone',
|
|
142
|
+
),
|
|
143
|
+
'neglog_levy': (
|
|
144
|
+
'levy.distribution', 'neglog_levy',
|
|
145
|
+
'levy.logpdf(), which returns log(pdf) -- note the opposite sign',
|
|
146
|
+
),
|
|
147
|
+
'fit_levy': (
|
|
148
|
+
'levy.fitting', 'fit_levy',
|
|
149
|
+
'levy.fit(), which returns a FitResult and rejects a misspelt '
|
|
150
|
+
'parameter name instead of ignoring it',
|
|
151
|
+
),
|
|
152
|
+
'random': (
|
|
153
|
+
'levy.sampling', 'random',
|
|
154
|
+
'levy.rvs(), which takes size= rather than shape=',
|
|
155
|
+
),
|
|
156
|
+
'Parameters': (
|
|
157
|
+
'levy.parametrization', 'Parameters',
|
|
158
|
+
'levy.StableParams to carry parameters, or '
|
|
159
|
+
'levy.parametrization.Parameters if you need the fitting wrapper that '
|
|
160
|
+
'tracks which components are held fixed',
|
|
161
|
+
),
|
|
162
|
+
'convert_to_par0': (
|
|
163
|
+
'levy.parametrization', 'convert_to_par0',
|
|
164
|
+
'levy.StableParams.from_par(), which validates the result',
|
|
165
|
+
),
|
|
166
|
+
'convert_from_par0': (
|
|
167
|
+
'levy.parametrization', 'convert_from_par0',
|
|
168
|
+
'levy.StableParams.to_par()',
|
|
169
|
+
),
|
|
170
|
+
'size': ('levy.constants', 'size', 'levy.constants.size'),
|
|
171
|
+
'par_bounds': ('levy.constants', 'par_bounds', 'levy.constants.par_bounds'),
|
|
172
|
+
'par_names': ('levy.constants', 'par_names', 'levy.constants.par_names'),
|
|
173
|
+
'default': ('levy.constants', 'default', 'levy.constants.default'),
|
|
174
|
+
'f_bounds': ('levy.constants', 'f_bounds', 'levy.constants.f_bounds'),
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
# The typed API. Defined in levy.api and reached here as `levy.pdf(...)`,
|
|
178
|
+
# `levy.fit(...)` and so on, which is the spelling the documentation uses;
|
|
179
|
+
# `levy.pdf` is the same object. Resolved lazily for the same reason `api`
|
|
180
|
+
# itself is below: pydantic stays off the critical path of `import levy` until
|
|
181
|
+
# a caller actually uses this surface.
|
|
182
|
+
_API = ('pdf', 'cdf', 'logpdf', 'rvs', 'fit', 'StableParams', 'FitResult')
|
|
183
|
+
|
|
184
|
+
# Submodules, resolved on attribute access by __getattr__ below. Several of
|
|
185
|
+
# them do also become attributes as a side effect of the re-exports above, but
|
|
186
|
+
# relying on that would make `import levy; levy.sampling.random(...)` work by
|
|
187
|
+
# accident: it would break the moment a re-export moved. `api` is here for a
|
|
188
|
+
# second reason -- resolving it lazily keeps pydantic off the critical path of
|
|
189
|
+
# `import levy`.
|
|
190
|
+
_SUBMODULES = (
|
|
191
|
+
'api',
|
|
192
|
+
'backends',
|
|
193
|
+
'constants',
|
|
194
|
+
'distribution',
|
|
195
|
+
'fitting',
|
|
196
|
+
'interpolation',
|
|
197
|
+
'parametrization',
|
|
198
|
+
'sampling',
|
|
199
|
+
'tables',
|
|
200
|
+
)
|
|
201
|
+
|
|
202
|
+
# The table-generation helpers moved to levy._build. Resolved lazily too, so
|
|
203
|
+
# importing levy does not pull in scipy.integrate for the sake of code only a
|
|
204
|
+
# maintainer regenerating tables ever runs.
|
|
205
|
+
_MOVED_TO_BUILD = {
|
|
206
|
+
'_calculate_levy': 'calculate_levy',
|
|
207
|
+
'_int_levy': 'interpolated_levy',
|
|
208
|
+
}
|
|
209
|
+
|
|
210
|
+
__all__ = _CURRENT + sorted(_DEPRECATED)
|
|
211
|
+
|
|
212
|
+
|
|
213
|
+
def __getattr__(name):
|
|
214
|
+
"""Resolve the typed API, the 1.x names, the submodules and the ``levy._build`` helpers.
|
|
215
|
+
|
|
216
|
+
Parameters
|
|
217
|
+
----------
|
|
218
|
+
name : str
|
|
219
|
+
Attribute being looked up.
|
|
220
|
+
|
|
221
|
+
Returns
|
|
222
|
+
-------
|
|
223
|
+
object
|
|
224
|
+
The requested object, unchanged. A deprecated name additionally emits a
|
|
225
|
+
:exc:`DeprecationWarning` naming its replacement.
|
|
226
|
+
|
|
227
|
+
Raises
|
|
228
|
+
------
|
|
229
|
+
AttributeError
|
|
230
|
+
For any other name, as a module lookup normally would.
|
|
231
|
+
|
|
232
|
+
Notes
|
|
233
|
+
-----
|
|
234
|
+
PEP 562 module-level lookup. Nothing here is a wrapper: a deprecated name
|
|
235
|
+
resolves to the very same object 1.1 exported, so numbers cannot drift
|
|
236
|
+
between the old spelling and the new one. Only the lookup is intercepted.
|
|
237
|
+
"""
|
|
238
|
+
if name in _API:
|
|
239
|
+
return getattr(importlib.import_module('levy.api'), name)
|
|
240
|
+
|
|
241
|
+
if name in _SUBMODULES:
|
|
242
|
+
return importlib.import_module(f'levy.{name}')
|
|
243
|
+
|
|
244
|
+
if name in ('set_backend', 'using'):
|
|
245
|
+
# levy.backends imports nothing heavier than levy._compat, so this
|
|
246
|
+
# costs nothing for a caller who never selects a backend.
|
|
247
|
+
return getattr(importlib.import_module('levy.backends'), name)
|
|
248
|
+
|
|
249
|
+
if name in _DEPRECATED:
|
|
250
|
+
module_name, attribute, replacement = _DEPRECATED[name]
|
|
251
|
+
warnings.warn(
|
|
252
|
+
f'levy.{name} is deprecated since 2.0 and will be removed in a future '
|
|
253
|
+
f'major release; '
|
|
254
|
+
f'use {replacement}. It still lives at {module_name}.{attribute} '
|
|
255
|
+
f'if you want the 1.x behavior without the warning.',
|
|
256
|
+
DeprecationWarning,
|
|
257
|
+
stacklevel=2,
|
|
258
|
+
)
|
|
259
|
+
return getattr(importlib.import_module(module_name), attribute)
|
|
260
|
+
|
|
261
|
+
if name in _MOVED_TO_BUILD:
|
|
262
|
+
return getattr(importlib.import_module('levy._build'), _MOVED_TO_BUILD[name])
|
|
263
|
+
|
|
264
|
+
raise AttributeError(f'module {__name__!r} has no attribute {name!r}')
|
|
265
|
+
|
|
266
|
+
|
|
267
|
+
def __dir__():
|
|
268
|
+
"""List the package's attributes, deprecated names included.
|
|
269
|
+
|
|
270
|
+
Returns
|
|
271
|
+
-------
|
|
272
|
+
list of str
|
|
273
|
+
Sorted names, so tab completion and ``dir(levy)`` still show the 1.x
|
|
274
|
+
spellings that lazy resolution keeps out of the module dictionary.
|
|
275
|
+
"""
|
|
276
|
+
return sorted(set(globals()) | set(__all__) | set(_SUBMODULES)
|
|
277
|
+
| set(_MOVED_TO_BUILD))
|
levy/__main__.py
ADDED
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
"""Entry point for ``python -m levy``.
|
|
2
|
+
|
|
3
|
+
This file did not exist before, which means ``python -m levy build`` -- the
|
|
4
|
+
command the module docstring gave for regenerating the lookup tables -- never
|
|
5
|
+
actually worked: for a package, ``-m`` requires ``__main__.py``, and the
|
|
6
|
+
``if __name__ == "__main__"`` block in ``__init__.py`` is only reachable by
|
|
7
|
+
running that file directly (``python levy/__init__.py build``).
|
|
8
|
+
|
|
9
|
+
Prefer the ``levy-tables`` console script; this exists so the documented
|
|
10
|
+
invocation does what it says.
|
|
11
|
+
"""
|
|
12
|
+
|
|
13
|
+
import sys
|
|
14
|
+
|
|
15
|
+
from levy._build.cli import main
|
|
16
|
+
|
|
17
|
+
if __name__ == "__main__":
|
|
18
|
+
argv = sys.argv[1:]
|
|
19
|
+
if argv and argv[0] == "build":
|
|
20
|
+
# The documented 1.x spelling. Keep it working, and say what
|
|
21
|
+
# replaced it -- and where it writes now. Written to stderr rather
|
|
22
|
+
# than logged: logging is only configured inside main(), so a log
|
|
23
|
+
# record emitted here would go to the library's NullHandler.
|
|
24
|
+
sys.stderr.write(
|
|
25
|
+
"`python -m levy build` is superseded by the `levy-tables` command; "
|
|
26
|
+
"the tables go to the user cache directory, not into the package.\n"
|
|
27
|
+
)
|
|
28
|
+
sys.exit(main(argv))
|
|
29
|
+
sys.exit(main(argv or ["where"]))
|
levy/_build/__init__.py
ADDED
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
"""Offline generation of the lookup tables pylevy interpolates from.
|
|
2
|
+
|
|
3
|
+
Nothing here is needed to *use* the package. It is imported only by the
|
|
4
|
+
``levy-tables`` command and by tests, which is the point: the previous
|
|
5
|
+
arrangement kept the quadrature code in ``levy/__init__.py``, so importing the
|
|
6
|
+
library pulled in ``scipy.integrate`` and ~90 lines that only a maintainer
|
|
7
|
+
regenerating the tables would ever run.
|
|
8
|
+
|
|
9
|
+
The tables take roughly 25 minutes of CPU to rebuild at the shipped
|
|
10
|
+
(200, 76, 101) resolution, and the crossover limits another 30, so the builders
|
|
11
|
+
here support parallel execution.
|
|
12
|
+
"""
|
|
13
|
+
|
|
14
|
+
from levy._build.quadrature import calculate_levy, interpolated_levy
|
|
15
|
+
from levy._build.tables import (
|
|
16
|
+
build_crossover_tables,
|
|
17
|
+
build_density_tables,
|
|
18
|
+
write_manifest,
|
|
19
|
+
)
|
|
20
|
+
|
|
21
|
+
__all__ = [
|
|
22
|
+
"build_crossover_tables",
|
|
23
|
+
"build_density_tables",
|
|
24
|
+
"calculate_levy",
|
|
25
|
+
"interpolated_levy",
|
|
26
|
+
"write_manifest",
|
|
27
|
+
]
|
levy/_build/cli.py
ADDED
|
@@ -0,0 +1,299 @@
|
|
|
1
|
+
"""``levy-tables``: regenerate the lookup tables.
|
|
2
|
+
|
|
3
|
+
levy-tables build # into the user cache directory
|
|
4
|
+
levy-tables build --out ./tables # somewhere explicit
|
|
5
|
+
levy-tables build --size 40,16,21 --jobs 6
|
|
6
|
+
levy-tables where # which tables are actually in use
|
|
7
|
+
|
|
8
|
+
Previously the only way to do this was ``python -m levy build``, which wrote
|
|
9
|
+
24 MB straight into the installed package -- impossible on a read-only or
|
|
10
|
+
system install, and silently destructive on a partial run.
|
|
11
|
+
"""
|
|
12
|
+
|
|
13
|
+
import argparse
|
|
14
|
+
import logging
|
|
15
|
+
import os
|
|
16
|
+
import sys
|
|
17
|
+
import time
|
|
18
|
+
|
|
19
|
+
logger = logging.getLogger("levy._build")
|
|
20
|
+
|
|
21
|
+
|
|
22
|
+
def _default_size():
|
|
23
|
+
"""Return the grid size of the tables shipped with the package.
|
|
24
|
+
|
|
25
|
+
Returns
|
|
26
|
+
-------
|
|
27
|
+
tuple of int
|
|
28
|
+
``levy.constants.size``.
|
|
29
|
+
"""
|
|
30
|
+
from levy.constants import size
|
|
31
|
+
|
|
32
|
+
return size
|
|
33
|
+
|
|
34
|
+
|
|
35
|
+
def _parse_size(text):
|
|
36
|
+
"""Parse an ``x,alpha,beta`` grid size from the command line.
|
|
37
|
+
|
|
38
|
+
Parameters
|
|
39
|
+
----------
|
|
40
|
+
text : str
|
|
41
|
+
Three comma-separated integers, e.g. ``"200,76,101"``.
|
|
42
|
+
|
|
43
|
+
Returns
|
|
44
|
+
-------
|
|
45
|
+
tuple of int
|
|
46
|
+
The grid shape.
|
|
47
|
+
|
|
48
|
+
Raises
|
|
49
|
+
------
|
|
50
|
+
argparse.ArgumentTypeError
|
|
51
|
+
If there are not three of them, or any is below 8, which is the
|
|
52
|
+
smallest grid cubic interpolation can use.
|
|
53
|
+
"""
|
|
54
|
+
parts = [int(p) for p in text.split(",")]
|
|
55
|
+
if len(parts) != 3:
|
|
56
|
+
raise argparse.ArgumentTypeError(
|
|
57
|
+
"expected three comma-separated integers, e.g. 200,76,101")
|
|
58
|
+
if any(p < 8 for p in parts):
|
|
59
|
+
raise argparse.ArgumentTypeError(
|
|
60
|
+
"each dimension must be at least 8 for cubic interpolation")
|
|
61
|
+
return tuple(parts)
|
|
62
|
+
|
|
63
|
+
|
|
64
|
+
def _parse_what(text):
|
|
65
|
+
"""Parse the comma-separated list of tables to build.
|
|
66
|
+
|
|
67
|
+
Parameters
|
|
68
|
+
----------
|
|
69
|
+
text : str
|
|
70
|
+
Any of ``pdf``, ``cdf`` and ``limits``, comma-separated.
|
|
71
|
+
|
|
72
|
+
Returns
|
|
73
|
+
-------
|
|
74
|
+
list of str
|
|
75
|
+
The requested tables, in the order given.
|
|
76
|
+
|
|
77
|
+
Raises
|
|
78
|
+
------
|
|
79
|
+
argparse.ArgumentTypeError
|
|
80
|
+
If any name is not one of the three.
|
|
81
|
+
"""
|
|
82
|
+
allowed = {"pdf", "cdf", "limits"}
|
|
83
|
+
what = [p.strip() for p in text.split(",") if p.strip()]
|
|
84
|
+
unknown = set(what) - allowed
|
|
85
|
+
if unknown:
|
|
86
|
+
raise argparse.ArgumentTypeError(
|
|
87
|
+
"unknown table(s): {}".format(", ".join(sorted(unknown))))
|
|
88
|
+
return what
|
|
89
|
+
|
|
90
|
+
|
|
91
|
+
# Which files each `--what` item rewrites, in either limits layout.
|
|
92
|
+
_WRITES = {
|
|
93
|
+
"pdf": ("pdf.npz",),
|
|
94
|
+
"cdf": ("cdf.npz",),
|
|
95
|
+
"limits": ("lower_limit.npz", "upper_limit.npz", "limits.npz"),
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
|
|
99
|
+
def _tables_left_over(out_dir, size, what):
|
|
100
|
+
"""Find tables in `out_dir` that this run would leave at another size.
|
|
101
|
+
|
|
102
|
+
Parameters
|
|
103
|
+
----------
|
|
104
|
+
out_dir : str
|
|
105
|
+
Directory the build writes to.
|
|
106
|
+
size : tuple of int
|
|
107
|
+
Grid shape being built.
|
|
108
|
+
what : list of str
|
|
109
|
+
The ``--what`` items, i.e. which tables this run rewrites.
|
|
110
|
+
|
|
111
|
+
Returns
|
|
112
|
+
-------
|
|
113
|
+
list of str
|
|
114
|
+
One description per offending file, ``"name is AxBxC"``; empty when
|
|
115
|
+
the directory holds nothing that would clash.
|
|
116
|
+
|
|
117
|
+
Notes
|
|
118
|
+
-----
|
|
119
|
+
``data_dir()`` takes a directory on the strength of which files exist, so
|
|
120
|
+
a partial rebuild at a new size would leave the untouched tables at the
|
|
121
|
+
old one and the set would be used together: the grid index comes from
|
|
122
|
+
the pdf's shape, and the other tables would be read with it. Refusing
|
|
123
|
+
here keeps a cache internally consistent by construction.
|
|
124
|
+
"""
|
|
125
|
+
import numpy as np
|
|
126
|
+
|
|
127
|
+
kept = set()
|
|
128
|
+
for item in ("pdf", "cdf", "limits"):
|
|
129
|
+
if item not in what:
|
|
130
|
+
kept.update(_WRITES[item])
|
|
131
|
+
wrong = []
|
|
132
|
+
for name in sorted(kept):
|
|
133
|
+
path = os.path.join(out_dir, name)
|
|
134
|
+
if not os.path.exists(path):
|
|
135
|
+
continue
|
|
136
|
+
with np.load(path) as archive:
|
|
137
|
+
shapes = {tuple(archive[key].shape) for key in archive.files}
|
|
138
|
+
expected = tuple(size) if name in ("pdf.npz", "cdf.npz") else tuple(size[1:])
|
|
139
|
+
if shapes != {expected}:
|
|
140
|
+
wrong.append("{} is {}".format(name, ", ".join("x".join(map(str, s)) for s in shapes)))
|
|
141
|
+
return wrong
|
|
142
|
+
|
|
143
|
+
|
|
144
|
+
def build(args):
|
|
145
|
+
"""Run the ``build`` subcommand.
|
|
146
|
+
|
|
147
|
+
Parameters
|
|
148
|
+
----------
|
|
149
|
+
args : argparse.Namespace
|
|
150
|
+
Parsed arguments: ``out``, ``size``, ``what`` and ``jobs``.
|
|
151
|
+
|
|
152
|
+
Returns
|
|
153
|
+
-------
|
|
154
|
+
int
|
|
155
|
+
Process exit status.
|
|
156
|
+
"""
|
|
157
|
+
from levy._build.tables import build_crossover_tables, build_density_tables, write_manifest
|
|
158
|
+
from levy.tables import data_dir
|
|
159
|
+
|
|
160
|
+
out_dir = args.out or data_dir(writable=True)
|
|
161
|
+
logger.info("Writing tables to %s", out_dir)
|
|
162
|
+
|
|
163
|
+
wrong = _tables_left_over(out_dir, args.size, args.what)
|
|
164
|
+
if wrong:
|
|
165
|
+
logger.error(
|
|
166
|
+
"%s already holds tables at another size that this run would leave in "
|
|
167
|
+
"place (%s); a set of mixed sizes is unusable. Rebuild everything "
|
|
168
|
+
"(--what pdf,cdf,limits), or use a different --out.",
|
|
169
|
+
out_dir, "; ".join(wrong),
|
|
170
|
+
)
|
|
171
|
+
return 2
|
|
172
|
+
|
|
173
|
+
started = time.time()
|
|
174
|
+
densities = [w for w in args.what if w in ("pdf", "cdf")]
|
|
175
|
+
cdf_table = None
|
|
176
|
+
if densities:
|
|
177
|
+
results = build_density_tables(out_dir, args.size, jobs=args.jobs, what=densities)
|
|
178
|
+
if "cdf" in results:
|
|
179
|
+
cdf_table = results["cdf"][0]
|
|
180
|
+
|
|
181
|
+
if "limits" in args.what:
|
|
182
|
+
# Without a cdf from this run, build_crossover_tables takes the one
|
|
183
|
+
# already in out_dir (that is how limits get recomputed for a table
|
|
184
|
+
# built earlier), and refuses any cdf that is not the requested size.
|
|
185
|
+
try:
|
|
186
|
+
build_crossover_tables(out_dir, args.size, jobs=args.jobs, cdf_table=cdf_table)
|
|
187
|
+
except ValueError as error:
|
|
188
|
+
logger.error("%s", error)
|
|
189
|
+
return 2
|
|
190
|
+
|
|
191
|
+
manifest = write_manifest(
|
|
192
|
+
out_dir, args.size, extra={"seconds": round(time.time() - started, 1)})
|
|
193
|
+
logger.info("Done in %.1fs. Manifest: %s",
|
|
194
|
+
time.time() - started, os.path.join(out_dir, "manifest.json"))
|
|
195
|
+
for name, entry in sorted(manifest["tables"].items()):
|
|
196
|
+
logger.info(" %-12s %8.2f MB %s", name, entry["bytes"] / 1e6, entry["sha256"][:16])
|
|
197
|
+
return 0
|
|
198
|
+
|
|
199
|
+
|
|
200
|
+
def where(args):
|
|
201
|
+
"""Run the ``where`` subcommand, reporting which tables are in use.
|
|
202
|
+
|
|
203
|
+
Parameters
|
|
204
|
+
----------
|
|
205
|
+
args : argparse.Namespace
|
|
206
|
+
Parsed arguments. Unused; present for the subcommand dispatch.
|
|
207
|
+
|
|
208
|
+
Returns
|
|
209
|
+
-------
|
|
210
|
+
int
|
|
211
|
+
Process exit status.
|
|
212
|
+
"""
|
|
213
|
+
import levy
|
|
214
|
+
|
|
215
|
+
print(f"tables in use : {levy.data_dir()}")
|
|
216
|
+
print(f"packaged : {levy.PACKAGED_DATA}")
|
|
217
|
+
print(f"user cache : {levy.user_cache_dir()}")
|
|
218
|
+
print("LEVY_DATA_DIR : {}".format(os.environ.get("LEVY_DATA_DIR", "(unset)")))
|
|
219
|
+
directory = levy.data_dir()
|
|
220
|
+
if not os.path.isdir(directory):
|
|
221
|
+
print(f" {directory} is not a directory")
|
|
222
|
+
return 1
|
|
223
|
+
|
|
224
|
+
# The required set, each marked present or MISSING, rather than whatever
|
|
225
|
+
# happens to be in the directory: an override or cache that is incomplete
|
|
226
|
+
# should say which file is the problem. The crossover limits are one
|
|
227
|
+
# merged file; tables built by an older version have two.
|
|
228
|
+
expected = ["pdf.npz", "cdf.npz"]
|
|
229
|
+
legacy = ("lower_limit.npz", "upper_limit.npz")
|
|
230
|
+
# Either legacy file, not both: a split layout missing one half should be
|
|
231
|
+
# reported as that half missing, not as limits.npz missing.
|
|
232
|
+
if any(os.path.exists(os.path.join(directory, n)) for n in legacy) and \
|
|
233
|
+
not os.path.exists(os.path.join(directory, "limits.npz")):
|
|
234
|
+
expected.extend(legacy)
|
|
235
|
+
else:
|
|
236
|
+
expected.append("limits.npz")
|
|
237
|
+
missing = 0
|
|
238
|
+
for name in expected + ["manifest.json"]:
|
|
239
|
+
path = os.path.join(directory, name)
|
|
240
|
+
if os.path.exists(path):
|
|
241
|
+
print(f" {name:<16} {os.path.getsize(path) / 1e6:8.2f} MB")
|
|
242
|
+
elif name == "manifest.json":
|
|
243
|
+
print(f" {name:<16} (none: not built by levy-tables)")
|
|
244
|
+
else:
|
|
245
|
+
print(f" {name:<16} MISSING")
|
|
246
|
+
missing += 1
|
|
247
|
+
return 1 if missing else 0
|
|
248
|
+
|
|
249
|
+
|
|
250
|
+
def main(argv=None):
|
|
251
|
+
"""Entry point for the ``levy-tables`` console script.
|
|
252
|
+
|
|
253
|
+
Parameters
|
|
254
|
+
----------
|
|
255
|
+
argv : sequence of str, optional
|
|
256
|
+
Command-line arguments. Read from ``sys.argv`` when omitted.
|
|
257
|
+
|
|
258
|
+
Returns
|
|
259
|
+
-------
|
|
260
|
+
int
|
|
261
|
+
Process exit status.
|
|
262
|
+
"""
|
|
263
|
+
parser = argparse.ArgumentParser(prog="levy-tables", description=__doc__,
|
|
264
|
+
formatter_class=argparse.RawDescriptionHelpFormatter)
|
|
265
|
+
parser.add_argument("-v", "--verbose", action="store_true",
|
|
266
|
+
help="verbose (DEBUG) logging")
|
|
267
|
+
subparsers = parser.add_subparsers(dest="command")
|
|
268
|
+
|
|
269
|
+
build_parser = subparsers.add_parser("build", help="regenerate the lookup tables")
|
|
270
|
+
build_parser.add_argument(
|
|
271
|
+
"--out", help="output directory (default: the user cache directory)")
|
|
272
|
+
build_parser.add_argument("--size", type=_parse_size, default=None,
|
|
273
|
+
help="grid as x,alpha,beta (default: 200,76,101)")
|
|
274
|
+
build_parser.add_argument("--what", type=_parse_what, default=["pdf", "cdf", "limits"],
|
|
275
|
+
help="which tables to build (default: pdf,cdf,limits)")
|
|
276
|
+
build_parser.add_argument("--jobs", type=int, default=1,
|
|
277
|
+
help="worker processes; a full build is ~55 CPU-minutes")
|
|
278
|
+
build_parser.set_defaults(func=build)
|
|
279
|
+
|
|
280
|
+
where_parser = subparsers.add_parser(
|
|
281
|
+
"where", help="show which tables are in use; exit status 1 if any is missing")
|
|
282
|
+
where_parser.set_defaults(func=where)
|
|
283
|
+
|
|
284
|
+
args = parser.parse_args(argv)
|
|
285
|
+
if args.command is None:
|
|
286
|
+
parser.print_help()
|
|
287
|
+
return 1
|
|
288
|
+
|
|
289
|
+
logging.basicConfig(
|
|
290
|
+
level=logging.DEBUG if args.verbose else logging.INFO,
|
|
291
|
+
format="%(message)s",
|
|
292
|
+
)
|
|
293
|
+
if args.command == "build" and args.size is None:
|
|
294
|
+
args.size = tuple(_default_size())
|
|
295
|
+
return args.func(args)
|
|
296
|
+
|
|
297
|
+
|
|
298
|
+
if __name__ == "__main__":
|
|
299
|
+
sys.exit(main())
|