SSTT-visuals 0.1.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.
@@ -0,0 +1,5 @@
1
+ """SSTT visualisation helpers for scan-statistic and occupancy summaries."""
2
+
3
+ from .plot import statisticsVisual
4
+
5
+ __all__ = ["statisticsVisual"]
@@ -0,0 +1,298 @@
1
+ """
2
+ Two goodness-of-fit tests against the null hypothesis that a 5x5 array of
3
+ nonnegative-integer counts arises from N i.i.d. uniform draws over the 25
4
+ cells (equivalently: the count vector is Multinomial(N, p=1/25 each)).
5
+
6
+ 1. occupancy_test: T = number of empty cells. Reports P(T >= t_obs), exact.
7
+ 2. scan_statistic_test: Q = min over "2-by-n" rectangles of the rectangle's
8
+ own upper-tail binomial p-value. Reports P(Q <= q_obs) via simulation,
9
+ since the exact null distribution of a minimum over overlapping,
10
+ dependent windows has no tractable closed form.
11
+ """
12
+
13
+ from fractions import Fraction
14
+ from math import comb
15
+
16
+ import scipy.stats as stats
17
+
18
+ from .mos import normal_isf_asymptotic
19
+
20
+ import numpy as np
21
+ from scipy.stats import binom
22
+
23
+ GRID_SIZE = 5
24
+ CENTER = (GRID_SIZE // 2, GRID_SIZE // 2) # (2, 2), zero-indexed
25
+
26
+
27
+ # ---------------------------------------------------------------------------
28
+ # Test 1: Occupancy test
29
+ # ---------------------------------------------------------------------------
30
+
31
+ def _surjections(n: int, j: int) -> int:
32
+ """Number of surjective functions from an n-set onto a j-set, via
33
+ inclusion-exclusion. Surj(n, 0) = 1 iff n == 0 (handled naturally by the
34
+ formula since 0**0 = 1 in Python)."""
35
+ total = 0
36
+ for i in range(j + 1):
37
+ total += (-1) ** i * comb(j, i) * (j - i) ** n
38
+ return total
39
+
40
+
41
+ def occupancyStatisticTest(data: np.ndarray):
42
+ """
43
+ Exact p-value P(T >= t_obs) for the occupancy test, where T is the
44
+ number of empty cells among the m = 25 cells of `data`, under the null
45
+ that N = sum(data) observations are i.i.d. uniform over the 25 cells.
46
+
47
+ Returns the p-value (float).
48
+ """
49
+ data = np.asarray(data)
50
+ if data.shape != (GRID_SIZE, GRID_SIZE):
51
+ raise ValueError(f"expected a {GRID_SIZE}x{GRID_SIZE} array")
52
+
53
+ m = data.size # 25 boxes
54
+ n = int(data.sum()) # total balls
55
+ t_obs = int(np.sum(data == 0))
56
+
57
+ p_tail = Fraction(0)
58
+ for k in range(t_obs, m + 1):
59
+ j = m - k # number of "occupied" boxes required among the chosen k...
60
+ # careful: k here is the number of EMPTY boxes, so m-k boxes must
61
+ # all be nonempty -> surjection of n balls onto (m-k) boxes
62
+ occupied = m - k
63
+ surj = _surjections(n, occupied)
64
+ p_tail += Fraction(comb(m, k) * surj, m ** n)
65
+
66
+ p_value = float(p_tail)
67
+ if p_value <= 0:
68
+ z_score = normal_isf_asymptotic(p_tail)
69
+ lp = None
70
+ else:
71
+ z_score = stats.norm.isf(p_value)
72
+ lp = -np.log(p_value)
73
+ return (p_value, z_score, lp)
74
+
75
+
76
+ # ---------------------------------------------------------------------------
77
+ # Test 2: Scan statistic test
78
+ # ---------------------------------------------------------------------------
79
+
80
+ def _enumerate_rectangles(size: int = GRID_SIZE, center=CENTER):
81
+ """
82
+ All axis-aligned rectangles with one side exactly length 2 and the other
83
+ side length 2..size (both orientations: 2-tall / n-wide, and n-tall /
84
+ 2-wide, with n >= 2 so both dimensions are at least 2), excluding any
85
+ rectangle that contains `center`.
86
+
87
+ Returns a list of (r0, r1, c0, c1, flat_indices, p) tuples where
88
+ (r0..r1, c0..c1) are inclusive cell ranges, flat_indices is the list of
89
+ flattened (row-major) cell indices in the rectangle, and p = ncells/25
90
+ is the null probability mass covered by the rectangle.
91
+ """
92
+ rects = set()
93
+
94
+ for w in range(2, size + 1):
95
+ h = 2
96
+ for r0 in range(size - h + 1):
97
+ for c0 in range(size - w + 1):
98
+ r1, c1 = r0 + h - 1, c0 + w - 1
99
+ if r0 <= center[0] <= r1 and c0 <= center[1] <= c1:
100
+ continue
101
+ rects.add((r0, r1, c0, c1))
102
+
103
+ for h in range(2, size + 1):
104
+ w = 2
105
+ for r0 in range(size - h + 1):
106
+ for c0 in range(size - w + 1):
107
+ r1, c1 = r0 + h - 1, c0 + w - 1
108
+ if r0 <= center[0] <= r1 and c0 <= center[1] <= c1:
109
+ continue
110
+ rects.add((r0, r1, c0, c1))
111
+
112
+ rect_info = []
113
+ for (r0, r1, c0, c1) in sorted(rects):
114
+ idx = [r * size + c for r in range(r0, r1 + 1) for c in range(c0, c1 + 1)]
115
+ p = len(idx) / (size * size)
116
+ rect_info.append((r0, r1, c0, c1, idx, p))
117
+ return rect_info
118
+
119
+
120
+ # These are the two composite regions in grid coordinates. Each tuple keeps
121
+ # the normal rectangle fields, followed by the cells in the union and the
122
+ # component rectangles used by the plotter for highlighting.
123
+ _SPECIAL_COMPOSITE_RECTS = [
124
+ (
125
+ 0, 4, 0, 2,
126
+ [0, 1, 5, 6, 10, 11, 15, 16, 17, 20, 21, 22],
127
+ 12 / 25,
128
+ ((3, 4, 0, 2), (0, 2, 0, 1)),
129
+ ),
130
+ (
131
+ 0, 4, 2, 4,
132
+ [2, 3, 4, 7, 8, 9, 13, 14, 18, 19, 23, 24],
133
+ 12 / 25,
134
+ ((0, 1, 2, 4), (2, 4, 3, 4)),
135
+ ),
136
+ ]
137
+
138
+
139
+ _RECTS = _enumerate_rectangles() + _SPECIAL_COMPOSITE_RECTS
140
+
141
+
142
+ def _compute_Q(flat_counts: np.ndarray, n_total: int) -> float:
143
+ """flat_counts: 1D array of length 25 (row-major). Returns Q = min_r q_r."""
144
+ q_min = 1.0
145
+ min_rect = None
146
+ for rect in _RECTS:
147
+ idx = rect[4]
148
+ p = rect[5]
149
+ s = flat_counts[idx].sum()
150
+ q_r = binom.sf(s - 1, n_total, p) # P(X >= s)
151
+ if q_r < q_min:
152
+ q_min = q_r
153
+ min_rect = rect
154
+ return q_min, min_rect
155
+
156
+
157
+ def _compute_Q_batch(flat_counts_batch: np.ndarray, n_total: int) -> np.ndarray:
158
+ """Vectorised version: flat_counts_batch has shape (n_sims, 25)."""
159
+ n_sims = flat_counts_batch.shape[0]
160
+ q_min = np.ones(n_sims)
161
+ for rect in _RECTS:
162
+ idx = rect[4]
163
+ p = rect[5]
164
+ s = flat_counts_batch[:, idx].sum(axis=1)
165
+ q_r = binom.sf(s - 1, n_total, p)
166
+ np.minimum(q_min, q_r, out=q_min)
167
+ return q_min
168
+
169
+
170
+ def scanStatisticTest(data, n_sims: int = 2000000, random_state=None, return_rect: bool = False):
171
+ """
172
+ Monte Carlo p-value P(Q <= q_obs) for the scan statistic test.
173
+
174
+ Q = min over all valid "2-by-n" rectangles r of q_r, where q_r is the
175
+ upper-tail binomial probability P(count in r >= observed count in r)
176
+ under the null that N = sum(data) observations are i.i.d. uniform over
177
+ the 25 cells.
178
+
179
+ Returns the p-value, z-score, log-p-value, q_obs, and optionally the
180
+ rectangle with the minimum q-value.
181
+ """
182
+ data = np.asarray(data)
183
+ if data.shape != (GRID_SIZE, GRID_SIZE):
184
+ raise ValueError(f"expected a {GRID_SIZE}x{GRID_SIZE} array")
185
+
186
+ n_total = int(data.sum())
187
+ flat_obs = data.reshape(-1)
188
+ q_obs, min_rect = _compute_Q(flat_obs, n_total)
189
+
190
+ if n_total == 0:
191
+ # Every rectangle sum is trivially 0 >= 0, so q_r = 1 for all r,
192
+ # Q = 1 always, under the null too -> degenerate p-value of 1.
193
+ result = (q_obs, 1.0)
194
+ if return_rect:
195
+ return (1.0, 0.0, 0.0, q_obs, min_rect)
196
+ return result
197
+
198
+ rng = np.random.default_rng(random_state)
199
+ flat_p = np.full(GRID_SIZE * GRID_SIZE, 1.0 / (GRID_SIZE * GRID_SIZE))
200
+ sims = rng.multinomial(n_total, flat_p, size=n_sims) # shape (n_sims, 25)
201
+
202
+ q_sims = _compute_Q_batch(sims, n_total)
203
+ p_value = float(np.mean(q_sims <= q_obs))
204
+
205
+ z_score = stats.norm.isf(p_value)
206
+ lp = np.inf if p_value == 0 else -np.log(p_value)
207
+
208
+ if return_rect:
209
+ return (p_value, z_score, lp, q_obs, min_rect)
210
+ return (p_value, z_score, lp, q_obs)
211
+
212
+ def _min_tail_prob_at_least_as_extreme(n_total: int, p: float, q_threshold: float) -> float:
213
+ """
214
+ For S ~ Binomial(n_total, p), let q(s) = P(S >= s) be the upper-tail
215
+ p-value function (non-increasing, right-continuous step function of s).
216
+ Returns P(q(S) <= q_threshold), exactly, with no simulation: this is
217
+ just sf(s* - 1) where s* is the smallest integer count whose own tail
218
+ probability already drops to or below q_threshold.
219
+ """
220
+ s_values = np.arange(0, n_total + 1)
221
+ tail = binom.sf(s_values - 1, n_total, p) # monotone non-increasing in s
222
+ mask = tail <= q_threshold
223
+ if not mask.any():
224
+ return 0.0
225
+ s_star = int(s_values[mask][0])
226
+ return float(binom.sf(s_star - 1, n_total, p))
227
+
228
+
229
+ def scanStatisticTestBonferroni(data: np.ndarray, return_rect: bool = False):
230
+ """
231
+ Exact (analytic, no simulation) UPPER BOUND on P(Q <= q_obs), via the
232
+ union bound over the fixed set of 38 rectangles:
233
+
234
+ P(Q <= q) = P(exists r: q_r <= q) <= sum_r P(q_r <= q)
235
+
236
+ Unlike the Monte Carlo version, this has no floor on how small a
237
+ p-value it can report -- useful when the true p-value is far smaller
238
+ than 1/n_sims. It is conservative: the true p-value is <= this bound.
239
+
240
+ Returns (bound, z_score, lp, q_obs) by default, or includes the
241
+ rectangle with the minimum q-value when return_rect=True.
242
+ """
243
+ data = np.asarray(data)
244
+ if data.shape != (GRID_SIZE, GRID_SIZE):
245
+ raise ValueError(f"expected a {GRID_SIZE}x{GRID_SIZE} array")
246
+
247
+ n_total = int(data.sum())
248
+ flat_obs = data.reshape(-1)
249
+ q_obs, min_rect = _compute_Q(flat_obs, n_total)
250
+
251
+ if n_total == 0:
252
+ raise ValueError("cannot compute scan statistic for empty data (n_total=0)")
253
+
254
+ bound = 0.0
255
+ for rect in _RECTS:
256
+ p = rect[5]
257
+ bound += _min_tail_prob_at_least_as_extreme(n_total, p, q_obs)
258
+ bound = min(bound, 1.0) # a union bound can nominally exceed 1
259
+
260
+ z_score = stats.norm.isf(bound)
261
+ lp = np.inf if bound == 0 else -np.log(bound)
262
+ if return_rect:
263
+ return (bound, z_score, lp, q_obs, min_rect)
264
+ return (bound, z_score, lp, q_obs)
265
+
266
+ # ---------------------------------------------------------------------------
267
+ # Demo
268
+ # ---------------------------------------------------------------------------
269
+
270
+ # if __name__ == "__main__":
271
+ # rng = np.random.default_rng(0)
272
+
273
+ # # Example: 40 observations thrown uniformly at random (should look "null-ish")
274
+ # example = rng.multinomial(40, np.full(25, 1 / 25)).reshape(5, 5)
275
+ # print("Example data:\n", example)
276
+
277
+ # t_obs = int(np.sum(example == 0))
278
+ # p_occ = occupancyStatisticTest(example)
279
+ # print(f"\nOccupancy test: t_obs = {t_obs}, P(T >= t_obs) = {p_occ:.4f}")
280
+
281
+ # q_obs, p_scan = scanStatisticTest(example, n_sims=2000000, random_state=1)
282
+ # print(f"Scan statistic test: q_obs = {q_obs:.4f}, P(Q <= q_obs) = {p_scan:.4f}")
283
+
284
+ # # Example designed to trip the tests: a clustered/skewed pattern
285
+ # skewed = np.array([
286
+ # [10, 10, 0, 0, 0],
287
+ # [10, 10, 0, 0, 0],
288
+ # [0, 0, 0, 0, 0],
289
+ # [0, 0, 0, 0, 0],
290
+ # [0, 0, 0, 0, 0],
291
+ # ])
292
+ # print("\nSkewed data:\n", skewed)
293
+ # t_obs2 = int(np.sum(skewed == 0))
294
+ # p_occ2 = occupancy_test(skewed)
295
+ # print(f"\nOccupancy test: t_obs = {t_obs2}, P(T >= t_obs) = {p_occ2:.6f}")
296
+
297
+ # q_obs2, p_scan2 = scan_statistic_test(skewed, n_sims=20000, random_state=1)
298
+ # print(f"Scan statistic test: q_obs = {q_obs2:.6f}, P(Q <= q_obs) = {p_scan2:.6f}")
SSTT_visuals/mos.py ADDED
@@ -0,0 +1,293 @@
1
+ import scipy.stats as stats
2
+ import gmpy2
3
+ from math import log, factorial, perm, sqrt, pi
4
+ from typing import Self, overload
5
+ from functools import cache
6
+ from sys import getsizeof
7
+ from time import perf_counter
8
+ try:
9
+ from .primeslist import PRIMES
10
+ except ImportError: # pragma: no cover - compatibility fallback for direct script execution
11
+ from primeslist import PRIMES
12
+
13
+ class PDFrac():
14
+ __slots__ = ('numerator', 'denominator', 'dmax', 'q')
15
+
16
+ @overload
17
+ def __init__(self, numerator : int): pass
18
+ @overload
19
+ def __init__(self, numerator : int, denominator : int): pass
20
+
21
+
22
+ def __init__(self, numerator, denominator = 1, dmax = None, q = 1):
23
+ # if not (type(numerator) is int\
24
+ # or type(numerator) is gmpy2.mpz)\
25
+ # or not (type(denominator) is int\
26
+ # or type(denominator) is gmpy2.mpz):
27
+ # raise TypeError(f"PDFrac recieved {type(numerator)}, {type(denominator)}")
28
+
29
+ # dmax represents the largest witnessed multiple of the denominator. This is
30
+ # a simple upper bound for the largest prime that could divide the denominator.
31
+
32
+ self.q = q
33
+ self.dmax = dmax if dmax is not None else denominator
34
+ self.numerator = gmpy2.mpz(numerator)
35
+ self.denominator = gmpy2.mpz(denominator)
36
+
37
+ def __add__(self, frac : Self|int):
38
+ if type(self) != PDFrac \
39
+ or type(frac) not in (PDFrac, int):
40
+ err_str = f"Addition defined only between PDFrac and PDFrac|int, "\
41
+ + f"but received {type(self)}, {type(frac)}"
42
+ raise TypeError(err_str)
43
+
44
+ if type(frac) == int:
45
+ frac = PDFrac(frac)
46
+
47
+ if self.numerator == 0:
48
+ return frac.copy()
49
+ if frac.numerator == 0:
50
+ return self.copy()
51
+
52
+ res = self.copy()
53
+
54
+ res.numerator = res.numerator * frac.denominator + frac.numerator * res.denominator
55
+ res.denominator = res.denominator * frac.denominator
56
+ res.dmax = max(self.denominator, frac.denominator)
57
+ res.q = max(self.q, frac.q)
58
+ return res
59
+
60
+ def __mul__(self, frac : Self|int):
61
+ if type(self) != PDFrac \
62
+ or type(frac) not in (PDFrac, int):
63
+ err_str = f"Multiplication defined only between PDFrac and PDFrac|int, "\
64
+ + f"but received {type(self)}, {type(frac)}"
65
+ raise TypeError(err_str)
66
+
67
+ if type(frac) == int:
68
+ frac = PDFrac(frac)
69
+
70
+ if self.numerator == 0 or frac.numerator == 0:
71
+ return PDFrac(0)
72
+
73
+ if self.numerator == 1 and self.denominator == 1:
74
+ return frac.copy()
75
+ if frac.numerator == 1 and frac.denominator == 1:
76
+ return self.copy()
77
+
78
+ res = self.copy()
79
+
80
+ res.numerator *= frac.numerator
81
+ res.denominator *= frac.denominator
82
+ res.dmax = max(self.denominator, frac.denominator)
83
+ res.q = self.q + frac.q
84
+ return res
85
+
86
+ def simplify(self):
87
+ largest_undivided_prime = 1
88
+ for i in range(len(PRIMES)):
89
+ p = PRIMES[i]
90
+ if p > self.dmax or p > self.numerator:
91
+ break
92
+
93
+ # if (p == 2 or self.q > 2) and p <= MAXP:
94
+ if self.q > 4 and p <= MAXP:
95
+ self.pOrderBSearch(p = p, i = i)
96
+ if self.denominator % p == 0:
97
+ largest_undivided_prime = p
98
+ continue
99
+
100
+ while True:
101
+ if self.denominator % p != 0:
102
+ break
103
+ if self.numerator % p != 0:
104
+ largest_undivided_prime = p
105
+ break
106
+ self.numerator //= p
107
+ self.denominator //= p
108
+ self.dmax = largest_undivided_prime
109
+
110
+ def pOrderBSearch(self, p, i):
111
+ if self.numerator % p != 0 or self.denominator % p != 0:
112
+ return
113
+
114
+ pk = PPOW[i][-1]
115
+ while self.numerator % pk == 0 and self.denominator % pk == 0:
116
+ self.denominator //= pk
117
+ self.numerator //= pk
118
+
119
+ self.pobs(i)
120
+ return
121
+
122
+ def pobs(self, i, d_low = None):
123
+ # d_low == True => (d | pk => n | pk). Hence only check d divis.
124
+ # d_low == False => (n | pk => d | pk). Hence only check n divis.
125
+ # d_low is None implies nothing. Hence check both
126
+
127
+ for pk in reversed(PPOW[i][:-1]):
128
+ if d_low is False:
129
+ d_divided = True
130
+ n_divided = (self.numerator % pk == 0)
131
+ elif d_low is True:
132
+ d_divided = (self.denominator % pk == 0)
133
+ n_divided = True
134
+ else:
135
+ d_divided = (self.denominator % pk == 0)
136
+ n_divided = (self.numerator % pk == 0)
137
+
138
+ if d_divided and n_divided:
139
+ self.denominator //= pk
140
+ self.numerator //= pk
141
+ elif d_low is None:
142
+ if d_divided:
143
+ d_low = False
144
+ elif n_divided:
145
+ d_low = True
146
+
147
+ return
148
+
149
+ def to_float(self) -> float:
150
+ return float(self.numerator / self.denominator)
151
+
152
+ def copy(self) -> Self:
153
+ new = PDFrac.__new__(PDFrac)
154
+ new.numerator = self.numerator
155
+ new.denominator = self.denominator
156
+ new.dmax = self.dmax
157
+ new.q = self.q
158
+ return new
159
+
160
+ def A(q: int, r: int, n: int, m: int) -> PDFrac:
161
+ q = gmpy2.mpz(q); r = gmpy2.mpz(r); n = gmpy2.mpz(n); m = gmpy2.mpz(m)
162
+ if q == 0:
163
+ return PDFrac(m**n)
164
+ rq = r * q
165
+ numerator = perm(n, rq) # n! / (n-rq)!
166
+ denominator = factorial(r)**q * factorial(q) # (r!)^q * q!
167
+ if m == q:
168
+ numerator *= factorial(m) # m!
169
+ else:
170
+ numerator *= perm(m, q) * (m - q)**(n - rq) # (m! / (m-q)!) * (m - q)^(n-rq)
171
+ return PDFrac(numerator, denominator, dmax=max(q, r), q=q)
172
+
173
+ @cache # Cache output for dynamic programming
174
+ def maxOrderStatistic(r : int, n : int, m : int) -> PDFrac:
175
+ # P(max urn <= r | marbles = n, urns = m)
176
+ #r : cdf
177
+ #n : number of balls
178
+ #m : number of urns
179
+ if r==0 and n!=0:
180
+ return PDFrac(0)
181
+ if r==0 and n==0:
182
+ return PDFrac(1)
183
+ if r >= n:
184
+ return PDFrac(1)
185
+ if n==0:
186
+ return PDFrac(1)
187
+
188
+ if r == 1:
189
+ if m >= n:
190
+ return PDFrac(perm(m, n), m**n)
191
+ else:
192
+ return PDFrac(0)
193
+ else:
194
+ probability = PDFrac(0)
195
+ qMax = n//r + 1
196
+ qMin = max(0, n - r*m + m)
197
+ for q in range(qMin, qMax):
198
+ probability += A(q, r, n, m) * maxOrderStatistic(r-1, n-r*q, m-q)
199
+ # probability.simplify()
200
+
201
+ probability *= PDFrac(1, m**n)
202
+ probability.simplify()
203
+
204
+ return probability
205
+
206
+ # Very small p_values <~ 1e-16 can't be captured by conjugating a float due to the scale mismatch
207
+ # between the p_value and 1. Hence, conjugation must be performed on the fraction before converting
208
+ # it to a float.
209
+ def conjMaxOrderStatistic(s, marbles, urns):
210
+ q_frac = maxOrderStatistic(s - 1, marbles, urns) # q = P(max urn < s | marbles, urns)
211
+ p_frac = q_frac.copy() # Cached mutable must be copied pre-edit
212
+ p_frac.numerator = p_frac.denominator - p_frac.numerator
213
+ return p_frac, q_frac
214
+
215
+ def PandZ(urnMax : int, marbles : int, urns : int, initialise = True) -> tuple[float, float, float]:
216
+ # urnMax = the number of marbles in the urn with the maximum number
217
+ # marbles = how many marbles did you place in the urns
218
+ # urns = the number of urns the marbles were distributed between
219
+
220
+ urnMax = int(urnMax); marbles = int(marbles); urns = int(urns)
221
+
222
+ if urnMax > 3000 or marbles > 6000:
223
+ raise NotImplementedError("Only supports urnMax <= P and marbles <= 2P, where P " +\
224
+ "is the largest prime in primeslist.PRIMES\n" +\
225
+ "Currently, P = 3000.")
226
+
227
+ if initialise:
228
+ initPPOW(prime_max = max(marbles, urns))
229
+
230
+ p_value_frac, _ = conjMaxOrderStatistic(urnMax, marbles, urns)
231
+ p_value = p_value_frac.to_float()
232
+ if p_value <= 0:
233
+ # print(f"Warning: nonpositive p-value {p_value:.2e}")
234
+ # p_value = 1e-307
235
+ z_score = normal_isf_asymptotic(p_value_frac)
236
+ lp = None
237
+ else:
238
+ z_score = stats.norm.isf(p_value)
239
+ lp = -log(p_value)
240
+ return (p_value, z_score, lp)
241
+
242
+ def initPPOW(prime_max, pow_max = 1e100, metrics = False):
243
+ global PPOW
244
+ global MAXP
245
+ PPOW = []
246
+ MAXP = prime_max
247
+
248
+ if metrics:
249
+ size = 0
250
+ pow_start = perf_counter()
251
+ for prime in PRIMES:
252
+ if prime > MAXP:
253
+ break
254
+ pow_list = []
255
+ pk = prime
256
+ while pk < pow_max:
257
+ if metrics:
258
+ size += getsizeof(pk)
259
+ pow_list.append(pk)
260
+ pk = pk**2
261
+ PPOW.append(pow_list)
262
+
263
+ if metrics:
264
+ pow_end = perf_counter()
265
+ print(f"Prime time: {pow_end-pow_start:.3f}")
266
+ print(f"Prime memory: {size}")
267
+
268
+ return
269
+
270
+ def normal_isf_asymptotic(frac, iters=4):
271
+ L = gmpy2.log(frac.denominator) - gmpy2.log(frac.numerator)
272
+ x2 = 2 * L
273
+ for _ in range(iters):
274
+ x = sqrt(x2)
275
+ x2 = 2*L - 2*log(x) - log(2*pi) + 2/x2
276
+ return sqrt(x2)
277
+
278
+ def clear_caches():
279
+ maxOrderStatistic.cache_clear()
280
+ return
281
+
282
+ def cache_size_MB():
283
+ return maxOrderStatistic.cache_info()[3]/ (1024 * 1024)
284
+
285
+ def main():
286
+ initPPOW(100, metrics = True)
287
+ start = perf_counter()
288
+ PandZ(50, 100, 100) # Takes 1.6-1.7 seconds on a 3.8GHz Intel Core i5
289
+ end = perf_counter()
290
+ print(f"Total time: {end-start:.3f}")
291
+
292
+ if __name__ == "__main__":
293
+ main()