pyrenyi 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.
pyrenyi/__init__.py ADDED
@@ -0,0 +1,16 @@
1
+ """PyRenyi: Renyi generalised dimensions D_q of point distributions.
2
+
3
+ Fixed-radius (point-centred and volume-weighted) and fixed-mass estimators,
4
+ for periodic boxes and for surveys with random catalogues and weights.
5
+ """
6
+ from . import mocks, regions
7
+ from .catalogue import Catalogue
8
+ from .fixed_mass import FixedMassResult, fixed_mass
9
+ from .fixed_radius import DEFAULT_Q, measure
10
+ from .io import read_catalogue
11
+ from .results import RenyiResult, jackknife_covariance, load
12
+
13
+ __version__ = '0.1.0'
14
+
15
+ __all__ = ['Catalogue', 'measure', 'fixed_mass', 'RenyiResult', 'FixedMassResult', 'load',
16
+ 'jackknife_covariance', 'DEFAULT_Q', 'read_catalogue', 'mocks', 'regions', '__version__']
pyrenyi/_common.py ADDED
@@ -0,0 +1,103 @@
1
+ """Shared input handling: geometry, q grid, jackknife regions, chunking."""
2
+ import math
3
+
4
+ import numba
5
+ import numpy as np
6
+
7
+ from . import regions as _regions
8
+
9
+ DEFAULT_Q = np.round(np.arange(-4.0, 6.0 + 1e-9, 0.5), 10)
10
+
11
+
12
+ def parse_box(box):
13
+ """None (no periodic box) or the box side lengths (3,)."""
14
+ if box is None:
15
+ return None
16
+ L = np.asarray(box, dtype=np.float64) * np.ones(3)
17
+ if L.shape != (3,) or np.any(L <= 0) or not np.all(np.isfinite(L)):
18
+ raise ValueError('box must be a positive scalar or three positive side lengths')
19
+ return L
20
+
21
+
22
+ def check_in_box(pos, L, name):
23
+ """Points must lie in [0, L); points at exactly L are mapped to 0."""
24
+ if np.any(pos < 0) or np.any(pos > L):
25
+ raise ValueError(f'{name}: positions must lie in [0, box) for a periodic box '
26
+ '(wrap them with np.mod(positions, box))')
27
+ if np.any(pos == L):
28
+ pos = pos.copy()
29
+ pos[pos == np.broadcast_to(L, pos.shape)] = 0.0
30
+ return pos
31
+
32
+
33
+ def parse_q(q):
34
+ q = DEFAULT_Q if q is None else np.atleast_1d(np.asarray(q, dtype=np.float64))
35
+ if q.ndim != 1 or not len(q) or not np.all(np.isfinite(q)):
36
+ raise ValueError('q must be a non-empty 1-d array of finite values')
37
+ return q
38
+
39
+
40
+ def is_integer(x, tol=1e-12):
41
+ return abs(x - round(x)) < tol
42
+
43
+
44
+ def set_threads(n):
45
+ if n is not None:
46
+ numba.set_num_threads(int(n))
47
+ return numba.get_num_threads()
48
+
49
+
50
+ def region_labels(jackknife, centres, L, seed):
51
+ """Integer labels 0..n-1 of the centres, and n (1 without jackknife).
52
+ jackknife: None, a number of regions, or one label per centre."""
53
+ nc = len(centres)
54
+ if jackknife is None or (np.isscalar(jackknife) and int(jackknife) <= 1):
55
+ return np.zeros(nc, np.int64), 1
56
+ if np.isscalar(jackknife):
57
+ n = int(jackknife)
58
+ if L is not None:
59
+ side = int(round(n ** (1.0 / 3.0)))
60
+ if side ** 3 != n:
61
+ raise ValueError(f'in a periodic box, jackknife must be a cube number of sub-boxes '
62
+ f'(e.g. {side ** 3} or {(side + 1) ** 3}), got {n}')
63
+ lab = _regions.subboxes(centres, L, side)
64
+ else:
65
+ lab = _regions.assign(centres, _regions.kmeans(centres, n, seed=seed))
66
+ return _regions.relabel(lab)
67
+ lab = np.asarray(jackknife).reshape(-1)
68
+ if len(lab) != nc:
69
+ raise ValueError('jackknife labels must have one entry per centre')
70
+ return _regions.relabel(lab)
71
+
72
+
73
+ def chunks(order_regions, nreg, nthr, chunk_size=None):
74
+ """Chunk boundaries over centres sorted by region; each chunk lies in one
75
+ region. Returns (chunk_start, chunk_region)."""
76
+ nc = len(order_regions)
77
+ cs = chunk_size or max(16, int(math.ceil(nc / (8 * max(nthr, 1)))))
78
+ bounds = np.searchsorted(order_regions, np.arange(nreg + 1))
79
+ start, creg = [], []
80
+ for r in range(nreg):
81
+ a, b = bounds[r], bounds[r + 1]
82
+ if b <= a:
83
+ continue
84
+ for s in range(a, b, cs):
85
+ start.append(s)
86
+ creg.append(r)
87
+ start.append(nc)
88
+ return np.asarray(start, np.int64), np.asarray(creg, np.int64)
89
+
90
+
91
+ def reduce_regions(arr, chunk_region, nreg):
92
+ """Sum per-chunk arrays into per-region arrays."""
93
+ out = np.zeros((nreg,) + arr.shape[1:])
94
+ np.add.at(out, chunk_region, arr)
95
+ return out
96
+
97
+
98
+ def unit_vector(v):
99
+ v = np.asarray(v, dtype=np.float64).reshape(3)
100
+ n = np.linalg.norm(v)
101
+ if n == 0:
102
+ raise ValueError('line-of-sight vector must be non-zero')
103
+ return v / n
pyrenyi/_grid.py ADDED
@@ -0,0 +1,51 @@
1
+ """Cell lists for the neighbour search, periodic or not."""
2
+ import numpy as np
3
+
4
+ MAX_CELLS = 2 ** 24
5
+
6
+
7
+ class Grid:
8
+ """A regular grid of cells covering a periodic box or the bounding box of
9
+ a set of point arrays, with cells of side >= r_max / 2.
10
+
11
+ Attributes
12
+ ----------
13
+ lo : (3,) lower corner; size : (3,) extent; n : (3,) cells per axis;
14
+ cell : (3,) cell side; reach : (3,) cells to visit on each side of a
15
+ centre's cell to cover r_max; periodic : bool.
16
+ """
17
+
18
+ def __init__(self, rmax, arrays, box=None):
19
+ if box is not None:
20
+ self.periodic = True
21
+ self.lo = np.zeros(3)
22
+ self.size = np.asarray(box, dtype=np.float64) * np.ones(3)
23
+ else:
24
+ self.periodic = False
25
+ pts = [a for a in arrays if a is not None and len(a)]
26
+ lo = np.min([a.min(0) for a in pts], 0)
27
+ hi = np.max([a.max(0) for a in pts], 0)
28
+ pad = 1e-9 * max(1.0, float(np.max(hi - lo)))
29
+ self.lo = lo - pad
30
+ self.size = np.maximum(hi - lo + 2 * pad, 1e-6)
31
+ n = np.maximum(np.floor(self.size / (rmax / 2.0)), 1).astype(np.int64)
32
+ while np.prod(n) > MAX_CELLS:
33
+ n = np.maximum(n // 2, 1)
34
+ self.n = n
35
+ self.cell = self.size / n
36
+ self.reach = np.ceil(rmax / self.cell).astype(np.int64)
37
+
38
+ def cell_index(self, pos):
39
+ """Flat cell index of each position."""
40
+ c = np.floor((pos - self.lo) / self.cell).astype(np.int64)
41
+ c = np.clip(c, 0, self.n - 1)
42
+ return (c[:, 0] * self.n[1] + c[:, 1]) * self.n[2] + c[:, 2]
43
+
44
+ def build(self, pos):
45
+ """(order, start): points of cell c are order[start[c]:start[c + 1]]."""
46
+ if pos is None or len(pos) == 0:
47
+ return np.zeros(0, np.int64), np.zeros(int(np.prod(self.n)) + 1, np.int64)
48
+ cid = self.cell_index(pos)
49
+ order = np.argsort(cid, kind='stable').astype(np.int64)
50
+ start = np.searchsorted(cid[order], np.arange(int(np.prod(self.n)) + 1)).astype(np.int64)
51
+ return order, start
pyrenyi/_kernels.py ADDED
@@ -0,0 +1,515 @@
1
+ """Numba kernels: neighbour sums in spheres, shells and wedges about each
2
+ centre, and the per-centre moments that make up the partition functions.
3
+
4
+ All kernels parallelise over chunks of centres; each chunk writes only its own
5
+ slice of the output, so no atomics are needed.
6
+ """
7
+ import math
8
+
9
+ import numpy as np
10
+ from numba import njit, prange
11
+
12
+
13
+ @njit(cache=True)
14
+ def digamma(x):
15
+ """psi(x) for x > 0 (recurrence to x >= 10, then the asymptotic series)."""
16
+ r = 0.0
17
+ while x < 10.0:
18
+ r -= 1.0 / x
19
+ x += 1.0
20
+ f = 1.0 / (x * x)
21
+ t = f * (-1.0 / 12.0 + f * (1.0 / 120.0 + f * (-1.0 / 252.0 + f * (1.0 / 240.0
22
+ + f * (-1.0 / 132.0)))))
23
+ return r + math.log(x) - 0.5 / x + t
24
+
25
+
26
+ @njit(cache=True)
27
+ def _cells(c, reach, n, periodic, buf):
28
+ """Cell indices along one axis within `reach` of cell c; returns the count."""
29
+ m = 0
30
+ if periodic:
31
+ if 2 * reach + 1 >= n:
32
+ for k in range(n):
33
+ buf[k] = k
34
+ return n
35
+ for o in range(-reach, reach + 1):
36
+ buf[m] = (c + o) % n
37
+ m += 1
38
+ return m
39
+ lo = max(0, c - reach)
40
+ hi = min(n - 1, c + reach)
41
+ for k in range(lo, hi + 1):
42
+ buf[m] = k
43
+ m += 1
44
+ return m
45
+
46
+
47
+ @njit(cache=True)
48
+ def _cell_of(x, lo, cell, n):
49
+ c = int(math.floor((x - lo) / cell))
50
+ if c < 0:
51
+ return 0
52
+ if c > n - 1:
53
+ return n - 1
54
+ return c
55
+
56
+
57
+ @njit(cache=True)
58
+ def _wedge(ex, ey, ez, d2, lx, ly, lz, mu_edges, equal_mu):
59
+ """Index of the |mu| wedge of separation e about the line of sight l."""
60
+ ll = lx * lx + ly * ly + lz * lz
61
+ if d2 <= 0.0 or ll <= 0.0:
62
+ return -1
63
+ mu = abs(ex * lx + ey * ly + ez * lz) / math.sqrt(d2 * ll)
64
+ nmu = mu_edges.shape[0] - 1
65
+ if equal_mu:
66
+ w = int(mu * nmu)
67
+ else:
68
+ w = 0
69
+ while w < nmu - 1 and mu >= mu_edges[w + 1]:
70
+ w += 1
71
+ if w > nmu - 1:
72
+ w = nmu - 1
73
+ return w
74
+
75
+
76
+ @njit(parallel=True, cache=True)
77
+ def fixed_radius(cen, cen_self, cen_rself, cen_c, cen_u, mu_norm, sb_fac, cen_los, chunk_start,
78
+ dpos, du, d_order, d_start, rpos, rw, rdv, r_order, r_start,
79
+ lo, cell, ncell, reach, periodic, boxl,
80
+ r2, case_start, vol, vsph, mu_edges, equal_mu, los_mid, observer,
81
+ s_vals, s_int, special, volume, unit_w, corr, u0, smax, fact,
82
+ have_rand, rand_corr, unit_rw, rw_mean, min_rand, fmin, rsplit, out_num, out_den):
83
+ """Per-chunk sums of c_i * (moment of the neighbour mass about centre i).
84
+
85
+ For every centre, the data (and random) neighbours are binned in shells
86
+ between consecutive radii and in |mu| wedges; the shells are accumulated
87
+ into spheres (case 0) or into shells beyond a minimum separation (case e,
88
+ starting at bin case_start[e]); for each region the expected neighbour
89
+ mass mu = mu_norm[i] * (random sum or analytic volume) normalises the
90
+ finite-sample-corrected moment T_s, and c_i * T_s / mu^s is added to
91
+ out_num (out_den holds the matching normalisation). With rand_corr, the
92
+ power mu^-s of the random-based expectation is replaced by its
93
+ Poisson-unbiased estimate alpha^-s Gamma(m + 1) / Gamma(m + 1 + s) of the
94
+ random count m (an effective count for weighted randoms). A region with
95
+ fewer than min_rand randoms takes its expectation from the smallest sphere
96
+ about the centre with at least min_rand randoms (or the sphere of the same
97
+ radius), scaled by the ratio of the region and sphere volumes. With
98
+ rsplit, the odd-numbered randoms give the footprint fraction (for the edge
99
+ cut) and the even-numbered ones the expected counts, so that the cut does
100
+ not select on the noise of the expected counts.
101
+ """
102
+ nch = chunk_start.shape[0] - 1
103
+ nr = r2.shape[0]
104
+ nq = s_vals.shape[0]
105
+ ncase = case_start.shape[0]
106
+ nmu = mu_edges.shape[0] - 1
107
+ nW = nmu + 1 if nmu > 1 else 1
108
+ iso = nW - 1
109
+ S = smax
110
+ r2max = r2[nr - 1]
111
+ have_dv = rdv.shape[0] > 0
112
+ nbuf = max(ncell.max(), 2 * reach.max() + 1)
113
+ for ch in prange(nch):
114
+ E = np.zeros((nr, nW, S + 1))
115
+ Ncnt = np.zeros((nr, nW), np.int64)
116
+ P1 = np.zeros((nr, nW))
117
+ P2 = np.zeros((nr, nW))
118
+ Rs = np.zeros((nr, nW))
119
+ Rn = np.zeros((nr, nW), np.int64)
120
+ R2 = np.zeros((nr, nW))
121
+ DV = np.zeros(nr)
122
+ Ec = np.zeros((nW, S + 1))
123
+ tmp = np.zeros(S + 1)
124
+ Ncum = np.zeros(nW, np.int64)
125
+ Q1 = np.zeros(nW)
126
+ Q2 = np.zeros(nW)
127
+ Rc = np.zeros(nW)
128
+ Rnc = np.zeros(nW, np.int64)
129
+ R2c = np.zeros(nW)
130
+ sR = np.zeros(nr)
131
+ sR2 = np.zeros(nr)
132
+ sN = np.zeros(nr, np.int64)
133
+ fr = np.ones(nr)
134
+ bx = np.empty(nbuf, np.int64)
135
+ by = np.empty(nbuf, np.int64)
136
+ bz = np.empty(nbuf, np.int64)
137
+ for i in range(chunk_start[ch], chunk_start[ch + 1]):
138
+ E[:, :, :] = 0.0
139
+ E[:, :, 0] = 1.0
140
+ Ncnt[:, :] = 0
141
+ P1[:, :] = 0.0
142
+ P2[:, :] = 0.0
143
+ Rs[:, :] = 0.0
144
+ Rn[:, :] = 0
145
+ R2[:, :] = 0.0
146
+ DV[:] = 0.0
147
+ x = cen[i, 0]
148
+ y = cen[i, 1]
149
+ z = cen[i, 2]
150
+ nx = _cells(_cell_of(x, lo[0], cell[0], ncell[0]), reach[0], ncell[0], periodic, bx)
151
+ ny = _cells(_cell_of(y, lo[1], cell[1], ncell[1]), reach[1], ncell[1], periodic, by)
152
+ nz = _cells(_cell_of(z, lo[2], cell[2], ncell[2]), reach[2], ncell[2], periodic, bz)
153
+ lx = cen_los[i, 0]
154
+ ly = cen_los[i, 1]
155
+ lz = cen_los[i, 2]
156
+ # ---- data neighbours ----
157
+ for a in range(nx):
158
+ for b in range(ny):
159
+ for c in range(nz):
160
+ cid = (bx[a] * ncell[1] + by[b]) * ncell[2] + bz[c]
161
+ for t in range(d_start[cid], d_start[cid + 1]):
162
+ j = d_order[t]
163
+ if j == cen_self[i]:
164
+ continue
165
+ ex = dpos[j, 0] - x
166
+ ey = dpos[j, 1] - y
167
+ ez = dpos[j, 2] - z
168
+ if periodic:
169
+ ex -= boxl[0] * np.round(ex / boxl[0])
170
+ ey -= boxl[1] * np.round(ey / boxl[1])
171
+ ez -= boxl[2] * np.round(ez / boxl[2])
172
+ d2 = ex * ex + ey * ey + ez * ez
173
+ if d2 > r2max:
174
+ continue
175
+ kb = np.searchsorted(r2, d2)
176
+ w = -1
177
+ if nmu > 1:
178
+ if los_mid:
179
+ w = _wedge(ex, ey, ez, d2, 0.5 * (dpos[j, 0] + x) - observer[0],
180
+ 0.5 * (dpos[j, 1] + y) - observer[1],
181
+ 0.5 * (dpos[j, 2] + z) - observer[2], mu_edges, equal_mu)
182
+ else:
183
+ w = _wedge(ex, ey, ez, d2, lx, ly, lz, mu_edges, equal_mu)
184
+ if unit_w:
185
+ Ncnt[kb, iso] += 1
186
+ if w >= 0:
187
+ Ncnt[kb, w] += 1
188
+ else:
189
+ u = du[j]
190
+ for k in range(S, 0, -1):
191
+ E[kb, iso, k] += u * E[kb, iso, k - 1]
192
+ P1[kb, iso] += u
193
+ P2[kb, iso] += u * u
194
+ if w >= 0:
195
+ for k in range(S, 0, -1):
196
+ E[kb, w, k] += u * E[kb, w, k - 1]
197
+ P1[kb, w] += u
198
+ P2[kb, w] += u * u
199
+ # ---- random neighbours ----
200
+ if have_rand:
201
+ for a in range(nx):
202
+ for b in range(ny):
203
+ for c in range(nz):
204
+ cid = (bx[a] * ncell[1] + by[b]) * ncell[2] + bz[c]
205
+ for t in range(r_start[cid], r_start[cid + 1]):
206
+ j = r_order[t]
207
+ if j == cen_rself[i]:
208
+ continue
209
+ ex = rpos[j, 0] - x
210
+ ey = rpos[j, 1] - y
211
+ ez = rpos[j, 2] - z
212
+ if periodic:
213
+ ex -= boxl[0] * np.round(ex / boxl[0])
214
+ ey -= boxl[1] * np.round(ey / boxl[1])
215
+ ez -= boxl[2] * np.round(ez / boxl[2])
216
+ d2 = ex * ex + ey * ey + ez * ez
217
+ if d2 > r2max:
218
+ continue
219
+ kb = np.searchsorted(r2, d2)
220
+ if rsplit:
221
+ # odd randoms: footprint fraction only
222
+ if j % 2 == 1:
223
+ DV[kb] += rdv[j]
224
+ continue
225
+ elif have_dv:
226
+ DV[kb] += rdv[j]
227
+ wj = rw[j]
228
+ Rs[kb, iso] += wj
229
+ Rn[kb, iso] += 1
230
+ R2[kb, iso] += wj * wj
231
+ if nmu > 1:
232
+ if los_mid:
233
+ w = _wedge(ex, ey, ez, d2, 0.5 * (rpos[j, 0] + x) - observer[0],
234
+ 0.5 * (rpos[j, 1] + y) - observer[1],
235
+ 0.5 * (rpos[j, 2] + z) - observer[2], mu_edges, equal_mu)
236
+ else:
237
+ w = _wedge(ex, ey, ez, d2, lx, ly, lz, mu_edges, equal_mu)
238
+ if w >= 0:
239
+ Rs[kb, w] += wj
240
+ Rn[kb, w] += 1
241
+ R2[kb, w] += wj * wj
242
+ # cumulative random sums in spheres: the reference for regions
243
+ # with fewer than min_rand randoms, and the footprint fraction
244
+ bi = 0
245
+ if have_rand:
246
+ a1 = 0.0
247
+ a2 = 0.0
248
+ a3 = 0
249
+ for ir in range(nr):
250
+ a1 += Rs[ir, iso]
251
+ a2 += R2[ir, iso]
252
+ a3 += Rn[ir, iso]
253
+ sR[ir] = a1
254
+ sR2[ir] = a2
255
+ sN[ir] = a3
256
+ if min_rand > 0:
257
+ bi = nr - 1
258
+ for ir in range(nr):
259
+ if sN[ir] >= min_rand:
260
+ bi = ir
261
+ break
262
+ if fmin > 0.0 and have_dv:
263
+ acc = 0.0
264
+ for ir in range(nr):
265
+ acc += DV[ir]
266
+ fr[ir] = acc / vsph[ir]
267
+ cc = cen_c[i]
268
+ # ---- spheres / shells, per radius and wedge ----
269
+ for cs in range(ncase):
270
+ Ec[:, :] = 0.0
271
+ Ec[:, 0] = 1.0
272
+ Ncum[:] = 0
273
+ Q1[:] = 0.0
274
+ Q2[:] = 0.0
275
+ Rc[:] = 0.0
276
+ Rnc[:] = 0
277
+ R2c[:] = 0.0
278
+ for ir in range(case_start[cs], nr):
279
+ for w in range(nW):
280
+ if unit_w:
281
+ Ncum[w] += Ncnt[ir, w]
282
+ else:
283
+ if S > 0:
284
+ for k in range(S + 1):
285
+ acc = 0.0
286
+ for a2_ in range(k + 1):
287
+ acc += Ec[w, a2_] * E[ir, w, k - a2_]
288
+ tmp[k] = acc
289
+ for k in range(S + 1):
290
+ Ec[w, k] = tmp[k]
291
+ Q1[w] += P1[ir, w]
292
+ Q2[w] += P2[ir, w]
293
+ Rc[w] += Rs[ir, w]
294
+ Rnc[w] += Rn[ir, w]
295
+ R2c[w] += R2[ir, w]
296
+ if fmin > 0.0 and fr[max(ir, bi)] < fmin:
297
+ continue
298
+ for w in range(nW):
299
+ # expected neighbour mass mu, and for the random-count
300
+ # correction the count meff with mu = alpha * meff
301
+ meff = 0.0
302
+ if have_rand:
303
+ if min_rand > 0 and Rnc[w] < min_rand:
304
+ b = max(ir, bi)
305
+ if sN[b] == 0:
306
+ continue
307
+ rv = vol[cs, ir, w] / vsph[b]
308
+ mu = mu_norm[i] * sR[b] * rv
309
+ if unit_rw:
310
+ meff = float(sN[b])
311
+ lal = math.log(mu_norm[i] * rw_mean * rv)
312
+ else:
313
+ meff = sR[b] * sR[b] / sR2[b]
314
+ lal = math.log(mu_norm[i] * sR2[b] / sR[b] * rv)
315
+ else:
316
+ mu = mu_norm[i] * Rc[w]
317
+ if Rnc[w] > 0:
318
+ if unit_rw:
319
+ meff = float(Rnc[w])
320
+ lal = math.log(mu_norm[i] * rw_mean)
321
+ else:
322
+ meff = Rc[w] * Rc[w] / R2c[w]
323
+ lal = math.log(mu_norm[i] * R2c[w] / Rc[w])
324
+ else:
325
+ lal = math.log(mu_norm[i] * rw_mean)
326
+ else:
327
+ mu = mu_norm[i] * vol[cs, ir, w]
328
+ lal = 0.0
329
+ corr_r = have_rand and rand_corr
330
+ if mu <= 0.0 and not corr_r:
331
+ continue
332
+ lmu = math.log(mu) if mu > 0.0 else lal
333
+ if corr_r:
334
+ lgm = math.lgamma(meff + 1.0)
335
+ lnmu = lal + digamma(meff + 1.0)
336
+ else:
337
+ lal = lmu
338
+ lgm = 0.0
339
+ lnmu = lmu
340
+ if unit_w:
341
+ tot = float(Ncum[w])
342
+ neff = tot
343
+ lc = 0.0
344
+ else:
345
+ tot = Q1[w]
346
+ if tot > 0.0:
347
+ neff = tot * tot / Q2[w]
348
+ lc = math.log(Q2[w] / tot)
349
+ else:
350
+ neff = 0.0
351
+ lc = math.log(u0)
352
+ occ = tot > 0.0
353
+ lg1 = math.lgamma(neff + 1.0)
354
+ for iq in range(nq):
355
+ s = s_vals[iq]
356
+ val = 0.0
357
+ dval = 1.0
358
+ # log of the estimate of mu^-s
359
+ if corr_r:
360
+ a4 = meff + 1.0 + s
361
+ if a4 <= 0.0:
362
+ lms = -np.inf
363
+ else:
364
+ lms = -s * lal + lgm - math.lgamma(a4)
365
+ else:
366
+ lms = -s * lmu
367
+ if volume:
368
+ if s < 0.0:
369
+ continue
370
+ if special[iq] == 1: # q = 1
371
+ if not occ:
372
+ continue
373
+ if corr_r:
374
+ ratio = tot * math.exp(-lal + lgm - math.lgamma(meff + 2.0))
375
+ else:
376
+ ratio = tot / mu
377
+ if corr == 2:
378
+ val = ratio * (math.log(tot) - lnmu)
379
+ else:
380
+ val = ratio * (digamma(neff) + lc - lnmu)
381
+ dval = ratio
382
+ elif s == 0.0: # q = 0
383
+ val = 1.0 if occ else 0.0
384
+ elif occ:
385
+ if corr == 2:
386
+ val = math.exp(s * math.log(tot) + lms)
387
+ elif (not unit_w) and corr == 0 and s_int[iq] >= 1:
388
+ val = fact[s_int[iq]] * Ec[w, s_int[iq]] * math.exp(lms)
389
+ else:
390
+ a3 = neff + 1.0 - s
391
+ if a3 > 0.0:
392
+ val = math.exp(lg1 - math.lgamma(a3) + s * lc + lms)
393
+ else:
394
+ if corr == 2: # sandbox: centre included
395
+ tsb = tot + cen_u[i]
396
+ if tsb <= 0.0 or mu <= 0.0:
397
+ continue
398
+ lsb = math.log(mu * sb_fac[i])
399
+ if special[iq] == 1:
400
+ val = math.log(tsb) - lsb
401
+ else:
402
+ val = math.exp(s * (math.log(tsb) - lsb))
403
+ elif special[iq] == 1: # q = 1
404
+ val = digamma(neff + 1.0) + lc - lnmu
405
+ elif (not unit_w) and corr == 0 and s_int[iq] >= 1:
406
+ val = fact[s_int[iq]] * Ec[w, s_int[iq]] * math.exp(lms)
407
+ else:
408
+ a3 = neff + 1.0 - s
409
+ if a3 > 0.0:
410
+ val = math.exp(lg1 - math.lgamma(a3) + s * lc + lms)
411
+ out_num[ch, cs, iq, ir, w] += cc * val
412
+ out_den[ch, cs, iq, ir, w] += cc * dval
413
+
414
+
415
+ @njit(parallel=True, cache=True)
416
+ def random_sums_at_radii(cen, cen_rself, rad, rmin2, rpos, rw, rdv, r_order, r_start,
417
+ lo, cell, ncell, periodic, boxl, rsplit, out_r, out_n, out_r2, out_dv):
418
+ """Cumulative random sums within the per-centre radii rad[i, :] (ascending):
419
+ weight (out_r), count (out_n) and squared weight (out_r2) beyond r_min,
420
+ and volume element (out_dv) from d = 0. With rsplit the odd randoms give
421
+ only the volume and the even ones only the sums."""
422
+ nc, nk = rad.shape
423
+ have_dv = rdv.shape[0] > 0
424
+ for i in prange(nc):
425
+ r2i = np.empty(nk)
426
+ for k in range(nk):
427
+ r2i[k] = rad[i, k] * rad[i, k]
428
+ rmax = rad[i, nk - 1]
429
+ sr = np.zeros(nk)
430
+ sn = np.zeros(nk)
431
+ s2 = np.zeros(nk)
432
+ sv = np.zeros(nk)
433
+ x = cen[i, 0]
434
+ y = cen[i, 1]
435
+ z = cen[i, 2]
436
+ rx = int(math.ceil(rmax / cell[0]))
437
+ ry = int(math.ceil(rmax / cell[1]))
438
+ rz = int(math.ceil(rmax / cell[2]))
439
+ bx = np.empty(max(ncell[0], 2 * rx + 1), np.int64)
440
+ by = np.empty(max(ncell[1], 2 * ry + 1), np.int64)
441
+ bz = np.empty(max(ncell[2], 2 * rz + 1), np.int64)
442
+ nx = _cells(_cell_of(x, lo[0], cell[0], ncell[0]), rx, ncell[0], periodic, bx)
443
+ ny = _cells(_cell_of(y, lo[1], cell[1], ncell[1]), ry, ncell[1], periodic, by)
444
+ nz = _cells(_cell_of(z, lo[2], cell[2], ncell[2]), rz, ncell[2], periodic, bz)
445
+ for a in range(nx):
446
+ for b in range(ny):
447
+ for c in range(nz):
448
+ cid = (bx[a] * ncell[1] + by[b]) * ncell[2] + bz[c]
449
+ for t in range(r_start[cid], r_start[cid + 1]):
450
+ j = r_order[t]
451
+ if j == cen_rself[i]:
452
+ continue
453
+ ex = rpos[j, 0] - x
454
+ ey = rpos[j, 1] - y
455
+ ez = rpos[j, 2] - z
456
+ if periodic:
457
+ ex -= boxl[0] * np.round(ex / boxl[0])
458
+ ey -= boxl[1] * np.round(ey / boxl[1])
459
+ ez -= boxl[2] * np.round(ez / boxl[2])
460
+ d2 = ex * ex + ey * ey + ez * ez
461
+ if d2 > r2i[nk - 1]:
462
+ continue
463
+ kb = np.searchsorted(r2i, d2)
464
+ if rsplit:
465
+ if j % 2 == 1:
466
+ sv[kb] += rdv[j]
467
+ continue
468
+ elif have_dv:
469
+ sv[kb] += rdv[j]
470
+ if d2 > rmin2:
471
+ wj = rw[j]
472
+ sr[kb] += wj
473
+ sn[kb] += 1.0
474
+ s2[kb] += wj * wj
475
+ a1 = 0.0
476
+ a2 = 0.0
477
+ a3 = 0.0
478
+ a4 = 0.0
479
+ for k in range(nk):
480
+ a1 += sr[k]
481
+ a2 += sn[k]
482
+ a3 += s2[k]
483
+ a4 += sv[k]
484
+ out_r[i, k] = a1
485
+ out_n[i, k] = a2
486
+ out_r2[i, k] = a3
487
+ out_dv[i, k] = a4
488
+
489
+
490
+ @njit(parallel=True, cache=True)
491
+ def tau_sums(lnA, b, ok, c, chunk_start, taus, out):
492
+ """out[chunk, it, k] = sum_i c_i E[nu_ik^(-t)], t = tau_t / 3, over the
493
+ centres of the chunk with ok[i, k]. The estimate of nu^-t is
494
+ A^-t Gamma(b) / Gamma(b + t) (b > 0: from a Poisson random count m with
495
+ b = m + 1, or the distance to the m-th random with b = m), or A^-t
496
+ exactly (b <= 0)."""
497
+ nch = chunk_start.shape[0] - 1
498
+ nk = lnA.shape[1]
499
+ nt = taus.shape[0]
500
+ for ch in prange(nch):
501
+ for i in range(chunk_start[ch], chunk_start[ch + 1]):
502
+ for k in range(nk):
503
+ if not ok[i, k]:
504
+ continue
505
+ la = lnA[i, k]
506
+ bb = b[i, k]
507
+ if bb > 0.0:
508
+ lgb = math.lgamma(bb)
509
+ for it in range(nt):
510
+ t = taus[it] / 3.0
511
+ if bb + t > 0.0:
512
+ out[ch, it, k] += c[i] * math.exp(-t * la + lgb - math.lgamma(bb + t))
513
+ else:
514
+ for it in range(nt):
515
+ out[ch, it, k] += c[i] * math.exp(-taus[it] / 3.0 * la)