sacc 1.0.2__py3-none-any.whl → 2.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/__init__.py CHANGED
@@ -3,4 +3,7 @@ from .windows import Window, BandpowerWindow, TopHatWindow, LogTopHatWindow # n
3
3
  from .data_types import standard_types, parse_data_type_name, build_data_type_name # noqa
4
4
  from .tracers import BaseTracer # noqa
5
5
  from .covariance import BaseCovariance # noqa
6
- __version__ = '1.0.2' #noqa
6
+ from .tracer_uncertainty import NZLinearUncertainty, NZShiftUncertainty, NZShiftStretchUncertainty # noqa
7
+ from .io import BaseIO # noqa
8
+ from . import io # noqa
9
+ __version__ = '2.0' #noqa
sacc/covariance.py CHANGED
@@ -1,12 +1,12 @@
1
- from astropy.io import fits
2
1
  from astropy.table import Table
3
2
  import scipy.linalg
4
3
  import numpy as np
4
+ import warnings
5
5
 
6
6
  from .utils import invert_spd_matrix
7
+ from .io import BaseIO, ONE_OBJECT_PER_TABLE, ONE_OBJECT_MULTIPLE_TABLES
7
8
 
8
-
9
- class BaseCovariance:
9
+ class BaseCovariance(BaseIO):
10
10
  """
11
11
  The abstract base class for covariances in different forms.
12
12
  These are not currently designed to be modified after creation.
@@ -26,7 +26,7 @@ class BaseCovariance:
26
26
  cov_type: string
27
27
  The type of the covariance (class variable)
28
28
  """
29
- _covariance_classes = {}
29
+ _sub_classes = {}
30
30
 
31
31
  def __init__(self):
32
32
  """Abstract superclass constructor.
@@ -37,13 +37,34 @@ class BaseCovariance:
37
37
  self._dense = None
38
38
  self._dense_inverse = None
39
39
 
40
- # This method gets called whenever a subclass is
41
- # defined. The keyword argument in the class definition
42
- # (e.g. cov_type='full' below is passed to this class method)
43
- @classmethod
44
- def __init_subclass__(cls, cov_type):
45
- cls._covariance_classes[cov_type] = cls
46
- cls.cov_type = cov_type
40
+ # At the moment we only allow one covariance object per table,
41
+ # so this is only used for consistency when saving objects.
42
+ self.name = "cov"
43
+
44
+ def __eq__(self, other):
45
+ """
46
+ Test for equality
47
+
48
+ Parameters
49
+ ----------
50
+ other: object
51
+ The other object to test for equality
52
+
53
+ Returns
54
+ -------
55
+ equal: bool
56
+ True if the objects are equal
57
+ """
58
+ if not isinstance(other, self.__class__):
59
+ return False
60
+ # We do not test the inverse; we rely on the fact that
61
+ # if the dense matrices are equal, then the inverses will be equal.
62
+ # We are also relying on each subclass to have an instance variable
63
+ # 'size'.
64
+ return self.name == other.name and \
65
+ self.size == other.size and \
66
+ ((self._dense is None and other._dense is None) or \
67
+ np.allclose(self._dense, other._dense))
47
68
 
48
69
  @classmethod
49
70
  def from_hdu(cls, hdu):
@@ -64,8 +85,11 @@ class BaseCovariance:
64
85
  instance: BaseCovariance
65
86
  A covariance instance
66
87
  """
88
+ warnings.warn("You are using an older SACC legacy SACC file format with old covariance data."
89
+ " Consider updating it by loading it and saving it again with the latest SACC version.",
90
+ DeprecationWarning)
67
91
  subclass_name = hdu.header['saccclss']
68
- subclass = cls._covariance_classes[subclass_name]
92
+ subclass = cls._sub_classes[subclass_name]
69
93
  return subclass.from_hdu(hdu)
70
94
 
71
95
  @classmethod
@@ -80,32 +104,29 @@ class BaseCovariance:
80
104
  Parameters
81
105
  ----------
82
106
  cov: list[array] or array
