sacc 1.0.2__tar.gz → 2.0__tar.gz

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.
Files changed (71) hide show
  1. {sacc-1.0.2 → sacc-2.0}/.github/workflows/desc-ci.yml +14 -4
  2. {sacc-1.0.2/sacc.egg-info → sacc-2.0}/PKG-INFO +6 -4
  3. {sacc-1.0.2 → sacc-2.0}/README.md +2 -0
  4. sacc-2.0/codecov.yml +36 -0
  5. sacc-2.0/examples/demo.py +50 -0
  6. {sacc-1.0.2 → sacc-2.0}/pyproject.toml +3 -5
  7. {sacc-1.0.2 → sacc-2.0}/sacc/__init__.py +4 -1
  8. {sacc-1.0.2 → sacc-2.0}/sacc/covariance.py +202 -96
  9. {sacc-1.0.2 → sacc-2.0}/sacc/data_types.py +64 -9
  10. sacc-2.0/sacc/io.py +414 -0
  11. {sacc-1.0.2 → sacc-2.0}/sacc/sacc.py +372 -164
  12. sacc-2.0/sacc/tracer_uncertainty/__init__.py +1 -0
  13. sacc-2.0/sacc/tracer_uncertainty/base.py +34 -0
  14. sacc-2.0/sacc/tracer_uncertainty/nz.py +287 -0
  15. sacc-2.0/sacc/tracers/__init__.py +6 -0
  16. sacc-2.0/sacc/tracers/base.py +127 -0
  17. sacc-2.0/sacc/tracers/clusters.py +332 -0
  18. sacc-2.0/sacc/tracers/maps.py +206 -0
  19. sacc-2.0/sacc/tracers/misc.py +87 -0
  20. sacc-2.0/sacc/tracers/nz.py +285 -0
  21. sacc-2.0/sacc/tracers/survey.py +75 -0
  22. {sacc-1.0.2 → sacc-2.0}/sacc/utils.py +46 -2
  23. {sacc-1.0.2 → sacc-2.0}/sacc/windows.py +116 -102
  24. {sacc-1.0.2 → sacc-2.0/sacc.egg-info}/PKG-INFO +6 -4
  25. {sacc-1.0.2 → sacc-2.0}/sacc.egg-info/SOURCES.txt +16 -2
  26. {sacc-1.0.2 → sacc-2.0}/sacc.egg-info/requires.txt +3 -3
  27. sacc-2.0/test/legacy_files/old_qp_sacc.fits +0 -0
  28. {sacc-1.0.2 → sacc-2.0}/test/test_cluster_data_tracers.py +17 -15
  29. sacc-2.0/test/test_covariance.py +141 -0
  30. {sacc-1.0.2 → sacc-2.0}/test/test_sacc.py +431 -148
  31. sacc-1.0.2/sacc/tracers.py +0 -1217
  32. {sacc-1.0.2 → sacc-2.0}/.git_archival.txt +0 -0
  33. {sacc-1.0.2 → sacc-2.0}/.gitattributes +0 -0
  34. {sacc-1.0.2 → sacc-2.0}/.github/workflows/publish.yml +0 -0
  35. {sacc-1.0.2 → sacc-2.0}/.gitignore +0 -0
  36. {sacc-1.0.2 → sacc-2.0}/.readthedocs.yml +0 -0
  37. {sacc-1.0.2 → sacc-2.0}/LICENSE +0 -0
  38. {sacc-1.0.2 → sacc-2.0}/MANIFEST.in +0 -0
  39. {sacc-1.0.2 → sacc-2.0}/doc/Makefile +0 -0
  40. {sacc-1.0.2 → sacc-2.0}/doc/format.md +0 -0
  41. {sacc-1.0.2 → sacc-2.0}/doc/requirements.txt +0 -0
  42. {sacc-1.0.2 → sacc-2.0}/doc/source/Makefile +0 -0
  43. {sacc-1.0.2 → sacc-2.0}/doc/source/api.rst +0 -0
  44. {sacc-1.0.2 → sacc-2.0}/doc/source/conf.py +0 -0
  45. {sacc-1.0.2 → sacc-2.0}/doc/source/covariance.rst +0 -0
  46. {sacc-1.0.2 → sacc-2.0}/doc/source/data_types.rst +0 -0
  47. {sacc-1.0.2 → sacc-2.0}/doc/source/index.rst +0 -0
  48. {sacc-1.0.2 → sacc-2.0}/doc/source/intro.rst +0 -0
  49. {sacc-1.0.2 → sacc-2.0}/doc/source/sacc.rst +0 -0
  50. {sacc-1.0.2 → sacc-2.0}/doc/source/tracers.rst +0 -0
  51. {sacc-1.0.2 → sacc-2.0}/doc/source/utils.rst +0 -0
  52. {sacc-1.0.2 → sacc-2.0}/doc/source/windows.rst +0 -0
  53. {sacc-1.0.2 → sacc-2.0}/examples/.gitignore +0 -0
  54. {sacc-1.0.2 → sacc-2.0}/examples/CMB_LSS_read.ipynb +0 -0
  55. {sacc-1.0.2 → sacc-2.0}/examples/CMB_LSS_write.ipynb +0 -0
  56. {sacc-1.0.2 → sacc-2.0}/examples/Convert_DES_Sacc.ipynb +0 -0
  57. {sacc-1.0.2 → sacc-2.0}/examples/Convert_KIDS_Sacc.ipynb +0 -0
  58. {sacc-1.0.2 → sacc-2.0}/examples/Create_Sacc.ipynb +0 -0
  59. {sacc-1.0.2 → sacc-2.0}/examples/README.md +0 -0
  60. {sacc-1.0.2 → sacc-2.0}/examples/SACC_for_clusters.ipynb +0 -0
  61. {sacc-1.0.2 → sacc-2.0}/examples/SACC_read.ipynb +0 -0
  62. {sacc-1.0.2 → sacc-2.0}/examples/SACC_write.ipynb +0 -0
  63. {sacc-1.0.2 → sacc-2.0}/examples/demo_sacc_for_clusters_N+M.ipynb +0 -0
  64. {sacc-1.0.2 → sacc-2.0}/examples/example-txpipe-sacc1.sacc +0 -0
  65. {sacc-1.0.2 → sacc-2.0}/requirements.txt +0 -0
  66. {sacc-1.0.2 → sacc-2.0}/sacc.egg-info/dependency_links.txt +0 -0
  67. {sacc-1.0.2 → sacc-2.0}/sacc.egg-info/top_level.txt +0 -0
  68. {sacc-1.0.2 → sacc-2.0}/setup.cfg +0 -0
  69. {sacc-1.0.2 → sacc-2.0}/test/__init__.py +0 -0
  70. {sacc-1.0.2 → sacc-2.0}/test/data/.gitignore +0 -0
  71. {sacc-1.0.2 → sacc-2.0}/test/make_test_data.py +0 -0
