Skip to content

Add fission bank birth-mesh tagging - #4199

Closed
ondrejch wants to merge 7 commits into
openmc-dev:developfrom
ondrejch:pr/birth-mesh-tagging
Closed

ondrejch wants to merge 7 commits into
openmc-dev:developfrom
ondrejch:pr/birth-mesh-tagging

Conversation

@ondrejch

@ondrejch ondrejch commented Oct 14, 2026 •

Copy link
Copy Markdown

Description

Summary

This PR adds a <birth_mesh> settings element, exposed in Python as Settings.birth_mesh. When it is set in an eigenvalue calculation, every fission site banked in the fission bank, in continuous-energy and in multigroup mode, is tagged with a bin of that mesh: the bin that contains the birth position of the neutron that produced the site (Particle::r_born()).

  • Bins: the tag is the 0-based bin that a MeshFilter on the same mesh gives for the birth position, which is the binning of MeshBornFilter, or -1 if that position is outside the mesh.
  • Mesh types: any type of mesh can be used. No translation or rotation is applied: these are attributes of a filter, not of a mesh, so a MeshFilter with a translation corresponds to a birth mesh that is itself moved.
  • Files: the tags are written next to the source bank as a new int32 dataset, birth_mesh_bin, aligned by index with source_bank, together with a birth_mesh_id attribute. This happens in state points that contain the source bank and in the source point files source.<batch>.h5 and source.h5. On a restart or with a file source, the tags are read back. In Python, StatePoint.birth_mesh_bin returns the tags and StatePoint.birth_mesh_id the mesh ID.
  • Format version: the state point and source file format version goes from 18.2 to 18.3. Older files still load in Python and as file sources, but a restart needs a state point of the same version (see "Compatibility").
  • Results: tagging consumes no random numbers and changes no scoring, so transport and tally results are unchanged.
  • Where it does nothing: in fixed-source mode and with the random ray solver, neither of which banks fission sites, the element is ignored with a warning. MCPL source point files cannot hold the tags, so an eigenvalue run with a birth mesh that writes them prints one warning.

Commits

  1. Add fission bank birth-mesh tagging. The settings element, the tagging in both transport modes, the datasets and their readers, the Python API, the documentation, the multigroup regression test birth_mesh, and the unit tests.
  2. Test birth-mesh tags with openmc.lib, MPI, any mesh and in CE. A Catch2 test of the SourceSite MPI datatype on one and two ranks, an openmc.lib test of the ctypes mirror and of the birth positions, a transport test of six mesh types against a MeshFilter, the continuous-energy regression test birth_mesh_ce, and two added checks in existing tests (see "Tests added in the second and third commits").
  3. Test the birth-mesh tags of sites read from a source file. A continuous-energy test that a FileSource reads the tags with the sites.
  4. Ignore the birth mesh with random ray and warn for MCPL source points. With the random ray solver, the element is ignored with a warning, as in fixed-source mode, and no birth_mesh_id is written. An eigenvalue run with a birth mesh that writes its source points to separate MCPL files prints one warning that these files carry no tags.
  5. Test the birth mesh with random ray and the MCPL source point warning. Two new tests (see "Tests of the random ray and MCPL behavior").
  6. Warn for MCPL source points written with overwrite_latest. With overwrite_latest, the latest source is written at every batch, also when write is false, and the warning of the fourth commit missed that case. The warning now covers both writes, and an empty list of source point batches no longer gives it.
  7. Test the MCPL source point warning with overwrite_latest. Three more cases for the MCPL warning test.

Motivation

The fission matrix couples the region where a neutron is born to the region where it causes fission. MeshBornFilter × MeshFilter tallies give the matrix itself. Several fission-matrix methods also need the individual sites, though. One example is the transient fission matrix method (Laureau et al., Annals of Nuclear Energy 85, 2015), which uses the time structure and delayed-group structure of the matrix. The source bank already stores the position, energy, time, and delayed group of each site, but not where the neutron that produced the site was born. A birth tag adds that, so a saved source bank holds the joint data (birth bin, site position, energy, time, delayed group) for every site. Post-processing can then form fission-matrix estimates and their time and delayed-group structure on any site binning, with no further runs or tallies.

The notion of birth and the binning match MeshBornFilter, so tags and tallies on the same mesh can be compared directly.

Design

Where the tag comes from. create_fission_sites() computes settings::birth_mesh->get_bin(p.r_born()) once per fission event, before the loop over the sampled sites, and stores it in every site. This happens in both physics.cpp (continuous energy) and physics_mg.cpp (multigroup), when a birth mesh is set. It uses no random numbers and changes no scoring, so the transport and the tally results are unchanged.

What "birth" means. It is the position at which the neutron was started from a site. For a neutron from the source bank, that is its own fission site. For the extra neutrons of (n,xn) reactions, it is the collision point, and for particles created by weight-window splitting, the split point. This is the same convention, and the same limitation, as MeshBornFilter, and the docs say so.

Mesh types and bins. Tagging only needs Mesh::get_bin(), the call through which MeshFilter and MeshBornFilter bin a position, so the birth mesh can be any mesh defined in settings.xml. When the element is read, the mesh is prepared for point location (prepare_for_point_location()), as MeshFilter::set_mesh() and the weight windows do. A MeshFilter applies its translation and rotation to the position before it bins it, and a MeshBornFilter its translation to the birth position. The birth mesh applies neither, because translation and rotation are attributes of those filters, not of the mesh. A tag on a mesh moved by a vector t therefore equals the bin of a MeshFilter with translation t on the unmoved mesh. The rule is stated in the same terms in the commit message, the comment in settings.cpp, the <birth_mesh> entry of io_formats/settings.rst, the Settings.birth_mesh docstring, and a new paragraph in the user's guide ("Generation of Output Files"). The second commit tests it in transport against a MeshFilter in the same run, for regular, translated regular, rectilinear, cylindrical, and spherical meshes, and for a translated filter (see "Tests"). Unstructured meshes are not tested here, since the builds have neither DAGMC nor libMesh.

Storage in SourceSite. The new member int birth_mesh_bin {-1} goes after n_collision, in what was tail padding: sizeof(SourceSite) stays 136 bytes, the new member sits at offset 132, and all other members keep their offsets. The member is added to the MPI datatype in initialize_mpi() (16 entries instead of 15), so the tags travel with the sites in synchronize_bank(), and to the openmc.lib ctypes mirror _SourceSite, so openmc.lib.source_bank() gains a birth_mesh_bin field. Measured on develop aa4afc4, in serial and MPI builds:

develop this PR
sizeof(SourceSite) 136 136
offset of birth_mesh_bin — 132
MPI datatype: entries, extent, true extent (bytes) 15, 136, 132 16, 136, 136
ctypes.sizeof(_SourceSite) 136 136, every offset equal to the C++ one

With #4178, which adds double lifetime after n_collision, the new member fills the 4 bytes of padding before lifetime, and the size stays at #4178's 144 bytes (see "Related PRs").

A separate dataset, not a new field of the source_bank compound type. read_source_bank() refuses a file whose compound member names differ from the current ones ("Source site attributes in file do not match ..."). A new compound field would therefore make every existing state point and source file unreadable as a restart or file source. It would also change surface source files, the initial source file, and the Python readers. A separate dataset avoids all of this:

  • Files without tags read exactly as before.
  • Older Python readers ignore the extra dataset.
  • Only files whose sites really are tagged fission sites carry it.

