Skip to content

Straxen vanilla#1643

Draft
cfuselli wants to merge 75 commits intomasterfrom
daq-vanilla
Draft

Straxen vanilla#1643
cfuselli wants to merge 75 commits intomasterfrom
daq-vanilla

Conversation

@cfuselli
Copy link
Member

@cfuselli cfuselli commented Feb 3, 2026

This pull request introduces new "vanilla" versions of several key plugins and updates the context configuration to support them, primarily for the XENONnT experiment data processing. The changes include the addition of new plugins for merged S2 signals and peak position reconstruction, updates to context options to select between standard and vanilla processing chains, and various improvements and fixes in plugin dependencies and dtype handling.

New vanilla plugins and context options:

  • Added MergedS2sVanilla and MergedS2sHighEnergyVanilla plugins, along with their imports and registration, to provide alternative algorithms for merging S2 signals. [1] [2] [3] [4]
  • Introduced PeakPositionsMLPVanilla and PeakPositionsCNFVanilla plugins for peak position reconstruction using MLP and CNF algorithms, respectively. [1] [2]
  • Updated context configuration in straxen/contexts.py to add common_opts_vanilla and logic in xenonnt_online for selecting vanilla plugins, with additional debug print statements. [1] [2] [3]

Plugin dependency and dtype handling improvements:

  • Updated PeaksVanilla to depend on peaklet_classification instead of enhanced_peaklet_classification, and improved dtype merging and alignment for compatibility with numba. [1] [2]
  • Improved dtype alignment and merging logic in PeaksSOM plugin to prevent numba errors.
  • Commented out the merged field in vanilla peak and event basics plugins to match the new vanilla processing chain. [1] [2] [3] (!!!!) This field we would actually like to add, but I did not manage to do it well!

Bug fixes and minor changes:

  • Fixed a bug in is_the_led_on to use the last document in a list instead of the first.
  • Minor formatting and import improvements in various files. [1] [2]

These changes collectively enable an alternative "vanilla" data processing path in the XENONnT analysis framework, with improved plugin modularity and maintainability.


References:

@cfuselli cfuselli marked this pull request as ready for review February 4, 2026 15:30
@cfuselli
Copy link
Member Author

cfuselli commented Feb 4, 2026

@WenzDaniel @dachengx I think if you would like to have a look at the changes most things make sense to me, and I see no issues or conflicts with the default pipeline. Just adding Vanilla/Online related stuff (plus scripts changes that are just DAQ stuff). Only thing still to fix is this merged field I removed for now.

Copy link
Collaborator

@WenzDaniel WenzDaniel left a comment

Choose a reason for hiding this comment

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

Thanks Carlo I think it looks good. I have not checked if you copied the code correctly, but I assume you did. Neither did I test process some data, but here I assume you did as well. I cannot really judge the ajax and bootstrax changes though. Below you can find a few smaller questions from my side. I think generally the base is good to iterate from here.

("tight_coincidence", np.int16, "channel within tight range of mean"),
("n_saturated_channels", np.int16, "total number of saturated channels"),
("merged", bool, "is merged from peaklets"),
# ("merged", bool, "is merged from peaklets"),
Copy link
Collaborator

Choose a reason for hiding this comment

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

Can you elaborate what the difficulty is? Is it because it is only in S2 and not S1?

Copy link
Member Author

Choose a reason for hiding this comment

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

Simply I didn't have the time to manage to make it work with the MergedS2sVanilla. Just some issues with dtypes and stuff, and I didnt know how to assign its value. It should not be easy to add again. Maybe @dachengx do you want to give it a quick try?

if isinstance(run_doc, list) and isinstance(run_doc[0], dict):
# Extract the dictionary
doc = run_doc[0]
doc = run_doc[-1]
Copy link
Collaborator

Choose a reason for hiding this comment

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

Why this change?

Copy link
Collaborator

Choose a reason for hiding this comment

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

There can be more than one document?

Copy link
Member Author

