@@ -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