MeasureIALightcone

measureia.MeasureIALightcone

Bases: MeasureWLightcone, MeasureMultipolesLightcone, MeasureWLightconeJackknife, MeasureMultipolesLightconeJackknife, MeasureJackknife, CheckInput

Manages the IA correlation function measurement methods used in the MeasureIA package based on speed and input. This class is used to call the methods that measure w_gg, w_g+ and multipoles for simulations (and observations), with lightcone data. Depending on the input parameters, various correlations incl covariance estimates are measured for given data.

Attributes:
  • data_dir (dict or NoneType) –

    Temporary storage space for added data directory to allow for flexibility in passing data or randoms to internal methods.

  • num_samples (dict or NoneType) –

    Dictionary containing the numbers of objects for each sample for lightcone-type measurements. Filled internally, no input needed.

Methods:

Name Description
measure_xi_w

Compute projected correlations \(w_{gg}\) and/or \(w_{g+}\).

measure_xi_multipoles

Compute multipoles of the correlation functions, \(\tilde{\xi}_{gg,0}\) and/or \(\tilde{\xi}_{g+,2}\).

Notes

Inherits attributes from 'SimInfo', where none are used in this class. Inherits attributes from 'MeasureIABase', where 'data', 'output_file_name', 'Num_position', 'Num_shape', 'r_min', 'r_max', 'num_bins_r', 'num_bins_pi', 'r_bins', 'pi_bins', 'mu_r_bins' are used.

Source code in src/measureia/measure_IA_lightcone.py
 11
 12
 13
 14
 15
 16
 17
 18
 19
 20
 21
 22
 23
 24
 25
 26
 27
 28
 29
 30
 31
 32
 33
 34
 35
 36
 37
 38
 39
 40
 41
 42
 43
 44
 45
 46
 47
 48
 49
 50
 51
 52
 53
 54
 55
 56
 57
 58
 59
 60
 61
 62
 63
 64
 65
 66
 67
 68
 69
 70
 71
 72
 73
 74
 75
 76
 77
 78
 79
 80
 81
 82
 83
 84
 85
 86
 87
 88
 89
 90
 91
 92
 93
 94
 95
 96
 97
 98
 99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