@@ -12,11 +12,11 @@ jobs:
12
12
  runs-on: ubuntu-latest
13
13
  strategy:
14
14
  matrix:
15
- python-version: [3.8, 3.9, "3.10", "3.11"]
15
+ python-version: ["3.10", "3.11", "3.12", "3.13"]
16
16
 
17
17
  steps:
18
18
  - name: Checkout repository
19
- uses: actions/checkout@v3
19
+ uses: actions/checkout@v4
20
20
 
21
21
  - name: Setup python
22
22
  uses: actions/setup-python@v4
@@ -26,15 +26,25 @@ jobs:
26
26
  - name: Dependencies
27
27
  run: |
28
28
  python -m pip install --upgrade numpy pytest pytest-cov coverage pip
29
+ # Temporary until fixes applied in another PR
29
30
  python -m pip install '.[all]'
30
31
 
31
32
  - name: Run tests and measure coverage
32
33
  run: |
33
- pytest --cov sacc --cov-report xml:coverage.xml ./test
34
+ pytest --cov sacc --cov-report xml:coverage.xml --cov-branch ./test
34
35
 
35
- - name: Report coverage
36
+ - name: Report coverage in GitHub PR
36
37
  uses: orgoro/coverage@v3.2
37
38
  if: matrix.python-version == '3.11'
38
39
  with:
39
40
  coverageFile: ./coverage.xml
40
41
  token: ${{ secrets.GITHUB_TOKEN }}
42
+ - name: Upload coverage reports to Codecov
43
+ if: matrix.python-version == '3.11'
44
+ uses: codecov/codecov-action@v5
45
+ with:
46
+ fail_ci_if_error: true
47
+ verbose: true
48
+ use_oidc: false
49
+ env:
50
+ CODECOV_TOKEN: ${{ secrets.CODECOV_TOKEN }}
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: sacc
3
- Version: 1.0.2
3
+ Version: 2.0
4
4
  Summary: SACC - the LSST/DESC summary statistic data format library
5
5
  Author-email: LSST DESC <joezuntz@googlemail.com>
6
6
  License: BSD-3-Clause
@@ -10,17 +10,19 @@ Classifier: Operating System :: OS Independent
10
10
  Description-Content-Type: text/markdown
11
11
  License-File: LICENSE
12
12
  Requires-Dist: scipy
13
- Requires-Dist: numpy>=1.20
13
+ Requires-Dist: numpy>=2
14
14
  Requires-Dist: astropy
15
15
  Provides-Extra: all
16
- Requires-Dist: qp-prob[all]; extra == "all"
16
+ Requires-Dist: qp-prob[all]>=1; extra == "all"
17
17
  Requires-Dist: numpydoc; extra == "all"
18
18
  Provides-Extra: doc
19
19
  Requires-Dist: numpydoc; extra == "doc"
20
20
  Provides-Extra: qp