Choose a reason for hiding this comment

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

The code in this plugin is actually misleading, what is called run_doc in the code is actually the comment of the rundoc (see the URLConfig, it calls comments on run_doc). So because sometimes the comment is wrong, you can updated it from the DAQ website, and this way we take the last one. So you can fix a comment for a run that would not work.

@@ -54,7 +54,7 @@ def infer_dtype(self):
np.int16,
),
(("Classification of the peak(let)", "type"), np.int8),
Copy link
Collaborator

Choose a reason for hiding this comment

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

Even if it does not work at event basics we can still leave it here is not it?

Copy link
Member Author

Choose a reason for hiding this comment

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

you mean the merged field? if we dont propagate it better to remove it..

Comment on lines +21 to +38
depends_on: Union[Tuple[str, ...], str] = (
"peaklets",
"enhanced_peaklet_classification",
"merged_s2s",
)

def infer_dtype(self):
# In case enhanced_peaklet_classification has more fields than peaklets,
# we need to merge them
peaklets_dtype = self.deps["peaklets"].dtype_for("peaklets")
peaklet_classification_dtype = self.deps["enhanced_peaklet_classification"].dtype_for(
"enhanced_peaklet_classification"
)
merged_dtype = strax.merged_dtype((peaklets_dtype, peaklet_classification_dtype))
# Numba is very picky about alignment for structured dtypes. Ensure an aligned dtype
# so assignments inside strax.replace_merged don't see "unaligned array(...)".
return np.dtype(merged_dtype, align=True)

Copy link
Collaborator

Choose a reason for hiding this comment

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

Is this a AI addition? Can you elaborate?

Copy link
Member Author

Choose a reason for hiding this comment

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

its from the peaks vanilla plugin... but I had to change the name of the deps.
this part:

#numba is very picky about alignment for structured dtypes. Ensure an aligned dtype
# so assignments inside strax.replace_merged don't see "unaligned array(...)".
return np.dtype(merged_dtype, align=True)

I dont remember if the unaligned array was a temporary error I had or if still needed..

Comment on lines +58 to +69
# # strax.replace_merged is numba-jitted and requires the two inputs to have the
# # exact same (and aligned) structured dtype. Some recent dtype-merging paths
# # produce unaligned dtypes, which triggers TypingError in numba.
# common_dtype = self.dtype

# _peaklets = np.zeros(len(peaklets), dtype=common_dtype)
# strax.copy_to_buffer(peaklets, _peaklets, f"_cast_{self.provides[0]}_peaklets")

# _merged_s2s = np.zeros(len(merged_s2s), dtype=common_dtype)
# strax.copy_to_buffer(merged_s2s, _merged_s2s, f"_cast_{self.provides[0]}_merged_s2s")

# _peaks = self.replace_merged(_peaklets, _merged_s2s, merge_s0=self.merge_s0)
Copy link
Collaborator

Choose a reason for hiding this comment

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

Can this go? I assume that the copying is not needed anymore because of this unaligned/aligned thing. I would be still interested to understand what the problem was.

Copy link
Member Author

Choose a reason for hiding this comment

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

yes this can go. Dont remember about the unaligned issue..

Copy link
Member Author

Choose a reason for hiding this comment

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

Numba failing inside strax.replace_merged because it’s being fed an unaligned structured array dtype:
• Key clue: setitem(unaligned array(Record(...)), ...)
• Numba’s jitted _replace_merged does assignments like result[result_i] = merge[window_i], and it refuses when the structured dtype is unaligned (or when orig and merge are not exactly the same aligned dtype).

this was it

Comment on lines +78 to +80
straxen.MergedS2s.save_when = SaveWhen.TARGET
straxen.PeakletClassificationVanilla.save_when = SaveWhen.TARGET
straxen.PeakletPositionsCNF.save_when = SaveWhen.TARGET
Copy link
Collaborator

Choose a reason for hiding this comment

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

Is it not possible to adjust the plugins themselves? Or does this collide with OSG reprocessing?