553
554
555
556
557
558
559
560
561
562
563
564
565
566
567
568
569
570
571
572
573
574
575
576
577
578
579
580
581
582
583
584
585
586
587
588
589
590
591
592
593
594
595
596
597
598
599
600
601
602
603
604
605
606
607
608
609
610
611
612
613
614
615
616
617
618
619
620
621
622
623
624
625
626
627
628
629
630
631
632
633
634
635
636
637
638
639
640
641
642
643
644
645
646
647
648
649
650
651
652
653
654
655
656
657
658
659
660
661
662
663
664
665
666
667
668
669
670
671
672
673
674
675
676
677
678
679
680
681
682
683
684
685
686
687
688
689
690
691
692
693
694
695
696
697
698
699
700
701
702
703
704
705
706
707
708
709
710
711
712
713
714
715
716
717
718
719
720
721
722
723
724
725
726
727
728
729
730
731
732
733
734
735
736
737
738
739
740
741
742
743
744
745
746
747
748
749
750
751
752
753
754
755
756
757
758
759
760
761
762
763
764
765
766
767
768
769
770
771
772
773
774
775
776
777
778
779
780
781
782
783
784
785
786
787
788
789
790
791
792
793
794
795
796
797
798
799
800
801
802
803
804
805
806
807
808
809
810
811
812
813
814
815
816
817
818
819
820
821
822
823
824
825
826
827
828
829
830
831
832
833
834
835
836
837
838
839
840
841
842
843
844
845
846
847
848
849
850
851
852
853
854
855
856
857
858
859
860
861
862
863
864
865
866
867
868
869
870
871
872
873
874
875
876
877
878
879
880
881
882
883
884
885
886
887
888
889
890
891
892
893
894
895
896
897
898
899
900
901
902
903
904
905
906
907
908
909
class MeasureIALightcone(MeasureWLightcone, MeasureMultipolesLightcone, MeasureWLightconeJackknife,
						 MeasureMultipolesLightconeJackknife, MeasureJackknife, CheckInput):
	r"""Manages the IA correlation function measurement methods used in the MeasureIA package based on speed and input.
	This class is used to call the methods that measure w_gg, w_g+ and multipoles for simulations (and observations),
	with lightcone data.
	Depending on the input parameters, various correlations incl covariance estimates are measured for given data.

	Attributes
	----------
	data_dir : dict or NoneType
		Temporary storage space for added data directory to allow for flexibility in passing data or randoms to internal
		methods.
	num_samples : dict or NoneType
		Dictionary containing the numbers of objects for each sample for lightcone-type measurements. Filled internally,
		no input needed.

	Methods
	-------
	measure_xi_w()
		Compute projected correlations $w_{gg}$ and/or $w_{g+}$.
	measure_xi_multipoles()
		Compute multipoles of the correlation functions, $\tilde{\xi}_{gg,0}$ and/or $\tilde{\xi}_{g+,2}$.

	Notes
	-----
	Inherits attributes from 'SimInfo', where none are used in this class.
	Inherits attributes from 'MeasureIABase', where 'data', 'output_file_name', 'Num_position',
	'Num_shape', 'r_min', 'r_max', 'num_bins_r', 'num_bins_pi', 'r_bins', 'pi_bins', 'mu_r_bins' are used.

	"""

	def __init__(
			self,
			data,
			randoms_data,
			output_file_name,
			separation_limits=[0.1, 20.0],
			num_bins_r=8,
			num_bins_pi=20,
			pi_max=None,
			num_nodes=1,
			RA_density_sample_name="RA",
			RA_shape_sample_name="RA_shape_sample",
			DEC_density_sample_name="DEC",
			DEC_shape_sample_name="DEC_shape_sample",
			redshift_density_sample_name="Redshift",
			redshift_shape_sample_name="Redshift_shape_sample",
			e1_name="e1",
			e2_name="e2",
			weight_density_sample_name="weight",
			weight_shape_sample_name="weight_shape_sample",
	):
		"""
		The __init__ method of the MeasureIALightcone class.

		Parameters
		----------
		randoms_data : dict or NoneType
			Dictionary with data of the randoms needed for lightcone-type measurements.
			The keywords are:
			'Redshift' and 'Redshift_shape_sample': (N_p) and (N_s) ndarray with redshifts of position and shape samples.
			'RA' and 'RA_shape_sample': (N_p) and (N_s) ndarray with RA coordinate of position and shape samples.
			'DEC' and 'DEC_shape_sample': (N_p) and (N_s) ndarray with DEC coordinate of position and shape samples.
			If only 'Redshift', 'RA' and 'DEC' are added, the sample will be used for both position and shape sample randoms.
		num_nodes : int, optional
			Number of cores to be used in multiprocessing. Default is 1.
		RA_density_sample_name : str, optional
			Name of the key in the data (and randoms) dictionary that contains the RA of the density sample.
		RA_shape_sample_name : str, optional
			Name of the key in the data (and randoms) dictionary that contains the RA of the shape sample.
		DEC_density_sample_name : str, optional
			Name of the key in the data (and randoms) dictionary that contains the DEC of the density sample.
		DEC_shape_sample_name : str, optional
			Name of the key in the data (and randoms) dictionary that contains the DEC of the shape sample.
		redshift_density_sample_name : str, optional
			Name of the key in the data (and randoms) dictionary that contains the redshift of the density sample.
		redshift_shape_sample_name : str, optional
			Name of the key in the data (and randoms) dictionary that contains the redshift of the shape sample.
		e1_name : str, optional
			Name of the key in the data dictionary that contains the first ellipticity component of the shape sample.
		e2_name : str, optional
			Name of the key in the data dictionary that contains the second ellipticity component of the shape sample.
		weight_density_sample_name : str, optional
			Name of the key in the data (and randoms) dictionary that contains the weights of the density sample.
		weight_shape_sample_name : str, optional
			Name of the key in the data (and randoms) dictionary that contains the weights of the shape sample.

		Notes
		-----
		Constructor parameters 'data', 'output_file_name', 'separation_limits', 'num_bins_r',
		'num_bins_pi', 'pi_max', are passed to MeasureIABase.
		The data, randoms and mask dictionaries may use any key names; they are given through the *_name
		parameters and translated to the internal default names on input.

		"""
		self._input_name_map = {
			RA_density_sample_name: "RA",
			RA_shape_sample_name: "RA_shape_sample",
			DEC_density_sample_name: "DEC",
			DEC_shape_sample_name: "DEC_shape_sample",
			redshift_density_sample_name: "Redshift",
			redshift_shape_sample_name: "Redshift_shape_sample",
			e1_name: "e1",
			e2_name: "e2",
			weight_density_sample_name: "weight",
			weight_shape_sample_name: "weight_shape_sample",
		}
		if output_file_name is not None:
			self.check_paths([output_file_name])
		if data is not None:
			self.check_dict(data, [RA_density_sample_name, RA_shape_sample_name, DEC_density_sample_name,
								   DEC_shape_sample_name, redshift_density_sample_name, redshift_shape_sample_name,
								   e1_name, e2_name])
			self.check_type_input_data_lightcone(data, (RA_density_sample_name, RA_shape_sample_name,
														DEC_density_sample_name, DEC_shape_sample_name,
														redshift_density_sample_name, redshift_shape_sample_name,
														e1_name, e2_name))
			data = self.rename_input_keys(data, self._input_name_map)
		if randoms_data is not None:
			self.check_dict(randoms_data,
							[RA_density_sample_name, DEC_density_sample_name, redshift_density_sample_name])
			randoms_data = self.rename_input_keys(randoms_data, self._input_name_map)
		super().__init__(data, output_file_name, False, None, separation_limits, num_bins_r, num_bins_pi,
						 pi_max, None, False)
		if not (isinstance(num_nodes, (int, np.integer)) and not isinstance(num_nodes, bool) and num_nodes >= 1):
			raise ValueError(f"num_nodes must be an integer >= 1, got {num_nodes!r}.")
		self.num_nodes = num_nodes
		self.randoms_data = randoms_data
		self.data_dir = None
		self.num_samples = None

		return

	def _merged_masks(self, masks_position, masks_shape):
		"""Combines the masks for a pair-count pass where the position and shape slots of self.data may hold
		different samples (data or randoms). Each slot's mask is taken from the mask dictionary of the sample
		that occupies it. Must be called after self.data has been set for the pass.

		Parameters
		----------
		masks_position : dict or NoneType
			Mask dictionary of the sample occupying the position slots ('Redshift', 'RA', 'DEC', 'weight').
		masks_shape : dict or NoneType
			Mask dictionary of the sample occupying the shape slots ('*_shape_sample', 'e1', 'e2').

		Returns
		-------
		dict or NoneType
			Combined mask dictionary, or None if no mask applies to either slot. A missing dictionary means no
			selection (all True); missing keys default to the slot's coordinate mask so all fields of one
			sample stay aligned.

		"""
		if masks_position is None and masks_shape is None:
			return None
		if masks_position is None:
			masks_position = {}
		if masks_shape is None:
			masks_shape = {}
		pos_default = np.ones(len(self.data["RA"]), dtype=bool)
		shape_default = np.ones(len(self.data["RA_shape_sample"]), dtype=bool)
		pos_mask = masks_position.get("RA", pos_default)
		shape_mask = masks_shape.get("RA_shape_sample", shape_default)
		merged = {}
		for key in ("Redshift", "RA", "DEC", "weight"):
			merged[key] = masks_position.get(key, pos_mask)
		for key in ("Redshift_shape_sample", "RA_shape_sample", "DEC_shape_sample", "weight_shape_sample",
					"e1", "e2"):
			merged[key] = masks_shape.get(key, shape_mask)
		return merged

	@staticmethod
	def _field_mask(masks, key, coordinate_key, length):
		"""Selects the mask for one field, following the same defaulting rule as '_merged_masks': a missing
		mask dictionary means no selection, and a key that is absent from the dictionary falls back to its
		sample's coordinate mask so all fields of one sample stay aligned.

		Parameters
		----------
		masks : dict or NoneType
			Mask dictionary supplied by the user, or None for no selection.
		key : str
			Field whose mask is requested.
		coordinate_key : str
			Coordinate field of the sample that 'key' belongs to ('RA' or 'RA_shape_sample').
		length : int
			Length of the sample, used to build the all-True default.

		Returns
		-------
		ndarray
			Boolean mask for the requested field.

		"""
		if masks is None:
			return np.ones(length, dtype=bool)
		if key in masks:
			return masks[key]
		if coordinate_key in masks:
			return masks[coordinate_key]
		return np.ones(length, dtype=bool)

	def _sample_coordinates(self, masks, masks_randoms):
		"""Builds the masked (RA, DEC) coordinate pairs of the position and shape samples and the masked
		random sample sizes, used to work out the sample-size normalisation of the estimator.

		Parameters
		----------
		masks : dict or NoneType
			Mask dictionary for the data sample.
		masks_randoms : dict or NoneType
			Mask dictionary for the randoms.

		Returns
		-------
		ndarray, ndarray, int, int
			Position-sample coordinates, shape-sample coordinates, number of randoms in the position slot,
			number of randoms in the shape slot.

		"""
		n_D = len(self.data_dir["RA"])
		n_S = len(self.data_dir["RA_shape_sample"])
		coords_D = np.column_stack((self.data_dir["RA"][self._field_mask(masks, "RA", "RA", n_D)],
									self.data_dir["DEC"][self._field_mask(masks, "DEC", "RA", n_D)]))
		coords_S = np.column_stack((
			self.data_dir["RA_shape_sample"][
				self._field_mask(masks, "RA_shape_sample", "RA_shape_sample", n_S)],
			self.data_dir["DEC_shape_sample"][
				self._field_mask(masks, "DEC_shape_sample", "RA_shape_sample", n_S)]))
		n_RD = len(self.randoms_data["RA"])
		n_RS = len(self.randoms_data["RA_shape_sample"])
		num_R_D = len(self.randoms_data["RA"][self._field_mask(masks_randoms, "RA", "RA", n_RD)])
		num_R_S = len(self.randoms_data["RA_shape_sample"][
						  self._field_mask(masks_randoms, "RA_shape_sample", "RA_shape_sample", n_RS)])
		return coords_D, coords_S, num_R_D, num_R_S

	def measure_xi_helper(self, method_count_pairs, method_shape_correlation, IA_estimator, dataset_name, corr_type,
						  masks=None, masks_randoms=None, cosmology=None, over_h=False, chunk_size=1000, num_nodes=1,
						  temp_file_path=None):
		# Shape-position combinations:
		# S+D (Cg+, Gg+)
		# S+R (Cg+, Gg+)
		if corr_type == "g+" or corr_type == "both":
			# S+D
			self.data = self.data_dir
			method_shape_correlation(masks=self._merged_masks(masks, masks), dataset_name=dataset_name,
									 over_h=over_h, data_suffix="_SplusD",
									 cosmology=cosmology, chunk_size=chunk_size, num_nodes=num_nodes,
									 temp_file_path=temp_file_path)
			# S+R
			self.data = {
				"Redshift": self.randoms_data["Redshift"],
				"Redshift_shape_sample": self.data_dir["Redshift_shape_sample"],
				"RA": self.randoms_data["RA"],
				"RA_shape_sample": self.data_dir["RA_shape_sample"],
				"DEC": self.randoms_data["DEC"],
				"DEC_shape_sample": self.data_dir["DEC_shape_sample"],
				"e1": self.data_dir["e1"],
				"e2": self.data_dir["e2"],
				"weight": self.randoms_data["weight"],
				"weight_shape_sample": self.data_dir["weight_shape_sample"]
			}
			# print(self.data)
			method_shape_correlation(masks=self._merged_masks(masks_randoms, masks), dataset_name=f"{dataset_name}",
									 over_h=over_h, data_suffix="_SplusR",
									 cosmology=cosmology, chunk_size=chunk_size, num_nodes=num_nodes,
									 temp_file_path=temp_file_path)

		# Position-position combinations:
		# SD (Cgg, Ggg)
		# SR (Cg+, Cgg, Ggg)
		# RD (Cgg, Ggg)
		# RR (Cgg, Gg+, Ggg)

		if corr_type == "gg":  # already have it for 'both'
			# SD (Cgg, Ggg)
			self.data = {
				"Redshift": self.data_dir["Redshift"],
				"Redshift_shape_sample": self.data_dir["Redshift_shape_sample"],
				"RA": self.data_dir["RA"],
				"RA_shape_sample": self.data_dir["RA_shape_sample"],
				"DEC": self.data_dir["DEC"],
				"DEC_shape_sample": self.data_dir["DEC_shape_sample"],
				"weight": self.data_dir["weight"],
				"weight_shape_sample": self.data_dir["weight_shape_sample"]
			}
			method_count_pairs(masks=self._merged_masks(masks, masks), dataset_name=dataset_name, over_h=over_h,
							   cosmology=cosmology,
							   data_suffix="_DD", chunk_size=chunk_size, num_nodes=num_nodes,
							   temp_file_path=temp_file_path)

			# SR (Cg+, Cgg, Ggg) - watch name (Obs estimator) # if g+ or both, already have it
			self.data = {
				"Redshift": self.randoms_data["Redshift"],
				"Redshift_shape_sample": self.data_dir["Redshift_shape_sample"],
				"RA": self.randoms_data["RA"],
				"RA_shape_sample": self.data_dir["RA_shape_sample"],
				"DEC": self.randoms_data["DEC"],
				"DEC_shape_sample": self.data_dir["DEC_shape_sample"],
				"weight": self.randoms_data["weight"],
				"weight_shape_sample": self.data_dir["weight_shape_sample"]
			}
			method_count_pairs(masks=self._merged_masks(masks_randoms, masks), dataset_name=dataset_name,
							   over_h=over_h, cosmology=cosmology,
							   data_suffix="_SR", chunk_size=chunk_size, num_nodes=num_nodes,
							   temp_file_path=temp_file_path)

		if corr_type == "gg" or corr_type == "both":
			# RD (Cgg, Ggg)
			self.data = {
				"Redshift": self.data_dir["Redshift"],
				"Redshift_shape_sample": self.randoms_data["Redshift_shape_sample"],
				"RA": self.data_dir["RA"],
				"RA_shape_sample": self.randoms_data["RA_shape_sample"],
				"DEC": self.data_dir["DEC"],
				"DEC_shape_sample": self.randoms_data["DEC_shape_sample"],
				"weight": self.data_dir["weight"],
				"weight_shape_sample": self.randoms_data["weight_shape_sample"]
			}
			method_count_pairs(masks=self._merged_masks(masks, masks_randoms), dataset_name=dataset_name,
							   over_h=over_h, cosmology=cosmology,
							   data_suffix="_RD", chunk_size=chunk_size, num_nodes=num_nodes,
							   temp_file_path=temp_file_path)

		if IA_estimator == "galaxies" or corr_type == "gg" or corr_type == "both":
			# RR (Cgg, Gg+, Ggg)
			self.data = {
				"Redshift": self.randoms_data["Redshift"],
				"Redshift_shape_sample": self.randoms_data["Redshift_shape_sample"],
				"RA": self.randoms_data["RA"],
				"RA_shape_sample": self.randoms_data["RA_shape_sample"],
				"DEC": self.randoms_data["DEC"],
				"DEC_shape_sample": self.randoms_data["DEC_shape_sample"],
				"weight": self.randoms_data["weight"],
				"weight_shape_sample": self.randoms_data["weight_shape_sample"]
			}
			method_count_pairs(masks=self._merged_masks(masks_randoms, masks_randoms), dataset_name=dataset_name,
							   over_h=over_h, cosmology=cosmology,
							   data_suffix="_RR", chunk_size=chunk_size, num_nodes=num_nodes,
							   temp_file_path=temp_file_path)
		return

	def measure_xi_jk_helper(self, method_count_pairs, method_shape_correlation, IA_estimator, dataset_name,
							 corr_type, jk_patches=None, masks=None, masks_randoms=None, cosmology=None, over_h=False,
							 chunk_size=1000, num_nodes=1, temp_file_path=None):
		num_jk = max(jk_patches["shape"]) - min(jk_patches["shape"]) + 1
		# Shape-position combinations:
		# S+D (Cg+, Gg+)
		# S+R (Cg+, Gg+)
		if corr_type == "g+" or corr_type == "both":
			# S+D
			self.data = self.data_dir
			method_shape_correlation(jackknife_region_indices_pos=jk_patches["position"],
									 jackknife_region_indices_shape=jk_patches["shape"],
									 masks=self._merged_masks(masks, masks),
									 dataset_name=dataset_name,
									 jk_group_name=f"{dataset_name}_jk{num_jk}",
									 over_h=over_h, data_suffix="_SplusD",
									 cosmology=cosmology, chunk_size=chunk_size, num_nodes=num_nodes,
									 temp_file_path=temp_file_path)
			# S+R
			self.data = {
				"Redshift": self.randoms_data["Redshift"],
				"Redshift_shape_sample": self.data_dir["Redshift_shape_sample"],
				"RA": self.randoms_data["RA"],
				"RA_shape_sample": self.data_dir["RA_shape_sample"],
				"DEC": self.randoms_data["DEC"],
				"DEC_shape_sample": self.data_dir["DEC_shape_sample"],
				"e1": self.data_dir["e1"],
				"e2": self.data_dir["e2"],
				"weight": self.randoms_data["weight"],
				"weight_shape_sample": self.data_dir["weight_shape_sample"]
			}
			method_shape_correlation(jackknife_region_indices_pos=jk_patches["randoms_position"],
									 jackknife_region_indices_shape=jk_patches["shape"],
									 masks=self._merged_masks(masks_randoms, masks),
									 dataset_name=f"{dataset_name}", data_suffix="_SplusR",
									 over_h=over_h, jk_group_name=f"{dataset_name}_jk{num_jk}",
									 cosmology=cosmology, chunk_size=chunk_size, num_nodes=num_nodes,
									 temp_file_path=temp_file_path)

		# Position-position combinations:
		# SD (Cgg, Ggg)
		# SR (Cg+, Cgg, Ggg)
		# RD (Cgg, Ggg)
		# RR (Cgg, Gg+, Ggg)

		if corr_type == "gg":  # already have it for 'both'
			# SD (Cgg, Ggg)
			self.data = {
				"Redshift": self.data_dir["Redshift"],
				"Redshift_shape_sample": self.data_dir["Redshift_shape_sample"],
				"RA": self.data_dir["RA"],
				"RA_shape_sample": self.data_dir["RA_shape_sample"],
				"DEC": self.data_dir["DEC"],
				"DEC_shape_sample": self.data_dir["DEC_shape_sample"],
				"weight": self.data_dir["weight"],
				"weight_shape_sample": self.data_dir["weight_shape_sample"]
			}
			method_count_pairs(jackknife_region_indices_pos=jk_patches["position"],
							   jackknife_region_indices_shape=jk_patches["shape"],
							   masks=self._merged_masks(masks, masks), dataset_name=dataset_name, over_h=over_h,
							   cosmology=cosmology,
							   jk_group_name=f"{dataset_name}_jk{num_jk}",
							   data_suffix="_DD", chunk_size=chunk_size, num_nodes=num_nodes,
							   temp_file_path=temp_file_path)

			# SR (Cg+, Cgg, Ggg) - watch name (Obs estimator) # if g+ or both, already have it
			self.data = {
				"Redshift": self.randoms_data["Redshift"],
				"Redshift_shape_sample": self.data_dir["Redshift_shape_sample"],
				"RA": self.randoms_data["RA"],
				"RA_shape_sample": self.data_dir["RA_shape_sample"],
				"DEC": self.randoms_data["DEC"],
				"DEC_shape_sample": self.data_dir["DEC_shape_sample"],
				"weight": self.randoms_data["weight"],
				"weight_shape_sample": self.data_dir["weight_shape_sample"]
			}
			method_count_pairs(jackknife_region_indices_pos=jk_patches["randoms_position"],
							   jackknife_region_indices_shape=jk_patches["shape"],
							   masks=self._merged_masks(masks_randoms, masks), dataset_name=dataset_name,
							   over_h=over_h, cosmology=cosmology,
							   jk_group_name=f"{dataset_name}_jk{num_jk}",
							   data_suffix="_SR", chunk_size=chunk_size, num_nodes=num_nodes,
							   temp_file_path=temp_file_path)

		if corr_type == "gg" or corr_type == "both":
			# RD (Cgg, Ggg)
			self.data = {
				"Redshift": self.data_dir["Redshift"],
				"Redshift_shape_sample": self.randoms_data["Redshift_shape_sample"],
				"RA": self.data_dir["RA"],
				"RA_shape_sample": self.randoms_data["RA_shape_sample"],
				"DEC": self.data_dir["DEC"],
				"DEC_shape_sample": self.randoms_data["DEC_shape_sample"],
				"weight": self.data_dir["weight"],
				"weight_shape_sample": self.randoms_data["weight_shape_sample"]
			}
			method_count_pairs(jackknife_region_indices_pos=jk_patches["position"],
							   jackknife_region_indices_shape=jk_patches["randoms_shape"],
							   masks=self._merged_masks(masks, masks_randoms), dataset_name=dataset_name,
							   over_h=over_h, cosmology=cosmology,
							   jk_group_name=f"{dataset_name}_jk{num_jk}",
							   data_suffix="_RD", chunk_size=chunk_size, num_nodes=num_nodes,
							   temp_file_path=temp_file_path)

		if IA_estimator == "galaxies" or corr_type == "gg" or corr_type == "both":
			# RR (Cgg, Gg+, Ggg)
			self.data = {
				"Redshift": self.randoms_data["Redshift"],
				"Redshift_shape_sample": self.randoms_data["Redshift_shape_sample"],
				"RA": self.randoms_data["RA"],
				"RA_shape_sample": self.randoms_data["RA_shape_sample"],
				"DEC": self.randoms_data["DEC"],
				"DEC_shape_sample": self.randoms_data["DEC_shape_sample"],
				"weight": self.randoms_data["weight"],
				"weight_shape_sample": self.randoms_data["weight_shape_sample"]
			}
			method_count_pairs(jackknife_region_indices_pos=jk_patches["randoms_position"],
							   jackknife_region_indices_shape=jk_patches["randoms_shape"],
							   masks=self._merged_masks(masks_randoms, masks_randoms), dataset_name=dataset_name,
							   over_h=over_h, cosmology=cosmology,
							   jk_group_name=f"{dataset_name}_jk{num_jk}",
							   data_suffix="_RR", chunk_size=chunk_size, num_nodes=num_nodes,
							   temp_file_path=temp_file_path)

		return

	@worker_pool.pooled
	def measure_xi_w(self, IA_estimator, dataset_name, corr_type, jk_patches=None, num_jk=None,
					 masks=None, masks_randoms=None, cosmology=None, over_h=False, tree=True,
					 chunk_size=1000, temp_file_path=None, seed=None, responsivity=False):
		"""Measures xi_gg, xi_g+ and w_gg, w_g+ including jackknife covariance if desired for lightcone data.
		Manages the various _measure_xi_rp_pi_obs and _measure_jackknife_covariance options in MeasureWObservations
		and MeasureJackknife.

		Parameters
		----------
		IA_estimator : str
			Choose which type of xi estimator is used. Choose from "clusters" or "galaxies".
		dataset_name : str
			Name of the dataset in the output file.
		corr_type : str
			Type of correlation to be measured. Choose from [g+, gg, both].
		jk_patches : dict or NoneType, optional
			Dictionary with entries of the jackknife patch numbers (ndarray) for each sample, named "position", "shape"
			and "random". Default is None.
		num_jk : int, optional
			Number of jackknife patches to be generated internally. Default is None (no covariance).
			The jackknife covariance is measured whenever jk_patches is given, or num_jk is greater
			than 0; pass neither (or num_jk=0) to skip it.
		masks : dict or NoneType, optional
			Dictionary of mask information in the same form as the data dictionary, where the masks are placed over
			the data to apply selections. Default is None.
		masks_randoms : dict or NoneType, optional
			Dictionary of mask information for the randoms data in the same form as the data dictionary,
			where the masks are placed over the data to apply selections. Default is None.
		cosmology : pyccl cosmology object or NoneType, optional
			Pyccl cosmology to use in the calculation. If None (default), the cosmology is used:
			ccl.Cosmology(Omega_c=0.225, Omega_b=0.045, sigma8=0.8, h=0.7, n_s=1.0)
		over_h : bool, optional
			If True, the units are assumed to be in not-over-h and converted to over-h units. Default is False.
		tree : bool, optional
			If True (default), pair counts use a KDTree backend; if False, a brute-force pair count is used.
			Default is True.
		chunk_size: int, optional
			Size of the chunks of data sent to each multiprocessing node. If larger, more RAM is needed per node.
			Default is 1000.
		temp_file_path : str or NoneType, optional
			Path to where the data is temporarily stored during multiprocessing [file name generated
			automatically]. Default is None.
		seed : int or NoneType, optional
			Seed for the internal jackknife patch generation (used only when num_jk is given), making the patch
			assignment reproducible. Default is None.
		responsivity : bool, optional
			If True, the g+ shape signal is calibrated by dividing by the responsivity factor 2R, with
			R = <w (1 - e^2 / 2)> / <w> the weighted shear responsivity. Default is False, appropriate when the
			input e1/e2 are already calibrated shears (e.g. from a shear catalogue); set to True for raw
			distortions. Only the g+ correlations are affected; the clustering (gg) signal is unchanged.
			Default is False.

		"""
		if IA_estimator == "clusters":
			if self.randoms_data == None:
				print("No randoms given, correlation defined as S+D/DD")
				raise KeyError("This version does not work yet, add randoms.")
			else:
				print("xi_g+ defined as S+D/SD - S+R/SR, xi_gg as (SD - RD - SR)/RR + 1")
				if masks != None and masks_randoms == None:
					print("Warning, masks given for data vector but not for randoms.")
		elif IA_estimator == "galaxies":
			if self.randoms_data == None:
				raise KeyError("No randoms given. Please provide input.")
			else:
				print("xi_g+ defined as (S+D - S+R)/RR, xi_gg as (SD - RD - SR)/RR + 1")
				if masks != None and masks_randoms == None:
					print("Warning, masks given for data vector but not for randoms.")
		else:
			raise KeyError("Unknown input for IA_estimator, choose from [clusters, galaxies].")

		self.responsivity_correction = responsivity
		masks = self.rename_input_keys(masks, self._input_name_map)
		masks_randoms = self.rename_input_keys(masks_randoms, self._input_name_map)
		# todo: Expand to include methods with trees and internal multiproc
		# todo: Checks to see if data directories include everything they need
		data = self.data  # temporary save so it can be restored at the end of the calculation

		try:  # Are there one or two random samples given?
			random_shape = self.randoms_data["RA_shape_sample"]
			one_random_sample = False
		except (KeyError, TypeError):  # TypeError: randoms_data is None
			one_random_sample = True
			self.randoms_data["RA_shape_sample"] = self.randoms_data["RA"]
			self.randoms_data["DEC_shape_sample"] = self.randoms_data["DEC"]
			self.randoms_data["Redshift_shape_sample"] = self.randoms_data["Redshift"]
		if "weight" not in self.randoms_data:
			self.randoms_data["weight"] = np.ones(len(self.randoms_data["RA"]))
		if "weight_shape_sample" not in self.randoms_data:
			if one_random_sample:
				self.randoms_data["weight_shape_sample"] = self.randoms_data["weight"]  # in case weights are given
			else:
				self.randoms_data["weight_shape_sample"] = np.ones(len(self.randoms_data["RA_shape_sample"]))

		# Covariance is requested by supplying patches or a positive patch count; num_jk=0/None
		# with no jk_patches means "no covariance", matching MeasureIABox's num_jk=0.
		measure_cov = jk_patches is not None or (num_jk is not None and num_jk > 0)
		if measure_cov:
			if jk_patches is None:
				jk_patches = self.assign_jackknife_patches(data, self.randoms_data, num_jk, seed=seed)
			else:
				if one_random_sample:
					jk_patches["randoms_position"] = jk_patches["randoms"]
					jk_patches["randoms_shape"] = jk_patches["randoms"]
			min_patch = min(jk_patches["shape"])
			max_patch = max(jk_patches["shape"])
			num_jk = max_patch - min_patch + 1
			if min_patch != 0:
				raise ValueError(
					f"Jackknife patch indices must start at 0 (minimum patch index found: {min_patch}). "
					"Renumber jk_patches, e.g. patches -= patches.min().")

		self.data_dir = data
		if "weight" not in self.data_dir:
			self.data_dir["weight"] = np.ones(len(self.data_dir["RA"]))
		if "weight_shape_sample" not in self.data_dir:
			self.data_dir["weight_shape_sample"] = np.ones(len(self.data_dir["RA_shape_sample"]))

		# Sample sizes are needed to correct for a different number of randoms and galaxies/clusters in the
		# data. Masks are resolved per field, defaulting to the sample's coordinate mask (see _field_mask).
		num_samples = {}
		coords_D, coords_S, num_R_D, num_R_S = self._sample_coordinates(masks, masks_randoms)
		# Use a structured view so np.intersect1d compares full pairs
		D_view = coords_D.view([('', coords_D.dtype)] * 2)
		S_view = coords_S.view([('', coords_S.dtype)] * 2)

		overlap, ind_D, ind_S = np.intersect1d(D_view, S_view, return_indices=True)

		num_samples["D"] = len(coords_D)
		num_samples["S"] = len(coords_S)
		num_samples["D_S"] = len(overlap)
		num_samples["R_D"] = num_R_D
		num_samples["R_S"] = num_R_S

		if measure_cov:
			if self.num_nodes == 1:
				if tree:
					self.measure_xi_jk_helper(self._count_pairs_xi_rp_pi_lightcone_jk_tree,
											  self._measure_xi_rp_pi_lightcone_jk_tree, IA_estimator, dataset_name,
											  corr_type, jk_patches=jk_patches, masks=masks,
											  masks_randoms=masks_randoms, cosmology=cosmology, over_h=over_h,
											  chunk_size=chunk_size, num_nodes=self.num_nodes,
											  temp_file_path=temp_file_path)
				else:
					self.measure_xi_jk_helper(self._count_pairs_xi_rp_pi_lightcone_jk_brute,
											  self._measure_xi_rp_pi_lightcone_jk_brute, IA_estimator, dataset_name,
											  corr_type, jk_patches=jk_patches, masks=masks,
											  masks_randoms=masks_randoms, cosmology=cosmology, over_h=over_h,
											  chunk_size=chunk_size, num_nodes=self.num_nodes,
											  temp_file_path=temp_file_path)
			else:
				self.measure_xi_jk_helper(self._count_pairs_xi_rp_pi_lightcone_jk_multiprocessing,
										  self._measure_xi_rp_pi_lightcone_jk_multiprocessing, IA_estimator,
										  dataset_name, corr_type, jk_patches=jk_patches, masks=masks,
										  masks_randoms=masks_randoms, cosmology=cosmology, over_h=over_h,
										  chunk_size=chunk_size, num_nodes=self.num_nodes,
										  temp_file_path=temp_file_path)
			self._obs_estimator([corr_type, "w"], IA_estimator, dataset_name, num_samples)
			self._measure_w_g_i(corr_type=corr_type, dataset_name=dataset_name, return_output=False)
			print(num_samples)
			for i in np.arange(num_jk):
				overlap_i = np.where(jk_patches["position"][ind_D] == (i + min_patch))
				num_samples_i = {
					"S": num_samples["S"] - sum(jk_patches["shape"] == (i + min_patch)),
					"D": num_samples["D"] - sum(jk_patches["position"] == (i + min_patch)),
					"R_S": num_samples["R_S"] - sum(jk_patches["randoms_shape"] == (i + min_patch)),
					"R_D": num_samples["R_D"] - sum(jk_patches["randoms_position"] == (i + min_patch)),
					"D_S": num_samples["D_S"] - len(overlap_i)
				}
				self._obs_estimator([corr_type, "w"], IA_estimator, f"{dataset_name}_{i}",
									num_samples_i, jk_group_name=f"{dataset_name}_jk{num_jk}")

				self._measure_w_g_i(corr_type=corr_type, dataset_name=f"{dataset_name}_{i}",
									jk_group_name=f"{dataset_name}_jk{num_jk}", return_output=False)
			if corr_type == "both":
				corr_group = ["w_g_plus", "w_gg"]
			elif corr_type == "g+":
				corr_group = ["w_g_plus"]
			elif corr_type == "gg":
				corr_group = ["w_gg"]
			else:
				raise KeyError("Unknown value for corr_type. Choose from [g+, gg, both]")
			self._combine_jackknife_information(dataset_name=dataset_name, jk_group_name=f"{dataset_name}_jk{num_jk}",
												corr_group=corr_group, num_box=num_jk)
		else:
			if self.num_nodes > 1:
				# Full-sample multiprocessing. This branch did not exist before
				# 0.5.0: num_nodes was accepted, passed down, and then ignored,
				# because only the jackknife path had an mp implementation
				# (benchmarks/FINDINGS.md F4). tree/brute is not consulted here
				# because the mp backend is the tree algorithm by construction.
				self.measure_xi_helper(self._count_pairs_xi_rp_pi_lightcone_multiprocessing,
									   self._measure_xi_rp_pi_lightcone_multiprocessing,
									   IA_estimator, dataset_name, corr_type, masks=masks,
									   masks_randoms=masks_randoms, cosmology=cosmology, over_h=over_h,
									   chunk_size=chunk_size, num_nodes=self.num_nodes,
									   temp_file_path=temp_file_path)
			elif tree:
				self.measure_xi_helper(self._count_pairs_xi_rp_pi_lightcone_tree,
									   self._measure_xi_rp_pi_lightcone_tree,
									   IA_estimator, dataset_name, corr_type, masks=masks,
									   masks_randoms=masks_randoms, cosmology=cosmology, over_h=over_h,
									   chunk_size=chunk_size, num_nodes=self.num_nodes,
									   temp_file_path=temp_file_path)
			else:
				self.measure_xi_helper(self._count_pairs_xi_rp_pi_lightcone_brute,
									   self._measure_xi_rp_pi_lightcone_brute,
									   IA_estimator, dataset_name, corr_type, masks=masks,
									   masks_randoms=masks_randoms, cosmology=cosmology, over_h=over_h,
									   chunk_size=chunk_size, num_nodes=self.num_nodes,
									   temp_file_path=temp_file_path)
			self._obs_estimator([corr_type, "w"], IA_estimator, dataset_name, num_samples)
			self._measure_w_g_i(corr_type=corr_type, dataset_name=dataset_name, return_output=False)

		self.data = data
		return

	@worker_pool.pooled
	def measure_xi_multipoles(self, IA_estimator, dataset_name, corr_type, jk_patches=None, num_jk=None,
							  masks=None, masks_randoms=None, cosmology=None, over_h=False,
							  tree=True, chunk_size=1000, temp_file_path=None, seed=None, responsivity=False):
		"""Measures xi_gg, xi_g+ and multipoles including jackknife covariance if desired for lightcone data.
		Manages the various _measure_xi_rp_pi_obs and _measure_jackknife_covariance options in MeasureWObservations
		and MeasureJackknife.

		Parameters
		----------
		IA_estimator : str
			Choose which type of xi estimator is used. Choose from "clusters" or "galaxies".
		dataset_name : str
			Name of the dataset in the output file.
		corr_type : str
			Type of correlation to be measured. Choose from [g+, gg, both].
		jk_patches : dict or NoneType, optional
			Dictionary with entries of the jackknife patch numbers (ndarray) for each sample, named "position", "shape"
			and "random". Default is None.
		num_jk : int, optional
			Number of jackknife patches to be generated internally. Default is None (no covariance).
			The jackknife covariance is measured whenever jk_patches is given, or num_jk is greater
			than 0; pass neither (or num_jk=0) to skip it.
		masks : dict or NoneType, optional
			Dictionary of mask information in the same form as the data dictionary, where the masks are placed over
			the data to apply selections. Default is None.
		masks_randoms : dict or NoneType, optional
			Dictionary of mask information for the randoms data in the same form as the data dictionary,
			where the masks are placed over the data to apply selections. Default is None.
		cosmology : pyccl cosmology object or NoneType, optional
			Pyccl cosmology to use in the calculation. If None (default), the cosmology is used:
			ccl.Cosmology(Omega_c=0.225, Omega_b=0.045, sigma8=0.8, h=0.7, n_s=1.0)
		over_h : bool, optional
			If True, the units are assumed to be in not-over-h and converted to over-h units. Default is False.
		tree : bool, optional
			If True (default), pair counts use a KDTree backend; if False, a brute-force pair count is used.
			Default is True.
		chunk_size: int, optional
			Size of the chunks of data sent to each multiprocessing node. If larger, more RAM is needed per node.
			Default is 1000.
		temp_file_path : str or NoneType, optional
			Path to where the data is temporarily stored during multiprocessing [file name generated
			automatically]. Default is None.
		seed : int or NoneType, optional
			Seed for the internal jackknife patch generation (used only when num_jk is given), making the patch
			assignment reproducible. Default is None.
		responsivity : bool, optional
			If True, the g+ shape signal is calibrated by dividing by the responsivity factor 2R, with
			R = <w (1 - e^2 / 2)> / <w> the weighted shear responsivity. Default is False, appropriate when the
			input e1/e2 are already calibrated shears (e.g. from a shear catalogue); set to True for raw
			distortions. Only the g+ correlations are affected; the clustering (gg) signal is unchanged.
			Default is False.

		"""
		if IA_estimator == "clusters":
			if self.randoms_data == None:
				print("No randoms given, correlation defined as S+D/DD")
				raise KeyError("This version does not work yet, add randoms.")
			else:
				print("xi_g+ defined as S+D/SD - S+R/SR, xi_gg as (SD - RD - SR)/RR + 1")
				if masks != None and masks_randoms == None:
					print("Warning, masks given for data vector but not for randoms.")
		elif IA_estimator == "galaxies":
			if self.randoms_data == None:
				raise KeyError("No randoms given. Please provide input.")
			else:
				print("xi_g+ defined as (S+D - S+R)/RR, xi_gg as (SD - RD - SR)/RR + 1")
				if masks != None and masks_randoms == None:
					print("Warning, masks given for data vector but not for randoms.")
		else:
			raise KeyError("Unknown input for IA_estimator, choose from [clusters, galaxies].")

		self.responsivity_correction = responsivity
		masks = self.rename_input_keys(masks, self._input_name_map)
		masks_randoms = self.rename_input_keys(masks_randoms, self._input_name_map)
		# todo: Expand to include methods with trees and internal multiproc
		# todo: Checks to see if data directories include everything they need
		data = self.data  # temporary save so it can be restored at the end of the calculation

		try:  # Are there one or two random samples given?
			random_shape = self.randoms_data["RA_shape_sample"]
			one_random_sample = False
		except (KeyError, TypeError):  # TypeError: randoms_data is None
			one_random_sample = True
			self.randoms_data["RA_shape_sample"] = self.randoms_data["RA"]
			self.randoms_data["DEC_shape_sample"] = self.randoms_data["DEC"]
			self.randoms_data["Redshift_shape_sample"] = self.randoms_data["Redshift"]
		if "weight" not in self.randoms_data:
			self.randoms_data["weight"] = np.ones(len(self.randoms_data["RA"]))
		if "weight_shape_sample" not in self.randoms_data:
			if one_random_sample:
				self.randoms_data["weight_shape_sample"] = self.randoms_data["weight"]  # in case weights are given
			else:
				self.randoms_data["weight_shape_sample"] = np.ones(len(self.randoms_data["RA_shape_sample"]))

		# Covariance is requested by supplying patches or a positive patch count; num_jk=0/None
		# with no jk_patches means "no covariance", matching MeasureIABox's num_jk=0.
		measure_cov = jk_patches is not None or (num_jk is not None and num_jk > 0)
		if measure_cov:
			if jk_patches is None:
				jk_patches = self.assign_jackknife_patches(data, self.randoms_data, num_jk, seed=seed)
			else:
				if one_random_sample:
					jk_patches["randoms_position"] = jk_patches["randoms"]
					jk_patches["randoms_shape"] = jk_patches["randoms"]
			min_patch = min(jk_patches["shape"])
			max_patch = max(jk_patches["shape"])
			num_jk = max_patch - min_patch + 1
			if min_patch != 0:
				raise ValueError(
					f"Jackknife patch indices must start at 0 (minimum patch index found: {min_patch}). "
					"Renumber jk_patches, e.g. patches -= patches.min().")

		self.data_dir = data
		if "weight" not in self.data_dir:
			self.data_dir["weight"] = np.ones(len(self.data_dir["RA"]))
		if "weight_shape_sample" not in self.data_dir:
			self.data_dir["weight_shape_sample"] = np.ones(len(self.data_dir["RA_shape_sample"]))

		# Sample sizes are needed to correct for a different number of randoms and galaxies/clusters in the
		# data. Masks are resolved per field, defaulting to the sample's coordinate mask (see _field_mask).
		num_samples = {}
		coords_D, coords_S, num_R_D, num_R_S = self._sample_coordinates(masks, masks_randoms)
		# Use a structured view so np.intersect1d compares full pairs
		D_view = coords_D.view([('', coords_D.dtype)] * 2)
		S_view = coords_S.view([('', coords_S.dtype)] * 2)

		overlap, ind_D, ind_S = np.intersect1d(D_view, S_view, return_indices=True)

		num_samples["D"] = len(coords_D)
		num_samples["S"] = len(coords_S)
		num_samples["D_S"] = len(overlap)
		num_samples["R_D"] = num_R_D
		num_samples["R_S"] = num_R_S

		if measure_cov:
			if self.num_nodes == 1:
				if tree:
					self.measure_xi_jk_helper(self._count_pairs_xi_r_mur_lightcone_jk_tree,
											  self._measure_xi_r_mur_lightcone_jk_tree, IA_estimator, dataset_name,
											  corr_type, jk_patches=jk_patches, masks=masks,
											  masks_randoms=masks_randoms, cosmology=cosmology, over_h=over_h,
											  chunk_size=chunk_size, num_nodes=self.num_nodes,
											  temp_file_path=temp_file_path)
				else:
					self.measure_xi_jk_helper(self._count_pairs_xi_r_mur_lightcone_jk_brute,
											  self._measure_xi_r_mur_lightcone_jk_brute, IA_estimator, dataset_name,
											  corr_type, jk_patches=jk_patches, masks=masks,
											  masks_randoms=masks_randoms, cosmology=cosmology, over_h=over_h,
											  chunk_size=chunk_size, num_nodes=self.num_nodes,
											  temp_file_path=temp_file_path)
			else:
				self.measure_xi_jk_helper(self._count_pairs_xi_r_mur_lightcone_jk_multiprocessing,
										  self._measure_xi_r_mur_lightcone_jk_multiprocessing, IA_estimator,
										  dataset_name, corr_type, jk_patches=jk_patches, masks=masks,
										  masks_randoms=masks_randoms, cosmology=cosmology, over_h=over_h,
										  chunk_size=chunk_size, num_nodes=self.num_nodes,
										  temp_file_path=temp_file_path)
			self._obs_estimator([corr_type, "multipoles"], IA_estimator, dataset_name, num_samples)
			self._measure_multipoles(corr_type=corr_type, dataset_name=dataset_name, return_output=False)
			for i in np.arange(num_jk):
				overlap_i = np.where(jk_patches["position"][ind_D] == (i + min_patch))
				num_samples_i = {
					"S": num_samples["S"] - sum(jk_patches["shape"] == (i + min_patch)),
					"D": num_samples["D"] - sum(jk_patches["position"] == (i + min_patch)),
					"R_S": num_samples["R_S"] - sum(jk_patches["randoms_shape"] == (i + min_patch)),
					"R_D": num_samples["R_D"] - sum(jk_patches["randoms_position"] == (i + min_patch)),
					"D_S": num_samples["D_S"] - len(overlap_i)
				}
				self._obs_estimator([corr_type, "multipoles"], IA_estimator, f"{dataset_name}_{i}",
									num_samples_i, jk_group_name=f"{dataset_name}_jk{num_jk}")

				self._measure_multipoles(corr_type=corr_type, dataset_name=f"{dataset_name}_{i}",
										 jk_group_name=f"{dataset_name}_jk{num_jk}", return_output=False)
			if corr_type == "both":
				corr_group = ["multipoles_g_plus", "multipoles_gg"]
			elif corr_type == "g+":
				corr_group = ["multipoles_g_plus"]
			elif corr_type == "gg":
				corr_group = ["multipoles_gg"]
			else:
				raise KeyError("Unknown value for corr_type. Choose from [g+, gg, both]")
			self._combine_jackknife_information(dataset_name=dataset_name, jk_group_name=f"{dataset_name}_jk{num_jk}",
												corr_group=corr_group, num_box=num_jk)
		else:
			if self.num_nodes > 1:
				# See the matching comment in measure_xi_w: full-sample
				# multiprocessing for the lightcone is new in 0.5.0.
				self.measure_xi_helper(self._count_pairs_xi_r_mur_lightcone_multiprocessing,
									   self._measure_xi_r_mur_lightcone_multiprocessing,
									   IA_estimator, dataset_name, corr_type, masks=masks,
									   temp_file_path=temp_file_path,
									   chunk_size=chunk_size, num_nodes=self.num_nodes,
									   masks_randoms=masks_randoms, cosmology=cosmology, over_h=over_h)
			elif tree:
				self.measure_xi_helper(self._count_pairs_xi_r_mur_lightcone_tree,
									   self._measure_xi_r_mur_lightcone_tree,
									   IA_estimator, dataset_name, corr_type, masks=masks,
									   temp_file_path=temp_file_path,
									   chunk_size=chunk_size, num_nodes=self.num_nodes,
									   masks_randoms=masks_randoms, cosmology=cosmology, over_h=over_h)
			else:
				self.measure_xi_helper(self._count_pairs_xi_r_mur_lightcone_brute,
									   self._measure_xi_r_mur_lightcone_brute,
									   IA_estimator, dataset_name, corr_type, masks=masks,
									   temp_file_path=temp_file_path,
									   chunk_size=chunk_size, num_nodes=self.num_nodes,
									   masks_randoms=masks_randoms, cosmology=cosmology, over_h=over_h)
			self._obs_estimator([corr_type, "multipoles"], IA_estimator, dataset_name, num_samples)
			self._measure_multipoles(corr_type=corr_type, dataset_name=dataset_name, return_output=False)

		self.data = data
		return