Tags are written only for the eigenvalue source bank:

  • Written: state points that contain the source bank, and source.<batch>.h5 and source.h5.
  • Not written: surface source files, the initial source file, and MCPL files (with a warning for MCPL source points, see "MCPL source points" below).
  • birth_mesh_id: written to every state point of a tagged run, even when the source bank is written to a separate file, so the file still records which mesh the tags in the separate file refer to.

Writing. write_source_bank() copies the tags of the local sites into an int32 buffer and writes it with the existing write_bank_dataset() template (bank_io.h), using H5T_NATIVE_INT32 and, with MPI, MPI_INT32_T. The tags therefore take exactly the same parallel HDF5 and MPI gather paths as the source bank. The buffer has the size of the local source bank, which the template also uses as the master's receive buffer.

Reading. When birth_mesh_bin exists, read_source_bank() checks that it is one-dimensional with the length of source_bank and stops with a fatal error otherwise. It reads the dataset through a memory dataspace sized to the buffer, selecting this process's share of the sites when the bank is distributed (restart). Without the length check, a dataset that is one entry too long would be read silently on restart.

Fixed-source mode and random ray. Neither a fixed-source calculation nor the random ray solver banks sites in the fission bank, so in both the element is ignored with a warning, every process prints it, and nothing is tagged or written, birth_mesh_id included. Before the fourth commit, a random ray eigenvalue run kept the mesh, tagged nothing, and still recorded birth_mesh_id in its state points.

MCPL source points. An MCPL file has no place for the tags. finalize_batch() writes source points in MCPL format in two cases: at the given batches to separate files, and with overwrite_latest the latest source at every batch, also when write is false. When an eigenvalue run with a birth mesh is set up for either, a warning says that the tags are not written to MCPL source point files. The master process prints it once, when the settings are read, not at every source point. Which batches the run reaches is known only during the run, so a requested batch counts as written; an empty list of batches gives no warning.

Format version. VERSION_STATEPOINT goes from 18.2 to 18.3, because state point and source files can now contain a new dataset and attribute. Source files share this version, and ParticleList.export_to_hdf5() writes [18, 3] too. docs/source/io_formats/statepoint.rst and source.rst give the new version, and source.rst now states the version explicitly. The <birth_mesh> element is documented in settings.rst, in alphabetical order, with the bin numbering, the meaning of birth, and which files carry tags.

Tests

Regression test tests/regression_tests/birth_mesh (first commit)

Model. Two one-group fissile slabs (5 cm each) are separated by a 10 cm pure absorber (Σt = Σa = 10 /cm), with reflective outer boundaries. A neutron that enters the absorber is absorbed at its first collision there, so to reach the other slab it needs at least 10 cm of uncollided path in the absorber. No neutron crosses it except with probability ≤ e^-100 per neutron (and 2^-64 per sampled flight distance, for a random number of exactly 0, which gives an infinite flight). The birth position of the neutron that produced a site is therefore in the slab of the site.

Checks. Five test functions, six test cases:

Test Checks
test_birth_mesh_eigenvalue Two-bin mesh (one bin per slab): every tag equals the bin of its own site; both bins occur; the tags are int32, aligned with source_bank, and equal to StatePoint.birth_mesh_bin; birth_mesh_id and StatePoint.birth_mesh_id are the mesh ID. The surface source file and the initial source file have no tags and no birth_mesh_id.
test_birth_mesh_source_files With sourcepoint = {'separate': True, 'overwrite': True}: the state point has birth_mesh_id but no source bank and no tags, so StatePoint.birth_mesh_bin is None and StatePoint.birth_mesh_id is the mesh ID. source.6.h5 and source.h5 carry identical, correct tags.
test_birth_mesh_birth_position[False] / [True] Two bins along x per slab, on an eight-bin regular mesh and on a nonuniform rectilinear mesh that also has two bins along y: tags never leave the slab of the site, they use the same y bins as the sites, and some tags differ from the bin of the site. A site-position tag would fail this.
test_birth_mesh_restart A restart from statepoint.3.h5 reads the tags and tags the new sites. Through openmc.lib, the restarted source bank holds the file's tags. Tag datasets one entry too short and one entry too long are rejected with the length error, both on restart and as a file source.
test_birth_mesh_fixed_source Fixed-source mode prints the warning; neither the state point nor the surface source file carries tags or birth_mesh_id, and StatePoint.birth_mesh_id is None.

Why it needs no nuclear data and no reference files. The test runs in multigroup mode with a library that it writes itself, so it needs neither OPENMC_CROSS_SECTIONS nor the NNDC library. Every assertion follows from the geometry or from the file layout, not from stored numbers, so there is no inputs_true.dat or results_true.dat. Apart from the e^-100 bound above, it does not depend on the random number sequence, the platform, or strict FP. It handles --mpi; the openmc.lib part is skipped under MPI.

Tests added in the second and third commits