83
- If a list, the total length of all the arrays in it
84
- should equal n. If an array, it should be either 1D of
85
- length n or 2D of shape (n x n).
107
+ If a list, it should be a list of array-like objects each of which
108
+ can be coerced into a 2d array. A BlockDiagonalCovariance will be
109
+ returned.
86
110
 
87
- n: int
88
- length of the data vector to which this covariance applies
111
+ If an array, it should be either 1D or 2d and square. Either a
112
+ DiagonalCovariance or a FullCovariance will be returned.
89
113
  """
90
114
  if isinstance(cov, list):
91
- s = 0
92
115
  for block in cov:
93
116
  block = np.atleast_2d(block)
94
117
  if (block.ndim != 2) or (block.shape[0] != block.shape[1]):
95
118
  raise ValueError("Covariance block has wrong size "
96
119
  f"or shape {block.shape}")
97
- s += block.shape[0]
98
120
  return BlockDiagonalCovariance(cov)
99
- else:
100
- cov = np.array(cov).squeeze()
101
- if cov.ndim == 0:
102
- return DiagonalCovariance(np.atleast_1d(cov))
103
- if cov.ndim == 1:
104
- return DiagonalCovariance(cov)
105
- if (cov.ndim != 2) or (cov.shape[0] != cov.shape[1]):
106
- raise ValueError("Covariance is not a 2D square matrix "
107
- f"- shape: {cov.shape}")
108
- return FullCovariance(cov)
121
+ cov = np.array(cov).squeeze()
122
+ if cov.ndim == 0:
123
+ return DiagonalCovariance(np.atleast_1d(cov))
124
+ if cov.ndim == 1:
125
+ return DiagonalCovariance(cov)
126
+ if (cov.ndim != 2) or (cov.shape[0] != cov.shape[1]):
127
+ raise ValueError("Covariance is not a 2D square matrix "
128
+ f"- shape: {cov.shape}")
129
+ return FullCovariance(cov)
109
130
 
110
131
  @property
111
132
  def dense(self):
@@ -140,7 +161,7 @@ class BaseCovariance:
140
161
  return self._dense_inverse
141
162
 
142
163
 
143
- class FullCovariance(BaseCovariance, cov_type='full'):
164
+ class FullCovariance(BaseCovariance, type_name='full'):
144
165
  """
145
166
  A covariance subclass representing a full matrix with correlations
146
167
  anywhere. Represented as an n x n matrix.
@@ -153,15 +174,40 @@ class FullCovariance(BaseCovariance, cov_type='full'):
153
174
  covmat: 2D array
154
175
  The matrix itself, of shape (size x size)
155
176
  """
177
+
178
+ storage_type = ONE_OBJECT_PER_TABLE
179
+
156
180
  def __init__(self, covmat):
157
181
  self.covmat = np.atleast_2d(covmat)
158
182
  self.size = self.covmat.shape[0]
159
183
  super().__init__()
160
184
 
161
- def to_hdu(self):
185
+ def __eq__(self, other):
186
+ return super().__eq__(other) and \
187
+ np.allclose(self.covmat, other.covmat)
188
+
189
+ @classmethod
190
+ def from_hdu(cls, hdu):
191
+ """
192
+
193
+ Load a covariance object from the data in the HDU
194
+ LEGACY METHOD: new sacc files will use from_table
195
+
196
+ Parameters
197
+ ----------
198
+ hdu: astropy.fits.ImageHDU instance
199
+
200
+ Returns
201
+ -------
202
+ cov: FullCovariance
203
+ Loaded covariance object
204
+ """
205
+ C = hdu.data
206
+ return cls(C)
207
+
208
+ def to_table(self):
162
209
  """
163
- Make an astropy FITS HDU object with this covariance in it.
164
- This is represented as an image.
210
+ Make an astropy table object with this covariance in it.
165
211
 
166
212
  Parameters
167
213
  ----------
@@ -169,32 +215,33 @@ class FullCovariance(BaseCovariance, cov_type='full'):
169
215
 
170
216
  Returns
171
217
  -------