__init__(data, randoms_data, output_file_name, separation_limits=[0.1, 20.0], num_bins_r=8, num_bins_pi=20, pi_max=None, num_nodes=1, RA_density_sample_name='RA', RA_shape_sample_name='RA_shape_sample', DEC_density_sample_name='DEC', DEC_shape_sample_name='DEC_shape_sample', redshift_density_sample_name='Redshift', redshift_shape_sample_name='Redshift_shape_sample', e1_name='e1', e2_name='e2', weight_density_sample_name='weight', weight_shape_sample_name='weight_shape_sample')

The init method of the MeasureIALightcone class.

Parameters:
  • randoms_data (dict or NoneType) –
    Dictionary with data of the randoms needed for lightcone-type measurements.
    The keywords are:
    'Redshift' and 'Redshift_shape_sample': (N_p) and (N_s) ndarray with redshifts of position and shape samples.
    'RA' and 'RA_shape_sample': (N_p) and (N_s) ndarray with RA coordinate of position and shape samples.
    'DEC' and 'DEC_shape_sample': (N_p) and (N_s) ndarray with DEC coordinate of position and shape samples.
    If only 'Redshift', 'RA' and 'DEC' are added, the sample will be used for both position and shape sample randoms.
    
  • num_nodes (int, default: 1 ) –
    Number of cores to be used in multiprocessing. Default is 1.
    
  • RA_density_sample_name (str, default: 'RA' ) –
    Name of the key in the data (and randoms) dictionary that contains the RA of the density sample.
    
  • RA_shape_sample_name (str, default: 'RA_shape_sample' ) –
    Name of the key in the data (and randoms) dictionary that contains the RA of the shape sample.
    
  • DEC_density_sample_name (str, default: 'DEC' ) –
    Name of the key in the data (and randoms) dictionary that contains the DEC of the density sample.
    
  • DEC_shape_sample_name (str, default: 'DEC_shape_sample' ) –
    Name of the key in the data (and randoms) dictionary that contains the DEC of the shape sample.
    
  • redshift_density_sample_name (str, default: 'Redshift' ) –
    Name of the key in the data (and randoms) dictionary that contains the redshift of the density sample.
    
  • redshift_shape_sample_name (str, default: 'Redshift_shape_sample' ) –
    Name of the key in the data (and randoms) dictionary that contains the redshift of the shape sample.
    
  • e1_name (str, default: 'e1' ) –
    Name of the key in the data dictionary that contains the first ellipticity component of the shape sample.
    
  • e2_name (str, default: 'e2' ) –
    Name of the key in the data dictionary that contains the second ellipticity component of the shape sample.
    
  • weight_density_sample_name (str, default: 'weight' ) –
    Name of the key in the data (and randoms) dictionary that contains the weights of the density sample.
    
  • weight_shape_sample_name (str, default: 'weight_shape_sample' ) –
    Name of the key in the data (and randoms) dictionary that contains the weights of the shape sample.
    
