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 +6 -0
- sacc/covariance.py +542 -0
- sacc/data_types.py +463 -0
- sacc/sacc.py +1458 -0
- sacc/tracers.py +1217 -0
- sacc/utils.py +168 -0
- sacc/windows.py +334 -0
- sacc-1.0.dist-info/METADATA +68 -0
- sacc-1.0.dist-info/RECORD +12 -0
- sacc-1.0.dist-info/WHEEL +5 -0
- sacc-1.0.dist-info/licenses/LICENSE +11 -0
- sacc-1.0.dist-info/top_level.txt +1 -0
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)
|