class DummyEventBasics(strax.Plugin): """Get evenly spaced random duration events that don't overlap""" n_chunks = strax.Config(default=3) event_time_range = strax.Config( default=(0, 3e6), help='Where to span the durations of chunks over') events_per_chunk = strax.Config(default=20) event_durations = strax.Config(default=(1000, 20_000), help='event durations (min, max) ns') depends_on = () save_when = strax.SaveWhen.ALWAYS provides = 'event_basics' data_kind = 'events' dtype = strax.time_fields _events = None def source_finished(self): return True def is_ready(self, chunk_i): return chunk_i < self.n_chunks def get_events_this_chunk(self, chunk_i): if self._events is None: res = np.zeros(int(self.events_per_chunk * self.n_chunks), dtype=self.dtype) times = np.linspace(*self.event_time_range, len(res)) res['time'] = times endtimes = res['time'] + np.random.randint(*self.event_durations, size=len(res)) # Don't allow overlapping events endtimes[:-1] = np.clip(endtimes[:-1], 0, res['time'][1:] - 1) res['endtime'] = endtimes self._events = np.split(res, self.n_chunks) return self._events[chunk_i] def chunk_start(self, chunk_i) -> int: # Get the start time of the requested chunk assert self._events is not None, "run get_events_this_chunk first!" if chunk_i == 0: return self._events[chunk_i]['time'][0] return self._events[chunk_i - 1]['endtime'][-1] def get_chunk_end(self, chunk_i): assert self._events is not None, "run get_events_this_chunk first!" if chunk_i == self.n_chunks: return self.event_time_range[1] return self._events[chunk_i]['endtime'][-1] def compute(self, chunk_i): events = self.get_events_this_chunk(chunk_i) events_within = events['endtime'] < self.event_time_range[1] events = events[events_within] return self.chunk(start=self.chunk_start(chunk_i), end=self.get_chunk_end(chunk_i), data=events)
class DummyPlugin(strax.Plugin): depends_on = () provides = ('dummy_data', ) dtype = strax.dtypes.time_fields + [ (('Some data description', 'some_data_name'), np.int32), ] int_config = strax.Config(type=int, default=42) str_config = strax.Config(type=str, default='forty_two')
class DummyDAQReader(DAQReader): """Dummy version of DAQReader with different provides and different lineage""" provides = [ 'raw_records', 'raw_records_nv', 'raw_records_aqmon', ] dummy_version = strax.Config( default=None, track=True, help= "Extra option to make sure that we are getting a different lineage if we want to", ) data_kind = dict(zip(provides, provides)) rechunk_on_save = False def _path(self, chunk_i): path = super()._path(chunk_i) path_exists = os.path.exists(path) print(f'looked for {chunk_i} on {path}. Is found = {path_exists}') return path
class DummyAqmonHits(strax.Plugin): """ Dummy plugin to make some aqmon hits that may span ON and OFF signals over chunks There are two channels, that signify ON or OFF of a logic signal We are interested in the deadtime, which is the time difference between ON and OFF singals. This plugin computes that deadtime, even if we have missed the first ON (so we start with OFF) or missed the last OFF (such that the last signal is an ON). If we miss one ON at the start or one OFF at the end, we should just consider all time before the first OFF or all the time after the last ON as deadtime. """ vetos_per_chunk = strax.Config( default=list(range(1, 10)), help='The number of ON/OFF signals per chunk, preferably a ' 'combination of odd and even such that we have both ' 'unmatched ON/OFF singals') start_with_channel_on = strax.Config( default=True, help='If True, start with an ON signal, otherwise start if OFF') channel_on = strax.Config( default=straxen.acqmon_processing.AqmonChannels.BUSY_START, help='ON channel. Just some channel known to the VetoIntervals plugin.', type=int) channel_off = strax.Config( default=straxen.acqmon_processing.AqmonChannels.BUSY_STOP, help='OFF channel. Just some channel known to the VetoIntervals plugin', type=int) veto_duration_max = strax.Config(default=300e9, help='Max duration of one veto [ns].') # Start from scratch depends_on = () parallel = False provides = straxen.AqmonHits.provides dtype = straxen.AqmonHits.dtype save_when = strax.SaveWhen.NEVER # Keep track for we need this from plugin to plugin _last_channel_was_off = True _last_endtime = 0 # Will overwrite this with a mutable default in the test TOTAL_DEADTIME = None TOTAL_SIGNALS = None # This value should be larger than the duration of a single hit gap_ns_between_chunks = 10 def source_finished(self): return True def is_ready(self, chunk_i): return chunk_i < len(self.vetos_per_chunk) def compute(self, chunk_i): if chunk_i == 0: self._last_channel_was_off = self.start_with_channel_on n_vetos = self.vetos_per_chunk[chunk_i] res = np.zeros(n_vetos, self.dtype) # Add some randomly increasing times (larger than previous chunk res['time'] = self._last_endtime + 1 + np.cumsum( np.random.randint(low=2, high=self.veto_duration_max, size=n_vetos)) res['dt'] = 1 res['length'] = 1 if self._last_channel_was_off: # Previous chunk, we ended with an off, so now we start with an ON res['channel'][::2] = self.channel_on res['channel'][1::2] = self.channel_off starts = res[::2]['time'] stops = res[1::2]['time'] # Sum the time differences between starts and stops to get the deadtime self.TOTAL_DEADTIME += [np.sum(stops - starts[:len(stops)])] else: # Previous chunk, we ended with an ON, so now we start with an OFF res['channel'][1::2] = self.channel_on res['channel'][::2] = self.channel_off starts = res[1::2]['time'] stops = res[::2]['time'] # Ignore the first stop (stops last from previous chunk) self.TOTAL_DEADTIME += [ np.sum(stops[1:] - starts[:len(stops) - 1]) ] # Additionally, add the deadtime that spans the chunks (i.e. the first stop) # The gap_ns_between_chunks may look a bit magic, but it's correct if chunk_i != 0: self.TOTAL_DEADTIME += [self.gap_ns_between_chunks] self.TOTAL_DEADTIME += [stops[0] - self._last_endtime] # Track the total number of signals both ON and OFF self.TOTAL_SIGNALS += [n_vetos] previous_end = self._last_endtime self._last_endtime = res['time'][-1] + self.gap_ns_between_chunks self._last_channel_was_off = res['channel'][-1] == self.channel_off if len(res) > 1: assert np.sum(res['channel']) if chunk_i == len( self.vetos_per_chunk) - 1 and not self._last_channel_was_off: # There is one ON without an OFF at the end of the run in # the last chunk. This should fill all the way to the end as # deadtime: dt = self._last_endtime - res['time'][-1] self.TOTAL_DEADTIME += [dt] return self.chunk(start=previous_end, end=self._last_endtime, data=res)
import strax import numpy as np import tempfile import unittest import hypothesis from hypothesis import given @strax.takes_config( strax.Option(name='int_option', type=int, default=42), strax.Option(name='str_option', type=str, default='forty_two'), strax.Config(name='mixed', type=int, default=42), ) class DummyPlugin(strax.Plugin): depends_on = () provides = ('dummy_data', ) dtype = strax.dtypes.time_fields + [ (('Some data description', 'some_data_name'), np.int32), ] int_config = strax.Config(type=int, default=42) str_config = strax.Config(type=str, default='forty_two') class TestPluginConfig(unittest.TestCase): @staticmethod def get_plugin(config): with tempfile.TemporaryDirectory() as temp_dir: context = strax.Context( storage=strax.DataDirectory(temp_dir, deep_scan=True), config=config,
class DetectorSynchronization(strax.Plugin): """ Plugin which computes the synchronization delay between TPC and vetos. Reference: * xenon:xenonnt:dsg:mveto:sync_monitor """ __version__ = '0.0.3' depends_on = ('raw_records_aqmon', 'raw_records_aqmon_nv', 'raw_records_aux_mv') provides = 'detector_time_offsets' data_kind = 'detector_time_offsets' tpc_internal_delay = straxen.URLConfig( default={ '0': 4917, '020380': 10137 }, type=dict, track=True, help='Internal delay between aqmon and regular TPC channels ins [ns]') adc_threshold_nim_signal = straxen.URLConfig( default=500, type=int, track=True, help='Threshold in [adc] to search for the NIM signal') # This value is only valid for SR0: epsilon_offset = straxen.URLConfig( default=76, type=int, track=True, help='Measured missing offset for nveto in [ns]') sync_max_delay = strax.Config( default=11e3, help='max delay DetectorSynchronization [ns]') sync_expected_min_clock_distance = straxen.URLConfig( default=9.9e9, help='min clock distance DetectorSynchronization [ns]') sync_expected_max_clock_distance = straxen.URLConfig( default=10.1e9, help='max clock distance DetectorSynchronization [ns]') def infer_dtype(self): dtype = [] dtype += strax.time_fields dtype += [(('Time offset for nV to synchronize with TPC in [ns]', 'time_offset_nv'), np.int64), (('Time offset for mV to synchronize with TPC in [ns]', 'time_offset_mv'), np.int64)] return dtype def compute(self, raw_records_aqmon, raw_records_aqmon_nv, raw_records_aux_mv, start, end): rr_tpc = raw_records_aqmon rr_nv = raw_records_aqmon_nv rr_mv = raw_records_aux_mv extra_offset = 0 _mask_tpc = (rr_tpc['channel'] == AqmonChannels.GPS_SYNC) if not np.any(_mask_tpc): # For some runs in the beginning no signal has been acquired here. # In that case we have to add the internal DAQ delay as an extra offset later. _mask_tpc = (rr_tpc['channel'] == AqmonChannels.GPS_SYNC_AM) extra_offset = self.get_delay() hits_tpc = self.get_nim_edge(rr_tpc[_mask_tpc], self.config['adc_threshold_nim_signal']) hits_tpc['time'] += extra_offset _mask_mveto = (rr_mv['channel'] == AqmonChannels.GPS_SYNC_MV) hits_mv = self.get_nim_edge(rr_mv[_mask_mveto], self.config['adc_threshold_nim_signal']) _mask_nveto = rr_nv['channel'] == AqmonChannels.GPS_SYNC_NV hits_nv = self.get_nim_edge(rr_nv[_mask_nveto], self.config['adc_threshold_nim_signal']) nveto_extra_offset = 0 if not len(hits_nv): # During SR0 sync signal was not recorded properly for the # neutron-veto, hence take waveform itself as "hits". _mask_nveto &= rr_nv['record_i'] == 0 nveto_extra_offset = self.config['epsilon_offset'] hits_nv = rr_nv[_mask_nveto] hits_nv['time'] += nveto_extra_offset offsets_mv = self.estimate_delay(hits_tpc, hits_mv) offsets_nv = self.estimate_delay(hits_tpc, hits_nv) assert len(offsets_mv) == len( offsets_nv), 'Unequal number of sync signals!' result = np.zeros(len(offsets_mv), dtype=self.dtype) result['time'] = hits_tpc['time'] result['endtime'] = strax.endtime(hits_tpc) result['time_offset_nv'] = offsets_nv result['time_offset_mv'] = offsets_mv return result def get_delay(self): delay = 0 for run_id, _delay in self.config['tpc_internal_delay'].items(): if int(self.run_id) >= int(run_id): delay = _delay return delay @staticmethod def get_nim_edge(raw_records, threshold=500): records = strax.raw_to_records(raw_records) strax.baseline(records) hits = strax.find_hits(records, min_amplitude=threshold) return hits def estimate_delay(self, hits_det0, hits_det1): """ Function to estimate the average offset between two hits. """ err_value = -10000000000 offsets = [] prev_time = 0 for ind in range(len(hits_det0)): offset = self.find_offset_nearest(hits_det1['time'], hits_det0['time'][ind]) if ind: # Cannot compute time to prev for first event time_to_prev = hits_det0['time'][ind] - prev_time else: time_to_prev = 10e9 # Additional check to avoid spurious signals _correct_distance_to_prev_lock = time_to_prev >= self.sync_expected_min_clock_distance _correct_distance_to_prev_lock = time_to_prev < self.sync_expected_max_clock_distance if (abs(offset) < self.sync_max_delay) & _correct_distance_to_prev_lock: offsets.append(offset) prev_time = hits_det0['time'][ind] else: # Add err_value in case offset is not valid offsets.append(err_value) prev_time = hits_det0['time'][ind] return np.array(offsets) def find_offset_nearest(self, array, value): if not len(array): return -self.sync_max_delay array = np.asarray(array) idx = (np.abs(array - value)).argmin() return value - array[idx]