pelutils 4.3.0__tar.gz → 4.4.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 (67) hide show
  1. {pelutils-4.3.0 → pelutils-4.4.0}/PKG-INFO +24 -16
  2. {pelutils-4.3.0 → pelutils-4.4.0}/README.md +23 -15
  3. {pelutils-4.3.0 → pelutils-4.4.0}/pelutils/__version__.py +1 -1
  4. {pelutils-4.3.0 → pelutils-4.4.0}/pelutils/array/_blob.py +21 -0
  5. pelutils-4.4.0/pelutils/serialization/__init__.py +51 -0
  6. {pelutils-4.3.0 → pelutils-4.4.0}/pelutils/stats/__init__.py +7 -7
  7. {pelutils-4.3.0 → pelutils-4.4.0}/pelutils/stats/_distributions.py +26 -15
  8. {pelutils-4.3.0 → pelutils-4.4.0}/pelutils/stats/_stats.py +7 -3
  9. {pelutils-4.3.0 → pelutils-4.4.0}/pelutils/ticktock/__init__.py +2 -2
  10. {pelutils-4.3.0 → pelutils-4.4.0}/pelutils/ticktock/_ticktock.py +52 -7
  11. {pelutils-4.3.0 → pelutils-4.4.0}/pelutils.egg-info/PKG-INFO +24 -16
  12. pelutils-4.3.0/pelutils/serialization/__init__.py +0 -51
  13. {pelutils-4.3.0 → pelutils-4.4.0}/MANIFEST.in +0 -0
  14. {pelutils-4.3.0 → pelutils-4.4.0}/pelutils/__init__.py +0 -0
  15. {pelutils-4.3.0 → pelutils-4.4.0}/pelutils/_c/__init__.py +0 -0
  16. {pelutils-4.3.0 → pelutils-4.4.0}/pelutils/_c/array/blob.c +0 -0
  17. {pelutils-4.3.0 → pelutils-4.4.0}/pelutils/_c/array/blob.h +0 -0
  18. {pelutils-4.3.0 → pelutils-4.4.0}/pelutils/_c/array/unique.c +0 -0
  19. {pelutils-4.3.0 → pelutils-4.4.0}/pelutils/_c/array/unique.h +0 -0
  20. {pelutils-4.3.0 → pelutils-4.4.0}/pelutils/_c/deque/deque.c +0 -0
  21. {pelutils-4.3.0 → pelutils-4.4.0}/pelutils/_c/deque/deque.h +0 -0
  22. {pelutils-4.3.0 → pelutils-4.4.0}/pelutils/_c/hashmap/LICENSE +0 -0
  23. {pelutils-4.3.0 → pelutils-4.4.0}/pelutils/_c/hashmap/hashmap.c +0 -0
  24. {pelutils-4.3.0 → pelutils-4.4.0}/pelutils/_c/hashmap/hashmap.h +0 -0
  25. {pelutils-4.3.0 → pelutils-4.4.0}/pelutils/_c/pelutils.c +0 -0
  26. {pelutils-4.3.0 → pelutils-4.4.0}/pelutils/_c/types.h +0 -0
  27. {pelutils-4.3.0 → pelutils-4.4.0}/pelutils/_entry_points/__init__.py +0 -0
  28. {pelutils-4.3.0 → pelutils-4.4.0}/pelutils/_entry_points/linecounter.py +0 -0
  29. {pelutils-4.3.0 → pelutils-4.4.0}/pelutils/array/__init__.py +0 -0
  30. {pelutils-4.3.0 → pelutils-4.4.0}/pelutils/array/_unique.py +0 -0
  31. {pelutils-4.3.0 → pelutils-4.4.0}/pelutils/array/_utils.py +0 -0
  32. {pelutils-4.3.0 → pelutils-4.4.0}/pelutils/job_parser/__init__.py +0 -0
  33. {pelutils-4.3.0 → pelutils-4.4.0}/pelutils/job_parser/_job_parser.py +0 -0
  34. {pelutils-4.3.0 → pelutils-4.4.0}/pelutils/job_parser/_structs.py +0 -0
  35. {pelutils-4.3.0 → pelutils-4.4.0}/pelutils/legacy/__init__.py +0 -0
  36. {pelutils-4.3.0 → pelutils-4.4.0}/pelutils/legacy/v3/__init__.py +0 -0
  37. {pelutils-4.3.0 → pelutils-4.4.0}/pelutils/legacy/v3/_datastorage.py +0 -0
  38. {pelutils-4.3.0 → pelutils-4.4.0}/pelutils/logging/__init__.py +0 -0
  39. {pelutils-4.3.0 → pelutils-4.4.0}/pelutils/logging/_logger.py +0 -0
  40. {pelutils-4.3.0 → pelutils-4.4.0}/pelutils/logging/_rich_string.py +0 -0
  41. {pelutils-4.3.0 → pelutils-4.4.0}/pelutils/logging/_rotate.py +0 -0
  42. {pelutils-4.3.0 → pelutils-4.4.0}/pelutils/misc/__init__.py +0 -0
  43. {pelutils-4.3.0 → pelutils-4.4.0}/pelutils/misc/_conditional_import.py +0 -0
  44. {pelutils-4.3.0 → pelutils-4.4.0}/pelutils/misc/_files.py +0 -0
  45. {pelutils-4.3.0 → pelutils-4.4.0}/pelutils/misc/_git.py +0 -0
  46. {pelutils-4.3.0 → pelutils-4.4.0}/pelutils/misc/_misc.py +0 -0
  47. {pelutils-4.3.0 → pelutils-4.4.0}/pelutils/misc/_platform.py +0 -0
  48. {pelutils-4.3.0 → pelutils-4.4.0}/pelutils/misc/_table.py +0 -0
  49. {pelutils-4.3.0 → pelutils-4.4.0}/pelutils/plots/__init__.py +0 -0
  50. {pelutils-4.3.0 → pelutils-4.4.0}/pelutils/plots/_figure.py +0 -0
  51. {pelutils-4.3.0 → pelutils-4.4.0}/pelutils/plots/_histogram.py +0 -0
  52. {pelutils-4.3.0 → pelutils-4.4.0}/pelutils/plots/_utils.py +0 -0
  53. {pelutils-4.3.0 → pelutils-4.4.0}/pelutils/py.typed +0 -0
  54. {pelutils-4.3.0 → pelutils-4.4.0}/pelutils/serialization/_jsonl.py +0 -0
  55. {pelutils-4.3.0 → pelutils-4.4.0}/pelutils/serialization/_pretty_json.py +0 -0
  56. {pelutils-4.3.0 → pelutils-4.4.0}/pelutils/serialization/_universal_json_model.py +0 -0
  57. {pelutils-4.3.0 → pelutils-4.4.0}/pelutils/tests/__init__.py +0 -0
  58. {pelutils-4.3.0 → pelutils-4.4.0}/pelutils/tests/_tests.py +0 -0
  59. {pelutils-4.3.0 → pelutils-4.4.0}/pelutils/types/__init__.py +0 -0
  60. {pelutils-4.3.0 → pelutils-4.4.0}/pelutils/types/_types.py +0 -0
  61. {pelutils-4.3.0 → pelutils-4.4.0}/pelutils.egg-info/SOURCES.txt +0 -0
  62. {pelutils-4.3.0 → pelutils-4.4.0}/pelutils.egg-info/dependency_links.txt +0 -0
  63. {pelutils-4.3.0 → pelutils-4.4.0}/pelutils.egg-info/entry_points.txt +0 -0
  64. {pelutils-4.3.0 → pelutils-4.4.0}/pelutils.egg-info/requires.txt +0 -0
  65. {pelutils-4.3.0 → pelutils-4.4.0}/pelutils.egg-info/top_level.txt +0 -0
  66. {pelutils-4.3.0 → pelutils-4.4.0}/setup.cfg +0 -0
  67. {pelutils-4.3.0 → pelutils-4.4.0}/setup.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.1
