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 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"]))
@@ -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())