Notes

Constructor parameters 'data', 'output_file_name', 'separation_limits', 'num_bins_r', 'num_bins_pi', 'pi_max', are passed to MeasureIABase. The data, randoms and mask dictionaries may use any key names; they are given through the *_name parameters and translated to the internal default names on input.

Source code in src/measureia/measure_IA_lightcone.py
 42
 43
 44
 45
 46
 47
 48
 49
 50
 51
 52
 53
 54
 55
 56
 57
 58
 59
 60
 61
 62
 63
 64
 65
 66
 67
 68
 69
 70
 71
 72
 73
 74
 75
 76
 77
 78
 79
 80
 81
 82
 83
 84
 85
 86
 87
 88
 89
 90
 91
 92
 93
 94
 95
 96
 97
 98
 99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
def __init__(
		self,
		data,
		randoms_data,
		output_file_name,
		separation_limits=[0.1, 20.0],
		num_bins_r=8,
		num_bins_pi=20,
		pi_max=None,
		num_nodes=1,
		RA_density_sample_name="RA",
		RA_shape_sample_name="RA_shape_sample",
		DEC_density_sample_name="DEC",
		DEC_shape_sample_name="DEC_shape_sample",
		redshift_density_sample_name="Redshift",
		redshift_shape_sample_name="Redshift_shape_sample",
		e1_name="e1",
		e2_name="e2",
		weight_density_sample_name="weight",
		weight_shape_sample_name="weight_shape_sample",
):
	"""
	The __init__ method of the MeasureIALightcone class.

	Parameters
	----------
	randoms_data : dict or NoneType
		Dictionary with data of the randoms needed for lightcone-type measurements.
		The keywords are:
		'Redshift' and 'Redshift_shape_sample': (N_p) and (N_s) ndarray with redshifts of position and shape samples.
		'RA' and 'RA_shape_sample': (N_p) and (N_s) ndarray with RA coordinate of position and shape samples.
		'DEC' and 'DEC_shape_sample': (N_p) and (N_s) ndarray with DEC coordinate of position and shape samples.
		If only 'Redshift', 'RA' and 'DEC' are added, the sample will be used for both position and shape sample randoms.
	num_nodes : int, optional
		Number of cores to be used in multiprocessing. Default is 1.
	RA_density_sample_name : str, optional
		Name of the key in the data (and randoms) dictionary that contains the RA of the density sample.
	RA_shape_sample_name : str, optional
		Name of the key in the data (and randoms) dictionary that contains the RA of the shape sample.
	DEC_density_sample_name : str, optional
		Name of the key in the data (and randoms) dictionary that contains the DEC of the density sample.
	DEC_shape_sample_name : str, optional
		Name of the key in the data (and randoms) dictionary that contains the DEC of the shape sample.
	redshift_density_sample_name : str, optional
		Name of the key in the data (and randoms) dictionary that contains the redshift of the density sample.
	redshift_shape_sample_name : str, optional
		Name of the key in the data (and randoms) dictionary that contains the redshift of the shape sample.
	e1_name : str, optional
		Name of the key in the data dictionary that contains the first ellipticity component of the shape sample.
	e2_name : str, optional
		Name of the key in the data dictionary that contains the second ellipticity component of the shape sample.
	weight_density_sample_name : str, optional
		Name of the key in the data (and randoms) dictionary that contains the weights of the density sample.
	weight_shape_sample_name : str, optional
		Name of the key in the data (and randoms) dictionary that contains the weights of the shape sample.

	Notes
	-----
	Constructor parameters 'data', 'output_file_name', 'separation_limits', 'num_bins_r',
	'num_bins_pi', 'pi_max', are passed to MeasureIABase.
	The data, randoms and mask dictionaries may use any key names; they are given through the *_name
	parameters and translated to the internal default names on input.

	"""
	self._input_name_map = {
		RA_density_sample_name: "RA",
		RA_shape_sample_name: "RA_shape_sample",
		DEC_density_sample_name: "DEC",
		DEC_shape_sample_name: "DEC_shape_sample",
		redshift_density_sample_name: "Redshift",
		redshift_shape_sample_name: "Redshift_shape_sample",
		e1_name: "e1",
		e2_name: "e2",
		weight_density_sample_name: "weight",
		weight_shape_sample_name: "weight_shape_sample",
	}
	if output_file_name is not None:
		self.check_paths([output_file_name])
	if data is not None:
		self.check_dict(data, [RA_density_sample_name, RA_shape_sample_name, DEC_density_sample_name,
							   DEC_shape_sample_name, redshift_density_sample_name, redshift_shape_sample_name,
							   e1_name, e2_name])
		self.check_type_input_data_lightcone(data, (RA_density_sample_name, RA_shape_sample_name,
													DEC_density_sample_name, DEC_shape_sample_name,
													redshift_density_sample_name, redshift_shape_sample_name,
													e1_name, e2_name))
		data = self.rename_input_keys(data, self._input_name_map)
	if randoms_data is not None:
		self.check_dict(randoms_data,
						[RA_density_sample_name, DEC_density_sample_name, redshift_density_sample_name])
		randoms_data = self.rename_input_keys(randoms_data, self._input_name_map)
	super().__init__(data, output_file_name, False, None, separation_limits, num_bins_r, num_bins_pi,
					 pi_max, None, False)
	if not (isinstance(num_nodes, (int, np.integer)) and not isinstance(num_nodes, bool) and num_nodes >= 1):
		raise ValueError(f"num_nodes must be an integer >= 1, got {num_nodes!r}.")
	self.num_nodes = num_nodes
	self.randoms_data = randoms_data
	self.data_dir = None
	self.num_samples = None

	return

