orpheus-npcf 0.2.1__cp310-cp310-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.
orpheus/npcf_third.py ADDED
@@ -0,0 +1,1684 @@
1
+ import numpy as np
2
+ from functools import reduce
3
+ from numba import jit, prange
4
+ from numba import config as nb_config
5
+ from numba import complex128 as nb_complex128
6
+ import operator
7
+ from pathlib import Path
8
+ from scipy.interpolate import interp1d
9
+
10
+ from .npcf_base import BinnedNPCF
11
+
12
+ __all__ = ["GGGCorrelation", "GNNCorrelation", "NGGCorrelation"]
13
+
14
+ class GGGCorrelation(BinnedNPCF):
15
+ r""" Class containing methods to measure and and obtain statistics that are built
16
+ from third-order shear correlation functions.
17
+
18
+ Attributes
19
+ ----------
20
+ n_cfs: int
21
+ The number of independent components of the NPCF.
22
+ min_sep: float
23
+ The smallest distance of each vertex for which the NPCF is computed.
24
+ max_sep: float
25
+ The largest distance of each vertex for which the NPCF is computed.
26
+
27
+ Notes
28
+ -----
29
+ Inherits all other parameters and attributes from :class:`BinnedNPCF`.
30
+ Additional child-specific parameters can be passed via ``kwargs``.
31
+ Either ``nbinsr`` or ``binsize`` has to be provided to fix the binning scheme .
32
+ """
33
+
34
+ def __init__(self, n_cfs, min_sep, max_sep, **kwargs):
35
+
36
+ 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)
37
+ self.nmax = self.nmaxs[0]
38
+ self.phi = self.phis[0]
39
+ self.projection = None
40
+ self.projections_avail = [None, "X", "Centroid"]
41
+ self.nbinsz = None
42
+ self.nzcombis = None
43
+
44
+ # (Add here any newly implemented projections)
45
+ self._initprojections(self)
46
+ self.project["X"]["Centroid"] = self._x2centroid
47
+
48
+ def saveinst(self, path_save, fname):
49
+
50
+ if not Path(path_save).is_dir():
51
+ raise ValueError('Path to directory does not exist.')
52
+
53
+ np.savez(path_save+fname,
54
+ nbinsz=self.nbinsz,
55
+ min_sep=self.min_sep,
56
+ max_sep=self.max_sep,
57
+ binsr=self.nbinsr,
58
+ nbinsphi=self.nbinsphi,
59
+ nmaxs=self.nmaxs,
60
+ method=self.method,
61
+ multicountcorr=self.multicountcorr,
62
+ shuffle_pix=self.shuffle_pix,
63
+ tree_resos=self.tree_resos,
64
+ rmin_pixsize=self.rmin_pixsize,
65
+ resoshift_leafs=self.resoshift_leafs,
66
+ minresoind_leaf=self.minresoind_leaf,
67
+ maxresoind_leaf=self.maxresoind_leaf,
68
+ nthreads=self.nthreads,
69
+ bin_centers=self.bin_centers,
70
+ npcf_multipoles=self.npcf_multipoles,
71
+ npcf_multipoles_norm=self.npcf_multipoles_norm)
72
+
73
+ def __process_patches(self, cat, dotomo=True, rotsignflip=False, apply_edge_correction=False, adjust_tree=False,
74
+ save_patchres=False, save_filebase="", keep_patchres=False):
75
+
76
+ if save_patchres:
77
+ if not Path(save_patchres).is_dir():
78
+ raise ValueError('Path to directory does not exist.')
79
+
80
+ for elp in range(cat.npatches):
81
+ if self._verbose_python:
82
+ print('Doing patch %i/%i'%(elp+1,cat.npatches))
83
+
84
+ # Compute statistics on patch
85
+ pcat = cat.frompatchind(elp,rotsignflip=rotsignflip)
86
+ pcorr = GGGCorrelation(
87
+ n_cfs=self.n_cfs,
88
+ min_sep=self.min_sep,
89
+ max_sep=self.max_sep,
90
+ nbinsr=self.nbinsr,
91
+ nbinsphi=self.nbinsphi,
92
+ nmaxs=self.nmaxs,
93
+ method=self.method,
94
+ multicountcorr=self.multicountcorr,
95
+ shuffle_pix=self.shuffle_pix,
96
+ tree_resos=self.tree_resos,
97
+ rmin_pixsize=self.rmin_pixsize,
98
+ resoshift_leafs=self.resoshift_leafs,
99
+ minresoind_leaf=self.minresoind_leaf,
100
+ maxresoind_leaf=self.maxresoind_leaf,
101
+ nthreads=self.nthreads,
102
+ verbosity=self.verbosity)
103
+ pcorr.process(pcat, dotomo=dotomo)
104
+
105
+ # Update the total measurement
106
+ if elp == 0:
107
+ self.nbinsz = pcorr.nbinsz
108
+ self.nzcombis = pcorr.nzcombis
109
+ self.bin_centers = np.zeros_like(pcorr.bin_centers)
110
+ self.npcf_multipoles = np.zeros_like(pcorr.npcf_multipoles)
111
+ self.npcf_multipoles_norm = np.zeros_like(pcorr.npcf_multipoles_norm)
112
+ _footnorm = np.zeros_like(pcorr.bin_centers)
113
+ if keep_patchres:
114
+ centers_patches = np.zeros((cat.npatches, *pcorr.bin_centers.shape), dtype=pcorr.bin_centers.dtype)
115
+ npcf_multipoles_patches = np.zeros((cat.npatches, *pcorr.npcf_multipoles.shape), dtype=pcorr.npcf_multipoles.dtype)
116
+ npcf_multipoles_norm_patches = np.zeros((cat.npatches, *pcorr.npcf_multipoles_norm.shape), dtype=pcorr.npcf_multipoles_norm.dtype)
117
+ _shelltriplets = np.array([[pcorr.npcf_multipoles_norm[0,z*self.nbinsz*self.nbinsz+z*self.nbinsz+z,i,i].real
118
+ 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
119
+ # Rough estimate of scaling of pair counts based on zeroth multipole of triplets. Note that we might get nans here due to numerical
120
+ # inaccuracies in the multiple counting corrections for bins with zero triplets, so we force those values to be zero.
121
+ _patchnorm = np.nan_to_num(np.sqrt(_shelltriplets))
122
+ self.bin_centers += _patchnorm*pcorr.bin_centers
123
+ _footnorm += _patchnorm
124
+ self.npcf_multipoles += pcorr.npcf_multipoles
125
+ self.npcf_multipoles_norm += pcorr.npcf_multipoles_norm
126
+ if keep_patchres:
127
+ centers_patches[elp] += pcorr.bin_centers
128
+ npcf_multipoles_patches[elp] += pcorr.npcf_multipoles
129
+ npcf_multipoles_norm_patches[elp] += pcorr.npcf_multipoles_norm
130
+ if save_patchres:
131
+ pcorr.saveinst(save_patchres, save_filebase+'_patch%i'%elp)
132
+
133
+ # Finalize the measurement on the full footprint
134
+ self.bin_centers = np.divide(self.bin_centers,_footnorm, out=np.zeros_like(self.bin_centers), where=_footnorm>0)
135
+ self.bin_centers_mean = np.mean(self.bin_centers,axis=0)
136
+ self.projection = "X"
137
+
138
+ if keep_patchres:
139
+ return centers_patches, npcf_multipoles_patches, npcf_multipoles_norm_patches
140
+
141
+
142
+ def process(self, cat, dotomo=True, rotsignflip=False, apply_edge_correction=False, adjust_tree=False,
143
+ save_patchres=False, save_filebase="", keep_patchres=False):
144
+ r"""
145
+ Compute a shear 3PCF provided a shape catalog
146
+
147
+ Parameters
148
+ ----------
149
+ cat: orpheus.SpinTracerCatalog
150
+ The shape catalog which is processed
151
+ dotomo: bool
152
+ Flag that decides whether the tomographic information in the shape catalog should be used. Defaults to `True`.
153
+ rotsignflip: bool
154
+ If the shape catalog is has been decomposed in patches, choose whether the rotation angle should be flipped.
155
+ For simulated data this was always ok to set to 'False`. Has no effect yet. Defaults to `False`.
156
+ apply_edge_correction: bool
157
+ Flag that decides how the NPCF in the real space basis is computed.
158
+ * If set to `True` the computation is done via edge-correcting the GGG-multipoles
159
+ * If set to `False` both GGG and NNN are transformed separately and the ratio is done in the real-space basis
160
+ Defaults to `False`.
161
+ adjust_tree: bool
162
+ Overrides the original setup of the tree-approximations in the instance based on the nbar of the shape catalog.
163
+ Not implemented yet, therefore no effect. Has no effect yet. Defaults to `False`
164
+ save_patchres: bool or str
165
+ If the shape catalog is has been decomposed in patches, flag whether to save the GGG measurements on the individual patches.
166
+ Note that the path needs to exist, otherwise a `ValueError` is raised. For a flat-sky catalog this parameter
167
+ has no effect. Defaults to `False`
168
+ save_filebase: str
169
+ Base of the filenames in which the patches are saved. The full filename will be `<save_patchres>/<save_filebase>_patchxx.npz`.
170
+ Only has an effect if the shape catalog consists of multiple patches and `save_patchres` is not `False`.
171
+ keep_patchres: bool
172
+ If the catalog consists of multiple patches, returns all measurements on the patches. Defaults to `False`.
173
+ """
174
+
175
+ # Make sure that in case the catalog is spherical, it has been decomposed into patches
176
+ if cat.geometry == 'spherical' and cat.patchinds is None:
177
+ raise ValueError('Error: Spherical catalog needs to be first decomposed into patches using the Catalog._topatches method.')
178
+
179
+ # Catalog consist of multiple patches
180
+ if cat.patchinds is not None:
181
+ return self.__process_patches(cat, dotomo=dotomo, rotsignflip=rotsignflip,
182
+ apply_edge_correction=apply_edge_correction, adjust_tree=adjust_tree,
183
+ save_patchres=save_patchres, save_filebase=save_filebase, keep_patchres=keep_patchres)
184
+
185
+ # Catalog does not consist of patches
186
+ else:
187
+ self._checkcats(cat, self.spins)
188
+ if not dotomo:
189
+ self.nbinsz = 1
190
+ old_zbins = cat.zbins[:]
191
+ cat.zbins = np.zeros(cat.ngal, dtype=np.int32)
192
+ self.nzcombis = 1
193
+ else:
194
+ self.nbinsz = cat.nbinsz
195
+ zbins = cat.zbins
196
+ self.nzcombis = self.nbinsz*self.nbinsz*self.nbinsz
197
+ if adjust_tree:
198
+ nbar = cat.ngal/(cat.len1*cat.len2)
199
+
200
+ sc = (4,self.nmax+1,self.nzcombis,self.nbinsr,self.nbinsr)
201
+ sn = (self.nmax+1,self.nzcombis,self.nbinsr,self.nbinsr)
202
+ szr = (self.nbinsz, self.nbinsr)
203
+ bin_centers = np.zeros(self.nbinsz*self.nbinsr).astype(np.float64)
204
+ threepcfs_n = np.zeros(4*(self.nmax+1)*self.nzcombis*self.nbinsr*self.nbinsr).astype(np.complex128)
205
+ threepcfsnorm_n = np.zeros((self.nmax+1)*self.nzcombis*self.nbinsr*self.nbinsr).astype(np.complex128)
206
+ args_basecat = (cat.isinner.astype(np.float64), cat.weight, cat.pos1, cat.pos2, cat.tracer_1, cat.tracer_2,
207
+ cat.zbins.astype(np.int32), np.int32(self.nbinsz), np.int32(cat.ngal), )
208
+ args_basesetup = (np.int32(0), np.int32(self.nmax), np.float64(self.min_sep),
209
+ np.float64(self.max_sep), np.array([-1.]).astype(np.float64),
210
+ np.int32(self.nbinsr), np.int32(self.multicountcorr), )
211
+ if self.method=="Discrete":
212
+ if not cat.hasspatialhash:
213
+ cat.build_spatialhash(dpix=max(1.,self.max_sep//10.))
214
+ args_pixgrid = (np.float64(cat.pix1_start), np.float64(cat.pix1_d), np.int32(cat.pix1_n),
215
+ np.float64(cat.pix2_start), np.float64(cat.pix2_d), np.int32(cat.pix2_n), )
216
+ args = (*args_basecat,
217
+ *args_basesetup,
218
+ cat.index_matcher,
219
+ cat.pixs_galind_bounds,
220
+ cat.pix_gals,
221
+ *args_pixgrid,
222
+ np.int32(self.nthreads),
223
+ np.int32(self._verbose_c),
224
+ bin_centers,
225
+ threepcfs_n,
226
+ threepcfsnorm_n)
227
+ func = self.clib.alloc_Gammans_discrete_ggg
228
+ elif self.method in ["Tree", "BaseTree", "DoubleTree"]:
229
+ if self._verbose_debug:
230
+ print("Doing multihash")
231
+ cutfirst = np.int32(self.tree_resos[0]==0.)
232
+ mhash = cat.multihash(dpixs=self.tree_resos[cutfirst:], dpix_hash=self.tree_resos[-1],
233
+ shuffle=self.shuffle_pix, w2field=True, normed=True)
234
+ ngal_resos, pos1s, pos2s, weights, zbins, isinners, allfields, index_matchers, pixs_galind_bounds, pix_gals, dpixs1_true, dpixs2_true = mhash
235
+ weight_resos = np.concatenate(weights).astype(np.float64)
236
+ pos1_resos = np.concatenate(pos1s).astype(np.float64)
237
+ pos2_resos = np.concatenate(pos2s).astype(np.float64)
238
+ zbin_resos = np.concatenate(zbins).astype(np.int32)
239
+ isinner_resos = np.concatenate(isinners).astype(np.float64)
240
+ e1_resos = np.concatenate([allfields[i][0] for i in range(len(allfields))]).astype(np.float64)
241
+ e2_resos = np.concatenate([allfields[i][1] for i in range(len(allfields))]).astype(np.float64)
242
+ _weightsq_resos = np.concatenate([allfields[i][2] for i in range(len(allfields))]).astype(np.float64)
243
+ weightsq_resos = _weightsq_resos*weight_resos # As in reduce we renorm all the fields --> need to `unrenorm'
244
+ index_matcher = np.concatenate(index_matchers).astype(np.int32)
245
+ pixs_galind_bounds = np.concatenate(pixs_galind_bounds).astype(np.int32)
246
+ pix_gals = np.concatenate(pix_gals).astype(np.int32)
247
+ args_pixgrid = (np.float64(cat.pix1_start), np.float64(cat.pix1_d), np.int32(cat.pix1_n),
248
+ np.float64(cat.pix2_start), np.float64(cat.pix2_d), np.int32(cat.pix2_n), )
249
+ args_resos = (weight_resos, pos1_resos, pos2_resos, e1_resos, e2_resos, zbin_resos, weightsq_resos,
250
+ index_matcher, pixs_galind_bounds, pix_gals, )
251
+ args_output = (bin_centers, threepcfs_n, threepcfsnorm_n, )
252
+ if self._verbose_debug:
253
+ print("Doing %s"%self.method)
254
+ if self.method=="Tree":
255
+ args = (*args_basecat,
256
+ np.int32(self.tree_nresos),
257
+ self.tree_redges,
258
+ np.array(ngal_resos, dtype=np.int32),
259
+ *args_resos,
260
+ *args_pixgrid,
261
+ *args_basesetup,
262
+ np.int32(self.nthreads),
263
+ np.int32(self._verbose_c),
264
+ *args_output)
265
+ func = self.clib.alloc_Gammans_tree_ggg
266
+ if self.method in ["BaseTree", "DoubleTree"]:
267
+ args_resos = (isinner_resos, ) + args_resos
268
+ index_matcher_flat = np.argwhere(cat.index_matcher>-1).flatten()
269
+ nregions = len(index_matcher_flat)
270
+ # Select regions with at least one inner galaxy (TODO: Optimize)
271
+ filledregions = []
272
+ for elregion in range(nregions):
273
+ _ = cat.pix_gals[cat.pixs_galind_bounds[elregion]:cat.pixs_galind_bounds[elregion+1]]
274
+ if np.sum(cat.isinner[_])>0:filledregions.append(elregion)
275
+ filledregions = np.asarray(filledregions, dtype=np.int32)
276
+ nfilledregions = np.int32(len(filledregions))
277
+ args_regions = (index_matcher_flat.astype(np.int32), np.int32(nregions), filledregions, nfilledregions, )
278
+ args_basesetup_dtree = (np.int32(self.nmax), np.float64(self.min_sep), np.float64(self.max_sep),
279
+ np.int32(self.nbinsr), np.int32(self.multicountcorr), )
280
+ args_treeresos = (np.int32(self.tree_nresos), np.int32(self.tree_nresos-cutfirst),
281
+ dpixs1_true.astype(np.float64), dpixs2_true.astype(np.float64), self.tree_redges, )
282
+ if self.method=="BaseTree":
283
+ func = self.clib.alloc_Gammans_basetree_ggg
284
+ if self.method=="DoubleTree":
285
+ args_leafs = (np.int32(self.resoshift_leafs), np.int32(self.minresoind_leaf),
286
+ np.int32(self.maxresoind_leaf), )
287
+ args_treeresos = args_treeresos + args_leafs
288
+ func = self.clib.alloc_Gammans_doubletree_ggg
289
+ args = (*args_treeresos,
290
+ np.array(ngal_resos, dtype=np.int32),
291
+ np.int32(self.nbinsz),
292
+ *args_resos,
293
+ *args_pixgrid,
294
+ *args_regions,
295
+ *args_basesetup_dtree,
296
+ np.int32(self.nthreads),
297
+ np.int32(self._verbose_c),
298
+ *args_output)
299
+ func(*args)
300
+
301
+ self.bin_centers = bin_centers.reshape(szr)
302
+ self.bin_centers_mean = np.mean(self.bin_centers, axis=0)
303
+ self.npcf_multipoles = threepcfs_n.reshape(sc)
304
+ self.npcf_multipoles_norm = threepcfsnorm_n.reshape(sn)
305
+ self.projection = "X"
306
+
307
+ if apply_edge_correction:
308
+ self.edge_correction()
309
+
310
+ if not dotomo:
311
+ cat.zbins = old_zbins
312
+
313
+
314
+ def edge_correction(self, ret_matrices=False):
315
+
316
+ def gen_M_matrix(thet1,thet2,threepcf_n_norm):
317
+ nvals, ntheta, _ = threepcf_n_norm.shape
318
+ nmax = (nvals-1)//2
319
+ narr = np.arange(-nmax,nmax+1, dtype=np.int)
320
+ nextM = np.zeros((nvals,nvals))
321
+ for ind, ell in enumerate(narr):
322
+ lminusn = ell-narr
323
+ sel = np.logical_and(lminusn+nmax>=0, lminusn+nmax<nvals)
324
+ nextM[ind,sel] = threepcf_n_norm[(lminusn+nmax)[sel],thet1,thet2].real / threepcf_n_norm[nmax,thet1,thet2].real
325
+ return nextM
326
+
327
+ nvals, nzcombis, ntheta, _ = self.npcf_multipoles_norm.shape
328
+ nmax = nvals-1
329
+ threepcf_n_full = np.zeros((4,2*nmax+1, nzcombis, ntheta, ntheta), dtype=complex)
330
+ threepcf_n_norm_full = np.zeros((2*nmax+1, nzcombis, ntheta, ntheta), dtype=complex)
331
+ threepcf_n_corr = np.zeros(threepcf_n_full.shape, dtype=np.complex)
332
+ threepcf_n_full[:,nmax:] = self.npcf_multipoles
333
+ threepcf_n_norm_full[nmax:] = self.npcf_multipoles_norm
334
+ for nextn in range(1,nvals):
335
+ threepcf_n_full[0,nmax-nextn] = self.npcf_multipoles[0,nextn].transpose(0,2,1)
336
+ threepcf_n_full[1,nmax-nextn] = self.npcf_multipoles[1,nextn].transpose(0,2,1)
337
+ threepcf_n_full[2,nmax-nextn] = self.npcf_multipoles[3,nextn].transpose(0,2,1)
338
+ threepcf_n_full[3,nmax-nextn] = self.npcf_multipoles[2,nextn].transpose(0,2,1)
339
+ threepcf_n_norm_full[nmax-nextn] = self.npcf_multipoles_norm[nextn].transpose(0,2,1)
340
+
341
+ if ret_matrices:
342
+ mats = np.zeros((nzcombis,ntheta,ntheta,nvals,nvals))
343
+ for indz in range(nzcombis):
344
+ #sys.stdout.write("%i"%indz)
345
+ for thet1 in range(ntheta):
346
+ for thet2 in range(ntheta):
347
+ nextM = gen_M_matrix(thet1,thet2,threepcf_n_norm_full[:,indz])
348
+ nextM_inv = np.linalg.inv(nextM)
349
+ if ret_matrices:
350
+ mats[indz,thet1,thet2] = nextM
351
+ for i in range(4):
352
+ threepcf_n_corr[i,:,indz,thet1,thet2] = np.matmul(nextM_inv,threepcf_n_full[i,:,indz,thet1,thet2])
353
+
354
+ self.npcf_multipoles = threepcf_n_corr[:,nmax:]
355
+ self.is_edge_corrected = True
356
+
357
+ if ret_matrices:
358
+ return threepcf_n_corr[:,nmax:], mats
359
+
360
+ # Legacy transform in pure python -- now upgraded to .c
361
+ def _multipoles2npcf_py(self):
362
+
363
+ _, nzcombis, rbins, rbins = np.shape(self.npcf_multipoles[0])
364
+ self.npcf = np.zeros((4, nzcombis, rbins, rbins, len(self.phi)), dtype=complex)
365
+ self.npcf_norm = np.zeros((nzcombis, rbins, rbins, len(self.phi)), dtype=complex)
366
+ ztiler = np.arange(self.nbinsz*self.nbinsz*self.nbinsz).reshape(
367
+ (self.nbinsz,self.nbinsz,self.nbinsz)).transpose(0,2,1).flatten().astype(np.int32)
368
+
369
+ # 3PCF components
370
+ conjmap = [0,1,3,2]
371
+ for elm in range(4):
372
+ for elphi, phi in enumerate(self.phi):
373
+ N0 = 1./(2*np.pi) * self.npcf_multipoles_norm[0].astype(complex)
374
+ tmp = 1./(2*np.pi) * self.npcf_multipoles[elm,0].astype(complex)
375
+ for n in range(1,self.nmax+1):
376
+ _const = 1./(2*np.pi) * np.exp(1J*n*phi)
377
+ tmp += _const * self.npcf_multipoles[elm,n].astype(complex)
378
+ tmp += _const.conj() * self.npcf_multipoles[conjmap[elm],n][ztiler].astype(complex).transpose(0,2,1)
379
+ self.npcf[elm,...,elphi] = tmp
380
+ # Number of triangles
381
+ for elphi, phi in enumerate(self.phi):
382
+ tmptotnorm = 1./(2*np.pi) * self.npcf_multipoles_norm[0].astype(complex)
383
+ for n in range(1,self.nmax+1):
384
+ _const = 1./(2*np.pi) * np.exp(1J*n*phi)
385
+ tmptotnorm += _const * self.npcf_multipoles_norm[n].astype(complex)
386
+ tmptotnorm += _const.conj() * self.npcf_multipoles_norm[n][ztiler].astype(complex).transpose(0,2,1)
387
+ self.npcf_norm[...,elphi] = tmptotnorm
388
+
389
+ if self.is_edge_corrected:
390
+ dphi = self.phi[1] - self.phi[0]
391
+ N0 = dphi/(2*np.pi) * self.npcf_multipoles_norm[self.nmax].astype(complex)
392
+ sel_zero = np.isnan(N0)
393
+ _a = self.npcf
394
+ _b = N0.real[np.newaxis, :, :, :, np.newaxis]
395
+ self.npcf = np.divide(_a, _b, out=np.zeros_like(_a), where=_b>0)
396
+ else:
397
+ _a = self.npcf
398
+ _b = self.npcf_norm
399
+ self.npcf = np.divide(_a, _b, out=np.zeros_like(_a), where=_b>0)
400
+ self.projection = "X"
401
+
402
+ def multipoles2npcf(self, projection='Centroid'):
403
+ r"""
404
+ Notes
405
+ -----
406
+ 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.
407
+ """
408
+ assert(projection in self.projections_avail)
409
+ int_projection = {'X':0,'Centroid':1}
410
+ _, nzcombis, rbins, rbins = np.shape(self.npcf_multipoles[0])
411
+ thisnpcf = np.zeros(4*self.nbinsz*self.nbinsz*self.nbinsz*self.nbinsr*self.nbinsr*len(self.phi), dtype=np.complex128)
412
+ thisnpcf_norm = np.zeros(self.nbinsz*self.nbinsz*self.nbinsz*self.nbinsr*self.nbinsr*len(self.phi), dtype=np.complex128)
413
+ self.clib.multipoles2npcf_ggg(
414
+ self.npcf_multipoles.flatten(), self.npcf_multipoles_norm.flatten(), np.int32(self.nmax), np.int32(self.nbinsz),
415
+ self.bin_centers_mean, np.int32(self.nbinsr), self.phi.astype(np.float64), np.int32(self.nbinsphi[0]),
416
+ np.int32(int_projection[projection]), np.int32(self.nthreads), thisnpcf, thisnpcf_norm)
417
+ self.npcf = thisnpcf.reshape((4,nzcombis,self.nbinsr,self.nbinsr,len(self.phi)))
418
+ self.npcf_norm = thisnpcf_norm.reshape((nzcombis,self.nbinsr,self.nbinsr,len(self.phi)))
419
+ self.projection = projection
420
+
421
+ ## PROJECTIONS (Preferably use direct in c-level) ##
422
+ def projectnpcf(self, projection):
423
+ super()._projectnpcf(self, projection)
424
+
425
+ def _x2centroid(self):
426
+ gammas_cen = np.zeros_like(self.npcf)
427
+ pimod = lambda x: x%(2*np.pi) - 2*np.pi*(x%(2*np.pi)>=np.pi)
428
+ npcf_cen = np.zeros(self.npcf.shape, dtype=complex)
429
+ _centers = np.mean(self.bin_centers, axis=0)
430
+ for elb1, bin1 in enumerate(_centers):
431
+ for elb2, bin2 in enumerate(_centers):
432
+ bin3 = np.sqrt(bin1**2 + bin2**2 - 2*bin1*bin2*np.cos(self.phi))
433
+ phiexp = np.exp(1J*self.phi)
434
+ phiexp_c = np.exp(-1J*self.phi)
435
+ prod1 = (bin1 + bin2*phiexp_c)/(bin1 + bin2*phiexp) #q1
436
+ prod2 = (2*bin1 - bin2*phiexp_c)/(2*bin1 - bin2*phiexp) #q2
437
+ prod3 = (2*bin2*phiexp_c - bin1)/(2*bin2*phiexp - bin1) #q3
438
+ prod1_inv = prod1.conj()/np.abs(prod1)
439
+ prod2_inv = prod2.conj()/np.abs(prod2)
440
+ prod3_inv = prod3.conj()/np.abs(prod3)
441
+ rot_nom = np.zeros((4,len(self.phi)))
442
+ rot_nom[0] = pimod(np.angle(prod1*prod2*prod3*np.exp(3*1J*self.phi)))
443
+ rot_nom[1] = pimod(np.angle(prod1_inv*prod2*prod3*np.exp(1J*self.phi)))
444
+ rot_nom[2] = pimod(np.angle(prod1*prod2_inv*prod3*np.exp(3*1J*self.phi)))
445
+ rot_nom[3] = pimod(np.angle(prod1*prod2*prod3_inv*np.exp(-1J*self.phi)))
446
+ gammas_cen[:,:,elb1,elb2] = self.npcf[:,:,elb1,elb2]*np.exp(1j*rot_nom)[:,np.newaxis,:]
447
+ return gammas_cen
448
+
449
+ def computeMap3(self, radii, do_multiscale=False, tofile=False, filtercache=None):
450
+ """
451
+ Compute third-order aperture statistics using the polynomial filter.
452
+ """
453
+
454
+ if self.npcf is None and self.npcf_multipoles is not None:
455
+ self.multipoles2npcf(projection='Centroid')
456
+
457
+ if self.projection != "Centroid":
458
+ self.projectnpcf("Centroid")
459
+
460
+ nradii = len(radii)
461
+ if not do_multiscale:
462
+ nrcombis = nradii
463
+ filterfunc = self._map3_filtergrid_singleR
464
+ _rcut = 1
465
+ else:
466
+ nrcombis = nradii*nradii*nradii
467
+ filterfunc = self._map3_filtergrid_multiR
468
+ _rcut = nradii
469
+ map3s = np.zeros((8, self.nzcombis, nrcombis), dtype=complex)
470
+ M3 = np.zeros((self.nzcombis, nrcombis), dtype=complex)
471
+ M2M1 = np.zeros((self.nzcombis, nrcombis), dtype=complex)
472
+ M2M2 = np.zeros((self.nzcombis, nrcombis), dtype=complex)
473
+ M2M3 = np.zeros((self.nzcombis, nrcombis), dtype=complex)
474
+ tmprcombi = 0
475
+
476
+ for elr1, R1 in enumerate(radii):
477
+ for elr2, R2 in enumerate(radii[:_rcut]):
478
+ for elr3, R3 in enumerate(radii[:_rcut]):
479
+ if not do_multiscale:
480
+ R2 = R1
481
+ R3 = R1
482
+ if filtercache is not None:
483
+ T0, T3_123, T3_231, T3_312 = filtercache[tmprcombi][0], filtercache[tmprcombi][1], filtercache[tmprcombi][2], filtercache[tmprcombi][3]
484
+ else:
485
+ T0, T3_123, T3_231, T3_312 = filterfunc(R1, R2, R3)
486
+ M3[:,tmprcombi] = np.nansum(T0*self.npcf[0,...],axis=(1,2,3))
487
+ M2M1[:,tmprcombi] = np.nansum(T3_123*self.npcf[1,...],axis=(1,2,3))
488
+ M2M2[:,tmprcombi] = np.nansum(T3_231*self.npcf[2,...],axis=(1,2,3))
489
+ M2M3[:,tmprcombi] = np.nansum(T3_312*self.npcf[3,...],axis=(1,2,3))
490
+ tmprcombi += 1
491
+ map3s[0] = 1./4. * (+M2M1+M2M2+M2M3 + M3).real # MapMapMap
492
+ map3s[1] = 1./4. * (+M2M1+M2M2-M2M3 + M3).imag # MapMapMx
493
+ map3s[2] = 1./4. * (+M2M1-M2M2+M2M3 + M3).imag # MapMxMap
494
+ map3s[3] = 1./4. * (-M2M1+M2M2+M2M3 + M3).imag # MxMapMap
495
+ map3s[4] = 1./4. * (-M2M1+M2M2+M2M3 - M3).real # MapMxMx
496
+ map3s[5] = 1./4. * (+M2M1-M2M2+M2M3 - M3).real # MxMapMx
497
+ map3s[6] = 1./4. * (+M2M1+M2M2-M2M3 - M3).real # MxMxMap
498
+ map3s[7] = 1./4. * (+M2M1+M2M2+M2M3 - M3).imag # MxMxMx
499
+
500
+ if tofile:
501
+ # Write to file
502
+ pass
503
+
504
+ return map3s
505
+
506
+ def _map3_filtergrid_singleR(self, R1, R2, R3):
507
+ return self.__map3_filtergrid_singleR(R1, R2, R3, self.bin_edges, self.bin_centers_mean, self.phi)
508
+
509
+ @staticmethod
510
+ @jit(nopython=True)
511
+ def __map3_filtergrid_singleR(R1, R2, R3, normys_edges, normys_centers, phis):
512
+
513
+ # To avoid zero divisions we set some default bin centers for the evaluation of the filter
514
+ # As for those positions the 3pcf is zero those will not contribute to the map3 integral
515
+ if (np.min(normys_centers)==0):
516
+ _sel = normys_centers!=0
517
+ _avratios = np.mean(normys_centers[_sel]/normys_edges[_sel])
518
+ normys_centers[~_sel] = _avratios*normys_edges[~_sel]
519
+
520
+ R_ap = R1
521
+ nbinsr = len(normys_centers)
522
+ nbinsphi = len(phis)
523
+ _cphis = np.cos(phis)
524
+ _c2phis = np.cos(2*phis)
525
+ _sphis = np.sin(phis)
526
+ _ephis = np.e**(1J*phis)
527
+ _ephisc = np.e**(-1J*phis)
528
+ _e2phis = np.e**(2J*phis)
529
+ _e2phisc = np.e**(-2J*phis)
530
+ T0 = np.zeros((nbinsr, nbinsr, nbinsphi), dtype=nb_complex128)
531
+ T3_123 = np.zeros((nbinsr, nbinsr, nbinsphi), dtype=nb_complex128)
532
+ T3_231 = np.zeros((nbinsr, nbinsr, nbinsphi), dtype=nb_complex128)
533
+ T3_312 = np.zeros((nbinsr, nbinsr, nbinsphi), dtype=nb_complex128)
534
+ for elb1 in range(nbinsr):
535
+ _y1 = normys_centers[elb1]
536
+ _dbin1 = normys_edges[elb1+1] - normys_edges[elb1]
537
+ for elb2 in range(nbinsr):
538
+ _y2 = normys_centers[elb2]
539
+ _y14 = _y1**4
540
+ _y13y2 = _y1**3*_y2
541
+ _y12y22 = _y1**2*_y2**2
542
+ _y1y23 = _y1*_y2**3
543
+ _y24 = _y2**4
544
+ _dbin2 = normys_edges[elb2+1] - normys_edges[elb2]
545
+ _dbinphi = phis[1] - phis[0]
546
+ _absq1s = 1./9.*(4*_y1**2 - 4*_y1*_y2*_cphis + 1*_y2**2)
547
+ _absq2s = 1./9.*(1*_y1**2 - 4*_y1*_y2*_cphis + 4*_y2**2)
548
+ _absq3s = 1./9.*(1*_y1**2 + 2*_y1*_y2*_cphis + 1*_y2**2)
549
+ _absq123s = 2./3. * (_y1**2+_y2**2-_y1*_y2*_cphis)
550
+ _absq1q2q3_2 = _absq1s*_absq2s*_absq3s
551
+ _measures = _y1*_dbin1/R_ap**2 * _y2*_dbin2/R_ap**2 * _dbinphi/(2*np.pi)
552
+ nextT0 = _absq1q2q3_2/R_ap**6 * np.e**(-_absq123s/(2*R_ap**2))
553
+ T0[elb1,elb2] = 1./24. * _measures * nextT0
554
+ _tmp1 = _y1**4 + _y2**4 + _y1**2*_y2**2 * (2*np.cos(2*phis)-5.)
555
+ _tmp2 = (_y1**2+_y2**2)*_cphis + 9J*(_y1**2-_y2**2)*_sphis
556
+ q1q2q3starsq = -1./81*(2*_tmp1 - _y1*_y2*_tmp2)
557
+ nextT3_123 = np.e**(-_absq123s/(2*R_ap**2)) * (1./24*_absq1q2q3_2/R_ap**6 -
558
+ 1./9.*q1q2q3starsq/R_ap**4 +
559
+ 1./27*(q1q2q3starsq**2/(_absq1q2q3_2*R_ap**2) +
560
+ 2*q1q2q3starsq/(_absq3s*R_ap**2)))
561
+ _231inner = -4*_y14 + 2*_y24 + _y13y2*8*_cphis + _y12y22*(8*_e2phis-4-_e2phisc) + _y1y23*(_ephisc-8*_ephis)
562
+ q2q3q1starsq = -1./81*(_231inner)
563
+ nextT3_231 = np.e**(-_absq123s/(2*R_ap**2)) * (1./24*_absq1q2q3_2/R_ap**6 -
564
+ 1./9.*q2q3q1starsq/R_ap**4 +
565
+ 1./27*(q2q3q1starsq**2/(_absq1q2q3_2*R_ap**2) +
566
+ 2*q2q3q1starsq/(_absq1s*R_ap**2)))
567
+ _312inner = 2*_y14 - 4*_y24 - _y13y2*(8*_ephisc-_ephis) - _y12y22*(4+_e2phis-8*_e2phisc) + 8*_y1y23*_cphis
568
+ q3q1q2starsq = -1./81*(_312inner)
569
+ nextT3_312 = np.e**(-_absq123s/(2*R_ap**2)) * (1./24*_absq1q2q3_2/R_ap**6 -
570
+ 1./9.*q3q1q2starsq/R_ap**4 +
571
+ 1./27*(q3q1q2starsq**2/(_absq1q2q3_2*R_ap**2) +
572
+ 2*q3q1q2starsq/(_absq2s*R_ap**2)))
573
+ T3_123[elb1,elb2] = _measures * nextT3_123
574
+ T3_231[elb1,elb2] = _measures * nextT3_231
575
+ T3_312[elb1,elb2] = _measures * nextT3_312
576
+
577
+ return T0, T3_123, T3_231, T3_312
578
+
579
+ def _map3_filtergrid_multiR(self, R1, R2, R3):
580
+ return self.__map3_filtergrid_multiR(R1, R2, R3, self.bin_edges, self.bin_centers_mean, self.phi, include_measure=True)
581
+
582
+ @staticmethod
583
+ @jit(nopython=True)
584
+ def __map3_filtergrid_multiR(R1, R2, R3, normys_edges, normys_centers, phis, include_measure=True):
585
+
586
+ # To avoid zero divisions we set some default bin centers for the evaluation of the filter
587
+ # As for those positions the 3pcf is zero those will not contribute to the map3 integral
588
+ if (np.min(normys_centers)==0):
589
+ _sel = normys_centers!=0
590
+ _avratios = np.mean(normys_centers[_sel]/normys_edges[_sel])
591
+ normys_centers[~_sel] = _avratios*normys_edges[~_sel]
592
+
593
+ nbinsr = len(normys_centers)
594
+ nbinsphi = len(phis)
595
+ _cphis = np.cos(phis)
596
+ _c2phis = np.cos(2*phis)
597
+ _sphis = np.sin(phis)
598
+ _ephis = np.e**(1J*phis)
599
+ _ephisc = np.e**(-1J*phis)
600
+ _e2phis = np.e**(2J*phis)
601
+ _e2phisc = np.e**(-2J*phis)
602
+ T0 = np.zeros((nbinsr, nbinsr, nbinsphi), dtype=nb_complex128)
603
+ T3_123 = np.zeros((nbinsr, nbinsr, nbinsphi), dtype=nb_complex128)
604
+ T3_231 = np.zeros((nbinsr, nbinsr, nbinsphi), dtype=nb_complex128)
605
+ T3_312 = np.zeros((nbinsr, nbinsr, nbinsphi), dtype=nb_complex128)
606
+ for elb1 in range(nbinsr):
607
+ _y1 = normys_centers[elb1]
608
+ _dbin1 = normys_edges[elb1+1] - normys_edges[elb1]
609
+ for elb2 in range(nbinsr):
610
+ Theta2 = np.sqrt((R1**2*R2**2 + R1**2*R3**2 + R2**2*R3**2)/3)
611
+ S = R1**2*R2**2*R3**2/Theta2**3
612
+
613
+ _y2 = normys_centers[elb2]
614
+ _y14 = _y1**4
615
+ _y13y2 = _y1**3*_y2
616
+ _y12y22 = _y1**2*_y2**2
617
+ _y1y23 = _y1*_y2**3
618
+ _y24 = _y2**4
619
+ _dbin2 = normys_edges[elb2+1] - normys_edges[elb2]
620
+ _dbinphi = phis[1] - phis[0]
621
+ _absq1s = 1./9.*(4*_y1**2 - 4*_y1*_y2*_cphis + 1*_y2**2)
622
+ _absq2s = 1./9.*(1*_y1**2 - 4*_y1*_y2*_cphis + 4*_y2**2)
623
+ _absq3s = 1./9.*(1*_y1**2 + 2*_y1*_y2*_cphis + 1*_y2**2)
624
+ _absq123s = 2./3. * (_y1**2+_y2**2-_y1*_y2*_cphis)
625
+ _absq1q2q3_2 = _absq1s*_absq2s*_absq3s
626
+
627
+ 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)
628
+ _frac231c = 1./3.*_y2*(2*_y1*_ephis-_y2)/_absq1s
629
+ _frac312c = 1./3.*_y1*(_y1-2*_y2*_ephisc)/_absq2s
630
+ _frac123c = 1./3.*(_y2**2-_y1**2+2J*_y1*_y2*_sphis)/_absq3s
631
+ f1 = (R2**2+R3**2)/(2*Theta2) + _frac231c * (R2**2-R3**2)/(6*Theta2)
632
+ f2 = (R1**2+R3**2)/(2*Theta2) + _frac312c * (R3**2-R1**2)/(6*Theta2)
633
+ f3 = (R1**2+R2**2)/(2*Theta2) + _frac123c * (R1**2-R2**2)/(6*Theta2)
634
+ f1c = f1.conj()
635
+ f2c = f2.conj()
636
+ f3c = f3.conj()
637
+ g1c = (R2**2*R3**2/Theta2**2 + R1**2*(R3**2-R2**2)/(3*Theta2**2)*_frac231c).conj()
638
+ g2c = (R3**2*R1**2/Theta2**2 + R2**2*(R1**2-R3**2)/(3*Theta2**2)*_frac312c).conj()
639
+ g3c = (R1**2*R2**2/Theta2**2 + R3**2*(R2**2-R1**2)/(3*Theta2**2)*_frac123c).conj()
640
+ _measures = _y1*_dbin1/Theta2 * _y2*_dbin2/Theta2 * _dbinphi/(2*np.pi)
641
+ if not include_measure:
642
+ _measures/=_measures
643
+ nextT0 = _absq1q2q3_2/Theta2**3 * f1c**2*f2c**2*f3c**2 * np.e**(-Z)
644
+ T0[elb1,elb2] = S/24. * _measures * nextT0
645
+
646
+ _tmp1 = _y1**4 + _y2**4 + _y1**2*_y2**2 * (2*np.cos(2*phis)-5.)
647
+ _tmp2 = (_y1**2+_y2**2)*_cphis + 9J*(_y1**2-_y2**2)*_sphis
648
+ q1q2q3starsq = -1./81*(2*_tmp1 - _y1*_y2*_tmp2)
649
+ nextT3_123 = np.e**(-Z) * (1./24*_absq1q2q3_2/Theta2**3 * f1c**2*f2c**2*f3**2 -
650
+ 1./9.*q1q2q3starsq/Theta2**2 * f1c*f2c*f3*g3c +
651
+ 1./27*(q1q2q3starsq**2/(_absq1q2q3_2*Theta2) * g3c**2 +
652
+ 2*R1**2*R2**2/Theta2**2 * q1q2q3starsq/(_absq3s*Theta2) * f1c*f2c))
653
+ _231inner = -4*_y14 + 2*_y24 + _y13y2*8*_cphis + _y12y22*(8*_e2phis-4-_e2phisc) + _y1y23*(_ephisc-8*_ephis)
654
+ q2q3q1starsq = -1./81*(_231inner)
655
+ nextT3_231 = np.e**(-Z) * (1./24*_absq1q2q3_2/Theta2**3 * f2c**2*f3c**2*f1**2 -
656
+ 1./9.*q2q3q1starsq/Theta2**2 * f2c*f3c*f1*g1c +
657
+ 1./27*(q2q3q1starsq**2/(_absq1q2q3_2*Theta2) * g1c**2 +
658
+ 2*R2**2*R3**2/Theta2**2 * q2q3q1starsq/(_absq1s*Theta2) * f2c*f3c))
659
+ _312inner = 2*_y14 - 4*_y24 - _y13y2*(8*_ephisc-_ephis) - _y12y22*(4+_e2phis-8*_e2phisc) + 8*_y1y23*_cphis
660
+ q3q1q2starsq = -1./81*(_312inner)
661
+ nextT3_312 = np.e**(-Z) * (1./24*_absq1q2q3_2/Theta2**3 * f3c**2*f1c**2*f2**2 -
662
+ 1./9.*q3q1q2starsq/Theta2**2 * f3c*f1c*f2*g2c +
663
+ 1./27*(q3q1q2starsq**2/(_absq1q2q3_2*Theta2) * g2c**2 +
664
+ 2*R3**2*R1**2/Theta2**2 * q3q1q2starsq/(_absq2s*Theta2) * f3c*f1c))
665
+
666
+ T3_123[elb1,elb2] = S * _measures * nextT3_123
667
+ T3_231[elb1,elb2] = S * _measures * nextT3_231
668
+ T3_312[elb1,elb2] = S * _measures * nextT3_312
669
+
670
+ return T0, T3_123, T3_231, T3_312
671
+
672
+
673
+ class GNNCorrelation(BinnedNPCF):
674
+ r""" Class containing methods to measure and and obtain statistics that are built
675
+ from third-order source-lens-lens (G3L) correlation functions.
676
+
677
+ Attributes
678
+ ----------
679
+ min_sep: float
680
+ The smallest distance of each vertex for which the NPCF is computed.
681
+ max_sep: float
682
+ The largest distance of each vertex for which the NPCF is computed.
683
+ zweighting: bool
684
+ Has no effect at the moment
685
+ zweighting_sigma: bool
686
+ Has not effect at the moment
687
+
688
+ Notes
689
+ -----
690
+ Inherits all other parameters and attributes from :class:`BinnedNPCF`.
691
+ Additional child-specific parameters can be passed via ``kwargs``.
692
+ Either ``nbinsr`` or ``binsize`` has to be provided to fix the binning scheme .
693
+ """
694
+
695
+ def __init__(self, min_sep, max_sep, zweighting=False, zweighting_sigma=None, **kwargs):
696
+ super().__init__(3, [2,0,0], n_cfs=1, min_sep=min_sep, max_sep=max_sep, **kwargs)
697
+ self.nmax = self.nmaxs[0]
698
+ self.phi = self.phis[0]
699
+ self.projection = None
700
+ self.projections_avail = [None, "X"]
701
+ self.nbinsz_source = None
702
+ self.nbinsz_lens = None
703
+
704
+ assert(zweighting in [True, False])
705
+ self.zweighting = zweighting
706
+ self.zweighting_sigma = zweighting_sigma
707
+ if not self.zweighting :
708
+ self.zweighting_sigma = None
709
+ else:
710
+ assert(isinstance(self.zweighting_sigma, float))
711
+
712
+ # (Add here any newly implemented projections)
713
+ self._initprojections(self)
714
+
715
+
716
+ def __process_patches(self, cat_source, cat_lens, dotomo_source=True, dotomo_lens=True, rotsignflip=False,
717
+ apply_edge_correction=False, save_patchres=False, save_filebase="", keep_patchres=False):
718
+ if save_patchres:
719
+ if not Path(save_patchres).is_dir():
720
+ raise ValueError('Path to directory does not exist.')
721
+
722
+ for elp in range(cat_source.npatches):
723
+ if self._verbose_python:
724
+ print('Doing patch %i/%i'%(elp+1,cat_source.npatches))
725
+ # Compute statistics on patch
726
+ pscat = cat_source.frompatchind(elp,rotsignflip=rotsignflip)
727
+ plcat = cat_lens.frompatchind(elp)
728
+ pcorr = GNNCorrelation(
729
+ min_sep=self.min_sep,
730
+ max_sep=self.max_sep,
731
+ nbinsr=self.nbinsr,
732
+ nbinsphi=self.nbinsphi,
733
+ nmaxs=self.nmaxs,
734
+ method=self.method,
735
+ multicountcorr=self.multicountcorr,
736
+ shuffle_pix=self.shuffle_pix,
737
+ tree_resos=self.tree_resos,
738
+ rmin_pixsize=self.rmin_pixsize,
739
+ resoshift_leafs=self.resoshift_leafs,
740
+ minresoind_leaf=self.minresoind_leaf,
741
+ maxresoind_leaf=self.maxresoind_leaf,
742
+ nthreads=self.nthreads,
743
+ verbosity=self.verbosity)
744
+ pcorr.process(pscat, plcat, dotomo_source=dotomo_source, dotomo_lens=dotomo_lens)
745
+
746
+ # Update the total measurement
747
+ if elp == 0:
748
+ self.nbinsz_source = pcorr.nbinsz_source
749
+ self.nbinsz_lens = pcorr.nbinsz_lens
750
+ self.bin_centers = np.zeros_like(pcorr.bin_centers)
751
+ self.npcf_multipoles = np.zeros_like(pcorr.npcf_multipoles)
752
+ self.npcf_multipoles_norm = np.zeros_like(pcorr.npcf_multipoles_norm)
753
+ _footnorm = np.zeros_like(pcorr.bin_centers)
754
+ if keep_patchres:
755
+ centers_patches = np.zeros((cat_source.npatches, *pcorr.bin_centers.shape), dtype=pcorr.bin_centers.dtype)
756
+ npcf_multipoles_patches = np.zeros((cat_source.npatches, *pcorr.npcf_multipoles.shape), dtype=pcorr.npcf_multipoles.dtype)
757
+ npcf_multipoles_norm_patches = np.zeros((cat_source.npatches, *pcorr.npcf_multipoles_norm.shape), dtype=pcorr.npcf_multipoles_norm.dtype)
758
+ _shelltriplets = np.array([[[pcorr.npcf_multipoles_norm[0,zs*self.nbinsz_lens*self.nbinsz_lens+zl*self.nbinsz_lens+zl,i,i].real
759
+ for i in range(pcorr.nbinsr)] for zl in range(self.nbinsz_lens)] for zs in range(self.nbinsz_source)])
760
+ # Rough estimate of scaling of pair counts based on zeroth multipole of triplets. Note that we might get nans here due to numerical
761
+ # inaccuracies in the multiple counting corrections for bins with zero triplets, so we force those values to be zero.
762
+ _patchnorm = np.nan_to_num(np.sqrt(_shelltriplets))
763
+ self.bin_centers += _patchnorm*pcorr.bin_centers
764
+ _footnorm += _patchnorm
765
+ self.npcf_multipoles += pcorr.npcf_multipoles
766
+ self.npcf_multipoles_norm += pcorr.npcf_multipoles_norm
767
+ if keep_patchres:
768
+ centers_patches[elp] += pcorr.bin_centers
769
+ npcf_multipoles_patches[elp] += pcorr.npcf_multipoles
770
+ npcf_multipoles_norm_patches[elp] += pcorr.npcf_multipoles_norm
771
+ if save_patchres:
772
+ pcorr.saveinst(save_patchres, save_filebase+'_patch%i'%elp)
773
+
774
+ # Finalize the measurement on the full footprint
775
+ self.bin_centers = np.divide(self.bin_centers,_footnorm, out=np.zeros_like(self.bin_centers), where=_footnorm>0)
776
+ self.bin_centers_mean =np.mean(self.bin_centers, axis=(0,1))
777
+ self.projection = "X"
778
+
779
+ if keep_patchres:
780
+ return centers_patches, npcf_multipoles_patches, npcf_multipoles_norm_patches
781
+
782
+ # TODO: Include z-weighting in estimator
783
+ # * False --> No z-weighting, nothing to do
784
+ # * True --> Tomographic zweighting: Use effective weight for each tomo bin combi. Do computation as tomo case with
785
+ # no z-weighting and then weight in postprocessing where (zs, zl1, zl2) --> w_{zl1, zl2} * (zs)
786
+ # As this could be many zbins, might want to only allow certain zcombis -- i.e. neighbouring zbins.
787
+ # Functional form similar to https://arxiv.org/pdf/1909.06190.pdf
788
+ # * Note that for spectroscopic catalogs we cannot do a full spectroscopic weighting as done i.e. the brute-force method
789
+ # in https://arxiv.org/pdf/1909.06190.pdf, as this breaks the multipole decomposition.
790
+ # * In general, think about what could be a consistent way get a good compromise between speed vs S/N. One extreme would
791
+ # be just to use some broad bins and and the std within them (so 'thinner' bins have more weight). Other extreme would
792
+ # be many small zbins with proper cross-weighting and maximum distance --> Becomes less efficient for more bins.
793
+ def process(self, cat_source, cat_lens, dotomo_source=True, dotomo_lens=True, rotsignflip=False, apply_edge_correction=False,
794
+ save_patchres=False, save_filebase="", keep_patchres=False):
795
+ r"""
796
+ Compute a shear-lens-lens correlation provided a source and a lens catalog.
797
+
798
+ Parameters
799
+ ----------
800
+ cat_source: orpheus.SpinTracerCatalog
801
+ The source catalog which is processed
802
+ cat_lens: orpheus.ScalarTracerCatalog
803
+ The lens catalog which is processed
804
+ dotomo_source: bool
805
+ Flag that decides whether the tomographic information in the source catalog should be used. Defaults to `True`.
806
+ dotomo_lens: bool
807
+ Flag that decides whether the tomographic information in the lens catalog should be used. Defaults to `True`.
808
+ rotsignflip: bool
809
+ If the shape catalog is has been decomposed in patches, choose whether the rotation angle should be flipped.
810
+ For simulated data this was always ok to set to 'False`. Defaults to `False`.
811
+ apply_edge_correction: bool
812
+ Flag that decides how the NPCF in the real space basis is computed.
813
+ * If set to `True` the computation is done via edge-correcting the GNN-multipoles
814
+ * If set to `False` both GNN and NNN are transformed separately and the ratio is done in the real-space basis
815
+ Defaults to `False`.
816
+ save_patchres: bool or str
817
+ If the shape catalog is has been decomposed in patches, flag whether to save the GG measurements on the individual patches.
818
+ Note that the path needs to exist, otherwise a `ValueError` is raised. For a flat-sky catalog this parameter
819
+ has no effect. Defaults to `False`
820
+ save_filebase: str
821
+ Base of the filenames in which the patches are saved. The full filename will be `<save_patchres>/<save_filebase>_patchxx.npz`.
822
+ Only has an effect if the shape catalog consists of multiple patches and `save_patchres` is not `False`.
823
+ keep_patchres: bool
824
+ If the catalog consists of multiple patches, returns all measurements on the patches. Defaults to `False`.
825
+ """
826
+ self._checkcats([cat_source, cat_lens, cat_lens], [2, 0, 0])
827
+
828
+ # Catch typical errors, i.e. incompatible catalogs or missin patch decompositions
829
+ if cat_source.geometry=='spherical' and cat_source.patchinds is None:
830
+ raise ValueError('Error: Spherical catalog needs to be first decomposed into patches using the Catalog._topatches method.')
831
+ if cat_lens.geometry=='spherical' and cat_lens.patchinds is None:
832
+ raise ValueError('Error: Spherical catalog needs to be first decomposed into patches using the Catalog._topatches method.')
833
+ if cat_source.geometry != cat_lens.geometry:
834
+ raise ValueError('Incompatible geometries of source catalog (%s) and lens catalog (%s).'%(
835
+ cat_source.geometry,cat_lens.geometry))
836
+
837
+ # Catalog consist of multiple patches
838
+ if (cat_source.patchinds is not None) and (cat_lens.patchinds is not None):
839
+ return self.__process_patches(cat_source, cat_lens, dotomo_source=dotomo_source, dotomo_lens=dotomo_lens,
840
+ rotsignflip=rotsignflip, apply_edge_correction=apply_edge_correction,
841
+ save_patchres=save_patchres, save_filebase=save_filebase, keep_patchres=keep_patchres)
842
+
843
+ # Catalog does not consist of patches
844
+ else:
845
+
846
+ if not dotomo_lens and self.zweighting:
847
+ print("Redshift-weighting requires tomographic computation for the lenses.")
848
+ dotomo_lens = True
849
+
850
+ if not dotomo_source:
851
+ self.nbinsz_source = 1
852
+ old_zbins_source = cat_source.zbins[:]
853
+ cat_source.zbins = np.zeros(cat_source.ngal, dtype=np.int32)
854
+ else:
855
+ self.nbinsz_source = cat_source.nbinsz
856
+ if not dotomo_lens:
857
+ self.nbinsz_lens = 1
858
+ old_zbins_lens = cat_lens.zbins[:]
859
+ cat_lens.zbins = np.zeros(cat_lens.ngal, dtype=np.int32)
860
+ else:
861
+ self.nbinsz_lens = cat_lens.nbinsz
862
+
863
+ if self.zweighting:
864
+ if cat_lens.zbins_mean is None:
865
+ print("Redshift-weighting requires information about mean redshift in tomo bins of lens catalog")
866
+ if cat_lens.zbins_std is None:
867
+ print("Warning: Redshift-dispersion in tomo bins of lens catalog not given. Set to zero.")
868
+ cat_lens.zbins_std = np.zeros(self.nbinsz_lens)
869
+
870
+ _z3combis = self.nbinsz_source*self.nbinsz_lens*self.nbinsz_lens
871
+ _r2combis = self.nbinsr*self.nbinsr
872
+ sc = (self.n_cfs, self.nmax+1, _z3combis, self.nbinsr, self.nbinsr)
873
+ sn = (self.nmax+1, _z3combis, self.nbinsr,self.nbinsr)
874
+ szr = (self.nbinsz_source, self.nbinsz_lens, self.nbinsr)
875
+ bin_centers = np.zeros(reduce(operator.mul, szr)).astype(np.float64)
876
+ Upsilon_n = np.zeros(reduce(operator.mul, sc)).astype(np.complex128)
877
+ Norm_n = np.zeros(reduce(operator.mul, sn)).astype(np.complex128)
878
+ args_sourcecat = (cat_source.isinner.astype(np.float64), cat_source.weight.astype(np.float64),
879
+ cat_source.pos1.astype(np.float64), cat_source.pos2.astype(np.float64),
880
+ cat_source.tracer_1.astype(np.float64), cat_source.tracer_2.astype(np.float64),
881
+ cat_source.zbins.astype(np.int32), np.int32(self.nbinsz_source), np.int32(cat_source.ngal), )
882
+ args_lenscat = (cat_lens.weight.astype(np.float64), cat_lens.pos1.astype(np.float64),
883
+ cat_lens.pos2.astype(np.float64), cat_lens.zbins.astype(np.int32),
884
+ np.int32(self.nbinsz_lens), np.int32(cat_lens.ngal), )
885
+ args_basesetup = (np.int32(self.nmax), np.float64(self.min_sep), np.float64(self.max_sep),
886
+ np.int32(self.nbinsr), np.int32(self.multicountcorr), )
887
+ if self.method=="Discrete":
888
+ hash_dpix = max(1.,self.max_sep//10.)
889
+ jointextent = list(cat_source._jointextent([cat_lens], extend=self.tree_resos[-1]))
890
+ cat_source.build_spatialhash(dpix=hash_dpix, extent=jointextent)
891
+ cat_lens.build_spatialhash(dpix=hash_dpix, extent=jointextent)
892
+ nregions = np.int32(len(np.argwhere(cat_source.index_matcher>-1).flatten()))
893
+ args_hash = (cat_source.index_matcher, cat_source.pixs_galind_bounds, cat_source.pix_gals,
894
+ cat_lens.index_matcher, cat_lens.pixs_galind_bounds, cat_lens.pix_gals, nregions, )
895
+ args_pixgrid = (np.float64(cat_lens.pix1_start), np.float64(cat_lens.pix1_d), np.int32(cat_lens.pix1_n),
896
+ np.float64(cat_lens.pix2_start), np.float64(cat_lens.pix2_d), np.int32(cat_lens.pix2_n), )
897
+ args = (*args_sourcecat,
898
+ *args_lenscat,
899
+ *args_basesetup,
900
+ *args_hash,
901
+ *args_pixgrid,
902
+ np.int32(self.nthreads),
903
+ np.int32(self._verbose_c),
904
+ bin_centers,
905
+ Upsilon_n,
906
+ Norm_n, )
907
+ func = self.clib.alloc_Gammans_discrete_GNN
908
+ if self.method == "DoubleTree":
909
+ cutfirst = np.int32(self.tree_resos[0]==0.)
910
+ jointextent = list(cat_source._jointextent([cat_lens], extend=self.tree_resos[-1]))
911
+ # Build multihashes for sources and lenses
912
+ mhash_source = cat_source.multihash(dpixs=self.tree_resos[cutfirst:], dpix_hash=self.tree_resos[-1],
913
+ shuffle=self.shuffle_pix, normed=True, extent=jointextent)
914
+ sngal_resos, spos1s, spos2s, sweights, szbins, sisinners, sallfields, sindex_matchers, \
915
+ spixs_galind_bounds, spix_gals, sdpixs1_true, sdpixs2_true = mhash_source
916
+ ngal_resos_source = np.array(sngal_resos, dtype=np.int32)
917
+ weight_resos_source = np.concatenate(sweights).astype(np.float64)
918
+ pos1_resos_source = np.concatenate(spos1s).astype(np.float64)
919
+ pos2_resos_source = np.concatenate(spos2s).astype(np.float64)
920
+ zbin_resos_source = np.concatenate(szbins).astype(np.int32)
921
+ isinner_resos_source = np.concatenate(sisinners).astype(np.float64)
922
+ e1_resos_source = np.concatenate([sallfields[i][0] for i in range(len(sallfields))]).astype(np.float64)
923
+ e2_resos_source = np.concatenate([sallfields[i][1] for i in range(len(sallfields))]).astype(np.float64)
924
+ index_matcher_source = np.concatenate(sindex_matchers).astype(np.int32)
925
+ pixs_galind_bounds_source = np.concatenate(spixs_galind_bounds).astype(np.int32)
926
+ pix_gals_source = np.concatenate(spix_gals).astype(np.int32)
927
+ mhash_lens = cat_lens.multihash(dpixs=self.tree_resos[cutfirst:], dpix_hash=self.tree_resos[-1],
928
+ shuffle=self.shuffle_pix, normed=True, extent=jointextent)
929
+ lngal_resos, lpos1s, lpos2s, lweights, lzbins, lisinners, lallfields, lindex_matchers, \
930
+ lpixs_galind_bounds, lpix_gals, ldpixs1_true, ldpixs2_true = mhash_lens
931
+ ngal_resos_lens = np.array(lngal_resos, dtype=np.int32)
932
+ weight_resos_lens = np.concatenate(lweights).astype(np.float64)
933
+ pos1_resos_lens = np.concatenate(lpos1s).astype(np.float64)
934
+ pos2_resos_lens = np.concatenate(lpos2s).astype(np.float64)
935
+ zbin_resos_lens = np.concatenate(lzbins).astype(np.int32)
936
+ isinner_resos_lens = np.concatenate(lisinners).astype(np.float64)
937
+ index_matcher_lens = np.concatenate(lindex_matchers).astype(np.int32)
938
+ pixs_galind_bounds_lens = np.concatenate(lpixs_galind_bounds).astype(np.int32)
939
+ pix_gals_lens = np.asarray(np.concatenate(lpix_gals)).astype(np.int32)
940
+ index_matcher_flat = np.argwhere(cat_source.index_matcher>-1).flatten().astype(np.int32)
941
+ nregions = np.int32(len(index_matcher_flat))
942
+ # Collect args
943
+ args_resoinfo = (np.int32(self.tree_nresos), np.int32(self.tree_nresos-cutfirst),
944
+ sdpixs1_true.astype(np.float64), sdpixs2_true.astype(np.float64), self.tree_redges, )
945
+ args_leafs = (np.int32(self.resoshift_leafs), np.int32(self.minresoind_leaf),
946
+ np.int32(self.maxresoind_leaf), )
947
+ args_resos = (isinner_resos_source, weight_resos_source, pos1_resos_source, pos2_resos_source,
948
+ e1_resos_source, e2_resos_source, zbin_resos_source, ngal_resos_source,
949
+ np.int32(self.nbinsz_source), isinner_resos_lens, weight_resos_lens, pos1_resos_lens,
950
+ pos2_resos_lens, zbin_resos_lens, ngal_resos_lens, np.int32(self.nbinsz_lens), )
951
+ args_mhash = (index_matcher_source, pixs_galind_bounds_source, pix_gals_source,
952
+ index_matcher_lens, pixs_galind_bounds_lens, pix_gals_lens, index_matcher_flat, nregions, )
953
+ args_pixgrid = (np.float64(cat_lens.pix1_start), np.float64(cat_lens.pix1_d), np.int32(cat_lens.pix1_n),
954
+ np.float64(cat_lens.pix2_start), np.float64(cat_lens.pix2_d), np.int32(cat_lens.pix2_n), )
955
+ args = (*args_resoinfo,
956
+ *args_leafs,
957
+ *args_resos,
958
+ *args_basesetup,
959
+ *args_mhash,
960
+ *args_pixgrid,
961
+ np.int32(self.nthreads),
962
+ np.int32(self._verbose_c),
963
+ bin_centers,
964
+ Upsilon_n,
965
+ Norm_n, )
966
+ func = self.clib.alloc_Gammans_doubletree_GNN
967
+ if self._verbose_debug:
968
+ for elarg, arg in enumerate(args):
969
+ toprint = (elarg, type(arg),)
970
+ if isinstance(arg, np.ndarray):
971
+ toprint += (type(arg[0]), arg.shape)
972
+ toprint += (func.argtypes[elarg], )
973
+ print(toprint)
974
+ print(arg)
975
+
976
+ func(*args)
977
+
978
+ self.bin_centers = bin_centers.reshape(szr)
979
+ self.bin_centers_mean = np.mean(self.bin_centers, axis=(0,1))
980
+ self.npcf_multipoles = np.nan_to_num(Upsilon_n.reshape(sc))
981
+ self.npcf_multipoles_norm = np.nan_to_num(Norm_n.reshape(sn))
982
+ self.projection = "X"
983
+ self.is_edge_corrected = False
984
+
985
+ if apply_edge_correction:
986
+ self.edge_correction()
987
+
988
+ if not dotomo_source:
989
+ cat_source.zbins = old_zbins_source
990
+ if not dotomo_lens:
991
+ cat_lens.zbins = old_zbins_lens
992
+
993
+ def edge_correction(self, ret_matrices=False):
994
+ assert(not self.is_edge_corrected)
995
+ def gen_M_matrix(thet1,thet2,threepcf_n_norm):
996
+ nvals, ntheta, _ = threepcf_n_norm.shape
997
+ nmax = (nvals-1)//2
998
+ narr = np.arange(-nmax,nmax+1, dtype=np.int)
999
+ nextM = np.zeros((nvals,nvals))
1000
+ for ind, ell in enumerate(narr):
1001
+ lminusn = ell-narr
1002
+ sel = np.logical_and(lminusn+nmax>=0, lminusn+nmax<nvals)
1003
+ nextM[ind,sel] = threepcf_n_norm[(lminusn+nmax)[sel],thet1,thet2].real / threepcf_n_norm[nmax,thet1,thet2].real
1004
+ return nextM
1005
+
1006
+ nvals, nzcombis, ntheta, _ = self.npcf_multipoles_norm.shape
1007
+ nmax = nvals-1
1008
+ threepcf_n_full = np.zeros((1,2*nmax+1, nzcombis, ntheta, ntheta), dtype=complex)
1009
+ threepcf_n_norm_full = np.zeros((2*nmax+1, nzcombis, ntheta, ntheta), dtype=complex)
1010
+ threepcf_n_corr = np.zeros(threepcf_n_full.shape, dtype=np.complex)
1011
+ threepcf_n_full[:,nmax:] = self.npcf_multipoles
1012
+ threepcf_n_norm_full[nmax:] = self.npcf_multipoles_norm
1013
+ for nextn in range(1,nvals):
1014
+ threepcf_n_full[0,nmax-nextn] = self.npcf_multipoles[0,nextn].transpose(0,2,1)
1015
+ threepcf_n_norm_full[nmax-nextn] = self.npcf_multipoles_norm[nextn].transpose(0,2,1)
1016
+
1017
+ if ret_matrices:
1018
+ mats = np.zeros((nzcombis,ntheta,ntheta,nvals,nvals))
1019
+ for indz in range(nzcombis):
1020
+ #sys.stdout.write("%i"%indz)
1021
+ for thet1 in range(ntheta):
1022
+ for thet2 in range(ntheta):
1023
+ nextM = gen_M_matrix(thet1,thet2,threepcf_n_norm_full[:,indz])
1024
+ nextM_inv = np.linalg.inv(nextM)
1025
+ if ret_matrices:
1026
+ mats[indz,thet1,thet2] = nextM
1027
+ threepcf_n_corr[0,:,indz,thet1,thet2] = np.matmul(nextM_inv,threepcf_n_full[0,:,indz,thet1,thet2])
1028
+
1029
+ self.npcf_multipoles = threepcf_n_corr[:,nmax:]
1030
+ self.is_edge_corrected = True
1031
+
1032
+ if ret_matrices:
1033
+ return threepcf_n_corr[:,nmax:], mats
1034
+
1035
+ # TODO:
1036
+ # * Include the z-weighting method
1037
+ # * Include the 2pcf as spline --> Should we also add an option to compute it here? Might be a mess
1038
+ # as then we also would need methods to properly distribute randoms...
1039
+ # * Do a voronoi-tesselation at the multipole level? Would be just 2D, but still might help? Eventually
1040
+ # bundle together cells s.t. tot_weight > theshold? However, this might then make the binning courser
1041
+ # for certain triangle configs(?)
1042
+ def multipoles2npcf(self, xi=None):
1043
+ r"""
1044
+ Notes
1045
+ -----
1046
+ * The Upsilon and Norms are only computed for the n>0 multipoles. The n<0 multipoles are recovered by symmetry considerations, i.e.:
1047
+
1048
+ .. math::
1049
+
1050
+ \Upsilon_{-n}(\theta_1, \theta_2, z_1, z_2, z_3) =
1051
+ \Upsilon_{n}(\theta_2, \theta_1, z_1, z_3, z_2)
1052
+
1053
+ As the tomographic bin combinations are interpreted as a flat list, they need to be appropriately shuffled. This is handled by ``ztiler``.
1054
+
1055
+ * When dividing by the (weighted) counts ``N``, all contributions for which ``N <= 0`` are set to zero.
1056
+
1057
+ """
1058
+ _, nzcombis, rbins, rbins = np.shape(self.npcf_multipoles[0])
1059
+ self.npcf = np.zeros((self.n_cfs, nzcombis, rbins, rbins, len(self.phi)), dtype=complex)
1060
+ self.npcf_norm = np.zeros((nzcombis, rbins, rbins, len(self.phi)), dtype=float)
1061
+ ztiler = np.arange(self.nbinsz_source*self.nbinsz_lens*self.nbinsz_lens).reshape(
1062
+ (self.nbinsz_source,self.nbinsz_lens,self.nbinsz_lens)).transpose(0,2,1).flatten().astype(np.int32)
1063
+
1064
+ # 3PCF components
1065
+ conjmap = [0]
1066
+ N0 = 1./(2*np.pi) * self.npcf_multipoles_norm[0].astype(complex)
1067
+ for elm in range(self.n_cfs):
1068
+ for elphi, phi in enumerate(self.phi):
1069
+ tmp = 1./(2*np.pi) * self.npcf_multipoles[elm,0].astype(complex)
1070
+ for n in range(1,self.nmax+1):
1071
+ _const = 1./(2*np.pi) * np.exp(1J*n*phi)
1072
+ tmp += _const * self.npcf_multipoles[elm,n].astype(complex)
1073
+ tmp += _const.conj() * self.npcf_multipoles[conjmap[elm],n][ztiler].astype(complex).transpose(0,2,1)
1074
+ self.npcf[elm,...,elphi] = tmp
1075
+ # Normalization
1076
+ for elphi, phi in enumerate(self.phi):
1077
+ tmptotnorm = 1./(2*np.pi) * self.npcf_multipoles_norm[0].astype(complex)
1078
+ for n in range(1,self.nmax+1):
1079
+ _const = 1./(2*np.pi) * np.exp(1J*n*phi)
1080
+ tmptotnorm += _const * self.npcf_multipoles_norm[n].astype(complex)
1081
+ tmptotnorm += _const.conj() * self.npcf_multipoles_norm[n][ztiler].astype(complex).transpose(0,2,1)
1082
+ self.npcf_norm[...,elphi] = tmptotnorm.real
1083
+
1084
+ if self.is_edge_corrected:
1085
+ sel_zero = np.isnan(N0)
1086
+ _a = self.npcf
1087
+ _b = N0.real[:, :, np.newaxis]
1088
+ self.npcf = np.divide(_a, _b, out=np.zeros_like(_a), where=np.abs(_b)>0)
1089
+ else:
1090
+ _a = self.npcf
1091
+ _b = self.npcf_norm
1092
+ self.npcf = np.divide(_a, _b, out=np.zeros_like(_a), where=np.abs(_b)>0)
1093
+ #self.npcf = self.npcf/self.npcf_norm[0][None, ...].astype(complex)
1094
+ self.projection = "X"
1095
+
1096
+ # Optionally correct by clustering correlation function
1097
+ # Assume
1098
+ # xi[0] has shape (nbinsr_xi, )
1099
+ # xi[1] has shape (nbinsz_lens * nbinsz_lens, nbinsr_xi, )
1100
+ if xi is not None:
1101
+ assert(len(xi)==2)
1102
+ assert(xi[1].shape[1]==len(xi[0]))
1103
+ assert(xi[1].shape[0]==self.nbinsz_lens*self.nbinsz_lens)
1104
+ # Get angular separation at which xi is evaluated
1105
+ _rs1 = self.bin_centers_mean[:, None, None]
1106
+ _rs2 = self.bin_centers_mean[None, :, None]
1107
+ _phis = self.phi[None, None, :]
1108
+ d_xi = np.sqrt(_rs1**2 + _rs2**2 - 2*_rs1*_rs2*np.cos(_phis))
1109
+ xi_corr = interp1d(xi[0], xi[1], axis=-1,
1110
+ bounds_error=False, fill_value=0.0, kind="linear")(d_xi)
1111
+ # Apply correction to 3pcf (TODO: Looks a bit ugly...)
1112
+ _npcf = self.npcf[0].reshape((self.nbinsz_source, self.nbinsz_lens*self.nbinsz_lens, *d_xi.shape))
1113
+ _npcf *= (1.0 + xi_corr[None, ...])
1114
+ self.npcf[0] = _npcf.reshape(self.npcf[0].shape)
1115
+
1116
+
1117
+ ## PROJECTIONS ##
1118
+ def projectnpcf(self, projection):
1119
+ super()._projectnpcf(self, projection)
1120
+
1121
+ ## INTEGRATED MEASURES ##
1122
+ def computeNNM(self, radii, do_multiscale=False, xi=None, tofile=False, filtercache=None):
1123
+ """
1124
+ Compute third-order aperture statistics using the polyonomial filter of Crittenden 2002.
1125
+ """
1126
+ nb_config.NUMBA_DEFAULT_NUM_THREADS = self.nthreads
1127
+ nb_config.NUMBA_NUM_THREADS = self.nthreads
1128
+
1129
+ if self.npcf is None and self.npcf_multipoles is not None:
1130
+ self.multipoles2npcf(xi=xi)
1131
+
1132
+ nradii = len(radii)
1133
+ if not do_multiscale:
1134
+ nrcombis = nradii
1135
+ _rcut = 1
1136
+ else:
1137
+ nrcombis = nradii*nradii*nradii
1138
+ _rcut = nradii
1139
+ NNM = np.zeros((1, self.nbinsz_source*self.nbinsz_lens*self.nbinsz_lens, nrcombis), dtype=complex)
1140
+ tmprcombi = 0
1141
+ for elr1, R1 in enumerate(radii):
1142
+ for elr2, R2 in enumerate(radii[:_rcut]):
1143
+ for elr3, R3 in enumerate(radii[:_rcut]):
1144
+ if not do_multiscale:
1145
+ R2 = R1
1146
+ R3 = R1
1147
+ if filtercache is not None:
1148
+ A_NNM = filtercache[tmprcombi]
1149
+ else:
1150
+ A_NNM = self._NNM_filtergrid(R1, R2, R3)
1151
+ NNM[0,:,tmprcombi] = np.nansum(A_NNM*self.npcf[0,...],axis=(1,2,3))
1152
+ tmprcombi += 1
1153
+ return NNM
1154
+
1155
+ def _NNM_filtergrid(self, R1, R2, R3):
1156
+ return self.__NNM_filtergrid(R1, R2, R3, self.bin_edges, self.bin_centers_mean, self.phi)
1157
+
1158
+ @staticmethod
1159
+ @jit(nopython=True, parallel=True)
1160
+ def __NNM_filtergrid(R1, R2, R3, edges, centers, phis):
1161
+ nbinsr = len(centers)
1162
+ nbinsphi = len(phis)
1163
+ _cphis = np.cos(phis)
1164
+ _ephis = np.e**(1J*phis)
1165
+ _ephisc = np.e**(-1J*phis)
1166
+ Theta4 = 1./3. * (R1**2*R2**2 + R1**2*R3**2 + R2**2*R3**2)
1167
+ a2 = 2./3. * R1**2*R2**2*R3**2 / Theta4
1168
+ ANNM = np.zeros((nbinsr,nbinsr,nbinsphi), dtype=nb_complex128)
1169
+ for elb in prange(nbinsr*nbinsr):
1170
+ elb1 = int(elb//nbinsr)
1171
+ elb2 = elb%nbinsr
1172
+ _y1 = centers[elb1]
1173
+ _dbin1 = edges[elb1+1] - edges[elb1]
1174
+ _y2 = centers[elb2]
1175
+ _dbin2 = edges[elb2+1] - edges[elb2]
1176
+ _dbinphi = phis[1] - phis[0]
1177
+ b0 = _y1**2/(2*R1**2)+_y2**2/(2*R2**2) - a2/4.*(
1178
+ _y1**2/R1**4 + 2*_y1*_y2*_cphis/(R1**2*R2**2) + _y2**2/R2**4)
1179
+ g1 = _y1 - a2/2. * (_y1/R1**2 + _y2*_ephisc/R2**2)
1180
+ g2 = _y2 - a2/2. * (_y2/R2**2 + _y1*_ephis/R1**2)
1181
+ g1c = g1.conj()
1182
+ g2c = g2.conj()
1183
+ F1 = 2*R1**2 - g1*g1c
1184
+ F2 = 2*R2**2 - g2*g2c
1185
+ pref = np.e**(-b0)/(72*np.pi*Theta4**2)
1186
+ sum1 = (g1-_y1)*(g2-_y2) * (1/a2*F1*F2 - (F1+F2) + 2*a2 + g1c*g2*_ephisc + g1*g2c*_ephis)
1187
+ sum2 = ((g2-_y2) + (g1-_y1)*_ephis) * (g1*(F2-2*a2) + g2*(F1-2*a2)*_ephisc)
1188
+ sum3 = 2*g1*g2*a2
1189
+ _measures = _y1*_dbin1 * _y2*_dbin2 * _dbinphi
1190
+ ANNM[elb1,elb2] = _measures * pref * (sum1-sum2+sum3)
1191
+
1192
+ return ANNM
1193
+
1194
+ class NGGCorrelation(BinnedNPCF):
1195
+ r""" Class containing methods to measure and and obtain statistics that are built
1196
+ from third-order lens-shear-shear correlation functions.
1197
+
1198
+ Attributes
1199
+ ----------
1200
+ min_sep: float
1201
+ The smallest distance of each vertex for which the NPCF is computed.
1202
+ max_sep: float
1203
+ The largest distance of each vertex for which the NPCF is computed.
1204
+
1205
+ Notes
1206
+ -----
1207
+ Inherits all other parameters and attributes from :class:`BinnedNPCF`.
1208
+ Additional child-specific parameters can be passed via ``kwargs``.
1209
+ Either ``nbinsr`` or ``binsize`` has to be provided to fix the binning scheme .
1210
+
1211
+ Note that the different components of the NGG correlator are ordered as
1212
+
1213
+ .. math::
1214
+
1215
+ \left[ \tilde{G}_-, \tilde{G}_+, \right] \ ,
1216
+
1217
+ which is different to the usual conventions, but matches orpheus' conventions to
1218
+ always start with a correlator in which not polar field is complex conjugated.
1219
+ """
1220
+ def __init__(self, min_sep, max_sep, **kwargs):
1221
+
1222
+ super().__init__(3, [0,2,2], n_cfs=2, min_sep=min_sep, max_sep=max_sep, **kwargs)
1223
+ self.nmax = self.nmaxs[0]
1224
+ self.phi = self.phis[0]
1225
+ self.projection = None
1226
+ self.projections_avail = [None, "X"]
1227
+ self.nbinsz_source = None
1228
+ self.nbinsz_lens = None
1229
+
1230
+ # (Add here any newly implemented projections)
1231
+ self._initprojections(self)
1232
+
1233
+ def __process_patches(self, cat_source, cat_lens, dotomo_source=True, dotomo_lens=True, rotsignflip=False,
1234
+ apply_edge_correction=False, save_patchres=False, save_filebase="", keep_patchres=False):
1235
+ if save_patchres:
1236
+ if not Path(save_patchres).is_dir():
1237
+ raise ValueError('Path to directory does not exist.')
1238
+
1239
+ for elp in range(cat_source.npatches):
1240
+ if self._verbose_python:
1241
+ print('Doing patch %i/%i'%(elp+1,cat_source.npatches))
1242
+ # Compute statistics on patch
1243
+ pscat = cat_source.frompatchind(elp,rotsignflip=rotsignflip)
1244
+ plcat = cat_lens.frompatchind(elp)
1245
+ pcorr = NGGCorrelation(
1246
+ min_sep=self.min_sep,
1247
+ max_sep=self.max_sep,
1248
+ nbinsr=self.nbinsr,
1249
+ nbinsphi=self.nbinsphi,
1250
+ nmaxs=self.nmaxs,
1251
+ method=self.method,
1252
+ multicountcorr=self.multicountcorr,
1253
+ shuffle_pix=self.shuffle_pix,
1254
+ tree_resos=self.tree_resos,
1255
+ rmin_pixsize=self.rmin_pixsize,
1256
+ resoshift_leafs=self.resoshift_leafs,
1257
+ minresoind_leaf=self.minresoind_leaf,
1258
+ maxresoind_leaf=self.maxresoind_leaf,
1259
+ nthreads=self.nthreads,
1260
+ verbosity=self.verbosity)
1261
+ pcorr.process(pscat, plcat, dotomo_source=dotomo_source, dotomo_lens=dotomo_lens)
1262
+
1263
+ # Update the total measurement
1264
+ if elp == 0:
1265
+ self.nbinsz_source = pcorr.nbinsz_source
1266
+ self.nbinsz_lens = pcorr.nbinsz_lens
1267
+ self.bin_centers = np.zeros_like(pcorr.bin_centers)
1268
+ self.npcf_multipoles = np.zeros_like(pcorr.npcf_multipoles)
1269
+ self.npcf_multipoles_norm = np.zeros_like(pcorr.npcf_multipoles_norm)
1270
+ _footnorm = np.zeros_like(pcorr.bin_centers)
1271
+ if keep_patchres:
1272
+ centers_patches = np.zeros((cat_source.npatches, *pcorr.bin_centers.shape), dtype=pcorr.bin_centers.dtype)
1273
+ npcf_multipoles_patches = np.zeros((cat_source.npatches, *pcorr.npcf_multipoles.shape), dtype=pcorr.npcf_multipoles.dtype)
1274
+ npcf_multipoles_norm_patches = np.zeros((cat_source.npatches, *pcorr.npcf_multipoles_norm.shape), dtype=pcorr.npcf_multipoles_norm.dtype)
1275
+ _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
1276
+ for i in range(pcorr.nbinsr)] for zs in range(self.nbinsz_source)] for zl in range(self.nbinsz_lens)])
1277
+ # Rough estimate of scaling of pair counts based on zeroth multipole of triplets. Note that we might get nans here due to numerical
1278
+ # inaccuracies in the multiple counting corrections for bins with zero triplets, so we force those values to be zero.
1279
+ _patchnorm = np.nan_to_num(np.sqrt(_shelltriplets))
1280
+ self.bin_centers += _patchnorm*pcorr.bin_centers
1281
+ _footnorm += _patchnorm
1282
+ self.npcf_multipoles += pcorr.npcf_multipoles
1283
+ self.npcf_multipoles_norm += pcorr.npcf_multipoles_norm
1284
+ if keep_patchres:
1285
+ centers_patches[elp] += pcorr.bin_centers
1286
+ npcf_multipoles_patches[elp] += pcorr.npcf_multipoles
1287
+ npcf_multipoles_norm_patches[elp] += pcorr.npcf_multipoles_norm
1288
+ if save_patchres:
1289
+ pcorr.saveinst(save_patchres, save_filebase+'_patch%i'%elp)
1290
+
1291
+ # Finalize the measurement on the full footprint
1292
+ self.bin_centers = np.divide(self.bin_centers,_footnorm, out=np.zeros_like(self.bin_centers), where=_footnorm>0)
1293
+ self.bin_centers_mean =np.mean(self.bin_centers, axis=(0,1))
1294
+ self.projection = "X"
1295
+
1296
+ if keep_patchres:
1297
+ return centers_patches, npcf_multipoles_patches, npcf_multipoles_norm_patches
1298
+
1299
+ def process(self, cat_source, cat_lens, dotomo_source=True, dotomo_lens=True, rotsignflip=False, apply_edge_correction=False,
1300
+ save_patchres=False, save_filebase="", keep_patchres=False):
1301
+ r"""
1302
+ Compute a lens-shear-shear correlation provided a source and a lens catalog.
1303
+
1304
+ Parameters
1305
+ ----------
1306
+ cat_source: orpheus.SpinTracerCatalog
1307
+ The source catalog which is processed
1308
+ cat_lens: orpheus.ScalarTracerCatalog
1309
+ The lens catalog which is processed
1310
+ dotomo_source: bool
1311
+ Flag that decides whether the tomographic information in the source catalog should be used. Defaults to `True`.
1312
+ dotomo_lens: bool
1313
+ Flag that decides whether the tomographic information in the lens catalog should be used. Defaults to `True`.
1314
+ rotsignflip: bool
1315
+ If the shape catalog is has been decomposed in patches, choose whether the rotation angle should be flipped.
1316
+ For simulated data this was always ok to set to 'False`. Defaults to `False`.
1317
+ apply_edge_correction: bool
1318
+ Flag that decides how the NPCF in the real space basis is computed.
1319
+ * If set to `True` the computation is done via edge-correcting the GNN-multipoles
1320
+ * If set to `False` both GNN and NNN are transformed separately and the ratio is done in the real-space basis
1321
+ Defaults to `False`.
1322
+ save_patchres: bool or str
1323
+ If the shape catalog is has been decomposed in patches, flag whether to save the GG measurements on the individual patches.
1324
+ Note that the path needs to exist, otherwise a `ValueError` is raised. For a flat-sky catalog this parameter
1325
+ has no effect. Defaults to `False`
1326
+ save_filebase: str
1327
+ Base of the filenames in which the patches are saved. The full filename will be `<save_patchres>/<save_filebase>_patchxx.npz`.
1328
+ Only has an effect if the shape catalog consists of multiple patches and `save_patchres` is not `False`.
1329
+ keep_patchres: bool
1330
+ If the catalog consists of multiple patches, returns all measurements on the patches. Defaults to `False`.
1331
+ """
1332
+
1333
+ self._checkcats([cat_lens, cat_source, cat_source], [0, 2, 2])
1334
+
1335
+ # Catch typical errors, i.e. incompatible catalogs or missin patch decompositions
1336
+ if cat_source.geometry=='spherical' and cat_source.patchinds is None:
1337
+ raise ValueError('Error: Spherical catalog needs to be first decomposed into patches using the Catalog._topatches method.')
1338
+ if cat_lens.geometry=='spherical' and cat_lens.patchinds is None:
1339
+ raise ValueError('Error: Spherical catalog needs to be first decomposed into patches using the Catalog._topatches method.')
1340
+ if cat_source.geometry != cat_lens.geometry:
1341
+ raise ValueError('Incompatible geometries of source catalog (%s) and lens catalog (%s).'%(
1342
+ cat_source.geometry,cat_lens.geometry))
1343
+
1344
+ # Catalog consist of multiple patches
1345
+ if (cat_source.patchinds is not None) and (cat_lens.patchinds is not None):
1346
+ return self.__process_patches(cat_source, cat_lens, dotomo_source=dotomo_source, dotomo_lens=dotomo_lens,
1347
+ rotsignflip=rotsignflip, apply_edge_correction=apply_edge_correction,
1348
+ save_patchres=save_patchres, save_filebase=save_filebase, keep_patchres=keep_patchres)
1349
+
1350
+ # Catalog does not consist of patches
1351
+ else:
1352
+ if not dotomo_source:
1353
+ self.nbinsz_source = 1
1354
+ old_zbins_source = cat_source.zbins[:]
1355
+ cat_source.zbins = np.zeros(cat_source.ngal, dtype=np.int32)
1356
+ else:
1357
+ self.nbinsz_source = cat_source.nbinsz
1358
+ if not dotomo_lens:
1359
+ self.nbinsz_lens = 1
1360
+ old_zbins_lens = cat_lens.zbins[:]
1361
+ cat_lens.zbins = np.zeros(cat_lens.ngal, dtype=np.int32)
1362
+ else:
1363
+ self.nbinsz_lens = cat_lens.nbinsz
1364
+
1365
+ _z3combis = self.nbinsz_lens*self.nbinsz_source*self.nbinsz_source
1366
+ _r2combis = self.nbinsr*self.nbinsr
1367
+ sc = (self.n_cfs, 2*self.nmax+1, _z3combis, self.nbinsr, self.nbinsr)
1368
+ sn = (2*self.nmax+1, _z3combis, self.nbinsr,self.nbinsr)
1369
+ szr = (self.nbinsz_lens, self.nbinsz_source, self.nbinsr)
1370
+ bin_centers = np.zeros(reduce(operator.mul, szr)).astype(np.float64)
1371
+ Upsilon_n = np.zeros(reduce(operator.mul, sc)).astype(np.complex128)
1372
+ Norm_n = np.zeros(reduce(operator.mul, sn)).astype(np.complex128)
1373
+ args_sourcecat = (cat_source.weight.astype(np.float64),
1374
+ cat_source.pos1.astype(np.float64), cat_source.pos2.astype(np.float64),
1375
+ cat_source.tracer_1.astype(np.float64), cat_source.tracer_2.astype(np.float64),
1376
+ cat_source.zbins.astype(np.int32), np.int32(self.nbinsz_source), np.int32(cat_source.ngal), )
1377
+ args_lenscat = (cat_lens.isinner.astype(np.float64), cat_lens.weight.astype(np.float64), cat_lens.pos1.astype(np.float64),
1378
+ cat_lens.pos2.astype(np.float64), cat_lens.zbins.astype(np.int32),
1379
+ np.int32(self.nbinsz_lens), np.int32(cat_lens.ngal), )
1380
+ args_basesetup = (np.int32(self.nmax), np.float64(self.min_sep), np.float64(self.max_sep),
1381
+ np.int32(self.nbinsr), np.int32(self.multicountcorr), )
1382
+ if self.method=="Discrete":
1383
+ hash_dpix = max(1.,self.max_sep//10.)
1384
+ jointextent = list(cat_source._jointextent([cat_lens], extend=self.tree_resos[-1]))
1385
+ cat_source.build_spatialhash(dpix=hash_dpix, extent=jointextent)
1386
+ cat_lens.build_spatialhash(dpix=hash_dpix, extent=jointextent)
1387
+ nregions = np.int32(len(np.argwhere(cat_lens.index_matcher>-1).flatten()))
1388
+ args_hash = (cat_source.index_matcher, cat_source.pixs_galind_bounds, cat_source.pix_gals,
1389
+ cat_lens.index_matcher, cat_lens.pixs_galind_bounds, cat_lens.pix_gals, nregions, )
1390
+ args_pixgrid = (np.float64(cat_lens.pix1_start), np.float64(cat_lens.pix1_d), np.int32(cat_lens.pix1_n),
1391
+ np.float64(cat_lens.pix2_start), np.float64(cat_lens.pix2_d), np.int32(cat_lens.pix2_n), )
1392
+ args = (*args_sourcecat,
1393
+ *args_lenscat,
1394
+ *args_basesetup,
1395
+ *args_hash,
1396
+ *args_pixgrid,
1397
+ np.int32(self.nthreads),
1398
+ np.int32(self._verbose_c),
1399
+ bin_centers,
1400
+ Upsilon_n,
1401
+ Norm_n, )
1402
+ func = self.clib.alloc_Gammans_discrete_NGG
1403
+ if self.method=="Tree" or self.method == "DoubleTree":
1404
+ cutfirst = np.int32(self.tree_resos[0]==0.)
1405
+ jointextent = list(cat_source._jointextent([cat_lens], extend=self.tree_resos[-1]))
1406
+ # Build multihashes for sources and lenses
1407
+ mhash_source = cat_source.multihash(dpixs=self.tree_resos[cutfirst:], dpix_hash=self.tree_resos[-1],
1408
+ shuffle=self.shuffle_pix, normed=True, extent=jointextent)
1409
+ sngal_resos, spos1s, spos2s, sweights, szbins, sisinners, sallfields, sindex_matchers, \
1410
+ spixs_galind_bounds, spix_gals, sdpixs1_true, sdpixs2_true = mhash_source
1411
+ ngal_resos_source = np.array(sngal_resos, dtype=np.int32)
1412
+ weight_resos_source = np.concatenate(sweights).astype(np.float64)
1413
+ pos1_resos_source = np.concatenate(spos1s).astype(np.float64)
1414
+ pos2_resos_source = np.concatenate(spos2s).astype(np.float64)
1415
+ zbin_resos_source = np.concatenate(szbins).astype(np.int32)
1416
+ isinner_resos_source = np.concatenate(sisinners).astype(np.float64)
1417
+ e1_resos_source = np.concatenate([sallfields[i][0] for i in range(len(sallfields))]).astype(np.float64)
1418
+ e2_resos_source = np.concatenate([sallfields[i][1] for i in range(len(sallfields))]).astype(np.float64)
1419
+ index_matcher_source = np.concatenate(sindex_matchers).astype(np.int32)
1420
+ pixs_galind_bounds_source = np.concatenate(spixs_galind_bounds).astype(np.int32)
1421
+ pix_gals_source = np.concatenate(spix_gals).astype(np.int32)
1422
+ mhash_lens = cat_lens.multihash(dpixs=self.tree_resos[cutfirst:], dpix_hash=self.tree_resos[-1],
1423
+ shuffle=self.shuffle_pix, normed=True, extent=jointextent)
1424
+ lngal_resos, lpos1s, lpos2s, lweights, lzbins, lisinners, lallfields, lindex_matchers, \
1425
+ lpixs_galind_bounds, lpix_gals, ldpixs1_true, ldpixs2_true = mhash_lens
1426
+ ngal_resos_lens = np.array(lngal_resos, dtype=np.int32)
1427
+ weight_resos_lens = np.concatenate(lweights).astype(np.float64)
1428
+ pos1_resos_lens = np.concatenate(lpos1s).astype(np.float64)
1429
+ pos2_resos_lens = np.concatenate(lpos2s).astype(np.float64)
1430
+ zbin_resos_lens = np.concatenate(lzbins).astype(np.int32)
1431
+ isinner_resos_lens = np.concatenate(lisinners).astype(np.float64)
1432
+ index_matcher_lens = np.concatenate(lindex_matchers).astype(np.int32)
1433
+ pixs_galind_bounds_lens = np.concatenate(lpixs_galind_bounds).astype(np.int32)
1434
+ pix_gals_lens = np.asarray(np.concatenate(lpix_gals)).astype(np.int32)
1435
+ index_matcher_flat = np.argwhere(cat_lens.index_matcher>-1).flatten().astype(np.int32)
1436
+ nregions = np.int32(len(index_matcher_flat))
1437
+ if self.method=="Tree":
1438
+ # Collect args
1439
+ args_resoinfo = (np.int32(self.tree_nresos), self.tree_redges,)
1440
+ args_resos_sourcecat = (weight_resos_source, pos1_resos_source, pos2_resos_source,
1441
+ e1_resos_source, e2_resos_source, zbin_resos_source,
1442
+ np.int32(self.nbinsz_source), ngal_resos_source, )
1443
+ args_mhash = (index_matcher_source, pixs_galind_bounds_source, pix_gals_source,
1444
+ index_matcher_lens, pixs_galind_bounds_lens, pix_gals_lens, nregions, )
1445
+ args_pixgrid = (np.float64(cat_lens.pix1_start), np.float64(cat_lens.pix1_d), np.int32(cat_lens.pix1_n),
1446
+ np.float64(cat_lens.pix2_start), np.float64(cat_lens.pix2_d), np.int32(cat_lens.pix2_n), )
1447
+ args = (*args_resoinfo,
1448
+ *args_resos_sourcecat,
1449
+ *args_lenscat,
1450
+ *args_mhash,
1451
+ *args_pixgrid,
1452
+ *args_basesetup,
1453
+ np.int32(self.nthreads),
1454
+ np.int32(self._verbose_c),
1455
+ bin_centers,
1456
+ Upsilon_n,
1457
+ Norm_n, )
1458
+ func = self.clib.alloc_Gammans_tree_NGG
1459
+ if self.method == "DoubleTree":
1460
+ # Collect args
1461
+ args_resoinfo = (np.int32(self.tree_nresos), np.int32(self.tree_nresos-cutfirst),
1462
+ sdpixs1_true.astype(np.float64), sdpixs2_true.astype(np.float64), self.tree_redges, )
1463
+ args_leafs = (np.int32(self.resoshift_leafs), np.int32(self.minresoind_leaf),
1464
+ np.int32(self.maxresoind_leaf), )
1465
+ args_resos = (isinner_resos_source, weight_resos_source, pos1_resos_source, pos2_resos_source,
1466
+ e1_resos_source, e2_resos_source, zbin_resos_source, ngal_resos_source,
1467
+ np.int32(self.nbinsz_source), isinner_resos_lens, weight_resos_lens, pos1_resos_lens,
1468
+ pos2_resos_lens, zbin_resos_lens, ngal_resos_lens, np.int32(self.nbinsz_lens), )
1469
+ args_mhash = (index_matcher_source, pixs_galind_bounds_source, pix_gals_source,
1470
+ index_matcher_lens, pixs_galind_bounds_lens, pix_gals_lens, index_matcher_flat, nregions, )
1471
+ args_pixgrid = (np.float64(cat_lens.pix1_start), np.float64(cat_lens.pix1_d), np.int32(cat_lens.pix1_n),
1472
+ np.float64(cat_lens.pix2_start), np.float64(cat_lens.pix2_d), np.int32(cat_lens.pix2_n), )
1473
+ args = (*args_resoinfo,
1474
+ *args_leafs,
1475
+ *args_resos,
1476
+ *args_basesetup,
1477
+ *args_mhash,
1478
+ *args_pixgrid,
1479
+ np.int32(self.nthreads),
1480
+ np.int32(self._verbose_c),
1481
+ bin_centers,
1482
+ Upsilon_n,
1483
+ Norm_n, )
1484
+ func = self.clib.alloc_Gammans_doubletree_NGG
1485
+ if self._verbose_debug:
1486
+ for elarg, arg in enumerate(args):
1487
+ toprint = (elarg, type(arg),)
1488
+ if isinstance(arg, np.ndarray):
1489
+ toprint += (type(arg[0]), arg.shape)
1490
+ try:
1491
+ toprint += (func.argtypes[elarg], )
1492
+ print(toprint)
1493
+ print(arg)
1494
+ except:
1495
+ print("We did have a problem for arg %i"%elarg)
1496
+
1497
+ func(*args)
1498
+
1499
+ # Components of npcf are ordered as (Ups_-, Ups_+)
1500
+ self.bin_centers = bin_centers.reshape(szr)
1501
+ self.bin_centers_mean = np.mean(self.bin_centers, axis=(0,1))
1502
+ self.npcf_multipoles = Upsilon_n.reshape(sc)
1503
+ self.npcf_multipoles_norm = Norm_n.reshape(sn)
1504
+ self.projection = "X"
1505
+ self.is_edge_corrected = False
1506
+
1507
+ if apply_edge_correction:
1508
+ self.edge_correction()
1509
+
1510
+ if not dotomo_source:
1511
+ cat_source.zbins = old_zbins_source
1512
+ if not dotomo_lens:
1513
+ cat_lens.zbins = old_zbins_lens
1514
+
1515
+ def edge_correction(self, ret_matrices=False):
1516
+
1517
+ assert(not self.is_edge_corrected)
1518
+ def gen_M_matrix(thet1,thet2,threepcf_n_norm):
1519
+ nvals, ntheta, _ = threepcf_n_norm.shape
1520
+ nmax = (nvals-1)//2
1521
+ narr = np.arange(-nmax,nmax+1, dtype=np.int)
1522
+ nextM = np.zeros((nvals,nvals))
1523
+ for ind, ell in enumerate(narr):
1524
+ lminusn = ell-narr
1525
+ sel = np.logical_and(lminusn+nmax>=0, lminusn+nmax<nvals)
1526
+ nextM[ind,sel] = threepcf_n_norm[(lminusn+nmax)[sel],thet1,thet2].real / threepcf_n_norm[nmax,thet1,thet2].real
1527
+ return nextM
1528
+
1529
+ _nvals, nzcombis, ntheta, _ = self.npcf_multipoles_norm.shape
1530
+ nvals = int((_nvals-1)/2)
1531
+ nmax = nvals-1
1532
+ threepcf_n_corr = np.zeros_like(self.npcf_multipoles)
1533
+ if ret_matrices:
1534
+ mats = np.zeros((nzcombis,ntheta,ntheta,nvals,nvals))
1535
+ for indz in range(nzcombis):
1536
+ #sys.stdout.write("%i"%indz)
1537
+ for thet1 in range(ntheta):
1538
+ for thet2 in range(ntheta):
1539
+ nextM = gen_M_matrix(thet1,thet2,self.npcf_multipoles_norm[:,indz])
1540
+ nextM_inv = np.linalg.inv(nextM)
1541
+ if ret_matrices:
1542
+ mats[indz,thet1,thet2] = nextM
1543
+ for el_cf in range(self.n_cfs):
1544
+ threepcf_n_corr[el_cf,:,indz,thet1,thet2] = np.matmul(
1545
+ nextM_inv,self.npcf_multipoles[el_cf,:,indz,thet1,thet2])
1546
+
1547
+ self.npcf_multipoles = threepcf_n_corr
1548
+ self.is_edge_corrected = True
1549
+
1550
+ if ret_matrices:
1551
+ return threepcf_n_corr, mats
1552
+
1553
+ def multipoles2npcf(self, integrated=False):
1554
+ r"""
1555
+ Notes
1556
+ -----
1557
+ * When dividing by the (weighted) counts ``N``, all contributions for which ``N <= 0`` are set to zero.
1558
+
1559
+ """
1560
+ _, nzcombis, rbins, rbins = np.shape(self.npcf_multipoles[0])
1561
+ self.npcf = np.zeros((self.n_cfs, nzcombis, rbins, rbins, len(self.phi)), dtype=complex)
1562
+ self.npcf_norm = np.zeros((nzcombis, rbins, rbins, len(self.phi)), dtype=float)
1563
+ ztiler = np.arange(self.nbinsz_lens*self.nbinsz_source*self.nbinsz_source).reshape(
1564
+ (self.nbinsz_lens,self.nbinsz_source,self.nbinsz_source)).transpose(0,2,1).flatten().astype(np.int32)
1565
+
1566
+ # NGG components
1567
+ for elphi, phi in enumerate(self.phi):
1568
+ tmp = np.zeros((self.n_cfs, nzcombis, rbins, rbins),dtype=complex)
1569
+ tmpnorm = np.zeros((nzcombis, rbins, rbins),dtype=complex)
1570
+ for n in range(2*self.nmax+1):
1571
+ dphi = self.phi[1] - self.phi[0]
1572
+ if integrated:
1573
+ if n==self.nmax:
1574
+ ifac = dphi
1575
+ else:
1576
+ ifac = 2./(n-self.nmax) * np.sin((n-self.nmax)*dphi/2.)
1577
+ else:
1578
+ ifac = dphi
1579
+ _const = 1./(2*np.pi) * np.exp(1J*(n-self.nmax)*phi) * ifac
1580
+ tmpnorm += _const * self.npcf_multipoles_norm[n].astype(complex)
1581
+ for el_cf in range(self.n_cfs):
1582
+ tmp[el_cf] += _const * self.npcf_multipoles[el_cf,n].astype(complex)
1583
+ self.npcf[...,elphi] = tmp
1584
+ self.npcf_norm[...,elphi] = tmpnorm.real
1585
+
1586
+ if self.is_edge_corrected:
1587
+ N0 = dphi/(2*np.pi) * self.npcf_multipoles_norm[self.nmax].astype(complex)
1588
+ sel_zero = np.isnan(N0)
1589
+ _a = self.npcf
1590
+ _b = N0.real[np.newaxis, :, :, :, np.newaxis]
1591
+ self.npcf = np.divide(_a, _b, out=np.zeros_like(_a), where=_b>0)
1592
+ else:
1593
+ _a = self.npcf
1594
+ _b = self.npcf_norm
1595
+ self.npcf = np.divide(_a, _b, out=np.zeros_like(_a), where=_b>0)
1596
+ #self.npcf = self.npcf/self.npcf_norm[0][None, ...].astype(complex)
1597
+ self.projection = "X"
1598
+
1599
+ ## PROJECTIONS ##
1600
+ def projectnpcf(self, projection):
1601
+ super()._projectnpcf(self, projection)
1602
+
1603
+ ## INTEGRATED MEASURES ##
1604
+ def computeNMM(self, radii, do_multiscale=False, tofile=False, filtercache=None):
1605
+ """
1606
+ Compute third-order aperture statistics
1607
+ """
1608
+
1609
+ nb_config.NUMBA_DEFAULT_NUM_THREADS = self.nthreads
1610
+ nb_config.NUMBA_NUM_THREADS = self.nthreads
1611
+
1612
+ if self.npcf is None and self.npcf_multipoles is not None:
1613
+ self.multipoles2npcf()
1614
+
1615
+ nradii = len(radii)
1616
+ if not do_multiscale:
1617
+ nrcombis = nradii
1618
+ _rcut = 1
1619
+ else:
1620
+ nrcombis = nradii*nradii*nradii
1621
+ _rcut = nradii
1622
+ NMM = np.zeros((3, self.nbinsz_lens*self.nbinsz_source*self.nbinsz_source, nrcombis), dtype=complex)
1623
+ tmprcombi = 0
1624
+ for elr1, R1 in enumerate(radii):
1625
+ for elr2, R2 in enumerate(radii[:_rcut]):
1626
+ for elr3, R3 in enumerate(radii[:_rcut]):
1627
+ if not do_multiscale:
1628
+ R2 = R1
1629
+ R3 = R1
1630
+ if filtercache is not None:
1631
+ A_NMM = filtercache[tmprcombi]
1632
+ else:
1633
+ A_NMM = self._NMM_filtergrid(R1, R2, R3)
1634
+ _NMM = np.nansum(A_NMM[0]*self.npcf[0,...],axis=(1,2,3))
1635
+ _NMMstar = np.nansum(A_NMM[1]*self.npcf[1,...],axis=(1,2,3))
1636
+ NMM[0,:,tmprcombi] = (_NMM + _NMMstar).real/2.
1637
+ NMM[1,:,tmprcombi] = (-_NMM + _NMMstar).real/2.
1638
+ NMM[2,:,tmprcombi] = (_NMM + _NMMstar).imag/2.
1639
+ tmprcombi += 1
1640
+ return NMM
1641
+
1642
+ def _NMM_filtergrid(self, R1, R2, R3):
1643
+ return self.__NMM_filtergrid(R1, R2, R3, self.bin_edges, self.bin_centers_mean, self.phi)
1644
+
1645
+ @staticmethod
1646
+ @jit(nopython=True, parallel=True)
1647
+ def __NMM_filtergrid(R1, R2, R3, edges, centers, phis):
1648
+ nbinsr = len(centers)
1649
+ nbinsphi = len(phis)
1650
+ _cphis = np.cos(phis)
1651
+ _ephis = np.e**(1J*phis)
1652
+ _ephisc = np.e**(-1J*phis)
1653
+ Theta4 = 1./3. * (R1**2*R2**2 + R1**2*R3**2 + R2**2*R3**2)
1654
+ a2 = 2./3. * R1**2*R2**2*R3**2 / Theta4
1655
+ ANMM = np.zeros((2,nbinsr,nbinsr,nbinsphi), dtype=nb_complex128)
1656
+ for elb in prange(nbinsr*nbinsr):
1657
+ elb1 = int(elb//nbinsr)
1658
+ elb2 = elb%nbinsr
1659
+ _y1 = centers[elb1]
1660
+ _dbin1 = edges[elb1+1] - edges[elb1]
1661
+ _y2 = centers[elb2]
1662
+ _dbin2 = edges[elb2+1] - edges[elb2]
1663
+ _dbinphi = phis[1] - phis[0]
1664
+
1665
+ csq = a2**2/4. * (_y1**2/R1**4 + _y2**2/R2**4 + 2*_y1*_y2*_cphis/(R1**2*R2**2))
1666
+ b0 = _y1**2/(2*R1**2)+_y2**2/(2*R2**2) - csq/a2
1667
+
1668
+ g1 = _y1 - a2/2. * (_y1/R1**2 + _y2*_ephisc/R2**2)
1669
+ g2 = _y2 - a2/2. * (_y2/R2**2 + _y1*_ephis/R1**2)
1670
+ g1c = g1.conj()
1671
+ g2c = g2.conj()
1672
+ pref = np.e**(-b0)/(72*np.pi*Theta4**2)
1673
+ _h1 = 2*(g2c*_y1+g1*_y2-2*g1*g2c)*(g1*g2c+2*a2*_ephisc)
1674
+ _h2 = 2*a2*(2*R3**2-csq-3*a2)*_ephisc*_ephisc
1675
+ _h3 = 4*g1*g2c*(2*R3**2-csq-2*a2)*_ephisc
1676
+ _h4 = (g1*g2c)**2/a2 * (2*R3**2-csq-a2)
1677
+ 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))
1678
+ sum_MMstarN = pref * (_h1 + _h2 + _h3 + _h4)
1679
+ _measures = _y1*_dbin1 * _y2*_dbin2 * _dbinphi
1680
+
1681
+ ANMM[0,elb1,elb2] = _measures * sum_MMN
1682
+ ANMM[1,elb1,elb2] = _measures * sum_MMstarN
1683
+
1684
+ return ANMM