2
2
  Name: pelutils
3
- Version: 4.3.0
3
+ Version: 4.4.0
4
4
  Summary: The Swiss army knife of Python projects
5
5
  Home-page: https://github.com/peleiden/pelutils
6
6
  Download-URL: https://pypi.org/project/pelutils/
@@ -164,10 +164,15 @@ importantly one per thread, as profiling is not thread-safe.
164
164
 
165
165
  ## Serialisation
166
166
 
167
- `UniversalJsonModel` extends `pydantic.BaseModel` with `save`/`load` methods and can
168
- serialise attributes that pydantic cannot — numpy arrays, tensors, and arbitrary
169
- objects are base64-pickled inline, everything else stays plain, human-readable JSON.
170
- Long lists are wrapped to the line-length limit instead of one element per line.
167
+ `UniversalJsonModel` is for storing Pydantic models that contain a mixture of ordinary JSON
168
+ values and Python-specific values. Ordinary values remain JSON, so the file structure can be
169
+ inspected and diffed. Values that JSON cannot represent, such as NumPy arrays, tensors, and
170
+ other Python objects, are stored as base64-encoded pickle payloads.
171
+
172
+ The `save()` and `load()` methods support convient file-based workflows.
173
+ `save()` serialises the model to a JSON file, while `load()` reconstructs the model from one.
174
+ Use `to_json_dict()` and `from_json_dict(...)` when the serialized representation belongs inside
175
+ another structure or is handled by another storage layer.
171
176
 
172
177
  ```py
173
178
  import numpy as np
@@ -193,11 +198,14 @@ result.save("results/run-1.json")
193
198
  result = Result.load("results/run-1.json")
194
199
  ```
195
200
 
196
- Use `to_json_dict()` / `from_json_dict(...)` to convert to and from a plain dict
197
- without touching the filesystem — useful for nesting inside other structures. The
198
- `pretty_json` helper function is also available on its own. The `serialization`
199
- module also includes JSONL read/write helpers (`jsonl_dump`, `jsonl_load`, ...)
200
- with largely the same interface as is provided by the built-in `json` module.
201
+ `pretty_json` is a string-returning formatter for standalone dictionaries and lists. It fills the
202
+ same role as `json.dumps`, with compact nested containers and wrapped primitive lists; write the
203
+ returned string to a file when needed. The module also includes JSONL read/write helpers
204
+ (`jsonl_dump`, `jsonl_load`, ...) for newline-delimited records.
205
+
206
+ This format is intended for trusted configuration objects, application state, experiment results, etc., not as a
207
+ language-neutral interchange format. Pickle payloads are Python-specific and loading them can
208
+ execute arbitrary code. Do not load files from untrusted sources.
201
209
 