measure_xi_w(IA_estimator, dataset_name, corr_type, jk_patches=None, num_jk=None, masks=None, masks_randoms=None, cosmology=None, over_h=False, tree=True, chunk_size=1000, temp_file_path=None, seed=None, responsivity=False)

Measures xi_gg, xi_g+ and w_gg, w_g+ including jackknife covariance if desired for lightcone data. Manages the various _measure_xi_rp_pi_obs and _measure_jackknife_covariance options in MeasureWObservations and MeasureJackknife.

Parameters:
  • IA_estimator (str) –
    Choose which type of xi estimator is used. Choose from "clusters" or "galaxies".
    
  • dataset_name (str) –
    Name of the dataset in the output file.
    
  • corr_type (str) –
    Type of correlation to be measured. Choose from [g+, gg, both].
    
  • jk_patches (dict or NoneType, default: None ) –
    Dictionary with entries of the jackknife patch numbers (ndarray) for each sample, named "position", "shape"
    and "random". Default is None.
    
  • num_jk (int, default: None ) –
    Number of jackknife patches to be generated internally. Default is None (no covariance).
    The jackknife covariance is measured whenever jk_patches is given, or num_jk is greater
    than 0; pass neither (or num_jk=0) to skip it.
    
  • masks (dict or NoneType, default: None ) –
    Dictionary of mask information in the same form as the data dictionary, where the masks are placed over
    the data to apply selections. Default is None.
    
  • masks_randoms (dict or NoneType, default: None ) –
    Dictionary of mask information for the randoms data in the same form as the data dictionary,
    where the masks are placed over the data to apply selections. Default is None.
    
  • cosmology (pyccl cosmology object or NoneType, default: None ) –
    Pyccl cosmology to use in the calculation. If None (default), the cosmology is used:
    ccl.Cosmology(Omega_c=0.225, Omega_b=0.045, sigma8=0.8, h=0.7, n_s=1.0)
    
  • over_h (bool, default: False ) –
    If True, the units are assumed to be in not-over-h and converted to over-h units. Default is False.
    
  • tree (bool, default: True ) –
    If True (default), pair counts use a KDTree backend; if False, a brute-force pair count is used.
    Default is True.
    
  • chunk_size
    Size of the chunks of data sent to each multiprocessing node. If larger, more RAM is needed per node.
    Default is 1000.
    
  • temp_file_path (str or NoneType, default: None ) –
    Path to where the data is temporarily stored during multiprocessing [file name generated
    automatically]. Default is None.
    
  • seed (int or NoneType, default: None ) –
    Seed for the internal jackknife patch generation (used only when num_jk is given), making the patch
    assignment reproducible. Default is None.
    
  • responsivity (bool, default: False ) –
    If True, the g+ shape signal is calibrated by dividing by the responsivity factor 2R, with
    R = <w (1 - e^2 / 2)> / <w> the weighted shear responsivity. Default is False, appropriate when the
    input e1/e2 are already calibrated shears (e.g. from a shear catalogue); set to True for raw
    distortions. Only the g+ correlations are affected; the clustering (gg) signal is unchanged.
    Default is False.
    