Copy link
Member Author

Choose a reason for hiding this comment

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

Yeah, I am not sure why they were set like this, maybe needed for reprocessing?
This needs to be coordinated well with DAQ, Transfers, Reprocessing..

Comment on lines +329 to +334
if _from_cutax and xedocs_version != "global_ONLINE":
context_options = {**straxen.contexts.common_opts, **kwargs}
else:
context_options = {**straxen.contexts.common_opts_vanilla, **kwargs}

st = straxen.contexts.xenonnt(**context_options)
Copy link
Collaborator

Choose a reason for hiding this comment

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

This implies that we will never move back to the other plugins for online processing? I think it make sense, but was this discussed with a wider audience? However, if this is the case we also should not make this if/else statement for the "online" context, but always register the vanilla plugins.

Copy link
Member Author

Choose a reason for hiding this comment

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

mmm "always" means in this straxen version, if this will change it would be anyway with a new straxen version where we can tune again the online context.
at the moment, i think its good to keep online from straxen using vanilla, and leave the option to use the common_opts if specifying online from cutax? idk.. this we can decide

@cfuselli
Copy link
Member Author

Thank you @WenzDaniel for the careful review.
I answered all the comments and implemented the changes where needed.
On a few points we might need to discuss a bit more..

cfuselli and others added 5 commits February 11, 2026 21:27
Merging Option A implementation - matches non-vanilla architecture
The merged field assignment in PeakletClassificationVanilla.compute()
was breaking PeakletClassificationSOM which inherits from it.

Problem:
- PeakletClassificationSOM inherits from PeakletClassificationVanilla
- SOM plugin uses its own dtype (without merged field)
- But calls parent's compute() which tried to set merged=False
- Result: ValueError: no field of name merged (92 tests failed)

Solution:
- Make the assignment conditional: only set if field exists in dtype
- This allows vanilla plugins to set it, SOM plugins to skip it safely

This fixes all test failures in the standard xenonnt context while
maintaining the merged field functionality in xenonnt_online context.
Also make merged field assignments conditional in:
- merged_s2s_vanilla.py (line 159)
- peaks_vanilla.py (line 80)

These were still setting merged unconditionally, causing failures
when SOM plugins inherit from vanilla but don't have merged in dtype.

This completes the fix for all 92+ test failures.
* Add CI testing for xenonnt_online context

This adds a new pytest_online job that runs the full test suite using
the xenonnt_online context (vanilla plugins) instead of the default
xenonnt context (SOM plugins).

Changes:
- tests/plugins/_core.py: Read STRAXEN_TEST_CONTEXT env var to select context
- .github/workflows/pytest.yml: Add pytest_online to test matrix

This will help catch vanilla plugin bugs (like the merged field issue)
before merging to production. The online context is what DAQ actually
uses, so direct testing is important.

Test matrix after this change:
- pytest_py3.10: xenonnt context
- pytest_py3.11: xenonnt context
- pytest_no_database_py3.10: xenonnt context
- pytest_online_py3.10: xenonnt_online context (NEW)
- coveralls_py3.10: xenonnt context

* Add pytest-instafail for immediate test failure reporting

Adds --instafail -vv --tb=short flags to all pytest runs to show
failures immediately as they happen instead of at the end.

This makes CI debugging much faster - you can see what's failing
without waiting for the entire test suite to finish.

* [pre-commit.ci] auto fixes from pre-commit.com hooks

for more information, see https://pre-commit.ci

* Exclude MLP plugins from xenonnt_online tests

MLP position reconstruction plugins require model files that are not
available in CI when using ONLINE corrections. Skip these plugins
for xenonnt_online context tests to avoid download failures.

The vanilla plugin logic is still tested with other plugins.

* [pre-commit.ci] auto fixes from pre-commit.com hooks

for more information, see https://pre-commit.ci

* Fix: Set MLP models to None instead of excluding plugins

