88import xarray as xr
99import h5py
1010
11+ from sklearn .linear_model import RANSACRegressor , LinearRegression
1112from extra_data import open_run , by_id , DataCollection , KeyData
1213from extra .components import Scan , XrayPulses
14+ from scipy .ndimage import gaussian_filter
1315
1416from .base import SerializableMixin
1517
1618def calc_mean (energy_id : int , scan : Scan ,
1719 grating : KeyData ,
20+ mask : KeyData = None
1821 ) -> np .ndarray :
1922 """
2023 Calculate mean over train IDs with a given energy value.
@@ -23,41 +26,46 @@ def calc_mean(energy_id: int, scan: Scan,
2326 energy_id: The id of this energy bin in the `scan` object.
2427 scan: The `Scan` object.
2528 grating: The camera key object.
29+ mask: Calibration mask object.
2630 """
2731 energy , train_ids = scan .steps [energy_id ]
2832 logging .debug (f"Energy { energy } , energy id { energy_id } " )
29- data = grating .select_trains (by_id [list (train_ids )]).xarray ()
30- return data .mean ('trainId' ).to_numpy ()
33+ data = grating .select_trains (by_id [list (train_ids )]).ndarray ()
34+ if mask is not None :
35+ mask = mask .select_trains (by_id [list (train_ids )]).ndarray ()
36+ data [mask > 0 ] = np .nan
37+ return np .nanmean (data , 0 )
3138
3239class Grating1DCalibration (SerializableMixin ):
3340 """
3441 Calibrate a 1D grating spectrometer.
3542
3643 Args:
3744 offset: Offset of the first pulse.
45+ sigma: Smoothing factor to apply to data to reduce noise in pixels.
3846
3947 Example:
4048 ```
4149 bkg_run = open_run(proposal=900485, run=590)
4250 calib_run = open_run(proposal=900485, run=611)
4351 scan = Scan(calib_run["SA3_XTD10_MONO/MDL/PHOTON_ENERGY", "actualEnergy"])
4452 grating_signal = calib_run["SQS_EXP_GH2-2/CORR/RECEIVER:daqOutput", "data.adc"]
45- grating_bkg = bkg_run["SQS_EXP_GH2-2/CORR/RECEIVER:daqOutput", "data.adc"]
4653 pulses = XrayPulses(calib_run)
4754 grating_calib = Grating1DCalibration()
48- grating_calib.setup(grating_signal, grating_bkg, scan, pulses)
55+ grating_calib.setup(grating_signal, scan, pulses)
4956
5057 # apply it in new data
5158 new_run = open_run(...)
5259 grating_calib.apply(new_run)
5360 ```
5461 """
55- def __init__ (self , offset : Optional [int ]= None , min_pixel : int = 500 , max_pixel : int = 1000 ):
56- self ._version = 1
62+ def __init__ (self , offset : Optional [int ]= None , min_pixel : int = 0 , max_pixel : int = 1280 , sigma : float = 2.0 ):
63+ self ._version = 2
5764 self .offset = offset
5865 self .min_pixel = min_pixel
5966 self .max_pixel = max_pixel
6067 self .calibration_mask = None
68+ self .sigma = sigma
6169 self ._all_fields = [
6270 "pulse_period" ,
6371 "offset" ,
@@ -66,23 +74,22 @@ def __init__(self, offset: Optional[int]=None, min_pixel: int=500, max_pixel: in
6674 "max_pixel" ,
6775 "e0" ,
6876 "slope" ,
69- "bkg" ,
70- "bkg_unc" ,
7177 "energy_axis" ,
7278 "calibration_energies" ,
7379 "calibration_data" ,
74- "calibration_unc" ,
7580 "grating_source" ,
7681 "grating_key" ,
7782 "sources" ,
83+ "grating_mask_key" ,
84+ "sigma" ,
7885 "_version" ,
7986 ]
8087
8188 def setup (self ,
8289 grating_signal : KeyData ,
8390 scan : Scan ,
8491 pulses : XrayPulses ,
85- grating_bkg : Optional [KeyData ]= None ,
92+ grating_mask : Optional [KeyData ]= None ,
8693 ):
8794 """
8895 Setup calibration.
@@ -94,13 +101,16 @@ def setup(self,
94101 Example: `Scan(run["SA3_XTD10_MONO/MDL/PHOTON_ENERGY", "actualEnergy"])`
95102 pulses: Object with bunch pattern table.
96103 Example: `XrayPulses(run)`
97- grating_bkg: Where to read the grating background data from .
98- Example: `bkg_run ["SQS_EXP_GH2-2/CORR/RECEIVER:daqOutput", "data.adc "]`
104+ grating_mask: Grating mask from the calibration system .
105+ Example: `signal_run ["SQS_EXP_GH2-2/CORR/RECEIVER:daqOutput", "data.mask "]`
99106 """
100107 self .grating_source = grating_signal .source
101108 self .grating_key = grating_signal .key
109+ self .grating_mask_key = ""
110+ if grating_mask is not None :
111+ self .grating_mask_key = grating_mask .key
102112 self ._grating_signal = grating_signal
103- self ._grating_bkg = grating_bkg
113+ self ._grating_mask = grating_mask
104114
105115 self .sources = [
106116 self .grating_source ,
@@ -123,10 +133,6 @@ def setup(self,
123133 logging .info ("Extract bunch pattern table" )
124134 self .pulse_period = self .get_pulse_period (pulses )
125135
126- # background
127- logging .info ("Load background ..." )
128- self .get_background_template ()
129-
130136 # load data
131137 logging .info ("Load data ..." )
132138 self .load_data ()
@@ -150,6 +156,8 @@ def _fromdict(cls, all_data):
150156 Rebuild object from dict.
151157 """
152158 self = cls ()
159+ # backwards compatibility
160+ self .grating_mask_key = ""
153161 for k , v in all_data .items ():
154162 setattr (self , k , v )
155163 return self
@@ -158,9 +166,24 @@ def guess_offset(self):
158166 """
159167 Guess offset.
160168 """
161- I = self ._grating_signal .xarray ().sel (dim_1 = np .s_ [self .min_pixel :self .max_pixel ]).mean ('dim_1' ).mean ('trainId' )
162- threshold = np .median (I )
163- self .offset = np .where (I >= threshold )[0 ][0 ]
169+ I = self ._grating_signal .xarray ().sel (dim_1 = np .s_ [self .min_pixel :self .max_pixel ]).to_numpy ()
170+ if self ._grating_mask is not None :
171+ Im = self ._grating_mask .xarray ().sel (dim_1 = np .s_ [self .min_pixel :self .max_pixel ]).to_numpy ()
172+ I [Im > 0 ] = 0
173+ # smooth over energy-pixels
174+ if self .sigma > 0 :
175+ I = gaussian_filter (np .nan_to_num (I ), axes = - 1 , sigma = self .sigma )
176+ # maximum over energy-pixels
177+ I = np .max (I , axis = - 1 )
178+ # mean over trains
179+ I = np .mean (I , 0 )
180+ # at this point I contains only the time dimension
181+ # look for peaks over background
182+ offset = [None , None ]
183+ for s in [0 , 1 ]:
184+ threshold = np .median (I [s ::2 ])
185+ offset [s ] = np .where (I [s ::2 ] >= threshold )[0 ][0 ]* 2 + s
186+ self .offset = max (offset )
164187 logging .info (f"Offset estimated at { self .offset } " )
165188
166189 def get_pulse_period (self , pulses : XrayPulses ):
@@ -181,42 +204,46 @@ def get_pulse_period(self, pulses: XrayPulses):
181204 logging .info (f"Pulse period estimated at { pulse_period } " )
182205 return pulse_period
183206
184- def get_background_template (self ):
185- """Get the background template.
186- """
187- if self ._grating_bkg is None :
188- self .bkg = None
189- self .bkg_unc = None
190- else :
191- self .bkg = self ._grating_bkg .ndarray ().mean (0 )
192- self .bkg_unc = self ._grating_bkg .ndarray ().std (0 )
207+ def apply_mask (self , arr ):
208+ """
209+ Interpolate nans.
210+ """
211+ axis_full = np .arange (arr .shape [- 1 ])
212+ shape = arr .shape
213+ arr = np .reshape (arr , (- 1 , shape [- 1 ]))
214+ # if all points are bad, set it to zero
215+ # nothing else can be done there ...
216+ all_bad = np .all (np .isnan (arr ), axis = 1 )
217+ arr [all_bad , :] = 0
218+ # otherwise interpolate
219+ arr = np .apply_along_axis (lambda a : np .interp (axis_full ,
220+ axis_full [~ np .isnan (a )],
221+ a [~ np .isnan (a )],
222+ left = 0 , right = 0 ),
223+ arr = arr , axis = 1 )
224+ arr = np .reshape (arr , shape )
225+ return arr
193226
194227 def load_data (self ):
195228 """Load calibration data."""
196229 from scipy .ndimage import rotate
197230 fn = partial (calc_mean ,
198231 scan = self ._scan ,
199232 grating = self ._grating_signal ,
233+ mask = self ._grating_mask ,
200234 )
201235 energy_ids = np .arange (len (self .calibration_energies ))
202236 # average data in each mono scan bin
203237 with ProcessPoolExecutor () as p :
204238 data = np .stack (list (p .map (fn , energy_ids )), axis = 0 )
205- # subtract the background
206- bkg_unc = np .zeros_like (data ).mean (0 )
207- if self .bkg is not None :
208- data = data - self .bkg
209- bkg_unc = self .bkg_unc
210- else :
211- self .bkg_unc = bkg_unc
212- self .bkg = np .zeros_like (data ).mean (0 )
213- self .calibration_unc = bkg_unc
214239 # skip offset and collect pulse data each pulse_period samples only
215240 self .calibration_data = data [:, self .offset ::self .pulse_period , self .min_pixel :self .max_pixel ]
216- self .calibration_unc = self .calibration_unc [self .offset ::self .pulse_period , self .min_pixel :self .max_pixel ]
241+ # apply mask
242+ self .calibration_data = self .apply_mask (self .calibration_data )
217243 # average over pulses
218- self .calibration_data = np .mean (self .calibration_data , axis = 1 )
219- self .calibration_unc = np .mean (self .calibration_unc , axis = 0 )
244+ self .calibration_data = np .nanmean (self .calibration_data , axis = 1 )
245+ if self .sigma > 0 :
246+ self .calibration_data = gaussian_filter (np .nan_to_num (self .calibration_data ), axes = - 1 , sigma = self .sigma )
220247 self .calibration_mask = np .ones (self .calibration_data .shape [0 ], dtype = bool )
221248
222249 def mask_calibration_point (self , energy : float , mask : bool = False , tol : float = 1.0 ):
@@ -234,14 +261,15 @@ def mask_calibration_point(self, energy: float, mask: bool=False, tol: float=1.0
234261
235262 def fit (self ):
236263 """Fit line."""
237- from scipy .stats import linregress
238264 mask = self .calibration_mask
239265 sample = np .arange (self .calibration_data .shape [- 1 ])
240- sample_mode = np .argmax (self .calibration_data , axis = - 1 )
241- #sample_mode = snp.sum(self.calibration_data*sample, axis=-1)/np.sum(self.calibration_data, axis=-1)
242- res = linregress (sample_mode [mask ], self .calibration_energies [mask ])
243- self .slope = res .slope
244- self .e0 = res .intercept
266+ sample_mode = np .nanargmax (self .calibration_data , axis = - 1 )
267+ x = sample_mode [mask ]
268+ y = self .calibration_energies [mask ]
269+ model = RANSACRegressor (estimator = LinearRegression (), random_state = 42 )
270+ model .fit (x [:,np .newaxis ], y [:, np .newaxis ])
271+ self .slope = model .estimator_ .coef_ [0 ,0 ]
272+ self .e0 = model .estimator_ .intercept_ [0 ]
245273 self .energy_axis = self .e0 + self .slope * sample
246274
247275 def plot (self ):
@@ -251,7 +279,7 @@ def plot(self):
251279 import matplotlib .pyplot as plt
252280 plt .figure (figsize = (10 , 8 ))
253281 sample = np .arange (self .calibration_data .shape [- 1 ])
254- sample_mode = np .argmax (self .calibration_data , axis = - 1 )
282+ sample_mode = np .nanargmax (self .calibration_data , axis = - 1 )
255283 plt .plot (sample , self .energy_axis , lw = 2 , label = "Fit" )
256284 plt .xlabel ("Pixel" )
257285 plt .ylabel ("Energy [eV]" )
@@ -267,26 +295,35 @@ def apply(self, run: DataCollection, load_all: bool=True) -> xr.Dataset:
267295
268296 Args:
269297 run: Input run.
270- load_all: If True, load all data in memory at once. This is faste , but uses more memory.
298+ load_all: If True, load all data in memory at once. This is faster , but uses more memory.
271299 Disable if not enough memory is available.
272300 """
273301 # do it per train to avoid memory overflow
274302 pulse_period = self .get_pulse_period (XrayPulses (run ))
275303 if load_all :
276304 out_data = run [self .grating_source , self .grating_key ].xarray ()
277305 trainId = out_data .trainId .to_numpy ()
278- out_data = out_data .to_numpy () - self . bkg
306+ out_data = out_data .to_numpy ()
279307 out_data = out_data [:, self .offset ::pulse_period , self .min_pixel :self .max_pixel ]
308+ if self .grating_mask_key :
309+ out_mask = run [self .grating_source , self .grating_mask_key ].ndarray () > 0
310+ out_mask = out_mask [:, self .offset ::pulse_period , self .min_pixel :self .max_pixel ]
311+ out_data [out_mask ] = np .nan
312+ out_data = self .apply_mask (out_data )
280313 else :
281314 trainId = list ()
282315 out_data = list ()
283316 for i , (tid , data ) in enumerate (run .trains ()):
284- #print(f"Train {tid}, idx {i}")
285317 d = data [self .grating_source ][self .grating_key ]
286- if self .bkg is not None :
287- d = d - self .bkg
288318 # skip offset and collect pulse data each pulse_period samples only
289319 d = d [self .offset ::pulse_period , self .min_pixel :self .max_pixel ]
320+
321+ if self .grating_mask_key :
322+ m = data [self .grating_source ][self .grating_mask_key ] > 0
323+ m = m [self .offset ::pulse_period , self .min_pixel :self .max_pixel ]
324+ d [m ] = np .nan
325+ d = self .apply_mask (d )
326+
290327 trainId += [tid ]
291328 out_data += [d ]
292329 out_data = np .stack (out_data , axis = 0 )
@@ -298,9 +335,7 @@ def apply(self, run: DataCollection, load_all: bool=True) -> xr.Dataset:
298335 energy = energy
299336 )
300337 )
301- out_unc = xr .DataArray (data = self .calibration_unc , dims = ('energy' ),
302- coords = dict (energy = energy ))
303- return xr .Dataset (data_vars = dict (data = out_data , unc = out_unc ))
338+ return xr .Dataset (data_vars = dict (data = out_data ))
304339
305340class Grating2DCalibration (SerializableMixin ):
306341 """
0 commit comments