Source code in src/measureia/measure_IA_lightcone.py
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
553
554
555
556
557
558
559
560
561
562
563
564
565
566
567
568
569
570
571
572
573
574
575
576
577
578
579
580
581
582
583
584
585
586
587
588
589
590
591
592
593
594
595
596
597
598
599
600
601
602
603
604
605
606
607
608
609
610
611
612
613
614
615
616
617
618
619
620
621
622
623
624
625
626
627
628
629
630
631
632
633
634
635
636
637
638
639
640
641
642
643
644
645
646
647
648
649
650
651
652
653
654
655
656
657
658
659
660
661
662
663
664
665
666
667
668
669
670
671
672
673
674
675
676
677
678
679
680
681
682
683
684
685
686
687
688
689
690
691
692
693
694
695
@worker_pool.pooled
def measure_xi_w(self, IA_estimator, dataset_name, corr_type, jk_patches=None, num_jk=None,
				 masks=None, masks_randoms=None, cosmology=None, over_h=False, tree=True,
				 chunk_size=1000, temp_file_path=None, seed=None, responsivity=False):
	"""Measures xi_gg, xi_g+ and w_gg, w_g+ including jackknife covariance if desired for lightcone data.
	Manages the various _measure_xi_rp_pi_obs and _measure_jackknife_covariance options in MeasureWObservations
	and MeasureJackknife.

	Parameters
	----------
	IA_estimator : str
		Choose which type of xi estimator is used. Choose from "clusters" or "galaxies".
	dataset_name : str
		Name of the dataset in the output file.
	corr_type : str
		Type of correlation to be measured. Choose from [g+, gg, both].
	jk_patches : dict or NoneType, optional
		Dictionary with entries of the jackknife patch numbers (ndarray) for each sample, named "position", "shape"
		and "random". Default is None.
	num_jk : int, optional
		Number of jackknife patches to be generated internally. Default is None (no covariance).
		The jackknife covariance is measured whenever jk_patches is given, or num_jk is greater
		than 0; pass neither (or num_jk=0) to skip it.
	masks : dict or NoneType, optional
		Dictionary of mask information in the same form as the data dictionary, where the masks are placed over
		the data to apply selections. Default is None.
	masks_randoms : dict or NoneType, optional
		Dictionary of mask information for the randoms data in the same form as the data dictionary,
		where the masks are placed over the data to apply selections. Default is None.
	cosmology : pyccl cosmology object or NoneType, optional
		Pyccl cosmology to use in the calculation. If None (default), the cosmology is used:
		ccl.Cosmology(Omega_c=0.225, Omega_b=0.045, sigma8=0.8, h=0.7, n_s=1.0)
	over_h : bool, optional
		If True, the units are assumed to be in not-over-h and converted to over-h units. Default is False.
	tree : bool, optional
		If True (default), pair counts use a KDTree backend; if False, a brute-force pair count is used.
		Default is True.
	chunk_size: int, optional
		Size of the chunks of data sent to each multiprocessing node. If larger, more RAM is needed per node.
		Default is 1000.
	temp_file_path : str or NoneType, optional
		Path to where the data is temporarily stored during multiprocessing [file name generated
		automatically]. Default is None.
	seed : int or NoneType, optional
		Seed for the internal jackknife patch generation (used only when num_jk is given), making the patch
		assignment reproducible. Default is None.
	responsivity : bool, optional
		If True, the g+ shape signal is calibrated by dividing by the responsivity factor 2R, with
		R = <w (1 - e^2 / 2)> / <w> the weighted shear responsivity. Default is False, appropriate when the
		input e1/e2 are already calibrated shears (e.g. from a shear catalogue); set to True for raw
		distortions. Only the g+ correlations are affected; the clustering (gg) signal is unchanged.
		Default is False.

	"""
	if IA_estimator == "clusters":
		if self.randoms_data == None:
			print("No randoms given, correlation defined as S+D/DD")
			raise KeyError("This version does not work yet, add randoms.")
		else:
			print("xi_g+ defined as S+D/SD - S+R/SR, xi_gg as (SD - RD - SR)/RR + 1")
			if masks != None and masks_randoms == None:
				print("Warning, masks given for data vector but not for randoms.")
	elif IA_estimator == "galaxies":
		if self.randoms_data == None:
			raise KeyError("No randoms given. Please provide input.")
		else:
			print("xi_g+ defined as (S+D - S+R)/RR, xi_gg as (SD - RD - SR)/RR + 1")
			if masks != None and masks_randoms == None:
				print("Warning, masks given for data vector but not for randoms.")
	else:
		raise KeyError("Unknown input for IA_estimator, choose from [clusters, galaxies].")

	self.responsivity_correction = responsivity
	masks = self.rename_input_keys(masks, self._input_name_map)
	masks_randoms = self.rename_input_keys(masks_randoms, self._input_name_map)
	# todo: Expand to include methods with trees and internal multiproc
	# todo: Checks to see if data directories include everything they need
	data = self.data  # temporary save so it can be restored at the end of the calculation

	try:  # Are there one or two random samples given?
		random_shape = self.randoms_data["RA_shape_sample"]
		one_random_sample = False
	except (KeyError, TypeError):  # TypeError: randoms_data is None
		one_random_sample = True
		self.randoms_data["RA_shape_sample"] = self.randoms_data["RA"]
		self.randoms_data["DEC_shape_sample"] = self.randoms_data["DEC"]
		self.randoms_data["Redshift_shape_sample"] = self.randoms_data["Redshift"]
	if "weight" not in self.randoms_data:
		self.randoms_data["weight"] = np.ones(len(self.randoms_data["RA"]))
	if "weight_shape_sample" not in self.randoms_data:
		if one_random_sample:
			self.randoms_data["weight_shape_sample"] = self.randoms_data["weight"]  # in case weights are given
		else:
			self.randoms_data["weight_shape_sample"] = np.ones(len(self.randoms_data["RA_shape_sample"]))

	# Covariance is requested by supplying patches or a positive patch count; num_jk=0/None
	# with no jk_patches means "no covariance", matching MeasureIABox's num_jk=0.
	measure_cov = jk_patches is not None or (num_jk is not None and num_jk > 0)
	if measure_cov:
		if jk_patches is None:
			jk_patches = self.assign_jackknife_patches(data, self.randoms_data, num_jk, seed=seed)
		else:
			if one_random_sample:
				jk_patches["randoms_position"] = jk_patches["randoms"]
				jk_patches["randoms_shape"] = jk_patches["randoms"]
		min_patch = min(jk_patches["shape"])
		max_patch = max(jk_patches["shape"])
		num_jk = max_patch - min_patch + 1
		if min_patch != 0:
			raise ValueError(
				f"Jackknife patch indices must start at 0 (minimum patch index found: {min_patch}). "
				"Renumber jk_patches, e.g. patches -= patches.min().")

	self.data_dir = data
	if "weight" not in self.data_dir:
		self.data_dir["weight"] = np.ones(len(self.data_dir["RA"]))
	if "weight_shape_sample" not in self.data_dir:
		self.data_dir["weight_shape_sample"] = np.ones(len(self.data_dir["RA_shape_sample"]))

	# Sample sizes are needed to correct for a different number of randoms and galaxies/clusters in the
	# data. Masks are resolved per field, defaulting to the sample's coordinate mask (see _field_mask).
	num_samples = {}
	coords_D, coords_S, num_R_D, num_R_S = self._sample_coordinates(masks, masks_randoms)
	# Use a structured view so np.intersect1d compares full pairs
	D_view = coords_D.view([('', coords_D.dtype)] * 2)
	S_view = coords_S.view([('', coords_S.dtype)] * 2)

	overlap, ind_D, ind_S = np.intersect1d(D_view, S_view, return_indices=True)

	num_samples["D"] = len(coords_D)
	num_samples["S"] = len(coords_S)
	num_samples["D_S"] = len(overlap)
	num_samples["R_D"] = num_R_D
	num_samples["R_S"] = num_R_S

	if measure_cov:
		if self.num_nodes == 1:
			if tree:
				self.measure_xi_jk_helper(self._count_pairs_xi_rp_pi_lightcone_jk_tree,
										  self._measure_xi_rp_pi_lightcone_jk_tree, IA_estimator, dataset_name,
										  corr_type, jk_patches=jk_patches, masks=masks,
										  masks_randoms=masks_randoms, cosmology=cosmology, over_h=over_h,
										  chunk_size=chunk_size, num_nodes=self.num_nodes,
										  temp_file_path=temp_file_path)
			else:
				self.measure_xi_jk_helper(self._count_pairs_xi_rp_pi_lightcone_jk_brute,
										  self._measure_xi_rp_pi_lightcone_jk_brute, IA_estimator, dataset_name,
										  corr_type, jk_patches=jk_patches, masks=masks,
										  masks_randoms=masks_randoms, cosmology=cosmology, over_h=over_h,
										  chunk_size=chunk_size, num_nodes=self.num_nodes,
										  temp_file_path=temp_file_path)
		else:
			self.measure_xi_jk_helper(self._count_pairs_xi_rp_pi_lightcone_jk_multiprocessing,
									  self._measure_xi_rp_pi_lightcone_jk_multiprocessing, IA_estimator,
									  dataset_name, corr_type, jk_patches=jk_patches, masks=masks,
									  masks_randoms=masks_randoms, cosmology=cosmology, over_h=over_h,
									  chunk_size=chunk_size, num_nodes=self.num_nodes,
									  temp_file_path=temp_file_path)
		self._obs_estimator([corr_type, "w"], IA_estimator, dataset_name, num_samples)
		self._measure_w_g_i(corr_type=corr_type, dataset_name=dataset_name, return_output=False)
		print(num_samples)
		for i in np.arange(num_jk):
			overlap_i = np.where(jk_patches["position"][ind_D] == (i + min_patch))
			num_samples_i = {
				"S": num_samples["S"] - sum(jk_patches["shape"] == (i + min_patch)),
				"D": num_samples["D"] - sum(jk_patches["position"] == (i + min_patch)),
				"R_S": num_samples["R_S"] - sum(jk_patches["randoms_shape"] == (i + min_patch)),
				"R_D": num_samples["R_D"] - sum(jk_patches["randoms_position"] == (i + min_patch)),
				"D_S": num_samples["D_S"] - len(overlap_i)
			}
			self._obs_estimator([corr_type, "w"], IA_estimator, f"{dataset_name}_{i}",
								num_samples_i, jk_group_name=f"{dataset_name}_jk{num_jk}")

			self._measure_w_g_i(corr_type=corr_type, dataset_name=f"{dataset_name}_{i}",
								jk_group_name=f"{dataset_name}_jk{num_jk}", return_output=False)
		if corr_type == "both":
			corr_group = ["w_g_plus", "w_gg"]
		elif corr_type == "g+":
			corr_group = ["w_g_plus"]
		elif corr_type == "gg":
			corr_group = ["w_gg"]
		else:
			raise KeyError("Unknown value for corr_type. Choose from [g+, gg, both]")
		self._combine_jackknife_information(dataset_name=dataset_name, jk_group_name=f"{dataset_name}_jk{num_jk}",
											corr_group=corr_group, num_box=num_jk)
	else:
		if self.num_nodes > 1:
			# Full-sample multiprocessing. This branch did not exist before
			# 0.5.0: num_nodes was accepted, passed down, and then ignored,
			# because only the jackknife path had an mp implementation
			# (benchmarks/FINDINGS.md F4). tree/brute is not consulted here
			# because the mp backend is the tree algorithm by construction.
			self.measure_xi_helper(self._count_pairs_xi_rp_pi_lightcone_multiprocessing,
								   self._measure_xi_rp_pi_lightcone_multiprocessing,
								   IA_estimator, dataset_name, corr_type, masks=masks,
								   masks_randoms=masks_randoms, cosmology=cosmology, over_h=over_h,
								   chunk_size=chunk_size, num_nodes=self.num_nodes,
								   temp_file_path=temp_file_path)
		elif tree:
			self.measure_xi_helper(self._count_pairs_xi_rp_pi_lightcone_tree,
								   self._measure_xi_rp_pi_lightcone_tree,
								   IA_estimator, dataset_name, corr_type, masks=masks,
								   masks_randoms=masks_randoms, cosmology=cosmology, over_h=over_h,
								   chunk_size=chunk_size, num_nodes=self.num_nodes,
								   temp_file_path=temp_file_path)
		else:
			self.measure_xi_helper(self._count_pairs_xi_rp_pi_lightcone_brute,
								   self._measure_xi_rp_pi_lightcone_brute,
								   IA_estimator, dataset_name, corr_type, masks=masks,
								   masks_randoms=masks_randoms, cosmology=cosmology, over_h=over_h,
								   chunk_size=chunk_size, num_nodes=self.num_nodes,
								   temp_file_path=temp_file_path)
		self._obs_estimator([corr_type, "w"], IA_estimator, dataset_name, num_samples)
		self._measure_w_g_i(corr_type=corr_type, dataset_name=dataset_name, return_output=False)

	self.data = data
	return