Setting tf_model_mlp=None allows MLP plugins to run (returning NaN)
without downloading model files. This preserves the dependency chain
while avoiding model download issues in xenonnt_online CI tests.

* [pre-commit.ci] auto fixes from pre-commit.com hooks

for more information, see https://pre-commit.ci

* Refactor: Test vanilla plugins using xenonnt context

Instead of using xenonnt_online (which requires ONLINE corrections),
test vanilla plugins by adding a _vanilla parameter to xenonnt() context.

Changes:
- Add _vanilla parameter to xenonnt() to use common_opts_vanilla
- Add use_vanilla parameter to nt_test_context()
- Rename pytest_online → pytest_vanilla in CI
- Use STRAXEN_USE_VANILLA env var instead of STRAXEN_TEST_CONTEXT

This avoids ONLINE corrections and xedocs model download issues while
still testing vanilla plugin logic.

* Use _vanilla flag in xenonnt_online context

Make xenonnt_online use the _vanilla parameter when calling xenonnt()
instead of manually merging common_opts. This simplifies the logic and
ensures consistency.

* Clean up redundant if/elif logic

* Fix data_top support in vanilla and exclude SE score tests

Changes:
- Fix merged_s2s_vanilla.py: use store_data_top=True instead of digitize_top=True
- Exclude SE score plugins from vanilla tests (advanced feature not supported)
- SE score tests will be skipped for vanilla context

This should fix test_sum_wf while properly excluding SE score tests.

* Fix SE score test exclusion at test generation time

The auto-generated tests are created at import time, before setUpClass runs.
We need to check the STRAXEN_USE_VANILLA env var and exclude SE score
plugins when generating the test list, not just when running setup.

This properly prevents SE score tests from being generated for vanilla context.

* [pre-commit.ci] auto fixes from pre-commit.com hooks

for more information, see https://pre-commit.ci

* Skip test_sum_wf for vanilla plugins

The test_sum_wf checks the store_data_top feature, which requires data_top
to be computed during peaklet creation. Vanilla plugins can't retroactively
compute data_top values in merged_s2s.

Reverted the store_data_top change in merged_s2s_vanilla as it doesn't
actually fill data_top with values, just creates an empty field.

Vanilla plugins work correctly without this advanced feature.

* [pre-commit.ci] auto fixes from pre-commit.com hooks

for more information, see https://pre-commit.ci

* Re-enable test_sum_wf for vanilla

Peaklets already has store_data_top=True by default for both SOM and vanilla.
Since vanilla uses the same Peaklets plugin, data_top should be properly
computed during peaklet creation.

The merged_s2s_vanilla code that checks for data_top is just a fallback
for edge cases. With store_data_top=True, vanilla should pass this test.

* Fix indentation error in test_sum_wf

Removed duplicate lines that were causing IndentationError

* Enable store_data_top for vanilla contexts

Root cause: merged_s2s_vanilla adds empty data_top field (zeros) as fallback
when Peaklets doesn't compute it. But those zeros never get filled.

Solution: Explicitly set store_data_top=True for vanilla contexts so
Peaklets computes data_top during peaklet creation.

This makes test_sum_wf pass by ensuring data_top contains actual
top PMT waveform data instead of zeros.

* Move store_data_top config to common_opts_vanilla

Previous approach set config after context creation, which didn't
affect the plugin initialization properly.

Moving it to common_opts_vanilla ensures it's part of the context
options from the start, so strax includes it in the lineage hash
and properly initializes plugins with this config.

* Fix config conflict in xenonnt()

common_opts_vanilla now has config key, but xenonnt() was also
passing config= to Context(). This caused 'multiple values for
keyword argument config' error.

Fixed by merging common_config with opts['config'] before passing
to Context().

* [pre-commit.ci] auto fixes from pre-commit.com hooks

for more information, see https://pre-commit.ci

* Skip test_sum_wf for vanilla (data_top issue)

Despite setting store_data_top=True in common_opts_vanilla config,
the data_top field still contains zeros instead of actual top PMT data.

