orpheus-npcf 0.1.0__cp312-cp312-musllinux_1_2_x86_64.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.

Potentially problematic release.


This version of orpheus-npcf might be problematic. Click here for more details.

orpheus/npcf.py ADDED
@@ -0,0 +1,3696 @@
1
+ from abc import ABC, abstractmethod
2
+
3
+ import ctypes as ct
4
+ from functools import reduce
5
+ import glob
6
+ from numba import jit, prange
7
+ from numba import config as nb_config
8
+ from numba import complex128 as nb_complex128
9
+ import numpy as np
10
+ from numpy.ctypeslib import ndpointer
11
+ import operator
12
+ from pathlib import Path
13
+ from scipy.interpolate import interp1d
14
+ import sys
15
+ from .catalog import Catalog, ScalarTracerCatalog, SpinTracerCatalog
16
+ from .utils import flatlist, gen_thetacombis_fourthorder, gen_n2n3indices_Upsfourth
17
+
18
+
19
+ __all__ = ["BinnedNPCF",
20
+ "GGCorrelation",
21
+ "GGGCorrelation", "GNNCorrelation", "NGGCorrelation",
22
+ "GGGGCorrelation_NoTomo", "NNNNCorrelation_NoTomo"]
23
+
24
+ ################################################
25
+ ## BASE CLASSES FOR NPCF AND THEIR MULTIPOLES ##
26
+ ################################################
27
+ class BinnedNPCF:
28
+ r"""Class of an binned N-point correlation function of various arbitrary tracer catalogs.
29
+ This class contains attributes and metods that can be used across any its children.
30
+
31
+ Attributes
32
+ ----------
33
+ order: int
34
+ The order of the correlation function.
35
+ spins: list
36
+ The spins of the tracer fields of which the NPCF is computed.
37
+ n_cfs: int
38
+ The number of independent components of the NPCF.
39
+ min_sep: float
40
+ The smallest distance of each vertex for which the NPCF is computed.
41
+ max_sep: float
42
+ The largest distance of each vertex for which the NPCF is computed.
43
+ nbinsr: int, optional
44
+ The number of radial bins for each vertex of the NPCF. If set to
45
+ ``None`` this attribute is inferred from the ``binsize`` attribute.
46
+ binsize: int, optional
47
+ The logarithmic slize of the radial bins for each vertex of the NPCF. If set to
48
+ ``None`` this attribute is inferred from the ``nbinsr`` attribute.
49
+ nbinsphi: float, optional
50
+ The number of angular bins for the NPCF in the real-space basis.
51
+ Defaults to ``100``.
52
+ nmaxs: list, optional
53
+ The largest multipole component considered for the NPCF in the multipole basis.
54
+ Defaults to ``30``.
55
+ method: str, optional
56
+ The method to be employed for the estimator. Defaults to ``DoubleTree``.
57
+ multicountcorr: bool, optional
58
+ Flag on whether to subtract of multiplets in which the same tracer appears more
59
+ than once. Defaults to ``True``.
60
+ shuffle_pix: int, optional
61
+ Choice of how to define centers of the cells in the spatial hash structure.
62
+ Defaults to ``1``, i.e. random positioning.
63
+ tree_resos: list, optional
64
+ The cell sizes of the hierarchical spatial hash structure
65
+ tree_redges: list, optional
66
+ List of radii where the tree changes resolution.
67
+ rmin_pixsize: int, optional
68
+ The limiting radial distance relative to the cell of the spatial hash
69
+ after which one switches to the next hash in the hierarchy. Defaults to ``20``.
70
+ resoshift_leafs: int, optional
71
+ Allows for a difference in how the hierarchical spatial hash is traversed for
72
+ pixels at the base of the NPCF and pixels at leafs. I.e. positive values indicate
73
+ that leafs will be evaluated at a courser resolutions than the base. Defaults to ``0``.
74
+ minresoind_leaf: int, optional
75
+ Sets the smallest resolution in the spatial hash hierarchy which can be used to access
76
+ tracers at leaf positions. If set to ``None`` uses the smallest specified cell size.
77
+ Defaults to ``None``.
78
+ maxresoind_leaf: int, optional
79
+ Sets the largest resolution in the spatial hash hierarchy which can be used to access
80
+ tracers at leaf positions. If set to ``None`` uses the largest specified cell size.
81
+ Defaults to ``None``.
82
+ verbosity: int, optional
83
+ The level of verbosity during the computation. Level 0: No verbosity, 1: Progress verbosity
84
+ on python layer, 2: Progress verbosity also on C level, 3: Debug verbosity. Defaults to ``0``.
85
+ nthreads: int, optional
86
+ The number of openmp threads used for the reduction procedure. Defaults to ``16``.
87
+ bin_centers: numpy.ndarray
88
+ The centers of the radial bins for each combination of tomographic redshifts.
89
+ bin_centers_mean: numpy.ndarray
90
+ The centers of the radial bins averaged over all combination of tomographic redshifts.
91
+ phis: list
92
+ The bin centers for the N-2 angles describing the NPCF
93
+ in the real-space basis.
94
+ npcf: numpy.ndarray
95
+ The natural components of the NPCF in the real space basis. The different axes
96
+ are specified as follows: ``(component, zcombi, rbin_1, ..., rbin_N-1, phiin_1, phibin_N-2)``.
97
+ npcf_norm: numpy.ndarray
98
+ The normalization of the natural components of the NPCF in the real space basis. The different axes
99
+ are specified as follows: ``(zcombi, rbin_1, ..., rbin_N-1, phiin_1, phibin_N-2)``.
100
+ npcf_multipoles: numpy.ndarray
101
+ The natural components of the NPCF in the multipole basis. The different axes
102
+ are specified as follows: ``(component, zcombi, multipole_1, ..., multipole_N-2, rbin_1, ..., rbin_N-1)``.
103
+ npcf_multipoles_norm: numpy.ndarray
104
+ The normalization of the natural components of the NPCF in the multipole basis. The different axes
105
+ are specified as follows: ``(zcombi, multipole_1, ..., multipole_N-2, rbin_1, ..., rbin_N-1)``.
106
+ is_edge_corrected: bool, optional
107
+ Flag signifying on wheter the NPCF multipoles have beed edge-corrected. Defaults to ``False``.
108
+ """
109
+
110
+ def __init__(self, order, spins, n_cfs, min_sep, max_sep, nbinsr=None, binsize=None, nbinsphi=100,
111
+ nmaxs=30, method="DoubleTree", multicountcorr=True, shuffle_pix=0,
112
+ tree_resos=[0,0.25,0.5,1.,2.], tree_redges=None, rmin_pixsize=20,
113
+ resoshift_leafs=0, minresoind_leaf=None, maxresoind_leaf=None,
114
+ methods_avail=["Discrete", "Tree", "BaseTree", "DoubleTree"], verbosity=0, nthreads=16):
115
+
116
+ self.order = int(order)
117
+ self.n_cfs = int(n_cfs)
118
+ self.min_sep = min_sep
119
+ self.max_sep = max_sep
120
+ self.nbinsphi = nbinsphi
121
+ self.nmaxs = nmaxs
122
+ self.method = method
123
+ self.multicountcorr = int(multicountcorr)
124
+ self.shuffle_pix = shuffle_pix
125
+ self.methods_avail = methods_avail
126
+ self.tree_resos = np.asarray(tree_resos, dtype=np.float64)
127
+ self.tree_nresos = int(len(self.tree_resos))
128
+ self.tree_redges = tree_redges
129
+ self.rmin_pixsize = rmin_pixsize
130
+ self.resoshift_leafs = resoshift_leafs
131
+ self.minresoind_leaf = minresoind_leaf
132
+ self.maxresoind_leaf = maxresoind_leaf
133
+ self.verbosity = np.int32(verbosity)
134
+ self.nthreads = np.int32(max(1,nthreads))
135
+
136
+ self.tree_resosatr = None
137
+ self.bin_centers = None
138
+ self.bin_centers_mean = None
139
+ self.phis = [None]*self.order
140
+ self.dphis = [None]*self.order
141
+ self.npcf = None
142
+ self.npcf_norm = None
143
+ self.npcf_multipoles = None
144
+ self.npcf_multipoles_norm = None
145
+ self.is_edge_corrected = False
146
+ self._verbose_python = self.verbosity > 0
147
+ self._verbose_c = self.verbosity > 1
148
+ self._verbose_debug = self.verbosity > 2
149
+
150
+ # Check types or arguments
151
+ if isinstance(self.nbinsphi, int):
152
+ self.nbinsphi = self.nbinsphi*np.ones(order-2)
153
+ self.nbinsphi = self.nbinsphi.astype(np.int32)
154
+ if isinstance(self.nmaxs, int):
155
+ self.nmaxs = self.nmaxs*np.ones(order-2)
156
+ self.nmaxs = self.nmaxs.astype(np.int32)
157
+ if isinstance(spins, int):
158
+ spins = spins*np.ones(order).astype(np.int32)
159
+ self.spins = np.asarray(spins, dtype=np.int32)
160
+ assert(isinstance(self.order, int))
161
+ assert(isinstance(self.spins, np.ndarray))
162
+ assert(isinstance(self.spins[0], np.int32))
163
+ assert(len(spins)==self.order)
164
+ assert(isinstance(self.n_cfs, int))
165
+ assert(isinstance(self.min_sep, float))
166
+ assert(isinstance(self.max_sep, float))
167
+ if self.order>2:
168
+ assert(isinstance(self.nbinsphi, np.ndarray))
169
+ assert(isinstance(self.nbinsphi[0], np.int32))
170
+ assert(len(self.nbinsphi)==self.order-2)
171
+ assert(isinstance(self.nmaxs, np.ndarray))
172
+ assert(isinstance(self.nmaxs[0], np.int32))
173
+ assert(len(self.nmaxs)==self.order-2)
174
+ assert(self.method in self.methods_avail)
175
+ assert(isinstance(self.tree_resos, np.ndarray))
176
+ assert(isinstance(self.tree_resos[0], np.float64))
177
+
178
+ # Setup radial bins
179
+ # Note that we always have self.binsize <= binsize
180
+ assert((binsize!=None) or (nbinsr!=None))
181
+ if nbinsr != None:
182
+ self.nbinsr = int(nbinsr)
183
+ if binsize != None:
184
+ assert(isinstance(binsize, float))
185
+ self.nbinsr = int(np.ceil(np.log(self.max_sep/self.min_sep)/binsize))
186
+ assert(isinstance(self.nbinsr, int))
187
+ self.bin_edges = np.geomspace(self.min_sep, self.max_sep, self.nbinsr+1)
188
+ self.binsize = np.log(self.bin_edges[1]/self.bin_edges[0])
189
+ # Setup variable for tree estimator according to input
190
+ if self.tree_redges != None:
191
+ assert(isinstance(self.tree_redges, np.ndarray))
192
+ self.tree_redges = self.tree_redges.astype(np.float64)
193
+ assert(len(self.tree_redges)==self.tree_resos+1)
194
+ self.tree_redges = np.sort(self.tree_redges)
195
+ assert(self.tree_redges[0]==self.min_sep)
196
+ assert(self.tree_redges[-1]==self.max_sep)
197
+ else:
198
+ self.tree_redges = np.zeros(len(self.tree_resos)+1)
199
+ self.tree_redges[-1] = self.max_sep
200
+ for elreso, reso in enumerate(self.tree_resos):
201
+ self.tree_redges[elreso] = (reso==0.)*self.min_sep + (reso!=0.)*self.rmin_pixsize*reso
202
+ _tmpreso = 0
203
+ self.tree_resosatr = np.zeros(self.nbinsr, dtype=np.int32)
204
+ for elbin, rbin in enumerate(self.bin_edges[:-1]):
205
+ if rbin > self.tree_redges[_tmpreso+1]:
206
+ _tmpreso += 1
207
+ self.tree_resosatr[elbin] = _tmpreso
208
+ # Update tree resolutions to make sure that `tree_redges` is monotonous
209
+ # (This is i.e. not fulfilled for a default tree setup and a large value of `rmin`)
210
+ _resomin = self.tree_resosatr[0]
211
+ _resomax = self.tree_resosatr[-1]
212
+ self._updatetree(self.tree_resos[_resomin:_resomax+1])
213
+
214
+ # Prepare leaf resolutions
215
+ if np.abs(self.resoshift_leafs)>=self.tree_nresos:
216
+ self.resoshift_leafs = np.int32((self.tree_nresos-1) * np.sign(self.resoshift_leafs))
217
+ print("Error: Parameter resoshift_leafs is out of bounds. Set to %i."%self.resoshift_leafs)
218
+ if self.minresoind_leaf is None:
219
+ self.minresoind_leaf=0
220
+ if self.maxresoind_leaf is None:
221
+ self.maxresoind_leaf=self.tree_nresos-1
222
+ if self.minresoind_leaf<0:
223
+ self.minresoind_leaf = 0
224
+ print("Error: Parameter minreso_leaf is out of bounds. Set to 0.")
225
+ if self.minresoind_leaf>=self.tree_nresos:
226
+ self.minresoind_leaf = self.tree_nresos-1
227
+ print("Error: Parameter minreso_leaf is out of bounds. Set to %i."%self.minresoint_leaf)
228
+ if self.maxresoind_leaf<0:
229
+ self.maxresoind_leaf = 0
230
+ print("Error: Parameter minreso_leaf is out of bounds. Set to 0.")
231
+ if self.maxresoind_leaf>=self.tree_nresos:
232
+ self.maxresoind_leaf = self.tree_nresos-1
233
+ print("Error: Parameter minreso_leaf is out of bounds. Set to %i."%self.maxresoint_leaf)
234
+ if self.maxresoind_leaf<self.minresoind_leaf:
235
+ print("Error: Parameter maxreso_leaf is smaller than minreso_leaf. Set to %i."%self.minreso_leaf)
236
+
237
+ # Setup phi bins
238
+ for elp in range(self.order-2):
239
+ _ = np.linspace(0,2*np.pi,self.nbinsphi[elp]+1)
240
+ self.phis[elp] = .5*(_[1:] + _[:-1])
241
+ self.dphis[elp] = _[1:] - _[:-1]
242
+
243
+ #############################
244
+ ## Link compiled libraries ##
245
+ #############################
246
+ # Method that works for LP
247
+ target_path = __import__('orpheus').__file__
248
+ self.library_path = str(Path(__import__('orpheus').__file__).parent.absolute())
249
+ self.clib = ct.CDLL(glob.glob(self.library_path+"/orpheus_clib*.so")[0])
250
+
251
+ # In case the environment is weird, compile code manually and load it here...
252
+ #self.clib = ct.CDLL("/vol/euclidraid4/data/lporth/HigherOrderLensing/Estimator/orpheus/orpheus/src/discrete.so")
253
+
254
+ # Method that works for RR (but not for LP with a local HPC install)
255
+ #self.clib = ct.CDLL(search_file_in_site_package(get_site_packages_dir(),"orpheus_clib"))
256
+ #self.library_path = str(Path(__import__('orpheus').__file__).parent.parent.absolute())
257
+ #print(self.library_path)
258
+ #print(self.clib)
259
+ p_c128 = ndpointer(complex, flags="C_CONTIGUOUS")
260
+ p_f64 = ndpointer(np.float64, flags="C_CONTIGUOUS")
261
+ p_f32 = ndpointer(np.float32, flags="C_CONTIGUOUS")
262
+ p_i32 = ndpointer(np.int32, flags="C_CONTIGUOUS")
263
+ p_f64_nof = ndpointer(np.float64)
264
+
265
+ ## Second order scalar-scalar statistics ##
266
+ if self.order==2 and np.array_equal(self.spins, np.array([0, 0], dtype=np.int32)):
267
+ self.clib.alloc_NNcounts_doubletree.restype = ct.c_void_p
268
+ self.clib.alloc_NNcounts_doubletree.argtypes = [
269
+ ct.c_int32, ct.c_int32, p_f64, p_f64, p_f64,
270
+ ct.c_int32, ct.c_int32, ct.c_int32,
271
+ p_i32, ct.c_int32, p_f64, p_f64, p_f64, p_f64, p_f64, p_i32,
272
+ p_i32, p_i32, p_i32,
273
+ ct.c_double, ct.c_double, ct.c_int32, ct.c_double, ct.c_double, ct.c_int32, ct.c_int32, p_i32,
274
+ ct.c_double, ct.c_double, ct.c_int32, ct.c_int32, ct.c_int32,
275
+ np.ctypeslib.ndpointer(dtype=np.float64),
276
+ np.ctypeslib.ndpointer(dtype=np.float64),
277
+ np.ctypeslib.ndpointer(dtype=np.int64)]
278
+
279
+ ## Second order shear-shear statistics ##
280
+ if self.order==2 and np.array_equal(self.spins, np.array([2, 2], dtype=np.int32)):
281
+ # Doubletree-based estimator of second-order shear correlation function
282
+ self.clib.alloc_xipm_doubletree.restype = ct.c_void_p
283
+ self.clib.alloc_xipm_doubletree.argtypes = [
284
+ ct.c_int32, ct.c_int32, p_f64, p_f64, p_f64,
285
+ ct.c_int32, ct.c_int32, ct.c_int32,
286
+ p_i32, ct.c_int32, p_f64, p_f64, p_f64, p_f64, p_f64, p_f64, p_i32,
287
+ p_i32, p_i32, p_i32,
288
+ ct.c_double, ct.c_double, ct.c_int32, ct.c_double, ct.c_double, ct.c_int32, ct.c_int32, p_i32,
289
+ ct.c_double, ct.c_double, ct.c_int32, ct.c_int32, ct.c_int32, ct.c_int32,
290
+ np.ctypeslib.ndpointer(dtype=np.float64),
291
+ np.ctypeslib.ndpointer(dtype=np.complex128),
292
+ np.ctypeslib.ndpointer(dtype=np.complex128),
293
+ np.ctypeslib.ndpointer(dtype=np.float64),
294
+ np.ctypeslib.ndpointer(dtype=np.int64)]
295
+
296
+ ## Third order shear-shear-shear statistics ##
297
+ if self.order==3 and np.array_equal(self.spins, np.array([2, 2, 2], dtype=np.int32)):
298
+ # Discrete estimator of third-order shear correlation function
299
+ self.clib.alloc_Gammans_discrete_ggg.restype = ct.c_void_p
300
+ self.clib.alloc_Gammans_discrete_ggg.argtypes = [
301
+ p_f64, p_f64, p_f64, p_f64, p_f64, p_f64, p_i32, ct.c_int32, ct.c_int32,
302
+ ct.c_int32, ct.c_int32, ct.c_double, ct.c_double, p_f64, ct.c_int32, ct.c_int32,
303
+ p_i32, p_i32, p_i32, ct.c_double, ct.c_double, ct.c_int32,
304
+ ct.c_double, ct.c_double, ct.c_int32, ct.c_int32, ct.c_int32,
305
+ np.ctypeslib.ndpointer(dtype=np.float64),
306
+ np.ctypeslib.ndpointer(dtype=np.complex128),
307
+ np.ctypeslib.ndpointer(dtype=np.complex128)]
308
+
309
+ # Tree-based estimator of third-order shear correlation function
310
+ self.clib.alloc_Gammans_tree_ggg.restype = ct.c_void_p
311
+ self.clib.alloc_Gammans_tree_ggg.argtypes = [
312
+ p_f64, p_f64, p_f64, p_f64, p_f64, p_f64, p_i32, ct.c_int32, ct.c_int32,
313
+ ct.c_int32, p_f64, p_i32,
314
+ p_f64, p_f64, p_f64, p_f64, p_f64, p_i32, p_f64,
315
+ p_i32, p_i32, p_i32, ct.c_double, ct.c_double, ct.c_int32, ct.c_double, ct.c_double, ct.c_int32,
316
+ ct.c_int32, ct.c_int32, ct.c_double, ct.c_double, p_f64, ct.c_int32, ct.c_int32, ct.c_int32, ct.c_int32,
317
+ np.ctypeslib.ndpointer(dtype=np.float64),
318
+ np.ctypeslib.ndpointer(dtype=np.complex128),
319
+ np.ctypeslib.ndpointer(dtype=np.complex128)]
320
+
321
+ # Basetree-based estimator of third-order shear correlation function
322
+ self.clib.alloc_Gammans_basetree_ggg.restype = ct.c_void_p
323
+ self.clib.alloc_Gammans_basetree_ggg.argtypes = [
324
+ ct.c_int32, ct.c_int32, p_f64, p_f64, p_f64, p_i32, ct.c_int32,
325
+ p_f64, p_f64, p_f64, p_f64, p_f64, p_f64, p_i32, p_f64,
326
+ p_i32, p_i32, p_i32,
327
+ ct.c_double, ct.c_double, ct.c_int32, ct.c_double, ct.c_double, ct.c_int32,
328
+ p_i32, ct.c_int32, p_i32, ct.c_int32,
329
+ ct.c_int32, ct.c_double, ct.c_double, ct.c_int32, ct.c_int32, ct.c_int32, ct.c_int32,
330
+ np.ctypeslib.ndpointer(dtype=np.float64),
331
+ np.ctypeslib.ndpointer(dtype=np.complex128),
332
+ np.ctypeslib.ndpointer(dtype=np.complex128)]
333
+
334
+ # Doubletree-based estimator of third-order shear correlation function
335
+ self.clib.alloc_Gammans_doubletree_ggg.restype = ct.c_void_p
336
+ self.clib.alloc_Gammans_doubletree_ggg.argtypes = [
337
+ ct.c_int32, ct.c_int32, p_f64, p_f64, p_f64,
338
+ ct.c_int32, ct.c_int32, ct.c_int32,
339
+ p_i32, ct.c_int32, p_f64, p_f64, p_f64, p_f64, p_f64, p_f64, p_i32, p_f64,
340
+ p_i32, p_i32, p_i32,
341
+ ct.c_double, ct.c_double, ct.c_int32, ct.c_double, ct.c_double, ct.c_int32,
342
+ p_i32, ct.c_int32, p_i32, ct.c_int32,
343
+ ct.c_int32, ct.c_double, ct.c_double, ct.c_int32, ct.c_int32, ct.c_int32, ct.c_int32,
344
+ np.ctypeslib.ndpointer(dtype=np.float64),
345
+ np.ctypeslib.ndpointer(dtype=np.complex128),
346
+ np.ctypeslib.ndpointer(dtype=np.complex128)]
347
+
348
+ # Conversion between 3pcf multipoles and 3pcf
349
+ self.clib.multipoles2npcf_ggg.restype = ct.c_void_p
350
+ self.clib.multipoles2npcf_ggg.argtypes = [
351
+ p_c128, p_c128, ct.c_int32, ct.c_int32,
352
+ p_f64, ct.c_int32, p_f64, ct.c_int32, ct.c_int32,
353
+ ct.c_int32,
354
+ np.ctypeslib.ndpointer(dtype=np.complex128),
355
+ np.ctypeslib.ndpointer(dtype=np.complex128)]
356
+
357
+ # Change projection of 3pcf between x and centroid
358
+ self.clib._x2centroid_ggg.restype = ct.c_void_p
359
+ self.clib._x2centroid_ggg.argtypes = [
360
+ p_c128, ct.c_int32,
361
+ p_f64, ct.c_int32, p_f64, ct.c_int32, ct.c_int32]
362
+
363
+ ## Third-order source-lens-lens statistics ##
364
+ if self.order==3 and np.array_equal(self.spins, np.array([2, 0, 0], dtype=np.int32)):
365
+ # Discrete estimator of third-order source-lens-lens (G3L) correlation function
366
+ self.clib.alloc_Gammans_discrete_GNN.restype = ct.c_void_p
367
+ self.clib.alloc_Gammans_discrete_GNN.argtypes = [
368
+ p_f64, p_f64, p_f64, p_f64, p_f64, p_f64, p_i32, ct.c_int32, ct.c_int32,
369
+ p_f64, p_f64, p_f64, p_i32, ct.c_int32, ct.c_int32,
370
+ ct.c_int32, ct.c_double, ct.c_double, ct.c_int32, ct.c_int32,
371
+ p_i32, p_i32, p_i32, p_i32, p_i32, p_i32, ct.c_int32,
372
+ ct.c_double, ct.c_double, ct.c_int32, ct.c_double, ct.c_double, ct.c_int32, ct.c_int32, ct.c_int32,
373
+ np.ctypeslib.ndpointer(dtype=np.float64),
374
+ np.ctypeslib.ndpointer(dtype=np.complex128),
375
+ np.ctypeslib.ndpointer(dtype=np.complex128)]
376
+
377
+ # Doubletree-based estimator of third-order source-lens-lens (G3L) correlation function
378
+ self.clib.alloc_Gammans_doubletree_GNN.restype = ct.c_void_p
379
+ self.clib.alloc_Gammans_doubletree_GNN.argtypes = [
380
+ ct.c_int32, ct.c_int32, p_f64, p_f64, p_f64,
381
+ ct.c_int32, ct.c_int32, ct.c_int32,
382
+ p_f64, p_f64, p_f64, p_f64, p_f64, p_f64, p_i32, p_i32, ct.c_int32,
383
+ p_f64, p_f64, p_f64, p_f64, p_i32, p_i32, ct.c_int32,
384
+ ct.c_int32, ct.c_double, ct.c_double, ct.c_int32, ct.c_int32,
385
+ p_i32, p_i32, p_i32, p_i32, p_i32, p_i32, p_i32, ct.c_int32,
386
+ ct.c_double, ct.c_double, ct.c_int32, ct.c_double, ct.c_double, ct.c_int32, ct.c_int32, ct.c_int32,
387
+ np.ctypeslib.ndpointer(dtype=np.float64),
388
+ np.ctypeslib.ndpointer(dtype=np.complex128),
389
+ np.ctypeslib.ndpointer(dtype=np.complex128)]
390
+
391
+ ## Third-order lens-source-source statistics ##
392
+ if self.order==3 and np.array_equal(self.spins, np.array([0, 2, 2], dtype=np.int32)):
393
+ # Discrete estimator of third-order lens-source-source correlation function
394
+ self.clib.alloc_Gammans_discrete_NGG.restype = ct.c_void_p
395
+ self.clib.alloc_Gammans_discrete_NGG.argtypes = [
396
+ p_f64, p_f64, p_f64, p_f64, p_f64, p_i32, ct.c_int32, ct.c_int32,
397
+ p_f64, p_f64, p_f64, p_f64, p_i32, ct.c_int32, ct.c_int32,
398
+ ct.c_int32, ct.c_double, ct.c_double, ct.c_int32, ct.c_int32,
399
+ p_i32, p_i32, p_i32, p_i32, p_i32, p_i32, ct.c_int32,
400
+ ct.c_double, ct.c_double, ct.c_int32, ct.c_double, ct.c_double, ct.c_int32, ct.c_int32, ct.c_int32,
401
+ np.ctypeslib.ndpointer(dtype=np.float64),
402
+ np.ctypeslib.ndpointer(dtype=np.complex128),
403
+ np.ctypeslib.ndpointer(dtype=np.complex128)]
404
+
405
+ self.clib.alloc_Gammans_tree_NGG.restype = ct.c_void_p
406
+ self.clib.alloc_Gammans_tree_NGG.argtypes = [
407
+ ct.c_int32, p_f64,
408
+ p_f64, p_f64, p_f64, p_f64, p_f64, p_i32, ct.c_int32, p_i32,
409
+ p_f64, p_f64, p_f64, p_f64, p_i32, ct.c_int32, ct.c_int32,
410
+ p_i32, p_i32, p_i32, p_i32, p_i32, p_i32, ct.c_int32,
411
+ ct.c_double, ct.c_double, ct.c_int32, ct.c_double, ct.c_double, ct.c_int32,
412
+ ct.c_int32, ct.c_double, ct.c_double, ct.c_int32, ct.c_int32, ct.c_int32, ct.c_int32,
413
+ np.ctypeslib.ndpointer(dtype=np.float64),
414
+ np.ctypeslib.ndpointer(dtype=np.complex128),
415
+ np.ctypeslib.ndpointer(dtype=np.complex128)]
416
+
417
+ self.clib.alloc_Gammans_doubletree_NGG.restype = ct.c_void_p
418
+ self.clib.alloc_Gammans_doubletree_NGG.argtypes = [
419
+ ct.c_int32, ct.c_int32, p_f64, p_f64, p_f64,
420
+ ct.c_int32, ct.c_int32, ct.c_int32,
421
+ p_f64, p_f64, p_f64, p_f64, p_f64, p_f64, p_i32, p_i32, ct.c_int32,
422
+ p_f64, p_f64, p_f64, p_f64, p_i32, p_i32, ct.c_int32,
423
+ ct.c_int32, ct.c_double, ct.c_double, ct.c_int32, ct.c_int32,
424
+ p_i32, p_i32, p_i32, p_i32, p_i32, p_i32, p_i32, ct.c_int32,
425
+ ct.c_double, ct.c_double, ct.c_int32, ct.c_double, ct.c_double, ct.c_int32, ct.c_int32, ct.c_int32,
426
+ np.ctypeslib.ndpointer(dtype=np.float64),
427
+ np.ctypeslib.ndpointer(dtype=np.complex128),
428
+ np.ctypeslib.ndpointer(dtype=np.complex128)]
429
+
430
+ ## Fourth-order counts-counts-counts-counts statistics ##
431
+ if self.order==4 and np.array_equal(self.spins, np.array([0, 0, 0, 0], dtype=np.int32)):
432
+
433
+ # Tree estimator of non-tomographic fourth-order counts correlation function
434
+ self.clib.alloc_notomoGammans_tree_nnnn.restype = ct.c_void_p
435
+ self.clib.alloc_notomoGammans_tree_nnnn.argtypes = [
436
+ p_f64, p_f64, p_f64, p_f64, ct.c_int32,
437
+ ct.c_int32, ct.c_double, ct.c_double, ct.c_int32, ct.c_int32, ct.c_int32,
438
+ p_i32, ct.c_int32,
439
+ ct.c_int32, p_f64, p_i32,
440
+ p_f64, p_f64, p_f64, p_f64,
441
+ p_i32, p_i32, p_i32, ct.c_int32,
442
+ ct.c_double, ct.c_double, ct.c_int32, ct.c_double, ct.c_double, ct.c_int32, ct.c_int32, ct.c_int32,
443
+ np.ctypeslib.ndpointer(dtype=np.float64),
444
+ np.ctypeslib.ndpointer(dtype=np.complex128)]
445
+
446
+ # Tree-based estimator of non-tomographic Map^4 statistics (low-mem)
447
+ self.clib.alloc_notomoNap4_tree_nnnn.restype = ct.c_void_p
448
+ self.clib.alloc_notomoNap4_tree_nnnn.argtypes = [
449
+ p_f64, p_f64, p_f64, p_f64, ct.c_int32,
450
+ ct.c_int32, ct.c_double, ct.c_double, ct.c_int32, ct.c_int32,
451
+ p_i32, ct.c_int32, p_f64, p_f64, ct.c_int32,
452
+ ct.c_int32, p_f64, p_i32,
453
+ p_f64, p_f64, p_f64, p_f64,
454
+ p_i32, p_i32, p_i32, ct.c_int32,
455
+ ct.c_double, ct.c_double, ct.c_int32, ct.c_double, ct.c_double, ct.c_int32,
456
+ p_i32, p_i32, p_i32, ct.c_int32,
457
+ ct.c_int32, p_f64, ct.c_int32, np.ctypeslib.ndpointer(dtype=np.complex128),
458
+ ct.c_int32, ct.c_int32,
459
+ np.ctypeslib.ndpointer(dtype=np.float64),
460
+ np.ctypeslib.ndpointer(dtype=np.complex128),np.ctypeslib.ndpointer(dtype=np.complex128)]
461
+
462
+ # Transformation between 4PCF from multipole-basis tp real-space basis for a fixed
463
+ # combination of radial bins
464
+ self.clib.multipoles2npcf_nnnn_singletheta.restype = ct.c_void_p
465
+ self.clib.multipoles2npcf_nnnn_singletheta.argtypes = [
466
+ p_c128, ct.c_int32, ct.c_int32,
467
+ ct.c_double, ct.c_double, ct.c_double,
468
+ p_f64, p_f64, ct.c_int32, ct.c_int32,
469
+ np.ctypeslib.ndpointer(dtype=np.complex128)]
470
+
471
+ ## Fourth-order shear-shear-shear-shear statistics ##
472
+ if self.order==4 and np.array_equal(self.spins, np.array([2, 2, 2, 2], dtype=np.int32)):
473
+
474
+ # Discrete estimator of non-tomographic fourth-order shear correlation function
475
+ self.clib.alloc_notomoGammans_discrete_gggg.restype = ct.c_void_p
476
+ self.clib.alloc_notomoGammans_discrete_gggg.argtypes = [
477
+ p_f64, p_f64, p_f64, p_f64, p_f64, p_f64, ct.c_int32,
478
+ ct.c_int32, ct.c_double, ct.c_double, p_f64, ct.c_int32, ct.c_int32,
479
+ p_i32, p_i32, p_i32, ct.c_int32,
480
+ ct.c_double, ct.c_double, ct.c_int32, ct.c_double, ct.c_double, ct.c_int32, ct.c_int32, ct.c_int32,
481
+ np.ctypeslib.ndpointer(dtype=np.float64),
482
+ np.ctypeslib.ndpointer(dtype=np.complex128),
483
+ np.ctypeslib.ndpointer(dtype=np.complex128)]
484
+
485
+ # Tree estimator of non-tomographic fourth-order shear correlation function
486
+ self.clib.alloc_notomoGammans_tree_gggg.restype = ct.c_void_p
487
+ self.clib.alloc_notomoGammans_tree_gggg.argtypes = [
488
+ p_f64, p_f64, p_f64, p_f64, p_f64, p_f64, ct.c_int32,
489
+ ct.c_int32, ct.c_double, ct.c_double, ct.c_int32, ct.c_int32, ct.c_int32,
490
+ p_i32, ct.c_int32,
491
+ ct.c_int32, p_f64, p_i32,
492
+ p_f64, p_f64, p_f64, p_f64, p_f64, p_f64,
493
+ p_i32, p_i32, p_i32, ct.c_int32,
494
+ ct.c_double, ct.c_double, ct.c_int32, ct.c_double, ct.c_double, ct.c_int32, ct.c_int32, ct.c_int32,
495
+ np.ctypeslib.ndpointer(dtype=np.float64),
496
+ np.ctypeslib.ndpointer(dtype=np.complex128),
497
+ np.ctypeslib.ndpointer(dtype=np.complex128)]
498
+
499
+ # Discrete estimator of non-tomographic Map^4 statistics (low-mem)
500
+ self.clib.alloc_notomoMap4_disc_gggg.restype = ct.c_void_p
501
+ self.clib.alloc_notomoMap4_disc_gggg.argtypes = [
502
+ p_f64, p_f64, p_f64, p_f64, p_f64, p_f64, ct.c_int32,
503
+ ct.c_int32, ct.c_double, ct.c_double, ct.c_int32, ct.c_int32, p_f64, p_f64, ct.c_int32,
504
+ p_i32, p_i32, p_i32, ct.c_int32,
505
+ ct.c_double, ct.c_double, ct.c_int32, ct.c_double, ct.c_double, ct.c_int32,
506
+ p_i32, p_i32, p_i32, ct.c_int32,
507
+ ct.c_int32, ct.c_int32, ct.c_int32, p_f64, ct.c_int32, np.ctypeslib.ndpointer(dtype=np.complex128),
508
+ ct.c_int32, ct.c_int32, np.ctypeslib.ndpointer(dtype=np.float64),
509
+ np.ctypeslib.ndpointer(dtype=np.complex128),np.ctypeslib.ndpointer(dtype=np.complex128),
510
+ np.ctypeslib.ndpointer(dtype=np.complex128),np.ctypeslib.ndpointer(dtype=np.complex128)]
511
+
512
+ # Tree-based estimator of non-tomographic Map^4 statistics (low-mem)
513
+ self.clib.alloc_notomoMap4_tree_gggg.restype = ct.c_void_p
514
+ self.clib.alloc_notomoMap4_tree_gggg.argtypes = [
515
+ p_f64, p_f64, p_f64, p_f64, p_f64, p_f64, ct.c_int32,
516
+ ct.c_int32, ct.c_double, ct.c_double, ct.c_int32, ct.c_int32,
517
+ p_i32, ct.c_int32, p_f64, p_f64, ct.c_int32,
518
+ ct.c_int32, p_f64, p_i32,
519
+ p_f64, p_f64, p_f64, p_f64, p_f64, p_f64,
520
+ p_i32, p_i32, p_i32, ct.c_int32,
521
+ ct.c_double, ct.c_double, ct.c_int32, ct.c_double, ct.c_double, ct.c_int32,
522
+ p_i32, p_i32, p_i32, ct.c_int32,
523
+ ct.c_int32, ct.c_int32, ct.c_int32, p_f64, ct.c_int32, np.ctypeslib.ndpointer(dtype=np.complex128),
524
+ ct.c_int32, ct.c_int32, np.ctypeslib.ndpointer(dtype=np.float64),
525
+ np.ctypeslib.ndpointer(dtype=np.complex128),np.ctypeslib.ndpointer(dtype=np.complex128),
526
+ np.ctypeslib.ndpointer(dtype=np.complex128),np.ctypeslib.ndpointer(dtype=np.complex128)]
527
+
528
+ self.clib.multipoles2npcf_gggg.restype = ct.c_void_p
529
+ self.clib.multipoles2npcf_gggg.argtypes = [
530
+ p_c128, p_c128, p_f64, ct.c_int32,
531
+ ct.c_int32, ct.c_int32, ct.c_int32, p_f64, ct.c_int32, p_f64, ct.c_int32,
532
+ ct.c_int32, p_c128, p_c128]
533
+
534
+ # Transformation between 4PCF from multipole-basis tp real-space basis for a fixed
535
+ # combination of radial bins
536
+ self.clib.multipoles2npcf_gggg_singletheta.restype = ct.c_void_p
537
+ self.clib.multipoles2npcf_gggg_singletheta.argtypes = [
538
+ p_c128, p_c128, ct.c_int32, ct.c_int32,
539
+ ct.c_double, ct.c_double, ct.c_double,
540
+ p_f64, p_f64, ct.c_int32, ct.c_int32,
541
+ ct.c_int32,
542
+ np.ctypeslib.ndpointer(dtype=np.complex128),np.ctypeslib.ndpointer(dtype=np.complex128)]
543
+
544
+ # Transformation between 4PCF from multipole-basis tp real-space basis for a fixed
545
+ # combination of radial bins. Explicitly checks convergence for orders of multipoles included
546
+ self.clib.multipoles2npcf_gggg_singletheta_nconvergence.restype = ct.c_void_p
547
+ self.clib.multipoles2npcf_gggg_singletheta_nconvergence.argtypes = [
548
+ p_c128, p_c128, ct.c_int32, ct.c_int32,
549
+ ct.c_double, ct.c_double, ct.c_double,
550
+ p_f64, p_f64, ct.c_int32, ct.c_int32,
551
+ ct.c_int32,
552
+ np.ctypeslib.ndpointer(dtype=np.complex128),np.ctypeslib.ndpointer(dtype=np.complex128)]
553
+
554
+ # Reconstruction of all 4pcf multipoles from symmetry properties given a set of
555
+ # multipoles with theta1<=theta2<=theta3
556
+ self.clib.getMultipolesFromSymm.restype = ct.c_void_p
557
+ self.clib.getMultipolesFromSymm.argtypes = [
558
+ p_c128, p_c128,
559
+ ct.c_int32, ct.c_int32, p_i32, ct.c_int32,
560
+ np.ctypeslib.ndpointer(dtype=np.complex128),np.ctypeslib.ndpointer(dtype=np.complex128)]
561
+
562
+ # Transformaton between 4pcf multipoles and M4 correlators of Map4 statistics
563
+ self.clib.fourpcfmultipoles2M4correlators.restype = ct.c_void_p
564
+ self.clib.fourpcfmultipoles2M4correlators.argtypes = [
565
+ ct.c_int32, ct.c_int32,
566
+ p_f64, p_f64, ct.c_int32,
567
+ p_f64, ct.c_int32,
568
+ p_f64, p_f64, p_f64, p_f64, ct.c_int32, ct.c_int32,
569
+ ct.c_int32, ct.c_int32,
570
+ p_c128, p_c128, np.ctypeslib.ndpointer(dtype=np.complex128)]
571
+
572
+ # [DEBUG]: Shear 4pt function in terms of xip/xim
573
+ self.clib.gauss4pcf_analytic.restype = ct.c_void_p
574
+ self.clib.gauss4pcf_analytic.argtypes = [
575
+ ct.c_double, ct.c_double, ct.c_double, p_f64, ct.c_int32,
576
+ p_f64, p_f64, ct.c_double, ct.c_double, ct.c_double,
577
+ np.ctypeslib.ndpointer(dtype=np.complex128)]
578
+
579
+ # [DEBUG]: Shear 4pt function in terms of xip/xim, subsampled within the 4pcf bins
580
+ self.clib.gauss4pcf_analytic_integrated.restype = ct.c_void_p
581
+ self.clib.gauss4pcf_analytic_integrated.argtypes = [
582
+ ct.c_int32, ct.c_int32, ct.c_int32, ct.c_int32,
583
+ p_f64, ct.c_int32, p_f64, ct.c_int32,
584
+ p_f64, p_f64, ct.c_double, ct.c_double, ct.c_double,
585
+ np.ctypeslib.ndpointer(dtype=np.complex128)]
586
+
587
+ # [DEBUG]: Map4 via analytic gaussian 4pcf
588
+ self.clib.alloc_notomoMap4_analytic.restype = ct.c_void_p
589
+ self.clib.alloc_notomoMap4_analytic.argtypes = [
590
+ ct.c_double, ct.c_double, ct.c_int32, p_f64, p_f64, ct.c_int32, ct.c_int32,
591
+ p_i32, p_i32, p_i32, ct.c_int32,
592
+ ct.c_int32, p_f64, ct.c_int32,
593
+ p_f64, p_f64, ct.c_double, ct.c_double, ct.c_int32, ct.c_int32,
594
+ np.ctypeslib.ndpointer(dtype=np.complex128)]
595
+
596
+ # [DEBUG]: Map4 filter function for single combination
597
+ self.clib.filter_Map4.restype = ct.c_void_p
598
+ self.clib.filter_Map4.argtypes = [
599
+ ct.c_double, ct.c_double, ct.c_double, ct.c_double, ct.c_double,
600
+ np.ctypeslib.ndpointer(dtype=np.complex128)]
601
+
602
+ # [DEBUG]: Conversion between 4pcf and Map4 for (theta1,theta2,theta3) subset
603
+ self.clib.fourpcf2M4correlators_parallel.restype = ct.c_void_p
604
+ self.clib.fourpcf2M4correlators_parallel.argtypes = [
605
+ ct.c_int32,
606
+ ct.c_double, ct.c_double, ct.c_double, ct.c_double, ct.c_double, ct.c_double,
607
+ p_f64, p_f64, p_f64, p_f64, ct.c_int32, ct.c_int32,
608
+ ct.c_int32,
609
+ np.ctypeslib.ndpointer(dtype=np.complex128), np.ctypeslib.ndpointer(dtype=np.complex128)]
610
+
611
+ ############################################################
612
+ ## Functions that deal with different projections of NPCF ##
613
+ ############################################################
614
+ def _initprojections(self, child):
615
+ assert(child.projection in child.projections_avail)
616
+ child.project = {}
617
+ for proj in child.projections_avail:
618
+ child.project[proj] = {}
619
+ for proj2 in child.projections_avail:
620
+ if proj==proj2:
621
+ child.project[proj][proj2] = lambda: child.npcf
622
+ else:
623
+ child.project[proj][proj2] = None
624
+
625
+ def _projectnpcf(self, child, projection):
626
+ """
627
+ Projects npcf to a new basis.
628
+ """
629
+ assert(child.npcf is not None)
630
+ if projection not in child.projections_avail:
631
+ print(f"Projection {projection} is not yet supported.")
632
+ self._print_npcfprojections_avail()
633
+ return
634
+
635
+ projection_func = child.project[child.projection].get(projection)
636
+ if projection_func is not None:
637
+ child.npcf = projection_func()
638
+ child.projection = projection
639
+ else:
640
+ print(f"Projection from {child.projection} to {projection} is not yet implemented.")
641
+ self._print_npcfprojections_avail(child)
642
+
643
+ def _print_npcfprojections_avail(self, child):
644
+ print(f"The following projections are available in the class {child.__class__.__name__}:")
645
+ for proj in child.projections_avail:
646
+ for proj2 in child.projections_avail:
647
+ if child.project[proj].get(proj2) is not None:
648
+ print(f" {proj} --> {proj2}")
649
+
650
+ ####################
651
+ ## MISC FUNCTIONS ##
652
+ ####################
653
+ def _checkcats(self, cats, spins):
654
+ if isinstance(cats, list):
655
+ assert(len(cats)==self.order)
656
+ for els, s in enumerate(self.spins):
657
+ if not isinstance(cats, list):
658
+ thiscat = cats
659
+ else:
660
+ thiscat = cats[els]
661
+ assert(thiscat.spin == s)
662
+
663
+ def _updatetree(self, new_resos):
664
+
665
+ new_resos = np.asarray(new_resos, dtype=np.float64)
666
+ new_nresos = int(len(new_resos))
667
+
668
+ new_redges = np.zeros(len(new_resos)+1)
669
+ new_redges[0] = self.min_sep
670
+ new_redges[-1] = self.max_sep
671
+ for elreso, reso in enumerate(new_resos[1:]):
672
+ new_redges[elreso+1] = self.rmin_pixsize*reso
673
+ _tmpreso = 0
674
+ new_resosatr = np.zeros(self.nbinsr, dtype=np.int32)
675
+ for elbin, rbin in enumerate(self.bin_edges[:-1]):
676
+ if rbin > new_redges[_tmpreso+1]:
677
+ _tmpreso += 1
678
+ new_resosatr[elbin] = _tmpreso
679
+
680
+ self.tree_resos = new_resos
681
+ self.tree_nresos = new_nresos
682
+ self.tree_redges = new_redges
683
+ self.tree_resosatr = new_resosatr
684
+
685
+
686
+
687
+ ###############################
688
+ ## SECOND - ORDER STATISTICS ##
689
+ ###############################
690
+ class GGCorrelation(BinnedNPCF):
691
+ """ Compute second-order correlation functions of spin-2 fields.
692
+
693
+ Parameters
694
+ ----------
695
+ min_sep: float
696
+ The smallest distance of each vertex for which the NPCF is computed.
697
+ max_sep: float
698
+ The largest distance of each vertex for which the NPCF is computed.
699
+
700
+ Attributes
701
+ ----------
702
+ xip: numpy.ndarray
703
+ The ξ₊ correlation function.
704
+ xim: numpy.ndarray
705
+ The ξ₋ correlation function.
706
+ norm: numpy.ndarray
707
+ The number of weighted pairs.
708
+ npair: numpy.ndarray
709
+ The number of unweighted pairs.
710
+
711
+ Notes
712
+ -----
713
+ Inherits all other parameters and attributes from :class:`BinnedNPCF`.
714
+ Additional child-specific parameters can be passed via ``kwargs``.
715
+ Either ``nbinsr`` or ``binsize`` has to be provided to fix the binning scheme .
716
+ """
717
+
718
+ def __init__(self, min_sep, max_sep, **kwargs):
719
+ super().__init__(order=2, spins=np.array([2,2], dtype=np.int32), n_cfs=2, min_sep=min_sep, max_sep=max_sep, **kwargs)
720
+ self.projection = None
721
+ self.projections_avail = [None]
722
+ self.nbinsz = None
723
+ self.nzcombis = None
724
+ self.counts = None
725
+ self.xip = None
726
+ self.xim = None
727
+ self.norm = None
728
+ self.npair = None
729
+
730
+ # (Add here any newly implemented projections)
731
+ self._initprojections(self)
732
+
733
+ def saveinst(self, path_save, fname):
734
+
735
+ if not Path(path_save).is_dir():
736
+ raise ValueError('Path to directory does not exist.')
737
+
738
+ np.savez(path_save+fname,
739
+ nbinsz=self.nbinsz,
740
+ min_sep=self.min_sep,
741
+ max_sep=self.max_sep,
742
+ binsr=self.nbinsr,
743
+ method=self.method,
744
+ shuffle_pix=self.shuffle_pix,
745
+ tree_resos=self.tree_resos,
746
+ rmin_pixsize=self.rmin_pixsize,
747
+ resoshift_leafs=self.resoshift_leafs,
748
+ minresoind_leaf=self.minresoind_leaf,
749
+ maxresoind_leaf=self.maxresoind_leaf,
750
+ nthreads=self.nthreads,
751
+ bin_centers=self.bin_centers,
752
+ xip=self.xip,
753
+ xim=self.xim,
754
+ npair=self.npair,
755
+ norm=self.norm)
756
+
757
+ def __process_patches(self, cat, dotomo=True, do_dc=False, rotsignflip=False, apply_edge_correction=False, adjust_tree=False,
758
+ save_patchres=False, save_filebase="", keep_patchres=False):
759
+
760
+ if save_patchres:
761
+ if not Path(save_patchres).is_dir():
762
+ raise ValueError('Path to directory does not exist.')
763
+
764
+ for elp in range(cat.npatches):
765
+ if self._verbose_python:
766
+ print('Doing patch %i/%i'%(elp+1,cat.npatches))
767
+
768
+ # Compute statistics on patch
769
+ pcat = cat.frompatchind(elp,rotsignflip=rotsignflip)
770
+ pcorr = GGCorrelation(
771
+ min_sep=self.min_sep,
772
+ max_sep=self.max_sep,
773
+ nbinsr=self.nbinsr,
774
+ method=self.method,
775
+ shuffle_pix=self.shuffle_pix,
776
+ tree_resos=self.tree_resos,
777
+ rmin_pixsize=self.rmin_pixsize,
778
+ resoshift_leafs=self.resoshift_leafs,
779
+ minresoind_leaf=self.minresoind_leaf,
780
+ maxresoind_leaf=self.maxresoind_leaf,
781
+ nthreads=self.nthreads,
782
+ verbosity=self.verbosity)
783
+ pcorr.process(pcat, dotomo=dotomo, do_dc=do_dc)
784
+
785
+ # Update the total measurement
786
+ if elp == 0:
787
+ self.nbinsz = pcorr.nbinsz
788
+ self.nzcombis = pcorr.nzcombis
789
+ self.bin_centers = np.zeros_like(pcorr.bin_centers)
790
+ self.xip = np.zeros_like(pcorr.xip)
791
+ self.xim = np.zeros_like(pcorr.xim)
792
+ self.norm = np.zeros_like(pcorr.norm)
793
+ self.npair = np.zeros_like(pcorr.norm)
794
+ if keep_patchres:
795
+ centers_patches = np.zeros((cat.npatches, *pcorr.bin_centers.shape), dtype=pcorr.bin_centers.dtype)
796
+ xip_patches = np.zeros((cat.npatches, *pcorr.xip.shape), dtype=pcorr.xip.dtype)
797
+ xim_patches = np.zeros((cat.npatches, *pcorr.xim.shape), dtype=pcorr.xim.dtype)
798
+ norm_patches = np.zeros((cat.npatches, *pcorr.norm.shape), dtype=pcorr.norm.dtype)
799
+ npair_patches = np.zeros((cat.npatches, *pcorr.npair.shape), dtype=pcorr.npair.dtype)
800
+ self.bin_centers += pcorr.norm*pcorr.bin_centers
801
+ self.xip += pcorr.norm*pcorr.xip
802
+ self.xim += pcorr.norm*pcorr.xim
803
+ self.norm += pcorr.norm
804
+ self.npair += pcorr.npair
805
+ if keep_patchres:
806
+ centers_patches[elp] += pcorr.bin_centers
807
+ xip_patches[elp] += pcorr.xip
808
+ xim_patches[elp] += pcorr.xim
809
+ norm_patches[elp] += pcorr.norm
810
+ npair_patches[elp] += pcorr.npair
811
+ if save_patchres:
812
+ pcorr.saveinst(save_patchres, save_filebase+'_patch%i'%elp)
813
+
814
+ # Finalize the measurement on the full footprint
815
+ self.bin_centers /= self.norm
816
+ self.bin_centers_mean = np.mean(self.bin_centers, axis=0)
817
+ self.xip /= self.norm
818
+ self.xim /= self.norm
819
+ self.projection = "xipm"
820
+
821
+ if keep_patchres:
822
+ return centers_patches, xip_patches, xim_patches, norm_patches, npair_patches
823
+
824
+ def process(self, cat, dotomo=True, do_dc=False, rotsignflip=False, adjust_tree=False,
825
+ save_patchres=False, save_filebase="", keep_patchres=False):
826
+ r"""
827
+ Compute a shear 2PCF provided a shape catalog
828
+
829
+ Parameters
830
+ ----------
831
+ cat: orpheus.SpinTracerCatalog
832
+ The shape catalog which is processed
833
+ dotomo: bool
834
+ Flag that decides whether the tomographic information in the shape catalog should be used. Defaults to `True`.
835
+ do_dc: bool
836
+ Flag that decides whether to double-count the paircounts. This will have no impact on $\xi_\pm$, but can
837
+ significantly reduce the amplitude of $\xi_x$. Defaults to `False`.
838
+ rotsignflip: bool
839
+ If the shape catalog is has been decomposed in patches, choose whether the rotation angle should be flipped.
840
+ For simulated data this was always ok to set to 'False`. Defaults to `False`.
841
+ adjust_tree: bool
842
+ Overrides the original setup of the tree-approximations in the instance based on the nbar of the shape catalog.
843
+ Not implemented yet, therefore no effect. Has no effect yet. Defaults to `False`
844
+ save_patchres: bool or str
845
+ If the shape catalog is has been decomposed in patches, flag whether to save the GG measurements on the individual patches.
846
+ Note that the path needs to exist, otherwise a `ValueError` is raised. For a flat-sky catalog this parameter
847
+ has no effect. Defaults to `False`
848
+ save_filebase: str
849
+ Base of the filenames in which the patches are saved. The full filename will be `<save_patchres>/<save_filebase>_patchxx.npz`.
850
+ Only has an effect if the shape catalog consists of multiple patches and `save_patchres` is not `False`.
851
+ keep_patchres: bool
852
+ If the catalog consists of multiple patches, returns all measurements on the patches. Defaults to `False`.
853
+ """
854
+
855
+ # Make sure that in case the catalog is spherical, it has been decomposed into patches
856
+ if cat.geometry == 'spherical' and cat.patchinds is None:
857
+ raise ValueError('Error: Spherical catalog needs to be first decomposed into patches using the Catalog._topatches method.')
858
+
859
+ # Catalog consist of multiple patches
860
+ if cat.patchinds is not None:
861
+ return self.__process_patches(cat, dotomo=dotomo, do_dc=do_dc, rotsignflip=rotsignflip, adjust_tree=adjust_tree,
862
+ save_patchres=save_patchres, save_filebase=save_filebase, keep_patchres=keep_patchres)
863
+ # Catalog does not consist of patches
864
+ else:
865
+ # Prechecks
866
+ self._checkcats(cat, self.spins)
867
+ if not dotomo:
868
+ self.nbinsz = 1
869
+ old_zbins = cat.zbins[:]
870
+ cat.zbins = np.zeros(cat.ngal, dtype=np.int32)
871
+ self.nzcombis = 1
872
+ else:
873
+ self.nbinsz = cat.nbinsz
874
+ zbins = cat.zbins
875
+ self.nzcombis = self.nbinsz*self.nbinsz
876
+
877
+ z2r = self.nbinsz*self.nbinsz*self.nbinsr
878
+ sz2r = (self.nbinsz*self.nbinsz, self.nbinsr)
879
+ bin_centers = np.zeros(z2r).astype(np.float64)
880
+ xip = np.zeros(z2r).astype(np.complex128)
881
+ xim = np.zeros(z2r).astype(np.complex128)
882
+ norm = np.zeros(z2r).astype(np.float64)
883
+ npair = np.zeros(z2r).astype(np.int64)
884
+
885
+ args_basesetup = (np.float64(self.min_sep), np.float64(self.max_sep), np.int32(self.nbinsr), )
886
+
887
+ cutfirst = np.int32(self.tree_resos[0]==0.)
888
+ mhash = cat.multihash(dpixs=self.tree_resos[cutfirst:], dpix_hash=self.tree_resos[-1],
889
+ shuffle=self.shuffle_pix, w2field=True, normed=True)
890
+ ngal_resos, pos1s, pos2s, weights, zbins, isinners, allfields, index_matchers, pixs_galind_bounds, pix_gals, dpixs1_true, dpixs2_true = mhash
891
+ weight_resos = np.concatenate(weights).astype(np.float64)
892
+ pos1_resos = np.concatenate(pos1s).astype(np.float64)
893
+ pos2_resos = np.concatenate(pos2s).astype(np.float64)
894
+ zbin_resos = np.concatenate(zbins).astype(np.int32)
895
+ isinner_resos = np.concatenate(isinners).astype(np.float64)
896
+ e1_resos = np.concatenate([allfields[i][0] for i in range(len(allfields))]).astype(np.float64)
897
+ e2_resos = np.concatenate([allfields[i][1] for i in range(len(allfields))]).astype(np.float64)
898
+ index_matcher = np.concatenate(index_matchers).astype(np.int32)
899
+ pixs_galind_bounds = np.concatenate(pixs_galind_bounds).astype(np.int32)
900
+ pix_gals = np.concatenate(pix_gals).astype(np.int32)
901
+ index_matcher_flat = np.argwhere(cat.index_matcher>-1).flatten()
902
+ nregions = len(index_matcher_flat)
903
+
904
+ args_treeresos = (np.int32(self.tree_nresos), np.int32(self.tree_nresos-cutfirst),
905
+ dpixs1_true.astype(np.float64), dpixs2_true.astype(np.float64), self.tree_redges,
906
+ np.int32(self.resoshift_leafs), np.int32(self.minresoind_leaf),
907
+ np.int32(self.maxresoind_leaf), np.array(ngal_resos, dtype=np.int32), )
908
+ args_resos = (isinner_resos, weight_resos, pos1_resos, pos2_resos, e1_resos, e2_resos, zbin_resos,
909
+ index_matcher, pixs_galind_bounds, pix_gals, )
910
+ args_hash = (np.float64(cat.pix1_start), np.float64(cat.pix1_d), np.int32(cat.pix1_n),
911
+ np.float64(cat.pix2_start), np.float64(cat.pix2_d), np.int32(cat.pix2_n),
912
+ np.int32(nregions), index_matcher_flat.astype(np.int32),)
913
+ args_binning = (np.float64(self.min_sep), np.float64(self.max_sep), np.int32(self.nbinsr), np.int32(do_dc))
914
+ args_output = (bin_centers, xip, xim, norm, npair, )
915
+ func = self.clib.alloc_xipm_doubletree
916
+ args = (*args_treeresos,
917
+ np.int32(self.nbinsz),
918
+ *args_resos,
919
+ *args_hash,
920
+ *args_binning,
921
+ np.int32(self.nthreads),
922
+ np.int32(self._verbose_c)+np.int32(self._verbose_debug),
923
+ *args_output)
924
+
925
+ func(*args)
926
+
927
+ self.bin_centers = bin_centers.reshape(sz2r)
928
+ self.bin_centers_mean = np.mean(self.bin_centers, axis=0)
929
+ self.npair = npair.reshape(sz2r)
930
+ self.norm = norm.reshape(sz2r)
931
+ self.xip = xip.reshape(sz2r)
932
+ self.xim = xim.reshape(sz2r)
933
+ self.projection = "xipm"
934
+
935
+ if not dotomo:
936
+ cat.zbins = old_zbins
937
+
938
+
939
+ def computeMap2(self, radii, tofile=False):
940
+ """ Computes second-order aperture mass statistics given the shear correlation functions.
941
+ Uses the Crittenden 2002 filter.
942
+ """
943
+
944
+ Tp = lambda x: 1./128. * (x**4-16*x**2+32) * np.exp(-x**2/4.)
945
+ Tm = lambda x: 1./128. * (x**4) * np.exp(-x**2/4.)
946
+ result = np.zeros((4, self.nzcombis, len(radii)), dtype=float)
947
+ for elr, R in enumerate(radii):
948
+ thetared = self.bin_centers/R
949
+ pref = self.binsize*thetared**2/2.
950
+ t1 = np.sum(pref*(Tp(thetared)*self.xip + Tm(thetared)*self.xim), axis=1)
951
+ t2 = np.sum(pref*(Tp(thetared)*self.xip - Tm(thetared)*self.xim), axis=1)
952
+ result[0,:,elr] = t1.real # Map2
953
+ result[1,:,elr] = t1.imag # MapMx
954
+ result[2,:,elr] = t2.real # Mx2
955
+ result[3,:,elr] = t2.imag # MxMap (Difference from MapMx gives ~level of estimator uncertainty)
956
+
957
+ return result
958
+
959
+
960
+
961
+
962
+ ##############################
963
+ ## THIRD - ORDER STATISTICS ##
964
+ ##############################
965
+ class GGGCorrelation(BinnedNPCF):
966
+ r""" Class containing methods to measure and and obtain statistics that are built
967
+ from third-order shear correlation functions.
968
+
969
+ Attributes
970
+ ----------
971
+ n_cfs: int
972
+ The number of independent components of the NPCF.
973
+ min_sep: float
974
+ The smallest distance of each vertex for which the NPCF is computed.
975
+ max_sep: float
976
+ The largest distance of each vertex for which the NPCF is computed.
977
+
978
+ Notes
979
+ -----
980
+ Inherits all other parameters and attributes from :class:`BinnedNPCF`.
981
+ Additional child-specific parameters can be passed via ``kwargs``.
982
+ Either ``nbinsr`` or ``binsize`` has to be provided to fix the binning scheme .
983
+ """
984
+
985
+ def __init__(self, n_cfs, min_sep, max_sep, **kwargs):
986
+
987
+ super().__init__(order=3, spins=np.array([2,2,2], dtype=np.int32), n_cfs=n_cfs, min_sep=min_sep, max_sep=max_sep, **kwargs)
988
+ self.nmax = self.nmaxs[0]
989
+ self.phi = self.phis[0]
990
+ self.projection = None
991
+ self.projections_avail = [None, "X", "Centroid"]
992
+ self.nbinsz = None
993
+ self.nzcombis = None
994
+
995
+ # (Add here any newly implemented projections)
996
+ self._initprojections(self)
997
+ self.project["X"]["Centroid"] = self._x2centroid
998
+
999
+ def saveinst(self, path_save, fname):
1000
+
1001
+ if not Path(path_save).is_dir():
1002
+ raise ValueError('Path to directory does not exist.')
1003
+
1004
+ np.savez(path_save+fname,
1005
+ nbinsz=self.nbinsz,
1006
+ min_sep=self.min_sep,
1007
+ max_sep=self.max_sep,
1008
+ binsr=self.nbinsr,
1009
+ nbinsphi=self.nbinsphi,
1010
+ nmaxs=self.nmaxs,
1011
+ method=self.method,
1012
+ multicountcorr=self.multicountcorr,
1013
+ shuffle_pix=self.shuffle_pix,
1014
+ tree_resos=self.tree_resos,
1015
+ rmin_pixsize=self.rmin_pixsize,
1016
+ resoshift_leafs=self.resoshift_leafs,
1017
+ minresoind_leaf=self.minresoind_leaf,
1018
+ maxresoind_leaf=self.maxresoind_leaf,
1019
+ nthreads=self.nthreads,
1020
+ bin_centers=self.bin_centers,
1021
+ npcf_multipoles=self.npcf_multipoles,
1022
+ npcf_multipoles_norm=self.npcf_multipoles_norm)
1023
+
1024
+ def __process_patches(self, cat, dotomo=True, rotsignflip=False, apply_edge_correction=False, adjust_tree=False,
1025
+ save_patchres=False, save_filebase="", keep_patchres=False):
1026
+
1027
+ if save_patchres:
1028
+ if not Path(save_patchres).is_dir():
1029
+ raise ValueError('Path to directory does not exist.')
1030
+
1031
+ for elp in range(cat.npatches):
1032
+ if self._verbose_python:
1033
+ print('Doing patch %i/%i'%(elp+1,cat.npatches))
1034
+
1035
+ # Compute statistics on patch
1036
+ pcat = cat.frompatchind(elp,rotsignflip=rotsignflip)
1037
+ pcorr = GGGCorrelation(
1038
+ n_cfs=self.n_cfs,
1039
+ min_sep=self.min_sep,
1040
+ max_sep=self.max_sep,
1041
+ nbinsr=self.nbinsr,
1042
+ nbinsphi=self.nbinsphi,
1043
+ nmaxs=self.nmaxs,
1044
+ method=self.method,
1045
+ multicountcorr=self.multicountcorr,
1046
+ shuffle_pix=self.shuffle_pix,
1047
+ tree_resos=self.tree_resos,
1048
+ rmin_pixsize=self.rmin_pixsize,
1049
+ resoshift_leafs=self.resoshift_leafs,
1050
+ minresoind_leaf=self.minresoind_leaf,
1051
+ maxresoind_leaf=self.maxresoind_leaf,
1052
+ nthreads=self.nthreads,
1053
+ verbosity=self.verbosity)
1054
+ pcorr.process(pcat, dotomo=dotomo)
1055
+
1056
+ # Update the total measurement
1057
+ if elp == 0:
1058
+ self.nbinsz = pcorr.nbinsz
1059
+ self.nzcombis = pcorr.nzcombis
1060
+ self.bin_centers = np.zeros_like(pcorr.bin_centers)
1061
+ self.npcf_multipoles = np.zeros_like(pcorr.npcf_multipoles)
1062
+ self.npcf_multipoles_norm = np.zeros_like(pcorr.npcf_multipoles_norm)
1063
+ _footnorm = np.zeros_like(pcorr.bin_centers)
1064
+ if keep_patchres:
1065
+ centers_patches = np.zeros((cat.npatches, *pcorr.bin_centers.shape), dtype=pcorr.bin_centers.dtype)
1066
+ npcf_multipoles_patches = np.zeros((cat.npatches, *pcorr.npcf_multipoles.shape), dtype=pcorr.npcf_multipoles.dtype)
1067
+ npcf_multipoles_norm_patches = np.zeros((cat.npatches, *pcorr.npcf_multipoles_norm.shape), dtype=pcorr.npcf_multipoles_norm.dtype)
1068
+ _shelltriplets = np.array([[pcorr.npcf_multipoles_norm[0,z*self.nbinsz*self.nbinsz+z*self.nbinsz+z,i,i].real
1069
+ for i in range(pcorr.nbinsr)] for z in range(self.nbinsz)]) # Rough estimate of scaling of pair counts based on zeroth multipole of triplets
1070
+ # Rough estimate of scaling of pair counts based on zeroth multipole of triplets. Note that we might get nans here due to numerical
1071
+ # inaccuracies in the multiple counting corrections for bins with zero triplets, so we force those values to be zero.
1072
+ _patchnorm = np.nan_to_num(np.sqrt(_shelltriplets))
1073
+ self.bin_centers += _patchnorm*pcorr.bin_centers
1074
+ _footnorm += _patchnorm
1075
+ self.npcf_multipoles += pcorr.npcf_multipoles
1076
+ self.npcf_multipoles_norm += pcorr.npcf_multipoles_norm
1077
+ if keep_patchres:
1078
+ centers_patches[elp] += pcorr.bin_centers
1079
+ npcf_multipoles_patches[elp] += pcorr.npcf_multipoles
1080
+ npcf_multipoles_norm_patches[elp] += pcorr.npcf_multipoles_norm
1081
+ if save_patchres:
1082
+ pcorr.saveinst(save_patchres, save_filebase+'_patch%i'%elp)
1083
+
1084
+ # Finalize the measurement on the full footprint
1085
+ self.bin_centers = np.divide(self.bin_centers,_footnorm, out=np.zeros_like(self.bin_centers), where=_footnorm>0)
1086
+ self.bin_centers_mean = np.mean(self.bin_centers,axis=0)
1087
+ self.projection = "X"
1088
+
1089
+ if keep_patchres:
1090
+ return centers_patches, npcf_multipoles_patches, npcf_multipoles_norm_patches
1091
+
1092
+
1093
+ def process(self, cat, dotomo=True, rotsignflip=False, apply_edge_correction=False, adjust_tree=False,
1094
+ save_patchres=False, save_filebase="", keep_patchres=False):
1095
+ r"""
1096
+ Compute a shear 3PCF provided a shape catalog
1097
+
1098
+ Parameters
1099
+ ----------
1100
+ cat: orpheus.SpinTracerCatalog
1101
+ The shape catalog which is processed
1102
+ dotomo: bool
1103
+ Flag that decides whether the tomographic information in the shape catalog should be used. Defaults to `True`.
1104
+ rotsignflip: bool
1105
+ If the shape catalog is has been decomposed in patches, choose whether the rotation angle should be flipped.
1106
+ For simulated data this was always ok to set to 'False`. Has no effect yet. Defaults to `False`.
1107
+ apply_edge_correction: bool
1108
+ Flag that decides how the NPCF in the real space basis is computed.
1109
+ * If set to `True` the computation is done via edge-correcting the GGG-multipoles
1110
+ * If set to `False` both GGG and NNN are transformed separately and the ratio is done in the real-space basis
1111
+ Defaults to `False`.
1112
+ adjust_tree: bool
1113
+ Overrides the original setup of the tree-approximations in the instance based on the nbar of the shape catalog.
1114
+ Not implemented yet, therefore no effect. Has no effect yet. Defaults to `False`
1115
+ save_patchres: bool or str
1116
+ If the shape catalog is has been decomposed in patches, flag whether to save the GGG measurements on the individual patches.
1117
+ Note that the path needs to exist, otherwise a `ValueError` is raised. For a flat-sky catalog this parameter
1118
+ has no effect. Defaults to `False`
1119
+ save_filebase: str
1120
+ Base of the filenames in which the patches are saved. The full filename will be `<save_patchres>/<save_filebase>_patchxx.npz`.
1121
+ Only has an effect if the shape catalog consists of multiple patches and `save_patchres` is not `False`.
1122
+ keep_patchres: bool
1123
+ If the catalog consists of multiple patches, returns all measurements on the patches. Defaults to `False`.
1124
+ """
1125
+
1126
+ # Make sure that in case the catalog is spherical, it has been decomposed into patches
1127
+ if cat.geometry == 'spherical' and cat.patchinds is None:
1128
+ raise ValueError('Error: Spherical catalog needs to be first decomposed into patches using the Catalog._topatches method.')
1129
+
1130
+ # Catalog consist of multiple patches
1131
+ if cat.patchinds is not None:
1132
+ return self.__process_patches(cat, dotomo=dotomo, rotsignflip=rotsignflip,
1133
+ apply_edge_correction=apply_edge_correction, adjust_tree=adjust_tree,
1134
+ save_patchres=save_patchres, save_filebase=save_filebase, keep_patchres=keep_patchres)
1135
+
1136
+ # Catalog does not consist of patches
1137
+ else:
1138
+ self._checkcats(cat, self.spins)
1139
+ if not dotomo:
1140
+ self.nbinsz = 1
1141
+ old_zbins = cat.zbins[:]
1142
+ cat.zbins = np.zeros(cat.ngal, dtype=np.int32)
1143
+ self.nzcombis = 1
1144
+ else:
1145
+ self.nbinsz = cat.nbinsz
1146
+ zbins = cat.zbins
1147
+ self.nzcombis = self.nbinsz*self.nbinsz*self.nbinsz
1148
+ if adjust_tree:
1149
+ nbar = cat.ngal/(cat.len1*cat.len2)
1150
+
1151
+ sc = (4,self.nmax+1,self.nzcombis,self.nbinsr,self.nbinsr)
1152
+ sn = (self.nmax+1,self.nzcombis,self.nbinsr,self.nbinsr)
1153
+ szr = (self.nbinsz, self.nbinsr)
1154
+ bin_centers = np.zeros(self.nbinsz*self.nbinsr).astype(np.float64)
1155
+ threepcfs_n = np.zeros(4*(self.nmax+1)*self.nzcombis*self.nbinsr*self.nbinsr).astype(np.complex128)
1156
+ threepcfsnorm_n = np.zeros((self.nmax+1)*self.nzcombis*self.nbinsr*self.nbinsr).astype(np.complex128)
1157
+ args_basecat = (cat.isinner.astype(np.float64), cat.weight, cat.pos1, cat.pos2, cat.tracer_1, cat.tracer_2,
1158
+ cat.zbins.astype(np.int32), np.int32(self.nbinsz), np.int32(cat.ngal), )
1159
+ args_basesetup = (np.int32(0), np.int32(self.nmax), np.float64(self.min_sep),
1160
+ np.float64(self.max_sep), np.array([-1.]).astype(np.float64),
1161
+ np.int32(self.nbinsr), np.int32(self.multicountcorr), )
1162
+ if self.method=="Discrete":
1163
+ if not cat.hasspatialhash:
1164
+ cat.build_spatialhash(dpix=max(1.,self.max_sep//10.))
1165
+ args_pixgrid = (np.float64(cat.pix1_start), np.float64(cat.pix1_d), np.int32(cat.pix1_n),
1166
+ np.float64(cat.pix2_start), np.float64(cat.pix2_d), np.int32(cat.pix2_n), )
1167
+ args = (*args_basecat,
1168
+ *args_basesetup,
1169
+ cat.index_matcher,
1170
+ cat.pixs_galind_bounds,
1171
+ cat.pix_gals,
1172
+ *args_pixgrid,
1173
+ np.int32(self.nthreads),
1174
+ np.int32(self._verbose_c),
1175
+ bin_centers,
1176
+ threepcfs_n,
1177
+ threepcfsnorm_n)
1178
+ func = self.clib.alloc_Gammans_discrete_ggg
1179
+ elif self.method in ["Tree", "BaseTree", "DoubleTree"]:
1180
+ if self._verbose_debug:
1181
+ print("Doing multihash")
1182
+ cutfirst = np.int32(self.tree_resos[0]==0.)
1183
+ mhash = cat.multihash(dpixs=self.tree_resos[cutfirst:], dpix_hash=self.tree_resos[-1],
1184
+ shuffle=self.shuffle_pix, w2field=True, normed=True)
1185
+ ngal_resos, pos1s, pos2s, weights, zbins, isinners, allfields, index_matchers, pixs_galind_bounds, pix_gals, dpixs1_true, dpixs2_true = mhash
1186
+ weight_resos = np.concatenate(weights).astype(np.float64)
1187
+ pos1_resos = np.concatenate(pos1s).astype(np.float64)
1188
+ pos2_resos = np.concatenate(pos2s).astype(np.float64)
1189
+ zbin_resos = np.concatenate(zbins).astype(np.int32)
1190
+ isinner_resos = np.concatenate(isinners).astype(np.float64)
1191
+ e1_resos = np.concatenate([allfields[i][0] for i in range(len(allfields))]).astype(np.float64)
1192
+ e2_resos = np.concatenate([allfields[i][1] for i in range(len(allfields))]).astype(np.float64)
1193
+ _weightsq_resos = np.concatenate([allfields[i][2] for i in range(len(allfields))]).astype(np.float64)
1194
+ weightsq_resos = _weightsq_resos*weight_resos # As in reduce we renorm all the fields --> need to `unrenorm'
1195
+ index_matcher = np.concatenate(index_matchers).astype(np.int32)
1196
+ pixs_galind_bounds = np.concatenate(pixs_galind_bounds).astype(np.int32)
1197
+ pix_gals = np.concatenate(pix_gals).astype(np.int32)
1198
+ args_pixgrid = (np.float64(cat.pix1_start), np.float64(cat.pix1_d), np.int32(cat.pix1_n),
1199
+ np.float64(cat.pix2_start), np.float64(cat.pix2_d), np.int32(cat.pix2_n), )
1200
+ args_resos = (weight_resos, pos1_resos, pos2_resos, e1_resos, e2_resos, zbin_resos, weightsq_resos,
1201
+ index_matcher, pixs_galind_bounds, pix_gals, )
1202
+ args_output = (bin_centers, threepcfs_n, threepcfsnorm_n, )
1203
+ if self._verbose_debug:
1204
+ print("Doing %s"%self.method)
1205
+ if self.method=="Tree":
1206
+ args = (*args_basecat,
1207
+ np.int32(self.tree_nresos),
1208
+ self.tree_redges,
1209
+ np.array(ngal_resos, dtype=np.int32),
1210
+ *args_resos,
1211
+ *args_pixgrid,
1212
+ *args_basesetup,
1213
+ np.int32(self.nthreads),
1214
+ np.int32(self._verbose_c),
1215
+ *args_output)
1216
+ func = self.clib.alloc_Gammans_tree_ggg
1217
+ if self.method in ["BaseTree", "DoubleTree"]:
1218
+ args_resos = (isinner_resos, ) + args_resos
1219
+ index_matcher_flat = np.argwhere(cat.index_matcher>-1).flatten()
1220
+ nregions = len(index_matcher_flat)
1221
+ # Select regions with at least one inner galaxy (TODO: Optimize)
1222
+ filledregions = []
1223
+ for elregion in range(nregions):
1224
+ _ = cat.pix_gals[cat.pixs_galind_bounds[elregion]:cat.pixs_galind_bounds[elregion+1]]
1225
+ if np.sum(cat.isinner[_])>0:filledregions.append(elregion)
1226
+ filledregions = np.asarray(filledregions, dtype=np.int32)
1227
+ nfilledregions = np.int32(len(filledregions))
1228
+ args_regions = (index_matcher_flat.astype(np.int32), np.int32(nregions), filledregions, nfilledregions, )
1229
+ args_basesetup_dtree = (np.int32(self.nmax), np.float64(self.min_sep), np.float64(self.max_sep),
1230
+ np.int32(self.nbinsr), np.int32(self.multicountcorr), )
1231
+ args_treeresos = (np.int32(self.tree_nresos), np.int32(self.tree_nresos-cutfirst),
1232
+ dpixs1_true.astype(np.float64), dpixs2_true.astype(np.float64), self.tree_redges, )
1233
+ if self.method=="BaseTree":
1234
+ func = self.clib.alloc_Gammans_basetree_ggg
1235
+ if self.method=="DoubleTree":
1236
+ args_leafs = (np.int32(self.resoshift_leafs), np.int32(self.minresoind_leaf),
1237
+ np.int32(self.maxresoind_leaf), )
1238
+ args_treeresos = args_treeresos + args_leafs
1239
+ func = self.clib.alloc_Gammans_doubletree_ggg
1240
+ args = (*args_treeresos,
1241
+ np.array(ngal_resos, dtype=np.int32),
1242
+ np.int32(self.nbinsz),
1243
+ *args_resos,
1244
+ *args_pixgrid,
1245
+ *args_regions,
1246
+ *args_basesetup_dtree,
1247
+ np.int32(self.nthreads),
1248
+ np.int32(self._verbose_c),
1249
+ *args_output)
1250
+ func(*args)
1251
+
1252
+ self.bin_centers = bin_centers.reshape(szr)
1253
+ self.bin_centers_mean = np.mean(self.bin_centers, axis=0)
1254
+ self.npcf_multipoles = threepcfs_n.reshape(sc)
1255
+ self.npcf_multipoles_norm = threepcfsnorm_n.reshape(sn)
1256
+ self.projection = "X"
1257
+
1258
+ if apply_edge_correction:
1259
+ self.edge_correction()
1260
+
1261
+ if not dotomo:
1262
+ cat.zbins = old_zbins
1263
+
1264
+
1265
+ def edge_correction(self, ret_matrices=False):
1266
+
1267
+ def gen_M_matrix(thet1,thet2,threepcf_n_norm):
1268
+ nvals, ntheta, _ = threepcf_n_norm.shape
1269
+ nmax = (nvals-1)//2
1270
+ narr = np.arange(-nmax,nmax+1, dtype=np.int)
1271
+ nextM = np.zeros((nvals,nvals))
1272
+ for ind, ell in enumerate(narr):
1273
+ lminusn = ell-narr
1274
+ sel = np.logical_and(lminusn+nmax>=0, lminusn+nmax<nvals)
1275
+ nextM[ind,sel] = threepcf_n_norm[(lminusn+nmax)[sel],thet1,thet2].real / threepcf_n_norm[nmax,thet1,thet2].real
1276
+ return nextM
1277
+
1278
+ nvals, nzcombis, ntheta, _ = self.npcf_multipoles_norm.shape
1279
+ nmax = nvals-1
1280
+ threepcf_n_full = np.zeros((4,2*nmax+1, nzcombis, ntheta, ntheta), dtype=complex)
1281
+ threepcf_n_norm_full = np.zeros((2*nmax+1, nzcombis, ntheta, ntheta), dtype=complex)
1282
+ threepcf_n_corr = np.zeros(threepcf_n_full.shape, dtype=np.complex)
1283
+ threepcf_n_full[:,nmax:] = self.npcf_multipoles
1284
+ threepcf_n_norm_full[nmax:] = self.npcf_multipoles_norm
1285
+ for nextn in range(1,nvals):
1286
+ threepcf_n_full[0,nmax-nextn] = self.npcf_multipoles[0,nextn].transpose(0,2,1)
1287
+ threepcf_n_full[1,nmax-nextn] = self.npcf_multipoles[1,nextn].transpose(0,2,1)
1288
+ threepcf_n_full[2,nmax-nextn] = self.npcf_multipoles[3,nextn].transpose(0,2,1)
1289
+ threepcf_n_full[3,nmax-nextn] = self.npcf_multipoles[2,nextn].transpose(0,2,1)
1290
+ threepcf_n_norm_full[nmax-nextn] = self.npcf_multipoles_norm[nextn].transpose(0,2,1)
1291
+
1292
+ if ret_matrices:
1293
+ mats = np.zeros((nzcombis,ntheta,ntheta,nvals,nvals))
1294
+ for indz in range(nzcombis):
1295
+ #sys.stdout.write("%i"%indz)
1296
+ for thet1 in range(ntheta):
1297
+ for thet2 in range(ntheta):
1298
+ nextM = gen_M_matrix(thet1,thet2,threepcf_n_norm_full[:,indz])
1299
+ nextM_inv = np.linalg.inv(nextM)
1300
+ if ret_matrices:
1301
+ mats[indz,thet1,thet2] = nextM
1302
+ for i in range(4):
1303
+ threepcf_n_corr[i,:,indz,thet1,thet2] = np.matmul(nextM_inv,threepcf_n_full[i,:,indz,thet1,thet2])
1304
+
1305
+ self.npcf_multipoles = threepcf_n_corr[:,nmax:]
1306
+ self.is_edge_corrected = True
1307
+
1308
+ if ret_matrices:
1309
+ return threepcf_n_corr[:,nmax:], mats
1310
+
1311
+ # Legacy transform in pure python -- now upgraded to .c
1312
+ def _multipoles2npcf_py(self):
1313
+
1314
+ _, nzcombis, rbins, rbins = np.shape(self.npcf_multipoles[0])
1315
+ self.npcf = np.zeros((4, nzcombis, rbins, rbins, len(self.phi)), dtype=complex)
1316
+ self.npcf_norm = np.zeros((nzcombis, rbins, rbins, len(self.phi)), dtype=complex)
1317
+ ztiler = np.arange(self.nbinsz*self.nbinsz*self.nbinsz).reshape(
1318
+ (self.nbinsz,self.nbinsz,self.nbinsz)).transpose(0,2,1).flatten().astype(np.int32)
1319
+
1320
+ # 3PCF components
1321
+ conjmap = [0,1,3,2]
1322
+ for elm in range(4):
1323
+ for elphi, phi in enumerate(self.phi):
1324
+ N0 = 1./(2*np.pi) * self.npcf_multipoles_norm[0].astype(complex)
1325
+ tmp = 1./(2*np.pi) * self.npcf_multipoles[elm,0].astype(complex)
1326
+ for n in range(1,self.nmax+1):
1327
+ _const = 1./(2*np.pi) * np.exp(1J*n*phi)
1328
+ tmp += _const * self.npcf_multipoles[elm,n].astype(complex)
1329
+ tmp += _const.conj() * self.npcf_multipoles[conjmap[elm],n][ztiler].astype(complex).transpose(0,2,1)
1330
+ self.npcf[elm,...,elphi] = tmp
1331
+ # Number of triangles
1332
+ for elphi, phi in enumerate(self.phi):
1333
+ tmptotnorm = 1./(2*np.pi) * self.npcf_multipoles_norm[0].astype(complex)
1334
+ for n in range(1,self.nmax+1):
1335
+ _const = 1./(2*np.pi) * np.exp(1J*n*phi)
1336
+ tmptotnorm += _const * self.npcf_multipoles_norm[n].astype(complex)
1337
+ tmptotnorm += _const.conj() * self.npcf_multipoles_norm[n][ztiler].astype(complex).transpose(0,2,1)
1338
+ self.npcf_norm[...,elphi] = tmptotnorm
1339
+
1340
+ if self.is_edge_corrected:
1341
+ dphi = self.phi[1] - self.phi[0]
1342
+ N0 = dphi/(2*np.pi) * self.npcf_multipoles_norm[self.nmax].astype(complex)
1343
+ sel_zero = np.isnan(N0)
1344
+ _a = self.npcf
1345
+ _b = N0.real[np.newaxis, :, :, :, np.newaxis]
1346
+ self.npcf = np.divide(_a, _b, out=np.zeros_like(_a), where=_b>0)
1347
+ else:
1348
+ _a = self.npcf
1349
+ _b = self.npcf_norm
1350
+ self.npcf = np.divide(_a, _b, out=np.zeros_like(_a), where=_b>0)
1351
+ self.projection = "X"
1352
+
1353
+ def multipoles2npcf(self, projection='Centroid'):
1354
+ r"""
1355
+ Notes
1356
+ -----
1357
+ The Upsilon and Norms are only computed for the n>0 multipoles. The n<0 multipoles are recovered by symmetry considerations given in Eq A.6 in Porth+23.
1358
+ """
1359
+ assert(projection in self.projections_avail)
1360
+ int_projection = {'X':0,'Centroid':1}
1361
+ _, nzcombis, rbins, rbins = np.shape(self.npcf_multipoles[0])
1362
+ thisnpcf = np.zeros(4*self.nbinsz*self.nbinsz*self.nbinsz*self.nbinsr*self.nbinsr*len(self.phi), dtype=np.complex128)
1363
+ thisnpcf_norm = np.zeros(self.nbinsz*self.nbinsz*self.nbinsz*self.nbinsr*self.nbinsr*len(self.phi), dtype=np.complex128)
1364
+ self.clib.multipoles2npcf_ggg(
1365
+ self.npcf_multipoles.flatten(), self.npcf_multipoles_norm.flatten(), np.int32(self.nmax), np.int32(self.nbinsz),
1366
+ self.bin_centers_mean, np.int32(self.nbinsr), self.phi.astype(np.float64), np.int32(self.nbinsphi[0]),
1367
+ np.int32(int_projection[projection]), np.int32(self.nthreads), thisnpcf, thisnpcf_norm)
1368
+ self.npcf = thisnpcf.reshape((4,nzcombis,self.nbinsr,self.nbinsr,len(self.phi)))
1369
+ self.npcf_norm = thisnpcf_norm.reshape((nzcombis,self.nbinsr,self.nbinsr,len(self.phi)))
1370
+ self.projection = projection
1371
+
1372
+ ## PROJECTIONS (Preferably use direct in c-level) ##
1373
+ def projectnpcf(self, projection):
1374
+ super()._projectnpcf(self, projection)
1375
+
1376
+ def _x2centroid(self):
1377
+ gammas_cen = np.zeros_like(self.npcf)
1378
+ pimod = lambda x: x%(2*np.pi) - 2*np.pi*(x%(2*np.pi)>=np.pi)
1379
+ npcf_cen = np.zeros(self.npcf.shape, dtype=complex)
1380
+ _centers = np.mean(self.bin_centers, axis=0)
1381
+ for elb1, bin1 in enumerate(_centers):
1382
+ for elb2, bin2 in enumerate(_centers):
1383
+ bin3 = np.sqrt(bin1**2 + bin2**2 - 2*bin1*bin2*np.cos(self.phi))
1384
+ phiexp = np.exp(1J*self.phi)
1385
+ phiexp_c = np.exp(-1J*self.phi)
1386
+ prod1 = (bin1 + bin2*phiexp_c)/(bin1 + bin2*phiexp) #q1
1387
+ prod2 = (2*bin1 - bin2*phiexp_c)/(2*bin1 - bin2*phiexp) #q2
1388
+ prod3 = (2*bin2*phiexp_c - bin1)/(2*bin2*phiexp - bin1) #q3
1389
+ prod1_inv = prod1.conj()/np.abs(prod1)
1390
+ prod2_inv = prod2.conj()/np.abs(prod2)
1391
+ prod3_inv = prod3.conj()/np.abs(prod3)
1392
+ rot_nom = np.zeros((4,len(self.phi)))
1393
+ rot_nom[0] = pimod(np.angle(prod1*prod2*prod3*np.exp(3*1J*self.phi)))
1394
+ rot_nom[1] = pimod(np.angle(prod1_inv*prod2*prod3*np.exp(1J*self.phi)))
1395
+ rot_nom[2] = pimod(np.angle(prod1*prod2_inv*prod3*np.exp(3*1J*self.phi)))
1396
+ rot_nom[3] = pimod(np.angle(prod1*prod2*prod3_inv*np.exp(-1J*self.phi)))
1397
+ gammas_cen[:,:,elb1,elb2] = self.npcf[:,:,elb1,elb2]*np.exp(1j*rot_nom)[:,np.newaxis,:]
1398
+ return gammas_cen
1399
+
1400
+ def computeMap3(self, radii, do_multiscale=False, tofile=False, filtercache=None):
1401
+ """
1402
+ Compute third-order aperture statistics using the polynomial filter.
1403
+ """
1404
+
1405
+ if self.npcf is None and self.npcf_multipoles is not None:
1406
+ self.multipoles2npcf(projection='Centroid')
1407
+
1408
+ if self.projection != "Centroid":
1409
+ self.projectnpcf("Centroid")
1410
+
1411
+ nradii = len(radii)
1412
+ if not do_multiscale:
1413
+ nrcombis = nradii
1414
+ filterfunc = self._map3_filtergrid_singleR
1415
+ _rcut = 1
1416
+ else:
1417
+ nrcombis = nradii*nradii*nradii
1418
+ filterfunc = self._map3_filtergrid_multiR
1419
+ _rcut = nradii
1420
+ map3s = np.zeros((8, self.nzcombis, nrcombis), dtype=complex)
1421
+ M3 = np.zeros((self.nzcombis, nrcombis), dtype=complex)
1422
+ M2M1 = np.zeros((self.nzcombis, nrcombis), dtype=complex)
1423
+ M2M2 = np.zeros((self.nzcombis, nrcombis), dtype=complex)
1424
+ M2M3 = np.zeros((self.nzcombis, nrcombis), dtype=complex)
1425
+ tmprcombi = 0
1426
+
1427
+ for elr1, R1 in enumerate(radii):
1428
+ for elr2, R2 in enumerate(radii[:_rcut]):
1429
+ for elr3, R3 in enumerate(radii[:_rcut]):
1430
+ if not do_multiscale:
1431
+ R2 = R1
1432
+ R3 = R1
1433
+ if filtercache is not None:
1434
+ T0, T3_123, T3_231, T3_312 = filtercache[tmprcombi][0], filtercache[tmprcombi][1], filtercache[tmprcombi][2], filtercache[tmprcombi][3]
1435
+ else:
1436
+ T0, T3_123, T3_231, T3_312 = filterfunc(R1, R2, R3)
1437
+ M3[:,tmprcombi] = np.nansum(T0*self.npcf[0,...],axis=(1,2,3))
1438
+ M2M1[:,tmprcombi] = np.nansum(T3_123*self.npcf[1,...],axis=(1,2,3))
1439
+ M2M2[:,tmprcombi] = np.nansum(T3_231*self.npcf[2,...],axis=(1,2,3))
1440
+ M2M3[:,tmprcombi] = np.nansum(T3_312*self.npcf[3,...],axis=(1,2,3))
1441
+ tmprcombi += 1
1442
+ map3s[0] = 1./4. * (+M2M1+M2M2+M2M3 + M3).real # MapMapMap
1443
+ map3s[1] = 1./4. * (+M2M1+M2M2-M2M3 + M3).imag # MapMapMx
1444
+ map3s[2] = 1./4. * (+M2M1-M2M2+M2M3 + M3).imag # MapMxMap
1445
+ map3s[3] = 1./4. * (-M2M1+M2M2+M2M3 + M3).imag # MxMapMap
1446
+ map3s[4] = 1./4. * (-M2M1+M2M2+M2M3 - M3).real # MapMxMx
1447
+ map3s[5] = 1./4. * (+M2M1-M2M2+M2M3 - M3).real # MxMapMx
1448
+ map3s[6] = 1./4. * (+M2M1+M2M2-M2M3 - M3).real # MxMxMap
1449
+ map3s[7] = 1./4. * (+M2M1+M2M2+M2M3 - M3).imag # MxMxMx
1450
+
1451
+ if tofile:
1452
+ # Write to file
1453
+ pass
1454
+
1455
+ return map3s
1456
+
1457
+ def _map3_filtergrid_singleR(self, R1, R2, R3):
1458
+ return self.__map3_filtergrid_singleR(R1, R2, R3, self.bin_edges, self.bin_centers_mean, self.phi)
1459
+
1460
+ @staticmethod
1461
+ @jit(nopython=True)
1462
+ def __map3_filtergrid_singleR(R1, R2, R3, normys_edges, normys_centers, phis):
1463
+
1464
+ # To avoid zero divisions we set some default bin centers for the evaluation of the filter
1465
+ # As for those positions the 3pcf is zero those will not contribute to the map3 integral
1466
+ if (np.min(normys_centers)==0):
1467
+ _sel = normys_centers!=0
1468
+ _avratios = np.mean(normys_centers[_sel]/normys_edges[_sel])
1469
+ normys_centers[~_sel] = _avratios*normys_edges[~_sel]
1470
+
1471
+ R_ap = R1
1472
+ nbinsr = len(normys_centers)
1473
+ nbinsphi = len(phis)
1474
+ _cphis = np.cos(phis)
1475
+ _c2phis = np.cos(2*phis)
1476
+ _sphis = np.sin(phis)
1477
+ _ephis = np.e**(1J*phis)
1478
+ _ephisc = np.e**(-1J*phis)
1479
+ _e2phis = np.e**(2J*phis)
1480
+ _e2phisc = np.e**(-2J*phis)
1481
+ T0 = np.zeros((nbinsr, nbinsr, nbinsphi), dtype=nb_complex128)
1482
+ T3_123 = np.zeros((nbinsr, nbinsr, nbinsphi), dtype=nb_complex128)
1483
+ T3_231 = np.zeros((nbinsr, nbinsr, nbinsphi), dtype=nb_complex128)
1484
+ T3_312 = np.zeros((nbinsr, nbinsr, nbinsphi), dtype=nb_complex128)
1485
+ for elb1 in range(nbinsr):
1486
+ _y1 = normys_centers[elb1]
1487
+ _dbin1 = normys_edges[elb1+1] - normys_edges[elb1]
1488
+ for elb2 in range(nbinsr):
1489
+ _y2 = normys_centers[elb2]
1490
+ _y14 = _y1**4
1491
+ _y13y2 = _y1**3*_y2
1492
+ _y12y22 = _y1**2*_y2**2
1493
+ _y1y23 = _y1*_y2**3
1494
+ _y24 = _y2**4
1495
+ _dbin2 = normys_edges[elb2+1] - normys_edges[elb2]
1496
+ _dbinphi = phis[1] - phis[0]
1497
+ _absq1s = 1./9.*(4*_y1**2 - 4*_y1*_y2*_cphis + 1*_y2**2)
1498
+ _absq2s = 1./9.*(1*_y1**2 - 4*_y1*_y2*_cphis + 4*_y2**2)
1499
+ _absq3s = 1./9.*(1*_y1**2 + 2*_y1*_y2*_cphis + 1*_y2**2)
1500
+ _absq123s = 2./3. * (_y1**2+_y2**2-_y1*_y2*_cphis)
1501
+ _absq1q2q3_2 = _absq1s*_absq2s*_absq3s
1502
+ _measures = _y1*_dbin1/R_ap**2 * _y2*_dbin2/R_ap**2 * _dbinphi/(2*np.pi)
1503
+ nextT0 = _absq1q2q3_2/R_ap**6 * np.e**(-_absq123s/(2*R_ap**2))
1504
+ T0[elb1,elb2] = 1./24. * _measures * nextT0
1505
+ _tmp1 = _y1**4 + _y2**4 + _y1**2*_y2**2 * (2*np.cos(2*phis)-5.)
1506
+ _tmp2 = (_y1**2+_y2**2)*_cphis + 9J*(_y1**2-_y2**2)*_sphis
1507
+ q1q2q3starsq = -1./81*(2*_tmp1 - _y1*_y2*_tmp2)
1508
+ nextT3_123 = np.e**(-_absq123s/(2*R_ap**2)) * (1./24*_absq1q2q3_2/R_ap**6 -
1509
+ 1./9.*q1q2q3starsq/R_ap**4 +
1510
+ 1./27*(q1q2q3starsq**2/(_absq1q2q3_2*R_ap**2) +
1511
+ 2*q1q2q3starsq/(_absq3s*R_ap**2)))
1512
+ _231inner = -4*_y14 + 2*_y24 + _y13y2*8*_cphis + _y12y22*(8*_e2phis-4-_e2phisc) + _y1y23*(_ephisc-8*_ephis)
1513
+ q2q3q1starsq = -1./81*(_231inner)
1514
+ nextT3_231 = np.e**(-_absq123s/(2*R_ap**2)) * (1./24*_absq1q2q3_2/R_ap**6 -
1515
+ 1./9.*q2q3q1starsq/R_ap**4 +
1516
+ 1./27*(q2q3q1starsq**2/(_absq1q2q3_2*R_ap**2) +
1517
+ 2*q2q3q1starsq/(_absq1s*R_ap**2)))
1518
+ _312inner = 2*_y14 - 4*_y24 - _y13y2*(8*_ephisc-_ephis) - _y12y22*(4+_e2phis-8*_e2phisc) + 8*_y1y23*_cphis
1519
+ q3q1q2starsq = -1./81*(_312inner)
1520
+ nextT3_312 = np.e**(-_absq123s/(2*R_ap**2)) * (1./24*_absq1q2q3_2/R_ap**6 -
1521
+ 1./9.*q3q1q2starsq/R_ap**4 +
1522
+ 1./27*(q3q1q2starsq**2/(_absq1q2q3_2*R_ap**2) +
1523
+ 2*q3q1q2starsq/(_absq2s*R_ap**2)))
1524
+ T3_123[elb1,elb2] = _measures * nextT3_123
1525
+ T3_231[elb1,elb2] = _measures * nextT3_231
1526
+ T3_312[elb1,elb2] = _measures * nextT3_312
1527
+
1528
+ return T0, T3_123, T3_231, T3_312
1529
+
1530
+ def _map3_filtergrid_multiR(self, R1, R2, R3):
1531
+ return self.__map3_filtergrid_multiR(R1, R2, R3, self.bin_edges, self.bin_centers_mean, self.phi, include_measure=True)
1532
+
1533
+ @staticmethod
1534
+ @jit(nopython=True)
1535
+ def __map3_filtergrid_multiR(R1, R2, R3, normys_edges, normys_centers, phis, include_measure=True):
1536
+
1537
+ # To avoid zero divisions we set some default bin centers for the evaluation of the filter
1538
+ # As for those positions the 3pcf is zero those will not contribute to the map3 integral
1539
+ if (np.min(normys_centers)==0):
1540
+ _sel = normys_centers!=0
1541
+ _avratios = np.mean(normys_centers[_sel]/normys_edges[_sel])
1542
+ normys_centers[~_sel] = _avratios*normys_edges[~_sel]
1543
+
1544
+ nbinsr = len(normys_centers)
1545
+ nbinsphi = len(phis)
1546
+ _cphis = np.cos(phis)
1547
+ _c2phis = np.cos(2*phis)
1548
+ _sphis = np.sin(phis)
1549
+ _ephis = np.e**(1J*phis)
1550
+ _ephisc = np.e**(-1J*phis)
1551
+ _e2phis = np.e**(2J*phis)
1552
+ _e2phisc = np.e**(-2J*phis)
1553
+ T0 = np.zeros((nbinsr, nbinsr, nbinsphi), dtype=nb_complex128)
1554
+ T3_123 = np.zeros((nbinsr, nbinsr, nbinsphi), dtype=nb_complex128)
1555
+ T3_231 = np.zeros((nbinsr, nbinsr, nbinsphi), dtype=nb_complex128)
1556
+ T3_312 = np.zeros((nbinsr, nbinsr, nbinsphi), dtype=nb_complex128)
1557
+ for elb1 in range(nbinsr):
1558
+ _y1 = normys_centers[elb1]
1559
+ _dbin1 = normys_edges[elb1+1] - normys_edges[elb1]
1560
+ for elb2 in range(nbinsr):
1561
+ Theta2 = np.sqrt((R1**2*R2**2 + R1**2*R3**2 + R2**2*R3**2)/3)
1562
+ S = R1**2*R2**2*R3**2/Theta2**3
1563
+
1564
+ _y2 = normys_centers[elb2]
1565
+ _y14 = _y1**4
1566
+ _y13y2 = _y1**3*_y2
1567
+ _y12y22 = _y1**2*_y2**2
1568
+ _y1y23 = _y1*_y2**3
1569
+ _y24 = _y2**4
1570
+ _dbin2 = normys_edges[elb2+1] - normys_edges[elb2]
1571
+ _dbinphi = phis[1] - phis[0]
1572
+ _absq1s = 1./9.*(4*_y1**2 - 4*_y1*_y2*_cphis + 1*_y2**2)
1573
+ _absq2s = 1./9.*(1*_y1**2 - 4*_y1*_y2*_cphis + 4*_y2**2)
1574
+ _absq3s = 1./9.*(1*_y1**2 + 2*_y1*_y2*_cphis + 1*_y2**2)
1575
+ _absq123s = 2./3. * (_y1**2+_y2**2-_y1*_y2*_cphis)
1576
+ _absq1q2q3_2 = _absq1s*_absq2s*_absq3s
1577
+
1578
+ Z = ((-R1**2+2*R2**2+2*R3**2)*_absq1s + (2*R1**2-R2**2+2*R3**2)*_absq2s + (2*R1**2+2*R2**2-R3**2)*_absq3s)/(6*Theta2**2)
1579
+ _frac231c = 1./3.*_y2*(2*_y1*_ephis-_y2)/_absq1s
1580
+ _frac312c = 1./3.*_y1*(_y1-2*_y2*_ephisc)/_absq2s
1581
+ _frac123c = 1./3.*(_y2**2-_y1**2+2J*_y1*_y2*_sphis)/_absq3s
1582
+ f1 = (R2**2+R3**2)/(2*Theta2) + _frac231c * (R2**2-R3**2)/(6*Theta2)
1583
+ f2 = (R1**2+R3**2)/(2*Theta2) + _frac312c * (R3**2-R1**2)/(6*Theta2)
1584
+ f3 = (R1**2+R2**2)/(2*Theta2) + _frac123c * (R1**2-R2**2)/(6*Theta2)
1585
+ f1c = f1.conj()
1586
+ f2c = f2.conj()
1587
+ f3c = f3.conj()
1588
+ g1c = (R2**2*R3**2/Theta2**2 + R1**2*(R3**2-R2**2)/(3*Theta2**2)*_frac231c).conj()
1589
+ g2c = (R3**2*R1**2/Theta2**2 + R2**2*(R1**2-R3**2)/(3*Theta2**2)*_frac312c).conj()
1590
+ g3c = (R1**2*R2**2/Theta2**2 + R3**2*(R2**2-R1**2)/(3*Theta2**2)*_frac123c).conj()
1591
+ _measures = _y1*_dbin1/Theta2 * _y2*_dbin2/Theta2 * _dbinphi/(2*np.pi)
1592
+ if not include_measure:
1593
+ _measures/=_measures
1594
+ nextT0 = _absq1q2q3_2/Theta2**3 * f1c**2*f2c**2*f3c**2 * np.e**(-Z)
1595
+ T0[elb1,elb2] = S/24. * _measures * nextT0
1596
+
1597
+ _tmp1 = _y1**4 + _y2**4 + _y1**2*_y2**2 * (2*np.cos(2*phis)-5.)
1598
+ _tmp2 = (_y1**2+_y2**2)*_cphis + 9J*(_y1**2-_y2**2)*_sphis
1599
+ q1q2q3starsq = -1./81*(2*_tmp1 - _y1*_y2*_tmp2)
1600
+ nextT3_123 = np.e**(-Z) * (1./24*_absq1q2q3_2/Theta2**3 * f1c**2*f2c**2*f3**2 -
1601
+ 1./9.*q1q2q3starsq/Theta2**2 * f1c*f2c*f3*g3c +
1602
+ 1./27*(q1q2q3starsq**2/(_absq1q2q3_2*Theta2) * g3c**2 +
1603
+ 2*R1**2*R2**2/Theta2**2 * q1q2q3starsq/(_absq3s*Theta2) * f1c*f2c))
1604
+ _231inner = -4*_y14 + 2*_y24 + _y13y2*8*_cphis + _y12y22*(8*_e2phis-4-_e2phisc) + _y1y23*(_ephisc-8*_ephis)
1605
+ q2q3q1starsq = -1./81*(_231inner)
1606
+ nextT3_231 = np.e**(-Z) * (1./24*_absq1q2q3_2/Theta2**3 * f2c**2*f3c**2*f1**2 -
1607
+ 1./9.*q2q3q1starsq/Theta2**2 * f2c*f3c*f1*g1c +
1608
+ 1./27*(q2q3q1starsq**2/(_absq1q2q3_2*Theta2) * g1c**2 +
1609
+ 2*R2**2*R3**2/Theta2**2 * q2q3q1starsq/(_absq1s*Theta2) * f2c*f3c))
1610
+ _312inner = 2*_y14 - 4*_y24 - _y13y2*(8*_ephisc-_ephis) - _y12y22*(4+_e2phis-8*_e2phisc) + 8*_y1y23*_cphis
1611
+ q3q1q2starsq = -1./81*(_312inner)
1612
+ nextT3_312 = np.e**(-Z) * (1./24*_absq1q2q3_2/Theta2**3 * f3c**2*f1c**2*f2**2 -
1613
+ 1./9.*q3q1q2starsq/Theta2**2 * f3c*f1c*f2*g2c +
1614
+ 1./27*(q3q1q2starsq**2/(_absq1q2q3_2*Theta2) * g2c**2 +
1615
+ 2*R3**2*R1**2/Theta2**2 * q3q1q2starsq/(_absq2s*Theta2) * f3c*f1c))
1616
+
1617
+ T3_123[elb1,elb2] = S * _measures * nextT3_123
1618
+ T3_231[elb1,elb2] = S * _measures * nextT3_231
1619
+ T3_312[elb1,elb2] = S * _measures * nextT3_312
1620
+
1621
+ return T0, T3_123, T3_231, T3_312
1622
+
1623
+
1624
+ class GNNCorrelation(BinnedNPCF):
1625
+ r""" Class containing methods to measure and and obtain statistics that are built
1626
+ from third-order source-lens-lens (G3L) correlation functions.
1627
+
1628
+ Attributes
1629
+ ----------
1630
+ min_sep: float
1631
+ The smallest distance of each vertex for which the NPCF is computed.
1632
+ max_sep: float
1633
+ The largest distance of each vertex for which the NPCF is computed.
1634
+ zweighting: bool
1635
+ Has no effect at the moment
1636
+ zweighting_sigma: bool
1637
+ Has not effect at the moment
1638
+
1639
+ Notes
1640
+ -----
1641
+ Inherits all other parameters and attributes from :class:`BinnedNPCF`.
1642
+ Additional child-specific parameters can be passed via ``kwargs``.
1643
+ Either ``nbinsr`` or ``binsize`` has to be provided to fix the binning scheme .
1644
+ """
1645
+
1646
+ def __init__(self, min_sep, max_sep, zweighting=False, zweighting_sigma=None, **kwargs):
1647
+ super().__init__(3, [2,0,0], n_cfs=1, min_sep=min_sep, max_sep=max_sep, **kwargs)
1648
+ self.nmax = self.nmaxs[0]
1649
+ self.phi = self.phis[0]
1650
+ self.projection = None
1651
+ self.projections_avail = [None, "X"]
1652
+ self.nbinsz_source = None
1653
+ self.nbinsz_lens = None
1654
+
1655
+ assert(zweighting in [True, False])
1656
+ self.zweighting = zweighting
1657
+ self.zweighting_sigma = zweighting_sigma
1658
+ if not self.zweighting :
1659
+ self.zweighting_sigma = None
1660
+ else:
1661
+ assert(isinstance(self.zweighting_sigma, float))
1662
+
1663
+ # (Add here any newly implemented projections)
1664
+ self._initprojections(self)
1665
+
1666
+
1667
+ def __process_patches(self, cat_source, cat_lens, dotomo_source=True, dotomo_lens=True, rotsignflip=False,
1668
+ apply_edge_correction=False, save_patchres=False, save_filebase="", keep_patchres=False):
1669
+ if save_patchres:
1670
+ if not Path(save_patchres).is_dir():
1671
+ raise ValueError('Path to directory does not exist.')
1672
+
1673
+ for elp in range(cat_source.npatches):
1674
+ if self._verbose_python:
1675
+ print('Doing patch %i/%i'%(elp+1,cat_source.npatches))
1676
+ # Compute statistics on patch
1677
+ pscat = cat_source.frompatchind(elp,rotsignflip=rotsignflip)
1678
+ plcat = cat_lens.frompatchind(elp,rotsignflip=rotsignflip)
1679
+ pcorr = GNNCorrelation(
1680
+ min_sep=self.min_sep,
1681
+ max_sep=self.max_sep,
1682
+ nbinsr=self.nbinsr,
1683
+ nbinsphi=self.nbinsphi,
1684
+ nmaxs=self.nmaxs,
1685
+ method=self.method,
1686
+ multicountcorr=self.multicountcorr,
1687
+ shuffle_pix=self.shuffle_pix,
1688
+ tree_resos=self.tree_resos,
1689
+ rmin_pixsize=self.rmin_pixsize,
1690
+ resoshift_leafs=self.resoshift_leafs,
1691
+ minresoind_leaf=self.minresoind_leaf,
1692
+ maxresoind_leaf=self.maxresoind_leaf,
1693
+ nthreads=self.nthreads,
1694
+ verbosity=self.verbosity)
1695
+ pcorr.process(pscat, plcat, dotomo_source=dotomo_source, dotomo_lens=dotomo_lens)
1696
+
1697
+ # Update the total measurement
1698
+ if elp == 0:
1699
+ self.nbinsz_source = pcorr.nbinsz_source
1700
+ self.nbinsz_lens = pcorr.nbinsz_lens
1701
+ self.bin_centers = np.zeros_like(pcorr.bin_centers)
1702
+ self.npcf_multipoles = np.zeros_like(pcorr.npcf_multipoles)
1703
+ self.npcf_multipoles_norm = np.zeros_like(pcorr.npcf_multipoles_norm)
1704
+ _footnorm = np.zeros_like(pcorr.bin_centers)
1705
+ if keep_patchres:
1706
+ centers_patches = np.zeros((cat_source.npatches, *pcorr.bin_centers.shape), dtype=pcorr.bin_centers.dtype)
1707
+ npcf_multipoles_patches = np.zeros((cat_source.npatches, *pcorr.npcf_multipoles.shape), dtype=pcorr.npcf_multipoles.dtype)
1708
+ npcf_multipoles_norm_patches = np.zeros((cat_source.npatches, *pcorr.npcf_multipoles_norm.shape), dtype=pcorr.npcf_multipoles_norm.dtype)
1709
+ _shelltriplets = np.array([[[pcorr.npcf_multipoles_norm[0,zs*self.nbinsz_lens*self.nbinsz_lens+zl*self.nbinsz_lens+zl,i,i].real
1710
+ for i in range(pcorr.nbinsr)] for zl in range(self.nbinsz_lens)] for zs in range(self.nbinsz_source)])
1711
+ # Rough estimate of scaling of pair counts based on zeroth multipole of triplets. Note that we might get nans here due to numerical
1712
+ # inaccuracies in the multiple counting corrections for bins with zero triplets, so we force those values to be zero.
1713
+ _patchnorm = np.nan_to_num(np.sqrt(_shelltriplets))
1714
+ self.bin_centers += _patchnorm*pcorr.bin_centers
1715
+ _footnorm += _patchnorm
1716
+ self.npcf_multipoles += pcorr.npcf_multipoles
1717
+ self.npcf_multipoles_norm += pcorr.npcf_multipoles_norm
1718
+ if keep_patchres:
1719
+ centers_patches[elp] += pcorr.bin_centers
1720
+ npcf_multipoles_patches[elp] += pcorr.npcf_multipoles
1721
+ npcf_multipoles_norm_patches[elp] += pcorr.npcf_multipoles_norm
1722
+ if save_patchres:
1723
+ pcorr.saveinst(save_patchres, save_filebase+'_patch%i'%elp)
1724
+
1725
+ # Finalize the measurement on the full footprint
1726
+ self.bin_centers = np.divide(self.bin_centers,_footnorm, out=np.zeros_like(self.bin_centers), where=_footnorm>0)
1727
+ self.bin_centers_mean =np.mean(self.bin_centers, axis=(0,1))
1728
+ self.projection = "X"
1729
+
1730
+ if keep_patchres:
1731
+ return centers_patches, npcf_multipoles_patches, npcf_multipoles_norm_patches
1732
+
1733
+ # TODO: Include z-weighting in estimator
1734
+ # * False --> No z-weighting, nothing to do
1735
+ # * True --> Tomographic zweighting: Use effective weight for each tomo bin combi. Do computation as tomo case with
1736
+ # no z-weighting and then weight in postprocessing where (zs, zl1, zl2) --> w_{zl1, zl2} * (zs)
1737
+ # As this could be many zbins, might want to only allow certain zcombis -- i.e. neighbouring zbins.
1738
+ # Functional form similar to https://arxiv.org/pdf/1909.06190.pdf
1739
+ # * Note that for spectroscopic catalogs we cannot do a full spectroscopic weighting as done i.e. the brute-force method
1740
+ # in https://arxiv.org/pdf/1909.06190.pdf, as this breaks the multipole decomposition.
1741
+ # * In general, think about what could be a consistent way get a good compromise between speed vs S/N. One extreme would
1742
+ # be just to use some broad bins and and the std within them (so 'thinner' bins have more weight). Other extreme would
1743
+ # be many small zbins with proper cross-weighting and maximum distance --> Becomes less efficient for more bins.
1744
+ def process(self, cat_source, cat_lens, dotomo_source=True, dotomo_lens=True, rotsignflip=False, apply_edge_correction=False,
1745
+ save_patchres=False, save_filebase="", keep_patchres=False):
1746
+ r"""
1747
+ Compute a shear-lens-lens correlation provided a source and a lens catalog.
1748
+
1749
+ Parameters
1750
+ ----------
1751
+ cat_source: orpheus.SpinTracerCatalog
1752
+ The source catalog which is processed
1753
+ cat_lens: orpheus.ScalarTracerCatalog
1754
+ The lens catalog which is processed
1755
+ dotomo_source: bool
1756
+ Flag that decides whether the tomographic information in the source catalog should be used. Defaults to `True`.
1757
+ dotomo_lens: bool
1758
+ Flag that decides whether the tomographic information in the lens catalog should be used. Defaults to `True`.
1759
+ rotsignflip: bool
1760
+ If the shape catalog is has been decomposed in patches, choose whether the rotation angle should be flipped.
1761
+ For simulated data this was always ok to set to 'False`. Defaults to `False`.
1762
+ apply_edge_correction: bool
1763
+ Flag that decides how the NPCF in the real space basis is computed.
1764
+ * If set to `True` the computation is done via edge-correcting the GNN-multipoles
1765
+ * If set to `False` both GNN and NNN are transformed separately and the ratio is done in the real-space basis
1766
+ Defaults to `False`.
1767
+ save_patchres: bool or str
1768
+ If the shape catalog is has been decomposed in patches, flag whether to save the GG measurements on the individual patches.
1769
+ Note that the path needs to exist, otherwise a `ValueError` is raised. For a flat-sky catalog this parameter
1770
+ has no effect. Defaults to `False`
1771
+ save_filebase: str
1772
+ Base of the filenames in which the patches are saved. The full filename will be `<save_patchres>/<save_filebase>_patchxx.npz`.
1773
+ Only has an effect if the shape catalog consists of multiple patches and `save_patchres` is not `False`.
1774
+ keep_patchres: bool
1775
+ If the catalog consists of multiple patches, returns all measurements on the patches. Defaults to `False`.
1776
+ """
1777
+ self._checkcats([cat_source, cat_lens, cat_lens], [2, 0, 0])
1778
+
1779
+ # Catch typical errors, i.e. incompatible catalogs or missin patch decompositions
1780
+ if cat_source.geometry=='spherical' and cat_source.patchinds is None:
1781
+ raise ValueError('Error: Spherical catalog needs to be first decomposed into patches using the Catalog._topatches method.')
1782
+ if cat_lens.geometry=='spherical' and cat_lens.patchinds is None:
1783
+ raise ValueError('Error: Spherical catalog needs to be first decomposed into patches using the Catalog._topatches method.')
1784
+ if cat_source.geometry != cat_lens.geometry:
1785
+ raise ValueError('Incompatible geometries of source catalog (%s) and lens catalog (%s).'%(
1786
+ cat_source.geometry,cat_lens.geometry))
1787
+
1788
+ # Catalog consist of multiple patches
1789
+ if (cat_source.patchinds is not None) and (cat_lens.patchinds is not None):
1790
+ return self.__process_patches(cat_source, cat_lens, dotomo_source=dotomo_source, dotomo_lens=dotomo_lens,
1791
+ rotsignflip=rotsignflip, apply_edge_correction=apply_edge_correction,
1792
+ save_patchres=save_patchres, save_filebase=save_filebase, keep_patchres=keep_patchres)
1793
+
1794
+ # Catalog does not consist of patches
1795
+ else:
1796
+
1797
+ if not dotomo_lens and self.zweighting:
1798
+ print("Redshift-weighting requires tomographic computation for the lenses.")
1799
+ dotomo_lens = True
1800
+
1801
+ if not dotomo_source:
1802
+ self.nbinsz_source = 1
1803
+ old_zbins_source = cat_source.zbins[:]
1804
+ cat_source.zbins = np.zeros(cat_source.ngal, dtype=np.int32)
1805
+ else:
1806
+ self.nbinsz_source = cat_source.nbinsz
1807
+ if not dotomo_lens:
1808
+ self.nbinsz_lens = 1
1809
+ old_zbins_lens = cat_lens.zbins[:]
1810
+ cat_lens.zbins = np.zeros(cat_lens.ngal, dtype=np.int32)
1811
+ else:
1812
+ self.nbinsz_lens = cat_lens.nbinsz
1813
+
1814
+ if self.zweighting:
1815
+ if cat_lens.zbins_mean is None:
1816
+ print("Redshift-weighting requires information about mean redshift in tomo bins of lens catalog")
1817
+ if cat_lens.zbins_std is None:
1818
+ print("Warning: Redshift-dispersion in tomo bins of lens catalog not given. Set to zero.")
1819
+ cat_lens.zbins_std = np.zeros(self.nbinsz_lens)
1820
+
1821
+ _z3combis = self.nbinsz_source*self.nbinsz_lens*self.nbinsz_lens
1822
+ _r2combis = self.nbinsr*self.nbinsr
1823
+ sc = (self.n_cfs, self.nmax+1, _z3combis, self.nbinsr, self.nbinsr)
1824
+ sn = (self.nmax+1, _z3combis, self.nbinsr,self.nbinsr)
1825
+ szr = (self.nbinsz_source, self.nbinsz_lens, self.nbinsr)
1826
+ bin_centers = np.zeros(reduce(operator.mul, szr)).astype(np.float64)
1827
+ Upsilon_n = np.zeros(reduce(operator.mul, sc)).astype(np.complex128)
1828
+ Norm_n = np.zeros(reduce(operator.mul, sn)).astype(np.complex128)
1829
+ args_sourcecat = (cat_source.isinner.astype(np.float64), cat_source.weight.astype(np.float64),
1830
+ cat_source.pos1.astype(np.float64), cat_source.pos2.astype(np.float64),
1831
+ cat_source.tracer_1.astype(np.float64), cat_source.tracer_2.astype(np.float64),
1832
+ cat_source.zbins.astype(np.int32), np.int32(self.nbinsz_source), np.int32(cat_source.ngal), )
1833
+ args_lenscat = (cat_lens.weight.astype(np.float64), cat_lens.pos1.astype(np.float64),
1834
+ cat_lens.pos2.astype(np.float64), cat_lens.zbins.astype(np.int32),
1835
+ np.int32(self.nbinsz_lens), np.int32(cat_lens.ngal), )
1836
+ args_basesetup = (np.int32(self.nmax), np.float64(self.min_sep), np.float64(self.max_sep),
1837
+ np.int32(self.nbinsr), np.int32(self.multicountcorr), )
1838
+ if self.method=="Discrete":
1839
+ hash_dpix = max(1.,self.max_sep//10.)
1840
+ jointextent = list(cat_source._jointextent([cat_lens], extend=self.tree_resos[-1]))
1841
+ cat_source.build_spatialhash(dpix=hash_dpix, extent=jointextent)
1842
+ cat_lens.build_spatialhash(dpix=hash_dpix, extent=jointextent)
1843
+ nregions = np.int32(len(np.argwhere(cat_source.index_matcher>-1).flatten()))
1844
+ args_hash = (cat_source.index_matcher, cat_source.pixs_galind_bounds, cat_source.pix_gals,
1845
+ cat_lens.index_matcher, cat_lens.pixs_galind_bounds, cat_lens.pix_gals, nregions, )
1846
+ args_pixgrid = (np.float64(cat_lens.pix1_start), np.float64(cat_lens.pix1_d), np.int32(cat_lens.pix1_n),
1847
+ np.float64(cat_lens.pix2_start), np.float64(cat_lens.pix2_d), np.int32(cat_lens.pix2_n), )
1848
+ args = (*args_sourcecat,
1849
+ *args_lenscat,
1850
+ *args_basesetup,
1851
+ *args_hash,
1852
+ *args_pixgrid,
1853
+ np.int32(self.nthreads),
1854
+ np.int32(self._verbose_c),
1855
+ bin_centers,
1856
+ Upsilon_n,
1857
+ Norm_n, )
1858
+ func = self.clib.alloc_Gammans_discrete_GNN
1859
+ if self.method == "DoubleTree":
1860
+ cutfirst = np.int32(self.tree_resos[0]==0.)
1861
+ jointextent = list(cat_source._jointextent([cat_lens], extend=self.tree_resos[-1]))
1862
+ # Build multihashes for sources and lenses
1863
+ mhash_source = cat_source.multihash(dpixs=self.tree_resos[cutfirst:], dpix_hash=self.tree_resos[-1],
1864
+ shuffle=self.shuffle_pix, normed=True, extent=jointextent)
1865
+ sngal_resos, spos1s, spos2s, sweights, szbins, sisinners, sallfields, sindex_matchers, \
1866
+ spixs_galind_bounds, spix_gals, sdpixs1_true, sdpixs2_true = mhash_source
1867
+ ngal_resos_source = np.array(sngal_resos, dtype=np.int32)
1868
+ weight_resos_source = np.concatenate(sweights).astype(np.float64)
1869
+ pos1_resos_source = np.concatenate(spos1s).astype(np.float64)
1870
+ pos2_resos_source = np.concatenate(spos2s).astype(np.float64)
1871
+ zbin_resos_source = np.concatenate(szbins).astype(np.int32)
1872
+ isinner_resos_source = np.concatenate(sisinners).astype(np.float64)
1873
+ e1_resos_source = np.concatenate([sallfields[i][0] for i in range(len(sallfields))]).astype(np.float64)
1874
+ e2_resos_source = np.concatenate([sallfields[i][1] for i in range(len(sallfields))]).astype(np.float64)
1875
+ index_matcher_source = np.concatenate(sindex_matchers).astype(np.int32)
1876
+ pixs_galind_bounds_source = np.concatenate(spixs_galind_bounds).astype(np.int32)
1877
+ pix_gals_source = np.concatenate(spix_gals).astype(np.int32)
1878
+ mhash_lens = cat_lens.multihash(dpixs=self.tree_resos[cutfirst:], dpix_hash=self.tree_resos[-1],
1879
+ shuffle=self.shuffle_pix, normed=True, extent=jointextent)
1880
+ lngal_resos, lpos1s, lpos2s, lweights, lzbins, lisinners, lallfields, lindex_matchers, \
1881
+ lpixs_galind_bounds, lpix_gals, ldpixs1_true, ldpixs2_true = mhash_lens
1882
+ ngal_resos_lens = np.array(lngal_resos, dtype=np.int32)
1883
+ weight_resos_lens = np.concatenate(lweights).astype(np.float64)
1884
+ pos1_resos_lens = np.concatenate(lpos1s).astype(np.float64)
1885
+ pos2_resos_lens = np.concatenate(lpos2s).astype(np.float64)
1886
+ zbin_resos_lens = np.concatenate(lzbins).astype(np.int32)
1887
+ isinner_resos_lens = np.concatenate(lisinners).astype(np.float64)
1888
+ index_matcher_lens = np.concatenate(lindex_matchers).astype(np.int32)
1889
+ pixs_galind_bounds_lens = np.concatenate(lpixs_galind_bounds).astype(np.int32)
1890
+ pix_gals_lens = np.asarray(np.concatenate(lpix_gals)).astype(np.int32)
1891
+ index_matcher_flat = np.argwhere(cat_source.index_matcher>-1).flatten().astype(np.int32)
1892
+ nregions = np.int32(len(index_matcher_flat))
1893
+ # Collect args
1894
+ args_resoinfo = (np.int32(self.tree_nresos), np.int32(self.tree_nresos-cutfirst),
1895
+ sdpixs1_true.astype(np.float64), sdpixs2_true.astype(np.float64), self.tree_redges, )
1896
+ args_leafs = (np.int32(self.resoshift_leafs), np.int32(self.minresoind_leaf),
1897
+ np.int32(self.maxresoind_leaf), )
1898
+ args_resos = (isinner_resos_source, weight_resos_source, pos1_resos_source, pos2_resos_source,
1899
+ e1_resos_source, e2_resos_source, zbin_resos_source, ngal_resos_source,
1900
+ np.int32(self.nbinsz_source), isinner_resos_lens, weight_resos_lens, pos1_resos_lens,
1901
+ pos2_resos_lens, zbin_resos_lens, ngal_resos_lens, np.int32(self.nbinsz_lens), )
1902
+ args_mhash = (index_matcher_source, pixs_galind_bounds_source, pix_gals_source,
1903
+ index_matcher_lens, pixs_galind_bounds_lens, pix_gals_lens, index_matcher_flat, nregions, )
1904
+ args_pixgrid = (np.float64(cat_lens.pix1_start), np.float64(cat_lens.pix1_d), np.int32(cat_lens.pix1_n),
1905
+ np.float64(cat_lens.pix2_start), np.float64(cat_lens.pix2_d), np.int32(cat_lens.pix2_n), )
1906
+ args = (*args_resoinfo,
1907
+ *args_leafs,
1908
+ *args_resos,
1909
+ *args_basesetup,
1910
+ *args_mhash,
1911
+ *args_pixgrid,
1912
+ np.int32(self.nthreads),
1913
+ np.int32(self._verbose_c),
1914
+ bin_centers,
1915
+ Upsilon_n,
1916
+ Norm_n, )
1917
+ func = self.clib.alloc_Gammans_doubletree_GNN
1918
+ if self._verbose_debug:
1919
+ for elarg, arg in enumerate(args):
1920
+ toprint = (elarg, type(arg),)
1921
+ if isinstance(arg, np.ndarray):
1922
+ toprint += (type(arg[0]), arg.shape)
1923
+ toprint += (func.argtypes[elarg], )
1924
+ print(toprint)
1925
+ print(arg)
1926
+
1927
+ func(*args)
1928
+
1929
+ self.bin_centers = bin_centers.reshape(szr)
1930
+ self.bin_centers_mean = np.mean(self.bin_centers, axis=(0,1))
1931
+ self.npcf_multipoles = np.nan_to_num(Upsilon_n.reshape(sc))
1932
+ self.npcf_multipoles_norm = np.nan_to_num(Norm_n.reshape(sn))
1933
+ self.projection = "X"
1934
+ self.is_edge_corrected = False
1935
+
1936
+ if apply_edge_correction:
1937
+ self.edge_correction()
1938
+
1939
+ if not dotomo_source:
1940
+ cat_source.zbins = old_zbins_source
1941
+ if not dotomo_lens:
1942
+ cat_lens.zbins = old_zbins_lens
1943
+
1944
+ def edge_correction(self, ret_matrices=False):
1945
+ assert(not self.is_edge_corrected)
1946
+ def gen_M_matrix(thet1,thet2,threepcf_n_norm):
1947
+ nvals, ntheta, _ = threepcf_n_norm.shape
1948
+ nmax = (nvals-1)//2
1949
+ narr = np.arange(-nmax,nmax+1, dtype=np.int)
1950
+ nextM = np.zeros((nvals,nvals))
1951
+ for ind, ell in enumerate(narr):
1952
+ lminusn = ell-narr
1953
+ sel = np.logical_and(lminusn+nmax>=0, lminusn+nmax<nvals)
1954
+ nextM[ind,sel] = threepcf_n_norm[(lminusn+nmax)[sel],thet1,thet2].real / threepcf_n_norm[nmax,thet1,thet2].real
1955
+ return nextM
1956
+
1957
+ nvals, nzcombis, ntheta, _ = self.npcf_multipoles_norm.shape
1958
+ nmax = nvals-1
1959
+ threepcf_n_full = np.zeros((1,2*nmax+1, nzcombis, ntheta, ntheta), dtype=complex)
1960
+ threepcf_n_norm_full = np.zeros((2*nmax+1, nzcombis, ntheta, ntheta), dtype=complex)
1961
+ threepcf_n_corr = np.zeros(threepcf_n_full.shape, dtype=np.complex)
1962
+ threepcf_n_full[:,nmax:] = self.npcf_multipoles
1963
+ threepcf_n_norm_full[nmax:] = self.npcf_multipoles_norm
1964
+ for nextn in range(1,nvals):
1965
+ threepcf_n_full[0,nmax-nextn] = self.npcf_multipoles[0,nextn].transpose(0,2,1)
1966
+ threepcf_n_norm_full[nmax-nextn] = self.npcf_multipoles_norm[nextn].transpose(0,2,1)
1967
+
1968
+ if ret_matrices:
1969
+ mats = np.zeros((nzcombis,ntheta,ntheta,nvals,nvals))
1970
+ for indz in range(nzcombis):
1971
+ #sys.stdout.write("%i"%indz)
1972
+ for thet1 in range(ntheta):
1973
+ for thet2 in range(ntheta):
1974
+ nextM = gen_M_matrix(thet1,thet2,threepcf_n_norm_full[:,indz])
1975
+ nextM_inv = np.linalg.inv(nextM)
1976
+ if ret_matrices:
1977
+ mats[indz,thet1,thet2] = nextM
1978
+ threepcf_n_corr[0,:,indz,thet1,thet2] = np.matmul(nextM_inv,threepcf_n_full[0,:,indz,thet1,thet2])
1979
+
1980
+ self.npcf_multipoles = threepcf_n_corr[:,nmax:]
1981
+ self.is_edge_corrected = True
1982
+
1983
+ if ret_matrices:
1984
+ return threepcf_n_corr[:,nmax:], mats
1985
+
1986
+ # TODO:
1987
+ # * Include the z-weighting method
1988
+ # * Include the 2pcf as spline --> Should we also add an option to compute it here? Might be a mess
1989
+ # as then we also would need methods to properly distribute randoms...
1990
+ # * Do a voronoi-tesselation at the multipole level? Would be just 2D, but still might help? Eventually
1991
+ # bundle together cells s.t. tot_weight > theshold? However, this might then make the binning courser
1992
+ # for certain triangle configs(?)
1993
+ def multipoles2npcf(self):
1994
+ r"""
1995
+ Notes
1996
+ -----
1997
+ * The Upsilon and Norms are only computed for the n>0 multipoles. The n<0 multipoles are recovered by symmetry considerations, i.e.:
1998
+
1999
+ .. math::
2000
+
2001
+ \Upsilon_{-n}(\theta_1, \theta_2, z_1, z_2, z_3) =
2002
+ \Upsilon_{n}(\theta_2, \theta_1, z_1, z_3, z_2)
2003
+
2004
+ As the tomographic bin combinations are interpreted as a flat list, they need to be appropriately shuffled. This is handled by ``ztiler``.
2005
+
2006
+ * When dividing by the (weighted) counts ``N``, all contributions for which ``N <= 0`` are set to zero.
2007
+
2008
+ """
2009
+ _, nzcombis, rbins, rbins = np.shape(self.npcf_multipoles[0])
2010
+ self.npcf = np.zeros((self.n_cfs, nzcombis, rbins, rbins, len(self.phi)), dtype=complex)
2011
+ self.npcf_norm = np.zeros((nzcombis, rbins, rbins, len(self.phi)), dtype=float)
2012
+ ztiler = np.arange(self.nbinsz_source*self.nbinsz_lens*self.nbinsz_lens).reshape(
2013
+ (self.nbinsz_source,self.nbinsz_lens,self.nbinsz_lens)).transpose(0,2,1).flatten().astype(np.int32)
2014
+
2015
+ # 3PCF components
2016
+ conjmap = [0]
2017
+ N0 = 1./(2*np.pi) * self.npcf_multipoles_norm[0].astype(complex)
2018
+ for elm in range(self.n_cfs):
2019
+ for elphi, phi in enumerate(self.phi):
2020
+ tmp = 1./(2*np.pi) * self.npcf_multipoles[elm,0].astype(complex)
2021
+ for n in range(1,self.nmax+1):
2022
+ _const = 1./(2*np.pi) * np.exp(1J*n*phi)
2023
+ tmp += _const * self.npcf_multipoles[elm,n].astype(complex)
2024
+ tmp += _const.conj() * self.npcf_multipoles[conjmap[elm],n][ztiler].astype(complex).transpose(0,2,1)
2025
+ self.npcf[elm,...,elphi] = tmp
2026
+ # Normalization
2027
+ for elphi, phi in enumerate(self.phi):
2028
+ tmptotnorm = 1./(2*np.pi) * self.npcf_multipoles_norm[0].astype(complex)
2029
+ for n in range(1,self.nmax+1):
2030
+ _const = 1./(2*np.pi) * np.exp(1J*n*phi)
2031
+ tmptotnorm += _const * self.npcf_multipoles_norm[n].astype(complex)
2032
+ tmptotnorm += _const.conj() * self.npcf_multipoles_norm[n][ztiler].astype(complex).transpose(0,2,1)
2033
+ self.npcf_norm[...,elphi] = tmptotnorm.real
2034
+
2035
+ if self.is_edge_corrected:
2036
+ sel_zero = np.isnan(N0)
2037
+ _a = self.npcf
2038
+ _b = N0.real[:, :, np.newaxis]
2039
+ self.npcf = np.divide(_a, _b, out=np.zeros_like(_a), where=np.abs(_b)>0)
2040
+ else:
2041
+ _a = self.npcf
2042
+ _b = self.npcf_norm
2043
+ self.npcf = np.divide(_a, _b, out=np.zeros_like(_a), where=np.abs(_b)>0)
2044
+ #self.npcf = self.npcf/self.npcf_norm[0][None, ...].astype(complex)
2045
+ self.projection = "X"
2046
+
2047
+
2048
+ ## PROJECTIONS ##
2049
+ def projectnpcf(self, projection):
2050
+ super()._projectnpcf(self, projection)
2051
+
2052
+ ## INTEGRATED MEASURES ##
2053
+ def computeNNM(self, radii, do_multiscale=False, tofile=False, filtercache=None):
2054
+ """
2055
+ Compute third-order aperture statistics using the polyonomial filter of Crittenden 2002.
2056
+ """
2057
+ nb_config.NUMBA_DEFAULT_NUM_THREADS = self.nthreads
2058
+ nb_config.NUMBA_NUM_THREADS = self.nthreads
2059
+
2060
+ if self.npcf is None and self.npcf_multipoles is not None:
2061
+ self.multipoles2npcf()
2062
+
2063
+ nradii = len(radii)
2064
+ if not do_multiscale:
2065
+ nrcombis = nradii
2066
+ _rcut = 1
2067
+ else:
2068
+ nrcombis = nradii*nradii*nradii
2069
+ _rcut = nradii
2070
+ NNM = np.zeros((1, self.nbinsz_source*self.nbinsz_lens*self.nbinsz_lens, nrcombis), dtype=complex)
2071
+ tmprcombi = 0
2072
+ for elr1, R1 in enumerate(radii):
2073
+ for elr2, R2 in enumerate(radii[:_rcut]):
2074
+ for elr3, R3 in enumerate(radii[:_rcut]):
2075
+ if not do_multiscale:
2076
+ R2 = R1
2077
+ R3 = R1
2078
+ if filtercache is not None:
2079
+ A_NNM = filtercache[tmprcombi]
2080
+ else:
2081
+ A_NNM = self._NNM_filtergrid(R1, R2, R3)
2082
+ NNM[0,:,tmprcombi] = np.nansum(A_NNM*self.npcf[0,...],axis=(1,2,3))
2083
+ tmprcombi += 1
2084
+ return NNM
2085
+
2086
+ def _NNM_filtergrid(self, R1, R2, R3):
2087
+ return self.__NNM_filtergrid(R1, R2, R3, self.bin_edges, self.bin_centers_mean, self.phi)
2088
+
2089
+ @staticmethod
2090
+ @jit(nopython=True, parallel=True)
2091
+ def __NNM_filtergrid(R1, R2, R3, edges, centers, phis):
2092
+ nbinsr = len(centers)
2093
+ nbinsphi = len(phis)
2094
+ _cphis = np.cos(phis)
2095
+ _ephis = np.e**(1J*phis)
2096
+ _ephisc = np.e**(-1J*phis)
2097
+ Theta4 = 1./3. * (R1**2*R2**2 + R1**2*R3**2 + R2**2*R3**2)
2098
+ a2 = 2./3. * R1**2*R2**2*R3**2 / Theta4
2099
+ ANNM = np.zeros((nbinsr,nbinsr,nbinsphi), dtype=nb_complex128)
2100
+ for elb in prange(nbinsr*nbinsr):
2101
+ elb1 = int(elb//nbinsr)
2102
+ elb2 = elb%nbinsr
2103
+ _y1 = centers[elb1]
2104
+ _dbin1 = edges[elb1+1] - edges[elb1]
2105
+ _y2 = centers[elb2]
2106
+ _dbin2 = edges[elb2+1] - edges[elb2]
2107
+ _dbinphi = phis[1] - phis[0]
2108
+ b0 = _y1**2/(2*R1**2)+_y2**2/(2*R2**2) - a2/4.*(
2109
+ _y1**2/R1**4 + 2*_y1*_y2*_cphis/(R1**2*R2**2) + _y2**2/R2**4)
2110
+ g1 = _y1 - a2/2. * (_y1/R1**2 + _y2*_ephisc/R2**2)
2111
+ g2 = _y2 - a2/2. * (_y2/R2**2 + _y1*_ephis/R1**2)
2112
+ g1c = g1.conj()
2113
+ g2c = g2.conj()
2114
+ F1 = 2*R1**2 - g1*g1c
2115
+ F2 = 2*R2**2 - g2*g2c
2116
+ pref = np.e**(-b0)/(72*np.pi*Theta4**2)
2117
+ sum1 = (g1-_y1)*(g2-_y2) * (1/a2*F1*F2 - (F1+F2) + 2*a2 + g1c*g2*_ephisc + g1*g2c*_ephis)
2118
+ sum2 = ((g2-_y2) + (g1-_y1)*_ephis) * (g1*(F2-2*a2) + g2*(F1-2*a2)*_ephisc)
2119
+ sum3 = 2*g1*g2*a2
2120
+ _measures = _y1*_dbin1 * _y2*_dbin2 * _dbinphi
2121
+ ANNM[elb1,elb2] = _measures * pref * (sum1-sum2+sum3)
2122
+
2123
+ return ANNM
2124
+
2125
+ # Very close to being a mere copy of GNN...
2126
+ class NGGCorrelation(BinnedNPCF):
2127
+ r""" Class containing methods to measure and and obtain statistics that are built
2128
+ from third-order lens-shear-shear correlation functions.
2129
+
2130
+ Attributes
2131
+ ----------
2132
+ min_sep: float
2133
+ The smallest distance of each vertex for which the NPCF is computed.
2134
+ max_sep: float
2135
+ The largest distance of each vertex for which the NPCF is computed.
2136
+
2137
+ Notes
2138
+ -----
2139
+ Inherits all other parameters and attributes from :class:`BinnedNPCF`.
2140
+ Additional child-specific parameters can be passed via ``kwargs``.
2141
+ Either ``nbinsr`` or ``binsize`` has to be provided to fix the binning scheme .
2142
+
2143
+ Note that the different components of the NGG correlator are ordered as
2144
+ .. math::
2145
+
2146
+ \left[ \tilde{G}_-, \tilde{G}_+, \right] \ ,
2147
+ which is different to the usual conventions, but matches orpheus' conventions to
2148
+ always start with a correlator in which not polar field is complex conjugated.
2149
+ """
2150
+ def __init__(self, min_sep, max_sep, **kwargs):
2151
+
2152
+ super().__init__(3, [0,2,2], n_cfs=2, min_sep=min_sep, max_sep=max_sep, **kwargs)
2153
+ self.nmax = self.nmaxs[0]
2154
+ self.phi = self.phis[0]
2155
+ self.projection = None
2156
+ self.projections_avail = [None, "X"]
2157
+ self.nbinsz_source = None
2158
+ self.nbinsz_lens = None
2159
+
2160
+ # (Add here any newly implemented projections)
2161
+ self._initprojections(self)
2162
+
2163
+ def __process_patches(self, cat_source, cat_lens, dotomo_source=True, dotomo_lens=True, rotsignflip=False,
2164
+ apply_edge_correction=False, save_patchres=False, save_filebase="", keep_patchres=False):
2165
+ if save_patchres:
2166
+ if not Path(save_patchres).is_dir():
2167
+ raise ValueError('Path to directory does not exist.')
2168
+
2169
+ for elp in range(cat_source.npatches):
2170
+ if self._verbose_python:
2171
+ print('Doing patch %i/%i'%(elp+1,cat_source.npatches))
2172
+ # Compute statistics on patch
2173
+ pscat = cat_source.frompatchind(elp,rotsignflip=rotsignflip)
2174
+ plcat = cat_lens.frompatchind(elp,rotsignflip=rotsignflip)
2175
+ pcorr = NGGCorrelation(
2176
+ min_sep=self.min_sep,
2177
+ max_sep=self.max_sep,
2178
+ nbinsr=self.nbinsr,
2179
+ nbinsphi=self.nbinsphi,
2180
+ nmaxs=self.nmaxs,
2181
+ method=self.method,
2182
+ multicountcorr=self.multicountcorr,
2183
+ shuffle_pix=self.shuffle_pix,
2184
+ tree_resos=self.tree_resos,
2185
+ rmin_pixsize=self.rmin_pixsize,
2186
+ resoshift_leafs=self.resoshift_leafs,
2187
+ minresoind_leaf=self.minresoind_leaf,
2188
+ maxresoind_leaf=self.maxresoind_leaf,
2189
+ nthreads=self.nthreads,
2190
+ verbosity=self.verbosity)
2191
+ pcorr.process(pscat, plcat, dotomo_source=dotomo_source, dotomo_lens=dotomo_lens)
2192
+
2193
+ # Update the total measurement
2194
+ if elp == 0:
2195
+ self.nbinsz_source = pcorr.nbinsz_source
2196
+ self.nbinsz_lens = pcorr.nbinsz_lens
2197
+ self.bin_centers = np.zeros_like(pcorr.bin_centers)
2198
+ self.npcf_multipoles = np.zeros_like(pcorr.npcf_multipoles)
2199
+ self.npcf_multipoles_norm = np.zeros_like(pcorr.npcf_multipoles_norm)
2200
+ _footnorm = np.zeros_like(pcorr.bin_centers)
2201
+ if keep_patchres:
2202
+ centers_patches = np.zeros((cat_source.npatches, *pcorr.bin_centers.shape), dtype=pcorr.bin_centers.dtype)
2203
+ npcf_multipoles_patches = np.zeros((cat_source.npatches, *pcorr.npcf_multipoles.shape), dtype=pcorr.npcf_multipoles.dtype)
2204
+ npcf_multipoles_norm_patches = np.zeros((cat_source.npatches, *pcorr.npcf_multipoles_norm.shape), dtype=pcorr.npcf_multipoles_norm.dtype)
2205
+ _shelltriplets = np.array([[[pcorr.npcf_multipoles_norm[pcorr.nmaxs[0],zl*self.nbinsz_source*self.nbinsz_source+zs*self.nbinsz_source+zs,i,i].real
2206
+ for i in range(pcorr.nbinsr)] for zs in range(self.nbinsz_source)] for zl in range(self.nbinsz_lens)])
2207
+ # Rough estimate of scaling of pair counts based on zeroth multipole of triplets. Note that we might get nans here due to numerical
2208
+ # inaccuracies in the multiple counting corrections for bins with zero triplets, so we force those values to be zero.
2209
+ _patchnorm = np.nan_to_num(np.sqrt(_shelltriplets))
2210
+ self.bin_centers += _patchnorm*pcorr.bin_centers
2211
+ _footnorm += _patchnorm
2212
+ self.npcf_multipoles += pcorr.npcf_multipoles
2213
+ self.npcf_multipoles_norm += pcorr.npcf_multipoles_norm
2214
+ if keep_patchres:
2215
+ centers_patches[elp] += pcorr.bin_centers
2216
+ npcf_multipoles_patches[elp] += pcorr.npcf_multipoles
2217
+ npcf_multipoles_norm_patches[elp] += pcorr.npcf_multipoles_norm
2218
+ if save_patchres:
2219
+ pcorr.saveinst(save_patchres, save_filebase+'_patch%i'%elp)
2220
+
2221
+ # Finalize the measurement on the full footprint
2222
+ self.bin_centers = np.divide(self.bin_centers,_footnorm, out=np.zeros_like(self.bin_centers), where=_footnorm>0)
2223
+ self.bin_centers_mean =np.mean(self.bin_centers, axis=(0,1))
2224
+ self.projection = "X"
2225
+
2226
+ if keep_patchres:
2227
+ return centers_patches, npcf_multipoles_patches, npcf_multipoles_norm_patches
2228
+
2229
+ def process(self, cat_source, cat_lens, dotomo_source=True, dotomo_lens=True, rotsignflip=False, apply_edge_correction=False,
2230
+ save_patchres=False, save_filebase="", keep_patchres=False):
2231
+ r"""
2232
+ Compute a lens-shear-shear correlation provided a source and a lens catalog.
2233
+
2234
+ Parameters
2235
+ ----------
2236
+ cat_source: orpheus.SpinTracerCatalog
2237
+ The source catalog which is processed
2238
+ cat_lens: orpheus.ScalarTracerCatalog
2239
+ The lens catalog which is processed
2240
+ dotomo_source: bool
2241
+ Flag that decides whether the tomographic information in the source catalog should be used. Defaults to `True`.
2242
+ dotomo_lens: bool
2243
+ Flag that decides whether the tomographic information in the lens catalog should be used. Defaults to `True`.
2244
+ rotsignflip: bool
2245
+ If the shape catalog is has been decomposed in patches, choose whether the rotation angle should be flipped.
2246
+ For simulated data this was always ok to set to 'False`. Defaults to `False`.
2247
+ apply_edge_correction: bool
2248
+ Flag that decides how the NPCF in the real space basis is computed.
2249
+ * If set to `True` the computation is done via edge-correcting the GNN-multipoles
2250
+ * If set to `False` both GNN and NNN are transformed separately and the ratio is done in the real-space basis
2251
+ Defaults to `False`.
2252
+ save_patchres: bool or str
2253
+ If the shape catalog is has been decomposed in patches, flag whether to save the GG measurements on the individual patches.
2254
+ Note that the path needs to exist, otherwise a `ValueError` is raised. For a flat-sky catalog this parameter
2255
+ has no effect. Defaults to `False`
2256
+ save_filebase: str
2257
+ Base of the filenames in which the patches are saved. The full filename will be `<save_patchres>/<save_filebase>_patchxx.npz`.
2258
+ Only has an effect if the shape catalog consists of multiple patches and `save_patchres` is not `False`.
2259
+ keep_patchres: bool
2260
+ If the catalog consists of multiple patches, returns all measurements on the patches. Defaults to `False`.
2261
+ """
2262
+
2263
+ self._checkcats([cat_lens, cat_source, cat_source], [0, 2, 2])
2264
+
2265
+ # Catch typical errors, i.e. incompatible catalogs or missin patch decompositions
2266
+ if cat_source.geometry=='spherical' and cat_source.patchinds is None:
2267
+ raise ValueError('Error: Spherical catalog needs to be first decomposed into patches using the Catalog._topatches method.')
2268
+ if cat_lens.geometry=='spherical' and cat_lens.patchinds is None:
2269
+ raise ValueError('Error: Spherical catalog needs to be first decomposed into patches using the Catalog._topatches method.')
2270
+ if cat_source.geometry != cat_lens.geometry:
2271
+ raise ValueError('Incompatible geometries of source catalog (%s) and lens catalog (%s).'%(
2272
+ cat_source.geometry,cat_lens.geometry))
2273
+
2274
+ # Catalog consist of multiple patches
2275
+ if (cat_source.patchinds is not None) and (cat_lens.patchinds is not None):
2276
+ return self.__process_patches(cat_source, cat_lens, dotomo_source=dotomo_source, dotomo_lens=dotomo_lens,
2277
+ rotsignflip=rotsignflip, apply_edge_correction=apply_edge_correction,
2278
+ save_patchres=save_patchres, save_filebase=save_filebase, keep_patchres=keep_patchres)
2279
+
2280
+ # Catalog does not consist of patches
2281
+ else:
2282
+ if not dotomo_source:
2283
+ self.nbinsz_source = 1
2284
+ old_zbins_source = cat_source.zbins[:]
2285
+ cat_source.zbins = np.zeros(cat_source.ngal, dtype=np.int32)
2286
+ else:
2287
+ self.nbinsz_source = cat_source.nbinsz
2288
+ if not dotomo_lens:
2289
+ self.nbinsz_lens = 1
2290
+ old_zbins_lens = cat_lens.zbins[:]
2291
+ cat_lens.zbins = np.zeros(cat_lens.ngal, dtype=np.int32)
2292
+ else:
2293
+ self.nbinsz_lens = cat_lens.nbinsz
2294
+
2295
+ _z3combis = self.nbinsz_lens*self.nbinsz_source*self.nbinsz_source
2296
+ _r2combis = self.nbinsr*self.nbinsr
2297
+ sc = (self.n_cfs, 2*self.nmax+1, _z3combis, self.nbinsr, self.nbinsr)
2298
+ sn = (2*self.nmax+1, _z3combis, self.nbinsr,self.nbinsr)
2299
+ szr = (self.nbinsz_lens, self.nbinsz_source, self.nbinsr)
2300
+ bin_centers = np.zeros(reduce(operator.mul, szr)).astype(np.float64)
2301
+ Upsilon_n = np.zeros(reduce(operator.mul, sc)).astype(np.complex128)
2302
+ Norm_n = np.zeros(reduce(operator.mul, sn)).astype(np.complex128)
2303
+ args_sourcecat = (cat_source.weight.astype(np.float64),
2304
+ cat_source.pos1.astype(np.float64), cat_source.pos2.astype(np.float64),
2305
+ cat_source.tracer_1.astype(np.float64), cat_source.tracer_2.astype(np.float64),
2306
+ cat_source.zbins.astype(np.int32), np.int32(self.nbinsz_source), np.int32(cat_source.ngal), )
2307
+ args_lenscat = (cat_lens.isinner.astype(np.float64), cat_lens.weight.astype(np.float64), cat_lens.pos1.astype(np.float64),
2308
+ cat_lens.pos2.astype(np.float64), cat_lens.zbins.astype(np.int32),
2309
+ np.int32(self.nbinsz_lens), np.int32(cat_lens.ngal), )
2310
+ args_basesetup = (np.int32(self.nmax), np.float64(self.min_sep), np.float64(self.max_sep),
2311
+ np.int32(self.nbinsr), np.int32(self.multicountcorr), )
2312
+ if self.method=="Discrete":
2313
+ hash_dpix = max(1.,self.max_sep//10.)
2314
+ jointextent = list(cat_source._jointextent([cat_lens], extend=self.tree_resos[-1]))
2315
+ cat_source.build_spatialhash(dpix=hash_dpix, extent=jointextent)
2316
+ cat_lens.build_spatialhash(dpix=hash_dpix, extent=jointextent)
2317
+ nregions = np.int32(len(np.argwhere(cat_lens.index_matcher>-1).flatten()))
2318
+ args_hash = (cat_source.index_matcher, cat_source.pixs_galind_bounds, cat_source.pix_gals,
2319
+ cat_lens.index_matcher, cat_lens.pixs_galind_bounds, cat_lens.pix_gals, nregions, )
2320
+ args_pixgrid = (np.float64(cat_lens.pix1_start), np.float64(cat_lens.pix1_d), np.int32(cat_lens.pix1_n),
2321
+ np.float64(cat_lens.pix2_start), np.float64(cat_lens.pix2_d), np.int32(cat_lens.pix2_n), )
2322
+ args = (*args_sourcecat,
2323
+ *args_lenscat,
2324
+ *args_basesetup,
2325
+ *args_hash,
2326
+ *args_pixgrid,
2327
+ np.int32(self.nthreads),
2328
+ np.int32(self._verbose_c),
2329
+ bin_centers,
2330
+ Upsilon_n,
2331
+ Norm_n, )
2332
+ func = self.clib.alloc_Gammans_discrete_NGG
2333
+ if self.method=="Tree" or self.method == "DoubleTree":
2334
+ cutfirst = np.int32(self.tree_resos[0]==0.)
2335
+ jointextent = list(cat_source._jointextent([cat_lens], extend=self.tree_resos[-1]))
2336
+ # Build multihashes for sources and lenses
2337
+ mhash_source = cat_source.multihash(dpixs=self.tree_resos[cutfirst:], dpix_hash=self.tree_resos[-1],
2338
+ shuffle=self.shuffle_pix, normed=True, extent=jointextent)
2339
+ sngal_resos, spos1s, spos2s, sweights, szbins, sisinners, sallfields, sindex_matchers, \
2340
+ spixs_galind_bounds, spix_gals, sdpixs1_true, sdpixs2_true = mhash_source
2341
+ ngal_resos_source = np.array(sngal_resos, dtype=np.int32)
2342
+ weight_resos_source = np.concatenate(sweights).astype(np.float64)
2343
+ pos1_resos_source = np.concatenate(spos1s).astype(np.float64)
2344
+ pos2_resos_source = np.concatenate(spos2s).astype(np.float64)
2345
+ zbin_resos_source = np.concatenate(szbins).astype(np.int32)
2346
+ isinner_resos_source = np.concatenate(sisinners).astype(np.float64)
2347
+ e1_resos_source = np.concatenate([sallfields[i][0] for i in range(len(sallfields))]).astype(np.float64)
2348
+ e2_resos_source = np.concatenate([sallfields[i][1] for i in range(len(sallfields))]).astype(np.float64)
2349
+ index_matcher_source = np.concatenate(sindex_matchers).astype(np.int32)
2350
+ pixs_galind_bounds_source = np.concatenate(spixs_galind_bounds).astype(np.int32)
2351
+ pix_gals_source = np.concatenate(spix_gals).astype(np.int32)
2352
+ mhash_lens = cat_lens.multihash(dpixs=self.tree_resos[cutfirst:], dpix_hash=self.tree_resos[-1],
2353
+ shuffle=self.shuffle_pix, normed=True, extent=jointextent)
2354
+ lngal_resos, lpos1s, lpos2s, lweights, lzbins, lisinners, lallfields, lindex_matchers, \
2355
+ lpixs_galind_bounds, lpix_gals, ldpixs1_true, ldpixs2_true = mhash_lens
2356
+ ngal_resos_lens = np.array(lngal_resos, dtype=np.int32)
2357
+ weight_resos_lens = np.concatenate(lweights).astype(np.float64)
2358
+ pos1_resos_lens = np.concatenate(lpos1s).astype(np.float64)
2359
+ pos2_resos_lens = np.concatenate(lpos2s).astype(np.float64)
2360
+ zbin_resos_lens = np.concatenate(lzbins).astype(np.int32)
2361
+ isinner_resos_lens = np.concatenate(lisinners).astype(np.float64)
2362
+ index_matcher_lens = np.concatenate(lindex_matchers).astype(np.int32)
2363
+ pixs_galind_bounds_lens = np.concatenate(lpixs_galind_bounds).astype(np.int32)
2364
+ pix_gals_lens = np.asarray(np.concatenate(lpix_gals)).astype(np.int32)
2365
+ index_matcher_flat = np.argwhere(cat_lens.index_matcher>-1).flatten().astype(np.int32)
2366
+ nregions = np.int32(len(index_matcher_flat))
2367
+ if self.method=="Tree":
2368
+ # Collect args
2369
+ args_resoinfo = (np.int32(self.tree_nresos), self.tree_redges,)
2370
+ args_resos_sourcecat = (weight_resos_source, pos1_resos_source, pos2_resos_source,
2371
+ e1_resos_source, e2_resos_source, zbin_resos_source,
2372
+ np.int32(self.nbinsz_source), ngal_resos_source, )
2373
+ args_mhash = (index_matcher_source, pixs_galind_bounds_source, pix_gals_source,
2374
+ index_matcher_lens, pixs_galind_bounds_lens, pix_gals_lens, nregions, )
2375
+ args_pixgrid = (np.float64(cat_lens.pix1_start), np.float64(cat_lens.pix1_d), np.int32(cat_lens.pix1_n),
2376
+ np.float64(cat_lens.pix2_start), np.float64(cat_lens.pix2_d), np.int32(cat_lens.pix2_n), )
2377
+ args = (*args_resoinfo,
2378
+ *args_resos_sourcecat,
2379
+ *args_lenscat,
2380
+ *args_mhash,
2381
+ *args_pixgrid,
2382
+ *args_basesetup,
2383
+ np.int32(self.nthreads),
2384
+ np.int32(self._verbose_c),
2385
+ bin_centers,
2386
+ Upsilon_n,
2387
+ Norm_n, )
2388
+ func = self.clib.alloc_Gammans_tree_NGG
2389
+ if self.method == "DoubleTree":
2390
+ # Collect args
2391
+ args_resoinfo = (np.int32(self.tree_nresos), np.int32(self.tree_nresos-cutfirst),
2392
+ sdpixs1_true.astype(np.float64), sdpixs2_true.astype(np.float64), self.tree_redges, )
2393
+ args_leafs = (np.int32(self.resoshift_leafs), np.int32(self.minresoind_leaf),
2394
+ np.int32(self.maxresoind_leaf), )
2395
+ args_resos = (isinner_resos_source, weight_resos_source, pos1_resos_source, pos2_resos_source,
2396
+ e1_resos_source, e2_resos_source, zbin_resos_source, ngal_resos_source,
2397
+ np.int32(self.nbinsz_source), isinner_resos_lens, weight_resos_lens, pos1_resos_lens,
2398
+ pos2_resos_lens, zbin_resos_lens, ngal_resos_lens, np.int32(self.nbinsz_lens), )
2399
+ args_mhash = (index_matcher_source, pixs_galind_bounds_source, pix_gals_source,
2400
+ index_matcher_lens, pixs_galind_bounds_lens, pix_gals_lens, index_matcher_flat, nregions, )
2401
+ args_pixgrid = (np.float64(cat_lens.pix1_start), np.float64(cat_lens.pix1_d), np.int32(cat_lens.pix1_n),
2402
+ np.float64(cat_lens.pix2_start), np.float64(cat_lens.pix2_d), np.int32(cat_lens.pix2_n), )
2403
+ args = (*args_resoinfo,
2404
+ *args_leafs,
2405
+ *args_resos,
2406
+ *args_basesetup,
2407
+ *args_mhash,
2408
+ *args_pixgrid,
2409
+ np.int32(self.nthreads),
2410
+ np.int32(self._verbose_c),
2411
+ bin_centers,
2412
+ Upsilon_n,
2413
+ Norm_n, )
2414
+ func = self.clib.alloc_Gammans_doubletree_NGG
2415
+ if self._verbose_debug:
2416
+ for elarg, arg in enumerate(args):
2417
+ toprint = (elarg, type(arg),)
2418
+ if isinstance(arg, np.ndarray):
2419
+ toprint += (type(arg[0]), arg.shape)
2420
+ try:
2421
+ toprint += (func.argtypes[elarg], )
2422
+ print(toprint)
2423
+ print(arg)
2424
+ except:
2425
+ print("We did have a problem for arg %i"%elarg)
2426
+
2427
+ func(*args)
2428
+
2429
+ # Components of npcf are ordered as (Ups_-, Ups_+)
2430
+ self.bin_centers = bin_centers.reshape(szr)
2431
+ self.bin_centers_mean = np.mean(self.bin_centers, axis=(0,1))
2432
+ self.npcf_multipoles = Upsilon_n.reshape(sc)
2433
+ self.npcf_multipoles_norm = Norm_n.reshape(sn)
2434
+ self.projection = "X"
2435
+ self.is_edge_corrected = False
2436
+
2437
+ if apply_edge_correction:
2438
+ self.edge_correction()
2439
+
2440
+ if not dotomo_source:
2441
+ cat_source.zbins = old_zbins_source
2442
+ if not dotomo_lens:
2443
+ cat_lens.zbins = old_zbins_lens
2444
+
2445
+ def edge_correction(self, ret_matrices=False):
2446
+
2447
+ assert(not self.is_edge_corrected)
2448
+ def gen_M_matrix(thet1,thet2,threepcf_n_norm):
2449
+ nvals, ntheta, _ = threepcf_n_norm.shape
2450
+ nmax = (nvals-1)//2
2451
+ narr = np.arange(-nmax,nmax+1, dtype=np.int)
2452
+ nextM = np.zeros((nvals,nvals))
2453
+ for ind, ell in enumerate(narr):
2454
+ lminusn = ell-narr
2455
+ sel = np.logical_and(lminusn+nmax>=0, lminusn+nmax<nvals)
2456
+ nextM[ind,sel] = threepcf_n_norm[(lminusn+nmax)[sel],thet1,thet2].real / threepcf_n_norm[nmax,thet1,thet2].real
2457
+ return nextM
2458
+
2459
+ _nvals, nzcombis, ntheta, _ = self.npcf_multipoles_norm.shape
2460
+ nvals = int((_nvals-1)/2)
2461
+ nmax = nvals-1
2462
+ threepcf_n_corr = np.zeros_like(self.npcf_multipoles)
2463
+ if ret_matrices:
2464
+ mats = np.zeros((nzcombis,ntheta,ntheta,nvals,nvals))
2465
+ for indz in range(nzcombis):
2466
+ #sys.stdout.write("%i"%indz)
2467
+ for thet1 in range(ntheta):
2468
+ for thet2 in range(ntheta):
2469
+ nextM = gen_M_matrix(thet1,thet2,self.npcf_multipoles_norm[:,indz])
2470
+ nextM_inv = np.linalg.inv(nextM)
2471
+ if ret_matrices:
2472
+ mats[indz,thet1,thet2] = nextM
2473
+ for el_cf in range(self.n_cfs):
2474
+ threepcf_n_corr[el_cf,:,indz,thet1,thet2] = np.matmul(
2475
+ nextM_inv,self.npcf_multipoles[el_cf,:,indz,thet1,thet2])
2476
+
2477
+ self.npcf_multipoles = threepcf_n_corr
2478
+ self.is_edge_corrected = True
2479
+
2480
+ if ret_matrices:
2481
+ return threepcf_n_corr, mats
2482
+
2483
+ def multipoles2npcf(self, integrated=False):
2484
+ r"""
2485
+ Notes
2486
+ -----
2487
+ * When dividing by the (weighted) counts ``N``, all contributions for which ``N <= 0`` are set to zero.
2488
+
2489
+ """
2490
+ _, nzcombis, rbins, rbins = np.shape(self.npcf_multipoles[0])
2491
+ self.npcf = np.zeros((self.n_cfs, nzcombis, rbins, rbins, len(self.phi)), dtype=complex)
2492
+ self.npcf_norm = np.zeros((nzcombis, rbins, rbins, len(self.phi)), dtype=float)
2493
+ ztiler = np.arange(self.nbinsz_lens*self.nbinsz_source*self.nbinsz_source).reshape(
2494
+ (self.nbinsz_lens,self.nbinsz_source,self.nbinsz_source)).transpose(0,2,1).flatten().astype(np.int32)
2495
+
2496
+ # NGG components
2497
+ for elphi, phi in enumerate(self.phi):
2498
+ tmp = np.zeros((self.n_cfs, nzcombis, rbins, rbins),dtype=complex)
2499
+ tmpnorm = np.zeros((nzcombis, rbins, rbins),dtype=complex)
2500
+ for n in range(2*self.nmax+1):
2501
+ dphi = self.phi[1] - self.phi[0]
2502
+ if integrated:
2503
+ if n==self.nmax:
2504
+ ifac = dphi
2505
+ else:
2506
+ ifac = 2./(n-self.nmax) * np.sin((n-self.nmax)*dphi/2.)
2507
+ else:
2508
+ ifac = dphi
2509
+ _const = 1./(2*np.pi) * np.exp(1J*(n-self.nmax)*phi) * ifac
2510
+ tmpnorm += _const * self.npcf_multipoles_norm[n].astype(complex)
2511
+ for el_cf in range(self.n_cfs):
2512
+ tmp[el_cf] += _const * self.npcf_multipoles[el_cf,n].astype(complex)
2513
+ self.npcf[...,elphi] = tmp
2514
+ self.npcf_norm[...,elphi] = tmpnorm.real
2515
+
2516
+ if self.is_edge_corrected:
2517
+ N0 = dphi/(2*np.pi) * self.npcf_multipoles_norm[self.nmax].astype(complex)
2518
+ sel_zero = np.isnan(N0)
2519
+ _a = self.npcf
2520
+ _b = N0.real[np.newaxis, :, :, :, np.newaxis]
2521
+ self.npcf = np.divide(_a, _b, out=np.zeros_like(_a), where=_b>0)
2522
+ else:
2523
+ _a = self.npcf
2524
+ _b = self.npcf_norm
2525
+ self.npcf = np.divide(_a, _b, out=np.zeros_like(_a), where=_b>0)
2526
+ #self.npcf = self.npcf/self.npcf_norm[0][None, ...].astype(complex)
2527
+ self.projection = "X"
2528
+
2529
+ ## PROJECTIONS ##
2530
+ def projectnpcf(self, projection):
2531
+ super()._projectnpcf(self, projection)
2532
+
2533
+ ## INTEGRATED MEASURES ##
2534
+ def computeNMM(self, radii, do_multiscale=False, tofile=False, filtercache=None):
2535
+ """
2536
+ Compute third-order aperture statistics
2537
+ """
2538
+
2539
+ nb_config.NUMBA_DEFAULT_NUM_THREADS = self.nthreads
2540
+ nb_config.NUMBA_NUM_THREADS = self.nthreads
2541
+
2542
+ if self.npcf is None and self.npcf_multipoles is not None:
2543
+ self.multipoles2npcf()
2544
+
2545
+ nradii = len(radii)
2546
+ if not do_multiscale:
2547
+ nrcombis = nradii
2548
+ _rcut = 1
2549
+ else:
2550
+ nrcombis = nradii*nradii*nradii
2551
+ _rcut = nradii
2552
+ NMM = np.zeros((3, self.nbinsz_lens*self.nbinsz_source*self.nbinsz_source, nrcombis), dtype=complex)
2553
+ tmprcombi = 0
2554
+ for elr1, R1 in enumerate(radii):
2555
+ for elr2, R2 in enumerate(radii[:_rcut]):
2556
+ for elr3, R3 in enumerate(radii[:_rcut]):
2557
+ if not do_multiscale:
2558
+ R2 = R1
2559
+ R3 = R1
2560
+ if filtercache is not None:
2561
+ A_NMM = filtercache[tmprcombi]
2562
+ else:
2563
+ A_NMM = self._NMM_filtergrid(R1, R2, R3)
2564
+ _NMM = np.nansum(A_NMM[0]*self.npcf[0,...],axis=(1,2,3))
2565
+ _NMMstar = np.nansum(A_NMM[1]*self.npcf[1,...],axis=(1,2,3))
2566
+ NMM[0,:,tmprcombi] = (_NMM + _NMMstar).real/2.
2567
+ NMM[1,:,tmprcombi] = (-_NMM + _NMMstar).real/2.
2568
+ NMM[2,:,tmprcombi] = (_NMM + _NMMstar).imag/2.
2569
+ tmprcombi += 1
2570
+ return NMM
2571
+
2572
+ def _NMM_filtergrid(self, R1, R2, R3):
2573
+ return self.__NMM_filtergrid(R1, R2, R3, self.bin_edges, self.bin_centers_mean, self.phi)
2574
+
2575
+ @staticmethod
2576
+ @jit(nopython=True, parallel=True)
2577
+ def __NMM_filtergrid(R1, R2, R3, edges, centers, phis):
2578
+ nbinsr = len(centers)
2579
+ nbinsphi = len(phis)
2580
+ _cphis = np.cos(phis)
2581
+ _ephis = np.e**(1J*phis)
2582
+ _ephisc = np.e**(-1J*phis)
2583
+ Theta4 = 1./3. * (R1**2*R2**2 + R1**2*R3**2 + R2**2*R3**2)
2584
+ a2 = 2./3. * R1**2*R2**2*R3**2 / Theta4
2585
+ ANMM = np.zeros((2,nbinsr,nbinsr,nbinsphi), dtype=nb_complex128)
2586
+ for elb in prange(nbinsr*nbinsr):
2587
+ elb1 = int(elb//nbinsr)
2588
+ elb2 = elb%nbinsr
2589
+ _y1 = centers[elb1]
2590
+ _dbin1 = edges[elb1+1] - edges[elb1]
2591
+ _y2 = centers[elb2]
2592
+ _dbin2 = edges[elb2+1] - edges[elb2]
2593
+ _dbinphi = phis[1] - phis[0]
2594
+
2595
+ csq = a2**2/4. * (_y1**2/R1**4 + _y2**2/R2**4 + 2*_y1*_y2*_cphis/(R1**2*R2**2))
2596
+ b0 = _y1**2/(2*R1**2)+_y2**2/(2*R2**2) - csq/a2
2597
+
2598
+ g1 = _y1 - a2/2. * (_y1/R1**2 + _y2*_ephisc/R2**2)
2599
+ g2 = _y2 - a2/2. * (_y2/R2**2 + _y1*_ephis/R1**2)
2600
+ g1c = g1.conj()
2601
+ g2c = g2.conj()
2602
+ pref = np.e**(-b0)/(72*np.pi*Theta4**2)
2603
+ _h1 = 2*(g2c*_y1+g1*_y2-2*g1*g2c)*(g1*g2c+2*a2*_ephisc)
2604
+ _h2 = 2*a2*(2*R3**2-csq-3*a2)*_ephisc*_ephisc
2605
+ _h3 = 4*g1*g2c*(2*R3**2-csq-2*a2)*_ephisc
2606
+ _h4 = (g1*g2c)**2/a2 * (2*R3**2-csq-a2)
2607
+ sum_MMN = pref*g1*g2 * ((R3**2/R1**2+R3**2/R2**2-csq/a2)*g1*g2 + 2*(g2*_y1+g1*_y2-2*g1*g2))
2608
+ sum_MMstarN = pref * (_h1 + _h2 + _h3 + _h4)
2609
+ _measures = _y1*_dbin1 * _y2*_dbin2 * _dbinphi
2610
+
2611
+ ANMM[0,elb1,elb2] = _measures * sum_MMN
2612
+ ANMM[1,elb1,elb2] = _measures * sum_MMstarN
2613
+
2614
+ return ANMM
2615
+
2616
+ #############################
2617
+ ## FOURTH-ORDER STATISTICS ##
2618
+ #############################
2619
+ class GGGGCorrelation_NoTomo(BinnedNPCF):
2620
+ r""" Class containing methods to measure and and obtain statistics that are built
2621
+ from nontomographic fourth-order shear correlation functions.
2622
+
2623
+ Attributes
2624
+ ----------
2625
+ min_sep: float
2626
+ The smallest distance of each vertex for which the NPCF is computed.
2627
+ max_sep: float
2628
+ The largest distance of each vertex for which the NPCF is computed.
2629
+ thetabatchsize_max: int, optional
2630
+ The largest number of radial bin combinations that are processed in parallel.
2631
+ Defaults to ``10 000``.
2632
+
2633
+ Notes
2634
+ -----
2635
+ Inherits all other parameters and attributes from :class:`BinnedNPCF`.
2636
+ Additional child-specific parameters can be passed via ``kwargs``.
2637
+ Either ``nbinsr`` or ``binsize`` has to be provided to fix the binning scheme .
2638
+
2639
+ """
2640
+
2641
+ def __init__(self, min_sep, max_sep, thetabatchsize_max=10000, method="Tree", **kwargs):
2642
+
2643
+ super().__init__(order=4, spins=np.array([2,2,2,2], dtype=np.int32),
2644
+ n_cfs=8, min_sep=min_sep, max_sep=max_sep,
2645
+ method=method, methods_avail=["Discrete", "Tree"], **kwargs)
2646
+
2647
+ self.thetabatchsize_max = thetabatchsize_max
2648
+ self.projection = None
2649
+ self.projections_avail = [None, "X", "Centroid"]
2650
+ self.proj_dict = {"X":0, "Centroid":1}
2651
+ self.nbinsz = 1
2652
+ self.nzcombis = 1
2653
+
2654
+ # (Add here any newly implemented projections)
2655
+ self._initprojections(self)
2656
+ self.project["X"]["Centroid"] = self._x2centroid
2657
+
2658
+ def process(self, cat, statistics="all", tofile=False, apply_edge_correction=False, projection="X",
2659
+ lowmem=None, mapradii=None, batchsize=None, custom_thetacombis=None, cutlen=2**31-1):
2660
+ r"""
2661
+ Arguments:
2662
+
2663
+ Logic works as follows:
2664
+ * Keyword 'statistics' \in [4pcf_real, 4pcf_multipoles, M4, Map4, M4c, Map4c, allMap, all4pcf, all]
2665
+ * - If 4pcf_multipoles in statistics --> save 4pcf_multipoles
2666
+ * - If 4pcf_real in statistics --> save 4pcf_real
2667
+ * - If only M4 in statistics --> Do not save any 4pcf. This is really the lowmem case.
2668
+ * - allMap, all4pcf, all are abbreviations as expected
2669
+ * If lowmem=True, uses the inefficient, but lowmem function for computation and output statistics
2670
+ from there as wanted.
2671
+ * If lowmem=False, use the fast functions to do the 4pcf multipole computation and do
2672
+ the potential conversions lateron.
2673
+ * Default lowmem to None and
2674
+ * - Set to true if any aperture statistics is in stats or we will run into mem error
2675
+ * - Set to false otherwise
2676
+ * - Raise error if lowmen=False and we will have more that 2^31-1 elements at any stage of the computation
2677
+
2678
+ custom_thetacombis: array of inds which theta combis will be selected
2679
+ """
2680
+
2681
+ ## Preparations ##
2682
+ # Build list of statistics to be calculated
2683
+ statistics_avail_4pcf = ["4pcf_real", "4pcf_multipole"]
2684
+ statistics_avail_map4 = ["M4", "Map4", "M4c", "Map4c"]
2685
+ statistics_avail_comp = ["allMap", "all4pcf", "all"]
2686
+ statistics_avail_phys = statistics_avail_4pcf + statistics_avail_map4
2687
+ statistics_avail = statistics_avail_4pcf + statistics_avail_map4 + statistics_avail_comp
2688
+ _statistics = []
2689
+ hasintegratedstats = False
2690
+ _strbadstats = lambda stat: ("The statistics `%s` has not been implemented yet. "%stat +
2691
+ "Currently supported statistics are:\n" + str(statistics_avail))
2692
+ if type(statistics) not in [list, str]:
2693
+ raise ValueError("The parameter `statistics` should either be a list or a string.")
2694
+ if type(statistics) is str:
2695
+ if statistics not in statistics_avail:
2696
+ raise ValueError(_strbadstats)
2697
+ statistics = [statistics]
2698
+ if type(statistics) is list:
2699
+ if "all" in statistics:
2700
+ _statistics = statistics_avail_phys
2701
+ elif "all4pcf" in statistics:
2702
+ _statistics.append(statistics_avail_4pcf)
2703
+ elif "allMap" in statistics:
2704
+ _statistics.append(statistics_avail_map4)
2705
+ _statistics = flatlist(_statistics)
2706
+ for stat in statistics:
2707
+ if stat not in statistics_avail:
2708
+ raise ValueError(_strbadstats)
2709
+ if stat in statistics_avail_phys and stat not in _statistics:
2710
+ _statistics.append(stat)
2711
+ statistics = list(set(flatlist(_statistics)))
2712
+ for stat in statistics:
2713
+ if stat in statistics_avail_map4:
2714
+ hasintegratedstats = True
2715
+
2716
+ # Check if the output will fit in memory
2717
+ if "4pcf_multipole" in statistics:
2718
+ _nvals = 8*self.nzcombis*(2*self.nmaxs[0]+1)*(2*self.nmaxs[1]+1)*self.nbinsr**3
2719
+ if _nvals>cutlen:
2720
+ raise ValueError(("4pcf in multipole basis will cause memory overflow " +
2721
+ "(requiring %.2fx10^9 > %.2fx10^9 elements)\n"%(_nvals/1e9, cutlen/1e9) +
2722
+ "If you are solely interested in integrated statistics (like Map4), you" +
2723
+ "only need to add those to the `statistics` argument."))
2724
+ if "4pcf_real" in statistics:
2725
+ _nvals = 8*self.nzcombis*self.nbinsphi[0]*self.nbinsphi[1]*self.nbinsr**3
2726
+ if _nvals>cutlen:
2727
+ raise ValueError(("4pcf in real basis will cause memory overflow " +
2728
+ "(requiring %.2fx10^9 > %.2fx10^9 elements)\n"%(_nvals/1e9, cutlen/1e9) +
2729
+ "If you are solely interested in integrated statistics (like Map4), you" +
2730
+ "only need to add those to the `statistics` argument."))
2731
+
2732
+ # Decide on whether to use low-mem functions or not
2733
+ if hasintegratedstats:
2734
+ if lowmem in [False, None]:
2735
+ if not lowmem:
2736
+ print("Low-memory computation enforced for integrated measures of the 4pcf. " +
2737
+ "Set `lowmem` from `%s` to `True`"%str(lowmem))
2738
+ lowmem = True
2739
+ else:
2740
+ if lowmem in [None, False]:
2741
+ maxlen = 0
2742
+ _lowmem = False
2743
+ if "4pcf_multipole" in statistics:
2744
+ _nvals = 8*self.nzcombis*(2*self.nmaxs[0]+1)*(2*self.nmaxs[1]+1)*self.nbinsr**3
2745
+ if _nvals > cutlen:
2746
+ if not lowmem:
2747
+ print("Switching to low-memory computation of 4pcf in multipole basis.")
2748
+ lowmem = True
2749
+ else:
2750
+ lowmem = False
2751
+ if "4pcf_real" in statistics:
2752
+ nvals = 8*self.nzcombis*self.nbinsphi[0]*self.nbinsphi[1]*self.nbinsr**3
2753
+ if _nvals > cutlen:
2754
+ if not lowmem:
2755
+ print("Switching to low-memory computation of 4pcf in real basis.")
2756
+ lowmem = True
2757
+ else:
2758
+ lowmem = False
2759
+
2760
+ # Misc checks
2761
+ assert(projection in self.projections_avail)
2762
+ self._checkcats(cat, self.spins)
2763
+ i_projection = np.int32(self.proj_dict[projection])
2764
+
2765
+ ## Build args for wrapped functions ##
2766
+ # Shortcuts
2767
+ _nmax = self.nmaxs[0]
2768
+ _nnvals = (2*_nmax+1)*(2*_nmax+1)
2769
+ _nbinsr3 = self.nbinsr*self.nbinsr*self.nbinsr
2770
+ _nphis = len(self.phis[0])
2771
+ sc = (8,2*_nmax+1,2*_nmax+1,self.nzcombis,self.nbinsr,self.nbinsr,self.nbinsr)
2772
+ sn = (2*_nmax+1,2*_nmax+1,self.nzcombis,self.nbinsr,self.nbinsr,self.nbinsr)
2773
+ szr = (self.nbinsz, self.nbinsr)
2774
+ s4pcf = (8,self.nzcombis,self.nbinsr,self.nbinsr,self.nbinsr,_nphis,_nphis)
2775
+ s4pcfn = (self.nzcombis,self.nbinsr,self.nbinsr,self.nbinsr,_nphis,_nphis)
2776
+ # Init default args
2777
+ bin_centers = np.zeros(self.nbinsz*self.nbinsr).astype(np.float64)
2778
+ if not cat.hasspatialhash:
2779
+ cat.build_spatialhash(dpix=max(1.,self.max_sep//10.))
2780
+ nregions = np.int32(len(np.argwhere(cat.index_matcher>-1).flatten()))
2781
+ args_basecat = (cat.isinner.astype(np.float64), cat.weight, cat.pos1, cat.pos2,
2782
+ cat.tracer_1, cat.tracer_2, np.int32(cat.ngal), )
2783
+ args_hash = (cat.index_matcher, cat.pixs_galind_bounds, cat.pix_gals, nregions,
2784
+ np.float64(cat.pix1_start), np.float64(cat.pix1_d), np.int32(cat.pix1_n),
2785
+ np.float64(cat.pix2_start), np.float64(cat.pix2_d), np.int32(cat.pix2_n), )
2786
+
2787
+ # Init optional args
2788
+ __lenflag = 10
2789
+ __fillflag = -1
2790
+ if "4pcf_multipole" in statistics:
2791
+ Upsilon_n = np.zeros(self.n_cfs*_nnvals*self.nzcombis*_nbinsr3).astype(np.complex128)
2792
+ N_n = np.zeros(_nnvals*self.nzcombis*_nbinsr3).astype(np.complex128)
2793
+ alloc_4pcfmultipoles = 1
2794
+ else:
2795
+ Upsilon_n = __fillflag*np.ones(__lenflag).astype(np.complex128)
2796
+ N_n = __fillflag*np.zeros(__lenflag).astype(np.complex128)
2797
+ alloc_4pcfmultipoles = 0
2798
+ if "4pcf_real" in statistics:
2799
+ fourpcf = np.zeros(8*_nphis*_nphis*self.nzcombis*_nbinsr3).astype(np.complex128)
2800
+ fourpcf_norm = np.zeros(_nphis*_nphis*self.nzcombis*_nbinsr3).astype(np.complex128)
2801
+ alloc_4pcfreal = 1
2802
+ else:
2803
+ fourpcf = __fillflag*np.ones(__lenflag).astype(np.complex128)
2804
+ fourpcf_norm = __fillflag*np.ones(__lenflag).astype(np.complex128)
2805
+ alloc_4pcfreal = 0
2806
+ if hasintegratedstats:
2807
+ if mapradii is None:
2808
+ raise ValueError("Aperture radii need to be specified in variable `mapradii`.")
2809
+ mapradii = mapradii.astype(np.float64)
2810
+ M4correlators = np.zeros(8*self.nzcombis*len(mapradii)).astype(np.complex128)
2811
+ else:
2812
+ mapradii = __fillflag*np.ones(__lenflag).astype(np.float64)
2813
+ N4correlators = __fillflag*np.ones(__lenflag).astype(np.complex128)
2814
+
2815
+ # Build args based on chosen methods
2816
+ if self.method=="Discrete" and not lowmem:
2817
+ args_basesetup = (np.int32(_nmax), np.float64(self.min_sep),
2818
+ np.float64(self.max_sep), np.array([-1.]).astype(np.float64),
2819
+ np.int32(self.nbinsr), np.int32(self.multicountcorr), )
2820
+ args = (*args_basecat,
2821
+ *args_basesetup,
2822
+ *args_hash,
2823
+ np.int32(self.nthreads),
2824
+ np.int32(self._verbose_c+self._verbose_debug),
2825
+ bin_centers,
2826
+ Upsilon_n,
2827
+ N_n)
2828
+ func = self.clib.alloc_notomoGammans_discrete_gggg
2829
+ if self.method=="Discrete" and lowmem:
2830
+ _resradial = gen_thetacombis_fourthorder(nbinsr=self.nbinsr, nthreads=self.nthreads, batchsize=batchsize,
2831
+ batchsize_max=self.thetabatchsize_max, ordered=True, custom=custom_thetacombis,
2832
+ verbose=self._verbose_python)
2833
+ _, _, thetacombis_batches, cumnthetacombis_batches, nthetacombis_batches, nbatches = _resradial
2834
+
2835
+ args_basesetup = (np.int32(_nmax),
2836
+ np.float64(self.min_sep), np.float64(self.max_sep), np.int32(self.nbinsr),
2837
+ np.int32(self.multicountcorr),
2838
+ self.phis[0].astype(np.float64),
2839
+ 2*np.pi/_nphis*np.ones(_nphis, dtype=np.float64), np.int32(_nphis))
2840
+ args_4pcf = (np.int32(alloc_4pcfmultipoles), np.int32(alloc_4pcfreal),
2841
+ bin_centers, Upsilon_n, N_n, fourpcf, fourpcf_norm, )
2842
+ args_thetas = (thetacombis_batches, nthetacombis_batches, cumnthetacombis_batches, nbatches, )
2843
+ args_map4 = (mapradii, np.int32(len(mapradii)), M4correlators)
2844
+ args = (*args_basecat,
2845
+ *args_basesetup,
2846
+ *args_hash,
2847
+ *args_thetas,
2848
+ np.int32(self.nthreads),
2849
+ np.int32(self._verbose_c+self._verbose_debug),
2850
+ i_projection,
2851
+ *args_map4,
2852
+ *args_4pcf)
2853
+ func = self.clib.alloc_notomoMap4_disc_gggg
2854
+ if self.method=="Tree":
2855
+ # Prepare mask for nonredundant theta- and multipole configurations
2856
+ _resradial = gen_thetacombis_fourthorder(nbinsr=self.nbinsr, nthreads=self.nthreads, batchsize=batchsize,
2857
+ batchsize_max=self.thetabatchsize_max, ordered=True, custom=custom_thetacombis,
2858
+ verbose=self._verbose_python*lowmem)
2859
+ _, _, thetacombis_batches, cumnthetacombis_batches, nthetacombis_batches, nbatches = _resradial
2860
+ assert(self.nmaxs[0]==self.nmaxs[1])
2861
+ _resmultipoles = gen_n2n3indices_Upsfourth(self.nmaxs[0])
2862
+ _shape, _inds, _n2s, _n3s = _resmultipoles
2863
+
2864
+ # Prepare reduced catalogs
2865
+ cutfirst = np.int32(self.tree_resos[0]==0.)
2866
+ mhash = cat.multihash(dpixs=self.tree_resos[cutfirst:], dpix_hash=self.tree_resos[-1],
2867
+ shuffle=self.shuffle_pix, w2field=True, normed=True)
2868
+ (ngal_resos, pos1s, pos2s, weights, zbins, isinners, allfields,
2869
+ index_matchers, pixs_galind_bounds, pix_gals, dpixs1_true, dpixs2_true) = mhash
2870
+ weight_resos = np.concatenate(weights).astype(np.float64)
2871
+ pos1_resos = np.concatenate(pos1s).astype(np.float64)
2872
+ pos2_resos = np.concatenate(pos2s).astype(np.float64)
2873
+ zbin_resos = np.concatenate(zbins).astype(np.int32)
2874
+ isinner_resos = np.concatenate(isinners).astype(np.float64)
2875
+ e1_resos = np.concatenate([allfields[i][0] for i in range(len(allfields))]).astype(np.float64)
2876
+ e2_resos = np.concatenate([allfields[i][1] for i in range(len(allfields))]).astype(np.float64)
2877
+ index_matcher_resos = np.concatenate(index_matchers).astype(np.int32)
2878
+ pixs_galind_bounds_resos = np.concatenate(pixs_galind_bounds).astype(np.int32)
2879
+ pix_gals_resos = np.concatenate(pix_gals).astype(np.int32)
2880
+ index_matcher_flat = np.argwhere(cat.index_matcher>-1).flatten()
2881
+ nregions = len(index_matcher_flat)
2882
+ if not lowmem:
2883
+ args_basesetup = (np.int32(_nmax),
2884
+ np.float64(self.min_sep), np.float64(self.max_sep), np.int32(self.nbinsr),
2885
+ np.int32(cumnthetacombis_batches[-1]), np.int32(self.multicountcorr),
2886
+ _inds, np.int32(len(_inds)),)
2887
+ args_resos = (np.int32(self.tree_nresos), self.tree_redges, np.array(ngal_resos, dtype=np.int32),
2888
+ isinner_resos, weight_resos, pos1_resos, pos2_resos, e1_resos, e2_resos,
2889
+ index_matcher_resos, pixs_galind_bounds_resos, pix_gals_resos, np.int32(nregions), )
2890
+ args_hash = (np.float64(cat.pix1_start), np.float64(cat.pix1_d), np.int32(cat.pix1_n),
2891
+ np.float64(cat.pix2_start), np.float64(cat.pix2_d), np.int32(cat.pix2_n), )
2892
+ args_out = ( bin_centers, Upsilon_n, N_n, )
2893
+ args = (*args_basecat,
2894
+ *args_basesetup,
2895
+ *args_resos,
2896
+ *args_hash,
2897
+ np.int32(self.nthreads),
2898
+ np.int32(self._verbose_c+self._verbose_debug),
2899
+ *args_out)
2900
+ func = self.clib.alloc_notomoGammans_tree_gggg
2901
+ if lowmem:
2902
+ # Build args
2903
+ args_basesetup = (np.int32(_nmax),
2904
+ np.float64(self.min_sep), np.float64(self.max_sep), np.int32(self.nbinsr),
2905
+ np.int32(self.multicountcorr),
2906
+ _inds, np.int32(len(_inds)), self.phis[0].astype(np.float64),
2907
+ 2*np.pi/_nphis*np.ones(_nphis, dtype=np.float64), np.int32(_nphis), )
2908
+ args_resos = (np.int32(self.tree_nresos), self.tree_redges, np.array(ngal_resos, dtype=np.int32),
2909
+ isinner_resos, weight_resos, pos1_resos, pos2_resos, e1_resos, e2_resos,
2910
+ index_matcher_resos, pixs_galind_bounds_resos, pix_gals_resos, np.int32(nregions), )
2911
+ args_hash = (np.float64(cat.pix1_start), np.float64(cat.pix1_d), np.int32(cat.pix1_n),
2912
+ np.float64(cat.pix2_start), np.float64(cat.pix2_d), np.int32(cat.pix2_n), )
2913
+ args_thetas = (thetacombis_batches, nthetacombis_batches, cumnthetacombis_batches, nbatches, )
2914
+ args_map4 = (mapradii, np.int32(len(mapradii)), M4correlators)
2915
+ args_4pcf = (np.int32(alloc_4pcfmultipoles), np.int32(alloc_4pcfreal),
2916
+ bin_centers, Upsilon_n, N_n, fourpcf, fourpcf_norm, )
2917
+ args = (*args_basecat,
2918
+ *args_basesetup,
2919
+ *args_resos,
2920
+ *args_hash,
2921
+ *args_thetas,
2922
+ np.int32(self.nthreads),
2923
+ np.int32(self._verbose_c+self._verbose_debug),
2924
+ i_projection,
2925
+ *args_map4,
2926
+ *args_4pcf)
2927
+ func = self.clib.alloc_notomoMap4_tree_gggg
2928
+
2929
+ # Optionally print the arguments
2930
+ if self._verbose_debug:
2931
+ print("We pass the following arguments:")
2932
+ for elarg, arg in enumerate(args):
2933
+ toprint = (elarg, type(arg),)
2934
+ if isinstance(arg, np.ndarray):
2935
+ toprint += (type(arg[0]), arg.shape)
2936
+ try:
2937
+ toprint += (func.argtypes[elarg], )
2938
+ print(toprint)
2939
+ print(arg)
2940
+ except:
2941
+ print("We did have a problem for arg %i"%elarg)
2942
+
2943
+ ## Compute 4th order stats ##
2944
+ func(*args)
2945
+ self.projection = projection
2946
+
2947
+ ## Massage the output ##
2948
+ istatout = ()
2949
+ self.bin_centers = bin_centers.reshape(szr)
2950
+ self.bin_centers_mean = np.mean(self.bin_centers, axis=0)
2951
+ if "4pcf_multipole" in statistics:
2952
+ self.npcf_multipoles = Upsilon_n.reshape(sc)
2953
+ self.npcf_multipoles_norm = N_n.reshape(sn)
2954
+ if "4pcf_real" in statistics:
2955
+ if lowmem:
2956
+ self.npcf = fourpcf.reshape(s4pcf)
2957
+ self.npcf_norm = fourpcf_norm.reshape(s4pcfn)
2958
+ else:
2959
+ if self._verbose_python:
2960
+ print("Transforming output to real space basis")
2961
+ self.multipoles2npcf_c(projection=projection)
2962
+ if hasintegratedstats:
2963
+ if "M4" in statistics:
2964
+ istatout += (M4correlators.reshape((8,self.nzcombis,len(mapradii))), )
2965
+ # TODO allocate map4, map4c etc.
2966
+
2967
+ return istatout
2968
+
2969
+ def multipoles2npcf_c(self, projection="X"):
2970
+ r""" Converts a 4PCF in the multipole basis in the real space basis.
2971
+ """
2972
+ assert((projection in self.proj_dict.keys()) and (projection in self.projections_avail))
2973
+
2974
+ _nzero1 = self.nmaxs[0]
2975
+ _nzero2 = self.nmaxs[1]
2976
+ _phis1 = self.phis[0].astype(np.float64)
2977
+ _phis2 = self.phis[1].astype(np.float64)
2978
+ _nphis1 = len(self.phis[0])
2979
+ _nphis2 = len(self.phis[1])
2980
+ ncfs, nnvals, _, nzcombis, nbinsr, _, _ = np.shape(self.npcf_multipoles)
2981
+
2982
+ shape_npcf = (self.n_cfs, nzcombis, nbinsr, nbinsr, nbinsr, _nphis1, _nphis2)
2983
+ shape_npcf_norm = (nzcombis, nbinsr, nbinsr, nbinsr, _nphis1, _nphis2)
2984
+ self.npcf = np.zeros(self.n_cfs*nzcombis*nbinsr*nbinsr*nbinsr*_nphis1*_nphis2, dtype=np.complex128)
2985
+ self.npcf_norm = np.zeros(nzcombis*nbinsr*nbinsr*nbinsr*_nphis1*_nphis2, dtype=np.complex128)
2986
+ self.clib.multipoles2npcf_gggg(self.npcf_multipoles.flatten(), self.npcf_multipoles_norm.flatten(),
2987
+ self.bin_centers_mean.astype(np.float64), np.int32(self.proj_dict[projection]),
2988
+ 8, nbinsr, self.nmaxs[0].astype(np.int32), _phis1, _nphis1, _phis2, _nphis2,
2989
+ self.nthreads, self.npcf, self.npcf_norm)
2990
+ self.npcf = self.npcf.reshape(shape_npcf)
2991
+ self.npcf_norm = self.npcf_norm.reshape(shape_npcf_norm)
2992
+ self.projection = projection
2993
+
2994
+
2995
+ def multipoles2npcf_singlethetcombi(self, elthet1, elthet2, elthet3, projection="X"):
2996
+ r""" Converts a 4PCF in the multipole basis in the real space basis for a fixed combination of radial bins.
2997
+
2998
+ Returns:
2999
+ --------
3000
+ npcf_out: np.ndarray
3001
+ Natural 4PCF components in the real-space bassi for all angular combinations.
3002
+ npcf_norm_out: np.ndarray
3003
+ 4PCF weighted counts in the real-space bassi for all angular combinations.
3004
+ """
3005
+ assert((projection in self.proj_dict.keys()) and (projection in self.projections_avail))
3006
+
3007
+ _phis1 = self.phis[0].astype(np.float64)
3008
+ _phis2 = self.phis[1].astype(np.float64)
3009
+ _nphis1 = len(self.phis[0])
3010
+ _nphis2 = len(self.phis[1])
3011
+ ncfs, nnvals, _, nzcombis, nbinsr, _, _ = np.shape(self.npcf_multipoles)
3012
+
3013
+ Upsilon_in = self.npcf_multipoles[...,elthet1,elthet2,elthet3].flatten()
3014
+ N_in = self.npcf_multipoles_norm[...,elthet1,elthet2,elthet3].flatten()
3015
+ npcf_out = np.zeros(self.n_cfs*nzcombis*_nphis1*_nphis2, dtype=np.complex128)
3016
+ npcf_norm_out = np.zeros(nzcombis*_nphis1*_nphis2, dtype=np.complex128)
3017
+
3018
+ self.clib.multipoles2npcf_gggg_singletheta(
3019
+ Upsilon_in, N_in, self.nmaxs[0], self.nmaxs[1],
3020
+ self.bin_centers_mean[elthet1], self.bin_centers_mean[elthet2], self.bin_centers_mean[elthet3],
3021
+ _phis1, _phis2, _nphis1, _nphis2,
3022
+ np.int32(self.proj_dict[projection]), npcf_out, npcf_norm_out)
3023
+
3024
+ return npcf_out.reshape((self.n_cfs, _nphis1,_nphis2)), npcf_norm_out.reshape((_nphis1,_nphis2))
3025
+
3026
+ def multipoles2npcf_gggg_singletheta_nconvergence(self, elthet1, elthet2, elthet3, projection="X"):
3027
+ r""" Checks convergence of the conversion between mutltipole-space and real space for a combination of radial bins.
3028
+
3029
+ Returns:
3030
+ --------
3031
+ npcf_out: np.ndarray
3032
+ Natural 4PCF components in the real-space basis for all angular combinations.
3033
+ npcf_norm_out: np.ndarray
3034
+ 4PCF weighted counts in the real-space basis for all angular combinations.
3035
+ """
3036
+ assert((projection in self.proj_dict.keys()) and (projection in self.projections_avail))
3037
+
3038
+ _phis1 = self.phis[0].astype(np.float64)
3039
+ _phis2 = self.phis[1].astype(np.float64)
3040
+ _nphis1 = len(self.phis[0])
3041
+ _nphis2 = len(self.phis[1])
3042
+
3043
+ ncfs, nnvals, _, nzcombis, nbinsr, _, _ = np.shape(self.npcf_multipoles)
3044
+
3045
+ Upsilon_in = self.npcf_multipoles[...,elthet1,elthet2,elthet3].flatten()
3046
+ N_in = self.npcf_multipoles_norm[...,elthet1,elthet2,elthet3].flatten()
3047
+ npcf_out = np.zeros(self.n_cfs*nzcombis*(self.nmaxs[0]+1)*(self.nmaxs[1]+1)*_nphis1*_nphis2, dtype=np.complex128)
3048
+ npcf_norm_out = np.zeros(nzcombis*(self.nmaxs[0]+1)*(self.nmaxs[1]+1)*_nphis1*_nphis2, dtype=np.complex128)
3049
+
3050
+ self.clib.multipoles2npcf_gggg_singletheta_nconvergence(
3051
+ Upsilon_in, N_in, self.nmaxs[0], self.nmaxs[1],
3052
+ self.bin_centers_mean[elthet1], self.bin_centers_mean[elthet2], self.bin_centers_mean[elthet3],
3053
+ _phis1, _phis2, _nphis1, _nphis2,
3054
+ np.int32(self.proj_dict[projection]), npcf_out, npcf_norm_out)
3055
+
3056
+ npcf_out = npcf_out.reshape((self.n_cfs, self.nmaxs[0]+1, self.nmaxs[1]+1, _nphis1, _nphis2))
3057
+ npcf_norm_out = npcf_norm_out.reshape((self.nmaxs[0]+1, self.nmaxs[1]+1, _nphis1, _nphis2))
3058
+
3059
+ return npcf_out, npcf_norm_out
3060
+
3061
+ def computeMap4(self, radii, nmax_trafo=None, basis='MapMx'):
3062
+ r"""Computes the fourth-order aperture mass statistcs using the polynomial filter of Crittenden 2002."""
3063
+
3064
+ assert(basis in ['MapMx','MM*','both'])
3065
+
3066
+ if nmax_trafo is None:
3067
+ nmax_trafo=self.nmaxs[0]
3068
+
3069
+ # Retrieve all the aperture measures in the MM* basis via the 5D transformation eqns
3070
+ M4correlators = np.zeros(8*len(radii), dtype=np.complex128)
3071
+ self.clib.fourpcfmultipoles2M4correlators(
3072
+ np.int32(self.nmaxs[0]), np.int32(nmax_trafo),
3073
+ self.bin_edges, self.bin_centers_mean, np.int32(self.nbinsr),
3074
+ radii.astype(np.float64), np.int32(len(radii)),
3075
+ self.phis[0].astype(np.float64), self.phis[1].astype(np.float64),
3076
+ self.dphis[0].astype(np.float64), self.dphis[1].astype(np.float64),
3077
+ len(self.phis[0]), len(self.phis[1]),
3078
+ np.int32(self.proj_dict[self.projection]), np.int32(self.nthreads),
3079
+ self.npcf_multipoles.flatten(), self.npcf_multipoles_norm.flatten(),
3080
+ M4correlators)
3081
+ res_MMStar = M4correlators.reshape((8,len(radii)))
3082
+
3083
+ # Allocate result
3084
+ res = ()
3085
+ if basis=='MM*' or basis=='both':
3086
+ res += (res_MMStar, )
3087
+ if basis=='MapMx' or basis=='both':
3088
+ res += ( GGGGCorrelation_NoTomo.MMStar2MapMx_fourth(res_MMStar), )
3089
+
3090
+ return res
3091
+
3092
+ ## PROJECTIONS ##
3093
+ def projectnpcf(self, projection):
3094
+ super()._projectnpcf(self, projection)
3095
+
3096
+ def _x2centroid(self):
3097
+ gammas_cen = np.zeros_like(self.npcf)
3098
+ pimod = lambda x: x%(2*np.pi) - 2*np.pi*(x%(2*np.pi)>=np.pi)
3099
+ npcf_cen = np.zeros(self.npcf.shape, dtype=complex)
3100
+ _centers = np.mean(self.bin_centers, axis=0)
3101
+ for elb1, bin1 in enumerate(_centers):
3102
+ for elb2, bin2 in enumerate(_centers):
3103
+ for elb3, bin3 in enumerate(_centers):
3104
+ phiexp = np.exp(1J*self.phis[0])
3105
+ phiexp_c = np.exp(-1J*self.phis[0])
3106
+ phi12grid, phi13grid = np.meshgrid(phiexp, phiexp)
3107
+ phi12grid_c, phi13grid_c = np.meshgrid(phiexp_c, phiexp_c)
3108
+ prod1 = (bin1 +bin2*phi12grid_c + bin3*phi13grid_c) /(bin1 + bin2*phi12grid + bin3*phi13grid) #q1
3109
+ prod2 = (3*bin1 -bin2*phi12grid_c - bin3*phi13grid_c) /(3*bin1 - bin2*phi12grid - bin3*phi13grid) #q2
3110
+ prod3 = (bin1 -3*bin2*phi12grid_c + bin3*phi13grid_c) /(bin1 - 3*bin2*phi12grid + bin3*phi13grid) #q3
3111
+ prod4 = (bin1 +bin2*phi12grid_c - 3*bin3*phi13grid_c)/(bin1 + bin2*phi12grid - 3*bin3*phi13grid) #q4
3112
+ prod1_inv = prod1.conj()/np.abs(prod1)
3113
+ prod2_inv = prod2.conj()/np.abs(prod2)
3114
+ prod3_inv = prod3.conj()/np.abs(prod3)
3115
+ prod4_inv = prod4.conj()/np.abs(prod4)
3116
+ rot_nom = np.zeros((8,len(self.phis[0]), len(self.phis[1])))
3117
+ rot_nom[0] = pimod(np.angle(prod1 *prod2 *prod3 *prod4 * phi12grid**2 * phi13grid**3))
3118
+ rot_nom[1] = pimod(np.angle(prod1_inv*prod2 *prod3 *prod4 * phi12grid**2 * phi13grid**1))
3119
+ rot_nom[2] = pimod(np.angle(prod1 *prod2_inv*prod3 *prod4 * phi12grid**2 * phi13grid**3))
3120
+ rot_nom[3] = pimod(np.angle(prod1 *prod2 *prod3_inv*prod4 * phi12grid_c**2 * phi13grid**3))
3121
+ rot_nom[4] = pimod(np.angle(prod1 *prod2 *prod3 *prod4_inv * phi12grid**2 * phi13grid_c**1))
3122
+ rot_nom[5] = pimod(np.angle(prod1_inv*prod2_inv*prod3 *prod4 * phi12grid**2 * phi13grid**1))
3123
+ rot_nom[6] = pimod(np.angle(prod1_inv*prod2 *prod3_inv*prod4 * phi12grid_c**2 * phi13grid**1))
3124
+ rot_nom[7] = pimod(np.angle(prod1_inv*prod2 *prod3 *prod4_inv * phi12grid**2 * phi13grid_c**3))
3125
+ gammas_cen[:,:,elb1,elb2,elb3] = self.npcf[:,:,elb1,elb2,elb3]*np.exp(1j*rot_nom)[:,np.newaxis,:,:]
3126
+ return gammas_cen
3127
+
3128
+ ## GAUSSIAN-FIELD SPECIFIC FUNCTIONS ##
3129
+ # Deprecate this as it has been ported to c
3130
+ @staticmethod
3131
+ def fourpcf_gauss_x(theta1, theta2, theta3, phi12, phi13, xipspl, ximspl):
3132
+ """ Computes disconnected part of the 4pcf in the 'x'-projection
3133
+ given a splined 2pcf
3134
+ """
3135
+ allgammas = [None]*8
3136
+ xprojs = [None]*8
3137
+ y1 = theta1 * np.ones_like(phi12)
3138
+ y2 = theta2*np.exp(1j*phi12)
3139
+ y3 = theta3*np.exp(1j*phi13)
3140
+ absy1 = np.abs(y1)
3141
+ absy2 = np.abs(y2)
3142
+ absy3 = np.abs(y3)
3143
+ absy12 = np.abs(y2-y1)
3144
+ absy13 = np.abs(y1-y3)
3145
+ absy23 = np.abs(y3-y2)
3146
+ q1 = -0.25*(y1+y2+y3)
3147
+ q2 = 0.25*(3*y1-y2-y3)
3148
+ q3 = 0.25*(3*y2-y3-y1)
3149
+ q4 = 0.25*(3*y3-y1-y2)
3150
+ q1c = q1.conj(); q2c = q2.conj(); q3c = q3.conj(); q4c = q4.conj();
3151
+ y123_cub = (np.abs(y1)*np.abs(y2)*np.abs(y3))**3
3152
+ ang1_4 = ((y1)/absy1)**4; ang2_4 = ((y2)/absy2)**4; ang3_4 = ((y3)/absy3)**4
3153
+ ang12_4 = ((y2-y1)/absy12)**4; ang13_4 = ((y3-y1)/absy13)**4; ang23_4 = ((y3-y2)/absy23)**4;
3154
+ xprojs[0] = (y1**3*y2**2*y3**3)/(np.abs(y1)**3*np.abs(y2)**2*np.abs(y3)**3)
3155
+ xprojs[1] = (y1**1*y2**2*y3**1)/(np.abs(y1)**1*np.abs(y2)**2*np.abs(y3)**1)
3156
+ xprojs[2] = (y1**-1*y2**2*y3**3)/(np.abs(y1)**-1*np.abs(y2)**2*np.abs(y3)**3)
3157
+ xprojs[3] = (y1**3*y2**-2*y3**3)/(np.abs(y1)**3*np.abs(y2)**-2*np.abs(y3)**3)
3158
+ xprojs[4] = (y1**3*y2**2*y3**-1)/(np.abs(y1)**3*np.abs(y2)**2*np.abs(y3)**-1)
3159
+ xprojs[5] = (y1**-3*y2**2*y3**1)/(np.abs(y1)**-3*np.abs(y2)**2*np.abs(y3)**1)
3160
+ xprojs[6] = (y1**1*y2**-2*y3**1)/(np.abs(y1)**1*np.abs(y2)**-2*np.abs(y3)**1)
3161
+ xprojs[7] = (y1**1*y2**2*y3**-3)/(np.abs(y1)**1*np.abs(y2)**2*np.abs(y3)**-3)
3162
+ allgammas[0] = 1./xprojs[0] * (
3163
+ ang23_4 * ang1_4 * ximspl(absy23) * ximspl(absy1) +
3164
+ ang13_4 * ang2_4 * ximspl(absy13) * ximspl(absy2) +
3165
+ ang12_4 * ang3_4 * ximspl(absy12) * ximspl(absy3))
3166
+ allgammas[1] = 1./xprojs[1] * (
3167
+ ang23_4 * xipspl(absy1) * ximspl(absy23) +
3168
+ ang13_4 * xipspl(absy2) * ximspl(absy13) +
3169
+ ang12_4 * xipspl(absy3) * ximspl(absy12))
3170
+ allgammas[2] = 1./xprojs[2] * (
3171
+ ang23_4 * xipspl(absy1) * ximspl(absy23) +
3172
+ ang2_4 * ximspl(absy2) * xipspl(absy13) +
3173
+ ang3_4 * ximspl(absy3) * xipspl(absy12))
3174
+ allgammas[3] = 1./xprojs[3] * (
3175
+ ang1_4 * ximspl(absy1) * xipspl(absy23) +
3176
+ ang13_4 * xipspl(absy2) * ximspl(absy13) +
3177
+ ang3_4 * ximspl(absy3) * xipspl(absy12))
3178
+ allgammas[4] = 1./xprojs[4] * (
3179
+ ang1_4 * ximspl(absy1) * xipspl(absy23) +
3180
+ ang2_4 * ximspl(absy2) * xipspl(absy13) +
3181
+ ang12_4 * xipspl(absy3) * ximspl(absy12))
3182
+ allgammas[5] = 1./xprojs[5] * (
3183
+ ang1_4.conj() * ang23_4 * ximspl(absy23) * ximspl(absy1) +
3184
+ xipspl(absy13) * xipspl(absy2) +
3185
+ xipspl(absy12) * xipspl(absy3))
3186
+ allgammas[6] = 1./xprojs[6] * (
3187
+ xipspl(absy23) * xipspl(absy1) +
3188
+ ang2_4.conj() * ang13_4 * ximspl(absy13) * ximspl(absy2) +
3189
+ xipspl(absy12) * xipspl(absy3))
3190
+ allgammas[7] = 1./xprojs[7] * (
3191
+ xipspl(absy23) * xipspl(absy1) +
3192
+ xipspl(absy13) * xipspl(absy2) +
3193
+ ang3_4.conj() * ang12_4 * ximspl(absy12) * ximspl(absy3))
3194
+
3195
+ return allgammas
3196
+
3197
+ # Disconnected 4pcf from binned 2pcf (might want to deprecate this as it is a special case of nsubr==1)
3198
+ def __gauss4pcf_analytic(self, theta1, theta2, theta3, xip_arr, xim_arr, thetamin_xi, thetamax_xi, dtheta_xi):
3199
+ gausss_4pcf = np.zeros(8*len(self.phis[0])*len(self.phis[0]),dtype=np.complex128)
3200
+ self.clib.gauss4pcf_analytic(theta1.astype(np.float64),
3201
+ theta2.astype(np.float64),
3202
+ theta3.astype(np.float64),
3203
+ self.phis[0].astype(np.float64), np.int32(len(self.phis[0])),
3204
+ xip_arr.astype(np.float64), xim_arr.astype(np.float64),
3205
+ thetamin_xi, thetamax_xi, dtheta_xi,
3206
+ gausss_4pcf)
3207
+ return gausss_4pcf
3208
+
3209
+
3210
+ # [Debug] Disconnected 4pcf from analytic 2pcf
3211
+ def gauss4pcf_analytic(self, itheta1, itheta2, itheta3, nsubr,
3212
+ xip_arr, xim_arr, thetamin_xi, thetamax_xi, dtheta_xi):
3213
+
3214
+ gauss_4pcf = np.zeros(8*self.nbinsphi[0]*self.nbinsphi[1],dtype=np.complex128)
3215
+
3216
+ self.clib.gauss4pcf_analytic_integrated(
3217
+ np.int32(itheta1),
3218
+ np.int32(itheta2),
3219
+ np.int32(itheta3),
3220
+ np.int32(nsubr),
3221
+ self.bin_edges.astype(np.float64),
3222
+ np.int32(self.nbinsr),
3223
+ self.phis[0].astype(np.float64),
3224
+ np.int32(self.nbinsphi[0]),
3225
+ xip_arr.astype(np.float64),
3226
+ xim_arr.astype(np.float64),
3227
+ np.float64(thetamin_xi),
3228
+ np.float64(thetamax_xi),
3229
+ np.float64(dtheta_xi),
3230
+ gauss_4pcf)
3231
+ return gauss_4pcf.reshape((8, self.nbinsphi[0], self.nbinsphi[1]))
3232
+
3233
+ # Compute disconnected part of 4pcf in multiple basis
3234
+ def gauss4pcf_multipolebasis(self, itheta1, itheta2, itheta3, nsubr,
3235
+ xip_arr, xim_arr, thetamin_xi, thetamax_xi, dtheta_xi):
3236
+
3237
+ # Obtain integrated 4pcf
3238
+ int_4pcf = self.gauss4pcf_analytic_integrated(itheta1, itheta2, itheta3, nsubr,
3239
+ xip_arr, xim_arr,
3240
+ thetamin_xi, thetamax_xi, dtheta_xi)
3241
+
3242
+ # Transform to multiple basis (cf eq xxx in P25)
3243
+ phigrid1, phigrid2 = np.meshgrid(self.phis[0],self.phis[1])
3244
+ gauss_multipoles = np.zeros((8,2*self.nmaxs[0]+1,2*self.nmaxs[1]+1),dtype=complex)
3245
+ for eln2,n2 in enumerate(np.arange(-self.nmaxs[0],self.nmaxs[0]+1)):
3246
+ fac1 = np.e**(-1J*n2*phigrid1)
3247
+ for eln3,n3 in enumerate(np.arange(-self.nmaxs[1],self.nmaxs[1]+1)):
3248
+ fac2 = np.e**(-1J*n3*phigrid2)
3249
+ for elcomp in range(8):
3250
+ gauss_multipoles[elcomp,eln2,eln3] = np.mean(int_4pcf[elcomp]*fac1*fac2)
3251
+
3252
+ return gauss_multipoles
3253
+
3254
+
3255
+ def estimateMap4disc(self, cat, radii, basis='MapMx',fac_minsep=0.05, fac_maxsep=2., binsize=0.1, nsubr=3, nsubsample_filter=1):
3256
+ """ Estimate disconnected part of fourth-order aperture statistics on a shape catalog. """
3257
+
3258
+ # Compute shear 2pcf from data
3259
+ min_sep_disc = fac_minsep*self.min_sep
3260
+ max_sep_disc = fac_maxsep*self.max_sep
3261
+ binsize_disc = min(0.1,self.binsize)
3262
+ ggcorr = GGCorrelation(min_sep=min_sep_disc, max_sep=max_sep_disc,binsize=binsize_disc,
3263
+ rmin_pixsize=self.rmin_pixsize, tree_resos=self.tree_resos, nthreads=self.nthreads)
3264
+ ggcorr.process(cat)
3265
+
3266
+ # Convert this to fourth-order aperture statistics
3267
+ linarr = np.linspace(min_sep_disc,max_sep_disc,int(max_sep_disc/(binsize_disc*min_sep_disc)))
3268
+ xip_spl = interp1d(x=ggcorr.bin_centers_mean,y=ggcorr.xip[0].real,fill_value=0,bounds_error=False)
3269
+ xim_spl = interp1d(x=ggcorr.bin_centers_mean,y=ggcorr.xim[0].real,fill_value=0,bounds_error=False)
3270
+ mapstat = self.Map4analytic(mapradii=radii,
3271
+ xip_spl=xip_spl,
3272
+ xim_spl=xim_spl,
3273
+ thetamin_xi=linarr[0],
3274
+ thetamax_xi=linarr[-1],
3275
+ ntheta_xi=len(linarr),
3276
+ nsubr=nsubr,nsubsample_filter=nsubsample_filter,basis=basis)
3277
+ return mapstat
3278
+
3279
+
3280
+ # Disconnected part of Map^4 from analytic 2pcf
3281
+ # thetamin_xi, thetamax_xi, ntheta_xi is the linspaced array in which the xipm are passed to the external function
3282
+ def Map4analytic(self, mapradii, xip_spl, xim_spl, thetamin_xi, thetamax_xi, ntheta_xi,
3283
+ nsubr=1, nsubsample_filter=1, batchsize=None, basis='MapMx'):
3284
+
3285
+ self.nbinsz = 1
3286
+ self.nzcombis = 1
3287
+ _nmax = self.nmaxs[0]
3288
+ _nnvals = (2*_nmax+1)*(2*_nmax+1)
3289
+ _nbinsr3 = self.nbinsr*self.nbinsr*self.nbinsr
3290
+ _nphis = len(self.phis[0])
3291
+ bin_centers = np.zeros(self.nbinsz*self.nbinsr).astype(np.float64)
3292
+ M4correlators = np.zeros(8*self.nzcombis*len(mapradii)).astype(np.complex128)
3293
+ # Define the radial bin batches
3294
+ if batchsize is None:
3295
+ batchsize = min(_nbinsr3,min(10000,int(_nbinsr3/self.nthreads)))
3296
+ if self._verbose_python:
3297
+ print("Using batchsize of %i for radial bins"%batchsize)
3298
+ nbatches = np.int32(_nbinsr3/batchsize)
3299
+ thetacombis_batches = np.arange(_nbinsr3).astype(np.int32)
3300
+ cumnthetacombis_batches = (np.arange(nbatches+1)*_nbinsr3/(nbatches)).astype(np.int32)
3301
+ nthetacombis_batches = (cumnthetacombis_batches[1:]-cumnthetacombis_batches[:-1]).astype(np.int32)
3302
+ cumnthetacombis_batches[-1] = _nbinsr3
3303
+ nthetacombis_batches[-1] = _nbinsr3-cumnthetacombis_batches[-2]
3304
+ thetacombis_batches = thetacombis_batches.flatten().astype(np.int32)
3305
+ nbatches = len(nthetacombis_batches)
3306
+
3307
+ args_4pcfsetup = (np.float64(self.min_sep), np.float64(self.max_sep), np.int32(self.nbinsr),
3308
+ self.phis[0].astype(np.float64),
3309
+ (self.phis[0][1]-self.phis[0][0])*np.ones(_nphis, dtype=np.float64), _nphis, np.int32(nsubr), )
3310
+ args_thetas = (thetacombis_batches, nthetacombis_batches, cumnthetacombis_batches, nbatches, )
3311
+ args_map4 = (mapradii.astype(np.float64), np.int32(len(mapradii)), )
3312
+ thetas_xi = np.linspace(thetamin_xi,thetamax_xi,ntheta_xi+1)
3313
+ args_xi = (xip_spl(thetas_xi), xim_spl(thetas_xi), thetamin_xi, thetamax_xi, ntheta_xi, nsubsample_filter, )
3314
+ args = (*args_4pcfsetup,
3315
+ *args_thetas,
3316
+ np.int32(self.nthreads),
3317
+ *args_map4,
3318
+ *args_xi,
3319
+ M4correlators)
3320
+ func = self.clib.alloc_notomoMap4_analytic
3321
+
3322
+ if self._verbose_debug:
3323
+ for elarg, arg in enumerate(args):
3324
+ toprint = (elarg, type(arg),)
3325
+ if isinstance(arg, np.ndarray):
3326
+ toprint += (type(arg[0]), arg.shape)
3327
+ try:
3328
+ toprint += (func.argtypes[elarg], )
3329
+ print(toprint)
3330
+ print(arg)
3331
+ except:
3332
+ print("We did have a problem for arg %i"%elarg)
3333
+
3334
+ func(*args)
3335
+
3336
+ res_MMStar = M4correlators.reshape((8,len(mapradii)))
3337
+ # Allocate result
3338
+ res = ()
3339
+ if basis=='MM*' or basis=='both':
3340
+ res += (res_MMStar, )
3341
+ if basis=='MapMx' or basis=='both':
3342
+ res += (GGGGCorrelation_NoTomo.MMStar2MapMx_fourth(res_MMStar), )
3343
+
3344
+ return res
3345
+
3346
+ def getMultipolesFromSymm(self, nmax_rec, itheta1, itheta2, itheta3, eltrafo):
3347
+
3348
+ nmax_alloc = 2*nmax_rec+1
3349
+ assert(nmax_alloc<=self.nmaxs[0])
3350
+
3351
+ # Only select relevant n1/n2 indices
3352
+ _dn = self.nmaxs[0]-nmax_alloc
3353
+
3354
+ _shape, _inds, _n2s, _n3s = gen_n2n3indices_Upsfourth(nmax_rec)
3355
+ Upsn_in = self.npcf_multipoles[:,_dn:-_dn,_dn:-_dn,0,itheta1,itheta2,itheta3].flatten()
3356
+ Nn_in = self.npcf_multipoles_norm[_dn:-_dn,_dn:-_dn,0,itheta1,itheta2,itheta3].flatten()
3357
+ Upsn_out = np.zeros(8*(2*nmax_rec+1)*(2*nmax_rec+1), dtype=np.complex128)
3358
+ Nn_out = np.zeros(1*(2*nmax_rec+1)*(2*nmax_rec+1), dtype=np.complex128)
3359
+
3360
+ self.clib.getMultipolesFromSymm(
3361
+ Upsn_in, Nn_in, nmax_rec, eltrafo, _inds, len(_inds), Upsn_out, Nn_out)
3362
+
3363
+ Upsn_out = Upsn_out.reshape((8,(2*nmax_rec+1),(2*nmax_rec+1)))
3364
+ Nn_out = Nn_out.reshape(((2*nmax_rec+1),(2*nmax_rec+1)))
3365
+
3366
+ return Upsn_out, Nn_out
3367
+
3368
+ ## MISC HELPERS ##
3369
+ @staticmethod
3370
+ def MMStar2MapMx_fourth(res_MMStar):
3371
+ """ Transforms fourth-order aperture correlators to fourth-order aperture mass.
3372
+ See i.e. Eqs (32)-(36) in Silvestre-Rosello+ 2025 (arxiv.org/pdf/2509.07973).
3373
+ """
3374
+ res_MapMx = np.zeros((16,*res_MMStar.shape[1:]))
3375
+ Mcorr2Map4_re = .125*np.array([[+1,+1,+1,+1,+1,+1,+1,+1],
3376
+ [-1,-1,-1,+1,+1,-1,+1,+1],
3377
+ [-1,-1,+1,-1,+1,+1,-1,+1],
3378
+ [-1,-1,+1,+1,-1,+1,+1,-1],
3379
+ [-1,+1,-1,-1,+1,+1,+1,-1],
3380
+ [-1,+1,-1,+1,-1,+1,-1,+1],
3381
+ [-1,+1,+1,-1,-1,-1,+1,+1],
3382
+ [+1,-1,-1,-1,-1,+1,+1,+1]])
3383
+ Mcorr2Map4_im = .125*np.array([[+1,-1,+1,+1,+1,-1,-1,-1],
3384
+ [+1,+1,-1,+1,+1,-1,+1,+1],
3385
+ [+1,+1,+1,-1,+1,+1,-1,+1],
3386
+ [+1,+1,+1,+1,-1,+1,+1,-1],
3387
+ [-1,-1,+1,+1,+1,+1,+1,+1],
3388
+ [-1,+1,-1,+1,+1,+1,-1,-1],
3389
+ [-1,+1,+1,-1,+1,-1,+1,-1],
3390
+ [-1,+1,+1,+1,-1,-1,-1,+1]])
3391
+ res_MapMx[[0,5,6,7,8,9,10,15]] = Mcorr2Map4_re@(res_MMStar.real)
3392
+ res_MapMx[[1,2,3,4,11,12,13,14]] = Mcorr2Map4_im@(res_MMStar.imag)
3393
+ return res_MapMx
3394
+
3395
+
3396
+ class NNNNCorrelation_NoTomo(BinnedNPCF):
3397
+ r""" Class containing methods to measure and and obtain statistics that are built
3398
+ from nontomographic fourth-order scalar correlation functions.
3399
+
3400
+ Attributes
3401
+ ----------
3402
+ min_sep: float
3403
+ The smallest distance of each vertex for which the NPCF is computed.
3404
+ max_sep: float
3405
+ The largest distance of each vertex for which the NPCF is computed.
3406
+ thetabatchsize_max: int, optional
3407
+ The largest number of radial bin combinations that are processed in parallel.
3408
+ Defaults to ``10 000``.
3409
+
3410
+ Notes
3411
+ -----
3412
+ Inherits all other parameters and attributes from :class:`BinnedNPCF`.
3413
+ Additional child-specific parameters can be passed via ``kwargs``.
3414
+ Either ``nbinsr`` or ``binsize`` has to be provided to fix the binning scheme .
3415
+
3416
+ """
3417
+
3418
+ def __init__(self, min_sep, max_sep, verbose=False, thetabatchsize_max=10000, method="Tree", **kwargs):
3419
+ super().__init__(order=4, spins=np.array([0,0,0,0], dtype=np.int32),
3420
+ n_cfs=1, min_sep=min_sep, max_sep=max_sep,
3421
+ method=method, methods_avail=["Tree"], **kwargs)
3422
+
3423
+ self.thetabatchsize_max = thetabatchsize_max
3424
+ self.nbinsz = 1
3425
+ self.nzcombis = 1
3426
+
3427
+ def process(self, cat, statistics="all", tofile=False, apply_edge_correction=False,
3428
+ lowmem=True, mapradii=None, batchsize=None, custom_thetacombis=None, cutlen=2**31-1):
3429
+ r"""
3430
+ Arguments:
3431
+
3432
+ Logic works as follows:
3433
+ * Keyword 'statistics' \in [4pcf_real, 4pcf_multipoles, N4, Nap4, Nap4, Nap4c, allNap, all4pcf, all]
3434
+ * - If 4pcf_multipoles in statistics --> save 4pcf_multipoles
3435
+ * - If 4pcf_real in statistics --> save 4pcf_real
3436
+ * - If only N4 in statistics --> Do not save any 4pcf. This is really the lowmem case.
3437
+ * - allNap, all4pcf, all are abbreviations as expected
3438
+ * If lowmem=True, uses the inefficient, but lowmem function for computation and output statistics
3439
+ from there as wanted.
3440
+ * If lowmem=False, use the fast functions to do the 4pcf multipole computation and do
3441
+ the potential conversions lateron.
3442
+ * Default lowmem to None and
3443
+ * - Set to true if any aperture statistics is in stats or we will run into mem error
3444
+ * - Set to false otherwise
3445
+ * - Raise error if lowmen=False and we will have more that 2^31-1 elements at any stage of the computation
3446
+
3447
+ custom_thetacombis: array of inds which theta combis will be selected
3448
+ """
3449
+
3450
+ ## Preparations ##
3451
+ # Build list of statistics to be calculated
3452
+ statistics_avail_4pcf = ["4pcf_real", "4pcf_multipole"]
3453
+ statistics_avail_nap4 = ["N4", "Nap4", "N4c", "Nap4c"]
3454
+ statistics_avail_comp = ["allNap", "all4pcf", "all"]
3455
+ statistics_avail_phys = statistics_avail_4pcf + statistics_avail_nap4
3456
+ statistics_avail = statistics_avail_4pcf + statistics_avail_nap4 + statistics_avail_comp
3457
+ _statistics = []
3458
+ hasintegratedstats = False
3459
+ _strbadstats = lambda stat: ("The statistics `%s` has not been implemented yet. "%stat +
3460
+ "Currently supported statistics are:\n" + str(statistics_avail))
3461
+ if type(statistics) not in [list, str]:
3462
+ raise ValueError("The parameter `statistics` should either be a list or a string.")
3463
+ if type(statistics) is str:
3464
+ if statistics not in statistics_avail:
3465
+ raise ValueError(_strbadstats)
3466
+ statistics = [statistics]
3467
+ if type(statistics) is list:
3468
+ if "all" in statistics:
3469
+ _statistics = statistics_avail_phys
3470
+ elif "all4pcf" in statistics:
3471
+ _statistics.append(statistics_avail_4pcf)
3472
+ elif "allNap" in statistics:
3473
+ _statistics.append(statistics_avail_nap4)
3474
+ _statistics = flatlist(_statistics)
3475
+ for stat in statistics:
3476
+ if stat not in statistics_avail:
3477
+ raise ValueError(_strbadstats)
3478
+ if stat in statistics_avail_phys and stat not in _statistics:
3479
+ _statistics.append(stat)
3480
+ statistics = list(set(flatlist(_statistics)))
3481
+ for stat in statistics:
3482
+ if stat in statistics_avail_nap4:
3483
+ hasintegratedstats = True
3484
+
3485
+ # Check if the output will fit in memory
3486
+ if "4pcf_multipole" in statistics:
3487
+ _nvals = self.nzcombis*(2*self.nmaxs[0]+1)*(2*self.nmaxs[1]+1)*self.nbinsr**3
3488
+ if _nvals>cutlen:
3489
+ raise ValueError(("4pcf in multipole basis will cause memory overflow " +
3490
+ "(requiring %.2fx10^9 > %.2fx10^9 elements)\n"%(_nvals/1e9, cutlen/1e9) +
3491
+ "If you are solely interested in integrated statistics (like Map4), you" +
3492
+ "only need to add those to the `statistics` argument."))
3493
+ if "4pcf_real" in statistics:
3494
+ _nvals = self.nzcombis*self.nbinsphi[0]*self.nbinsphi[1]*self.nbinsr**3
3495
+ if _nvals>cutlen:
3496
+ raise ValueError(("4pcf in real basis will cause memory overflow " +
3497
+ "(requiring %.2fx10^9 > %.2fx10^9 elements)\n"%(_nvals/1e9, cutlen/1e9) +
3498
+ "If you are solely interested in integrated statistics (like Map4), you" +
3499
+ "only need to add those to the `statistics` argument."))
3500
+
3501
+ # Decide on whether to use low-mem functions or not
3502
+ if hasintegratedstats:
3503
+ if lowmem in [False, None]:
3504
+ if not lowmem:
3505
+ print("Low-memory computation enforced for integrated measures of the 4pcf. " +
3506
+ "Set `lowmem` from `%s` to `True`"%str(lowmem))
3507
+ lowmem = True
3508
+ else:
3509
+ if lowmem in [None, False]:
3510
+ maxlen = 0
3511
+ _lowmem = False
3512
+ if "4pcf_multipole" in statistics:
3513
+ _nvals = self.nzcombis*(2*self.nmaxs[0]+1)*(2*self.nmaxs[1]+1)*self.nbinsr**3
3514
+ if _nvals > cutlen:
3515
+ if not lowmem:
3516
+ print("Switching to low-memory computation of 4pcf in multipole basis.")
3517
+ lowmem = True
3518
+ else:
3519
+ lowmem = False
3520
+ if "4pcf_real" in statistics:
3521
+ nvals = self.nzcombis*self.nbinsphi[0]*self.nbinsphi[1]*self.nbinsr**3
3522
+ if _nvals > cutlen:
3523
+ if not lowmem:
3524
+ print("Switching to low-memory computation of 4pcf in real basis.")
3525
+ lowmem = True
3526
+ else:
3527
+ lowmem = False
3528
+
3529
+ # Misc checks
3530
+ self._checkcats(cat, self.spins)
3531
+
3532
+ ## Build args for wrapped functions ##
3533
+ # Shortcuts
3534
+ _nmax = self.nmaxs[0]
3535
+ _nnvals = (2*_nmax+1)*(2*_nmax+1)
3536
+ _nbinsr3 = self.nbinsr*self.nbinsr*self.nbinsr
3537
+ _nphis = len(self.phis[0])
3538
+ sc = (2*_nmax+1,2*_nmax+1,self.nzcombis,self.nbinsr,self.nbinsr,self.nbinsr)
3539
+ szr = (self.nbinsz, self.nbinsr)
3540
+ s4pcf = (self.nzcombis,self.nbinsr,self.nbinsr,self.nbinsr,_nphis,_nphis)
3541
+ # Init default args
3542
+ bin_centers = np.zeros(self.nbinsz*self.nbinsr).astype(np.float64)
3543
+ if not cat.hasspatialhash:
3544
+ cat.build_spatialhash(dpix=max(1.,self.max_sep//10.))
3545
+ nregions = np.int32(len(np.argwhere(cat.index_matcher>-1).flatten()))
3546
+ args_basecat = (cat.isinner.astype(np.float64), cat.weight, cat.pos1, cat.pos2,
3547
+ np.int32(cat.ngal), )
3548
+ args_hash = (cat.index_matcher, cat.pixs_galind_bounds, cat.pix_gals, nregions,
3549
+ np.float64(cat.pix1_start), np.float64(cat.pix1_d), np.int32(cat.pix1_n),
3550
+ np.float64(cat.pix2_start), np.float64(cat.pix2_d), np.int32(cat.pix2_n), )
3551
+
3552
+ # Init optional args
3553
+ __lenflag = 10
3554
+ __fillflag = -1
3555
+ if "4pcf_multipole" in statistics:
3556
+ N_n = np.zeros(_nnvals*self.nzcombis*_nbinsr3).astype(np.complex128)
3557
+ alloc_4pcfmultipoles = 1
3558
+ else:
3559
+ N_n = __fillflag*np.zeros(__lenflag).astype(np.complex128)
3560
+ alloc_4pcfmultipoles = 0
3561
+ if "4pcf_real" in statistics:
3562
+ fourpcf = np.zeros(_nphis*_nphis*self.nzcombis*_nbinsr3).astype(np.complex128)
3563
+ alloc_4pcfreal = 1
3564
+ else:
3565
+ fourpcf = __fillflag*np.ones(__lenflag).astype(np.complex128)
3566
+ alloc_4pcfreal = 0
3567
+ if hasintegratedstats:
3568
+ if mapradii is None:
3569
+ raise ValueError("Aperture radii need to be specified in variable `mapradii`.")
3570
+ mapradii = mapradii.astype(np.float64)
3571
+ N4correlators = np.zeros(self.nzcombis*len(mapradii)).astype(np.complex128)
3572
+ else:
3573
+ mapradii = __fillflag*np.ones(__lenflag).astype(np.float64)
3574
+ N4correlators = __fillflag*np.ones(__lenflag).astype(np.complex128)
3575
+
3576
+
3577
+ # Build args based on chosen methods
3578
+ if self.method=="Discrete" and not lowmem:
3579
+ raise NotImplementedError
3580
+ if self.method=="Discrete" and lowmem:
3581
+ raise NotImplementedError
3582
+ if self.method=="Tree" and lowmem:
3583
+ # Prepare mask for nonredundant theta- and multipole configurations
3584
+ _resradial = gen_thetacombis_fourthorder(nbinsr=self.nbinsr, nthreads=self.nthreads, batchsize=batchsize,
3585
+ batchsize_max=self.thetabatchsize_max, ordered=True, custom=custom_thetacombis,
3586
+ verbose=self._verbose_python)
3587
+ _, _, thetacombis_batches, cumnthetacombis_batches, nthetacombis_batches, nbatches = _resradial
3588
+ assert(self.nmaxs[0]==self.nmaxs[1])
3589
+ _resmultipoles = gen_n2n3indices_Upsfourth(self.nmaxs[0])
3590
+ _shape, _inds, _n2s, _n3s = _resmultipoles
3591
+
3592
+ # Prepare reduced catalogs
3593
+ cutfirst = np.int32(self.tree_resos[0]==0.)
3594
+ mhash = cat.multihash(dpixs=self.tree_resos[cutfirst:], dpix_hash=self.tree_resos[-1],
3595
+ shuffle=self.shuffle_pix, normed=False)
3596
+ (ngal_resos, pos1s, pos2s, weights, zbins, isinners, allfields,
3597
+ index_matchers, pixs_galind_bounds, pix_gals, dpixs1_true, dpixs2_true) = mhash
3598
+ weight_resos = np.concatenate(weights).astype(np.float64)
3599
+ pos1_resos = np.concatenate(pos1s).astype(np.float64)
3600
+ pos2_resos = np.concatenate(pos2s).astype(np.float64)
3601
+ zbin_resos = np.concatenate(zbins).astype(np.int32)
3602
+ isinner_resos = np.concatenate(isinners).astype(np.float64)
3603
+ index_matcher_resos = np.concatenate(index_matchers).astype(np.int32)
3604
+ pixs_galind_bounds_resos = np.concatenate(pixs_galind_bounds).astype(np.int32)
3605
+ pix_gals_resos = np.concatenate(pix_gals).astype(np.int32)
3606
+ index_matcher_flat = np.argwhere(cat.index_matcher>-1).flatten()
3607
+ nregions = len(index_matcher_flat)
3608
+ # Build args
3609
+ args_basesetup = (np.int32(_nmax),
3610
+ np.float64(self.min_sep), np.float64(self.max_sep), np.int32(self.nbinsr),
3611
+ np.int32(self.multicountcorr),
3612
+ _inds, np.int32(len(_inds)), self.phis[0].astype(np.float64),
3613
+ 2*np.pi/_nphis*np.ones(_nphis, dtype=np.float64), np.int32(_nphis), )
3614
+ args_resos = (np.int32(self.tree_nresos), self.tree_redges, np.array(ngal_resos, dtype=np.int32),
3615
+ isinner_resos, weight_resos, pos1_resos, pos2_resos,
3616
+ index_matcher_resos, pixs_galind_bounds_resos, pix_gals_resos, np.int32(nregions), )
3617
+ args_hash = (np.float64(cat.pix1_start), np.float64(cat.pix1_d), np.int32(cat.pix1_n),
3618
+ np.float64(cat.pix2_start), np.float64(cat.pix2_d), np.int32(cat.pix2_n), )
3619
+ args_thetas = (thetacombis_batches, nthetacombis_batches, cumnthetacombis_batches, nbatches, )
3620
+ args_nap4 = (mapradii, np.int32(len(mapradii)), N4correlators)
3621
+ args_4pcf = (np.int32(alloc_4pcfmultipoles), np.int32(alloc_4pcfreal),
3622
+ bin_centers, N_n, fourpcf)
3623
+ args = (*args_basecat,
3624
+ *args_basesetup,
3625
+ *args_resos,
3626
+ *args_hash,
3627
+ *args_thetas,
3628
+ np.int32(self.nthreads),
3629
+ *args_nap4,
3630
+ *args_4pcf)
3631
+ func = self.clib.alloc_notomoNap4_tree_nnnn
3632
+
3633
+ # Optionally print the arguments
3634
+ if self._verbose_debug:
3635
+ print("We pass the following arguments:")
3636
+ for elarg, arg in enumerate(args):
3637
+ toprint = (elarg, type(arg),)
3638
+ if isinstance(arg, np.ndarray):
3639
+ toprint += (type(arg[0]), arg.shape)
3640
+ try:
3641
+ toprint += (func.argtypes[elarg], )
3642
+ print(toprint)
3643
+ print(arg)
3644
+ except:
3645
+ print("We did have a problem for arg %i"%elarg)
3646
+
3647
+ ## Compute 4th order stats ##
3648
+ func(*args)
3649
+
3650
+ ## Massage the output ##
3651
+ istatout = ()
3652
+ self.bin_centers = bin_centers.reshape(szr)
3653
+ self.bin_centers_mean = np.mean(self.bin_centers, axis=0)
3654
+ if "4pcf_multipole" in statistics:
3655
+ self.npcf_multipoles = N_n.reshape(sc)
3656
+ if "4pcf_real" in statistics:
3657
+ if lowmem:
3658
+ self.npcf = fourpcf.reshape(s4pcf)
3659
+ else:
3660
+ if self._verbose_python:
3661
+ print("Transforming output to real space basis")
3662
+ self.multipoles2npcf_c()
3663
+ if hasintegratedstats:
3664
+ if "N4" in statistics:
3665
+ istatout += (N4correlators.reshape((self.nzcombis,len(mapradii))), )
3666
+ # TODO allocate map4, map4c etc.
3667
+
3668
+ return istatout
3669
+
3670
+ def multipoles2npcf_singlethetcombi(self, elthet1, elthet2, elthet3):
3671
+ r""" Converts a 4PCF in the multipole basis in the real space basis for a fixed combination of radial bins.
3672
+
3673
+ Returns:
3674
+ --------
3675
+ npcf_out: np.ndarray
3676
+ Natural 4PCF components in the real-space basis for all angular combinations.
3677
+ npcf_norm_out: np.ndarray
3678
+ 4PCF weighted counts in the real-space basis for all angular combinations.
3679
+ """
3680
+
3681
+ _phis1 = self.phis[0].astype(np.float64)
3682
+ _phis2 = self.phis[1].astype(np.float64)
3683
+ _nphis1 = len(self.phis[0])
3684
+ _nphis2 = len(self.phis[1])
3685
+ nnvals, _, nzcombis, nbinsr, _, _ = np.shape(self.npcf_multipoles)
3686
+
3687
+ N_in = self.npcf_multipoles[...,elthet1,elthet2,elthet3].flatten()
3688
+ npcf_out = np.zeros(nzcombis*_nphis1*_nphis2, dtype=np.complex128)
3689
+
3690
+ self.clib.multipoles2npcf_nnnn_singletheta(
3691
+ N_in, self.nmaxs[0], self.nmaxs[1],
3692
+ self.bin_centers_mean[elthet1], self.bin_centers_mean[elthet2], self.bin_centers_mean[elthet3],
3693
+ _phis1, _phis2, _nphis1, _nphis2,
3694
+ npcf_out)
3695
+
3696
+ return npcf_out.reshape(( _nphis1,_nphis2))