measure_xi_multipoles(IA_estimator, dataset_name, corr_type, jk_patches=None, num_jk=None, masks=None, masks_randoms=None, cosmology=None, over_h=False, tree=True, chunk_size=1000, temp_file_path=None, seed=None, responsivity=False)

Measures xi_gg, xi_g+ and multipoles including jackknife covariance if desired for lightcone data. Manages the various _measure_xi_rp_pi_obs and _measure_jackknife_covariance options in MeasureWObservations and MeasureJackknife.

Parameters:
  • IA_estimator (str) –
    Choose which type of xi estimator is used. Choose from "clusters" or "galaxies".
    
  • dataset_name (str) –
    Name of the dataset in the output file.
    
  • corr_type (str) –
    Type of correlation to be measured. Choose from [g+, gg, both].
    
  • jk_patches (dict or NoneType, default: None ) –
    Dictionary with entries of the jackknife patch numbers (ndarray) for each sample, named "position", "shape"
    and "random". Default is None.
    
  • num_jk (int, default: None ) –
    Number of jackknife patches to be generated internally. Default is None (no covariance).
    The jackknife covariance is measured whenever jk_patches is given, or num_jk is greater
    than 0; pass neither (or num_jk=0) to skip it.
    
  • masks (dict or NoneType, default: None ) –
    Dictionary of mask information in the same form as the data dictionary, where the masks are placed over
    the data to apply selections. Default is None.
    
  • masks_randoms (dict or NoneType, default: None ) –
    Dictionary of mask information for the randoms data in the same form as the data dictionary,
    where the masks are placed over the data to apply selections. Default is None.
    
  • cosmology (pyccl cosmology object or NoneType, default: None ) –
    Pyccl cosmology to use in the calculation. If None (default), the cosmology is used:
    ccl.Cosmology(Omega_c=0.225, Omega_b=0.045, sigma8=0.8, h=0.7, n_s=1.0)
    
  • over_h (bool, default: False ) –
    If True, the units are assumed to be in not-over-h and converted to over-h units. Default is False.
    
  • tree (bool, default: True ) –
    If True (default), pair counts use a KDTree backend; if False, a brute-force pair count is used.
    Default is True.
    
  • chunk_size
    Size of the chunks of data sent to each multiprocessing node. If larger, more RAM is needed per node.
    Default is 1000.
    
  • temp_file_path (str or NoneType, default: None ) –
    Path to where the data is temporarily stored during multiprocessing [file name generated
    automatically]. Default is None.
    
  • seed (int or NoneType, default: None ) –
    Seed for the internal jackknife patch generation (used only when num_jk is given), making the patch
    assignment reproducible. Default is None.
    
  • responsivity (bool, default: False ) –
    If True, the g+ shape signal is calibrated by dividing by the responsivity factor 2R, with
    R = <w (1 - e^2 / 2)> / <w> the weighted shear responsivity. Default is False, appropriate when the
    input e1/e2 are already calibrated shears (e.g. from a shear catalogue); set to True for raw
    distortions. Only the g+ correlations are affected; the clustering (gg) signal is unchanged.
    Default is False.
    
