Skip to content

MCPL_output: write simulated ray count when a run is interrupted - #3087

Open
willend wants to merge 5 commits into
mccode-dev:mainfrom
willend:mcpl-output-interrupt-raycount
Open

willend wants to merge 5 commits into
mccode-dev:mainfrom
willend:mcpl-output-interrupt-raycount

Conversation

@willend

@willend willend commented Nov 10, 2026 •

Copy link
Copy Markdown
Contributor

Free-form text area

Fixes #3055.

When a run was interrupted (SIGINT/SIGTERM), MCPL_output still wrote the requested ncount as stat:sum:initial_ray_count, so the file looked complete and intensities derived from it were too low.

Changes, in McStas MCPL_output, MCPL_output_noacc and McXtrace MCPL_output:

  • On interrupt, initial_ray_count is the number of rays simulated up to the last recorded particle, taken from _particle->_uid. The stored weights are p * ncount_requested, so no rescaling is needed.
  • The count starts at 0 and stays 0 while no particle is stored: with nothing stored there is no weight to renormalise.
  • It is set to -1 (unavailable) just before the first particle is written, and gets its final value in FINALLY. With OpenACC, where _uid order is not execution order, an interrupted run with stored particles keeps -1.
  • A warning is printed to stderr on interrupt, e.g. Warning (vout): run interrupted, initial_ray_count set to 4353479 of 2000000000 requested rays.
  • Completed runs are unchanged.
Situation initial_ray_count
No particle stored yet (also: interrupted before any particle) 0
From the first particle until FINALLY (also: hard kill) -1
Interrupted, CPU rays up to the last stored particle
Interrupted, OpenACC, particles stored -1
Completed run ncount

MCPL_output_noacc only: default weight_mode is now 1, as in MCPL_output and as its header already documented (it used to fall back to 2 with a warning). This changes the default output of that component.

New regression test test_mcpl_interrupt.py next to Test_MCPL_output.instr (identical copies for McStas and McXtrace), run from the basictest workflows (main job; passes there on Linux and macOS). It interrupts a run and checks that the ray count matches the number of particles. It skips on non-POSIX systems.

Notes / limitations:

  • The count can be one lower than the number of particles if the signal lands inside the component, and rays simulated after the last recorded particle are not counted. Relative error is about 1/N_recorded.
  • With MPI, each rank writes its own count. mpirun kills the ranks while they compress their files, so an interrupted MPI run leaves per-rank .mcpl files and no merged file. Not addressed here.
  • The test is not run in the FreeBSD job: there a non-MPI instrument is killed by SIGINT (exit status -2) without the runtime signal handler running, so no final save happens at all. The cause is not understood and is independent of this change. Nota Bene: FreeBSD is not an official McStas/McXtrace-supported platform, only in CI for sake of portability.

Declaration of use of AI-tools

  • Please add a checkmark here if you used AI-tools during the work for this contribution
  • Furter, please describe how / where and for what the tools were used: Claude Code was used for the analysis, the component changes, the test script, the CI wiring and this description.

Development OS / boundary conditions

Developed and tested on macOS arm64 (clang, conda mccode-dev environment), CPU only. Linux and macOS are covered by CI through the new test. Not tested on Windows (test skips), FreeBSD or GPU/OpenACC. The test needs mcpltool.

Tested locally:

  • test_mcpl_interrupt.py fails with the unpatched component and passes with the patched one, McStas and McXtrace.
  • Intensity (sum of weights / ray count) of an interrupted run agrees with a completed run within 0.1 %, single process and per rank with -np 2.
  • MCPL_output_noacc: completed and interrupted runs write the expected count.

PR Checklist for contributing to McStas/McXtrace

  • My contribution includes patches to an existing component file

    • I have used the mcdoc utility and rendered a reasonable documentation page for the component (please attach as screenshot in comments!)
    • I have ensured that basic use of the component is OK (e.g. an instrument using it compiles?)
    • I have used the mctest utility to test one or more instruments making use of the component (please attach mcviewtest report as screenshot in comments)
    • I have used the mccode-clangformat tool to apply the standard McCode component indentation scheme
    • I have used the mcrun --c-lint "linter" and followed advice to remove most / all warnings that are raised
  • My PR is meant to fix a specific, existing issue

  • My contribution contains something else

    • Explanation is added in free form text above or below the checklist