172
- hdu: astropy.fits.ImageHDU instance
173
- HDU that can be used to reconstruct the object.
218
+ table: astropy.table.Table instance
219
+ Table that can be used to reconstruct the object.
174
220
  """
175
- hdu = fits.ImageHDU(self.covmat)
176
- hdu.header['EXTNAME'] = 'covariance'
177
- hdu.header['SACCTYPE'] = 'cov'
178
- hdu.header['SACCCLSS'] = self.cov_type
179
- hdu.header['SIZE'] = self.size
180
- return hdu
221
+ col_names = [f'col_{i}' for i in range(self.size)]
222
+ cols = [self.covmat[i] for i in range(self.size)]
223
+ table = Table(data=cols, names=col_names)
224
+ table.meta['SIZE'] = self.size
225
+ return table
181
226
 
182
227
  @classmethod
183
- def from_hdu(cls, hdu):
228
+ def from_table(cls, table):
184
229
  """
185
- Load a covariance object from the data in the HDU
230
+ Load a covariance object from the data in the table
186
231
 
187
232
  Parameters
188
233
  ----------
189
- hdu: astropy.fits.ImageHDU instance
234
+ table: astropy.table.Table instance
190
235
 
191
236
  Returns
192
237
  -------
193
238
  cov: FullCovariance
194
239
  Loaded covariance object
195
240
  """
196
- C = hdu.data
197
- return cls(C)
241
+ size = table.meta['SIZE']
242
+ covmat = np.array([table[f'col_{i}'] for i in range(size)])
243
+ return cls(covmat)
244
+
198
245
 
199
246
  def keeping_indices(self, indices):
200
247
  """
@@ -240,7 +287,7 @@ class FullCovariance(BaseCovariance, cov_type='full'):
240
287
  return self.covmat.copy()
241
288
 
242
289
 
243
- class BlockDiagonalCovariance(BaseCovariance, cov_type='block'):
290
+ class BlockDiagonalCovariance(BaseCovariance, type_name='block'):
244
291
  """A covariance subclass representing block diagonal covariances
245
292
 
246
293
  Block diagonal covariances have sub-blocks that are full dense matrices,
@@ -258,6 +305,9 @@ class BlockDiagonalCovariance(BaseCovariance, cov_type='block'):
258
305
  size: int
259
306
  overall total size of the matrix
260
307
  """
308
+
309
+ storage_type = ONE_OBJECT_MULTIPLE_TABLES
310
+
261
311
  def __init__(self, blocks):
262
312
  """Create a BlockDiagonalCovariance object from a list of blocks
263
313
 
@@ -271,34 +321,17 @@ class BlockDiagonalCovariance(BaseCovariance, cov_type='block'):
271
321
  self.size = sum(self.block_sizes)
272
322
  super().__init__()
273
323
 
274
- def to_hdu(self):
275
- """Write a FITS HDU from the data, ready to be saved.
276
-
277
- The data in the HDU is stored as a single 1 x size image,
278
- and the header contains the information needed to reconstruct it.
279
-
280
- Parameters
281
- ----------
282
- None
283
-
284
- Returns
285
- -------
286
- hdu: astropy.fits.ImageHDU object
287
- HDU containing data and metadata
288
- """
289
- hdu = fits.ImageHDU(np.concatenate([b.flatten() for b in self.blocks]))
290
- hdu.name = 'covariance'
291
- hdu.header['sacctype'] = 'cov'
292
- hdu.header['saccclss'] = self.cov_type
293
- hdu.header['size'] = self.size
294
- hdu.header['blocks'] = len(self.blocks)
295
- for i, s in enumerate(self.block_sizes):
296
- hdu.header[f'size_{i}'] = s
297
- return hdu
324
+ def __eq__(self, other):
325
+ return super().__eq__(other) and \
326
+ self.block_sizes == other.block_sizes and \
327
+ all(np.allclose(b1, b2)
328
+ for b1, b2
329
+ in zip(self.blocks, other.blocks))
298
330
 
299
331
  @classmethod
300
332
  def from_hdu(cls, hdu):
301
333
  """Read a covariance object from a loaded FITS HDU.
334
+ LEGACY METHOD: new sacc files will use from_tables
302
335
 
303
336
  Parameters