202
210
  ## Config and command-line argument parsing
203
211
 
@@ -254,18 +262,18 @@ values, index, inverse, counts = unique(
254
262
 
255
263
  ### Statistics
256
264
 
257
- Common statistical helpers, plus wrappers around scipy distributions reparametrised
258
- as in Jim Pitman's *Probability* (rather than scipy's `loc`/`scale`, which are
265
+ Common statistical helpers, plus wrappers around SciPy distributions reparametrised
266
+ as in Jim Pitman's *Probability* (rather than SciPy's `loc`/`scale`, which are
259
267
  unintuitive for many distributions).
260
268
 
261
269
  ```py
262
- from pelutils.stats import z_score
263
270
  from pelutils.stats import expon
271
+ from pelutils.stats import z_score
264
272
 
265
- # 95 % confidence interval half-width for a standard normal (defaults give ~1.96)
266
- half_width = std * z_score()
273
+ # 95 % confidence interval half-width for a standard normal
274
+ half_width = z_score() # standard deviation is 1
267
275
 
268
- # One-sided z value for an Exponential(λ=2) at the 1 % significance level
276
+ # Upper 1 % critical value for an Exponential(lambda=2)
269
277
  zval = z_score(alpha=0.01, two_sided=False, distribution=expon(lambda_=2))
270
278
  ```
271
279
 
@@ -138,10 +138,15 @@ importantly one per thread, as profiling is not thread-safe.
138
138
 
139
139
  ## Serialisation
140
140
 
141
- `UniversalJsonModel` extends `pydantic.BaseModel` with `save`/`load` methods and can
142
- serialise attributes that pydantic cannot — numpy arrays, tensors, and arbitrary
143
- objects are base64-pickled inline, everything else stays plain, human-readable JSON.
144
- Long lists are wrapped to the line-length limit instead of one element per line.
141
+ `UniversalJsonModel` is for storing Pydantic models that contain a mixture of ordinary JSON
142
+ values and Python-specific values. Ordinary values remain JSON, so the file structure can be
143
+ inspected and diffed. Values that JSON cannot represent, such as NumPy arrays, tensors, and
144
+ other Python objects, are stored as base64-encoded pickle payloads.
145
+
146
+ The `save()` and `load()` methods support convient file-based workflows.
147
+ `save()` serialises the model to a JSON file, while `load()` reconstructs the model from one.
148
+ Use `to_json_dict()` and `from_json_dict(...)` when the serialized representation belongs inside
149
+ another structure or is handled by another storage layer.
145
150
 
146
151
  ```py
147
152
  import numpy as np
@@ -167,11 +172,14 @@ result.save("results/run-1.json")
167
172
  result = Result.load("results/run-1.json")
168
173
  ```
169
174
 
170
- Use `to_json_dict()` / `from_json_dict(...)` to convert to and from a plain dict
171
- without touching the filesystem — useful for nesting inside other structures. The
172
- `pretty_json` helper function is also available on its own. The `serialization`
173
- module also includes JSONL read/write helpers (`jsonl_dump`, `jsonl_load`, ...)
174
- with largely the same interface as is provided by the built-in `json` module.
175
+ `pretty_json` is a string-returning formatter for standalone dictionaries and lists. It fills the
176
+ same role as `json.dumps`, with compact nested containers and wrapped primitive lists; write the
177
+ returned string to a file when needed. The module also includes JSONL read/write helpers
178
+ (`jsonl_dump`, `jsonl_load`, ...) for newline-delimited records.
179
+
180
+ This format is intended for trusted configuration objects, application state, experiment results, etc., not as a
181
+ language-neutral interchange format. Pickle payloads are Python-specific and loading them can
182
+ execute arbitrary code. Do not load files from untrusted sources.
175
183
 
176
184
  ## Config and command-line argument parsing
177
185
 
@@ -228,18 +236,18 @@ values, index, inverse, counts = unique(
228
236
 
229
237
  ### Statistics
230
238
 
231
- Common statistical helpers, plus wrappers around scipy distributions reparametrised
232
- as in Jim Pitman's *Probability* (rather than scipy's `loc`/`scale`, which are
239
+ Common statistical helpers, plus wrappers around SciPy distributions reparametrised
240
+ as in Jim Pitman's *Probability* (rather than SciPy's `loc`/`scale`, which are
233
241
  unintuitive for many distributions).
234
242
 
