Skip to content

Commit 591ee1d

Browse files
authored
Merge pull request #364 from McKelvey-Engineering-CSE/new_response_format
Improved Full Detector Response storage and conversion
2 parents 7abbeea + 52fdccb commit 591ee1d

56 files changed

Lines changed: 1714 additions & 1932 deletions

File tree

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

CHANGELOG.md

Lines changed: 49 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,12 +1,59 @@
11
# Changelog
22

3-
## Version 0.3.0
3+
## Version 0.4.0
44

55
Under development.
66

77
Developers: please keep track of notable changes here.
88

9-
-
9+
### New FullDetectorResponse
10+
11+
HDF5 response file format has changed substantially. The new format,
12+
which is *not* compatible with the .h5 response format from prior
13+
releases, features
14+
- internal compression using BitShuffle algorithm, which gives chunk
15+
access times within 50% of uncompressed for smaller responses and
16+
much faster for larger ones while offering same or better compression
17+
on disk.
18+
- much faster conversion from .rsp.gz to .h5, with greatly reduced
19+
memory usage
20+
- ability to convert from .rsp.gz to .h5 and back to
21+
.rsp.gz without losing any of the original header information
22+
(which facilitates creating lower-resolution versions of large
23+
responses)
24+
- separation of raw counts from effective area, which allows
25+
FullDetectorResponse to export the latter as needed and
26+
enables future performance improvements for code that
27+
needs to compute and average PSRs. The dtype of the effective
28+
area, and hence the values returned when computing PSRs etc.,
29+
can now be set at load time; hence, the same response can be
30+
used in float32 or float64 with no additional overhead.
31+
- automatic "good chunks" chunking of .h5 responses at creation time
32+
- HDF5 tweaks to avoid writing timestamps, so that (with support
33+
from a forthcoming release of histpy) the MD5 signature of an
34+
.h5 response does not change every time it is written.
35+
36+
.rsp.gz conversion has been broken out from FullDetectorResponse into
37+
its own class, RspConverter, which simplifies the FullDetectorResponse
38+
code and in particular its open() interface.
39+
40+
The new response code deprecates and removes the following
41+
response-related functionality:
42+
- sparse response format
43+
- reading spectrum for effective area computation from a file
44+
(was broken/bit-rotted in DC3 release)
45+
- miniDC2 format support (unused since DC2)
46+
47+
### New wasabi location for development files
48+
49+
To support file format changes in the develop branch that are
50+
incompatible with DC3 and cosipy versions <= 0.3, we have established
51+
a "COSI-SMEX/develop" tree in the public wasabi bucket. Currently,
52+
this tree holds the new-format .h5 detector responses and the
53+
corresponding .rsp.gz files. The DC3 tutorials have been updated to
54+
use this new tree for files whose format has changed since DC3.
55+
56+
1057

1158
## Version 0.2.x
1259

cosipy/image_deconvolution/dataIF_COSI_DC2.py

Lines changed: 20 additions & 54 deletions
Original file line numberDiff line numberDiff line change
@@ -36,13 +36,10 @@ def __init__(self, name = None):
3636
self._image_response = None # histpy.Histogram (dense)
3737

3838
# None if using Galactic CDS, mandotary if using local CDS
39-
self._coordsys_conv_matrix = None
40-
41-
# optional
42-
self.is_miniDC2_format = False #should be removed in the future
39+
self._coordsys_conv_matrix = None
4340

