sacc 1.0__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
sacc/sacc.py ADDED
@@ -0,0 +1,1458 @@
1
+ import copy
2
+ import warnings
3
+ import os
4
+ from io import BytesIO
5
+
6
+ import numpy as np
7
+ from astropy.io import fits
8
+ from astropy.table import Table
9
+
10
+ from .tracers import BaseTracer
11
+ from .windows import BaseWindow, BandpowerWindow
12
+ from .covariance import BaseCovariance, concatenate_covariances
13
+ from .utils import unique_list
14
+ from .data_types import standard_types, DataPoint
15
+
16
+
17
+ class Sacc:
18
+ """
19
+ A class containing a selection of LSST summary statistic measurements,
20
+ their covariance, and the metadata necessary to compute theoretical
21
+ predictions for them.
22
+ """
23
+
24
+ def __init__(self):
25
+ """
26
+ Create an empty data set ready to be built up
27
+ """
28
+ self.data = []
29
+ self.tracers = {}
30
+ self.covariance = None
31
+ self.metadata = {}
32
+
33
+ def __len__(self):
34
+ """
35
+ Return the number of data points in the data set.
36
+
37
+ Returns
38
+ -------
39
+ n: int
40
+ The number of data points
41
+ """
42
+ return len(self.data)
43
+
44
+ def copy(self):
45
+ """
46
+ Create a copy of the data set with no data shared with the original.
47
+ You can safely modify the copy without it affecting the original.
48
+
49
+ Returns
50
+ -------
51
+ S: Sacc instance
52
+ A new instance of the data set.
53
+ """
54
+ return copy.deepcopy(self)
55
+
56
+ def to_canonical_order(self):
57
+ """
58
+ Re-order the data set in-place to a standard ordering.
59
+ """
60
+
61
+ # Define the ordering to be used
62
+ # We need a key function that will return the
63
+ # object that python's default sorted function will use.
64
+ def order_key(row):
65
+ # Put data types in the order in allowed_types.
66
+ # If not present then just use the hash of the data type.
67
+ if row.data_type in standard_types:
68
+ dt = standard_types.index(row.data_type)
69
+ else:
70
+ dt = hash(row.data_type)
71
+ # If known, order by ell or theta.
72
+ # Otherwise just use whatever we have.
73
+ if 'ell' in row.tags:
74
+ return (dt, row.tracers, row.tags['ell'])
75
+ elif 'theta' in row.tags:
76
+ return (dt, row.tracers, row.tags['theta'])
77
+ else:
78
+ return (dt, row.tracers, 0.0)
79
+ # This from
80
+ # https://stackoverflow.com/questions/6422700/how-to-get-indices-of-a-sorted-array-in-python
81
+ indices = [i[0] for i in sorted(enumerate(self.data),
82
+ key=lambda x:order_key(x[1]))]
83
+
84
+ # Assign the new order.
85
+ self.reorder(indices)
86
+
87
+ def reorder(self, indices):
88
+ """
89
+ Re-order the data set in-place according to the indices passed in.
90
+
91
+ If not all indices are included in the input then the data set will
92
+ be cut down.
93
+
94
+ Parameters
95
+ ----------
96
+ indices: integer list or array
97
+ Indices for the re-ordered data
98
+ """
99
+ self.data = [self.data[i] for i in indices]
100
+
101
+ if self.has_covariance():
102
+ self.covariance = self.covariance.keeping_indices(indices)
103
+
104
+ #
105
+ # Builder methods for building up Sacc data from scratch in memory
106
+ #
107
+
108
+ def add_tracer(self, tracer_type, name,
109
+ *args, **kwargs):
110
+ """
111
+ Add a new tracer
112
+
113
+ Parameters
114
+ ----------
115
+ tracer_type: str
116
+ A string corresponding to one of the known tracer types,
117
+ or 'misc' to use a new tracer with no parameters.
118
+ e.g. "NZ" for n(z) tracers
119
+
120
+ name: str
121
+ A name for the tracer
122
+
123
+ *args:
124
+ Additional arguments to pass to the tracer constructor.
125
+ These depend on the type of the tracer. For n(z) tracers
126
+ these should be z and nz arrays
127
+
128
+ **kwargs:
129
+ Additional keyword arguments to pass to the tracer constructor.
130
+ These depend on the type of the tracer. There are no
131
+ kwargs for n(z) tracers
132
+
133
+ Returns
134
+ -------
135
+ None
136
+
137
+ """
138
+ tracer = BaseTracer.make(tracer_type, name,
139
+ *args, **kwargs)
140
+ self.add_tracer_object(tracer)
141
+
142
+ def add_tracer_object(self, tracer):
143
+ """
144
+ Add a pre-constructed BaseTracer instance to this data set.
145
+ If you just have, for example the z and n(z) data then
146
+ use the add_tracer method instead.
147
+
148
+ Parameters
149
+ ----------
150
+ tracer: Tracer instance
151
+ The tracer object to add to the data set
152
+ """
153
+ self.tracers[tracer.name] = tracer
154
+
155
+ def add_data_point(self, data_type, tracers, value,
156
+ tracers_later=False, **tags):
157
+ """
158
+ Add a data point to the set.
159
+
160
+ Parameters
161
+ ----------
162
+ data_type: str
163
+
164
+ tracers: tuple of str
165
+ Strings corresponding to zero or more of the tracers
166
+ in the data set. These should either be already set up
167
+ using the add_tracer method, or you could set
168
+ tracers_later=True if you want to add them later.
169
+ e.g. for 2pt measurements the tracers are the names of
170
+ the two n(z) samples.
171
+
172
+ value: float
173
+ A single value for the data point
174
+
175
+ tracers_later: bool
176
+ If True, do not complain if the tracers are not know
177
+ already.
178
+
179
+ **tags:
180
+ Tags to apply to this data point.
181
+ Tags can be any arbitrary metadata that you might want later,
182
+ For 2pt data the tag would include an angle theta or ell.
183
+
184
+ Returns
185
+ -------
186
+ None
187
+ """
188
+ if self.has_covariance():
189
+ raise ValueError("You cannot add a data point after "
190
+ "setting the covariance")
191
+ tracers = tuple(tracers)
192
+ for tracer in tracers:
193
+ if (tracer not in self.tracers) and (not tracers_later):
194
+ raise ValueError(f"Tracer named '{tracer}' is not in the "
195
+ "known list of tracers. "
196
+ "Either put it in before adding data "
197
+ "points or set tracers_later=True")
198
+ d = DataPoint(data_type, tracers, value, **tags)
199
+ self.data.append(d)
200
+
201
+ def add_covariance(self, covariance, overwrite=False):
202
+ """
203
+ Once you have finished adding data points, add a covariance
204
+ for the entire set.
205
+
206
+ Parameters
207
+ ----------
208
+ covariance: array or list
209
+ 2x2 numpy array containing the covariance of the added data points
210
+ OR a list of blocks
211
+ overwrite: bool
212
+ If True, it overwrites the stored covariance matrix with the given
213
+ one.
214
+
215
+ Returns
216
+ -------
217
+ None
218
+ """
219
+ if self.has_covariance() and not overwrite:
220
+ raise RuntimeError("This sacc file already contains a covariance"
221
+ "matrix. Use overwrite=True if you want to "
222
+ "replace it for the new one")
223
+
224
+ if isinstance(covariance, BaseCovariance):
225
+ cov = covariance
226
+ else:
227
+ cov = BaseCovariance.make(covariance)
228
+
229
+ expected_size = len(self)
230
+ if not cov.size == expected_size:
231
+ raise ValueError("Covariance has the wrong size. "
232
+ f"Should be {expected_size} but is {cov.size}")
233
+
234
+ self.covariance = cov
235
+
236
+ def has_covariance(self):
237
+ """ Return whether or not this data set has a covariance attached to it
238
+
239
+ Returns
240
+ -------
241
+ bool
242
+ Whether or not a covariance has been added to this data
243
+ """
244
+ return self.covariance is not None
245
+
246
+ def _indices_to_bool(self, indices):
247
+ # Convert an array of indices into a boolean True mask
248
+ if indices.dtype not in [np.int8, np.int16, np.int32, np.int64]:
249
+ raise ValueError(f"Wrong indices type ({indices.dtype}) - "
250
+ "expected integers or boolean")
251
+ m = np.zeros(len(self), dtype=bool)
252
+ for i in indices:
253
+ m[i] = True
254
+ return m
255
+
256
+ def keep_indices(self, indices):
257
+ """
258
+ Select data points, keeping only values where the mask is True or
259
+ an index is included in it.
260
+
261
+ You can use Sacc.remove_indices to do the opposite operation,
262
+ keeping points where the mask is False.
263
+
264
+ You use the Sacc.keep_selection method to find indices and apply
265
+ this method automatically, or the Sacc.indices method to manually
266
+ select indices.
267
+
268
+ Parameters
269
+ ----------
270
+ indices: array or list
271
+ Mask must be either a boolean array or a list/array of integer
272
+ indices to remove. If boolean then True means to keep a data
273
+ point and False means to cut it if integers then values
274
+ indicate data points to keep.
275
+ """
276
+ indices = np.array(indices)
277
+
278
+ # Convert integer masks to booleans
279
+ if indices.dtype != bool:
280
+ indices = self._indices_to_bool(indices)
281
+
282
+ self.data = [d for i, d in enumerate(self.data) if indices[i]]
283
+ if self.has_covariance():
284
+ self.covariance = self.covariance.keeping_indices(indices)
285
+
286
+ def remove_indices(self, indices):
287
+ """
288
+ Remove data points, getting rid of points where the mask is True
289
+ or an index is included in it.
290
+
291
+ You can use Sacc.keep_indices to do the opposite operation,
292
+ keeping points where the mask is True.
293
+
294
+ You use the Sacc.remove_selection method to find indices and
295
+ apply this method automatically, or the Sacc.indices method to
296
+ manually select indices.
297
+
298
+ Parameters
299
+ ----------
300
+ indices: array or list
301
+ Mask must be either a boolean array or a list/array of
302
+ integer indices to remove. If boolean then True means
303
+ to cut data point and False means to keep it if integers
304
+ then values indicate data points to cut out
305
+ """
306
+ indices = np.array(indices)
307
+
308
+ # Convert integer masks to booleans
309
+ if indices.dtype != bool:
310
+ indices = self._indices_to_bool(indices)
311
+
312
+ # Get the mask method to do the actual work
313
+ self.keep_indices(~indices)
314
+
315
+ def indices(self, data_type=None, tracers=None, warn_empty=True, **select):
316
+ """
317
+ Find the indices of all points matching the given selection criteria.
318
+
319
+ Parameters
320
+ ----------
321
+ data_type: str
322
+ Select only data points which are of this data type.
323
+ If None (the default) then match any data types
324
+
325
+ tracers: tuple
326
+ Select only data points which match this tracer combination.
327
+ If None (the default) then match any tracer combinations.
328
+
329
+ **select:
330
+ Select only data points with tag names and values matching
331
+ all values provided in this kwargs option.
332
+ You can also use the syntax name__lt=value or
333
+ name__gt=value in the selection to select points
334
+ less or greater than a threshold
335
+
336
+ Returns
337
+ indices: array
338
+ Array of integer indices of matching data points
339
+
340
+ """
341
+ indices = []
342
+ if tracers is not None:
343
+ tracers = tuple(tracers)
344
+
345
+ # Look through all data points we have
346
+ for i, d in enumerate(self.data):
347
+ # Skip things with the wrong type or tracer
348
+ if not ((tracers is None) or (d.tracers == tracers)):
349
+ continue
350
+ if not ((data_type is None or d.data_type == data_type)):
351
+ continue
352
+ # Remove any objects that don't match the required tags,
353
+ # including the fact that we can specify tag__lt and tag__gt
354
+ # in order to remove/accept ranges
355
+ ok = True
356
+ for name, val in select.items():
357
+ if name.endswith("__lt"):
358
+ name = name[:-4]
359
+ if not d.get_tag(name) < val:
360
+ ok = False
361
+ break
362
+ elif name.endswith("__gt"):
363
+ name = name[:-4]
364
+ if not d.get_tag(name) > val:
365
+ ok = False
366
+ break
367
+ else:
368
+ if not d.get_tag(name) == val:
369
+ ok = False
370
+ break
371
+ # Record this index
372
+ if ok:
373
+ indices.append(i)
374
+
375
+ if len(indices) == 0 and warn_empty:
376
+ if tracers is None:
377
+ warnings.warn("Empty index selected")
378
+ else:
379
+ warnings.warn("Empty index selected - maybe you "
380
+ "should check the tracer order?")
381
+
382
+ return np.array(indices, dtype=int)
383
+
384
+ def remove_selection(self, data_type=None, tracers=None,
385
+ warn_empty=True, **select):
386
+ """
387
+ Remove data points, getting rid of points matching the given criteria.
388
+
389
+ You can use Sacc.keep_selection to do the opposite operation, keeping
390
+ points where the criteria are matched.
391
+
392
+ You can manually remove points using the Sacc.indices and
393
+ Sacc.remove_indices methods.
394
+
395
+ Parameters
396
+ ----------
397
+ data_type: str
398
+ Select only data points which are of this data type.
399
+ If None (the default) then match any data types
400
+
401
+ tracers: tuple
402
+ Select only data points which match this tracer combination.
403
+ If None (the default) then match any tracer combinations.
404
+
405
+ **select:
406
+ Select only data points with tag names and values matching
407
+ all values provided in this kwargs option.
408
+ You can also use the syntax name__lt=value or
409
+ name__gt=value in the selection to select points
410
+ less or greater than a threshold
411
+ """
412
+
413
+ indices = self.indices(data_type=data_type, tracers=tracers,
414
+ warn_empty=warn_empty, **select)
415
+ self.remove_indices(indices)
416
+
417
+ def keep_selection(self, data_type=None, tracers=None,
418
+ warn_empty=True, **select):
419
+ """
420
+ Remove data points, keeping only points matching the given criteria.
421
+
422
+ You can use Sacc.remove_selection to do the opposite operation,
423
+ keeping points where the criteria are not matched.
424
+
425
+ You can manually remove points using the Sacc.indices and
426
+ Sacc.keep_indices methods.
427
+
428
+ Parameters
429
+ ----------
430
+ data_type: str
431
+ Select only data points which are of this data type.
432
+ If None (the default) then match any data types
433
+
434
+ tracers: tuple
435
+ Select only data points which match this tracer combination.
436
+ If None (the default) then match any tracer combinations.
437
+
438
+ **select:
439
+ Select only data points with tag names and values matching
440
+ all values provided in this kwargs option.
441
+ You can also use the syntax name__lt=value or
442
+ name__gt=value in the selection to select points
443
+ less or greater than a threshold
444
+ """
445
+ indices = self.indices(data_type=data_type, tracers=tracers,
446
+ warn_empty=warn_empty, **select)
447
+ self.keep_indices(indices)
448
+
449
+ def _get_tags_by_index(self, tags, indices):
450
+ """
451
+ Get the value of a one or more named tags for (a subset of) the data.
452
+
453
+ Parameters
454
+ ----------
455
+
456
+ tags: list of str
457
+ Tags to look up on the selected data
458
+
459
+ indices: list or array
460
+ Indices of data points
461
+
462
+ Returns
463
+ -------
464
+ values: list of lists
465
+ For each input tag, a corresponding list of the value of that
466
+ tag for given selection, in the order the matching data points
467
+ were added.
468
+
469
+
470
+ """
471
+ indices = set(indices)
472
+ values = [[d.get_tag(tag)
473
+ for i, d in enumerate(self.data) if i in indices]
474
+ for tag in tags]
475
+ return values
476
+
477
+ def get_tags(self, tags, data_type=None, tracers=None, **select):
478
+ """
479
+ Get the value of a one or more named tags for (a subset of) the data.
480
+
481
+ Parameters
482
+ ----------
483
+
484
+ tags: list of str
485
+ Tags to look up on the selected data
486
+
487
+ data_type: str
488
+ Select only data points which are of this data type.
489
+ If None (the default) then match any data types
490
+
491
+ tracers: tuple
492
+ Select only data points which match this tracer combination.
493
+ If None (the default) then match any tracer combinations.
494
+
495
+ **select:
496
+ Select only data points with tag names and values matching
497
+ all values provided in this kwargs option.
498
+ You can also use the syntax name__lt=value or
499
+ name__gt=value in the selection to select points
500
+ less or greater than a threshold
501
+
502
+ Returns
503
+ -------
504
+ values: list of lists
505
+ For each input tag, a corresponding list of the value of
506
+ that tag for given selection, in the order the matching
507
+ data points were added.
508
+ """
509
+ indices = self.indices(data_type=data_type,
510
+ tracers=tracers, **select)
511
+ return self._get_tags_by_index(tags, indices)
512
+
513
+ def get_tag(self, tag, data_type=None, tracers=None, **select):
514
+ """
515
+ Get the value of a one tag for (a subset of) the data.
516
+
517
+ Parameters
518
+ ----------
519
+ tag: str
520
+ Tag to look up on the selected data
521
+
522
+ data_type: str
523
+ Select only data points which are of this data type.
524
+ If None (the default) then match any data types
525
+
526
+ tracers: tuple
527
+ Select only data points which match this tracer combination.
528
+ If None (the default) then match any tracer combinations.
529
+
530
+ **select:
531
+ Select only data points with tag names and values matching
532
+ all values provided in this kwargs option.
533
+ You can also use the syntax name__lt=value or
534
+ name__gt=value in the selection to select points
535
+ less or greater than a threshold
536
+
537
+ Returns
538
+ -------
539
+ values: list
540
+ A list of the value of the tag for given selection,
541
+ in the order the matching data points were added.
542
+ """
543
+ return self.get_tags([tag], data_type=data_type,
544
+ tracers=tracers, **select)[0]
545
+
546
+ def get_data_points(self, data_type=None, tracers=None, **select):
547
+ """
548
+ Get data point objects for a subset of the data
549
+
550
+ Parameters
551
+ ----------
552
+ data_type: str
553
+ Select only data points which are of this data type.
554
+ If None (the default) then match any data types
555
+
556
+ tracers: tuple
557
+ Select only data points which match this tracer combination.
558
+ If None (the default) then match any tracer combinations.
559
+
560
+ **select:
561
+ Select only data points with tag names and values matching
562
+ all values provided in this kwargs option.
563
+ You can also use the syntax name__lt=value or
564
+ name__gt=value in the selection to select points
565
+ less or greater than a threshold
566
+
567
+ Returns
568
+ -------
569
+ values: list
570
+ A list of the data point objects for the selection,
571
+ in the order they were added.
572
+ """
573
+ indices = self.indices(data_type=data_type, tracers=tracers, **select)
574
+ return [self.data[i] for i in indices]
575
+
576
+ def get_mean(self, data_type=None, tracers=None, **select):
577
+ """
578
+ Get mean values for each data point matching the criteria.
579
+
580
+ Parameters
581
+ ----------
582
+
583
+ data_type: str
584
+ Select only data points which are of this data type.
585
+ If None (the default) then match any data types
586
+
587
+ tracers: tuple
588
+ Select only data points which match this tracer combination.
589
+ If None (the default) then match any tracer combinations.
590
+
591
+ **select:
592
+ Select only data points with tag names and values matching
593
+ all values provided in this kwargs option.
594
+ You can also use the syntax name__lt=value or
595
+ name__gt=value in the selection to select points
596
+ less or greater than a threshold
597
+
598
+ Returns
599
+ -------
600
+ values: list
601
+ The mean values for each matching data point,
602
+ in the order they were added.
603
+
604
+ """
605
+ indices = self.indices(data_type=data_type, tracers=tracers, **select)
606
+ return self.mean[indices]
607
+
608
+ def get_standard_deviation(self, data_type=None, tracers=None, **select):
609
+ """
610
+ Get standard deviation values for each data point matching the criteria.
611
+
612
+ This requires the covariance matrix to be set.
613
+
614
+ Parameters
615
+ ----------
616
+
617
+ data_type: str
618
+ Select only data points which are of this data type.
619
+ If None (the default) then match any data types
620
+
621
+ tracers: tuple
622
+ Select only data points which match this tracer combination.
623
+ If None (the default) then match any tracer combinations.
624
+
625
+ **select:
626
+ Select only data points with tag names and values matching
627
+ all values provided in this kwargs option.
628
+ You can also use the syntax name__lt=value or
629
+ name__gt=value in the selection to select points
630
+ less or greater than a threshold
631
+
632
+ Returns
633
+ -------
634
+ values: array
635
+ The standard deviation values for each matching data point,
636
+ in the order they were added.
637
+
638
+ """
639
+ indices = self.indices(data_type=data_type, tracers=tracers, **select)
640
+ return np.sqrt(self.covariance.get_block(indices).diagonal())
641
+
642
+ def get_data_types(self, tracers=None):
643
+ """
644
+ Get a list of the different data types stored in the Sacc
645
+
646
+ Parameters
647
+ ----------
648
+ tracers: tuple
649
+ Select only data types which match this tracer combination.
650
+ If None (the default) then match any tracer combinations.
651
+
652
+ Returns
653
+ --------
654
+ data_types: list of strings
655
+ A list of the string data types in the data set
656
+ """
657
+ data_types = unique_list(d.data_type for d in self.data if
658
+ ((tracers is None) or (d.tracers == tracers)))
659
+
660
+ return data_types
661
+
662
+ def has_tracer(self, name):
663
+ """
664
+ Determine whether a tracer object with the given name is present
665
+
666
+ Parameters
667
+ ----------
668
+ name: str
669
+ A string name of a tracer
670
+
671
+ Returns
672
+ -------
673
+ value: True if the tracer exists, else False
674
+ """
675
+ return name in self.tracers
676
+
677
+ def get_tracer(self, name):
678
+ """
679
+ Get the tracer object with the given name
680
+
681
+ Parameters
682
+ -----------
683
+ name: str
684
+ A string name of a tracer
685
+
686
+ Returns
687
+ -------
688
+ tracer: BaseTracer object
689
+ The object corresponding to the name.
690
+ """
691
+ return self.tracers[name]
692
+
693
+ def get_tracer_combinations(self, data_type=None):
694
+ """
695
+ Find all the tracer combinations (e.g. tomographic bin pairs)
696
+ for the given data type
697
+
698
+ Parameters
699
+ -----------
700
+ data_type: str
701
+ A string name of the data type to find
702
+
703
+ Returns
704
+ -------
705
+ combinations: list of tuples of strings
706
+ A list of all the tracer combinations found
707
+ in any data point. No specific ordering.
708
+ """
709
+ indices = self.indices(data_type=data_type)
710
+ return unique_list(self.data[i].tracers for i in indices)
711
+
712
+ def remove_tracers(self, names):
713
+ """
714
+ Remove the tracer objects and their associated data points
715
+
716
+ Parameters
717
+ -----------
718
+ names: list
719
+ A list of string names of the tracers to be removed
720
+
721
+ """
722
+
723
+ for trs in self.get_tracer_combinations():
724
+ for tri in trs:
725
+ if tri in names:
726
+ self.remove_selection(tracers=trs)
727
+ break
728
+
729
+ for name in names:
730
+ del self.tracers[name]
731
+
732
+ def keep_tracers(self, names):
733
+ """
734
+ Keep only the tracer objects and their associated data points.
735
+
736
+ Parameters
737
+ -----------
738
+ names: list
739
+ A list of string names of the tracers to be kept
740
+
741
+ """
742
+
743
+ for trs in self.get_tracer_combinations():
744
+ for tri in trs:
745
+ if tri not in names:
746
+ self.remove_selection(tracers=trs)
747
+
748
+ trs_names = list(self.tracers.keys())
749
+ for name in trs_names:
750
+ if name not in names:
751
+ del self.tracers[name]
752
+
753
+ def rename_tracer(self, name, new_name):
754
+ """
755
+ Get the tracer object with the given name
756
+
757
+ Parameters
758
+ -----------
759
+ name: str
760
+ A string name of a tracer to be changed the name
761
+ new_name: str
762
+ A string with the new name of the tracer
763
+
764
+ """
765
+
766
+ tr = self.tracers.pop(name)
767
+ tr.name = new_name
768
+ self.tracers[new_name] = tr
769
+
770
+ for d in self.data:
771
+ new_trs = []
772
+ for tri in d.tracers:
773
+ if tri == name:
774
+ tri = new_name
775
+
776
+ new_trs.append(tri)
777
+
778
+ d.tracers = tuple(new_trs)
779
+
780
+ @property
781
+ def mean(self):
782
+ """
783
+ Get the vector of mean values for the entire data set.
784
+
785
+ Returns
786
+ -------
787
+ mean: array
788
+ numpy array with all the mean values in the data set
789
+ """
790
+ return np.array([d.value for d in self.data])
791
+
792
+ @mean.setter
793
+ def mean(self, mu):
794
+ """
795
+ Set the vector of mean values for the entire data set.
796
+
797
+ Parameters
798
+ -----------
799
+
800
+ mu: array
801
+ Replace the mean values of all the data points.
802
+ """
803
+ if not len(mu) == len(self.data):
804
+ raise ValueError("Tried to set mean with thing of length {}"
805
+ " but data is length {}".format(len(mu),
806
+ len(self.data)))
807
+ for m, d in zip(mu, self.data):
808
+ d.value = m
809
+
810
+ def _make_window_tables(self):
811
+ # Convert any window objects in the data set to tables,
812
+ # and record a mapping from those objects to table references
813
+ # This could easily be extended to other types
814
+ all_windows = unique_list(d.get_tag('window') for d in self.data)
815
+ window_ids = {w: id(w) for w in all_windows}
816
+ tables = BaseWindow.to_tables(all_windows)
817
+ return tables, window_ids
818
+
819
+ def save_fits(self, filename, overwrite=False):
820
+ """
821
+ Save this data set to a FITS format Sacc file.
822
+
823
+ Parameters
824
+ ----------
825
+ filename: str
826
+ Destination FITS file name
827
+
828
+ overwrite: bool
829
+ If False (the default), raise an error if the file already exists
830
+ If True, overwrite the file silently.
831
+ """
832
+
833
+ # Since we don't want to re-order the file as a side effect
834
+ # we first make a copy of ourself and re-order that.
835
+ # Tables for the windows
836
+ tables, window_ids = self._make_window_tables()
837
+ lookup = {'window': window_ids}
838
+
839
+ # Tables for the tracers
840
+ tables += BaseTracer.to_tables(self.tracers.values())
841
+
842
+ # Tables for the data sets
843
+ for dt in self.get_data_types():
844
+ indices = self.indices(dt)
845
+ data = [self.data[i] for i in indices]
846
+ table = DataPoint.to_table(data, lookup)
847
+ table.add_column(indices, name='sacc_ordering')
848
+ # Could move this inside to_table?
849
+ table.meta['SACCTYPE'] = 'data'
850
+ table.meta['SACCNAME'] = dt
851
+ table.meta['EXTNAME'] = f'data:{dt}'
852
+ tables.append(table)
853
+
854
+ # Create the actual fits object
855
+ hdr = fits.Header()
856
+
857
+ # save any global metadata in the header.
858
+ # We save the keys and values as separate header cards,
859
+ # because otherwise the keys are all forced to upper case
860
+ hdr['NMETA'] = len(self.metadata)
861
+ for i, (k, v) in enumerate(self.metadata.items()):
862
+ hdr[f'KEY{i}'] = k
863
+ hdr[f'VAL{i}'] = v
864
+ hdus = [fits.PrimaryHDU(header=hdr)] + \
865
+ [fits.table_to_hdu(table) for table in tables]
866
+
867
+ # Covariance, if needed.
868
+ # All the other data elements become astropy tables first,
869
+ # But covariances are a bit more complicated and dense, so we
870
+ # allow them to convert straight to
871
+ if self.covariance is not None:
872
+ hdus.append(self.covariance.to_hdu())
873
+
874
+ # Make and save the final FITS data
875
+ hdu_list = fits.HDUList(hdus)
876
+
877
+ # The astropy writeto shows very poor performance
878
+ # when writing lots of small metadata strings on
879
+ # the NERSC Lustre file system. So we write to
880
+ # a buffer first and then save that.
881
+
882
+ # First we have to manually check for overwritten files
883
+ # We raise the same error as astropy
884
+ if os.path.exists(filename) and not overwrite:
885
+ raise OSError(f"File {filename} already exists and overwrite=False")
886
+
887
+ # Create the buffer and write the data to it
888
+ buf = BytesIO()
889
+ hdu_list.writeto(buf)
890
+
891
+ # Rewind and read the binary data we just wrote
892
+ buf.seek(0)
893
+ output_data = buf.read()
894
+
895
+ # Write the binary data to the target file
896
+ with open(filename, "wb") as f:
897
+ f.write(output_data)
898
+
899
+ @classmethod
900
+ def load_fits(cls, filename):
901
+ """
902
+ Load a Sacc data set from a FITS file.
903
+
904
+ Don't try to make these FITS files yourself - use the tools
905
+ provided in this package to make and save them.
906
+
907
+ Parameters
908
+ ----------
909
+ filename: str
910
+ A FITS format sacc file
911
+ """
912
+ hdu_list = fits.open(filename, "readonly")
913
+
914
+ # Split the HDU's into the different sacc types
915
+ tracer_tables = [Table.read(hdu)
916
+ for hdu in hdu_list
917
+ if hdu.header.get('SACCTYPE') == 'tracer']
918
+ window_tables = [Table.read(hdu)
919
+ for hdu in hdu_list
920
+ if hdu.header.get('SACCTYPE') == 'window']
921
+ data_tables = [Table.read(hdu) for hdu in hdu_list
922
+ if hdu.header.get('SACCTYPE') == 'data']
923
+ cov = [hdu for hdu in hdu_list if hdu.header.get('SACCTYPE') == 'cov']
924
+
925
+ # Pull out the classes for these components.
926
+ tracers = BaseTracer.from_tables(tracer_tables)
927
+ windows = BaseWindow.from_tables(window_tables)
928
+
929
+ # The lookup table is used to convert from ID numbers to
930
+ # Window objects.
931
+ lookup = {'window': windows}
932
+
933
+ # Collect together all the data points from the different sections
934
+ data_unordered = []
935
+ index = []
936
+ for table in data_tables:
937
+ index += table["sacc_ordering"].tolist()
938
+ table.remove_column('sacc_ordering')
939
+ data_unordered += DataPoint.from_table(table, lookup)
940
+
941
+ # Put the data back in its original order, matching the
942
+ # covariance.
943
+ data = [None for i in range(len(data_unordered))]
944
+ for i, d in zip(index, data_unordered):
945
+ data[i] = d
946
+
947
+ # Finally, take all the pieces that we have collected
948
+ # and add them all into this data set.
949
+ S = cls()
950
+ for tracer in tracers.values():
951
+ S.add_tracer_object(tracer)
952
+
953
+ # Add the data points manually instead of using the API, since we
954
+ # have already constructed them.
955
+ for d in data:
956
+ S.data.append(d)
957
+
958
+ # Assume there is only a single covariance extension,
959
+ # if there are any
960
+ if cov:
961
+ S.add_covariance(BaseCovariance.from_hdu(cov[0]))
962
+
963
+ # Load metadata from the primary heaer
964
+ header = hdu_list[0].header
965
+
966
+ # Load each key,value pair in turn.
967
+ # This will work for normal scalar data types;
968
+ # arrays etc. will need some thought.
969
+ n_meta = header['NMETA']
970
+ for i in range(n_meta):
971
+ k = header[f'KEY{i}']
972
+ v = header[f'VAL{i}']
973
+ S.metadata[k] = v
974
+
975
+ hdu_list.close()
976
+
977
+ return S
978
+
979
+ #
980
+ # Methods below here are helper functions for specific types of data.
981
+ # We can add more of them as it becomes clear what people need.
982
+ #
983
+ #
984
+
985
+ def _get_2pt(self, data_type, tracer1, tracer2, return_cov,
986
+ angle_name, return_ind=False):
987
+ # Internal helper method for get_ell_cl and get_theta_xi
988
+ ind = self.indices(data_type, (tracer1, tracer2))
989
+
990
+ mu = np.array(self.mean[ind])
991
+ angle = np.array(self._get_tags_by_index([angle_name], ind)[0])
992
+
993
+ if return_cov:
994
+ if not self.has_covariance():
995
+ raise ValueError("This sacc data does not have "
996
+ "a covariance attached")
997
+ cov_block = self.covariance.get_block(ind)
998
+ if return_ind:
999
+ return angle, mu, cov_block, ind
1000
+ else:
1001
+ return angle, mu, cov_block
1002
+ else:
1003
+ if return_ind:
1004
+ return angle, mu, ind
1005
+ else:
1006
+ return angle, mu
1007
+
1008
+ def get_bandpower_windows(self, indices):
1009
+ """
1010
+ Returns bandpower window functoins for a given set of datapoints.
1011
+ All datapoints must share the same bandpower window.
1012
+
1013
+ Parameters
1014
+ ----------
1015
+ indices: array
1016
+ indices of the data points you want windows for
1017
+
1018
+ Returns
1019
+ -------
1020
+ windows: BandpowerWindow object containing the bandpower window
1021
+ functions for these indices.
1022
+ """
1023
+ ws = unique_list(self.data[i].tags.get('window') for i in indices)
1024
+ if len(ws) != 1:
1025
+ raise ValueError("You have asked for window functions, "
1026
+ "however, the points you have selected "
1027
+ "have different windows associated to them."
1028
+ "Please narrow down your selection (specify "
1029
+ "tracers and data type) or get windows "
1030
+ "later.")
1031
+ ws = ws[0]
1032
+ if not isinstance(ws, BandpowerWindow):
1033
+ warnings.warn("No bandpower windows associated with these data")
1034
+ return None
1035
+ else:
1036
+ w_inds = np.array(self._get_tags_by_index(['window_ind'],
1037
+ indices)[0])
1038
+ return ws.get_section(w_inds)
1039
+
1040
+ def get_ell_cl(self, data_type, tracer1, tracer2,
1041
+ return_cov=False, return_ind=False):
1042
+ """
1043
+ Helper method to extract the ell and C_ell values for a specific
1044
+ data type (e.g. 'shear_ee' and pair of tomographic bins)
1045
+
1046
+ Parameters
1047
+ ----------
1048
+ data_type: str
1049
+ Which C_ell type to extract
1050
+
1051
+ tracer1: str
1052
+ The name of the first tracer, for example a tomographic bin name
1053
+
1054
+ tracer2: str
1055
+ The name of the second tracer
1056
+
1057
+ return_cov: bool
1058
+ If True, also return the block of the covariance
1059
+ corresponding to these points. Default=False
1060
+
1061
+ return_ind: bool
1062
+ If True, also return the datapoint indices. Default=False
1063
+
1064
+ Returns
1065
+ -------
1066
+ ell: array
1067
+ Ell values for this tracer pair
1068
+ mu: array
1069
+ Mean values for this tracer pair
1070
+ cov_block: 2D array
1071
+ (Only if return_cov=True) The block of the covariance for
1072
+ these points
1073
+ indices: array
1074
+ (Only if return_ind=True) datapoint indices.
1075
+ """
1076
+ return self._get_2pt(data_type, tracer1, tracer2, return_cov,
1077
+ 'ell', return_ind)
1078
+
1079
+ def get_theta_xi(self, data_type, tracer1, tracer2,
1080
+ return_cov=False, return_ind=False):
1081
+ """
1082
+ Helper method to extract the theta and correlation function
1083
+ values for a specific data type (e.g. 'shear_xi' and pair of
1084
+ tomographic bins).
1085
+
1086
+ Parameters
1087
+ ----------
1088
+
1089
+ data_type: str
1090
+ Which type of xi to extract
1091
+
1092
+ tracer1: str
1093
+ The name of the first tracer, for example a tomographic bin name
1094
+
1095
+ tracer2: str
1096
+ The name of the second tracer
1097
+
1098
+ return_cov: bool
1099
+ If True, also return the block of the covariance
1100
+ corresponding to these points. Default=False
1101
+
1102
+ return_ind: bool
1103
+ If True, also return the datapoint indices. Default=False
1104
+
1105
+ Returns
1106
+ -------
1107
+ ell: array
1108
+ Ell values for this tracer pair
1109
+
1110
+ mu: array
1111
+ Mean values for this tracer pair
1112
+
1113
+ cov_block: 2D array
1114
+ (Only if return_cov=True) The block of the covariance for
1115
+ these points
1116
+ indices: array
1117
+ (Only if return_ind=True) datapoint indices.
1118
+ """
1119
+ return self._get_2pt(data_type, tracer1, tracer2, return_cov,
1120
+ 'theta', return_ind)
1121
+
1122
+ def _add_2pt(self, data_type, tracer1, tracer2, x, tag_val, tag_name,
1123
+ window, tracers_later, tag_extra=None, tag_extra_name=None):
1124
+ """
1125
+ Internal method for adding 2pt data points.
1126
+ Copes with multiple values for the parameters
1127
+ """
1128
+ # single data point case
1129
+ if np.isscalar(tag_val):
1130
+ t = {tag_name: float(tag_val)}
1131
+ if tag_extra_name is not None:
1132
+ t[tag_extra_name] = tag_extra
1133
+ if window is not None:
1134
+ t['window'] = window
1135
+ self.add_data_point(data_type, (tracer1, tracer2), x,
1136
+ tracers_later=tracers_later, **t)
1137
+ return
1138
+ # multiple ell/theta values but same bin
1139
+ elif np.isscalar(tracer1):
1140
+ n1 = len(x)
1141
+ n2 = len(tag_val)
1142
+ if tag_extra_name is None:
1143
+ tag_extra = np.zeros(n1)
1144
+ n3 = n1
1145
+ else:
1146
+ n3 = len(tag_extra)
1147
+ if not (n1 == n2 == n3):
1148
+ raise ValueError("Length of inputs do not match in"
1149
+ f"added 2pt data ({n1},{n2},{n3})")
1150
+ if window is None:
1151
+ for tag_i, x_i, te_i in zip(tag_val, x, tag_extra):
1152
+ self._add_2pt(data_type, tracer1, tracer2, x_i,
1153
+ tag_i, tag_name, window,
1154
+ tracers_later, te_i, tag_extra_name)
1155
+ else:
1156
+ for tag_i, x_i, w_i, te_i in zip(tag_val, x,
1157
+ window, tag_extra):
1158
+ self._add_2pt(data_type, tracer1, tracer2, x_i,
1159
+ tag_i, tag_name, w_i,
1160
+ tracers_later, te_i, tag_extra_name)
1161
+ # multiple bin values
1162
+ elif np.isscalar(data_type):
1163
+ n1 = len(x)
1164
+ n2 = len(tag_val)
1165
+ n3 = len(tracer1)
1166
+ n4 = len(tracer2)
1167
+ if tag_extra_name is None:
1168
+ tag_extra = np.zeros(n1)
1169
+ n5 = n1
1170
+ else:
1171
+ n5 = len(tag_extra)
1172
+ if not (n1 == n2 == n3 == n4 == n5):
1173
+ raise ValueError("Length of inputs do not match in "
1174
+ f"added 2pt data ({n1},{n2},{n3},{n4},{n5})")
1175
+ if window is None:
1176
+ for b1, b2, tag_i, x_i, te_i in zip(tracer1, tracer2, tag_val,
1177
+ x, tag_extra):
1178
+ self._add_2pt(data_type, b1, b2, x_i, tag_i, tag_name,
1179
+ window, tracers_later, te_i, tag_extra_name)
1180
+ else:
1181
+ for b1, b2, tag_i, x_i, w_i, te_i in zip(tracer1,
1182
+ tracer2,
1183
+ tag_val,
1184
+ x,
1185
+ window,
1186
+ tag_extra):
1187
+ self._add_2pt(data_type, b1, x_i, tag_i, tag_name,
1188
+ w_i, tracers_later, te_i, tag_extra_name)
1189
+ # multiple data point values
1190
+ else:
1191
+ n1 = len(x)
1192
+ n2 = len(tag_val)
1193
+ n3 = len(tracer1)
1194
+ n4 = len(tracer2)
1195
+ n5 = len(data_type)
1196
+ if tag_extra_name is None:
1197
+ tag_extra = np.zeros(n1)
1198
+ n6 = n1
1199
+ else:
1200
+ n6 = len(tag_extra)
1201
+ if not (n1 == n2 == n3 == n4 == n5 == n6):
1202
+ raise ValueError("Length of inputs do not match in added "
1203
+ f"2pt data ({n1},{n2},{n3},{n4},{n5},{n6})")
1204
+ if window is None:
1205
+ for d, b1, b2, tag_i, x_i, te_i in zip(data_type,
1206
+ tracer1,
1207
+ tracer2,
1208
+ tag_val,
1209
+ x,
1210
+ tag_extra):
1211
+ self._add_2pt(d, b1, b2, x_i, tag_i, tag_name,
1212
+ window, tracers_later,
1213
+ te_i, tag_extra_name)
1214
+ else:
1215
+ for d, b1, b2, tag_i, x_i, w_i, te_i in zip(data_type,
1216
+ tracer1,
1217
+ tracer2,
1218
+ tag_val,
1219
+ x,
1220
+ window,
1221
+ tag_extra):
1222
+ self._add_2pt(d, b1, b2, x_i, tag_i, tag_name,
1223
+ w_i, tracers_later,
1224
+ te_i, tag_extra_name)
1225
+
1226
+ def add_ell_cl(self, data_type, tracer1, tracer2, ell, x,
1227
+ window=None, tracers_later=False):
1228
+ """
1229
+ Add a series of 2pt Fourier space data points, either
1230
+ individually or as a group.
1231
+
1232
+ Parameters
1233
+ ----------
1234
+ data_type: str or array/list of str
1235
+ Which type C_ell to add
1236
+
1237
+ tracer1: str or array/list of str
1238
+ The name(s) of the first tracer, for example a tomographic bin name
1239
+
1240
+ tracer2: str or array/list of str
1241
+ The name(s) of the second tracer
1242
+
1243
+ ell: int or array/list of int/float
1244
+ The ell values for these data points
1245
+
1246
+ x: float or array/list of float
1247
+ The C_ell values for these data points
1248
+
1249
+ window: Window instance
1250
+ Optional window object describing the window function
1251
+ of the data point.
1252
+
1253
+ tracers_later: bool
1254
+ Optional. If False (the default), complain if n(z) tracers have
1255
+ not yet been defined. Otherwise, suppress this warning
1256
+
1257
+ Returns
1258
+ -------
1259
+ None
1260
+
1261
+ """
1262
+ if isinstance(window, BandpowerWindow):
1263
+ if len(ell) != window.nv:
1264
+ raise ValueError("Input bandpowers are misshapen")
1265
+ tag_extra = range(window.nv)
1266
+ tag_extra_name = "window_ind"
1267
+ window_use = [window for _ in range(window.nv)]
1268
+ else:
1269
+ tag_extra = None
1270
+ tag_extra_name = None
1271
+ window_use = window
1272
+
1273
+ self._add_2pt(data_type, tracer1, tracer2, x, ell, 'ell',
1274
+ window_use, tracers_later,
1275
+ tag_extra, tag_extra_name)
1276
+
1277
+ def add_theta_xi(self, data_type, tracer1, tracer2, theta, x,
1278
+ window=None, tracers_later=False):
1279
+ """
1280
+ Add a series of 2pt real space data points, either
1281
+ individually or as a group.
1282
+
1283
+ Parameters
1284
+ ----------
1285
+ data_type: str or array/list of str
1286
+ Which xi type to extract
1287
+
1288
+ tracer1: str or array/list of str
1289
+ The name(s) of the first tracer, for example a tomographic bin name
1290
+
1291
+ tracer2: str or array/list of str
1292
+ The name(s) of the second tracer
1293
+
1294
+ theta: float or array/list of int
1295
+ The ell values for these data points
1296
+
1297
+ x: float or array/list of float
1298
+ The C_ell values for these data points
1299
+
1300
+ window: Window instance
1301
+ Optional window object describing the window function
1302
+ of the data point.
1303
+
1304
+ tracers_later: bool
1305
+ Optional. If False (the default), complain if n(z) tracers have
1306
+ not yet been defined. Otherwise, suppress this warning
1307
+
1308
+ Returns
1309
+ -------
1310
+ None
1311
+
1312
+ """
1313
+ self._add_2pt(data_type, tracer1, tracer2, x, theta, 'theta',
1314
+ window, tracers_later)
1315
+
1316
+
1317
+ def concatenate_data_sets(*data_sets, labels=None, same_tracers=None):
1318
+ """Combine multiple sacc data sets together into one.
1319
+
1320
+ In case of two tracers or metadata items with the same name,
1321
+ you can use the labels option to pass in a list of strings to append
1322
+ to all the names.
1323
+
1324
+ The Covariance will be combined into either a BlockDiagonal covariance or
1325
+ a Diagonal covariance, depending on the inputs. Either all inputs should
1326
+ have a covariance attached or none of them.
1327
+
1328
+ Parameters
1329
+ ----------
1330
+ *data_sets: Sacc objects
1331
+ The data sets to combined
1332
+
1333
+ labels: List[str]
1334
+ Optional list of strings to append to tracer and metadata names, in
1335
+ case of a clash.
1336
+
1337
+ same_tracers: List[str]
1338
+ Optional list of tracers that are assumed to be the same in the
1339
+ different data_sets but with no correlation between the data points
1340
+ involving them. Only the first occurance of each tracer will be added
1341
+ to the combined data set.
1342
+
1343
+ Returns
1344
+ -------
1345
+ output: Sacc object
1346
+ The combined data set.
1347
+
1348
+ """
1349
+ # Early return of an empty data set object
1350
+ if len(data_sets) == 0:
1351
+ return Sacc()
1352
+
1353
+ # check for wrong number of labels
1354
+ if labels is not None:
1355
+ if len(labels) != len(data_sets):
1356
+ raise ValueError("Wrong number of labels supplied when "
1357
+ "concatenating data sets")
1358
+
1359
+ # Make same_tracers an empty list for easy comparison
1360
+ if same_tracers is None:
1361
+ same_tracers = []
1362
+
1363
+ data_0 = data_sets[0]
1364
+
1365
+ # Either all the data sets should have covariances or none of
1366
+ # them should. Concatenating covariances should be
1367
+ # straightforward and should always result in a block-diagonal
1368
+ # covariance
1369
+ if data_0.has_covariance():
1370
+ if not all(data_set.has_covariance()
1371
+ for data_set in data_sets):
1372
+ raise ValueError("Either all concatenated data sets must "
1373
+ "have covariances, or none of them")
1374
+ else:
1375
+ if any(data_set.has_covariance()
1376
+ for data_set in data_sets):
1377
+ raise ValueError("Either all concatenated data sets must "
1378
+ "have covariances, or none of them")
1379
+
1380
+ output = Sacc()
1381
+
1382
+ # Copy the tracers to the new
1383
+ for i, data_set in enumerate(data_sets):
1384
+ for tracer in data_set.tracers.values():
1385
+
1386
+ # We will be modifying the tracer, so we copy it.
1387
+ tracer = copy.deepcopy(tracer)
1388
+
1389
+ # Optionally add a suffix label to avoid name clashes.
1390
+ if (labels is not None) and (tracer.name not in same_tracers):
1391
+ tracer.name = f'{tracer.name}_{labels[i]}'
1392
+
1393
+ # Check for duplicate tracer names.
1394
+ # Probably this happens because the user has not provided
1395
+ # any labels to use as tracer suffices. But it could also
1396
+ # happen if they have chosen really really bad labels
1397
+ if tracer.name in output.tracers:
1398
+ if tracer.name in same_tracers:
1399
+ pass
1400
+ elif labels is None:
1401
+ raise ValueError("There is a name clash between "
1402
+ "tracers in the data sets. "
1403
+ "Use the labels option to give "
1404
+ "new names to them")
1405
+ else:
1406
+ raise ValueError("After applying your labels "
1407
+ "there is still a name clash "
1408
+ "between tracers in your concatenation."
1409
+ " Try different labels?")
1410
+ else:
1411
+ # Build up the combined tracer collection
1412
+ output.add_tracer_object(tracer)
1413
+
1414
+ for d in data_set.data:
1415
+ # Shallow copy because we do not want to clone Window functions,
1416
+ # since they are often shared. The reason we do it at all
1417
+ # is because we may be modifying the tracers names below.
1418
+ d = copy.copy(d)
1419
+
1420
+ # Rename the tracers if required.
1421
+ if labels is not None:
1422
+ label = labels[i]
1423
+ d.tracers = tuple([f'{t}_{label}' for t in d.tracers])
1424
+ # Data points might reasonably have a label already,
1425
+ # but we would like to add a label from this concatenation
1426
+ # process too. If they have both, we concatenat them.
1427
+ # For consistency with the tracers we don't include an
1428
+ # underscore
1429
+ orig_label = d.get_tag('label', '')
1430
+ d.tags['label'] = (f'{orig_label}_{label}'
1431
+ if orig_label else label)
1432
+
1433
+ # And build up the combined data vector
1434
+ output.data.append(d)
1435
+
1436
+ # Combine the covariances
1437
+ if data_sets[0].has_covariance():
1438
+ covs = [d.covariance for d in data_sets]
1439
+ cov = concatenate_covariances(*covs)
1440
+ output.add_covariance(cov)
1441
+
1442
+ # Now just the metadata left.
1443
+ # It is an error if there is a key that is the same in both
1444
+ for i, data_set in enumerate(data_sets):
1445
+ for key, val in data_set.metadata.items():
1446
+
1447
+ # Use the label as a suffix here also.
1448
+ if labels is not None:
1449
+ key = key + labels[i]
1450
+
1451
+ # Check for clashing metadata
1452
+ if key in output.metadata:
1453
+ raise ValueError("Metadata in concatenated Saccs have "
1454
+ "same name. Set the labels parameter "
1455
+ "to fix this.")
1456
+ output.metadata[key] = val
1457
+
1458
+ return output