304
337
  ----------
@@ -320,6 +353,59 @@ class BlockDiagonalCovariance(BaseCovariance, cov_type='block'):
320
353
  blocks.append(B)
321
354
  return cls(blocks)
322
355
 
356
+ @classmethod
357
+ def from_tables(cls, tables):
358
+ """
359
+ Load a covariance object from the data in the tables
360
+
361
+ Parameters
362
+ ----------
363
+ tables: list
364
+ list of astropy.table.Table instances in block order
365
+
366
+ Returns
367
+ -------
368
+ cov: BlockDiagonalCovariance
369
+ Loaded covariance object
370
+ """
371
+
372
+ blocks = []
373
+ # Get the block count from the first table
374
+ nblock = list(tables.values())[0].meta['SACCBCNT']
375
+ for i in range(nblock):
376
+ table = tables[f'block_{i}']
377
+ block_size = table.meta['SACCBSZE']
378
+ cols = [table[f'block_col_{i}'] for i in range(block_size)]
379
+ blocks.append(np.array(cols))
380
+ return cls(blocks)
381
+
382
+ def to_tables(self):
383
+ """
384
+ Make an astropy table object with this covariance in it.
385
+
386
+ Parameters
387
+ ----------
388
+ None
389
+
390
+ Returns
391
+ -------
392
+ table: astropy.table.Table instance
393
+ Table that can be used to reconstruct the object.
394
+ """
395
+ tables = {}
396
+ nblock = len(self.blocks)
397
+ for j, block in enumerate(self.blocks):
398
+ b = len(block)
399
+ col_names = [f'block_col_{i}' for i in range(b)]
400
+ cols = [block[i] for i in range(b)]
401
+ table = Table(data=cols, names=col_names)
402
+ table.meta['SIZE'] = self.size
403
+ table.meta['SACCBIDX'] = j
404
+ table.meta['SACCBCNT'] = nblock
405
+ table.meta['SACCBSZE'] = b
406
+ tables[f'block_{j}'] = table
407
+ return tables
408
+
323
409
  def get_block(self, indices):
324
410
  """Read a (not necessarily contiguous) sublock of the matrix
325
411
 
@@ -378,7 +464,7 @@ class BlockDiagonalCovariance(BaseCovariance, cov_type='block'):
378
464
  blocks = [self.blocks[i][m][:, m] for i, m in
379
465
  enumerate(block_masks)]
380
466
  return self.__class__(blocks)
381
- elif (np.diff(indices) > 0).all():
467
+ if (np.diff(indices) > 0).all():
382
468
  s = 0
383
469
  sub_blocks = []
384
470
  for block, sz in zip(self.blocks, self.block_sizes):
@@ -387,10 +473,9 @@ class BlockDiagonalCovariance(BaseCovariance, cov_type='block'):
387
473
  sub_blocks.append(block[m][:, m])
388
474
  s += sz
389
475
  return self.__class__(sub_blocks)
390
- else:
391
- C = scipy.linalg.block_diag(*self.blocks)
392
- C = C[indices][:, indices]
393
- return FullCovariance(C)
476
+ C = scipy.linalg.block_diag(*self.blocks)
477
+ C = C[indices][:, indices]
478
+ return FullCovariance(C)
394
479
 
395
480
  def _get_dense_inverse(self):
396
481
  # Invert all the blocks individually and then
@@ -405,7 +490,7 @@ class BlockDiagonalCovariance(BaseCovariance, cov_type='block'):
405
490
  return scipy.linalg.block_diag(*self.blocks)
406
491
 
407
492
 
408
- class DiagonalCovariance(BaseCovariance, cov_type='diagonal'):
493
+ class DiagonalCovariance(BaseCovariance, type_name='diagonal'):
409
494
  """A covariance subclass representing covariances that are
410
495
  purely diagonal.
411
496
 
@@ -417,6 +502,9 @@ class DiagonalCovariance(BaseCovariance, cov_type='diagonal'):
417
502
  diag: array
418
503
  The diagonal terms in the covariance (i.e. the variances)
