class LabIlluminationGenerator: def __init__(self, camera, extractor, pedestal, nsb_rate): self.camera = camera self.extractor = extractor self.pedestal = pedestal self.nsb_rate = nsb_rate pulse_area = self.camera.photoelectron_pulse.area spectrum_average = self.camera.photoelectron_spectrum.average pe_conversion = pulse_area * spectrum_average self.pe_conversion = pe_conversion self.n_pixels = self.camera.mapping.n_pixels self.n_superpixels = self.camera.mapping.n_superpixels self.signal_time = 40 # ns self.signal_index = self.camera.get_waveform_sample_from_time( self.signal_time) self.source = PhotoelectronSource(camera=self.camera) self.acquisition = EventAcquisition(camera=self.camera) self.illumination = 50 def set_illumination(self, illumination): self.illumination = illumination @property def event_table_layout(self): class EventTable(tables.IsDescription): n_triggers = tables.Int64Col(shape=self.n_superpixels) true_charge = tables.Int64Col(shape=self.n_pixels) measured_charge = tables.Float64Col(shape=self.n_pixels) # Event metadata illumination = tables.Float64Col() return EventTable def generate_event(self): nsb_pe = self.source.get_nsb(self.nsb_rate) signal_pe = self.source.get_uniform_illumination(self.signal_time, self.illumination, laser_pulse_width=3) true_charge = signal_pe.get_photoelectrons_per_pixel(self.n_pixels) readout = self.acquisition.get_continuous_readout(nsb_pe + signal_pe) trigger = self.acquisition.trigger digital_trigger = trigger.get_superpixel_digital_trigger_line(readout) n_triggers = trigger.get_n_superpixel_triggers(digital_trigger) waveform = self.acquisition.get_sampled_waveform(readout) measured_charge = self.extractor.extract(waveform, self.signal_index) calibrated_charge = (measured_charge - self.pedestal) / self.pe_conversion return dict( n_triggers=n_triggers, true_charge=true_charge, measured_charge=calibrated_charge, illumination=self.illumination, )
def _perform_sp_bias_scan(camera, nsb): """ While observing NSB only, scan through trigger threshold and measure superpixel trigger rate Parameters ---------- camera : Camera Description of the camera nsb : float NSB rate (Unit: MHz) Returns ------- thresholds : ndarray Thresholds sampled mean_rate : ndarray Average trigger rate at each threshold std_rate : ndarray Standard deviation of trigger rate at each threshold """ original_n_pixels = camera.mapping.n_pixels camera.mapping.reinitialise(4) # Only use 1 superpixel for this process source = PhotoelectronSource(camera=camera) acquisition = EventAcquisition(camera=camera) trigger = acquisition.trigger n_repeats = 10000 # Repeats for statistics n_thresholds = 50 thresholds = None # Delayed initialisation based on readout limits n_triggers = np.zeros((n_repeats, n_thresholds)) for iev in trange(n_repeats, desc="Measuring bias curve"): photoelectrons = source.get_nsb(nsb) readout = acquisition.get_continuous_readout(photoelectrons) # Define thresholds based on waveform readout if thresholds is None: min_ = readout.min() min_ = min_ if min_ >= 0 else 0 max_ = readout.max() * 5 max_ = max_ if max_ > 0 else 1 thresholds = np.linspace(min_, max_, n_thresholds) thresholds /= camera.photoelectron_pulse.height # Convert to p.e. # Scan through thresholds to count triggers for i, thresh in enumerate(thresholds): camera.update_trigger_threshold(thresh) digital_trigger = trigger.get_superpixel_digital_trigger_line(readout) n_triggers[iev, i] = trigger.get_n_superpixel_triggers(digital_trigger) n_triggers[:, n_triggers.sum(0) <= 1] = np.nan # Remove entries with low statistics n_triggers_avg = n_triggers.mean(0) n_triggers_err = np.sqrt(n_triggers_avg / n_repeats) rate_avg = n_triggers_avg / (camera.continuous_readout_duration * 1e-9) rate_err = n_triggers_err / (camera.continuous_readout_duration * 1e-9) camera.mapping.reinitialise(original_n_pixels) return thresholds, rate_avg, rate_err
def measure_opct(camera, plot_path=None): print("Measuring OPCT") original_n_pixels = camera.mapping.n_pixels camera.mapping.reinitialise(1) # Only use 1 pixel for this process camera.coupling.update_nsb_rate(0) # No NSB source = PhotoelectronSource(camera=camera) acquisition = EventAcquisition(camera=camera) illuminations = [1.0, 1.5, 2.0] n_illuminations = len(illuminations) n_events = 20000 # Simulate waveforms and extract charge charge_arrays = [] time = 20 extractor = ChargeExtractor.from_camera(camera) # Default extractor for illumination in illuminations: charge_array = np.zeros(n_events) for iev in range(n_events): pe = source.get_uniform_illumination(time, illumination) readout = acquisition.get_continuous_readout(pe) waveform = acquisition.get_sampled_waveform(readout) charge_array[iev] = extractor.extract(waveform, time)[0] charge_arrays.append(charge_array / camera.photoelectron_pulse.area) charge_containers = [ ChargeContainer(c, n_bins=100, range_=(-1, 8)) for c in charge_arrays ] # Fit spectra pdf = SiPMGentile(n_illuminations=n_illuminations) cost = BinnedNLL(pdf, charge_containers) values, errors = minimize_with_iminuit(cost) if plot_path is not None: values_array = np.array(list(values.values())) fig = plt.figure(figsize=(20, 10)) x = np.linspace(-1, 8, 100000) for i in range(n_illuminations): ax = fig.add_subplot(n_illuminations, 1, i+1) charge = charge_containers[i] ax.hist( charge.between, weights=charge.hist, bins=charge.edges, histtype='step', density=True ) fy = pdf(x, values_array, i) ax.plot(x, fy) print(f"Saving plot: {plot_path}") fig.savefig(plot_path) camera.mapping.reinitialise(original_n_pixels) return values['opct'], errors['opct']
def get_camera_and_readout(pulse_width, mv_per_pe, spectrum, nsb_rate): camera = Camera(continuous_readout_duration=int(2e5), n_waveform_samples=int(2e5), mapping=SSTCameraMapping(n_pixels=1), photoelectron_pulse=GaussianPulse(sigma=pulse_width, mv_per_pe=mv_per_pe), photoelectron_spectrum=spectrum) source = PhotoelectronSource(camera=camera, seed=1) acquisition = EventAcquisition(camera=camera, seed=1) pe = source.get_nsb(nsb_rate) return camera, acquisition.get_continuous_readout(pe)
def main(): normalise_charge = True spectra = { "No OCT": SiPMPrompt(opct=0, normalise_charge=normalise_charge), "Prompt OCT (20%)": SiPMPrompt(opct=0.2, normalise_charge=normalise_charge), "Delayed OCT (20%, τ=24ns)": SiPMDelayed(opct=0.2, time_constant=24, normalise_charge=normalise_charge), "Delayed OCT (55%, τ=24ns)": SiPMDelayed(opct=0.55, time_constant=24, normalise_charge=normalise_charge), } path = "/Users/Jason/Software/TargetCalib/source/dev/reference_pulse_checs_V1-1-0.cfg" ref_x, ref_y = np.loadtxt(path, unpack=True) pulse = GenericPulse(ref_x * 1e9, ref_y, mv_per_pe=4) p_waveform = WaveformPlot(talk=True) for name, spectrum in spectra.items(): camera = Camera( continuous_readout_duration=128, n_waveform_samples=128, mapping=SSTCameraMapping(n_pixels=1), photoelectron_spectrum=spectrum, photoelectron_pulse=pulse, ) source = PhotoelectronSource(camera=camera) acquisition = EventAcquisition(camera=camera) n_events = 100 waveform = np.zeros((n_events, 128)) for iev in trange(n_events): pe = source.get_uniform_illumination(30, 50) readout = acquisition.get_continuous_readout(pe) waveform[iev] = acquisition.get_sampled_waveform(readout)[0] waveform_avg = np.mean(waveform, 0) p_waveform.ax.plot(waveform_avg, label=name) p_waveform.add_legend('best') p_waveform.ax.set_xlabel("Time (ns)") p_waveform.ax.set_ylabel("Amplitude (mV)") p_waveform.ax.set_title("Average Pulse (50 p.e.)") p_waveform.save("pulse.pdf")
def obtain_pedestal(camera, extractor, nsb_rate=40): print("Obtaining pedestal") # Update the AC coupling for this nsb rate camera.coupling.update_nsb_rate(nsb_rate) source = PhotoelectronSource(camera=camera) acquisition = EventAcquisition(camera=camera) n_empty = 100 pedestal_array = np.zeros((n_empty, camera.mapping.n_pixels)) for i in trange(n_empty, desc="Measuring pedestal"): nsb_pe = source.get_nsb(nsb_rate) readout = acquisition.get_continuous_readout(nsb_pe) waveform = acquisition.get_sampled_waveform(readout) charge = extractor.extract(waveform, camera.n_waveform_samples//2) pedestal_array[i] = charge return np.median(pedestal_array)
def measure_50pe_pulse(camera): print("Measuring 50pe pulse") original_n_pixels = camera.mapping.n_pixels camera.mapping.reinitialise(1) # Only use 1 pixel for this process camera.coupling.update_nsb_rate(0) # No NSB source = PhotoelectronSource(camera=camera) acquisition = EventAcquisition(camera=camera) time = 20 illumination = 50 n_events = 100 readout = np.zeros((n_events, camera.continuous_readout_time_axis.size)) for iev in range(n_events): pe = source.get_uniform_illumination(time, illumination) readout[iev] = acquisition.get_continuous_readout(pe)[0] avg = readout.mean(0) camera.mapping.reinitialise(original_n_pixels) return camera.continuous_readout_time_axis, avg
def __init__(self, camera, extractor, pedestal, nsb_rate): self.camera = camera self.extractor = extractor self.pedestal = pedestal self.nsb_rate = nsb_rate pulse_area = self.camera.photoelectron_pulse.area spectrum_average = self.camera.photoelectron_spectrum.average pe_conversion = pulse_area * spectrum_average self.pe_conversion = pe_conversion self.n_pixels = self.camera.mapping.n_pixels self.n_superpixels = self.camera.mapping.n_superpixels self.signal_time = 40 # ns self.signal_index = self.camera.get_waveform_sample_from_time( self.signal_time) self.source = PhotoelectronSource(camera=self.camera) self.acquisition = EventAcquisition(camera=self.camera) self.illumination = 50
def __init__(self, path, camera, extractor, pedestal, nsb_rate): if not exists(path): raise ValueError(f"No path found: {self.path}") self.path = path self.camera = camera self.extractor = extractor self.pedestal = pedestal self.nsb_rate = nsb_rate self.n_pixels = self.camera.mapping.n_pixels self.reader = PhotoelectronReader(self.path) if self.n_pixels != 2048: print( "Warning: Full camera pixels not simulated for cherenkov events" ) self.source = PhotoelectronSource(camera=self.camera) self.acquisition = EventAcquisition(camera=self.camera)
def main(): spectrum = SiPMGentileSPE(opct=0.2, normalise_x=False) camera = Camera(photoelectron_spectrum=spectrum, mapping=SSTCameraMapping(n_pixels=1)) n_events = 200000 source = PhotoelectronSource(camera=camera) pe1 = [] pe50 = [] for iev in trange(n_events): pe1.append(source.get_uniform_illumination(20, 1).charge.sum()) pe50.append(source.get_uniform_illumination(20, 20).charge.sum()) pe1 = np.array(pe1) pe50 = np.array(pe50) # embed() plot = SpectrumPlot(sidebyside=True) plot.ax.hist( pe1, bins=70, histtype='step', label=f"λ = 1, Average Charge Per Event = {pe1.mean():.2f} f.c.") plot.ax.hist( pe50, bins=70, histtype='step', label=f"λ = 20, Average Charge Per Event = {pe50.mean():.2f} f.c.") # plot.ax.set_yscale('log') plot.ax.set_xlabel("Charge Per Event (f.c.)") plot.ax.set_ylabel("Number of Events") plot.add_legend('best') plot.save(get_plot("d201029_sipm_calib_doc/charge_mc.pdf")) spectrum = SiPMGentileSPE(opct=0.2, normalise_x=True) camera = Camera(photoelectron_spectrum=spectrum, mapping=SSTCameraMapping(n_pixels=1)) n_events = 200000 source = PhotoelectronSource(camera=camera) pe1 = [] pe50 = [] for iev in trange(n_events): pe1.append(source.get_uniform_illumination(20, 1).charge.sum()) pe50.append(source.get_uniform_illumination(20, 20).charge.sum()) pe1 = np.array(pe1) pe50 = np.array(pe50) # embed() plot = SpectrumPlot(sidebyside=True) plot.ax.hist( pe1, bins=70, histtype='step', label=f"λ = 1, Average Charge Per Event = {pe1.mean():.2f} p.e.") plot.ax.hist( pe50, bins=70, histtype='step', label=f"λ = 20, Average Charge Per Event = {pe50.mean():.2f} p.e.") # plot.ax.set_yscale('log') plot.ax.set_xlabel("Charge Per Event (p.e.)") plot.ax.set_ylabel("Number of Events") plot.add_legend('best') plot.save(get_plot("d201029_sipm_calib_doc/charge_pe.pdf"))
class CherenkovShowerGenerator: def __init__(self, path, camera, extractor, pedestal, nsb_rate): if not exists(path): raise ValueError(f"No path found: {self.path}") self.path = path self.camera = camera self.extractor = extractor self.pedestal = pedestal self.nsb_rate = nsb_rate self.n_pixels = self.camera.mapping.n_pixels self.reader = PhotoelectronReader(self.path) if self.n_pixels != 2048: print( "Warning: Full camera pixels not simulated for cherenkov events" ) self.source = PhotoelectronSource(camera=self.camera) self.acquisition = EventAcquisition(camera=self.camera) @property def event_table_layout(self): class EventTable(tables.IsDescription): n_triggers = tables.Int64Col(shape=1) signal_pe = tables.Int64Col(shape=self.n_pixels) signal_charge = tables.Float64Col(shape=self.n_pixels) peak_index = tables.Int64Col(shape=self.n_pixels) measured_charge = tables.Float64Col(shape=self.n_pixels) # Event metadata event_index = tables.UInt64Col() event_id = tables.UInt64Col() telescope_id = tables.UInt8Col() n_photoelectrons = tables.UInt64Col() energy = tables.Float64Col() alt = tables.Float64Col() az = tables.Float64Col() core_x = tables.Float64Col() core_y = tables.Float64Col() h_first_int = tables.Float64Col() x_max = tables.Float64Col() shower_primary_id = tables.UInt8Col() return EventTable def __iter__(self): for cherenkov_pe in self.reader: nsb_pe = self.source.get_nsb(self.nsb_rate) signal_pe = self.source.resample_photoelectron_charge(cherenkov_pe) signal_pe_per_pixel = signal_pe.get_photoelectrons_per_pixel( self.n_pixels) signal_charge_per_pixel = signal_pe.get_charge_per_pixel( self.n_pixels) readout = self.acquisition.get_continuous_readout(nsb_pe + signal_pe) n_triggers = self.acquisition.get_trigger(readout).size waveform = self.acquisition.get_sampled_waveform(readout) peak_index = self.extractor.obtain_peak_index_from_neighbours( waveform) measured_charge = self.extractor.extract(waveform, peak_index) yield dict( n_triggers=n_triggers, signal_pe=signal_pe_per_pixel, signal_charge=signal_charge_per_pixel, peak_index=peak_index, measured_charge=measured_charge, **signal_pe.metadata, )
def main(): camera = Camera( continuous_readout_duration=10000, mapping=SSTCameraMapping(n_pixels=1), photoelectron_spectrum=SiPMDelayed(opct=0.2, time_constant=24), # photoelectron_spectrum=PerfectPhotosensor(), ) source = PhotoelectronSource(camera=camera) nsb_rate = 2.3 n_events = 100000 dt = [] rng = np.random.RandomState(None) for iev in trange(n_events): pe = source.get_nsb(rate=nsb_rate) # pe = Photoelectrons( # pixel=np.zeros(1), # time=np.full(1, 20.), # charge=np.ones(1), # ) # pe = camera.photoelectron_spectrum.apply(pe, rng) time = np.sort(pe.time) # time = pe.time if time.size > 1: # embed() dt.append(time[1] - time[0]) # dt.append(time[1:] - time[0]) # if (np.diff(time) < 100).any(): # embed() # dt.append(np.diff(time)) # dt = dt[dt < 1000] # dt = (time - time[:, None]).ravel() # dt = dt[dt > 8] # embed() dt = np.array(dt) # dt = np.concatenate(dt) dt = dt[(dt > 0) & (dt < 1000)] hist, edges = np.histogram(dt, bins=200) between = (edges[1:] + edges[:-1]) / 2 def func(t, a0, tc0, a1, tc1): return a0 * 1 / tc0 * np.exp(t / -tc0) + a1 * 1 / tc1 * np.exp( t / -tc1) def cost_binned_nll(a0, tc0, a1, tc1): f_y = func(between, a0, tc0, a1, tc1) scale = np.sum(hist) / np.sum(f_y) return np.sum(_bin_nll(f_y * scale, hist)) def cost_ls(a0, tc0, a1, tc1): f_y = func(between, a0, tc0, a1, tc1) gt5 = hist > 5 return np.sum((hist[gt5] - f_y[gt5])**2 / hist[gt5]) def cost_unbinned_nll(a0, tc0, a1, tc1): if np.isnan(np.array([a0, tc0, a1, tc1])).any(): return np.inf f_y = func(dt, a0, tc0, a1, tc1) return -_sum_log_x(f_y) initial = dict(a0=1, tc0=30, a1=1, tc1=1e3 / nsb_rate) limits = dict(limit_a0=(0, None), limit_tc0=(1, None), limit_a1=(0, None), limit_tc1=(1, None)) fixed = dict( # fix_a0=True, # fix_tc0=True, # fix_a1=True, # fix_tc1=True ) m0 = iminuit.Minuit( cost_binned_nll, **initial, **limits, **fixed, errordef=0.5, print_level=0, pedantic=False, throw_nan=True, ) m0.migrad() m0.hesse() # # Attempt to run HESSE to compute parabolic errors. # with warnings.catch_warnings(): # warnings.simplefilter("ignore", iminuit.util.HesseFailedWarning) # m0.hesse() # embed() print(m0.values) print(m0.errors) # popt, pcov = curve_fit(func, between, hist) # embed() p_timesep = TimeSeperationPlot(talk=True) # p_timesep.ax.hist(dt, bins=100)#, density=True) # p_timesep.ax.plot(between, hist, '.') (_, caps, _) = p_timesep.ax.errorbar(between, hist, yerr=np.sqrt(hist), mew=1, capsize=1, elinewidth=0.5, markersize=2, linewidth=0.5, fmt='.', zorder=1) # p_timesep.ax.errorbar(between, hist, yerr=np.sqrt(hist)) f_y = func(between, *m0.values.values()) scale = np.sum(hist) / np.sum(f_y) p_timesep.ax.plot(between, f_y * scale) p_timesep.ax.text(0.1, 0.8, fr"$τ_S$={m0.values['tc0']:.2f}", transform=p_timesep.ax.transAxes) p_timesep.ax.text(0.7, 0.4, fr"$τ_L$={m0.values['tc1']:.2f}", transform=p_timesep.ax.transAxes) p_timesep.ax.set_yscale("log") p_timesep.ax.set_xlabel("Δt (ns)") p_timesep.save("timesep.pdf")