235
243
  ```py
236
- from pelutils.stats import z_score
237
244
  from pelutils.stats import expon
245
+ from pelutils.stats import z_score
238
246
 
239
- # 95 % confidence interval half-width for a standard normal (defaults give ~1.96)
240
- half_width = std * z_score()
247
+ # 95 % confidence interval half-width for a standard normal
248
+ half_width = z_score() # standard deviation is 1
241
249
 
242
- # One-sided z value for an Exponential(λ=2) at the 1 % significance level
250
+ # Upper 1 % critical value for an Exponential(lambda=2)
243
251
  zval = z_score(alpha=0.01, two_sided=False, distribution=expon(lambda_=2))
244
252
  ```
245
253
 
@@ -1,2 +1,2 @@
1
1
  # Do not put anything else in this file
2
- __version__ = "4.3.0"
2
+ __version__ = "4.4.0"
@@ -1,7 +1,10 @@
1
1
  from __future__ import annotations
2
2
 
3
+ from typing import SupportsIndex
4
+
3
5
  import _pelutils_c as _c
4
6
  import numpy as np
7
+ from typing_extensions import override
5
8
 
6
9
  import pelutils._c as c_utils
7
10
  from pelutils.types import BoolArray, IntArray
@@ -171,3 +174,21 @@ class SparseGridBlobDetection:
171
174
  # If the error happens early in init (e.g. wrong call to __init__), _pointer_args has not yet been defined,
172
175
  # and the hashmap has not been created
173
176
  pass
177
+
178
+ def __copy__(self):
179
+ """Prevent shallow copies of the C-backed detector."""
180
+ raise TypeError("SparseGridBlobDetection instances cannot be copied or pickled")
181
+
182
+ def __deepcopy__(self, memo: dict[int, object]):
183
+ """Prevent deep copies of the C-backed detector."""
184
+ raise TypeError("SparseGridBlobDetection instances cannot be copied or pickled")
185
+
186
+ @override
187
+ def __reduce__(self):
188
+ """Prevent pickling of the C-backed detector."""
189
+ raise TypeError("SparseGridBlobDetection instances cannot be copied or pickled")
190
+
191
+ @override
192
+ def __reduce_ex__(self, protocol: SupportsIndex):
193
+ """Prevent protocol-specific pickling of the C-backed detector."""
194
+ raise TypeError("SparseGridBlobDetection instances cannot be copied or pickled")
@@ -0,0 +1,51 @@
1
+ """JSON persistence for Pydantic models containing non-JSON values.
2
+
3
+ JSON is useful when the structure of a file should remain inspectable, but it does not represent
4
+ values such as NumPy arrays, tensors, or arbitrary Python objects. ``UniversalJsonModel``
5
+ keeps JSON-native values in the document and stores unsupported field values as base64-encoded
6
+ pickle payloads. This preserves a readable JSON envelope without requiring callers to write
7
+ converters for every field type.
8
+
9
+ The usual interface is :meth:`UniversalJsonModel.save` and
10
+ :meth:`UniversalJsonModel.load`. ``save`` creates missing parent directories and writes the model to a JSON
11
+ file; ``load`` reconstructs the model from that file. Use :meth:`UniversalJsonModel.to_json_dict`
12
+ and :meth:`UniversalJsonModel.from_json_dict` when the serialized representation should be nested
13
+ inside another structure or handled by another storage layer.
14
+
15
+ For standalone dictionaries and lists, ``pretty_json`` converts Python objects to pretty-formatted JSON,
16
+ filling the same role as ``json.dumps``, but often making the resulting JSON more readable.
17
+ JSONL helpers handle files containing multiple JSON records separated by newlines.
18
+
19
+ Quick start
20
+ -----------
21
+
22
+ .. code-block:: python
23
+
24
+ import numpy as np
25
+ from pelutils.serialization import UniversalJsonModel
26
+ from pelutils.types import FloatArray
27
+
28
+ class Result(UniversalJsonModel):
29
+ accuracy: float
30
+ predictions: FloatArray # numpy arrays are handled automatically
31
+
32
+ result = Result(accuracy=0.97, predictions=np.arange(5, dtype=np.float16))
33
+ result.save("results/run-1.json")
34
+ result = Result.load("results/run-1.json")
35
+
36
+ This format is intended for trusted application data such as experiment results and cached state,
37
+ not as a language-neutral interchange format. Pickle payloads are Python-specific, and loading
38
+ them can execute arbitrary code. Use a format with an explicit schema and safe decoder when data
39
+ comes from outside the application.
40
+
41
+ .. warning::
42
+
43
+ ``UniversalJsonModel.load`` and the pickle fallback execute code while loading. Never load
44
+ files from an untrusted source.
45
+ """
46
+
47
+ from ._jsonl import jsonl_dump, jsonl_dumps, jsonl_load, jsonl_loads
48
+ from ._pretty_json import pretty_json
49
+ from ._universal_json_model import UniversalJsonModel
50
+
51
+ __all__ = ("UniversalJsonModel", "jsonl_dump", "jsonl_dumps", "jsonl_load", "jsonl_loads", "pretty_json")
@@ -1,11 +1,11 @@
1
- """Statistical helpers and scipy distributions parametrised the way textbooks are.
1
+ """Statistical helpers and SciPy distributions parametrised the way textbooks are.
2
2
 
3
3
  ``scipy.stats`` parametrises every distribution through generic ``loc``/``scale``
