def make_single_pulse_event(self, **kwargs): event = datastructure.Event( n_channels=10, start_time=0, length=100, sample_duration=self.pax.config['DEFAULT']['sample_duration'] ) event.pulses.append(datastructure.Pulse(**kwargs)) return event
def get_events(self): for event_i in range(self.number_of_events): self.t.GetEntry(event_i) root_event = self.t.events event = datastructure.Event(n_channels=root_event.n_channels, start_time=root_event.start_time, sample_duration=root_event.sample_duration, stop_time=root_event.stop_time) self.set_python_object_attrs(root_event, event, self.config['fields_to_ignore']) yield event
def test_hitfinder(self): # Integration test for the hitfinder self.pax = core.Processor( config_names='XENON100', just_testing=True, config_dict={ 'pax': { 'plugin_group_names': ['test'], 'encoder_plugin': None, 'decoder_plugin': None, 'test': ['PulseProperties.PulseProperties', 'HitFinder.FindHits'] }, 'HitFinder.FindHits': { 'left_extension': 0, 'right_extension': 0 } }) for test_w, hit_bounds, pulse_min, pulse_max in ( # Keep in mind the hitfinder flips the pulse... [np.zeros(100), [], 0, 0], [np.ones(100), [], 0, 0], [-3 * np.ones(100), [], 0, 0], [self.peak_at(70, amplitude=-100, width=4), [[70, 73]], 0, 100], [ self.peak_at(70, amplitude=-100, width=4) + self.peak_at(80, amplitude=10, width=4), [[70, 73]], -10, 100 ], [ self.peak_at(70, amplitude=-100, width=4) + self.peak_at(80, amplitude=-100, width=4), [[70, 73], [80, 83]], 0, 100 ], ): e = datastructure.Event( n_channels=self.pax.config['DEFAULT']['n_channels'], start_time=0, sample_duration=self.pax.config['DEFAULT']['sample_duration'], stop_time=int(1e6), pulses=[ dict(left=0, raw_data=np.array(test_w).astype(np.int16), channel=1) ]) e = self.pax.process_event(e) self.assertEqual(hit_bounds, [[hit['left'], hit['right']] for hit in e.all_hits]) self.assertEqual(pulse_min, e.pulses[0].minimum) self.assertEqual(pulse_max, e.pulses[0].maximum) delattr(self, 'pax')
def convert_record(self, class_to_load_to, record): # We defined a nice custom init for event... ahem... now we have to do # cumbersome stuff... if class_to_load_to == datastructure.Event: result = datastructure.Event( n_channels=self.config['n_channels'], start_time=record['start_time'], stop_time=record['stop_time'], sample_duration=record['sample_duration']) else: result = class_to_load_to() for k, v in self._numpy_record_to_dict(record).items(): # If result doesn't have this attribute, ignore it # This happens for n_peaks etc. and attributes that have been removed if hasattr(result, k): setattr(result, k, v) return result
def test_concatenation(self): for pulse_bounds, concatenated_pulse_bounds in ( ([[0, 1], [4, 5]], [[0, 1], [4, 5]]), ([[0, 1], [2, 5]], [[0, 5]]), ([[0, 0], [1, 5]], [[0, 5]]), ([[0, 0], [1, 2], [3, 3], [4, 5]], [[0, 5]]), ([[0, 0], [1, 2], [3, 5]], [[0, 5]]), ([[0, 0], [1, 1], [3, 5]], [[0, 1], [3, 5]]), ([], []), ): e = datastructure.Event( n_channels=self.plugin.config['n_channels'], start_time=0, sample_duration=self.plugin.config['sample_duration'], stop_time=int(1e6), pulses=[ dict(left=l, right=r, channel=1) for l, r in pulse_bounds ]) e = self.plugin.transform_event(e) found_pulse_bounds = [[p.left, p.right] for p in e.pulses] self.assertEqual(concatenated_pulse_bounds, found_pulse_bounds)
def get_all_events_in_current_file(self): for line in self.current_file: yield datastructure.Event(**json.loads(line))
def make_pax_event(self): """Simulate PMT response to the queued photon signals Returns None if no photons have been queued else returns start_time (in units, ie ns), pmt waveform matrix # TODO: Account for random initial digitizer state wrt interaction? Where? """ log.debug("Now performing hitpattern to waveform conversion") start_time = int(time.time() * units.s) # Find out the duration of the event all_times = np.concatenate( list(self.arrival_times_per_channel.values())) if not len(all_times): log.warning("No photons to simulate: making a noise-only event") max_time = 0 else: max_time = np.concatenate( list(self.arrival_times_per_channel.values())).max() event = datastructure.Event( n_channels=self.config['n_channels'], start_time=start_time, stop_time=start_time + int(max_time + 2 * self.config['event_padding']), sample_duration=self.config['sample_duration']) # Ensure the event length is even (else it cannot be written to XED) if event.length() % 2 != 0: event.stop_time += self.config['sample_duration'] # Convenience variables dt = self.config['sample_duration'] dv = self.config['digitizer_voltage_range'] / 2**( self.config['digitizer_bits']) start_index = 0 end_index = event.length() - 1 pulse_length = end_index - start_index + 1 # Setup things for real noise simulation if self.config['real_noise_sample_size']: noise_sample_len = self.config['real_noise_sample_size'] available_noise_samples = self.noise_data.shape[ 1] // noise_sample_len needed_noise_samples = int( math.ceil(pulse_length / noise_sample_len)) noise_sample_mode = self.config.get('real_noise_sample_mode', 'incoherent') if noise_sample_mode == 'coherent': # Choose a single set of noise sample numbers for the event chosen_noise_sample_numbers = np.random.randint( 0, available_noise_samples - 1, needed_noise_samples) roll_number = np.random.randint(noise_sample_len) # Setup the lone-hit arrival_times_per_channel lone_hit_arrival_times_per_channels = self.lone_hits(max_time) # Build waveform channel by channel for channel, photon_detection_times in self.arrival_times_per_channel.items( ): # If the channel is dead, fake, or not in the TPC, we don't do anything. if (self.config['gains'][channel] == 0 or (self.config['pmt_0_is_fake'] and channel == 0) or channel not in self.config['channels_in_detector']['tpc']): continue photon_detection_times = np.array(photon_detection_times) # Add double photoelectron emission if len(photon_detection_times): n_dpe = np.random.binomial( len(photon_detection_times), p=self.config['p_double_pe_emision']) if n_dpe: dpe_times = np.random.choice(photon_detection_times, n_dpe, replace=False) photon_detection_times = np.concatenate( [photon_detection_times, dpe_times]) log.debug( "Simulating %d photons in channel %d (gain=%s, gain_sigma=%s)" % (len(photon_detection_times), channel, self.config['gains'][channel], self.config['gain_sigmas'][channel])) # Combine the lone-hits with the normal PMT pulses photon_detection_times = np.concatenate( (photon_detection_times, lone_hit_arrival_times_per_channels[channel])) gains = self.get_gains(channel, len(photon_detection_times)) # Add PMT afterpulses ap_times = [] ap_amplifications = [] ap_gains = [] # Get the afterpulse settings for this channel: # 1. If we have a specific afterpulse config for this channel, we use it # 2. Else we fall back to a default configuration # 3. If this does not exist, we don't make any afterpulses all_ap_data = self.config.get('pmt_afterpulse_types', []) if 'each_pmt_afterpulse_types' in self.config and channel in self.config[ 'each_pmt_afterpulse_types']: all_ap_data = self.config['each_pmt_afterpulse_types'][channel] for ap_data in all_ap_data.values(): if not ap_data: continue # print(ap_data) # How many photons will make this kind of afterpulse? n_afterpulses = np.random.binomial( n=len(photon_detection_times), p=ap_data['p']) if not n_afterpulses: continue # Find the time and gain of the afterpulses dist_kwargs = ap_data['time_parameters'] dist_kwargs['size'] = n_afterpulses ap_times.extend( np.random.choice(photon_detection_times, size=n_afterpulses, replace=False) + getattr(np.random, ap_data['time_distribution'])( **dist_kwargs)) # Afterpulse gains can be different from regular gains: sample an amplification factor if 'amp_mean' in ap_data: ap_amplifications.extend( truncated_gauss_rvs(my_mean=ap_data['amp_mean'], my_std=ap_data['amp_rms'], left_boundary=0, right_boundary=float('inf'), n_rvs=n_afterpulses)) else: ap_amplifications.extend([1.] * n_afterpulses) ap_gains.extend(self.get_gains(channel, n_afterpulses)) # Combine the afterpulses with the normal PMT pulses ap_gains = [x * y for x, y in zip(ap_gains, ap_amplifications)] gains = np.concatenate((gains, ap_gains)) photon_detection_times = np.concatenate( (photon_detection_times, ap_times)) # Add padding, sort (eh.. or were we already sorted? and is sorting necessary at all??) pmt_pulse_centers = np.sort(photon_detection_times + self.config['event_padding']) # Build the waveform pulse by pulse (bin by bin was slow, hope this is faster) # Compute offset & center index for each pe-pulse # 'index' refers to the (hypothetical) event waveform, as usual pmt_pulse_centers = np.array(pmt_pulse_centers, dtype=np.int) offsets = pmt_pulse_centers % dt center_index = ( pmt_pulse_centers - offsets) / dt # Absolute index in waveform of pe-pulse center center_index = center_index.astype(np.int) # Simulate an event-long waveform in this channel # Remember start padding has already been added to times, so just one padding in end_index current_wave = np.zeros(pulse_length) for i, _ in enumerate(pmt_pulse_centers): # Add some current for this photon pulse # Compute the integrated pmt pulse at various samples, then # do their diffs/dt generated_pulse = self.pmt_pulse_current(gain=gains[i], offset=offsets[i]) # +1 due to np.diff in pmt_pulse_current #???? left_index = center_index[i] - start_index + 1 left_index -= int(self.config['samples_before_pulse_center']) righter_index = center_index[i] - start_index + 1 righter_index += int(self.config['samples_after_pulse_center']) # Abandon the pulse if it goes the left/right boundaries if len(generated_pulse) != righter_index - left_index: raise RuntimeError( "Generated pulse is %s samples long, can't be inserted between %s and %s" % (len(generated_pulse), left_index, righter_index)) elif left_index < 0: log.debug("Invalid left index %s: can't be negative" % left_index) continue elif righter_index >= len(current_wave): log.debug( "Invalid right index %s: can't be longer than length of wave (%s)!" % (righter_index, len(current_wave))) continue current_wave[left_index:righter_index] += generated_pulse # Did you order some Gaussian current noise with that? if self.config['gauss_noise_sigmas']: # if the baseline fluc. is defined for each channel # use that in prior noise_sigma_current = self.config['gauss_noise_sigmas'][ channel] * self.config['gains'][channel] / dt current_wave += np.random.normal(0, noise_sigma_current, len(current_wave)) elif self.config['gauss_noise_sigma']: # / dt is for charge -> current conversion, as in pmt_pulse_current noise_sigma_current = self.config[ 'gauss_noise_sigma'] * self.config['gains'][channel] / dt, current_wave += np.random.normal(0, noise_sigma_current, len(current_wave)) # Convert from PMT current to ADC counts adc_wave = current_wave adc_wave *= self.config[ 'pmt_circuit_load_resistor'] # Now in voltage adc_wave *= self.config[ 'external_amplification'] # Now in voltage after amplifier adc_wave /= dv # Now in float ADC counts above baseline adc_wave = np.trunc(adc_wave) # Now in integer ADC counts "" "" # Could round instead of trunc... who cares? # PMT signals are negative excursions, so flip them. adc_wave = -adc_wave # Did you want to superpose onto real noise samples? if self.config['real_noise_file']: if noise_sample_mode != 'coherent': # For each channel, choose different noise sample numbers chosen_noise_sample_numbers = np.random.randint( 0, available_noise_samples - 1, needed_noise_samples) roll_number = np.random.randint(noise_sample_len) # Extract the chosen noise samples and concatenate them # Have to use a listcomp here, unless you know a way to select multiple slices in numpy? # -- yeah making an index list with np.arange would work, but honestly?? real_noise = np.concatenate([ self.noise_data[channel - self.channel_offset] [nsn * noise_sample_len:(nsn + 1) * noise_sample_len] for nsn in chosen_noise_sample_numbers ]) # Roll the noise samples by a fraction of the sample size, # to avoid same artifacts falling at the same point every time np.roll(real_noise, roll_number, axis=0) # Adjust the noise amplitude if needed, then add it to the ADC wave noise_amplitude = self.config.get('adjust_noise_amplitude', {}).get(str(channel), 1) if noise_amplitude != 1: # Determine a rough baseline for the noise, then adjust towards it baseline = np.mean(real_noise[:min(len(real_noise), 50)]) real_noise = baseline + noise_amplitude * (real_noise - baseline) adc_wave += real_noise[:pulse_length] else: # If you don't want to superpose onto real noise, # we should add a reference baseline adc_wave += self.config['digitizer_reference_baseline'] # Digitizers have finite number of bits per channel, so clip the signal. adc_wave = np.clip(adc_wave, 0, 2**(self.config['digitizer_bits'])) event.pulses.append( datastructure.Pulse(channel=channel, left=start_index, raw_data=adc_wave.astype(np.int16))) log.debug("Simulated pax event of %s samples length and %s pulses " "created." % (event.length(), len(event.pulses))) self.clear_signals_queue() return event
def decode_event(self, event_msgpack): event_dict = msgpack.unpackb(event_msgpack) # MessagePack returns byte keys, which we can't pass as keyword arguments # Unfortunately we have to duplicate this code in data_model too event_dict = {k.decode('ascii'): v for k, v in event_dict.items()} return datastructure.Event(**event_dict)