Skip to content

Commit 74019be

Browse files
committed
Energy Integration with discontinuous intrumental responses
In this draft, based on a mixing between pull requests #503 and #588, a different energy integrator has been tested, in the case of signals presenting bad quality data among good ones. As X-PSI is initially based on integration on continuous bins, the use of instrumental responses with "holes" is not adapted in the initial configuration, returning the response error related to the lack of positive values or the assertion errors related to the energies and channel edges. So a new version of the energy integrator is proposed here, with the edges designed into 2 arrays: one with the lower bounds and one with the upper bounds for each energy bin. Another extension is also given to enable X-PSI to use linear interpolation.
1 parent 6d01b41 commit 74019be

10 files changed

Lines changed: 396 additions & 72 deletions

File tree

setup.py

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -182,6 +182,7 @@ def EXTENSION(modname):
182182
'xpsi.cellmesh.rays',
183183
'xpsi.tools.energy_interpolator',
184184
'xpsi.tools.energy_integrator',
185+
'xpsi.tools.energy_integrator_2Dedges',
185186
'xpsi.tools.phase_integrator',
186187
'xpsi.tools.phase_interpolator',
187188
'xpsi.tools.synthesise',

xpsi/Data.py

Lines changed: 19 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -225,6 +225,8 @@ def trim_data( self ,
225225
# Make the table of required channels
226226
assert min_channel >= self.channels[0]
227227
assert max_channel <= self.channels[-1]
228+
if max_channel == -1:
229+
max_channel = self.channels[-1]
228230
new_channels = [ min_channel <= c <= max_channel for c in self.channels]
229231

230232
# Trim the counts and channels
@@ -488,6 +490,8 @@ def from_pha( cls, path,
488490
counts_data = spectrum['RATE']
489491
exposure = _np.double(1.0)
490492

493+
TLMIN= _np.int32(Header['TLMIN1'])
494+
491495
# No channels specified, use everything
492496
if channels is None:
493497
min_channel = _np.min( channel_data )
@@ -497,6 +501,20 @@ def from_pha( cls, path,
497501
min_channel = channels[0]
498502
max_channel = channels[-1]
499503

504+
if 'QUALITY' in spectrum.columns.names :
505+
print('Bad bins detected...')
506+
quality=spectrum['QUALITY']
507+
qualchannels = _np.where(quality==0)
508+
spectrum = spectrum[qualchannels]
509+
print(r'Spectrum reduced to {} channels'.format(_np.shape(qualchannels)[1]))
510+
#channel_data = spectrum['CHANNEL']
511+
channel_data = _np.arange(TLMIN, TLMIN+len(spectrum['CHANNEL']))
512+
min_channel = _np.min( channel_data )
513+
max_channel = _np.max( channel_data )
514+
channels = _np.arange(min_channel,max_channel+1)
515+
print(channel_data, channels, qualchannels)
516+
counts_data = spectrum['COUNTS']
517+
500518
# Get intrinsinc values
501519
first = 0
502520
last = max_channel - min_channel
@@ -622,4 +640,4 @@ def plot(self, num_rot = 2, dpi=200, colormap='inferno'):
622640
fig.colorbar( im , ax=ax3 , location='right', label='Counts')
623641
fig.set_dpi(dpi)
624642

625-
return fig, axs
643+
return fig, axs

xpsi/Instrument.py

Lines changed: 134 additions & 28 deletions
Original file line numberDiff line numberDiff line change
@@ -79,6 +79,12 @@ class Instrument(ParameterSubspace):
7979
of edges must be :math:`p + 1`. The edges must be monotonically
8080
increasing. These edges will correspond to the nominal response matrix
8181
and any deviation from this matrix (see above).
82+
83+
:param ndarray[p+1] quality_checks:
84+
Check if spectra counts present good quality. Default is False.
85+
86+
:param ndarray[p+1] goodchannels:
87+
Instrument channel numbers for which we have good quality.
8288
8389
:param tuple args:
8490
Container of parameter instances.
@@ -89,14 +95,22 @@ class Instrument(ParameterSubspace):
8995
find its way to the base class.
9096
9197
"""
92-
def __init__(self, matrix, energy_edges, channels, channel_edges=None,
98+
def __init__(self, matrix, energy_edges, channels, channel_edges=None, quality_checks=False, goodchannels=None,
9399
*args, **kwargs):
94100

95101
self.matrix = matrix
96-
self.energy_edges = energy_edges
97102
self.channels = channels
103+
self._quality_checks = quality_checks
104+
if self._quality_checks==True:
105+
try:
106+
#self._quality_checks = quality_checks
107+
self._goodchannels = goodchannels
108+
except TypeError:
109+
raise ChannelError('If quality is checked, then a list of good channels must be returned')
110+
self.energy_edges = energy_edges
98111
if channel_edges is not None:
99112
self.channel_edges = channel_edges
113+
100114

101115
super(Instrument, self).__init__(*args, **kwargs)
102116

@@ -134,10 +148,13 @@ def matrix(self, matrix):
134148
try:
135149
for i in range(matrix.shape[0]):
136150
assert matrix[i,:].any()
151+
except AssertionError:
152+
raise ResponseError('Each row of the matrix must contain at least one positive number.')
153+
try:
137154
for j in range(matrix.shape[1]):
138155
assert matrix[:,j].any()
139156
except AssertionError:
140-
raise ResponseError('Each row and column of the matrix must contain at least one positive number.')
157+
raise ResponseError('Each column of the matrix must contain at least one positive number.')
141158
self._matrix = matrix
142159

143160
def construct_matrix(self):
@@ -218,13 +235,27 @@ def energy_edges(self, energy_edges):
218235
except TypeError:
219236
raise EdgesError('Energy edges must be in a one-dimensional array of positive increasing values.')
220237

238+
221239
try:
222-
assert energy_edges.ndim == 1
223-
assert (energy_edges >= 0.0).all()
224-
assert energy_edges.shape[0] == self._matrix.shape[1] + 1
225-
assert not (energy_edges[1:] <= energy_edges[:-1]).any()
240+
#"""
241+
if self._quality_checks==True:
242+
if energy_edges.ndim == 2 :
243+
for i in range(energy_edges.ndim):
244+
assert (energy_edges[i] >= 0.0).all()
245+
assert not (energy_edges[i, 1:] <= energy_edges[i, :-1]).any()
246+
else:
247+
assert energy_edges.ndim == 1
248+
assert (energy_edges >= 0.0).all()
249+
assert energy_edges.shape[0] == self._matrix.shape[1] + 1
250+
assert not (energy_edges[1:] <= energy_edges[:-1]).any()
251+
#"""
252+
#assert energy_edges.ndim == 1
253+
#assert (energy_edges >= 0.0).all()
254+
#assert energy_edges.shape[0] == self._matrix.shape[1] + 1
255+
#assert not (energy_edges[1:] <= energy_edges[:-1]).any()
226256
except AssertionError:
227-
raise EdgesError('Energy edges must be in a one-dimensional array of positive increasing values, with a '
257+
raise EdgesError('Unless if there are bad channels causing the removal of response contribution from some energy bins,'
258+
'energy edges must be in a one-dimensional array of positive increasing values, with a '
228259
'length equal to number of energy intervals in the matrix + 1.')
229260

230261
self._energy_edges = energy_edges
@@ -266,12 +297,19 @@ def channel_edges(self, channel_edges):
266297
raise EdgesError('Channel edges must be in a one-dimensional array of positive increasing values.')
267298

268299
try:
269-
assert channel_edges.ndim == 1
270-
assert (channel_edges >= 0.0).all()
271-
assert channel_edges.shape[0] == self._matrix.shape[0] + 1
272-
assert not (channel_edges[1:] <= channel_edges[:-1]).any()
300+
if self._quality_checks==True:
301+
assert channel_edges.ndim == 2
302+
for i in range(channel_edges.ndim):
303+
assert (channel_edges[i] >= 0.0).all()
304+
assert not (channel_edges[i, 1:] <= channel_edges[i, :-1]).any()
305+
#assert channel_edges.shape[0] == self._matrix.shape[0] + 1
306+
else:
307+
assert channel_edges.ndim == 1
308+
assert (channel_edges >= 0.0).all()
309+
assert channel_edges.shape[0] == self._matrix.shape[0] + 1
310+
assert not (channel_edges[1:] <= channel_edges[:-1]).any()
273311
except AssertionError:
274-
raise EdgesError('Channel edges must be in a one-dimensional array of positive increasing values, with a '
312+
raise EdgesError('Unless if there are bad channels, channel edges must be in a one-dimensional array of positive increasing values, with a '
275313
'length equal to the number of channel intervals in the matrix + 1.')
276314

277315
self._channel_edges = channel_edges
@@ -311,12 +349,39 @@ def channels(self, channel_array):
311349

312350
yield
313351

352+
@property
353+
def goodchannels(self):
354+
""" Get the array of channels with good quality.
355+
356+
"""
357+
return self._goodchannels
358+
359+
@goodchannels.setter
360+
@make_verbose('Setting channels with good quality',
361+
'Good channels set')
362+
def goodchannels(self, channel_array):
363+
if not isinstance(channel_array, _np.ndarray):
364+
try:
365+
channel_array = _np.array(channel_array)
366+
except TypeError:
367+
raise ChannelError('Channel numbers must be in an array.')
368+
369+
try:
370+
assert channel_array.ndim == 1
371+
assert (channel_array >= 0).all()
372+
except AssertionError:
373+
raise ChannelError('Good channel numbers must be in a one-dimensional array of positive integers (including zero)')
374+
375+
self._goodchannels = channel_array
376+
377+
yield
378+
314379
@make_verbose('Trimming instrument response',
315380
'Instrument response trimmed')
316381
def trim_response(self,
317382
min_channel=0,
318383
max_channel=-1,
319-
threshold=1e-5 ):
384+
threshold=1e-5):
320385
""" Trim the instrument response to the specified channel range.
321386
322387
:param int min_channel:
@@ -328,7 +393,7 @@ def trim_response(self,
328393
:param float threshold:
329394
The threshold value to use for trimming the instrument response.
330395
Channels / inputs with a total response below this value will be removed.
331-
396+
332397
"""
333398

334399
# Make the table of required channels
@@ -343,7 +408,7 @@ def trim_response(self,
343408
new_matrix = self.matrix[new_channels_indexes]
344409
empty_channels = _np.all( new_matrix <= threshold, axis=1)
345410
empty_inputs = _np.all( new_matrix <= threshold, axis=0)
346-
411+
347412
# Apply to matrix and channels directly
348413
self.matrix = new_matrix[~empty_channels][:,~empty_inputs]
349414
self.channels = self.channels[new_channels_indexes][ ~empty_channels ]
@@ -352,8 +417,20 @@ def trim_response(self,
352417
new_energy_edges = [ self.energy_edges[k] for k in range(len(empty_inputs)) if not empty_inputs[k] ]
353418
self.energy_edges = _np.hstack( (new_energy_edges , self.energy_edges[ _np.where( self.energy_edges == new_energy_edges[-1] )[0] + 1 ] ) )
354419
if hasattr( self , 'channel_edges' ):
355-
new_channels_edges = [ self.channel_edges[ _np.where(old_channels==chan)[0][0]] for chan in self.channels]
356-
self.channel_edges = _np.hstack( (new_channels_edges , self.channel_edges[_np.where( old_channels==self.channels[-1])[0] + 1]) )
420+
if hasattr(self, 'goodchannels'):
421+
EMINS, EMAXS = self.channel_edges[0], self.channel_edges[1]
422+
MINMAXS = _np.concatenate([EMINS,EMAXS])
423+
minmaxs = MINMAXS.tolist()
424+
old_channels_edges = _np.sort(np.array(list(dict.fromkeys(minmaxs))))
425+
new_channels_edges = [ old.channel_edges[ _np.where(old_channels==chan)[0][0]] for chan in self.channels]
426+
allchannel_edges = _np.hstack( (new_channels_edges , old.channel_edges[_np.where( old_channels==self.channels[-1])[0] + 1]) )
427+
EnewMINS = allchannel_edges[:-1][self.goodchannels]
428+
EnewMAXS = allchannel_edges[1:][self.goodchannels]
429+
self.channel_edges = _np.stack((EnewMINS,EnewMAXS))
430+
431+
else:
432+
new_channels_edges = [ self.channel_edges[ _np.where(old_channels==chan)[0][0]] for chan in self.channels]
433+
self.channel_edges = _np.hstack( (new_channels_edges , self.channel_edges[_np.where( old_channels==self.channels[-1])[0] + 1]) )
357434

358435
# Print if any trimming happens
359436
if empty_inputs.sum() > 0:
@@ -379,6 +456,7 @@ def from_ogip_fits(cls,
379456
min_input=1,
380457
max_input=-1,
381458
datafolder=None,
459+
Data_path=None,
382460
**kwargs):
383461
""" Loading method for Instrument using OGIP defined ARF/RMF or RSP.
384462
@@ -388,6 +466,9 @@ def from_ogip_fits(cls,
388466
:param str | None ARF_path:
389467
The path to the ARF file which should be OGIP compliant or None if the RMF_path points to a RSP file.
390468
469+
:param str | None Data_path:
470+
The path to the Data file which should be OGIP compliant. Only to mention if there are quality information.
471+
391472
:param int min_channel:
392473
The minimum channel for which the instrument response is loaded.
393474
@@ -408,6 +489,7 @@ def from_ogip_fits(cls,
408489
"""
409490

410491
if datafolder:
492+
Data_path = _os.path.join( datafolder, Data_path ) if Data_path is not None else None
411493
ARF_path = _os.path.join( datafolder, ARF_path ) if ARF_path is not None else None
412494
RMF_path = _os.path.join( datafolder, RMF_path )
413495

@@ -426,12 +508,12 @@ def from_ogip_fits(cls,
426508
assert RMF_instr == ARF_hdul['SPECRESP'].header['INSTRUME']
427509

428510
# Get the values and change the -1 values if requried
511+
if min_channel == 0:
512+
min_channel = TLMIN
429513
if max_channel == -1:
430514
max_channel = DETCHANS -1
431515
if max_input == -1:
432516
max_input = NUMGRP
433-
channels = _np.arange( min_channel, max_channel+1 )
434-
inputs = _np.arange( min_input, max_input+1 )
435517

436518
# Perform routine checks
437519
assert min_channel >= TLMIN and max_channel <= TLMAX
@@ -442,6 +524,10 @@ def from_ogip_fits(cls,
442524
RMF_MATRIX = RMF_hdul['MATRIX'].data
443525
RMF_EBOUNDS = RMF_hdul['EBOUNDS'].data
444526

527+
# Get all the channels and input energies
528+
channels = _np.array( RMF_EBOUNDS['CHANNEL'] )
529+
inputs = _np.arange( 1, NUMGRP+1 )
530+
445531
# Get the channels from the data
446532
RMF = _np.zeros((DETCHANS, NUMGRP))
447533
for i, (N_GRP, F_CHAN, N_CHAN, RMF_line) in enumerate( zip(RMF_MATRIX['N_GRP'], RMF_MATRIX['F_CHAN'], RMF_MATRIX['N_CHAN'], RMF_MATRIX['MATRIX']) ):
@@ -462,18 +548,24 @@ def from_ogip_fits(cls,
462548
if n_chan == 0:
463549
continue
464550

465-
RMF[f_chan:f_chan+n_chan,i] += RMF_line[n_skip:n_skip+n_chan]
551+
RMF[f_chan-TLMIN:f_chan+n_chan-TLMIN,i] += RMF_line[n_skip:n_skip+n_chan]
466552
n_skip += n_chan
467553

554+
# Get the indexes of channels
555+
channels_indexes = _np.where( ( channels >= min_channel ) & ( channels <= max_channel ) )[0]
556+
inputs_indexes = _np.where( ( inputs >= min_input ) & ( inputs <= max_input ) )[0]
557+
channels = channels[channels_indexes]
558+
inputs = inputs[inputs_indexes]
559+
RMF = RMF[channels_indexes][:,inputs_indexes]
560+
468561
# Make the RSP, depending on the input files
469562
if ARF_path is None:
470-
RSP = RMF[min_channel:max_channel+1,min_input-1:max_input]
563+
RSP = RMF
471564
ARF_area = RMF.sum( axis=0 )
472565
else:
473566
ARF = Table.read(ARF_path, 'SPECRESP')
474-
ARF_area = ARF['SPECRESP']
567+
ARF_area = ARF['SPECRESP'][inputs_indexes]
475568
RSP = RMF * ARF_area
476-
RSP = RSP[min_channel:max_channel+1,min_input-1:max_input]
477569

478570
# Find empty columns and lines
479571
empty_channels = _np.all(RSP == 0, axis=1)
@@ -490,18 +582,32 @@ def from_ogip_fits(cls,
490582

491583
# Get the edges of energies for both input and channel
492584
energy_edges = _np.append( RMF_MATRIX['ENERG_LO'][inputs-1], RMF_MATRIX['ENERG_HI'][inputs[-1]-1]).astype(dtype=_np.double)
493-
channel_energy_edges = _np.append(RMF_EBOUNDS['E_MIN'][channels],RMF_EBOUNDS['E_MAX'][channels[-1]])
585+
channel_energy_edges = _np.append(RMF_EBOUNDS['E_MIN'][channels-TLMIN],RMF_EBOUNDS['E_MAX'][channels[-1]-TLMIN])
586+
587+
# If required, check for quality.
588+
quality_checks=False
589+
goodchannels=None
590+
if Data_path is not None:
591+
with fits.open( Data_path ) as Data_hdul:
592+
try:
593+
QUAL = Data_hdul['SPECTRUM'].data['QUALITY']
594+
quality_checks=True
595+
goodchannels=_np.where(QUAL==0)
596+
except TypeError:
597+
raise ChannelError('Quality of source channels should be specified!')
494598

495599
# Make the instrument
496600
Instrument = cls(RSP,
497601
energy_edges,
498602
channels,
499603
channel_energy_edges,
604+
quality_checks=quality_checks,
605+
goodchannels=goodchannels,
500606
**kwargs)
501607

502608
# Add ARF and RMF for plotting
503-
Instrument.RMF = RMF[min_channel:max_channel+1,min_input-1:max_input][~empty_channels][:,~empty_inputs]
504-
Instrument.ARF = ARF_area[min_input-1:max_input][~empty_inputs]
609+
Instrument.RMF = RMF[~empty_channels][:,~empty_inputs]
610+
Instrument.ARF = ARF_area[~empty_inputs]
505611
Instrument.name = RMF_instr
506612

507613
return Instrument
@@ -699,4 +805,4 @@ def from_ogip_fits(cls,
699805
pileup = XrayPileup(Instrument)
700806
Instrument.pileup = pileup
701807

702-
return Instrument
808+
return Instrument

0 commit comments

Comments
 (0)