4
4
  arguments, which rarely match how a distribution is actually defined: an exponential's
5
5
  ``scale`` is ``1 / lambda``, a normal takes a standard deviation rather than a variance,
6
6
  and a gamma's ``a``/``scale`` bear no obvious relation to its shape and rate. This module
7
- wraps the scipy distributions so you pass their *natural* parameters instead — following
8
- the conventions in Jim Pitman's *Probability* — and returns an ordinary frozen scipy
7
+ wraps the SciPy distributions so you pass their *natural* parameters instead — following
8
+ the conventions in Jim Pitman's *Probability* — and returns an ordinary frozen SciPy
9
9
  distribution, so ``.pdf``, ``.cdf``, ``.rvs`` and friends all work as usual. It also
10
10
  provides :func:`z_score` for turning a significance level into a critical value.
11
11
 
@@ -17,12 +17,12 @@ Quick start
17
17
  from pelutils.stats import expon, norm, z_score
18
18
 
19
19
  dist = norm(mu=0, sigma2=4) # mean and *variance*, not standard deviation
20
- dist.cdf(1.5) # a plain frozen scipy distribution
20
+ dist.cdf(1.5) # a plain frozen SciPy distribution
21
21
 
22
- # 95 % confidence-interval half-width for a standard normal (defaults give ~1.96)
23
- half_width = std * z_score()
22
+ # 95 % confidence-interval half-width for a standard normal
23
+ half_width = z_score() # standard deviation is 1
24
24
 
25
- # One-sided z value for an Exponential(lambda=2) at the 1 % significance level
25
+ # Upper 1 % critical value for an Exponential(lambda=2)
26
26
  zval = z_score(alpha=0.01, two_sided=False, distribution=expon(lambda_=2))
27
27
 
28
28
  Continuous distributions: :func:`norm`, :func:`lognorm`, :func:`expon`, :func:`gamma`,
@@ -1,4 +1,4 @@
1
- """Functions returning scipy.stats distribution objects reparameterized to Jim Pitman's "Probability"."""
1
+ """Functions returning SciPy distribution objects reparameterized to Jim Pitman's "Probability"."""
2
2
 
3
3
  import numpy as np
4
4
  from scipy import stats
@@ -6,25 +6,25 @@ from scipy import stats
6
6
 
7
7
  # Continuous distributions
8
8
  def norm(mu: float, sigma2: float):
9
- """Return a normal distribution from mean and variance."""
9
+ """Return a normal distribution with mean ``mu`` and variance ``sigma2``."""
10
10
  assert sigma2 > 0
11
11
  return stats.norm(loc=mu, scale=np.sqrt(sigma2))
12
12
 
13
13
 
14
14
  def lognorm(mu: float, sigma2: float):
15
- """Return a log normal distribution from the mean and variance of the corresponding normal distribution."""
15
+ """Return a log-normal distribution where ``log(X)`` has mean ``mu`` and variance ``sigma2``."""
16
16
  assert sigma2 > 0
17
17
  return stats.lognorm(s=np.sqrt(sigma2), scale=np.exp(mu))
18
18
 
19
19
 
20
20
  def expon(lambda_: float):
21
- """Return an exponential distribution."""
21
+ """Return an exponential distribution with rate ``lambda_`` and mean ``1 / lambda_``."""
22
22
  assert lambda_ > 0
23
23
  return stats.expon(scale=1 / lambda_)
24
24
 
25
25
 
26
26
  def gamma(r: float, lambda_: float):
27
- """Return a gamma distribution."""
27
+ """Return a gamma distribution with shape ``r`` and rate ``lambda_``."""
28
28
  assert r > 0 and lambda_ > 0
29
29
  return stats.gamma(a=r, loc=0, scale=1 / lambda_)
30
30
 
@@ -32,15 +32,16 @@ def gamma(r: float, lambda_: float):
32
32
  def chi2(n: int | float):
33
33
  """Return a chi-squared distribution with the given number of degrees of freedom.
34
34
 
35
- The chi-squared distribution is generally defined only on the natural numbers, but here being defined as a special case
36
- of the gamma distribution, it generalises easily to positive rational numbers, hence why floats are accepted.
35
+ Although degrees of freedom are commonly given as an integer, the distribution
36
+ is here represented as a special case of the gamma distribution, making it defined
37
+ for any positive real ``n``.
37
38
  """
38
39
  assert n > 0
39
40
  return gamma(n / 2, 1 / 2)
40
41
 
41
42
 
42
43
  def rayleigh():
43
- """Return a rayleigh distribution."""
44
+ """Return a Rayleigh distribution."""
44
45
  return stats.rayleigh()
45
46
 
46
47
 
@@ -52,42 +53,52 @@ def beta(r: float, s: float):
52
53
 
53
54
  # Discrete distributions
54
55
  def bernoulli(p: float):
55
- """Return a Bernoulli distribution."""
56
+ """Return a Bernoulli distribution with success probability ``p``."""
56
57
  assert 0 <= p <= 1
57
58
  return stats.bernoulli(p)
58
59
 
59
60
 
60
61
  def binomial(n: int, p: float):
