Skip to content

Implement Pyrrha functionalities in Irene#953

Open
Ian0sborne wants to merge 55 commits into
next-exp:masterfrom
Ian0sborne:implement_pyrrha
Open

Implement Pyrrha functionalities in Irene#953
Ian0sborne wants to merge 55 commits into
next-exp:masterfrom
Ian0sborne:implement_pyrrha

Conversation

@Ian0sborne

Copy link
Copy Markdown
Contributor

This PR addresses the data processing issues cause by the SiPM thresholds in Irene. It retains standard Irene functionalities but also implements the new Pyrrha selection methods for SiPMs, which relies on a spatial selection rather than a charge threshold.

The user is free to decide how they would rather process the raw waveforms (using the old Irene or Pyrrha), as the config file allows for either method to be selected.

Ian0sborne and others added 30 commits February 25, 2026 16:27
Done for backwards compatibility, this function now mirrors that
of `calibrate_sipms()` before thresholding was removed.
This is the main controlling function, that decides which type of cut
will be applied.
This included the adjustment of the cuts, applying
`select_wfs_above_time_integrated_thr()` and the zeroing integration cut
before the waveform rebinning.
This is to avoid possible situations where `calibrate_sipms()` is used
outwith irene. `thr` is now a keyword argument to ensure that if a
threshold is applied it is done so with intent.
Initially this was done over the entire waveforms due to the reordering
of the cuts when applied in `build_sipm_responses()`. As suggested by
@Ian0sborne, it would be wise to move these cuts out of
`compute_and_write_pmaps()`
Comment on lines +145 to +150
if thr is None:
return cwfs
else:
# if you apply a threshold here, apply as usual
thr = to_col_vector(np.full(sipm_wfs.shape[0], thr))
return np.where(cwfs > thr, cwfs, 0)

@Ian0sborne Ian0sborne May 7, 2026

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Even though this is not used in the code, this is needed for 4 tests in calib_sensor_functions_test.py.

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Add another function that does the thresholds suppression, redo those tests doing the threshold suppression outside calibrate_sipms, and finally remove the else branch here

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The function charge_threshold_method() in reco/wfm_functions.py does the zero suppression step. Should I make a function that does just that? If I do this, do I remove the test that rely on zero suppression from calib_sensor_functions_test.py and implement them specifically for this new function?

Comment thread invisible_cities/calib/calib_sensors_functions.py Outdated

@gonzaponte gonzaponte left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I will take a deeper look, but here is a first go.

Comment thread invisible_cities/calib/calib_sensors_functions.py Outdated
Comment on lines 1217 to +1277


def waveform_integrator(limits):
def integrate_wfs(wfs):
return cf.spaced_integrals(wfs, limits)[:, ::2]
return integrate_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):
h5out, apply_cut, sipm_rwf_to_cal=None):

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think we need a better name for apply_cut. Think of something along the lines of "sipm selection algorithm"

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I agree, I'll do sipm_selection_algo, but I will change it last so there's no back and forth

Comment thread invisible_cities/reco/peak_functions.py Outdated
Comment on lines +84 to +91
if apply_cut is not None:
# apply cut before slicing and rebinning
(sipm_ids,
sipm_wfs) = apply_cut(sipm_wfs)
else:
# give all sipm ids as index if no cut is applied
sipm_ids = np.arange(sipm_wfs.shape[0])

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

What does apply_cut = None represent? I thought there were only 2 options: threshold or pyrrha

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think this is a leftover from when we were first making the implementation, it doesn't make sense anymore I agree

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Okay I removed the else clause and some tests for the peak_functions.py use to the None case for the apply_cut parameter, so now without the else fallback they fail

FAILED invisible_cities/reco/peak_functions_test.py::test_build_peak_development[S2-40-True] - TypeError: 'NoneType' object is not callable
FAILED invisible_cities/reco/peak_functions_test.py::test_find_peaks_s2_style - TypeError: 'NoneType' object is not callable
FAILED invisible_cities/reco/peak_functions_test.py::test_get_pmap - TypeError: 'NoneType' object is not callable

