class EventPositions(strax.Plugin): """ Computes the observed and corrected position for the main S1/S2 pairs in an event. For XENONnT data, it returns the FDC corrected positions of the default_reconstruction_algorithm. In case the fdc_map is given as a file (not through CMT), then the coordinate system should be given as (x, y, z), not (x, y, drift_time). """ depends_on = ('event_basics', ) __version__ = '0.1.5' default_reconstruction_algorithm = straxen.URLConfig( default=DEFAULT_POSREC_ALGO, help="default reconstruction algorithm that provides (x,y)") electron_drift_velocity = straxen.URLConfig( default='cmt://' 'electron_drift_velocity' '?version=ONLINE&run_id=plugin.run_id', cache=True, help='Vertical electron drift velocity in cm/ns (1e4 m/ms)') electron_drift_time_gate = straxen.URLConfig( default='cmt://' 'electron_drift_time_gate' '?version=ONLINE&run_id=plugin.run_id', help='Electron drift time from the gate in ns', cache=True) def infer_dtype(self): dtype = [] for j in 'x y r'.split(): comment = f'Main interaction {j}-position, field-distortion corrected (cm)' dtype += [(j, np.float32, comment)] for s_i in [1, 2]: comment = f'Alternative S{s_i} interaction (rel. main S{int(2*(1.5-s_i)+s_i)}) {j}-position, field-distortion corrected (cm)' field = f'alt_s{s_i}_{j}_fdc' dtype += [(field, np.float32, comment)] for j in ['z']: comment = 'Interaction z-position, using mean drift velocity only (cm)' dtype += [(j, np.float32, comment)] for s_i in [1, 2]: comment = f'Alternative S{s_i} z-position (rel. main S{int(2*(1.5-s_i)+s_i)}), using mean drift velocity only (cm)' field = f'alt_s{s_i}_z' dtype += [(field, np.float32, comment)] naive_pos = [] fdc_pos = [] for j in 'r z'.split(): naive_pos += [ (f'{j}_naive', np.float32, f'Main interaction {j}-position with observed position (cm)') ] fdc_pos += [ (f'{j}_field_distortion_correction', np.float32, f'Correction added to {j}_naive for field distortion (cm)') ] for s_i in [1, 2]: naive_pos += [( f'alt_s{s_i}_{j}_naive', np.float32, f'Alternative S{s_i} interaction (rel. main S{int(2*(1.5-s_i)+s_i)}) {j}-position with observed position (cm)' )] fdc_pos += [( f'alt_s{s_i}_{j}_field_distortion_correction', np.float32, f'Correction added to alt_s{s_i}_{j}_naive for field distortion (cm)' )] dtype += naive_pos + fdc_pos for s_i in [1, 2]: dtype += [( f'alt_s{s_i}_theta', np.float32, f'Alternative S{s_i} (rel. main S{int(2*(1.5-s_i)+s_i)}) interaction angular position (radians)' )] dtype += [('theta', np.float32, f'Main interaction angular position (radians)')] return dtype + strax.time_fields def setup(self): if isinstance(self.config['fdc_map'], str): self.map = InterpolatingMap( get_resource(self.config['fdc_map'], fmt='binary')) elif is_cmt_option(self.config['fdc_map']): self.map = InterpolatingMap( get_cmt_resource( self.run_id, tuple([ 'suffix', self.config['default_reconstruction_algorithm'], *self.config['fdc_map'] ]), fmt='binary')) self.map.scale_coordinates([1., 1., -self.electron_drift_velocity]) else: raise NotImplementedError('FDC map format not understood.') def compute(self, events): result = {'time': events['time'], 'endtime': strax.endtime(events)} # s_i == 0 indicates the main event, while s_i != 0 means alternative S1 or S2 is used based on s_i value # while the other peak is the main one (e.g., s_i == 1 means that the event is defined using altS1 and main S2) for s_i in [0, 1, 2]: # alt_sx_interaction_drift_time is calculated between main Sy and alternative Sx drift_time = events['drift_time'] if not s_i else events[ f'alt_s{s_i}_interaction_drift_time'] z_obs = -self.electron_drift_velocity * drift_time xy_pos = 's2_' if s_i != 2 else 'alt_s2_' orig_pos = np.vstack( [events[f'{xy_pos}x'], events[f'{xy_pos}y'], z_obs]).T r_obs = np.linalg.norm(orig_pos[:, :2], axis=1) delta_r = self.map(orig_pos) z_obs = z_obs + self.electron_drift_velocity * self.electron_drift_time_gate # apply radial correction with np.errstate(invalid='ignore', divide='ignore'): r_cor = r_obs + delta_r scale = np.divide(r_cor, r_obs, out=np.zeros_like(r_cor), where=r_obs != 0) # z correction due to longer drift time for distortion # calculated based on the Pythagorean theorem where # the electron track is assumed to be a straight line # (geometrical reasoning not valid if |delta_r| > |z_obs|, # as cathetus cannot be longer than hypothenuse) with np.errstate(invalid='ignore'): z_cor = -(z_obs**2 - delta_r**2)**0.5 invalid = np.abs(z_obs) < np.abs(delta_r) # do not apply z correction above gate invalid |= z_obs >= 0 z_cor[invalid] = z_obs[invalid] delta_z = z_cor - z_obs pre_field = '' if s_i == 0 else f'alt_s{s_i}_' post_field = '' if s_i == 0 else '_fdc' result.update({ f'{pre_field}x{post_field}': orig_pos[:, 0] * scale, f'{pre_field}y{post_field}': orig_pos[:, 1] * scale, f'{pre_field}r{post_field}': r_cor, f'{pre_field}r_naive': r_obs, f'{pre_field}r_field_distortion_correction': delta_r, f'{pre_field}theta': np.arctan2(orig_pos[:, 1], orig_pos[:, 0]), f'{pre_field}z_naive': z_obs, # using z_obs in agreement with the dtype description # the FDC for z (z_cor) is found to be not reliable (see #527) f'{pre_field}z': z_obs, f'{pre_field}z_field_distortion_correction': delta_z }) return result
class EventPositions(strax.Plugin): """ Computes the observed and corrected position for the main S1/S2 pairs in an event. For XENONnT data, it returns the FDC corrected positions of the default_reconstruction_algorithm. In case the fdc_map is given as a file (not through CMT), then the coordinate system should be given as (x, y, z), not (x, y, drift_time). """ depends_on = ('event_basics', ) __version__ = '0.1.3' dtype = [ ('x', np.float32, 'Interaction x-position, field-distortion corrected (cm)'), ('y', np.float32, 'Interaction y-position, field-distortion corrected (cm)'), ('z', np.float32, 'Interaction z-position, field-distortion corrected (cm)'), ('r', np.float32, 'Interaction radial position, field-distortion corrected (cm)'), ('z_naive', np.float32, 'Interaction z-position using mean drift velocity only (cm)'), ('r_naive', np.float32, 'Interaction r-position using observed S2 positions directly (cm)'), ('r_field_distortion_correction', np.float32, 'Correction added to r_naive for field distortion (cm)'), ('theta', np.float32, 'Interaction angular position (radians)') ] + strax.time_fields def setup(self): is_CMT = isinstance(self.config['fdc_map'], tuple) if is_CMT: cmt, cmt_conf, is_nt = self.config['fdc_map'] cmt_conf = ( f'{cmt_conf[0]}_{self.config["default_reconstruction_algorithm"]}', cmt_conf[1]) map_algo = cmt, cmt_conf, is_nt self.map = InterpolatingMap( get_resource(get_config_from_cmt(self.run_id, map_algo), fmt='binary')) self.map.scale_coordinates( [1., 1., -self.config['electron_drift_velocity']]) elif isinstance(self.config['fdc_map'], str): self.map = InterpolatingMap( get_resource(self.config['fdc_map'], fmt='binary')) else: raise NotImplementedError('FDC map format not understood.') def compute(self, events): result = {'time': events['time'], 'endtime': strax.endtime(events)} z_obs = -self.config['electron_drift_velocity'] * events['drift_time'] orig_pos = np.vstack([events[f's2_x'], events[f's2_y'], z_obs]).T r_obs = np.linalg.norm(orig_pos[:, :2], axis=1) delta_r = self.map(orig_pos) # apply radial correction with np.errstate(invalid='ignore', divide='ignore'): r_cor = r_obs + delta_r scale = r_cor / r_obs # z correction due to longer drift time for distortion # (geometrical reasoning not valid if |delta_r| > |z_obs|, # as cathetus cannot be longer than hypothenuse) with np.errstate(invalid='ignore'): z_cor = -(z_obs**2 - delta_r**2)**0.5 invalid = np.abs(z_obs) < np.abs(delta_r) z_cor[invalid] = z_obs[invalid] result.update({ 'x': orig_pos[:, 0] * scale, 'y': orig_pos[:, 1] * scale, 'r': r_cor, 'r_naive': r_obs, 'r_field_distortion_correction': delta_r, 'theta': np.arctan2(orig_pos[:, 1], orig_pos[:, 0]), 'z_naive': z_obs, 'z': z_cor }) return result
class EventPositions(strax.Plugin): """ Computes the observed and corrected position for the main S1/S2 pairs in an event. For XENONnT data, it returns the FDC corrected positions of the default_reconstruction_algorithm. In case the fdc_map is given as a file (not through CMT), then the coordinate system should be given as (x, y, z), not (x, y, drift_time). """ depends_on = ('event_basics', ) __version__ = '0.1.4' default_reconstruction_algorithm = straxen.URLConfig( default=DEFAULT_POSREC_ALGO, help="default reconstruction algorithm that provides (x,y)") dtype = [ ('x', np.float32, 'Interaction x-position, field-distortion corrected (cm)'), ('y', np.float32, 'Interaction y-position, field-distortion corrected (cm)'), ('z', np.float32, 'Interaction z-position, using mean drift velocity only (cm)'), ('r', np.float32, 'Interaction radial position, field-distortion corrected (cm)'), ('z_naive', np.float32, 'Interaction z-position using mean drift velocity only (cm)'), ('r_naive', np.float32, 'Interaction r-position using observed S2 positions directly (cm)'), ('r_field_distortion_correction', np.float32, 'Correction added to r_naive for field distortion (cm)'), ('z_field_distortion_correction', np.float32, 'Correction added to z_naive for field distortion (cm)'), ('theta', np.float32, 'Interaction angular position (radians)') ] + strax.time_fields def setup(self): self.electron_drift_velocity = get_correction_from_cmt( self.run_id, self.config['electron_drift_velocity']) self.electron_drift_time_gate = get_correction_from_cmt( self.run_id, self.config['electron_drift_time_gate']) if isinstance(self.config['fdc_map'], str): self.map = InterpolatingMap( get_resource(self.config['fdc_map'], fmt='binary')) elif is_cmt_option(self.config['fdc_map']): self.map = InterpolatingMap( get_cmt_resource( self.run_id, tuple([ 'suffix', self.config['default_reconstruction_algorithm'], *self.config['fdc_map'] ]), fmt='binary')) self.map.scale_coordinates([1., 1., -self.electron_drift_velocity]) else: raise NotImplementedError('FDC map format not understood.') def compute(self, events): result = {'time': events['time'], 'endtime': strax.endtime(events)} z_obs = -self.electron_drift_velocity * (events['drift_time'] - self.electron_drift_time_gate) orig_pos = np.vstack([events[f's2_x'], events[f's2_y'], z_obs]).T r_obs = np.linalg.norm(orig_pos[:, :2], axis=1) delta_r = self.map(orig_pos) # apply radial correction with np.errstate(invalid='ignore', divide='ignore'): r_cor = r_obs + delta_r scale = r_cor / r_obs # z correction due to longer drift time for distortion # (geometrical reasoning not valid if |delta_r| > |z_obs|, # as cathetus cannot be longer than hypothenuse) with np.errstate(invalid='ignore'): z_cor = -(z_obs**2 - delta_r**2)**0.5 invalid = np.abs(z_obs) < np.abs(delta_r) # do not apply z correction above gate invalid |= z_obs >= 0 z_cor[invalid] = z_obs[invalid] delta_z = z_cor - z_obs result.update({ 'x': orig_pos[:, 0] * scale, 'y': orig_pos[:, 1] * scale, 'r': r_cor, 'r_naive': r_obs, 'r_field_distortion_correction': delta_r, 'theta': np.arctan2(orig_pos[:, 1], orig_pos[:, 0]), 'z_naive': z_obs, # using z_obs in agreement with the dtype description # the FDC for z (z_cor) is found to be not reliable (see #527) 'z': z_obs, 'z_field_distortion_correction': delta_z }) return result