Root cause unclear - needs deeper investigation of why Peaklets plugin
isn't properly computing data_top even with config set.

Skipping this one test to unblock CI. Vanilla tests (319 other tests)
should now pass.

* Fix precommit issues

1. Add type annotation to exclude_plugins_vanilla to fix mypy error
2. Use raw string (r""") for RegexDispatcher docstring to fix
   SyntaxWarning about invalid escape sequences

* Simplify vanilla config - remove store_data_top logic

---------

Co-authored-by: pre-commit-ci[bot] <66853113+pre-commit-ci[bot]@users.noreply.github.com>
@cfuselli cfuselli requested a review from dachengx February 12, 2026 13:24
@cfuselli
Copy link
Member Author

Since the last review, I've added CI testing for vanilla plugins..

What changed:

  • Added _vanilla parameter to xenonnt() context to switch between SOM/vanilla plugins
  • Added pytest_vanilla CI job that runs all plugin tests with vanilla
  • Simple implementation - just one line to choose the plugin set

Test results:
The skipped/excluded ones:

  • SE score tests (2): these plugins don't exist in vanilla
  • test_sum_wf (1): tests the store_data_top feature which has an issue in vanilla where data_top contains zeros instead of actual waveform data. This is an edge case that needs deeper investigation.

Overall the vanilla plugins work well including the new merged field (not sure if its implementation makes sense though)

…n vanilla

Root cause: merged_s2s_vanilla had two bugs:
1. Line 115: Used invalid parameter 'digitize_top=True' (should be 'store_data_top=True')
2. After merging peaks, properties were not recomputed, leaving area_fraction_top=0

Impact: All merged S2 peaks in vanilla had area_fraction_top=0, affecting:
- S1/S2 discrimination
- Position reconstruction
- Energy reconstruction

Fix:
1. Corrected parameter to store_data_top=True
2. Added strax.compute_properties() call after merge to recompute all peak properties
3. Removed test skip - test_sum_wf now passes for vanilla

This fixes a critical data quality issue caught by the test.
@cfuselli
Copy link
Member Author

Ok I figured out what was going on with the area_fraction_top test failing!

The Problem:
The test_sum_wf was failing because area_fraction_top was being set to zero for all merged S2 peaks in vanilla. The pattern was interesting - every other peak had zero (unmerged peaks were fine, merged S2s had zeros).

Root Cause:
Found two bugs in merged_s2s_vanilla.py:

  1. Line 115 typo: Used digitize_top=True which isn't even a valid parameter (should be store_data_top=True)

  2. Missing recomputation: After strax.merge_peaks() creates new peak arrays with np.zeros(), the old peak data gets copied over BUT fields like area_fraction_top are initialized to zero and never recomputed. The SOM version calls strax.compute_properties() to fix this, vanilla didn't.

Impact:
This is actually a critical data quality bug! All merged S2 peaks in vanilla had area_fraction_top=0, which breaks:

  • S1/S2 discrimination
  • Position reconstruction
  • Energy reconstruction

The Fix:

  1. Fixed the parameter: digitize_top=Truestore_data_top=True
  2. Added strax.compute_properties(merged_s2s, n_top_channels=self.n_top_pmts) after merging to properly recompute all peak properties
  3. Removed the test skip - now test_sum_wf passes for vanilla!

Good thing we added these tests, they caught a real bug! 🐛

@cfuselli
Copy link
Member Author

Here the context comparison between "offline" and "online" contexts, meaning "default" vs "vanilla" plugins.

straxen_vanilla_offline straxen_vanilla_online

@cfuselli
Copy link
Member Author

I changed the way we start the mongo instance in the tests because it was giving:

  docker: Error response from daemon: client version 1.40 is too old. Minimum supported API version is 1.44, please upgrade your client to a newer version.

cfuselli and others added 4 commits February 13, 2026 16:40
When store_data_start=False is set in Peaklets, the peaklets don't have
the data_start field. When MergedS2sVanilla adds the data_top field, it
was incorrectly checking if the OUTPUT (merged_s2s) should have data_start
instead of checking if the INPUT (peaklets) has it.

This caused a crash in strax.merge_peaks() when it tried to access
p['data_start'] on peaklets that don't have that field.

Fix: Check peaklets.dtype.names instead of self.dtype_for('merged_s2s').names
Strax's numba-compiled merge_peaks cannot handle dtypes where optional
fields are missing - numba fails to compile any code that accesses
p['data_start'] even if behind an 'if store_data_start:' guard.

Workaround: Always include data_start field in buffer (stays zeros if
not used). This adds ~800 bytes per merged S2, but avoids compilation
errors. Field will be properly filled when peaklets have data_start,
and stay zeros when they don't.

TODO: Remove this workaround once strax properly supports optional fields
via function overloading or separate code paths.
@cfuselli cfuselli marked this pull request as draft February 17, 2026 17:00
cfuselli and others added 2 commits March 2, 2026 11:42
* what if we do only cnf on daq

* [pre-commit.ci] auto fixes from pre-commit.com hooks

for more information, see https://pre-commit.ci

* fkn precommit

* fix tests

* [pre-commit.ci] auto fixes from pre-commit.com hooks

for more information, see https://pre-commit.ci

* fix tests

* explicitly register normal peak positions

* [pre-commit.ci] auto fixes from pre-commit.com hooks

for more information, see https://pre-commit.ci

* explicitly register normal peak positions

---------

Co-authored-by: pre-commit-ci[bot] <66853113+pre-commit-ci[bot]@users.noreply.github.com>
* what if we do only cnf on daq

* [pre-commit.ci] auto fixes from pre-commit.com hooks

for more information, see https://pre-commit.ci

* fkn precommit

* fix tests

* [pre-commit.ci] auto fixes from pre-commit.com hooks

for more information, see https://pre-commit.ci

* fix tests

* explicitly register normal peak positions

* [pre-commit.ci] auto fixes from pre-commit.com hooks

for more information, see https://pre-commit.ci

* explicitly register normal peak positions

* Add peak-only CNF position reconstruction for low-memory online processing

- Create PeakPositionsCNFPeakOnly plugin that computes positions from peak area_per_channel
- Does not depend on peaklet_positions, avoiding loading of all peaklet waveforms
- Reduces RAM usage by ~27 GB (1.5M peaklets × 1KB × 15 workers)
- Based on v2.2.7 approach (PeakPositionsBaseNT)
- Add _low_memory_positions parameter to xenonnt() context
- Enable by default in xenonnt_online() for DAQ

Trade-offs:
- Pro: ~27 GB RAM savings
- Con: Slightly less accurate positions (no peaklet-level info)
- Con: Less effective S2 merging (time-only, not time+position)

This addresses the RAM increase from PR #1482 (peaklet-level positions) for
online DAQ where RAM is constrained. Offline analysis can still use the full
peaklet-level position reconstruction for better accuracy.

* cleanup context

* Fix PeakPositionsCNFPeakOnly to use proper CNF algorithm

- Use JAX-based conditional normalizing flow (not TensorFlow MLP)
- Include uncertainty contours and r/theta uncertainties
- Properly format flow condition: normalized PMT pattern + log(area)
- Match PeakletPositionsCNF compute logic but operate on peaks
- Add helper methods: calculate_theta_diff, polygon_area, prediction_loop

This correctly implements CNF position reconstruction at peak level
without requiring peaklet waveforms.

* [pre-commit.ci] auto fixes from pre-commit.com hooks

for more information, see https://pre-commit.ci

* dont store data top and start

* [pre-commit.ci] auto fixes from pre-commit.com hooks

for more information, see https://pre-commit.ci

* dont store data top and start

---------

Co-authored-by: pre-commit-ci[bot] <66853113+pre-commit-ci[bot]@users.noreply.github.com>
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