61
- """Return a binomial distribution."""
62
+ """Return a binomial distribution for ``n`` trials with success probability ``p``."""
62
63
  assert n >= 0 and 0 <= p <= 1
63
64
  return stats.binom(n=n, p=p)
64
65
 
65
66
 
66
67
  def poisson(mu: float):
67
- """Return a Poisson distribution."""
68
+ """Return a Poisson distribution with mean and rate ``mu``."""
68
69
  assert mu >= 0
69
70
  return stats.poisson(mu=mu)
70
71
 
71
72
 
72
73
  def hypergeom(n: int, N: int, G: int): # noqa: N803
73
- """Return a hypergeometric distribution."""
74
+ """Return a hypergeometric distribution.
75
+
76
+ Parameters
77
+ ----------
78
+ n:
79
+ Number of items drawn from the population.
80
+ N:
81
+ Population size.
82
+ G:
83
+ Number of successes in the population.
84
+ """
74
85
  assert N > n > 0 and N >= G > 0 and N > 1
75
86
  return stats.hypergeom(M=N, n=G, N=n)
76
87
 
77
88
 
78
89
  def geom0(p: float):
79
- """Return a geometric distribution defined on {0, 1, ...}."""
90
+ """Return a geometric distribution of failures before the first success, with success probability ``p``."""
80
91
  assert 0 < p <= 1
81
92
  return stats.geom(loc=-1, p=p)
82
93
 
83
94
 
84
95
  def geom1(p: float):
85
- """Return a geometric distribution defined on {1, 2, ...}."""
96
+ """Return a geometric distribution of the trial of the first success, with success probability ``p``."""
86
97
  assert 0 < p <= 1
87
98
  return stats.geom(p=p)
88
99
 
89
100
 
90
101
  def nbinom(r: int, p: float):
91
- """Return a negative binomial distribution."""
102
+ """Return a negative binomial distribution of failures before ``r`` successes, with success probability ``p``."""
92
103
  assert r > 0 and 0 <= p <= 1
93
104
  return stats.nbinom(n=r, p=p)
@@ -4,16 +4,20 @@ from ._distributions import norm
4
4
 
5
5
 
6
6
  def z_score(alpha: float = 0.05, two_sided: bool = True, distribution: Any | None = None) -> float: # pyright: ignore[reportExplicitAny]
7
- """Return the critical value (z score) for a given significance level.
7
+ """Return an upper critical value for a given significance level.
8
8
 
9
9
  Parameters
10
10
  ----------
11
11
  alpha : float, optional
12
12
  Significance level in ``[0, 1]``.
13
13
  two_sided : bool, optional
14
- If ``True``, split ``alpha`` across both tails; otherwise use a single tail.
14
+ If ``True``, use an upper-tail probability of ``alpha / 2``; otherwise use
15
+ ``alpha``. For symmetric distributions this gives the usual two-sided
16
+ cutoff for a distribution symmetric about zero, with the lower cutoff equal
17
+ to the negative of the returned value.
18
+ For asymmetric distributions, the returned value is only the upper cutoff.
15
19
  distribution : Any | None, optional
16
- A frozen scipy distribution to draw the quantile from. Defaults to ``N(0, 1)``,
20
+ A frozen SciPy distribution to draw the quantile from. Defaults to ``N(0, 1)``,
17
21
  in which case the two-sided default returns the familiar ``~1.96``.
18
22
  """
19
23
  if not 0 <= alpha <= 1:
@@ -37,6 +37,6 @@ periodic tasks, and ``fuse``/``fuse_multiple`` for combining results across
37
37
  processes.
38
38
  """
39
39
 
40
- from ._ticktock import TT, Profile, TickTock, TickTockException
40
+ from ._ticktock import TT, Profile, TickTock, TickTockException, get_active_ticktock
41
41
 
42
- __all__ = ("TT", "Profile", "TickTock", "TickTockException")
42
+ __all__ = ("TT", "Profile", "TickTock", "TickTockException", "get_active_ticktock")
@@ -1,8 +1,9 @@
1
+ import itertools
2
+ import threading
1
3
  import warnings
2
4
  from collections.abc import Hashable, Iterable, Iterator, Sequence
3
5
  from contextlib import contextmanager
4
6
  from copy import deepcopy
5
- from threading import current_thread
6
7
  from time import perf_counter
7
8
  from typing import TypeAlias, TypeVar, cast
8
9
 