419
504
  """
505
+
506
+ storage_type = ONE_OBJECT_PER_TABLE
507
+
420
508
  def __init__(self, variances):
421
509
  """
422
510
  Create a DiagonalCovariance object from the variances
@@ -431,26 +519,10 @@ class DiagonalCovariance(BaseCovariance, cov_type='diagonal'):
431
519
  self.size = len(self.diag)
432
520
  super().__init__()
433
521
 
434
- def to_hdu(self):
435
- """
436
- Make an astropy FITS HDU object with this covariance in it.
437
- In this can a binary table HDU is created.
522
+ def __eq__(self, other):
523
+ return super().__eq__(other) and \
524
+ np.allclose(self.diag, other.diag)
438
525
 
439
- Parameters
440
- ----------
441
- None
442
-
443
- Returns
444
- -------
445
- hdu: astropy.fits.BinTableHDU instance
446
- HDU that can be used to reconstruct the object.
447
- """
448
- table = Table(names=['variance'], data=[self.diag])
449
- hdu = fits.table_to_hdu(table)
450
- hdu.name = 'covariance'
451
- hdu.header['sacctype'] = 'cov'
452
- hdu.header['saccclss'] = self.cov_type
453
- return hdu
454
526
 
455
527
  def keeping_indices(self, indices):
456
528
  """
@@ -472,6 +544,40 @@ class DiagonalCovariance(BaseCovariance, cov_type='diagonal'):
472
544
  D = self.diag[indices]
473
545
  return self.__class__(D)
474
546
 
547
+ @classmethod
548
+ def from_table(cls, table):
549
+ """
550
+ Load a covariance object from the data in the table
551
+
552
+ Parameters
553
+ ----------
554
+ table: astropy.table.Table instance
555
+
556
+ Returns
557
+ -------
558
+ cov: DiagonalCovariance
559
+ Loaded covariance object
560
+ """
561
+ D = table['variance']
562
+ return cls(D)
563
+
564
+ def to_table(self):
565
+ """
566
+ Make an astropy table object with this covariance in it.
567
+
568
+ Parameters
569
+ ----------
570
+ None
571
+
572
+ Returns
573
+ -------
574
+ table: astropy.table.Table instance
575
+ Table that can be used to reconstruct the object.
576
+ """
577
+ table = Table(data=[self.diag], names=['variance'])
578
+ table.meta['SIZE'] = self.size
579
+ return table
580
+
475
581
  @classmethod
476
582
  def from_hdu(cls, hdu):
477
583
  """
sacc/data_types.py CHANGED
@@ -1,8 +1,9 @@
1
1
  from collections import namedtuple
2
2
  from astropy.table import Table
3
3
 
4
- from .utils import (Namespace, hide_null_values,
4
+ from .utils import (Namespace, hide_null_values, numpy_to_vanilla,
5
5
  null_values, camel_case_split_and_lowercase)
6
+ from .io import BaseIO, MULTIPLE_OBJECTS_PER_TABLE
6
7
 
7
8
  # The format for a data type name looks like this:
8
9
  # {sources}_{properties}_{statistic_type}[_{statistic_subtype}]
@@ -220,8 +221,7 @@ def build_data_type_name(sources, properties, statistic, subtype=None):
220
221
  for s in properties[1:]])
221
222
  if subtype:
222
223
  return f"{sources}_{properties}_{statistic}_{subtype}"
223
- else:
224
- return f"{sources}_{properties}_{statistic}"
224
+ return f"{sources}_{properties}_{statistic}"
225
225
 
226
226
 
227
227
  # This makes a namespace object, so you can do:
@@ -232,7 +232,7 @@ def build_data_type_name(sources, properties, statistic, subtype=None):
232
232
  standard_types = Namespace(*required_tags.keys())
233
233
 
234
234
 
235
- class DataPoint:
235
+ class DataPoint(BaseIO, type_name="DataPoint"):
236
236
  """A class for a single data point (one scalar value).
237
237
 
238
238
  Data points have a type, zero or more tracers, a value,
@@ -258,6 +258,8 @@ class DataPoint:
258
258
  Dictionary of further data point metadata, such as binning
259
259
  info, angles, etc.
260
260
  """