Should we redo the functions that are using apply_cut = None so that there is some parameter passes through?

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think there are only two or three options, but correct me if I'm wrong:

  • None meant "no selection", so everything is kept. You would need to add this option to CutAlgo
  • None meant "threshold selection". In this case you need to adapt the tests by explicitly sending the original default threshold parameters.
  • None meant "threshold selection but with some (non-defaul) specific value of the parameters (e.g. thr=0). You would need to tweak the tests to specify these values as well.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

None in this case means no cut is applied at all, I think the best option would be to implement this explicitly as an option.

I noticed that in the old version of the tests that failed above they set thr_sipm_s2 = -1, this is essentially equivalent to setting no threshold the way I see it.

If you agree I'll make an option for no threshold and that should fix things.

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yup. Go ahead.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Comment thread invisible_cities/reco/peak_functions_test.py Outdated
Comment thread invisible_cities/reco/wfm_functions.py Outdated
Comment thread invisible_cities/reco/wfm_functions.py Outdated
Comment thread invisible_cities/reco/wfm_functions.py
Comment thread invisible_cities/reco/wfm_functions.py Outdated
Comment thread invisible_cities/reco/wfm_functions.py Outdated
Comment thread invisible_cities/reco/wfm_functions.py

@gonzaponte gonzaponte left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

A few more comments. I still need to check a few things, but at least you can make some progress while I do that.

Comment thread invisible_cities/cities/components.py
Comment on lines +813 to +816
def apply_cutting_function(wfs):
return func(wfs)

return apply_cutting_function

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

this is the same as return func. you can also use the return statement directly in the if/elif branches.

Comment on lines +828 to +833
# assume that if the detector_db is None, you return sipm threshold as the number provided
if detector_db is None:
sipm_thr = thr_sipm
else:
# extract sipm threshold
sipm_thr = get_actual_sipm_thr(thr_sipm_type, thr_sipm, detector_db, run_number)

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

If you add comments, make them say something that is not obvious from the code. In this case, since it is more a description of the function overall (rather than an implementation detail or trick), I would use the docstring to explain what the function does.

-------
2D array of shape (n_sipms, n_time_bins) containing the input waveforms with entries below threshold set to zero.
"""
thr = to_col_vector(np.full(wfs.shape[0], zeroing_thr))

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
thr = to_col_vector(np.full(wfs.shape[0], zeroing_thr))
thr = np.full((wfs.shape[0], 1), zeroing_thr)

Comment on lines +197 to +201
# zero entries below threshold
zwfs = zero_wfs_below_threshold(wfs, zeroing_thr)

# returns selected ids and waveforms above integral
return select_wfs_above_time_integrated_thr(zwfs, integration_thr)

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

remove comments

Comment on lines +265 to +285
def apply_circular_padding(selected_ids_no_isolated : np.ndarray,
sipm_x : np.ndarray,
sipm_y : np.ndarray,
padding_radius : float) -> np.ndarray:
"""
Receives a list of SiPM IDs corresponding to the most energetic SiPMs clustered
near the event. For these SiPMs, creates circular padding of radius padding_radius,
selecting all SiPMs within that radius. Stores the union of all selected SiPMs.
Outputs the SiPM IDs which are relevant for a given event.

Parameters
----------
selected_ids_no_isolated : Boolean array of shape (n_sipms,) corresponding to the "relevant" SiPMs.
sipm_x : 1D array of shape (n_sipms,) containing the x positions of the SiPMs.
sipm_y : 1D array of shape (n_sipms,) containing the y positions of the SiPMs.
padding_radius : Distance threshold in mm used to create circular padding around selected SiPMs.

Returns
-------
sipm_ids_with_signal : Boolean array of shape (n_sipms,) where True indicates that the SiPM is selected.
"""

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Whenever possible, try to frame the function in a more general way. Here, you use variable names and an overall function explanation that is perhaps easy to follow if you read the whole Pyrrha implementation, but not on their own. It should be possible to read and interpret this function without understanding Pyrrha as a whole.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants