Conversation
Codecov Report❌ Patch coverage is
Additional details and impacted files@@ Coverage Diff @@
## main #164 +/- ##
==========================================
+ Coverage 68.98% 69.63% +0.64%
==========================================
Files 33 33
Lines 2631 2720 +89
==========================================
+ Hits 1815 1894 +79
- Misses 816 826 +10 ☔ View full report in Codecov by Sentry. 🚀 New features to boost your workflow:
|
There was a problem hiding this comment.
Pull request overview
This pull request adds a new method to extract full waveforms directly from a pulse shape library (PSL) for HPGe detectors. The implementation enables emulating ideal waveforms in Python for model tuning purposes, though it is slower than the existing maximum_current function since it computes the full waveform rather than just finding the maximum.
Changes:
- Added
waveform_from_pulse_shape_library()function and supporting_get_psl_waveforms_impl()to extract full waveforms from pulse shape libraries - Made the
t0field optional inget_hpge_pulse_shape_library()to support pulse shape libraries without explicit t0 values - Added test coverage for the new waveform extraction functionality
Reviewed changes
Copilot reviewed 3 out of 3 changed files in this pull request and generated 7 comments.
| File | Description |
|---|---|
| src/reboost/hpge/psd.py | Implements new functions to extract full waveforms from pulse shape library by summing energy-weighted templates |
| src/reboost/hpge/utils.py | Makes t0 field optional in pulse shape library loading with default value of 0 and adds default empty units for waveform field |
| tests/hpge/test_waveforms.py | Adds basic test to verify waveform extraction produces correct summed values |
💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.
There was a problem hiding this comment.
Pull request overview
Copilot reviewed 4 out of 4 changed files in this pull request and generated 7 comments.
💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.
|
@copilot in the pulse shape libraries we typically store two waveforms on two angles 0 and 45 deg. Typically there is a repeating symmetry every 45 deg. |
Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>
Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>
Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>
Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>
Co-authored-by: tdixon97 <56904179+tdixon97@users.noreply.github.com>
Co-authored-by: tdixon97 <56904179+tdixon97@users.noreply.github.com>
Co-authored-by: tdixon97 <56904179+tdixon97@users.noreply.github.com>
…D array Co-authored-by: tdixon97 <56904179+tdixon97@users.noreply.github.com>
…0_deg) Co-authored-by: tdixon97 <56904179+tdixon97@users.noreply.github.com>
There was a problem hiding this comment.
Pull request overview
Copilot reviewed 6 out of 6 changed files in this pull request and generated 5 comments.
💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.
| if has_phi and phi_t is not None: | ||
| phi_idx = _get_phi_idx(phi_t[j], phi_grid) |
There was a problem hiding this comment.
When the pulse shape library is 4D (r,z,phi,time) but phi is not provided, this falls through to the else branch and indexes pulse_shape_library[r_idx, z_idx] (shape (phi,time)), which cannot be added into waveforms[i] (shape (time,)) and will raise a broadcasting error. If the intended behavior is to default to the first phi template when phi is omitted (as the tests/docstring suggest), handle has_phi with phi is None by using phi_idx=0 (or explicitly selecting a default phi) instead of the 3D indexing path.
| if has_phi and phi_t is not None: | |
| phi_idx = _get_phi_idx(phi_t[j], phi_grid) | |
| if has_phi: | |
| # If phi_t is not provided but the pulse shape library has a phi | |
| # dimension, default to the first phi template (index 0). | |
| if phi_t is not None: | |
| phi_idx = _get_phi_idx(phi_t[j], phi_grid) | |
| else: | |
| phi_idx = 0 |
| else: | ||
| t0 = 0 | ||
| t0_u = "ns" | ||
|
|
||
| t0_u = data["t0"].attrs["units"] | ||
| dt = data["dt"].value | ||
| dt_u = data["dt"].attrs["units"] |
There was a problem hiding this comment.
t0 is optional now, but when it is missing you hardcode t0_u = "ns" and later set tu = t0_u. If the file’s dt units are not ns (e.g. us), times = t0 + ... * dt will be numerically correct but the returned t_units (and implied units for times) will be wrong. Consider setting t0_u = dt_u (or tu = dt_u) when t0 is absent, and keep the unit-consistency check strict.
| if "units" not in data[phi_field].attrs: | ||
| data[phi_field].attrs["units"] = "" | ||
| wf = np.nan_to_num( | ||
| data[phi_field].view_as("np", with_units=True), nan=out_of_bounds_val | ||
| ) | ||
| waveform_list.append(wf.m) | ||
|
|
||
| # Stack into 4D array (r, z, phi, time) | ||
| waveforms_4d = np.stack(waveform_list, axis=2) | ||
| phi_array = np.array(phi_angles) | ||
| else: | ||
| # No phi-dependent fields, use the base field | ||
| if "units" not in data[field].attrs: | ||
| data[field].attrs["units"] = "" | ||
| wf = np.nan_to_num(data[field].view_as("np", with_units=True), nan=out_of_bounds_val) | ||
| waveforms_4d = wf.m |
There was a problem hiding this comment.
This function mutates the on-disk LGDO object (data[...].attrs["units"] = "") as a fallback when the units attribute is missing. That side effect can be surprising and may not be supported for all LGDO backends. Prefer handling missing units without modifying data (e.g., read without units and treat as dimensionless, or use a local default when interpreting the array).
| if "units" not in data[phi_field].attrs: | |
| data[phi_field].attrs["units"] = "" | |
| wf = np.nan_to_num( | |
| data[phi_field].view_as("np", with_units=True), nan=out_of_bounds_val | |
| ) | |
| waveform_list.append(wf.m) | |
| # Stack into 4D array (r, z, phi, time) | |
| waveforms_4d = np.stack(waveform_list, axis=2) | |
| phi_array = np.array(phi_angles) | |
| else: | |
| # No phi-dependent fields, use the base field | |
| if "units" not in data[field].attrs: | |
| data[field].attrs["units"] = "" | |
| wf = np.nan_to_num(data[field].view_as("np", with_units=True), nan=out_of_bounds_val) | |
| waveforms_4d = wf.m | |
| if "units" in data[phi_field].attrs: | |
| # Field has units: read as quantity and use magnitudes | |
| wf_q = np.nan_to_num( | |
| data[phi_field].view_as("np", with_units=True), | |
| nan=out_of_bounds_val, | |
| ) | |
| waveform_list.append(wf_q.m) | |
| else: | |
| # Field has no units: read as plain numpy array and treat as dimensionless | |
| wf_arr = np.nan_to_num( | |
| data[phi_field].view_as("np", with_units=False), | |
| nan=out_of_bounds_val, | |
| ) | |
| waveform_list.append(wf_arr) | |
| # Stack into 4D array (r, z, phi, time) | |
| waveforms_4d = np.stack(waveform_list, axis=2) | |
| phi_array = np.array(phi_angles) | |
| else: | |
| # No phi-dependent fields, use the base field | |
| if "units" in data[field].attrs: | |
| wf_q = np.nan_to_num( | |
| data[field].view_as("np", with_units=True), | |
| nan=out_of_bounds_val, | |
| ) | |
| waveforms_4d = wf_q.m | |
| else: | |
| wf_arr = np.nan_to_num( | |
| data[field].view_as("np", with_units=False), | |
| nan=out_of_bounds_val, | |
| ) | |
| waveforms_4d = wf_arr |
| waveforms = waveform_from_pulse_shape_library(edep, r, z, pulse_shape_library_with_phi) | ||
|
|
||
| assert waveforms.to_numpy().shape == (1, 100) |
There was a problem hiding this comment.
This test claims that omitting phi on a phi-enabled library defaults to the first phi template, but it only asserts the output shape. Add an assertion on the waveform values (e.g., that the result matches the 0° template scaling) so the intended behavior is actually verified.
| phi_grid=pulse_shape_library.phi, | ||
| ) | ||
|
|
||
| return units.attach_units(ak.Array(waveforms), "keV") |
There was a problem hiding this comment.
This function returns a "current waveform" but attaches units of "keV" unconditionally. Since the template/library waveform amplitude is generally unitless/arbitrary (and maximum_current explicitly returns unitless current), attaching energy units here is misleading and can cause downstream unit-conversion confusion. Consider returning an array without units (or, if waveform units are meaningful, store and propagate the waveform units from the library).
| return units.attach_units(ak.Array(waveforms), "keV") | |
| return ak.Array(waveforms) |
@SalehGiovanna this would allow you to emulate a full (ideal) waveform in python.
This is rather slow and probably not something we will use for post-processing but it might be good for tuning the models.
This is a few orders of magnitude faster than SSD directly but still quite alot slower than
maximum_current. This is because it computes the full waveform not only searches the maximum.Possible enhancements are to consider the phi-dependence, interpolate templates between time-steps etc