261
+ storage_type = MULTIPLE_OBJECTS_PER_TABLE
262
+ _sub_classes = {}
261
263
  def __init__(self, data_type, tracers, value,
262
264
  ignore_missing_tags=False, **tags):
263
265
  """Create a new data point.
@@ -297,12 +299,38 @@ class DataPoint:
297
299
  f"{data_type} "
298
300
  "(ignore_missing_tags=False)")
299
301
 
302
+
300
303
  def __repr__(self):
301
- t = ", ".join(f'{k}={v}' for (k, v) in self.tags.items())
304
+ t = ", ".join(f'{k}={v}' for (k, v) in self.tags.items() if k != 'sacc_ordering')
302
305
  st = f"DataPoint(data_type='{self.data_type}', "
303
306
  st += f"tracers={self.tracers}, value={self.value}, {t})"
304
307
  return st
305
308
 
309
+ def __eq__(self, other):
310
+ """
311
+ Check equality with another DataPoint.
312
+
313
+ This method compares the current DataPoint instance with another
314
+ to determine if they are equivalent. Two DataPoints are considered
315
+ equal if they have the same data_type, tracers, value, and tags.
316
+
317
+ Parameters
318
+ ----------
319
+ other: DataPoint
320
+ The other DataPoint instance to compare against.
321
+
322
+ Returns
323
+ -------
324
+ bool
325
+ True if the DataPoints are equal, False otherwise.
326
+ """
327
+ if not isinstance(other, DataPoint):
328
+ return False
329
+ return (self.data_type == other.data_type and
330
+ self.tracers == other.tracers and
331
+ self.value == other.value and
332
+ self.tags == other.tags)
333
+
306
334
  def get_tag(self, tag, default=None):
307
335
  """
308
336
  Get the value of the the named tag, or None if not found.
@@ -356,10 +384,37 @@ class DataPoint:
356
384
  tracers = [f'tracer_{i}' for i in range(ntracer)]
357
385
  return tracers, tags
358
386
 
387
+ @classmethod
388
+ def to_tables(cls, data, lookups=None):
389
+ data_by_type = {}
390
+ for i, d in enumerate(data):
391
+ d.tags['sacc_ordering'] = i
392
+ # Get the data type name
393
+ data_type = d.data_type
394
+ if data_type not in data_by_type:
395
+ data_by_type[data_type] = []
396
+ # Add the data point to the table for this type
397
+ data_by_type[data_type].append(d)
398
+
399
+ tables = {}
400
+ for data_type, data_points in data_by_type.items():
401
+ # Convert the data points to a table
402
+ table = cls.to_table(data_points, lookups)
403
+ # Add metadata to the table
404
+ table.meta['SACCTYPE'] = 'data'
405
+ table.meta['SACCNAME'] = data_type
406
+ tables[data_type] = table
407
+
408
+ # Now remove the temporary ordering tag
409
+ for d in data:
410
+ del d.tags['sacc_ordering']
411
+
412
+ return tables
413
+
359
414
  @classmethod
360
415
  def to_table(cls, data, lookups=None):
361
416
  """
362
- Convert a list of data points to a single homogenous table
417
+ Convert a list of data points to a single homogenous table.
363
418
 
364
419
  Since data points can have varying tags, this method uses
365
420
  null values to represent non-present tags.
@@ -424,11 +479,11 @@ class DataPoint:
424
479
  data = []
425
480
  for row in table:
426
481
  # Get basic data elements
427
- tracers = tuple([row[f'tracer_{i}'] for i in range(nt)])
428
- value = row['value']
482
+ tracers = tuple([numpy_to_vanilla(row[f'tracer_{i}']) for i in range(nt)])
483
+ value = numpy_to_vanilla(row['value'])
429
484
 
430
485
  # Deal with tags. First just pull out all remaining columns
431
- tags = {name: row[name] for name in tag_names}
486
+ tags = {numpy_to_vanilla(name): row[name] for name in tag_names}
432
487
  for k, v in list(tags.items()):
433
488
  # Deal with any tags that we should replace.
434
489
  # This is mainly used for Window instances.