21
- Requires-Dist: qp-prob[all]; extra == "qp"
21
+ Requires-Dist: qp-prob[all]>=1; extra == "qp"
22
22
  Dynamic: license-file
23
23
 
24
+ [![codecov](https://codecov.io/github/LSSTDESC/sacc/graph/badge.svg?token=YZf4IXbzA0)](https://codecov.io/github/LSSTDESC/sacc)
25
+
24
26
  Sacc
25
27
  ====
26
28
 
@@ -1,3 +1,5 @@
1
+ [![codecov](https://codecov.io/github/LSSTDESC/sacc/graph/badge.svg?token=YZf4IXbzA0)](https://codecov.io/github/LSSTDESC/sacc)
2
+
1
3
  Sacc
2
4
  ====
3
5
 
sacc-2.0/codecov.yml ADDED
@@ -0,0 +1,36 @@
1
+ # See https://docs.codecov.com/docs/codecovyml-reference for config reference
2
+ # To validate: `curl -X POST --data-binary @codecov.yml https://codecov.io/validate`
3
+
4
+ comment:
5
+ layout: "diff, flags, files"
6
+ behavior: default
7
+ require_changes: false
8
+ require_base: false
9
+ require_head: true
10
+ hide_project_coverage: false
11
+
12
+ coverage:
13
+ precision: 1
14
+ round: down
15
+ # red below 70, yellow between 70 and 95, green above 95.
16
+ range: 70..95
17
+
18
+ status:
19
+ patch:
20
+ default:
21
+ target: 100%
22
+ threshold: 0%
23
+ if_ci_failed: error
24
+ only_pulls: false
25
+
26
+ project:
27
+ default:
28
+ target: auto
29
+ threshold: 0%
30
+ if_ci_failed: error
31
+ only_pulls: false
32
+ removed_code_behavior: fully_covered_patch
33
+
34
+ github_checks:
35
+ annotations: true
36
+
@@ -0,0 +1,50 @@
1
+
2
+ import numpy as np
3
+ from sacc import Sacc, BandpowerWindow
4
+
5
+ # Create Sacc object
6
+ s = Sacc()
7
+
8
+ # Add metadata
9
+ s.metadata['creator'] = 'Demo'
10
+ s.metadata['purpose'] = 'Full feature test'
11
+
12
+ # Add tracers
13
+ z = np.linspace(0, 1, 101)
14
+ nz = np.exp(-((z-0.5)/0.1)**2)
15
+ s.add_tracer('NZ', 'source_0', z, nz)
16
+ s.add_tracer('NZ', 'source_1', z, nz)
17
+
18
+ nu = np.linspace(30, 60, 5)
19
+ bandpass = np.ones(5)
20
+
21
+ ell = np.linspace(10, 1000, 100)
22
+ beam = np.exp(-0.01 * ell)
23
+ s.add_tracer('NuMap', 'cmb', 2, nu, bandpass, ell, beam)
24
+
25
+
26
+
27
+
28
+
29
+
30
+
31
+
32
+
33
+ # Add window (correct shape)
34
+ ells_large = np.arange(100)
35
+ window_matrix = np.eye(100) # shape (100, 100)
36
+ win1 = BandpowerWindow(ells_large, window_matrix)
37
+
38
+ # Add data points, referencing the window object directly
39
+ for i in range(100):
40
+ s.add_data_point('cl_ee', ('source_0', 'source_1'), 0.1*i, ell=ell[i], window=win1)
41
+ s.add_data_point('cl_00', ('source_0', 'cmb'), 0.2*i, ell=ell[i], window=win1)
42
+
43
+ # Add covariance
44
+ n = len(s.data)
45
+ cov = np.eye(n)
46
+ s.add_covariance(cov)
47
+
48
+ # Write to files
49
+ s.save_fits('demo.fits', overwrite=True)
50
+ s.save_hdf5('demo.hdf5', overwrite=True)
@@ -17,7 +17,7 @@ classifiers = [
17
17
 
18
18
  dependencies = [
19
19
  "scipy",
20
- "numpy>=1.20",
20
+ "numpy>=2",
21
21
  "astropy"
22
22
  ]
23
23
 
@@ -37,9 +37,9 @@ Homepage = "https://github.com/LSSTDESC/sacc"
37
37
 
38
38
 
39
39
  [project.optional-dependencies]
40
- all = ["qp-prob[all]", "numpydoc"]
40
+ all = ["qp-prob[all]>=1", "numpydoc"]
41
41
  doc = ["numpydoc"]
42
- qp = ["qp-prob[all]"]
42
+ qp = ["qp-prob[all]>=1"]
43
43
 
44
44
  [tool.setuptools.packages.find]
45
45
  where = ["."]
@@ -48,5 +48,3 @@ include = ["sacc"]
48
48
  [tool.setuptools.dynamic]
49
49
  version = {attr = "sacc.__version__"}
50
50
 
51
- [tool.coverage.run]
52
- relative_paths = true
@@ -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
@@ -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
  """