🤖 Generated with Claude Code

willend and others added 3 commits November 10, 2026 10:42
On SIGINT/SIGTERM, stat:sum:initial_ray_count was the requested ncount,
so intensities read from the file were too low (mccode-dev#3055). It is now the
number of rays simulated up to the last recorded particle (from _uid),
or left at -1 if nothing was recorded or with OpenACC. A warning is
printed in both cases.

MCPL_output_noacc: default weight_mode=1, as MCPL_output.

Add test_mcpl_interrupt.py and run it in the basictests.

Co-Authored-By: Claude Fable 5.2 <noreply@anthropic.com>
Interrupt only once 1 MB of MCPL output is on disk, and print the exit
status and the instrument output when the check fails.

Co-Authored-By: Claude Fable 5.2 <noreply@anthropic.com>
There the instrument is killed by SIGINT without the runtime signal
handler running, so the final save under test never happens.

Co-Authored-By: Claude Fable 5.2 <noreply@anthropic.com>
@willend
willend requested a review from tkittel November 10, 2026 10:23
@tkittel

tkittel commented Nov 10, 2026

Copy link
Copy Markdown
Contributor

Looks sensible I guess, although the changes are more intrinsic to how mccode operates than how mcpl works, so I could easily miss something. One thing stood out to me, namely:

It is left at -1 (unavailable) if no particle was recorded,

I also looked at MCPL_output.comp, and my concern remains: Having zero particles hitting a detector after 1000 rays coming out of the source is not the same as "I crashed and didn't have a chance to update the file", is it? In case the user ran -n 1000 and had all rays absorbed before getting to MCPL_output.comp we would expect an initial_ray_count of 0, so why should it be -1 after a ctrl-c which lead to the same conditions?

@willend

willend commented Nov 10, 2026

Copy link
Copy Markdown
Contributor Author

Good point, will apply a relevant fix in this direction.

@tkittel

tkittel commented Nov 10, 2026

Copy link
Copy Markdown
Contributor

Correction to what I wrote: In case the user ran -n 1000 and had all rays absorbed before getting to MCPL_output.comp we would expect an initial_ray_count of 1000 (not 0 like I wrote).

@willend

willend commented Nov 10, 2026

Copy link
Copy Markdown
Contributor Author

Thanks for the correction, and I agree in principle: the ideal number is the count of rays actually simulated before the interrupt.

The catch is that MCPL_output can’t know that number. It only sees the rays that reach it, and McCode keeps no running ray count that is valid when a run is interrupted.

So I’d rather keep this fix inside the component. What it writes is the number of rays up to the last particle recorded, so the file is consistent as “a run that stopped at that particle”. The rays simulated after it are not counted, which is a small error, well below the statistical one. Compared to today, where an interrupted file silently claims the full requested ray count, I think this is a good trade.

That leaves the case you raised, an interrupted run where no particle was recorded at all. We can’t write the true count there, so it is a choice between two values, and I’d like your preference from the MCPL side:

0: the same rule as above taken to its limit, giving a file with 0 rays and 0 particles. It is valid and adds nothing when merged with other files, but the rays that were simulated without a hit are lost.
-1: “count unavailable”. It is honest about us not knowing, but it looks the same as a crashed run, and I assume it makes the count of a merged file unavailable too.

Which of the two would you rather see in such a file?

For completeness: the exact count could be had by adding a ray counter to the main loop that the code generator writes. We would like to avoid that. It changes the generated code of every instrument, and it has to be made correct for MPI, OpenACC and the FUNNEL mode, all to improve a corner case of an interrupted run. If the approximate count turns out not to be good enough in practice, we can revisit it as a separate change.

@willend

willend commented Nov 10, 2026

Copy link
Copy Markdown
Contributor Author

My own preference would probably be something like:

  • Initialize by 0 (which is a valid number as long as no particle was received / nothing to normalise how ever way we put it)
  • When first particle is received, then flip to "number of particles simulated" ( Fall back to -1 on e.g. OpenACC/GPU where we don't know how many were simulated up to now)
  • If everything is succesful, update to full number of particles simulated in FINALLY

@tkittel

tkittel commented Nov 10, 2026

Copy link
Copy Markdown
Contributor

TLDR (all).

But what is mcget_ncount returning then, if a component can't know the number of rays?

I would avoid any sort of "fall back to 0' since that defeats the entire purpose of how stat:sum's are supposed to be implemented.

@willend

willend commented Nov 10, 2026 •

Copy link
Copy Markdown
Contributor Author

mcget_ncount() is the requested statistics at start of run and as such used for e.g. weight-normalisation in the source-components.

_particle->_uid is the thread-number of the current particle (aka. how many did we simulate up to now.)

We originally had a function mcget_run_num(), I believe that today returns _particle->_uid

The count starts at 0, is set to -1 just before the first particle is
written, and gets its final value in FINALLY: ncount for a completed
run, the simulated count for an interrupted one (-1 with OpenACC).
An interrupted run that stored nothing now carries 0 instead of -1.

Co-Authored-By: Claude Fable 5.2 <noreply@anthropic.com>
@willend

willend commented Nov 10, 2026 •

Copy link
Copy Markdown
Contributor Author

Pushed 8fb8bd2. stat:sum:initial_ray_count now evolves like this:

  • 0 until the first particle is stored. With no rays stored there is no weight to renormalise, so 0 rays / 0 particles is a valid statement about the file.
  • -1 from just before the first particle is written (no header rewrite needed, MCPL writes the header at that point).
  • Final value in FINALLY: ncount for a completed run; for an interrupted run the rays simulated up to the last stored particle, or -1 with OpenACC.

So 0 only ever appears in a file without particles. PR description updated to match.

@tkittel

tkittel commented Nov 11, 2026

Copy link
Copy Markdown
Contributor

So for the record, I think it is important for consistency with all the other work going into this that any file that does not have access to the correct value puts a -1 rather than 0 into the stat:sum. A very short simulation in which the user hit ctrl-c before any particles are entered, are also unlikely to have a big file that is crucial to keep around.

@willend

willend commented Nov 11, 2026

Copy link
Copy Markdown
Contributor Author

You seem to be contradicting yourself - and you have not understood what I wrote above:

MCPL_output can not know the number of particles simulated until a particle actually arrives! The number of particles simulated is carried with each arriving particle - so the following situation can never happen:

Correction to what I wrote: In case the user ran -n 1000 and had all rays absorbed before getting to MCPL_output.comp we would expect an initial_ray_count of 1000 (not 0 like I wrote).

Absorbed particles don't arrive. If all rays are absorbed before getting to MCPL_output, there is no particle and hence no record of the number of particles simulated

Hence, without a code-generator change the only setup that will make sense is what I proposed / committed above:


Pushed 8fb8bd2. stat:sum:initial_ray_count now evolves like this:

0 until the first particle is stored. With no rays stored there is no weight to renormalise, so 0 rays / 0 particles is a valid statement about the file.
-1 from just before the first particle is written (no header rewrite needed, MCPL writes the header at that point).
Final value in FINALLY: ncount for a completed run; for an interrupted run the rays simulated up to the last stored particle, or -1 with OpenACC.
So 0 only ever appears in a file without particles. PR description updated to match.


The only alternative is to have it always be at -1 - which will make an empty file / no particles stored will look like a crash:
left at -1 (unavailable) if no particle was recorded,

@willend

willend commented Nov 11, 2026

Copy link
Copy Markdown
Contributor Author

Or completely lazy / unclever: -1 whenever the full stat is not reached. Which again has the side-effect that a file without particles will look like the simulation crashed, no matter if it actually crashed.

Granted, I like my solution above better, but I also dislike repeated misunderstandings / discussing the same point ad infinitum in a comment thread.

Timeout, talk IRL.

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.

MCPL_output: stat:sum:initial_ray_count is the requested ray count when a run is interrupted

2 participants