Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
69 commits
Select commit Hold shift + click to select a range
eff4479
add sipm selection function using median and std
Ian0sborne Feb 25, 2026
77c47ad
add sipm selection method using a charge threshold
Ian0sborne Feb 25, 2026
aa94415
add sipm selection function using top n most energetic sipms
Ian0sborne Feb 25, 2026
b41d2ca
add function that kills isolated sipms
Ian0sborne Feb 25, 2026
d636192
add function that creates circular padding around selected sipms
Ian0sborne Feb 25, 2026
2d5679c
add function that creates the overall sipm masks
Ian0sborne Feb 25, 2026
96cc2be
load detector geometry in
Ian0sborne Feb 25, 2026
4af4b02
remove thresholds from `calibrate_sipms()`
jwaiton Feb 25, 2026
416b618
create `CutAlgo()` class
jwaiton Feb 25, 2026
5a5677a
modify `charge_threshold_method()`
jwaiton Feb 25, 2026
742ed5f
Implement `threshold_sipm_selection()`
jwaiton Feb 25, 2026
6bb7dca
Include `apply_cutting_function()`
jwaiton Feb 25, 2026
3660704
Implement `apply_cut()` into irene
jwaiton Feb 25, 2026
6aa1c3b
Implement `apply_cut()` into `compute_and_write_pmaps()`
jwaiton Feb 25, 2026
0b3710c
Update default irene config
jwaiton Feb 25, 2026
05dd34a
Remove whitespaces, add newline
jwaiton Feb 25, 2026
56d7922
Allow `calibrate_sipms()` to apply threshold.
jwaiton Feb 27, 2026
55ca25f
Include `thr_sipm_s2` into `cutting_params`
jwaiton Feb 27, 2026
f51919b
Implement `thr_sipm_s2` in `charge_threshold_method()`
jwaiton Feb 27, 2026
e3e631d
Implement integration cut based on slices
jwaiton Feb 27, 2026
6c7846d
Adjust tests to account for `cut_params`
jwaiton Feb 27, 2026
705c38c
Implement modifiable cuts in hypathia
jwaiton Feb 27, 2026
3cc372a
Update docstrings, type annotation
jwaiton Feb 27, 2026
9f3602d
add pyrrha cutting method as an option in `CutAlgo` and `apply_cuttin…
Ian0sborne Mar 3, 2026
6a0724d
pass `load_db` parameters into `make_sipm_selection` function
Ian0sborne Mar 3, 2026
61127bf
add documentation to `make_sipm_selection`
Ian0sborne Mar 3, 2026
7eb8618
implement `spatial_selection_method` (previously named `make_sipm_sel…
Ian0sborne Mar 3, 2026
c9db07e
modify `spatial_selection_method` to return the ids of the selected S…
Ian0sborne Mar 4, 2026
8ebeeb3
implement waveform index slicing in `spatial_selection_method`
Ian0sborne Mar 4, 2026
2ad15f0
ammend function to return sliced waveforms
Ian0sborne Mar 4, 2026
b27f4a1
revert back to full waveform
Ian0sborne Mar 4, 2026
88afc48
implement `SiPMSelectionMethod` class to call specific selection meth…
Ian0sborne Mar 5, 2026
866c532
modify `pyrrha_sipm_selection` and `spatial_selection_method` to use …
Ian0sborne Mar 5, 2026
7f8812e
include another method for initial SiPM selection and raise an error …
Ian0sborne Mar 5, 2026
7770a71
remove time index dependency of SiPM cutting functions
Ian0sborne Mar 10, 2026
83abad7
remove `select_wf_slices_above_time_integrated_thr`
Ian0sborne Mar 10, 2026
c862256
update docstrings
Ian0sborne Mar 10, 2026
1b4dbd7
remove unused variable from hypathia
Ian0sborne May 7, 2026
d9659d5
raise error for incorrect cutting function input
Ian0sborne May 7, 2026
3145200
add fixture and test for `charge_threshold_method()`
Ian0sborne May 7, 2026
d3448e3
add test for `charge_threshold_method()`
Ian0sborne May 7, 2026
7844412
simplify naming
Ian0sborne May 9, 2026
f3d8374
add test for `top_n_method`
Ian0sborne May 9, 2026
82092fe
add fixture and test for `kill_isolated_sipms()`
Ian0sborne May 10, 2026
cc158da
amend `apply_circular_padding()` to not kill SiPMs when padding is se…
Ian0sborne May 10, 2026
b4c3057
add test for `apply_circular_padding()` and modify fixture to accomod…
Ian0sborne May 10, 2026
549e3de
add `zero_wfs_below_threshold()` to zero wavefrom entries below a thr…
Ian0sborne May 11, 2026
c63d9d1
update `calibrate_sipms()` and corresponding test it to rely on `zero…
Ian0sborne May 11, 2026
952fffd
add test for `zero_wfs_below_threshold()` with small tweak in fixture
Ian0sborne May 11, 2026
6493950
Change comment position
Ian0sborne Jun 5, 2026
f5a939a
Update `SiPMCalibMode` name to match new functionality
Ian0sborne Jun 5, 2026
4ebae1e
Remove default parameters in `wfm_functions.py`
Ian0sborne Jun 5, 2026
b895612
Update & improve docstrings
Ian0sborne Jun 5, 2026
0780c5c
Remove trailing underscores
Ian0sborne Jun 8, 2026
59fb6c1
Implement `no_cut` option in cut algorithms
Ian0sborne Jun 8, 2026
cc79f2b
Improve function name
Ian0sborne Jul 3, 2026
a941a42
Remove unnecessary comments
Ian0sborne Jul 8, 2026
1cc8367
Clean up `select_cutting_algorithm()`
Ian0sborne Jul 8, 2026
37f5449
Make docstrings self contained
Ian0sborne Jul 9, 2026
3cdc975
Implement `CutAlgo.no_cut` as default parameter
Ian0sborne Jul 9, 2026
476c5d7
Remove unused `thr_sipm*` params
Ian0sborne Jul 9, 2026
9d752da
Guard against zero-suppressed data
Ian0sborne Jul 13, 2026
7153a25
Improve `zero_wfs_below_threshold()`
Ian0sborne Jul 14, 2026
4c01f8e
Add Irene config with Pyrrha settings
Ian0sborne Jul 16, 2026
249da17
Change `apply_cut` to `sipm_selection_algo`
Ian0sborne Jul 16, 2026
3a1f8e7
Fix parameter ordering
Ian0sborne Jul 16, 2026
3471923
Fix indentation
Ian0sborne Jul 23, 2026
6116960
Improve docstrings
Ian0sborne Jul 23, 2026
814e1dc
Remove unused imports
Ian0sborne Jul 23, 2026
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
38 changes: 20 additions & 18 deletions invisible_cities/calib/calib_sensors_functions.py
Original file line number Diff line number Diff line change
Expand Up @@ -59,8 +59,9 @@ def modes (wfs): return to_col_vector(mode (wfs, axis=1))

def subtract_baseline(wfs, *, bls_mode=BlsMode.mean):
"""
Subtract the baseline to all waveforms in the input
with a specific algorithm.
Subtract the baseline from all waveforms in the input
with a specific algorithm, leaving individual samples
that are already 0 untouched.

Parameters
----------
Expand All @@ -75,16 +76,19 @@ def subtract_baseline(wfs, *, bls_mode=BlsMode.mean):
Returns
-------
bls: np.ndarray with shape (n, m)
Baseline-subtracted waveforms.
Baseline-subtracted waveforms, with originally-zero
samples left as 0.
"""

if bls_mode is BlsMode.mean : return wfs - means (wfs)
elif bls_mode is BlsMode.median : return wfs - medians (wfs)
elif bls_mode is BlsMode.mode : return wfs - modes (wfs)
elif bls_mode is BlsMode.scipymode: return wfs - scipy_mode(wfs, axis=1)
if bls_mode is BlsMode.mean : baseline = means (wfs)
elif bls_mode is BlsMode.median : baseline = medians (wfs)
elif bls_mode is BlsMode.mode : baseline = modes (wfs)
elif bls_mode is BlsMode.scipymode: baseline = scipy_mode(wfs, axis=1)
else:
raise TypeError(f"Unrecognized baseline subtraction option: {bls_mode}")

return np.where(wfs == 0, 0, wfs - baseline) # subtracts the baseline to non-zero samples only


def calibrate_wfs(wfs, adc_to_pes):
"""
Expand Down Expand Up @@ -136,15 +140,13 @@ def pmt_subtract_maw(cwfs, n_maw=100):
return cwfs - maw


def calibrate_sipms(sipm_wfs, adc_to_pes, thr, *, bls_mode=BlsMode.mode):
def calibrate_sipms(sipm_wfs, adc_to_pes, *, bls_mode=BlsMode.mode):
"""
Subtracts the baseline, calibrates waveforms to pes
and suppresses values below `thr` (in pes).
Subtract baseline, and calibrates waveforms to pes.
"""
thr = to_col_vector(np.full(sipm_wfs.shape[0], thr))
bls = subtract_baseline(sipm_wfs, bls_mode=bls_mode)
cwfs = calibrate_wfs(bls, adc_to_pes)
return np.where(cwfs > thr, cwfs, 0)
return cwfs


def subtract_mean (wfs): return subtract_baseline(wfs, bls_mode=BlsMode.mean )
Expand All @@ -159,10 +161,10 @@ def sipm_subtract_median_and_calibrate(sipm_wfs, adc_to_pes): return calibrate_w

# Dict of functions for SiPM processing
sipm_processing = {
SiPMCalibMode.subtract_mode : subtract_mode ,# For gain extraction
SiPMCalibMode.subtract_median : subtract_median ,# For gain extraction
SiPMCalibMode.subtract_mode_calibrate : sipm_subtract_mode_and_calibrate ,# For PDF calculation
SiPMCalibMode.subtract_mean_calibrate : sipm_subtract_mean_and_calibrate ,# For PDF calculation
SiPMCalibMode.subtract_median_calibrate: sipm_subtract_median_and_calibrate,# For PDF calculation
SiPMCalibMode.subtract_mode_zs : calibrate_sipms # For data processing
SiPMCalibMode.subtract_mode : subtract_mode ,# For gain extraction
SiPMCalibMode.subtract_median : subtract_median ,# For gain extraction
SiPMCalibMode.subtract_mode_calibrate : sipm_subtract_mode_and_calibrate ,# For PDF calculation
SiPMCalibMode.subtract_mean_calibrate : sipm_subtract_mean_and_calibrate ,# For PDF calculation
SiPMCalibMode.subtract_median_calibrate : sipm_subtract_median_and_calibrate,# For PDF calculation
SiPMCalibMode.subtract_baseline_calibrate: calibrate_sipms # For data processing
}
28 changes: 15 additions & 13 deletions invisible_cities/calib/calib_sensors_functions_test.py
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@

from . import calib_sensors_functions as csf
from .. core import core_functions as cf
from .. reco import wfm_functions as wfm
from .. sierpe import fee as FE
from .. sierpe import waveform_generator as wfg

Expand Down Expand Up @@ -207,19 +208,20 @@ def test_calibrate_sipms_stat(oscillating_waveform_with_baseline,
baseline ) = oscillating_waveform_with_baseline
#n_maw = n_samples // 500

ccwfs = csf.calibrate_sipms(wfs, adc_to_pes, nsigma * noise_sigma, bls_mode=BlsMode.mode)
ccwfs = csf.calibrate_sipms(wfs, adc_to_pes, bls_mode=BlsMode.mode)
zeroed_ccwfs = wfm.zero_wfs_below_threshold(ccwfs, zeroing_thr=nsigma * noise_sigma)

number_of_zeros = np.count_nonzero(ccwfs == 0)
assert number_of_zeros > fraction * ccwfs.size
number_of_zeros = np.count_nonzero(zeroed_ccwfs == 0)
assert number_of_zeros > fraction * zeroed_ccwfs.size


def test_calibrate_sipms_common_threshold(toy_sipm_signal):
(signal_adc, adc_to_pes,
signal_zs_common_threshold, _,
common_threshold, _) = toy_sipm_signal

zs_wf = csf.calibrate_sipms(signal_adc, adc_to_pes,
common_threshold, bls_mode=BlsMode.mode)
ccwf = csf.calibrate_sipms(signal_adc, adc_to_pes, bls_mode=BlsMode.mode)
zs_wf = wfm.zero_wfs_below_threshold(ccwf, zeroing_thr=common_threshold)

for actual, expected in zip(zs_wf, signal_zs_common_threshold):
assert actual == approx(expected)
Expand All @@ -230,10 +232,9 @@ def test_calibrate_sipms_individual_thresholds(toy_sipm_signal):
_, signal_zs_individual_thresholds,
_, individual_thresholds) = toy_sipm_signal

ccwf = csf.calibrate_sipms(signal_adc, adc_to_pes, bls_mode=BlsMode.mode)
zs_wf = wfm.zero_wfs_below_threshold(ccwf, zeroing_thr=individual_thresholds)

zs_wf = csf.calibrate_sipms(signal_adc, adc_to_pes,
individual_thresholds,
bls_mode=BlsMode.mode)
for actual, expected in zip(zs_wf, signal_zs_individual_thresholds):
assert actual == approx(expected)

Expand Down Expand Up @@ -309,11 +310,12 @@ def test_area_of_sum_equals_sum_of_areas_pmts(square_pmt_and_sipm_waveforms):

def test_area_of_sum_equals_sum_of_areas_sipms(square_pmt_and_sipm_waveforms):
_, nsensors, _, _, _, sipms_wfm, _ = square_pmt_and_sipm_waveforms
adc_to_pes = np.full(nsensors, 100, dtype=float)
cwfs = csf.calibrate_sipms(sipms_wfm, adc_to_pes, thr=10, bls_mode=BlsMode.mode)
stot = np.sum(cwfs[0]) * nsensors
sums = np.sum(cwfs, axis=1)
stot2 = reduce(add, sums)
adc_to_pes = np.full(nsensors, 100, dtype=float)
cwfs = csf.calibrate_sipms(sipms_wfm, adc_to_pes, bls_mode=BlsMode.mode)
zeroed_cwfs = wfm.zero_wfs_below_threshold(cwfs, zeroing_thr=10)
stot = np.sum(zeroed_cwfs[0]) * nsensors
sums = np.sum(zeroed_cwfs, axis=1)
stot2 = reduce(add, sums)
assert stot == approx(stot2, rel=1e-3)


Expand Down
85 changes: 77 additions & 8 deletions invisible_cities/cities/components.py
Original file line number Diff line number Diff line change
Expand Up @@ -84,6 +84,8 @@
from .. types .ic_types import types_dict_summary
from .. types .ic_types import types_dict_tracks
from .. types .symbols import WfType
from .. types .symbols import CutAlgo
from .. types .symbols import SiPMSelectionMethod
from .. types .symbols import RebinMethod
from .. types .symbols import SiPMCharge
from .. types .symbols import BlsMode
Expand Down Expand Up @@ -762,7 +764,8 @@ def sensor_data(path, wf_type):

def build_pmap(detector_db, run_number, pmt_samp_wid, sipm_samp_wid,
s1_lmax, s1_lmin, s1_rebin_stride, s1_stride, s1_tmax, s1_tmin,
s2_lmax, s2_lmin, s2_rebin_stride, s2_stride, s2_tmax, s2_tmin, thr_sipm_s2):
s2_lmax, s2_lmin, s2_rebin_stride, s2_stride, s2_tmax, s2_tmin,
sipm_selection_algo):
s1_params = dict(time = minmax(min = s1_tmin,
max = s1_tmax),
length = minmax(min = s1_lmin,
Expand All @@ -783,8 +786,8 @@ def build_pmap(detector_db, run_number, pmt_samp_wid, sipm_samp_wid,
sipm_ids = np.argwhere(datasipm.Active.values==1).flatten()
def build_pmap(ccwf, s1_indx, s2_indx, sipmzs): # -> PMap
return pkf.get_pmap(ccwf, s1_indx, s2_indx, sipmzs,
s1_params, s2_params, thr_sipm_s2, pmt_ids, sipm_ids,
pmt_samp_wid, sipm_samp_wid)
s1_params, s2_params, pmt_ids, sipm_ids,
pmt_samp_wid, sipm_samp_wid, sipm_selection_algo)

return build_pmap

Expand All @@ -802,20 +805,85 @@ def calibrate_pmts(cwf):# -> CCwfs:
return calibrate_pmts


def calibrate_sipms(dbfile, run_number, thr_sipm):
def calibrate_sipms(dbfile, run_number):
DataSiPM = load_db.DataSiPM(dbfile, run_number)
DataSiPM = DataSiPM.loc[lambda df: df.Active==1]
adc_to_pes = np.abs(DataSiPM.adc_to_pes.values)

def calibrate_sipms(rwf):
return csf.calibrate_sipms(rwf,
adc_to_pes = adc_to_pes,
thr = thr_sipm,
bls_mode = BlsMode.mode)

return calibrate_sipms


def select_cutting_algorithm(algo, **cutting_params):
if algo is CutAlgo.threshold:
return threshold_sipm_selection(**cutting_params)
elif algo is CutAlgo.pyrrha:
return pyrrha_sipm_selection(**cutting_params)
elif algo is CutAlgo.no_cut:
return no_cut_sipm_selection()
else:
raise ValueError(f"Unsupported cutting algorithm: {algo!r}. Expected one of {list(CutAlgo)}")


def threshold_sipm_selection( thr_sipm_type
, thr_sipm
, thr_sipm_s2
, run_number
, detector_db = None):
'''
Applies energy thresholds to SiPM S2 waveforms:
- thr_sipm: applied per time bin. Waveform samples below this
threshold are set to zero.
- thr_sipm_s2: applied to the integrated waveform charge. If the total
charge is below this threshold, it is set to zero.
'''
if detector_db is None:
sipm_thr = thr_sipm
else:
sipm_thr = get_actual_sipm_thr(thr_sipm_type, thr_sipm, detector_db, run_number)

def threshold_sipm_selection(wfs):
return wfm.charge_threshold_method(wfs, zeroing_thr = sipm_thr, integration_thr=thr_sipm_s2)

return threshold_sipm_selection


def pyrrha_sipm_selection( selection_method : SiPMSelectionMethod
, selection_kwargs : dict
, proximity_threshold : float
, padding_radius : float
, run_number : int
, detector_db : str):
'''
Applies a generic selection function to the sipms, which can be used to
implement a spatial SiPM selection method (called Pyrrha).
'''
def pyrrha_sipm_selection(wfs):
return wfm.spatial_selection_method(wfs,
selection_method,
selection_kwargs,
proximity_threshold,
padding_radius,
run_number,
detector_db)

return pyrrha_sipm_selection


def no_cut_sipm_selection():
""""
Function that applies no cuts to the SiPM waveforms.
"""
def no_cut_sipm_selection(wfs):
sipm_ids = np.arange(wfs.shape[0])
return sipm_ids, wfs
return no_cut_sipm_selection


def calibrate_with_mean(dbfile, run_number):
DataSiPM = load_db.DataSiPM(dbfile, run_number)
adc_to_pes = np.abs(DataSiPM.adc_to_pes.values)
Expand Down Expand Up @@ -1231,8 +1299,8 @@ def integrate_wfs(wfs):
# Compound components
def compute_and_write_pmaps(detector_db, run_number, pmt_samp_wid, sipm_samp_wid,
s1_lmax, s1_lmin, s1_rebin_stride, s1_stride, s1_tmax, s1_tmin,
s2_lmax, s2_lmin, s2_rebin_stride, s2_stride, s2_tmax, s2_tmin, thr_sipm_s2,
h5out, sipm_rwf_to_cal=None):
s2_lmax, s2_lmin, s2_rebin_stride, s2_stride, s2_tmax, s2_tmin,
h5out, sipm_selection_algo, sipm_rwf_to_cal=None):

# Filter events without signal over threshold
indices_pass = fl.map(check_nonempty_indices,
Expand All @@ -1243,7 +1311,8 @@ def compute_and_write_pmaps(detector_db, run_number, pmt_samp_wid, sipm_samp_wid
# Build the PMap
compute_pmap = fl.map(build_pmap(detector_db, run_number, pmt_samp_wid, sipm_samp_wid,
s1_lmax, s1_lmin, s1_rebin_stride, s1_stride, s1_tmax, s1_tmin,
s2_lmax, s2_lmin, s2_rebin_stride, s2_stride, s2_tmax, s2_tmin, thr_sipm_s2),
s2_lmax, s2_lmin, s2_rebin_stride, s2_stride, s2_tmax, s2_tmin,
sipm_selection_algo),
args = ("ccwfs", "s1_indices", "s2_indices", "sipm"),
out = "pmap")

Expand Down
63 changes: 35 additions & 28 deletions invisible_cities/cities/hypathia.py
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,7 @@
from .. io .run_and_event_io import run_and_event_writer
from .. io . trigger_io import trigger_writer
from .. types.symbols import WfType
from .. types.symbols import CutAlgo
from .. types.symbols import SiPMThreshold

from .. dataflow import dataflow as fl
Expand All @@ -48,36 +49,38 @@
from . components import calibrate_sipms
from . components import get_actual_sipm_thr
from . components import sensor_masker
from . components import select_cutting_algorithm

from typing import Dict
from typing import Any


@city
def hypathia( files_in : OneOrManyFiles
, file_out : str
, compression : str
, event_range : EventRangeType
, print_mod : int
, detector_db : str
, run_number : int
, sipm_noise_cut : float
, filter_padding : int
, thr_sipm : float
, thr_sipm_type : SiPMThreshold
, pmt_wfs_rebin : int
, pmt_pe_rms : float
, s1_lmin : int , s1_lmax : int
, s1_tmin : float, s1_tmax : float
, s1_rebin_stride : int , s1_stride : int
, thr_csum_s1 : float
, s2_lmin : int , s2_lmax : int
, s2_tmin : float, s2_tmax : float
, s2_rebin_stride : int , s2_stride : int
, thr_csum_s2 : float, thr_sipm_s2 : float
, pmt_samp_wid : float
, sipm_samp_wid : float
def hypathia( files_in : OneOrManyFiles
, file_out : str
, compression : str
, event_range : EventRangeType
, print_mod : int
, detector_db : str
, run_number : int
, sipm_noise_cut : float
, filter_padding : int
, pmt_wfs_rebin : int
, pmt_pe_rms : float
, s1_lmin : int , s1_lmax : int
, s1_tmin : float, s1_tmax : float
, s1_rebin_stride : int , s1_stride : int
, thr_csum_s1 : float
, s2_lmin : int , s2_lmax : int
, s2_tmin : float, s2_tmax : float
, s2_rebin_stride : int , s2_stride : int
, thr_csum_s2 : float
, pmt_samp_wid : float
, sipm_samp_wid : float
, cutting_function : CutAlgo
, cutting_params : Dict[str, Any]
):

sipm_thr = get_actual_sipm_thr(thr_sipm_type, thr_sipm, detector_db, run_number)

#### Define data transformations
sd = sensor_data(files_in[0], WfType.mcrd)

Expand Down Expand Up @@ -115,9 +118,13 @@ def hypathia( files_in : OneOrManyFiles
item="sipm")

# SiPMs calibration
sipm_rwf_to_cal = fl.map(calibrate_sipms(detector_db, run_number, sipm_thr),
sipm_rwf_to_cal = fl.map(calibrate_sipms(detector_db, run_number),
item = "sipm")

# apply function depending on user input, from provided list of functions
sipm_selection_algo = select_cutting_algorithm(cutting_function, **cutting_params)


event_count_in = fl.spy_count()
event_count_out = fl.spy_count()

Expand All @@ -136,8 +143,8 @@ def hypathia( files_in : OneOrManyFiles
compute_pmaps, empty_indices, empty_pmaps = compute_and_write_pmaps(
detector_db, run_number, pmt_samp_wid, sipm_samp_wid,
s1_lmax, s1_lmin, s1_rebin_stride, s1_stride, s1_tmax, s1_tmin,
s2_lmax, s2_lmin, s2_rebin_stride, s2_stride, s2_tmax, s2_tmin, thr_sipm_s2,
h5out, sipm_rwf_to_cal)
s2_lmax, s2_lmin, s2_rebin_stride, s2_stride, s2_tmax, s2_tmin,
h5out, sipm_selection_algo, sipm_rwf_to_cal)

result = push(source = wf_from_files(files_in, WfType.mcrd),
pipe = pipe(fl.slice(*event_range, close_all=True),
Expand Down
Loading
Loading