4441
@classmethod
45-
def load(cls, name, event_binned_data, dict_bkg_binned_data, rsp, coordsys_conv_matrix = None, is_miniDC2_format = False):
42+
def load(cls, name, event_binned_data, dict_bkg_binned_data, rsp, coordsys_conv_matrix = None):
4643
"""
4744
Load data
4845
@@ -57,9 +54,7 @@ def load(cls, name, event_binned_data, dict_bkg_binned_data, rsp, coordsys_conv_
5754
rsp : :py:class:`histpy.Histogram` or :py:class:`cosipy.response.FullDetectorResponse`
5855
Response
5956
coordsys_conv_matrix : :py:class:`cosipy.image_deconvolution.CoordsysConversionMatrix`, default False
60-
Coordsys conversion matrix
61-
is_miniDC2_format : bool, default False
62-
Whether the file format is for mini-DC2. It will be removed in the future.
57+
Coordsys conversion matrix
6358
6459
Returns
6560
-------
@@ -87,24 +82,22 @@ def load(cls, name, event_binned_data, dict_bkg_binned_data, rsp, coordsys_conv_
8782
if new._coordsys_conv_matrix is not None and \
8883
new._coordsys_conv_matrix.is_sparse:
8984
new._coordsys_conv_matrix.contents.enable_caching()
90-
91-
new.is_miniDC2_format = is_miniDC2_format
9285

9386
if isinstance(rsp, FullDetectorResponse):
9487
logger.info('Loading the response matrix onto your computer memory...')
95-
new._load_full_detector_response_on_memory(rsp, is_miniDC2_format)
88+
new._image_response = rsp.to_dr()
9689
logger.info('Finished')
9790
elif isinstance(rsp, Histogram):
9891
new._image_response = rsp
99-
92+
10093
# We modify the axes in event, bkg_models, response. This is only for DC2.
10194
new._modify_axes()
102-
95+
10396
new._data_axes = new._event.axes
104-
97+
10598
if new._coordsys_conv_matrix is None:
10699
axes = (new._image_response.axes['NuLambda'].copy(), new._image_response.axes['Ei']) # will mutate axes[0]
107-
axes[0].label = 'lb'
100+
axes[0].label = 'lb'
108101
# The gamma-ray direction of pre-computed response in DC2 is in the galactic coordinate, not in the local coordinate.
109102
# Actually, it is labeled as 'NuLambda'. So I replace it with 'lb'.
110103
new._model_axes = Axes(axes, copy_axes = False)
@@ -134,39 +127,35 @@ def _modify_axes(self):
134127
for name in axis_name:
135128

136129
logger.info(f"... checking the axis {name} of the event and background files...")
137-
130+
138131
event_edges, event_unit = self._event.axes[name].edges, self._event.axes[name].unit
139132

140133
for key in self._bkg_models:
141134

142135
bkg_edges, bkg_unit = self._bkg_models[key].axes[name].edges, self._bkg_models[key].axes[name].unit
143136

144137
if np.all(event_edges == bkg_edges):
145-
logger.info(f" --> pass (edges)")
138+
logger.info(f" --> pass (edges)")
146139
else:
147140
logger.error(f"Warning: the edges of the axis {name} are not consistent between the event and the background model {key}!")
148141
logger.error(f" event : {event_edges}")
149142
logger.error(f" background : {bkg_edges}")
150143
raise ValueError
151144

152-
# check the axes of the event/response files.
145+
# check the axes of the event/response files.
153146
# Note that currently (2023-08-29) no unit is stored in the binned data. So only the edges are compared. This should be modified in the future.
154147

155148
axis_name = ['Em', 'Phi', 'PsiChi']
156-
149+
157150
for name in axis_name:
158151

159152
logger.info(f"...checking the axis {name} of the event and response files...")
160153

161154
event_edges, event_unit = self._event.axes[name].edges, self._event.axes[name].unit
162155
response_edges, response_unit = self._image_response.axes[name].edges, self._image_response.axes[name].unit
163-
164-
# if type(response_edges) == u.quantity.Quantity and self.is_miniDC2_format == True:
165-
if event_unit is None and response_unit is not None and self.is_miniDC2_format == True: # this is only for the old data in the miniDC2 challenge. I will remove them in the near future (or in the final dataIF).
166-
response_edges = response_edges.value
167156

168157
if np.all(event_edges == response_edges):
169-
logger.info(f" --> pass (edges)")
158+
logger.info(f" --> pass (edges)")
170159
else:
171160
logger.error(f"Warning: the edges of the axis {name} are not consistent between the event and background!")
172161
logger.error(f" event : {event_edges}")
@@ -184,12 +173,12 @@ def _modify_axes(self):
184173
self._image_response.axes["Phi"], \
185174
self._image_response.axes["PsiChi"]),
186175
copy_axes=False)
187-
176+
188177
self._event = Histogram(axes_cds,
189178
contents = self._event.contents,
190179
unit = self._event.unit,
191180
copy_contents = False) # overwrite axes of existing Histogram
192-
181+
193182
for key in self._bkg_models:
194183
bkg_model = self._bkg_models[key]
195184
self._bkg_models[key] = Histogram(axes_cds,
@@ -201,36 +190,13 @@ def _modify_axes(self):
201190

202191
return True
203192

204-
def _load_full_detector_response_on_memory(self, full_detector_response, is_miniDC2_format):
205-
"""
206-
Load a response file on the computer memory.
207-
"""
208-
209-
axes_image_response = Axes((full_detector_response.axes["NuLambda"],
210-
full_detector_response.axes["Ei"],
211-
full_detector_response.axes["Em"],
212-
full_detector_response.axes["Phi"],
213-
full_detector_response.axes["PsiChi"]),
214-
copy_axes=False)
215-
216-
if is_miniDC2_format:
217-
npix = axes_image_response["NuLambda"].npix
218-
slices = [ np.sum(full_detector_response[ipix].to_dense(), axis = (4,5)) for ipix in tqdm(range(npix)) ] #Ei, Em, Phi, ChiPsi
219-
contents = np.stack(slices)
220-
else:
221-
contents = full_detector_response._file['DRM']['CONTENTS']
222-
223-
self._image_response = Histogram(axes_image_response, contents=contents,
224-
unit = full_detector_response.unit,
225-
copy_contents = False)
226-
227193
def _calc_exposure_map(self):
228194
"""
229195
Calculate exposure_map, which is an intermediate matrix used in RL algorithm.
230196
"""
231197

232198
logger.info("Calculating an exposure map...")
233-
199+
234200
if self._coordsys_conv_matrix is None:
235201
exposure_map = np.sum(self._image_response.contents, axis = (2,3,4))
236202
else:
@@ -275,7 +241,7 @@ def calc_expectation(self, model, dict_bkg_norm = None, almost_zero = 1e-12):
275241
# This is just because in DC2 the rotate response for galactic coordinate CDS does not have an axis for time/scatt binning.
276242
# However it is likely that it will have such an axis in the future in order to consider background variability depending on time and pointing direction etc.
277243
# Then, the implementation here will not work. Thus, keep in mind that we need to modify it once the response format is fixed.
278-
244+
279245
if self._coordsys_conv_matrix is None:
280246
expectation = np.tensordot( model.contents, self._image_response.contents, axes = ((0,1),(0,1)))
281247
# ['lb', 'Ei'] x [NuLambda(lb), Ei, Em, Phi, PsiChi] -> [Em, Phi, PsiChi]
@@ -293,12 +259,12 @@ def calc_expectation(self, model, dict_bkg_norm = None, almost_zero = 1e-12):
293259
expectation *= model.axes['lb'].pixarea()
294260
expectation += almost_zero
295261

296-
if dict_bkg_norm is not None:
262+
if dict_bkg_norm is not None:
297263
for key in self.keys_bkg_models():
298264
expectation += self.bkg_model(key).contents * dict_bkg_norm[key]
299265

300266
return Histogram(self.data_axes, contents = expectation, copy_contents = False)
301-
267+
302268
def calc_T_product(self, dataspace_histogram):
303269
"""
304270
Calculate the product of the input histogram with the transonse matrix of the response function.
@@ -375,4 +341,4 @@ def calc_log_likelihood(self, expectation):
375341
"""
376342
log_likelihood = np.sum( self.event * np.log(expectation) ) - np.sum(expectation)
377343

378-
return log_likelihood
344+
return log_likelihood

cosipy/response/DetectorResponse.py

Lines changed: 14 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -69,7 +69,7 @@ def get_spectral_response(self, copy = True):
6969
----------
7070
copy : bool
7171
If true, a copy of the cached spectral response will be returned.
72-
72+
7373
Returns
7474
-------
7575
:py:class:`DetectorResponse`
@@ -80,13 +80,14 @@ def get_spectral_response(self, copy = True):
8080
spec = self.project(['Ei','Em'])
8181
self._spec = DetectorResponse(spec.axes,
8282
contents = spec.contents,
83-
unit = spec.unit)
83+
unit = spec.unit,
84+
copy_contents = False)
8485

8586
if copy:
8687
return self._spec.copy()
8788
else:
8889
return self._spec
89-
90+
9091
def get_effective_area(self, energy = None, copy = True):
9192
"""
9293
Compute the effective area at a given energy. If no energy is specified, the
@@ -98,14 +99,14 @@ def get_effective_area(self, energy = None, copy = True):
9899
Energy/energies at which to interpolate the linearly effective area
99100
copy : bool
100101
If true, a copy of the cached effective will be returned.
101-
102+
102103
Returns
103104
-------
104105
:py:class:`astropy.units.Quantity` or :py:class:`histpy.Histogram`
105106
"""
106-
107+
107108
if self._aeff is None:
108-
self._aeff = self.get_spectral_response(copy = False).project('Ei').to_dense()
109+
self._aeff = self.get_spectral_response(copy = False).project('Ei')
109110

110111
if energy is None:
111112
if copy:
@@ -125,7 +126,7 @@ def get_dispersion_matrix(self):
125126
-------
126127
:py:class:`histpy.Histogram`
127128
"""
128-
129+
129130
# Get spectral response and effective area normalization
130131
spec = self.get_spectral_response(copy = False)
131132
norm = self.get_effective_area().full_contents
@@ -139,10 +140,10 @@ def get_dispersion_matrix(self):
139140
norm[norm == 0] = 1*norm.unit
140141

141142
logger.warn("Null effective area, cannot properly compute dispersion matrix.")
142-
143+
143144
# "Broadcast" such that it has the compatible dimensions with the 2D matrix
144145
norm = spec.expand_dims(norm, 'Ei')
145-
146+
146147
# Normalize column-by-column
147148
return (spec / norm)
148149

@@ -155,22 +156,18 @@ def photon_energy_axis(self):
155156
-------
156157
:py:class:`histpy.Axes`
157158
"""
158-
159+
159160
return self.axes['Ei']
160161

161-
162+
162163
@property
163164
def measured_energy_axis(self):
164165
"""
165166
Measured energy bins (``Em``).
166167
167168
Returns
168169
-------
169-
:py:class:`histpy.Axes`
170+
:py:class:`histpy.Axes`
170171
"""
171-
172-
return self.axes['Em']
173-
174172

175-
176-
173+
return self.axes['Em']

0 commit comments

Comments
 (0)