<jupyter_start><jupyter_text>Tutorial 5 : Charge ResolutionThis notebook demonstrates a very basic Charge Resolution investigation. It simulates a uniform illumination source, with no NSB and no electronic noise. The charge is extracted from the waveform by integrating the full waveform. Due to its simplicity, the result of the Charge Resolution only describes the photosensor Excess Noise Factor component and the poisson fluctions of the photons. This result is therefore analytically calculable from the `SPESpectrum`, and demonstrated alongside the final result.<jupyter_code>from sstcam_simulation import Camera, SSTCameraMapping, PhotoelectronSource, EventAcquisition
from CHECLabPy.utils.resolutions import ChargeResolution
from tqdm import tqdm, trange
import numpy as np
from matplotlib import pyplot as plt
%matplotlib inline
# Define the camera
camera = Camera(
    mapping=SSTCameraMapping(n_pixels=1),  # Only need a single pixel
    continuous_readout_duration=128,  # Only need a single-waveform's worth of readout
    n_waveform_samples=128,
)
n_pixels = camera.mapping.n_pixels
pixel_array = np.arange(n_pixels)
source = PhotoelectronSource(camera=camera)
acquisition = EventAcquisition(camera=camera)
charge_resolution = ChargeResolution(mc_true=True)
n_events = 100
for illumination in tqdm(np.logspace(1, 3, 100)):
    for iev in range(n_events):
        pe = source.get_uniform_illumination(time=60, illumination=illumination)
        readout = acquisition.get_continuous_readout(pe)
        waveform = acquisition.get_sampled_waveform(readout)
        
        # Charge Extraction
        measured_charge = waveform.sum(1)
        
        true_charge = pe.get_photoelectrons_per_pixel(n_pixels)
        charge_resolution.add(pixel_array, true_charge, measured_charge)
df, _ = charge_resolution.finish()
def bin_dataframe(df, n_bins=40):
    true = df['true'].values
    min_ = true.min()
    max_ = (true.max() // 500 + 1) * 500
    bins = np.geomspace(0.1, max_, n_bins)
    bins = np.append(bins, 10**(np.log10(bins[-1]) + np.diff(np.log10(bins))[0]))
    df['bin'] = np.digitize(true, bins, right=True) - 1

    log = np.log10(bins)
    between = 10**((log[1:] + log[:-1]) / 2)
    edges = np.repeat(bins, 2)[1:-1].reshape((bins.size-1 , 2))
    edge_l = edges[:, 0]
    edge_r = edges[:, 1]
    df['between'] = between[df['bin']]
    df['edge_l'] = edge_l[df['bin']]
    df['edge_r'] = edge_r[df['bin']]

    return df
df = bin_dataframe(df, n_bins=40)
df_mean = df.groupby('bin').mean()
bin_ = df_mean.index
x = df_mean['true'].values
y = df_mean['charge_resolution'].values

poisson_limit = np.sqrt(x) / x
enf_limit = np.sqrt(camera.photoelectron_spectrum.excess_noise_factor * x) / x

plt.plot(x, poisson_limit, label="Poisson Limit")
plt.plot(x, enf_limit, label="ENF Limit", color='black')
plt.plot(x, y, '.', label="From Waveform")
plt.legend(loc="best")
plt.xlabel("Number of Photoelectrons")
plt.ylabel(r"Fractional Charge Resolution $\frac{{\sigma_Q}}{{Q}}$")
plt.xscale('log')<jupyter_output><empty_output>
{'fraction_non_alphanumeric': 0.11954022988505747, 'fraction_numerical': 0.0180623973727422, 'mean_word_length': 4.812977099236641, 'pattern_counts': {'":': 0, '<': 5, '<?xml version=': 0, '>': 5, 'https://': 0, 'lorem ipsum': 0, 'www.': 0, 'xml': 0}, 'pii_count': 0, 'substrings_counts': 0, 'word_list_counts': {'cursed_substrings.json': 0, 'profanity_word_list.json': 0, 'sexual_word_list.json': 0, 'zh_pornsignals.json': 0}}
{'chain_length': '1', 'dir': 'jupyter-structured-clean-dedup', 'id': '114809', 'license': 'BSD-3-Clause', 'n_tokens_mistral': 1092, 'n_tokens_neox': 992, 'n_words': 253, 'path': 'tutorials/5_charge_resolution.ipynb', 'repo_name': 'thomasarmstrong/sstcam-simulation', 'size': '3045'}