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 +16 -0
- pyrenyi/_common.py +103 -0
- pyrenyi/_grid.py +51 -0
- pyrenyi/_kernels.py +515 -0
- pyrenyi/catalogue.py +88 -0
- pyrenyi/cli.py +313 -0
- pyrenyi/fixed_mass.py +418 -0
- pyrenyi/fixed_radius.py +385 -0
- pyrenyi/io.py +184 -0
- pyrenyi/mocks.py +86 -0
- pyrenyi/regions.py +52 -0
- pyrenyi/results.py +162 -0
- pyrenyi-0.1.0.dist-info/METADATA +394 -0
- pyrenyi-0.1.0.dist-info/RECORD +18 -0
- pyrenyi-0.1.0.dist-info/WHEEL +5 -0
- pyrenyi-0.1.0.dist-info/entry_points.txt +2 -0
- pyrenyi-0.1.0.dist-info/licenses/LICENSE +21 -0
- pyrenyi-0.1.0.dist-info/top_level.txt +1 -0
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)
|