Source code in src/measureia/measure_IA_lightcone.py
697
698
699
700
701
702
703
704
705
706
707
708
709
710
711
712
713
714
715
716
717
718
719
720
721
722
723
724
725
726
727
728
729
730
731
732
733
734
735
736
737
738
739
740
741
742
743
744
745
746
747
748
749
750
751
752
753
754
755
756
757
758
759
760
761
762
763
764
765
766
767
768
769
770
771
772
773
774
775
776
777
778
779
780
781
782
783
784
785
786
787
788
789
790
791
792
793
794
795
796
797
798
799
800
801
802
803
804
805
806
807
808
809
810
811
812
813
814
815
816
817
818
819
820
821
822
823
824
825
826
827
828
829
830
831
832
833
834
835
836
837
838
839
840
841
842
843
844
845
846
847
848
849
850
851
852
853
854
855
856
857
858
859
860
861
862
863
864
865
866
867
868
869
870
871
872
873
874
875
876
877
878
879
880
881
882
883
884
885
886
887
888
889
890
891
892
893
894
895
896
897
898
899
900
901
902
903
904
905
906
907
908
909
@worker_pool.pooled
def measure_xi_multipoles(self, IA_estimator, dataset_name, corr_type, jk_patches=None, num_jk=None,
						  masks=None, masks_randoms=None, cosmology=None, over_h=False,
						  tree=True, chunk_size=1000, temp_file_path=None, seed=None, responsivity=False):
	"""Measures xi_gg, xi_g+ and multipoles including jackknife covariance if desired for lightcone data.
	Manages the various _measure_xi_rp_pi_obs and _measure_jackknife_covariance options in MeasureWObservations
	and MeasureJackknife.

	Parameters
	----------
	IA_estimator : str
		Choose which type of xi estimator is used. Choose from "clusters" or "galaxies".
	dataset_name : str
		Name of the dataset in the output file.
	corr_type : str
		Type of correlation to be measured. Choose from [g+, gg, both].
	jk_patches : dict or NoneType, optional
		Dictionary with entries of the jackknife patch numbers (ndarray) for each sample, named "position", "shape"
		and "random". Default is None.
	num_jk : int, optional
		Number of jackknife patches to be generated internally. Default is None (no covariance).
		The jackknife covariance is measured whenever jk_patches is given, or num_jk is greater
		than 0; pass neither (or num_jk=0) to skip it.
	masks : dict or NoneType, optional
		Dictionary of mask information in the same form as the data dictionary, where the masks are placed over
		the data to apply selections. Default is None.
	masks_randoms : dict or NoneType, optional
		Dictionary of mask information for the randoms data in the same form as the data dictionary,
		where the masks are placed over the data to apply selections. Default is None.
	cosmology : pyccl cosmology object or NoneType, optional
		Pyccl cosmology to use in the calculation. If None (default), the cosmology is used:
		ccl.Cosmology(Omega_c=0.225, Omega_b=0.045, sigma8=0.8, h=0.7, n_s=1.0)
	over_h : bool, optional
		If True, the units are assumed to be in not-over-h and converted to over-h units. Default is False.
	tree : bool, optional
		If True (default), pair counts use a KDTree backend; if False, a brute-force pair count is used.
		Default is True.
	chunk_size: int, optional
		Size of the chunks of data sent to each multiprocessing node. If larger, more RAM is needed per node.
		Default is 1000.
	temp_file_path : str or NoneType, optional
		Path to where the data is temporarily stored during multiprocessing [file name generated
		automatically]. Default is None.
	seed : int or NoneType, optional
		Seed for the internal jackknife patch generation (used only when num_jk is given), making the patch
		assignment reproducible. Default is None.
	responsivity : bool, optional
		If True, the g+ shape signal is calibrated by dividing by the responsivity factor 2R, with
		R = <w (1 - e^2 / 2)> / <w> the weighted shear responsivity. Default is False, appropriate when the
		input e1/e2 are already calibrated shears (e.g. from a shear catalogue); set to True for raw
		distortions. Only the g+ correlations are affected; the clustering (gg) signal is unchanged.
		Default is False.

	"""
	if IA_estimator == "clusters":
		if self.randoms_data == None:
			print("No randoms given, correlation defined as S+D/DD")
			raise KeyError("This version does not work yet, add randoms.")
		else:
			print("xi_g+ defined as S+D/SD - S+R/SR, xi_gg as (SD - RD - SR)/RR + 1")
			if masks != None and masks_randoms == None:
				print("Warning, masks given for data vector but not for randoms.")
	elif IA_estimator == "galaxies":
		if self.randoms_data == None:
			raise KeyError("No randoms given. Please provide input.")
		else:
			print("xi_g+ defined as (S+D - S+R)/RR, xi_gg as (SD - RD - SR)/RR + 1")
			if masks != None and masks_randoms == None:
				print("Warning, masks given for data vector but not for randoms.")
	else:
		raise KeyError("Unknown input for IA_estimator, choose from [clusters, galaxies].")

	self.responsivity_correction = responsivity
	masks = self.rename_input_keys(masks, self._input_name_map)
	masks_randoms = self.rename_input_keys(masks_randoms, self._input_name_map)
	# todo: Expand to include methods with trees and internal multiproc
	# todo: Checks to see if data directories include everything they need
	data = self.data  # temporary save so it can be restored at the end of the calculation

	try:  # Are there one or two random samples given?
		random_shape = self.randoms_data["RA_shape_sample"]
		one_random_sample = False
	except (KeyError, TypeError):  # TypeError: randoms_data is None
		one_random_sample = True
		self.randoms_data["RA_shape_sample"] = self.randoms_data["RA"]
		self.randoms_data["DEC_shape_sample"] = self.randoms_data["DEC"]
		self.randoms_data["Redshift_shape_sample"] = self.randoms_data["Redshift"]
	if "weight" not in self.randoms_data:
		self.randoms_data["weight"] = np.ones(len(self.randoms_data["RA"]))
	if "weight_shape_sample" not in self.randoms_data:
		if one_random_sample:
			self.randoms_data["weight_shape_sample"] = self.randoms_data["weight"]  # in case weights are given
		else:
			self.randoms_data["weight_shape_sample"] = np.ones(len(self.randoms_data["RA_shape_sample"]))

	# Covariance is requested by supplying patches or a positive patch count; num_jk=0/None
	# with no jk_patches means "no covariance", matching MeasureIABox's num_jk=0.
	measure_cov = jk_patches is not None or (num_jk is not None and num_jk > 0)
	if measure_cov:
		if jk_patches is None:
			jk_patches = self.assign_jackknife_patches(data, self.randoms_data, num_jk, seed=seed)
		else:
			if one_random_sample:
				jk_patches["randoms_position"] = jk_patches["randoms"]
				jk_patches["randoms_shape"] = jk_patches["randoms"]
		min_patch = min(jk_patches["shape"])
		max_patch = max(jk_patches["shape"])
		num_jk = max_patch - min_patch + 1
		if min_patch != 0:
			raise ValueError(
				f"Jackknife patch indices must start at 0 (minimum patch index found: {min_patch}). "
				"Renumber jk_patches, e.g. patches -= patches.min().")

	self.data_dir = data
	if "weight" not in self.data_dir:
		self.data_dir["weight"] = np.ones(len(self.data_dir["RA"]))
	if "weight_shape_sample" not in self.data_dir:
		self.data_dir["weight_shape_sample"] = np.ones(len(self.data_dir["RA_shape_sample"]))

	# Sample sizes are needed to correct for a different number of randoms and galaxies/clusters in the
	# data. Masks are resolved per field, defaulting to the sample's coordinate mask (see _field_mask).
	num_samples = {}
	coords_D, coords_S, num_R_D, num_R_S = self._sample_coordinates(masks, masks_randoms)
	# Use a structured view so np.intersect1d compares full pairs
	D_view = coords_D.view([('', coords_D.dtype)] * 2)
	S_view = coords_S.view([('', coords_S.dtype)] * 2)

	overlap, ind_D, ind_S = np.intersect1d(D_view, S_view, return_indices=True)

	num_samples["D"] = len(coords_D)
	num_samples["S"] = len(coords_S)
	num_samples["D_S"] = len(overlap)
	num_samples["R_D"] = num_R_D
	num_samples["R_S"] = num_R_S

	if measure_cov:
		if self.num_nodes == 1:
			if tree:
				self.measure_xi_jk_helper(self._count_pairs_xi_r_mur_lightcone_jk_tree,
										  self._measure_xi_r_mur_lightcone_jk_tree, IA_estimator, dataset_name,
										  corr_type, jk_patches=jk_patches, masks=masks,
										  masks_randoms=masks_randoms, cosmology=cosmology, over_h=over_h,
										  chunk_size=chunk_size, num_nodes=self.num_nodes,
										  temp_file_path=temp_file_path)
			else:
				self.measure_xi_jk_helper(self._count_pairs_xi_r_mur_lightcone_jk_brute,
										  self._measure_xi_r_mur_lightcone_jk_brute, IA_estimator, dataset_name,
										  corr_type, jk_patches=jk_patches, masks=masks,
										  masks_randoms=masks_randoms, cosmology=cosmology, over_h=over_h,
										  chunk_size=chunk_size, num_nodes=self.num_nodes,
										  temp_file_path=temp_file_path)
		else:
			self.measure_xi_jk_helper(self._count_pairs_xi_r_mur_lightcone_jk_multiprocessing,
									  self._measure_xi_r_mur_lightcone_jk_multiprocessing, IA_estimator,
									  dataset_name, corr_type, jk_patches=jk_patches, masks=masks,
									  masks_randoms=masks_randoms, cosmology=cosmology, over_h=over_h,
									  chunk_size=chunk_size, num_nodes=self.num_nodes,
									  temp_file_path=temp_file_path)
		self._obs_estimator([corr_type, "multipoles"], IA_estimator, dataset_name, num_samples)
		self._measure_multipoles(corr_type=corr_type, dataset_name=dataset_name, return_output=False)
		for i in np.arange(num_jk):
			overlap_i = np.where(jk_patches["position"][ind_D] == (i + min_patch))
			num_samples_i = {
				"S": num_samples["S"] - sum(jk_patches["shape"] == (i + min_patch)),
				"D": num_samples["D"] - sum(jk_patches["position"] == (i + min_patch)),
				"R_S": num_samples["R_S"] - sum(jk_patches["randoms_shape"] == (i + min_patch)),
				"R_D": num_samples["R_D"] - sum(jk_patches["randoms_position"] == (i + min_patch)),
				"D_S": num_samples["D_S"] - len(overlap_i)
			}
			self._obs_estimator([corr_type, "multipoles"], IA_estimator, f"{dataset_name}_{i}",
								num_samples_i, jk_group_name=f"{dataset_name}_jk{num_jk}")

			self._measure_multipoles(corr_type=corr_type, dataset_name=f"{dataset_name}_{i}",
									 jk_group_name=f"{dataset_name}_jk{num_jk}", return_output=False)
		if corr_type == "both":
			corr_group = ["multipoles_g_plus", "multipoles_gg"]
		elif corr_type == "g+":
			corr_group = ["multipoles_g_plus"]
		elif corr_type == "gg":
			corr_group = ["multipoles_gg"]
		else:
			raise KeyError("Unknown value for corr_type. Choose from [g+, gg, both]")
		self._combine_jackknife_information(dataset_name=dataset_name, jk_group_name=f"{dataset_name}_jk{num_jk}",
											corr_group=corr_group, num_box=num_jk)
	else:
		if self.num_nodes > 1:
			# See the matching comment in measure_xi_w: full-sample
			# multiprocessing for the lightcone is new in 0.5.0.
			self.measure_xi_helper(self._count_pairs_xi_r_mur_lightcone_multiprocessing,
								   self._measure_xi_r_mur_lightcone_multiprocessing,
								   IA_estimator, dataset_name, corr_type, masks=masks,
								   temp_file_path=temp_file_path,
								   chunk_size=chunk_size, num_nodes=self.num_nodes,
								   masks_randoms=masks_randoms, cosmology=cosmology, over_h=over_h)
		elif tree:
			self.measure_xi_helper(self._count_pairs_xi_r_mur_lightcone_tree,
								   self._measure_xi_r_mur_lightcone_tree,
								   IA_estimator, dataset_name, corr_type, masks=masks,
								   temp_file_path=temp_file_path,
								   chunk_size=chunk_size, num_nodes=self.num_nodes,
								   masks_randoms=masks_randoms, cosmology=cosmology, over_h=over_h)
		else:
			self.measure_xi_helper(self._count_pairs_xi_r_mur_lightcone_brute,
								   self._measure_xi_r_mur_lightcone_brute,
								   IA_estimator, dataset_name, corr_type, masks=masks,
								   temp_file_path=temp_file_path,
								   chunk_size=chunk_size, num_nodes=self.num_nodes,
								   masks_randoms=masks_randoms, cosmology=cosmology, over_h=over_h)
		self._obs_estimator([corr_type, "multipoles"], IA_estimator, dataset_name, num_samples)
		self._measure_multipoles(corr_type=corr_type, dataset_name=dataset_name, return_output=False)

	self.data = data
	return

assign_jackknife_patches(data, randoms_data, num_jk, seed=None)

Assigns jackknife patches to data and randoms given a number of patches.

The patch centres are fitted to the position randoms with k-means on the sphere (see measureia.kmeans_sphere), so the patches are compact sky regions; every other sample is then assigned to its nearest centre.

Parameters:
  • data (dict) –
    Dictionary containing position and shape sample data. Keywords: "RA", "DEC", "RA_shape_sample",
    "DEC_shape_sample"
    
  • randoms_data (dict) –
    Dictionary containing position and shape sample data of randoms. Keywords: "RA", "DEC", "RA_shape_sample",
    "DEC_shape_sample"
    
  • num_jk (int) –
    Number of jackknife patches. Cannot exceed the number of position randoms, since the
    patch centres are fitted to them.
    
  • seed (int or NoneType, default: None ) –
    Seed for the k-means initialisation, making the patch assignment reproducible. If None (default),
    the patches differ between runs. The global random state is never touched.
    
Returns:
  • dict

    Dictionary with patch numbers for each sample. Keywords: 'position', 'shape', 'randoms_position', 'randoms_shape'

Warns:
  • UserWarning

    If any sample ends up with patches holding fewer than 10 objects, which makes the jackknife covariance unreliable, or if the k-means fit does not converge.

Source code in src/measureia/measure_jackknife.py
 62
 63
 64
 65
 66
 67
 68
 69
 70
 71
 72
 73
 74
 75
 76
 77
 78
 79
 80
 81
 82
 83
 84
 85
 86
 87
 88
 89
 90
 91
 92
 93
 94
 95
 96
 97
 98
 99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
def assign_jackknife_patches(self, data, randoms_data, num_jk, seed=None):
	"""Assigns jackknife patches to data and randoms given a number of patches.

	The patch centres are fitted to the position randoms with k-means on the sphere
	(see `measureia.kmeans_sphere`), so the patches are compact sky regions; every
	other sample is then assigned to its nearest centre.

	Parameters
	----------
	data : dict
		Dictionary containing position and shape sample data. Keywords: "RA", "DEC", "RA_shape_sample",
		"DEC_shape_sample"
	randoms_data : dict
		Dictionary containing position and shape sample data of randoms. Keywords: "RA", "DEC", "RA_shape_sample",
		"DEC_shape_sample"
	num_jk : int
		Number of jackknife patches. Cannot exceed the number of position randoms, since the
		patch centres are fitted to them.
	seed : int or NoneType, optional
		Seed for the k-means initialisation, making the patch assignment reproducible. If None (default),
		the patches differ between runs. The global random state is never touched.

	Returns
	-------
	dict
		Dictionary with patch numbers for each sample. Keywords: 'position', 'shape', 'randoms_position',
		'randoms_shape'

	Warns
	-----
	UserWarning
		If any sample ends up with patches holding fewer than 10 objects, which makes the
		jackknife covariance unreliable, or if the k-means fit does not converge.

	"""

	jk_patches = {}

	# Read the randoms file from which the jackknife regions will be created
	RA = randoms_data['RA']
	DEC = randoms_data['DEC']

	# Define a number of jackknife regions and find their centres using k-means
	X = np.column_stack((RA, DEC))
	if not (isinstance(num_jk, (int, np.integer)) and not isinstance(num_jk, bool) and num_jk >= 1):
		raise ValueError(f"num_jk must be an integer >= 1, got {num_jk!r}.")
	if num_jk > len(X):
		raise ValueError(
			f"num_jk ({num_jk}) cannot exceed the number of position randoms ({len(X)}), since the "
			f"patch centres are fitted to them. Lower num_jk or provide more randoms.")
	km = kmeans_sample(X, num_jk, maxiter=100, tol=1.0e-5, seed=seed)
	jk_labels = km.labels

	jk_patches['randoms_position'] = jk_labels

	RA = randoms_data['RA_shape_sample']
	DEC = randoms_data['DEC_shape_sample']
	X2 = np.column_stack((RA, DEC))
	jk_labels = km.find_nearest(X2)

	jk_patches['randoms_shape'] = jk_labels

	RA_data = data['RA']
	DEC_data = data['DEC']
	X3 = np.column_stack((RA_data, DEC_data))
	jk_labels = km.find_nearest(X3)

	jk_patches['position'] = jk_labels

	RA_data = data['RA_shape_sample']
	DEC_data = data['DEC_shape_sample']
	X4 = np.column_stack((RA_data, DEC_data))
	jk_labels = km.find_nearest(X4)

	jk_patches['shape'] = jk_labels

	self._warn_on_sparse_patches(jk_patches, num_jk)

	return jk_patches