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/__init__.py ADDED
@@ -0,0 +1,6 @@
1
+ from .sacc import Sacc, DataPoint, concatenate_data_sets # noqa
2
+ from .windows import Window, BandpowerWindow, TopHatWindow, LogTopHatWindow # noqa
3
+ from .data_types import standard_types, parse_data_type_name, build_data_type_name # noqa
4
+ from .tracers import BaseTracer # noqa
5
+ from .covariance import BaseCovariance # noqa
6
+ __version__ = '1.0' #noqa
sacc/covariance.py ADDED
@@ -0,0 +1,542 @@
1
+ from astropy.io import fits
2
+ from astropy.table import Table
3
+ import scipy.linalg
4
+ import numpy as np
5
+
6
+ from .utils import invert_spd_matrix
7
+
8
+
9
+ class BaseCovariance:
10
+ """
11
+ The abstract base class for covariances in different forms.
12
+ These are not currently designed to be modified after creation.
13
+
14
+ The three concrete subclasses that are created are:
15
+
16
+ FullCovariance - for dense matrices
17
+
18
+ BlockDiagonalCovariance - for block diagonal matrices
19
+ (those in which some sub-blocks are dense but without correlation
20
+ between the blocks
21
+
22
+ DiagonalCovariance - a covariance where the elements are uncorrelated
23
+
24
+ Attributes
25
+ ----------
26
+ cov_type: string
27
+ The type of the covariance (class variable)
28
+ """
29
+ _covariance_classes = {}
30
+
31
+ def __init__(self):
32
+ """Abstract superclass constructor.
33
+
34
+ All the subclasses need _dense and _dense_inverse forms.
35
+
36
+ """
37
+ self._dense = None
38
+ self._dense_inverse = None
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
47
+
48
+ @classmethod
49
+ def from_hdu(cls, hdu):
50
+ """
51
+ Make a covariance object from an astropy FITS HDU object.
52
+
53
+ The type of the covariance is determined from a keyword
54
+ in the HDU, and then the corresponding subclass from_hdu
55
+ method is called.
56
+
57
+ Parameters
58
+ ----------
59
+ hdu: astropy.fits.ImageHDU instance
60
+ An HDU object with covariance info in it
61
+
62
+ Returns
63
+ -------
64
+ instance: BaseCovariance
65
+ A covariance instance
66
+ """
67
+ subclass_name = hdu.header['saccclss']
68
+ subclass = cls._covariance_classes[subclass_name]
69
+ return subclass.from_hdu(hdu)
70
+
71
+ @classmethod
72
+ def make(cls, cov):
73
+ """Make an appropriate covariance object from the matrix info itself.
74
+
75
+ You can pass in a list of covariance blocks for a block-diagonal,
76
+ covariance a 1D array for a diagonal covariance, or a full matrix.
77
+
78
+ A different subclass is returned for each of these cases.
79
+
80
+ Parameters
81
+ ----------
82
+ 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).
86
+
87
+ n: int
88
+ length of the data vector to which this covariance applies
89
+ """
90
+ if isinstance(cov, list):
91
+ s = 0
92
+ for block in cov:
93
+ block = np.atleast_2d(block)
94
+ if (block.ndim != 2) or (block.shape[0] != block.shape[1]):
95
+ raise ValueError("Covariance block has wrong size "
96
+ f"or shape {block.shape}")
97
+ s += block.shape[0]
98
+ 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)
109
+
110
+ @property
111
+ def dense(self):
112
+ """
113
+ A dense matrix form of the covariance
114
+
115
+ Parameters
116
+ ----------
117
+ None
118
+
119
+ Returns
120
+ -------
121
+ covmat: 2D array
122
+ Numpy array of dense form of matrix
123
+ """
124
+ if self._dense is None:
125
+ self._dense = self._get_dense()
126
+
127
+ return self._dense
128
+
129
+ @property
130
+ def inverse(self):
131
+ """A dense matrix form of the inverse of the covariance matrix
132
+
133
+ Returns
134
+ -------
135
+ invC: array
136
+ Inverse covariance
137
+ """
138
+ if self._dense_inverse is None:
139
+ self._dense_inverse = self._get_dense_inverse()
140
+ return self._dense_inverse
141
+
142
+
143
+ class FullCovariance(BaseCovariance, cov_type='full'):
144
+ """
145
+ A covariance subclass representing a full matrix with correlations
146
+ anywhere. Represented as an n x n matrix.
147
+
148
+ Attributes
149
+ ----------
150
+ size: int
151
+ the length of the corresponding data vector
152
+
153
+ covmat: 2D array
154
+ The matrix itself, of shape (size x size)
155
+ """
156
+ def __init__(self, covmat):
157
+ self.covmat = np.atleast_2d(covmat)
158
+ self.size = self.covmat.shape[0]
159
+ super().__init__()
160
+
161
+ def to_hdu(self):
162
+ """
163
+ Make an astropy FITS HDU object with this covariance in it.
164
+ This is represented as an image.
165
+
166
+ Parameters
167
+ ----------
168
+ None
169
+
170
+ Returns
171
+ -------
172
+ hdu: astropy.fits.ImageHDU instance
173
+ HDU that can be used to reconstruct the object.
174
+ """
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
181
+
182
+ @classmethod
183
+ def from_hdu(cls, hdu):
184
+ """
185
+ Load a covariance object from the data in the HDU
186
+
187
+ Parameters
188
+ ----------
189
+ hdu: astropy.fits.ImageHDU instance
190
+
191
+ Returns
192
+ -------
193
+ cov: FullCovariance
194
+ Loaded covariance object
195
+ """
196
+ C = hdu.data
197
+ return cls(C)
198
+
199
+ def keeping_indices(self, indices):
200
+ """
201
+ Return a new instance with only the specified indices retained.
202
+
203
+ Parameters
204
+ ----------
205
+ indices: array or list
206
+ Either an array or list of integer indices, or a boolean
207
+ array of the same size (1D) as the matrix.
208
+ Specifies rows/cols to keep in the new matrix.
209
+
210
+ Returns
211
+ -------
212
+ cov: FullCovariance
213
+ A covariance with only the corresponding data points remaining
214
+ """
215
+ C = self.covmat[indices][:, indices]
216
+ return self.__class__(C)
217
+
218
+ def get_block(self, indices):
219
+ """Read a (not necessarily contiguous) sublock of the matrix
220
+
221
+ Parameters
222
+ ----------
223
+ indices: array
224
+ An array of integer indices
225
+
226
+ Returns
227
+ -------
228
+ block: array
229
+ a 2D array of the relevant sub-block of the matrix
230
+ """
231
+ return self.covmat[indices][:, indices]
232
+
233
+ def _get_dense_inverse(self):
234
+ return invert_spd_matrix(self.covmat)
235
+
236
+ def _get_dense(self):
237
+ # Internal method to get a dense form of the matrix.
238
+ # Use the property Covariance.dense instead of calling this
239
+ # directly.
240
+ return self.covmat.copy()
241
+
242
+
243
+ class BlockDiagonalCovariance(BaseCovariance, cov_type='block'):
244
+ """A covariance subclass representing block diagonal covariances
245
+
246
+ Block diagonal covariances have sub-blocks that are full dense matrices,
247
+ but without correlations between the blocks. This feature can be taken
248
+ advantage of when doing matrix operations like multiplication or inversion.
249
+
250
+ Parameters
251
+ ----------
252
+ blocks: list[arrays]
253
+ list of sub-blocks of the matrix
254
+
255
+ block_sizes: list[int]
256
+ list of sizes n of each the n x n sub-blocks
257
+
258
+ size: int
259
+ overall total size of the matrix
260
+ """
261
+ def __init__(self, blocks):
262
+ """Create a BlockDiagonalCovariance object from a list of blocks
263
+
264
+ Parameters
265
+ ----------
266
+ blocks: sequence of arrays
267
+ List or other sequence of the sub-matrices
268
+ """
269
+ self.blocks = [np.atleast_2d(B) for B in blocks]
270
+ self.block_sizes = [len(B) for B in self.blocks]
271
+ self.size = sum(self.block_sizes)
272
+ super().__init__()
273
+
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
298
+
299
+ @classmethod
300
+ def from_hdu(cls, hdu):
301
+ """Read a covariance object from a loaded FITS HDU.
302
+
303
+ Parameters
304
+ ----------
305
+ hdu: FITS HDU object as read in by astropy.
306
+
307
+ Returns
308
+ -------
309
+ cov: BlockDiagonalCovariance
310
+ Loaded covariance object
311
+ """
312
+ n = hdu.header['blocks']
313
+ block_sizes = [hdu.header[f'size_{i}'] for i in range(n)]
314
+ s = 0
315
+
316
+ blocks = []
317
+ for b in block_sizes:
318
+ B = hdu.data[s:s + b**2].reshape((b, b))
319
+ s += b**2
320
+ blocks.append(B)
321
+ return cls(blocks)
322
+
323
+ def get_block(self, indices):
324
+ """Read a (not necessarily contiguous) sublock of the matrix
325
+
326
+ Parameters
327
+ ----------
328
+ indices: array
329
+ An array of integer indices, which must be in
330
+ ascending order
331
+
332
+ Returns
333
+ -------
334
+ cov: array
335
+ A full (dense) 2x2 array of the submatrix.
336
+ """
337
+ indices = np.array(indices)
338
+
339
+ if np.any(np.diff(indices)) < 0:
340
+ raise ValueError("Indices passed to "
341
+ "BlockDiagonalCovariance.get_block "
342
+ "must be in ascending order")
343
+ s = 0
344
+ sub_blocks = []
345
+ for block, sz in zip(self.blocks, self.block_sizes):
346
+ e = s + sz
347
+ m = indices[(indices >= s) & (indices < e)] - s
348
+ sub_blocks.append(block[m][:, m])
349
+ s += sz
350
+ return scipy.linalg.block_diag(*sub_blocks)
351
+
352
+ def keeping_indices(self, indices):
353
+ """
354
+ Return a new instance with only the specified elements retained.
355
+
356
+ This method will try to return another BlockDiagonalCovariance if
357
+ it can, but otherwise will revert to a full one: if the mask passed
358
+ in is of a boolean type or if it is integers in it can remain
359
+ block diagonal
360
+
361
+ Parameters
362
+ ----------
363
+ indices: array or list
364
+ Either an array or list of integer indices, or a boolean
365
+ array of the same size (1D) as the matrix.
366
+ Specifies rows/cols to keep in the new matrix.
367
+
368
+ Returns
369
+ -------
370
+ cov: FullCovariance or BlockDiagonalCovariance
371
+ A covariance with only the corresponding data points remaining
372
+ """
373
+ indices = np.array(indices)
374
+
375
+ if indices.dtype == bool:
376
+ breaks = np.cumsum(self.block_sizes)[:-1]
377
+ block_masks = np.split(indices, breaks)
378
+ blocks = [self.blocks[i][m][:, m] for i, m in
379
+ enumerate(block_masks)]
380
+ return self.__class__(blocks)
381
+ elif (np.diff(indices) > 0).all():
382
+ s = 0
383
+ sub_blocks = []
384
+ for block, sz in zip(self.blocks, self.block_sizes):
385
+ e = s + sz
386
+ m = indices[(indices >= s) & (indices < e)] - s
387
+ sub_blocks.append(block[m][:, m])
388
+ s += sz
389
+ return self.__class__(sub_blocks)
390
+ else:
391
+ C = scipy.linalg.block_diag(*self.blocks)
392
+ C = C[indices][:, indices]
393
+ return FullCovariance(C)
394
+
395
+ def _get_dense_inverse(self):
396
+ # Invert all the blocks individually and then
397
+ # connect them all together
398
+ return scipy.linalg.block_diag(*[invert_spd_matrix(B)
399
+ for B in self.blocks])
400
+
401
+ def _get_dense(self):
402
+ # Internal method to get a dense form of the matrix.
403
+ # Use the property Covariance.dense instead of calling this
404
+ # directly.
405
+ return scipy.linalg.block_diag(*self.blocks)
406
+
407
+
408
+ class DiagonalCovariance(BaseCovariance, cov_type='diagonal'):
409
+ """A covariance subclass representing covariances that are
410
+ purely diagonal.
411
+
412
+ Parameters
413
+ ----------
414
+ size: int
415
+ The size of the matrix
416
+
417
+ diag: array
418
+ The diagonal terms in the covariance (i.e. the variances)
419
+ """
420
+ def __init__(self, variances):
421
+ """
422
+ Create a DiagonalCovariance object from the variances
423
+ of the data points.
424
+
425
+ Parameters
426
+ ----------
427
+ variances: array
428
+ 2D array of variances of the data points.
429
+ """
430
+ self.diag = np.atleast_1d(variances)
431
+ self.size = len(self.diag)
432
+ super().__init__()
433
+
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.
438
+
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
+
455
+ def keeping_indices(self, indices):
456
+ """
457
+ Return a new DiagonalCovariance with only the specified indices
458
+ retained.
459
+
460
+ Parameters
461
+ ----------
462
+ indices: array or list
463
+ Either an array or list of integer indices, or a boolean
464
+ array of the same size (1D) as the matrix.
465
+ Specifies rows/cols to keep in the new matrix.
466
+
467
+ Returns
468
+ -------
469
+ cov: DiagonalCovariance
470
+ A covariance with only the corresponding data points remaining
471
+ """
472
+ D = self.diag[indices]
473
+ return self.__class__(D)
474
+
475
+ @classmethod
476
+ def from_hdu(cls, hdu):
477
+ """
478
+ Load a covariance object from the data in the HDU
479
+
480
+ Parameters
481
+ ----------
482
+ hdu: astropy.fits.BinTableHDU instance
483
+
484
+ Returns
485
+ -------
486
+ cov: DiagonalCovariance
487
+ Loaded covariance object
488
+ """
489
+ D = hdu.data['variance']
490
+ return cls(D)
491
+
492
+ def get_block(self, indices):
493
+ """Read a (not necessarily contiguous) sublock of the matrix
494
+
495
+ Parameters
496
+ ----------
497
+ indices: array
498
+ An array of integer indices, which should be in
499
+ ascending order (for consistency with the
500
+ block diagonal interface)
501
+
502
+ Returns
503
+ -------
504
+ cov: array
505
+ A full (dense) 2x2 array of the submatrix.
506
+ """
507
+ return np.diag(self.diag[indices])
508
+
509
+ def _get_dense_inverse(self):
510
+ # Trivial inverse
511
+ return np.diag(1.0/self.diag)
512
+
513
+ def _get_dense(self):
514
+ # Internal method to get a dense form of the matrix.
515
+ # Use the property Covariance.dense instead of calling this
516
+ # directly.
517
+ return np.diag(self.diag)
518
+
519
+
520
+ def concatenate_covariances(*covariances):
521
+ # If all the covariances are diagonal then the concatenated
522
+ # version can be diagonal
523
+ if all(isinstance(cov, DiagonalCovariance) for cov in covariances):
524
+ variances = np.concatenate([cov.diag for cov in covariances])
525
+ return DiagonalCovariance(variances)
526
+
527
+ # Otherwise we have to get things in a common form, and
528
+ # make a block-diagonal covariance.
529
+ blocks = []
530
+
531
+ # For each of the pieces we extract any blocks
532
+ # that will go into the concatenation
533
+ for cov in covariances:
534
+ # For an existing block-diagonal covariance
535
+ # we retain the block structure
536
+ if isinstance(cov, BlockDiagonalCovariance):
537
+ blocks += cov.blocks
538
+ # For everything else we just use a dense matrix
539
+ else:
540
+ blocks.append(cov.dense)
541
+
542
+ return BlockDiagonalCovariance(blocks)