The second commit adds tests/cpp_unit_tests/test_source_site.cpp (registered in tests/cpp_unit_tests/CMakeLists.txt), two tests and two checks in tests/regression_tests/birth_mesh/test.py, and the continuous-energy test tests/regression_tests/birth_mesh_ce. The third commit adds a fifth test to birth_mesh_ce. None of these tests needs reference files.

  • MPI datatype (test_source_site, Catch2). On an MPI build, it first checks that mpi::source_site covers every byte of SourceSite: its extent, its true extent, and its size (MPI_Type_size) all equal sizeof(SourceSite). With birth_mesh_bin the struct has no padding, so a datatype that leaves out a member or part of one, or gives a member a type of another size, holds fewer or more bytes than the struct. It then fills every byte of 4 sites per rank with a pattern that depends on the rank, the site, and the byte, sets every member to a distinct value, and sends the sites to the next rank with MPI_Sendrecv. The received sites must equal the sent ones member by member and byte for byte, and the 2 sites after them in the receive buffer must be untouched. The byte comparison also covers members that the test does not name, such as Keep the lifetime clock and delayed group of (n,xn) and split neutrons #4178's lifetime. CTest runs it on one rank as test_source_site and on two ranks as test_source_site_mpi, with --order decl, because Catch2 3.16.0, the version that CI builds, orders test cases randomly on each rank. On a build without MPI, it is skipped.
  • openmc.lib layout (test_birth_mesh_lib_source_bank). It runs the multigroup two-slab problem through openmc.lib, with a regular mesh of 2.5 cm bins along x and 5 cm bins along y that leaves the outer half of the right slab outside. After the last batch, the fields r, u, E, time, wgt, delayed_group, surf_id, particle, and birth_mesh_bin of openmc.lib.source_bank() must equal the source_bank and birth_mesh_bin datasets of the state point, so a member of the ctypes mirror at a wrong offset or of a wrong type shows up as a mismatch. The tags must include -1 and at least two bins. The test also compares each tag with the bin of the source site from which the neutron that produced the site was started (parent_id indexes the source bank of the last batch): with these data no secondary neutrons are created, so that source site is the neutron's birth position, and the two bins must be equal for every site, while the bin of the site itself differs for some.
  • Any mesh type, in transport (test_birth_mesh_any_mesh, 6 cases). Eight 10 cm fuel islands sit in the absorber of the two-slab data (Σt = Σa = 10 /cm), which fills the rest of a 120 cm × 120 cm × 10 cm box with vacuum faces along x and y and reflective faces along z. Any two islands are at least 18 cm (180 mean free paths) apart, so a site and the birth position of the neutron that produced it lie in the same island, by the argument above. Each island lies inside one element of the birth mesh or wholly outside the mesh. A tally [CellFilter(islands), MeshFilter(mesh)] with a collision estimator gives, in the same run, the bin of each island: exactly one nonzero bin per island inside the mesh, distinct for distinct islands, and none for an island outside. Every site must lie in one island, every island must hold sites, and every tag must equal the bin of its island, or -1 outside the mesh. The six cases are a regular 4 × 4 × 1 mesh; the same mesh moved by (30, −30, 0) cm, which leaves three islands outside; the moved mesh checked against a MeshFilter with that translation on the unmoved mesh; a nonuniform rectilinear mesh; a cylindrical mesh; and a spherical mesh, the last two with four azimuthal sectors and an origin off the center. The reference is the MeshFilter itself, not a Python reimplementation of the bin numbering. In all six cases, the bins that the tally gives the islands agree with bins computed from the geometry before the test was first run.
  • Continuous energy (birth_mesh_ce, 5 tests). The multigroup tests cannot reach the continuous-energy tagging line in physics.cpp; this test does, with the NNDC library that CI uses. Two 10 cm slabs of U-235 and H-1 are separated by 10 cm of B-10 at 50 g/cm³. The total cross section of B-10 is at least 1.4 b below 20 MeV, so the layer is more than 40 mean free paths thick at every energy, and a neutron born in one slab causes fissions only in that slab. The checks are those of the multigroup test: with one bin per slab, every tag equals the bin of its own site; with two bins per slab along x and two along y, some tags differ from the bin of the site and none leaves the slab; with a mesh over the left half, the sites in the right slab are tagged -1. A fourth test runs the problem through openmc.lib and compares each tag with the bin of the source site from which its history was started. A neutron created by an (n,2n) or similar reaction in that history is born at the reaction instead, so the test requires at least 99 % of the tags to equal that bin, and more than 10 % to differ from the bin of the site. In a diagnostic run, 1 of the 1000 tags differed from it; the neutron that produced that site was such a secondary, and its tag was the bin of its own birth point. A fifth test, from the third commit, covers the path by which a file source reads the tags, where the bank is read whole rather than distributed as on restart. A tagged run writes source.h5, and a second run with a FileSource on that file is initialized through openmc.lib; after simulation_init(), every site of the first batch must carry the tag of the file site at the same position. It uses continuous energy because on develop a multigroup FileSource reads the stored group index as an energy (Multigroup FileSource reads the group index stored in OpenMC source files as an energy in eV #4183). The same checks were first made by hand, measured on develop 3cded0f with an earlier version of the first commit and ENDF/B-VII.1 data (k = 1.278, 4000 sites): with two bins per slab, 1714 of the 4000 tags differed from the site bin, and none left its slab.
  • Two added checks. test_birth_mesh_restart requires that the restart run prints no HDF5 diagnostics, and test_birth_mesh_source_files that ParticleList.export_to_hdf5() writes the format version of the source files that OpenMC writes.

The openmc.lib parts of test_birth_mesh_lib_source_bank, test_birth_mesh_ce_birth_position, test_birth_mesh_ce_file_source, and test_birth_mesh_restart run in the pytest process, always in history mode, also under --event. Under --mpi, the first three tests are skipped, and so is the openmc.lib part of the fourth.

On develop aa4afc4 with the first two commits, birth_mesh (13 tests), birth_mesh_ce (4), test_settings.py (3), and test_statepoint.py (2) pass in history and --event mode, 22 of 22. With --mpi (2 ranks × 2 threads, through the wrapper described under "MPI runs", with mpi4py available), 20 pass and the 2 openmc.lib tests are skipped. With the third commit, birth_mesh (13) and birth_mesh_ce (5) pass in history and --event mode, 18 of 18, and with --mpi 15 pass and the 3 openmc.lib tests are skipped.

Tests of the random ray and MCPL behavior (fifth and seventh commits)

Both tests are in tests/regression_tests/birth_mesh/test.py, use the multigroup two-slab problem, run OpenMC as a subprocess in every mode, and count the warnings in its output.

  • test_birth_mesh_random_ray. The problem solved with the random ray solver and a birth mesh: the random ray warning is printed once by each process, neither the fixed-source warning nor the MCPL warning is printed, and the state point has no birth_mesh_id and no birth_mesh_bin.
  • test_birth_mesh_mcpl_warning, 10 cases. The MCPL warning must be printed exactly once, by the master process, in three cases, and never in the seven others:
    • once: MCPL source points at batch 7, which the 6-batch run does not reach, so that no MCPL file is written; MCPL source points at batches 3 and 6, both files checked; and overwrite_latest with write false, from the seventh commit;
    • never: HDF5 source points (their files must hold tags); overwrite_latest with HDF5 output (source.h5 must hold tags); an empty list of batches; MCPL with separate false (the state point must hold tags); MCPL with write false; fixed-source mode (with the fixed-source warning); and the random ray solver (with the random ray warning).
    • The case with MCPL files at batches 3 and 6 needs the MCPL library and is skipped without mcpl-config, as the other MCPL tests are. The overwrite_latest case writes an MCPL file at every batch, so without the library the run stops at the first write, after the warning; the test accepts that stop, and only with the error that the MCPL library is not available. With the library, it checks that source.mcpl is written.

At the fourth commit, before its tests, birth_mesh, birth_mesh_ce, test_settings.py, and test_statepoint.py (23 tests) pass in history mode; with --mpi, 20 pass and the 3 openmc.lib tests are skipped. At the sixth commit, with the tests of the fifth, the same files (31 tests) give 30 passed and 1 skipped in history mode, and 27 passed and 4 skipped with --mpi. At the seventh, birth_mesh (24 tests) gives 23 passed and 1 skipped in history and --event mode, and 22 passed and 2 skipped with --mpi. The skipped tests are the MCPL case above and, under --mpi, the openmc.lib tests. These runs were made on develop 3aec0ed; the --mpi runs used mpi4py and the wrapper described under "MPI runs".

Unit tests (first commit)

  • tests/unit_tests/test_settings.py: Settings.birth_mesh is part of the full export/import round trip. A new test_birth_mesh checks the default None, rejects a bare mesh ID, writes no element when unset, writes a mesh shared with ufs_mesh only once, round-trips a CylindricalMesh birth mesh, and raises ValueError for an undefined mesh ID.
  • tests/unit_tests/test_statepoint.py: StatePoint.birth_mesh_bin and StatePoint.birth_mesh_id on hand-written files: tags present (the ID is returned as an int); tagged but with a separate source file (ID present, tags None); untagged (both None). These files have version [18, 0], so the test also covers older minor versions.

Mutation check

Measured on develop 3cded0f, with an earlier version of the first commit. Each mutation edits one spot of C++ in a scratch tree of the commit and rebuilds; then the birth_mesh test is run. All 11 are detected, and the unmutated tree passes:

Mutation Failing tests
MG tag from the site position p.r() instead of p.r_born() birth_position (both meshes)
No MG tagging eigenvalue, source_files, birth_position, restart
read_source_bank() ignores the tag dataset restart
Length check removed (rank check only) restart
Source point files written without tags source_files
birth_mesh_id written only when the source bank is in the state point source_files
One tag written off by one all eigenvalue tests
Tags written in reverse order all eigenvalue tests
Fixed-source mode not ignored fixed_source
State point written without tags all eigenvalue tests
Birth mesh restricted to regular meshes birth_position[True] (rectilinear)

For the tests of the second and third commits, on develop aa4afc4, each mutation below was built and run against this PR's tests: the C++ tests with ctest on MPI builds, and the Python tests in history mode. Every one is detected. In the table, the test names drop the prefix test_birth_mesh_.

Mutation Failing tests
birth_mesh_bin left out of the MPI datatype, given MPI_DOUBLE, or given the displacement of n_collision test_source_site, test_source_site_mpi
ctypes n_collision and birth_mesh_bin swapped, or birth_mesh_bin left out lib_source_bank, restart, ce_birth_position
ctypes birth_mesh_bin as c_uint lib_source_bank
MG tag from the site position p.r() birth_position[False], birth_position[True], lib_source_bank
MG tag from p.r_last() lib_source_bank
CE tag from p.r() ce_two_bins_per_slab, ce_birth_position
CE tag from p.r_last() ce_birth_position
MG tag not stored in the sites 12 of the 13 multigroup tests (not fixed_source)
Birth mesh restricted to regular meshes any_mesh[rectilinear], any_mesh[cylindrical], any_mesh[spherical], birth_position[True]
Bin flattened with the last index fastest, in both transport modes all six any_mesh cases, birth_position[True], lib_source_bank, ce_two_bins_per_slab, ce_birth_position
Tags outside the mesh set to 0 instead of -1 lib_source_bank, any_mesh[translated], any_mesh[translated_filter], ce_left_half
Tags read from a file but not copied into the sites restart
Tags copied into the sites only when the bank is distributed, as on restart, not when it is read whole, as for a file source ce_file_source (third commit)
Tag dataset handles closed before the read, or closed a second time after it restart
Length check removed, or moved after the check of the read status restart
ParticleList.export_to_hdf5() writes 18.2, or VERSION_STATEPOINT stays 18.2 while it writes 18.3 source_files

On top of #4178, with the same tests:

Mutation Failing tests
birth_mesh_bin after lifetime (sizeof(SourceSite) 152) test_source_site, test_source_site_mpi
MPI datatype left at the auto-merged 16 entries test_source_site, test_source_site_mpi, test_secondary_bank_mpi
lifetime at disp[15] in a 17-entry datatype test_source_site (segmentation fault), test_source_site_mpi, test_secondary_bank
16 block counts with a count of 17 test_source_site (segmentation fault), test_source_site_mpi, test_secondary_bank, test_secondary_bank_mpi
lifetime as MPI_INT test_source_site, test_source_site_mpi, test_secondary_bank_mpi
birth_mesh_bin as MPI_DOUBLE test_source_site, test_source_site_mpi (test_secondary_bank_mpi passes)
ctypes birth_mesh_bin and lifetime swapped, or birth_mesh_bin left out lib_source_bank, restart, ce_birth_position
Tags copied into the sites only when the bank is distributed ce_file_source (third commit)

For the fourth and sixth commits, each mutation below was built and run against the tests of the fifth and seventh commits, in history mode, and under --mpi where stated. Every one is detected; the two that drop the master-process guard are detected only under --mpi.

Mutation Failing tests
Random ray: no warning; the mesh kept; or the warning printed but the mesh kept random_ray, mcpl_warning[random_ray]
Fixed-source and random ray checks swapped, with each other's warning fixed_source, random_ray, mcpl_warning[fixed_source], mcpl_warning[random_ray]
Random ray check inverted, or the Monte Carlo solver ignored too 17 and 15 tests, among them every tagging test of birth_mesh
MCPL warning removed mcpl_warning[mcpl]
MCPL warning given at every MCPL source point written, or at every batch, instead of once mcpl_warning[mcpl]
MCPL warning without the MCPL condition mcpl_warning[hdf5], and with the sixth commit also mcpl_warning[overwrite_hdf5]
MCPL warning without the separate condition mcpl_warning[not_separate]
MCPL warning without the write condition (fourth commit) mcpl_warning[not_written]
MCPL warning without the birth mesh condition mcpl_warning[fixed_source], mcpl_warning[random_ray]
MCPL warning on every process (master guard dropped) under --mpi: mcpl_warning[mcpl], and with the sixth commit also mcpl_warning[overwrite_not_written]
overwrite_latest dropped from the condition, as in the fourth commit, or write still required for it mcpl_warning[overwrite_not_written]
Empty list of batches not excluded mcpl_warning[empty_batches]
Separate source points at the given batches dropped from the condition mcpl_warning[mcpl]

Three changes are not detected:

  • the version bump undone in both VERSION_STATEPOINT and ParticleList.export_to_hdf5(), since no test pins a version number;
  • the handles of the tag dataset left open before the fatal error of the length check, since that error ends the run;
  • on MPI builds, a restart in which every rank reads the tags from the start of the dataset instead of from its own share: the tags read on restart only label the sites of the first batch, which are replaced by new fission sites before any file is written, so only openmc.lib under MPI could see them, and the tests run openmc.lib only without MPI.

Compatibility checks across builds

Measured on develop 3cded0f, comparing an earlier version of the first commit with a build of develop at that commit (version 18.2) and with the Python package of that commit:

Check Result
This branch's openmc.StatePoint on an untagged 18.2 state point from the base build loads: same k-eff, source bank, and tallies as the base reader; birth_mesh_bin and birth_mesh_id are None
This branch's ParticleList.from_hdf5 on an 18.2 source file loads (1000 particles)
Tagged branch run with an 18.2 source file as its file source runs; writes 18.3 files with tags 0 and 1
Base Python reader on 18.3 state points (with tags, or with a separate source) and on a tagged 18.3 source file loads, ignoring the new dataset and attribute
Base build with a tagged 18.3 source file, or a ParticleList.export_to_hdf5() file, as its file source runs
Restart across versions (branch from an 18.2 state point, base from an 18.3 one) refused: "State point version does not match current version in OpenMC." (unchanged exact-match rule, see "Compatibility")
Branch restart from its own 18.3 state point runs
One OpenMP thread: base vs this branch untagged vs this branch tagged k_generation, tally results, and source_bank bitwise identical

The multigroup file sources in these checks widen energy_bounds because of #4183: on develop, a multigroup FileSource reads the group index that OpenMC stores in E as an energy in eV, so the default bounds reject the group-0 sites of these one-group files, and widening the bounds gives the right group here only because the lowest group edge is 0 (#4191 proposes a fix).

Test suite

The test set of CI (tests/test_matplotlib_import.py tests/unit_tests tests/regression_tests) was run on develop aa4afc4 and on this branch on that base at its first commit. All runs used the same environment: the NNDC HDF5 library (its cross_sections.xml matches the md5 in tests/conftest.py) and OMP_NUM_THREADS=2, in three pytest processes per run (the unit tests, and the regression tests split in two). All builds are RelWithDebInfo with strict FP and OpenMP on; the MPI build uses parallel HDF5.

Mode develop aa4afc4 this branch, first commit
history 1513 passed, 147 skipped, 66 errors 1521 passed, 147 skipped, 66 errors
--event 1494 passed, 166 skipped, 66 errors 1502 passed, 166 skipped, 66 errors
--mpi --mpi-np=2 (2 ranks x 2 threads) 1432 passed, 157 skipped, 137 errors 1440 passed, 157 skipped, 137 errors

No test failed. The 8 extra passes in each mode are this PR's 6 regression test cases and its 2 new unit tests (test_settings.py::test_birth_mesh and test_statepoint.py::test_birth_mesh_tags). The skipped tests and the tests with errors are the same on both sides, and every error is raised at setup by the environment:

  • 63 tests need OPENMC_ENDF_DATA, which was not set (test_data_*.py, test_deplete_chain.py, and test_endf.py).
  • 3 tests (cpp_driver, source_dlopen, and source_parameterized_dlopen) build against an installed OpenMC, and these builds were not installed.
  • 71 tests, in the MPI run only, need mpi4py, which was not installed.

This PR's own tests, at its first commit, run on their own (birth_mesh, test_settings.py, and test_statepoint.py; 11 tests): all 11 pass in history and --event mode. With --mpi (2 ranks x 2 threads), 10 pass, and test_settings.py::test_properties_file_load, which this PR does not change, errors at setup because mpi4py is missing, as on develop; with the system's mpi4py 3.1.5 made available, it passes on both. These 11 tests are among the 22 run with the first two commits (see "Tests added in the second and third commits").

MPI runs. The MPI runs used Open MPI 4.1.6, through a small wrapper around mpiexec that unsets the OMPI_*, PMIX_*, OPAL_*, and ORTE_* environment variables before it calls mpiexec. An MPI initialization inside the pytest process leaves these variables in that process, and without the wrapper every later mpiexec -n 2 openmc in it aborts, on develop and on this PR alike. Without mpi4py, that initialization is the in-process openmc.lib.init() of the CMFD regression tests (a singleton MPI_Init under Open MPI). With mpi4py installed, it is already the import of openmc.lib, which tests/conftest.py performs at collection: openmc.lib.core imports openmc.mpi, which imports mpi4py.MPI. The runs at the first commit kept process binding off (OMPI_MCA_hwloc_base_binding_policy=none, which their wrapper preserves); the runs with the second and third commits used Open MPI's default binding.

Reference files. Regenerating every regression reference with pytest --update tests/regression_tests (history mode) on develop aa4afc4 and on this branch at its first commit gives byte-identical files, apart from run-to-run differences and the format version:

  • At 2 threads, 58 of the 1137 files the two share differ, all in 8 directories (collision_track, the four deplete_* directories, microxs, surface_source, surface_source_write). All 58 also differ between two runs of develop.
  • At 1 thread, 52 files in those directories differ, the same 52 as between two 1-thread runs of develop. In 29 of them only timestamps differ: the wall-clock depletion time dataset of the depletion results (22 files) and the modification times in HDF5 object headers (7 files), which also differ between the two runs of develop. The other 23 are surface source references (surface_source and surface_source_write cases 01 to 21 and a01), whose root version attribute is written as [18, 3] instead of [18, 2]; apart from that, they differ only in object-header times.

No committed reference file is changed by this PR. A later --update would write version 18.3 into those 23 files; their tests compare only the source_bank dataset, so the committed files still pass.

Other checks

  • ctest at the first commit, on the serial and the MPI build: 14 tests, 13 passed, test_mcpl_stat_sum skipped because MCPL is not enabled. With the second commit: 15 tests on the serial build, 13 passed, with test_mcpl_stat_sum and test_source_site (which needs MPI) skipped; and 16 on the MPI build, 15 passed, with test_mcpl_stat_sum skipped, built once with the system's Catch2 3.4.0 and once with Catch2 3.16.0, the version pinned in cmake/Dependencies.cmake. On the Catch2 3.16.0 build, test_source_site_mpi passed 20 runs out of 20, each with 90 assertions in 2 test cases per rank. The third commit changes no C++ file, and gives the same counts.
  • clang-format 18.1.8 (--dry-run --Werror) reports no changes for the 10 C++ files that the first commit changes. pyflakes 3.2.0 on its 8 changed Python files and pycodestyle 2.11.1 on its Python diff report nothing. For the second commit, clang-format reports no changes for test_source_site.cpp, and pyflakes on the two changed test files and pycodestyle on the Python diff report nothing; for the third commit, pyflakes on birth_mesh_ce/test.py and pycodestyle on its diff report nothing.
  • On develop 3aec0ed, at the fourth to seventh commits: ctest 15 tests on the serial build (13 passed, test_mcpl_stat_sum and test_source_site skipped) and 16 on the MPI build (15 passed, test_mcpl_stat_sum skipped). clang-format 18.1.8 reports no changes for src/settings.cpp and include/openmc/settings.h, and pyflakes on openmc/settings.py and birth_mesh/test.py and pycodestyle on the Python diff report nothing.
  • Behavior outside the test suite, on develop 3aec0ed, 1 thread. With openmc.examples.random_ray_pin_cell() and a birth mesh, the run prints the random ray warning once, and its state point has no birth_mesh_id; before the fourth commit it printed no warning and recorded the ID. With the Monte Carlo solver, MCPL source points at batches 3 to 5 give one MCPL warning, and the run then stops at the first MCPL write, since the MCPL library is not installed. The same holds with overwrite_latest and write false, which before the sixth commit gave no warning; an empty list of batches gave a warning before it and gives none since. HDF5 source points give no warning, and their files hold tags.
  • Sphinx (measured on develop 3cded0f): a build of docs/source/io_formats gave no warnings for the changed sections. It was not repeated after the wording of the mesh rule changed in io_formats/settings.rst, for the new user's guide paragraph, or for the random ray sentences of the fourth commit.
  • Environment: Ubuntu 24.04 (x86_64), GCC 13.3.0, HDF5 1.10.10 (serial, or Open MPI parallel for the MPI build), Python 3.12.3 with numpy 2.5.3, h5py 3.16.0, scipy 1.18.1, pandas 3.0.6, and pytest 9.1.1.

On top of #4178. The resolution described under "Related PRs" was built and tested on #4178's commits applied to develop aa4afc4:

  • Layout: sizeof(SourceSite) 144, birth_mesh_bin at 132, lifetime at 136, every other offset as on develop; the MPI datatype has 17 entries with extent and true extent 144; ctypes.sizeof(_SourceSite) 144, every offset equal to the C++ one. Keep the lifetime clock and delayed group of (n,xn) and split neutrons #4178 alone gives the same size, with 4 bytes of padding at offset 132.
  • ctest at the first commit: 16 tests on the serial build and 17 on the MPI build, all passed except the skipped test_mcpl_stat_sum, among them Keep the lifetime clock and delayed group of (n,xn) and split neutrons #4178's test_particle, test_secondary_bank, and two-rank test_secondary_bank_mpi.
  • At the first commit, birth_mesh passes in history, --event, and --mpi mode, and the IFP regression tests and test_ifp_weight_windows.py pass in history mode.
  • The CI test set in history mode: 1527 passed, 147 skipped, 66 errors, against 1519, 147, and 66 for Keep the lifetime clock and delayed group of (n,xn) and split neutrons #4178 alone; the 8 extra passes are this PR's, and the failing and skipped sets are the same. Regenerating the references gives the same picture as above, the same 23 version-only files included.
  • With the second commit on top (its only conflict, in tests/cpp_unit_tests/CMakeLists.txt, is resolved as described under "Related PRs"): ctest runs 17 tests on the serial build, 15 passed and 2 skipped (test_mcpl_stat_sum and test_source_site), and 19 on the MPI build, 18 passed and test_mcpl_stat_sum skipped, among them test_source_site_mpi and Keep the lifetime clock and delayed group of (n,xn) and split neutrons #4178's test_secondary_bank_mpi. In test_source_site, the byte comparison covers lifetime, and the datatype covers all 144 bytes. birth_mesh, birth_mesh_ce, test_settings.py, and test_statepoint.py give the same results as without Keep the lifetime clock and delayed group of (n,xn) and split neutrons #4178: 22 of 22 pass in history and --event mode, and with --mpi 20 pass and the 2 openmc.lib tests are skipped.
  • With the third commit on top too (it applies without a conflict): ctest gives the same counts, and birth_mesh and birth_mesh_ce give the same results as without Keep the lifetime clock and delayed group of (n,xn) and split neutrons #4178: 18 of 18 in history and --event mode, and with --mpi 15 passed and the 3 openmc.lib tests skipped.
  • The fourth to seventh commits apply on top of these without a conflict. With all seven: ctest 17 tests on the serial build (15 passed, 2 skipped) and 19 on the MPI build (18 passed, test_mcpl_stat_sum skipped), and birth_mesh gives the same results as without Keep the lifetime clock and delayed group of (n,xn) and split neutrons #4178: 23 passed and 1 skipped in history and --event mode, and 22 passed and 2 skipped with --mpi.

Rebase onto 3aec0ed. Unless another base is named, the runs reported in this section for the first three commits were made with this branch on develop aa4afc4. The rebase of those commits onto develop 3aec0ed (#4189 and #4190) changed only openmc/data/reaction.py and openmc/model/model.py, which this PR does not touch; each commit has the same diff as before. The fourth to seventh commits were made on 3aec0ed. At the rebased third commit:

Not run (CI configurations not reproduced):

  • MPICH: the MPI runs used Open MPI 4.1.6, through the wrapper above. The MPI runs of the CI test set had no mpi4py; the runs of this PR's own tests with the second and third commits had the system's mpi4py 3.1.5.
  • Builds with MCPL or coverage, and builds with OpenMP off. Without MCPL, the MCPL source point path, which carries no tags, was not exercised: test_birth_mesh_mcpl_warning[mcpl_written] was skipped, and the overwrite_latest case stopped, as its test allows, at the first MCPL write.
  • Builds with DAGMC, libMesh, XDG, or NCrystal, so no unstructured mesh was used as a birth mesh.
  • The tests that need OPENMC_ENDF_DATA or an installed OpenMC (errors on both sides, listed above).
  • CI's Python environment: the package versions above may differ from CI's.
  • The CI test set after the first commit: its results above are at the first commit. The later commits add tests, and the fourth and sixth also change the reading of the <birth_mesh> element and its warnings; at each of them, this PR's own tests were run on their own (see "Tests added in the second and third commits" and "Tests of the random ray and MCPL behavior").
  • On top of Keep the lifetime clock and delayed group of (n,xn) and split neutrons #4178, the --event and MPI runs of the CI test set.
  • On develop 3aec0ed, everything except the runs listed under "Rebase onto 3aec0ed" and those of the fourth to seventh commits.

Compatibility

  • File formats. The state point and source file format version is now 18.3. The only additions are the optional birth_mesh_bin dataset and birth_mesh_id attribute; nothing is removed or changed, and untagged runs write the same datasets as before.
    • Python: openmc.StatePoint checks only the major version, so 18.0–18.2 files still load. ParticleList.from_hdf5() does not check the version.
    • C++ file sources: read_source_bank() handles files of any version as before; the particle-code check is minor >= 2 within major 18.
    • Restart: statepoint_version_check() requires an exact match, as for the 18.1 → 18.2 bump in Refactor ParticleType to use PDG Monte Carlo numbering scheme #3756. A restart from a state point written by develop (18.2) is therefore refused, and so is the reverse. This PR does not change that rule.
    • Properties files: openmc_properties_export() also writes VERSION_STATEPOINT as its version attribute, so properties.h5 files now say 18.3. No reader checks it.
  • Python API. New: Settings.birth_mesh (any openmc.MeshBase, default None), StatePoint.birth_mesh_bin (numpy.ndarray or None) and StatePoint.birth_mesh_id (int or None). openmc.lib.source_bank() records gain a birth_mesh_bin field.
  • C++ API. SourceSite gains birth_mesh_bin (default -1) in its former tail padding; all other members keep their offsets and the size is unchanged. write_source_point(), write_h5_source_point(), and write_source_bank() gain a trailing bool birth_mesh_tags = false, so existing calls still compile and behave as before; the signatures change at the ABI level. New global: settings::birth_mesh (const Mesh*). Compiled source libraries need no source changes, but should be rebuilt against the new headers.
  • Warnings. A <birth_mesh> element is ignored with a warning in fixed-source mode and with the random ray solver. An eigenvalue run with a birth mesh that writes MCPL source points prints one warning.
  • Results. No transport or tally result changes, with or without tagging. No committed reference file changes; regenerated surface source references record version 18.3 (see "Reference files").

Related PRs

This PR is based on develop and does not depend on any other PR. Several of my open PRs touch the same files:

None of these PRs changes VERSION_STATEPOINT.

AI Assistance

Code, tests, analysis, reviews and text:

  • Harness: Claude Code
  • Model: Claude Opus 5.5 (claude-opus-5-5)
  • Reasoning effort: xhigh

Review of the pull request formalities:

  • Harness: OpenCode
  • Model: muse-spark
  • Reasoning effort: not recorded

Review of the change:

  • Harness: OpenCode
  • Model: grok-4.7
  • Reasoning effort: not recorded

Checklist

  • I have performed a self-review of my own code
  • I have run clang-format (version 18) on any C++ source files (if applicable)
  • I have followed the style guidelines for Python source files (if applicable)
  • I have made corresponding changes to the documentation (if applicable)
  • I have added tests that prove my fix is effective or that my feature works (if applicable)

Summary by CodeRabbit

  • New Features
    • Eigenvalue calculations can tag fission source sites with the zero-based mesh bin containing the neutron’s birth position, or -1 when outside the mesh.
    • Birth-mesh tags are saved with source banks in statepoint and HDF5 source-point files, restored on restart, and accessible through the Python API.
  • Documentation
    • Documented birth-mesh settings, tag behavior, and source/statepoint file formats.
  • Compatibility
    • Updated the documented statepoint and source file format version to 18.3. Tags are not stored in surface-source, initial-source, or MCPL files.

Add a <birth_mesh> settings element (Settings.birth_mesh) that tags
every fission site banked in an eigenvalue calculation with the bin of a
mesh that contains the birth position, Particle::r_born(), of the
neutron that produced the site, or with -1 if that position is outside
the mesh. Any type of mesh can be used. The tag is the 0-based bin that
a MeshFilter on the mesh gives for the birth position, which is the
binning of MeshBornFilter. No translation or rotation is applied: these
are attributes of a filter, not of a mesh, so a MeshFilter with a
translation corresponds to a birth mesh that is itself moved. The tags
of a saved source bank can thus be combined with the site positions,
energies, times and delayed groups to form fission-matrix estimates in
post-processing. Continuous-energy and multigroup fission sites are both
tagged; the bin is computed once per fission event. Tagging consumes no
random numbers and changes no scoring, so transport and tally results
are unchanged. In fixed-source mode, where no fission sites are banked,
the element is ignored with a warning.

The tag is a new SourceSite member, birth_mesh_bin, which fills the
existing tail padding of the struct (sizeof(SourceSite) is unchanged)
and is added to the MPI datatype and to the openmc.lib ctypes mirror.
It is not part of the source_bank compound type. Instead, the tags are
written with the existing write_bank_dataset() template as a separate
int32 birth_mesh_bin dataset, aligned by index with source_bank,
together with a birth_mesh_id attribute, in state points that contain
the source bank and in the source point files source.<batch>.h5 and
source.h5. A state point of a tagged run records birth_mesh_id even
when the source bank is written separately. Surface source files, the
initial source file and MCPL files carry no tags.

read_source_bank() reads the tags back for a restart or a file source.
The dataset must be one-dimensional with the extent of source_bank,
otherwise the run stops with a fatal error. The tags are read through a
memory dataspace sized to the buffer, selecting this process's share of
the sites when the bank is distributed.

The state point and source file format version is bumped to 18.3.
Files written by earlier versions still load in Python and as file
sources: StatePoint checks only the major version and returns None for
birth_mesh_bin and birth_mesh_id when a file has no tags, and
read_source_bank() handles the particle codes of any file as before. As
for earlier minor version bumps, a restart still requires a state point
of the current version. ParticleList.export_to_hdf5() now writes
version 18.3 too.

Tests: the data-independent multigroup regression test birth_mesh uses
two fissile slabs separated by an absorber that no neutron crosses
except with probability <= e^-100 (and 2^-64 per sampled flight
distance, for a random number of exactly 0), so that every tag must
equal the bin of its own site on a two-bin mesh. Finer regular and
nonuniform rectilinear meshes show that the tags come from the birth
position rather than from the site. The test also checks which files
carry tags, a restart (through openmc.lib), the rejection of tag
datasets that are one entry too short or too long on restart and for a
file source, and fixed-source mode. Unit tests cover the Settings XML
round trip, including a cylindrical birth mesh, and
StatePoint.birth_mesh_bin and StatePoint.birth_mesh_id.
Add tests of the fission bank birth-mesh tags:

- test_source_site (Catch2), also run on two MPI ranks as
  test_source_site_mpi: the MPI datatype of SourceSite holds every byte
  of the struct, and sites sent from one rank to another keep every
  member.
- test_birth_mesh_lib_source_bank: every field of
  openmc.lib.source_bank() equals the source bank and the tags written
  to the state point, and each tag is the bin of the source site from
  which the neutron that produced the site was started.
- test_birth_mesh_any_mesh: in a problem of eight decoupled fuel islands,
  the tag of every site is the bin that a MeshFilter gives in its
  island, for a regular, a translated regular, a rectilinear, a
  cylindrical and a spherical mesh, and for a regular mesh with a
  translated filter.
- birth_mesh_ce: the two-slab checks of birth_mesh in continuous-energy
  transport, with U-235 and H-1 slabs separated by B-10, and the
  comparison of the tags with the bins of the source sites from which
  the histories were started.

test_birth_mesh_restart also checks that a restart run reads the tags
without errors from HDF5, and test_birth_mesh_source_files that
ParticleList.export_to_hdf5() writes the format version of the source
files written by OpenMC.
Add test_birth_mesh_ce_file_source: a tagged continuous-energy run
writes source.h5, and an eigenvalue run with a FileSource on that file
is initialized through openmc.lib. Each site sampled for the first
batch must have the tag of the file site that it copies, which checks
that read_source_bank() copies the tags into the sites when the whole
bank is read. Like the other openmc.lib tests, it is skipped under MPI.
The random ray solver banks no fission sites, so a <birth_mesh> element
tagged nothing in a random ray eigenvalue run, while its state points
still recorded a birth_mesh_id. The element is now ignored with a
warning, as in fixed-source mode, and no birth_mesh_id is written.

MCPL source point files have no place for the tags, which were dropped
without notice. When an eigenvalue run with a birth mesh writes its
source points to separate MCPL files, a warning now says so. It is
printed once, by the master process, when the settings are read, and
not at every source point written.
Add test_birth_mesh_random_ray: with the random ray solver, a
<birth_mesh> element is ignored with a warning, printed once by each
process, and the state point records no birth_mesh_id and no tags.

Add test_birth_mesh_mcpl_warning: the warning that MCPL source point
files carry no tags is given exactly once, by the master process, for
an eigenvalue run that writes its source points to separate MCPL files,
and not for HDF5 source points, for source points kept in the state
point, when source points are not written, in fixed-source mode or with
the random ray solver. Source points are requested at a batch that the
run does not reach, so that these cases need no MCPL library. A case
that writes MCPL source points at two batches is skipped without
mcpl-config, as the other MCPL tests are.
The warning that MCPL source point files carry no birth-mesh tags
required the source to be written to separate files. With
overwrite_latest, finalize_batch() writes the latest source at every
batch even when the source is otherwise not written, so a run with
write set to false and MCPL output dropped the tags without the
warning. The warning now follows both writes: the source points of the
given batches when the source is written to separate files, and the
latest source with overwrite_latest.
Extend test_birth_mesh_mcpl_warning. With overwrite_latest the latest
source is written to an MCPL file at every batch, also when write is
False, so that run gives the warning once. An empty list of source
point batches and HDF5 output with overwrite_latest give none. Without
the MCPL library, the overwrite_latest run stops where it writes the
first MCPL file, after the warning; the test accepts that stop, and
with the library it checks that source.mcpl is written.
@coderabbitai

coderabbitai Bot commented Oct 14, 2026 •

Copy link
Copy Markdown

Review in Change Stack →

No actionable comments were generated in the recent review. 🎉

ℹ️ Recent review info
⚙️ Run configuration
  • Configuration used: defaults
  • Review profile: CHILL
  • Plan: Advanced
  • Run ID: d53e9085-80fa-4209-9d7a-e0c3d4434b46
📥 Commits

Reviewing files that changed from the base of the PR and between 3aec0ed and 71590c0.

📒 Files selected for processing: 26
  • docs/source/io_formats/settings.rst
  • docs/source/io_formats/source.rst
  • docs/source/io_formats/statepoint.rst
  • docs/source/usersguide/settings.rst
  • include/openmc/constants.h
  • include/openmc/particle_data.h
  • include/openmc/settings.h
  • include/openmc/state_point.h
  • openmc/lib/core.py
  • openmc/settings.py
  • openmc/source.py
  • openmc/statepoint.py
  • src/initialize.cpp
  • src/physics.cpp
  • src/physics_mg.cpp
  • src/settings.cpp
  • src/simulation.cpp
  • src/state_point.cpp
  • tests/cpp_unit_tests/CMakeLists.txt
  • tests/cpp_unit_tests/test_source_site.cpp
  • tests/regression_tests/birth_mesh/__init__.py
  • tests/regression_tests/birth_mesh/test.py
  • tests/regression_tests/birth_mesh_ce/__init__.py
  • tests/regression_tests/birth_mesh_ce/test.py
  • tests/unit_tests/test_settings.py
  • tests/unit_tests/test_statepoint.py

Included review availability: This review used your included allowance. Your plan provides up to 10 included reviews per hour; 9 remain after this review.

📝 Walkthrough

The change adds an eigenvalue setting that tags fission source sites with the producing neutron’s birth-position mesh bin. It writes and restores these tags with eligible source-bank files and exposes them through the Python statepoint API.

Changes: Birth-mesh tagging

Layer / File(s) Summary
Configure the birth mesh
openmc/settings.py, src/settings.cpp, include/openmc/settings.h, docs/source/usersguide/settings.rst, docs/source/io_formats/settings.rst, tests/unit_tests/test_settings.py
The settings API and parser resolve the configured mesh, serialize and load it, and warn when tagging is ignored or MCPL output cannot store tags.
Tag fission sites and carry tags through MPI
include/openmc/particle_data.h, openmc/lib/core.py, src/physics.cpp, src/physics_mg.cpp, src/initialize.cpp, tests/cpp_unit_tests/*
Fission-site creation assigns the parent neutron’s birth-mesh bin or -1. The SourceSite MPI datatype includes the new field, with tests for its size and transfer.
Write and load source-bank tags
include/openmc/state_point.h, src/state_point.cpp, src/simulation.cpp, openmc/statepoint.py, openmc/source.py, include/openmc/constants.h, docs/source/io_formats/source.rst, docs/source/io_formats/statepoint.rst, tests/unit_tests/test_statepoint.py
Statepoint and HDF5 source-point output records mesh metadata and index-aligned tags. Source-bank loading validates and restores the tags. The file format version and Python statepoint properties are updated.
Validate tagging and file behavior
tests/regression_tests/birth_mesh/*, tests/regression_tests/birth_mesh_ce/*
Regression tests cover bin assignments, mesh types, statepoint and source-point files, restart and file-source loading, unsupported modes, and MCPL output cases.

Priority: ➖ Normal

Estimated code review effort: 4 (Complex) | ~45 minutes

Change: Feature

Sequence Diagram(s)

sequenceDiagram
  participant SettingsParser
  participant create_fission_sites
  participant SourceSite
  participant write_source_bank
  participant HDF5SourceBank
  SettingsParser->>create_fission_sites: configure birth mesh
  create_fission_sites->>SourceSite: assign birth_mesh_bin
  SourceSite->>write_source_bank: provide tagged source sites
  write_source_bank->>HDF5SourceBank: write birth_mesh_bin dataset
  HDF5SourceBank->>SourceSite: load tags for source sites
Loading
Merge Risk: ⚪ Minimal · up to 71590

No actionable merge-blocking issue was established. The change is mergeable after normal checks.

Security architecture risk: 🟡 Moderate · up to 71590

Birth-mesh tags add useful source-bank metadata, but a partially written file can lose the tags without being recognized as incomplete. No privilege expansion or confirmed security vulnerability was found.

Retained concerns

  • Medium · reliability · inferred: A readable, partially written tagged source bank can retain its mesh ID and source sites without its companion tags; loading treats the missing dataset as an untagged bank rather than detecting incomplete output.
Security review details

Security Blast Radius

  • inferred — The inspected exposure is model configuration, simulation source-bank files, restart state, and downstream readers of their metadata. Evidence does not establish a wider tenant, network, credential, or service-authority boundary.

Trust Boundaries and Controls

  • observed — Configuration references must resolve to registered meshes. For file-provided tags, the native reader rejects a present dataset with the wrong rank or length, but does not require a tag dataset when a mesh-ID attribute is present.

Resilience and Maintainability Implications

  • observed — Explicitly reused Python mesh IDs produce a warning, not a uniqueness error. The new birth-mesh XML reference is ID-based, so it cannot distinguish two different mesh objects assigned the same ID; no duplicate-ID regression case was established.

Hardening Proposals

  • proposed — For source-bearing files that declare a birth mesh, validate the mesh-ID and tag-dataset pairing on load, and make completion of the two datasets distinguishable from a partial write.
Pre-merge checks: Passed 2 · Failed 3

Failed Failed checks: 3 warnings

Check name Status Explanation Resolution
Linked Issues check Warning Direct issue [#4183] requires a general multigroup FileSource fix. The PR diff only changes source-file version writing in openmc/source.py and adds birth-mesh tag reading and writing. The continu… Implement the multigroup FileSource read path required by [#4183]. Add regression tests for same-library group preservation, group-0 sources under default bounds, and prevention of incorrect or out-of-range group conversion.
Out of Scope Changes check Warning The PR changes are outside the scope of directly linked issue [#4183]. Settings.birth_mesh, SourceSite.birth_mesh_bin, transport tagging, birth-mesh HDF5 metadata, state-point APIs, mesh warnings,… Remove the unrelated birth-mesh feature from a PR intended to resolve [#4183], or link the birth-mesh work to its appropriate issue and submit the [#4183] fix separately.
Docstring Coverage Warning Docstring coverage is 24.05% which is insufficient. The required threshold is 80.00%. Docstring coverage is scoped to functions touched by this diff. Analyzed 79 functions across 19 files. (5 skipped:… Write docstrings for the functions missing them to satisfy the coverage threshold.
Passed Passed checks: 2 passed
Check name Status Explanation
Title check Passed The title clearly and concisely identifies the primary change: adding fission-bank birth-mesh tagging.
Description check Passed The description is comprehensive and covers the change, motivation, design, compatibility, tests, AI assistance, and checklist. The linked issue field remains as “Fixes # (issue),” but this is a minor…
Full details: Linked Issues check

Explanation

Direct issue [#4183] requires a general multigroup FileSource fix. The PR diff only changes source-file version writing in openmc/source.py and adds birth-mesh tag reading and writing. The continuous-energy birth_mesh_ce test checks tag propagation, not multigroup group-index interpretation. No change establishes preservation of source groups, acceptance of group-0 sites under default bounds, or prevention of invalid group indices.

Full details: Out of Scope Changes check

Explanation

The PR changes are outside the scope of directly linked issue [#4183]. Settings.birth_mesh, SourceSite.birth_mesh_bin, transport tagging, birth-mesh HDF5 metadata, state-point APIs, mesh warnings, and their tests implement a separate birth-mesh feature. The PR description also states that the [#4183] multigroup FileSource fix remains unimplemented.

Full details: Docstring Coverage

Explanation

Docstring coverage is 24.05% which is insufficient. The required threshold is 80.00%. Docstring coverage is scoped to functions touched by this diff. Analyzed 79 functions across 19 files. (5 skipped: 5 unsupported.)

  • Fix all pre-merge checks with AI
✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create a new PR
  • Autofix · Keep fixing CodeRabbit findings and required CI, and resolving merge conflicts

Comment @coderabbitai help to get the list of available commands.

@paulromano

Copy link
Copy Markdown
Contributor

@ondrejch Thanks a lot for your recent contributions. I wanted to personally let you know that the @openmc-dev/technical-committee has decided to enforce a limit on the number of open PRs (5) from any given individual in order to more effectively deal with the high volume of PRs that the project is receiving. Our team has limited bandwidth and we want to make sure that we're able to clear out the backlog of older PRs. In the meantime, this means that because you have 6 open PRs you won't be able to open further ones without the existing ones being resolved. Rest assured, we do intend to get to them but we ask for your patience as it may take some time in light of the long backlog.

@ondrejch

Copy link
Copy Markdown
Author

Thanks for letting me know, Paul. That makes sense, and I won't open any new PRs until some of these are resolved. In case it helps with the order, I'd suggest the three bug fixes first (#4180, fission sites rejected by the time cutoff; #4182, the double filter weighting with EnergyoutFilter and DelayedGroupFilter; and #4191, the multigroup FileSource groups of #4183), then #4178 (the lifetime clock and delayed group of (n,xn) and split neutrons), and then #4195, which is stacked on #4182, and #4199.

@GuySten

GuySten commented Oct 16, 2026

Copy link
Copy Markdown
Contributor

Thanks for the contribution! Before going further, I wanted to check whether this can already be done with existing tallies.

A neutron started from the source bank has its fission site as its birth position. So a tally combining a MeshBornFilter and a MeshFilter on the same mesh, scoring nu-fission, gives the fission matrix directly. Entry (i, j) is the neutron production in bin j by neutrons born in bin i:

tally = openmc.Tally(name='fission matrix')
tally.filters = [openmc.MeshBornFilter(mesh), openmc.MeshFilter(mesh)]
tally.scores = ['nu-fission']

# after the run
F = sp.get_tally(name='fission matrix').mean.reshape(n_bins, n_bins)

Would this cover your use case?

@ondrejch

Copy link
Copy Markdown
Author

Thanks, Guy. Yes, for the fission matrix itself that tally is the right tool, and it is what I use: my TFM exports are built from MeshBornFilter × MeshFilter tallies (nu-fission, prompt-nu-fission and absorption), with the time moments from the LifetimeMomentFilter of #4195.

The tags would only add site-level data: a saved source bank could be re-binned on the destination side (site position, energy, delayed group) after the run, without a new run. I don't need that at the moment, and on its own it isn't worth a new SourceSite member, an MPI datatype change, and a file format bump. I'll close this PR to free review time; the branch stays on my fork if a site-level use case comes up.

@ondrejch ondrejch closed this Oct 16, 2026
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