@@ -23,6 +24,20 @@ _time_units: tuple[_TimeUnit, ...] = (
23
24
 
24
25
  T = TypeVar("T")
25
26
 
27
+ _lock = threading.RLock()
28
+ _active_ticktocks: "dict[int, TickTock]" = dict()
29
+ _ids = itertools.count()
30
+ _ids_lock = threading.RLock()
31
+ _local_thread = threading.local()
32
+
33
+
34
+ def _get_thread_uid() -> int:
35
+ """Return a stable, program-lifetime-unique ID for the calling thread."""
36
+ if not hasattr(_local_thread, "__ticktock_uid__"):
37
+ with _ids_lock:
38
+ _local_thread.__ticktock_uid__ = next(_ids)
39
+ return _local_thread.__ticktock_uid__
40
+
26
41
 
27
42
  def _get_smallest_suitable_unit(duration_s: float) -> _TimeUnit:
28
43
  """Return the smallest unit for which the duration is at least the same size as the unit."""
@@ -160,8 +175,8 @@ class TickTock:
160
175
  self._profile_stack: list[Profile] = list() # LIFO stack of active profiles
161
176
  self._root_profiles: list[Profile] = list() # Top level profiles
162
177
 
163
- self._thread_name = current_thread().name
164
- self._thread_id = id(current_thread())
178
+ self._thread_name = threading.current_thread().name
179
+ self._thread_id = _get_thread_uid()
165
180
 
166
181
  def tick(self, key: Hashable = None):
167
182
  """Start a timer identified by an optional hashable key."""
@@ -244,10 +259,10 @@ class TickTock:
244
259
  self._end_profile()
245
260
 
246
261
  def _warn_if_wrong_thread(self, *, stacklevel: int):
247
- if self._thread_id != id(current_thread()):
262
+ if self._thread_id != _get_thread_uid():
248
263
  warnings.warn(
249
264
  f"This TickTock instance was created in the {self._thread_name} thread but profiling was started in "
250
- + f"{current_thread().name}. Profiling is NOT designed to deal with multiple threads. Instead, create a "
265
+ + f"{threading.current_thread().name}. Profiling is NOT designed to deal with multiple threads. Instead, create a "
251
266
  + "TickTock instance for each thread requiring profiling.",
252
267
  stacklevel=stacklevel,
253
268
  )
@@ -393,8 +408,8 @@ class TickTock:
393
408
  raise ValueError("Some TickTocks are the same instance, which is not allowed")
394
409
  for tt in tts[1:]:
395
410
  ticktock.fuse(tt)
396
- ticktock._thread_name = current_thread().name
397
- ticktock._thread_id = id(current_thread())
411
+ ticktock._thread_name = threading.current_thread().name
412
+ ticktock._thread_id = _get_thread_uid()
398
413
  return ticktock
399
414
 
400
415
  @staticmethod
@@ -446,5 +461,35 @@ class TickTock:
446
461
  """Return True if any profiling has been performed."""
447
462
  return len(self._root_profiles) > 0
448
463
 
464
+ @contextmanager
465
+ def as_active(self):
466
+ """Set the ``TickTock`` instance as the active instance in the current thread.
467
+
468
+ It can then be retrieved with :func:`get_active_ticktock`.
469
+ """
470
+ current_thread_id = _get_thread_uid()
471
+ with _lock:
472
+ if current_thread_id in _active_ticktocks:
473
+ raise TickTockException(f"An active TickTock instance has already been set in thread {threading.current_thread().name}")
474
+ _active_ticktocks[current_thread_id] = self
475
+ try:
476
+ yield
477
+ finally:
478
+ with _lock:
479
+ del _active_ticktocks[current_thread_id]
480
+
481
+
482
+ def get_active_ticktock() -> TickTock:
483
+ """Return the active ``TickTock`` instance in the current thread (as set by :meth:`TickTock.as_active`).
484
+
485
+ If no active ``TickTock`` is set, a new ``TickTock`` instance is returned.
486
+ This allows normal profiling usage without having to care if it's actually used or not.
487
+ """
488
+ with _lock:
489
+ ticktock = _active_ticktocks.get(_get_thread_uid())
490
+ return ticktock if ticktock is not None else TickTock()
491
+
449
492
 
493
+ # Globally accessible default instance
494
+ # It suffices to use this for most cases
450
495
  TT = TickTock()
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.1
2
2
  Name: pelutils
3
- Version: 4.3.0
3
+ Version: 4.4.0
4
4
  Summary: The Swiss army knife of Python projects
5
5
  Home-page: https://github.com/peleiden/pelutils
6
6
  Download-URL: https://pypi.org/project/pelutils/
@@ -164,10 +164,15 @@ importantly one per thread, as profiling is not thread-safe.
164
164
 
165
165
  ## Serialisation
166
166
 
167
- `UniversalJsonModel` extends `pydantic.BaseModel` with `save`/`load` methods and can
168
- serialise attributes that pydantic cannot — numpy arrays, tensors, and arbitrary
169
- objects are base64-pickled inline, everything else stays plain, human-readable JSON.
170
- Long lists are wrapped to the line-length limit instead of one element per line.
167
+ `UniversalJsonModel` is for storing Pydantic models that contain a mixture of ordinary JSON
168
+ values and Python-specific values. Ordinary values remain JSON, so the file structure can be
169
+ inspected and diffed. Values that JSON cannot represent, such as NumPy arrays, tensors, and
170
+ other Python objects, are stored as base64-encoded pickle payloads.
171
+
172
+ The `save()` and `load()` methods support convient file-based workflows.
173
+ `save()` serialises the model to a JSON file, while `load()` reconstructs the model from one.
174
+ Use `to_json_dict()` and `from_json_dict(...)` when the serialized representation belongs inside
175
+ another structure or is handled by another storage layer.
171
176
 
172
177
  ```py
173
178
  import numpy as np
@@ -193,11 +198,14 @@ result.save("results/run-1.json")
193
198
  result = Result.load("results/run-1.json")
194
199
  ```
195
200
 
196
- Use `to_json_dict()` / `from_json_dict(...)` to convert to and from a plain dict
197
- without touching the filesystem — useful for nesting inside other structures. The
198
- `pretty_json` helper function is also available on its own. The `serialization`
199
- module also includes JSONL read/write helpers (`jsonl_dump`, `jsonl_load`, ...)
200
- with largely the same interface as is provided by the built-in `json` module.
201
+ `pretty_json` is a string-returning formatter for standalone dictionaries and lists. It fills the
202
+ same role as `json.dumps`, with compact nested containers and wrapped primitive lists; write the
203
+ returned string to a file when needed. The module also includes JSONL read/write helpers
204
+ (`jsonl_dump`, `jsonl_load`, ...) for newline-delimited records.
205
+
206
+ This format is intended for trusted configuration objects, application state, experiment results, etc., not as a
207
+ language-neutral interchange format. Pickle payloads are Python-specific and loading them can
208
+ execute arbitrary code. Do not load files from untrusted sources.
201
209
 
202
210
  ## Config and command-line argument parsing
203
211
 
@@ -254,18 +262,18 @@ values, index, inverse, counts = unique(
254
262
 
255
263
  ### Statistics
256
264
 
257
- Common statistical helpers, plus wrappers around scipy distributions reparametrised
258
- as in Jim Pitman's *Probability* (rather than scipy's `loc`/`scale`, which are
265
+ Common statistical helpers, plus wrappers around SciPy distributions reparametrised
266
+ as in Jim Pitman's *Probability* (rather than SciPy's `loc`/`scale`, which are
259
267
  unintuitive for many distributions).
260
268
 
261
269
  ```py
262
- from pelutils.stats import z_score
263
270
  from pelutils.stats import expon
271
+ from pelutils.stats import z_score
264
272
 
265
- # 95 % confidence interval half-width for a standard normal (defaults give ~1.96)
266
- half_width = std * z_score()
273
+ # 95 % confidence interval half-width for a standard normal
274
+ half_width = z_score() # standard deviation is 1
267
275
 
268
- # One-sided z value for an Exponential(λ=2) at the 1 % significance level
276
+ # Upper 1 % critical value for an Exponential(lambda=2)
269
277
  zval = z_score(alpha=0.01, two_sided=False, distribution=expon(lambda_=2))
270
278
  ```
271
279
 
@@ -1,51 +0,0 @@
1
- """JSON persistence for values that plain ``json`` and even ``pydantic`` cannot handle.
2
-
3
- Persisting data, whatever it may be, to disk usually usually comes down to a choice between
4
- human-readable or not. If efficiency is not crucial, human-readable is preferable, but often
5
- times, the data is not trivially serialisable to human-friendly formats. While ``pydantic``'s
6
- ``BaseModel.model_dump`` takes you some of the way, it falls short as soon as you introduce
7
- data types which are not JSON-serialisable by default, making pickling the default choice and
8
- ending up with an unreadable binary blob.
9
-
10
- Even if all data is easily serialisable to a readable format, you still have to deal with all
11
- the boilerplate of ensuring parent directories exist, and opening and closing files.
12
-
13
- Enter :class:`UniversalJsonModel` — a ``pydantic.BaseModel`` whose
14
- ``save``/``load`` methods serialise/deserialise *any* attribute: JSON-native values stay plain and
15
- human-readable, while anything else (numpy arrays, torch tensors, arbitrary objects) is
16
- pickled and base64-encoded inline. The output is a single, easily readable JSON file.
17
-
18
- Quick start
19
- -----------
20
-
21
- .. code-block:: python
22
-
23
- import numpy as np
24
- from pelutils.serialization import UniversalJsonModel
25
- from pelutils.types import FloatArray
26
-
27
- class Result(UniversalJsonModel):
28
- accuracy: float
29
- predictions: FloatArray # numpy arrays are handled automatically
30
-
31
- result = Result(accuracy=0.97, predictions=np.arange(5, dtype=np.float16))
32
- result.save("results/run-1.json")
33
- result = Result.load("results/run-1.json")
34
-
35
- The module also exposes :func:`pretty_json`, a function similar to the built-in `json.dumps`,
36
- but which formats the given object to a pretty JSON string;
37
- short containers stay on one line, and long primitive lists are packed across both the width and height
38
- of the file.
39
- JSONL helpers (:func:`jsonl_dump`, :func:`jsonl_load`, and their string variants) for files where each
40
- line is its own JSON object are also provided.
41
-
42
- .. warning::
43
-
44
- Loading pickles executes arbitrary code, so never ``load`` data from an untrusted source.
45
- """
46
-
47
- from ._jsonl import jsonl_dump, jsonl_dumps, jsonl_load, jsonl_loads
48
- from ._pretty_json import pretty_json
49
- from ._universal_json_model import UniversalJsonModel
50
-
51
- __all__ = ("UniversalJsonModel", "jsonl_dump", "jsonl_dumps", "jsonl_load", "jsonl_loads", "pretty_json")
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes