Skip to content

Commit e36c0ae

Browse files
bpolaniapaulromano
andauthored
Add stat:sum field to MCPL files for proper weight normalization (#3522)
Co-authored-by: Paul Romano <paul.k.romano@gmail.com>
1 parent 4500f07 commit e36c0ae

5 files changed

Lines changed: 225 additions & 2 deletions

File tree

include/openmc/mcpl_interface.h

Lines changed: 11 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -19,8 +19,17 @@ namespace openmc {
1919
//! \return Vector of source sites
2020
vector<SourceSite> mcpl_source_sites(std::string path);
2121

22-
//! Write an MCPL source file
23-
//
22+
//! Write an MCPL source file with stat:sum metadata
23+
//!
24+
//! This function writes particle data to an MCPL file. For MCPL >= 2.1.0,
25+
//! it includes a stat:sum field (key: "openmc_np1") containing the total
26+
//! number of source particles, which is essential for proper file merging
27+
//! and weight normalization when using MCPL files with McStas/McXtrace.
28+
//!
29+
//! The stat:sum field follows the crash-safety pattern:
30+
//! - Initially set to -1 when opening (indicates incomplete file)
31+
//! - Updated with actual particle count before closing
32+
//!
2433
//! \param[in] filename Path to MCPL file
2534
//! \param[in] source_bank Vector of SourceSites to write to file for this
2635
//! MPI rank.

src/mcpl_interface.cpp

Lines changed: 36 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -67,6 +67,8 @@ using mcpl_hdr_set_srcname_fpt = void (*)(
6767
using mcpl_add_particle_fpt = void (*)(
6868
mcpl_outfile_t* outfile_handle, const mcpl_particle_repr_t* particle);
6969
using mcpl_close_outfile_fpt = void (*)(mcpl_outfile_t* outfile_handle);
70+
using mcpl_hdr_add_stat_sum_fpt = void (*)(
71+
mcpl_outfile_t* outfile_handle, const char* key, double value);
7072

7173
namespace openmc {
7274

@@ -110,6 +112,7 @@ struct McplApi {
110112
mcpl_hdr_set_srcname_fpt hdr_set_srcname;
111113
mcpl_add_particle_fpt add_particle;
112114
mcpl_close_outfile_fpt close_outfile;
115+
mcpl_hdr_add_stat_sum_fpt hdr_add_stat_sum;
113116

114117
explicit McplApi(LibraryHandleType lib_handle)
115118
{
@@ -147,6 +150,15 @@ struct McplApi {
147150
load_symbol_platform("mcpl_add_particle"));
148151
close_outfile = reinterpret_cast<mcpl_close_outfile_fpt>(
149152
load_symbol_platform("mcpl_close_outfile"));
153+
154+
// Try to load mcpl_hdr_add_stat_sum (available in MCPL >= 2.1.0)
155+
// Set to nullptr if not available for graceful fallback
156+
try {
157+
hdr_add_stat_sum = reinterpret_cast<mcpl_hdr_add_stat_sum_fpt>(
158+
load_symbol_platform("mcpl_hdr_add_stat_sum"));
159+
} catch (const std::runtime_error&) {
160+
hdr_add_stat_sum = nullptr;
161+
}
150162
}
151163
};
152164

@@ -498,12 +510,36 @@ void write_mcpl_source_point(const char* filename, span<SourceSite> source_bank,
498510
"OpenMC {}.{}.{}", VERSION_MAJOR, VERSION_MINOR, VERSION_RELEASE);
499511
}
500512
g_mcpl_api->hdr_set_srcname(file_id, src_line.c_str());
513+
514+
// Initialize stat:sum with -1 to indicate incomplete file (issue #3514)
515+
// This follows MCPL >= 2.1.0 convention for tracking simulation statistics
516+
// The -1 value indicates "not available" if file creation is interrupted
517+
if (g_mcpl_api->hdr_add_stat_sum) {
518+
// Using key "openmc_np1" following tkittel's recommendation
519+
// Initial value of -1 prevents misleading values in case of crashes
520+
g_mcpl_api->hdr_add_stat_sum(file_id, "openmc_np1", -1.0);
521+
}
501522
}
502523

503524
write_mcpl_source_bank_internal(file_id, source_bank, bank_index);
504525

505526
if (mpi::master) {
506527
if (file_id) {
528+
// Update stat:sum with actual particle count before closing (issue #3514)
529+
// This represents the original number of source particles in the
530+
// simulation (not the number of particles in the file)
531+
if (g_mcpl_api->hdr_add_stat_sum) {
532+
// Calculate total source particles from active batches
533+
// Per issue #3514: this should be the original number of source
534+
// particles, not the number written to the file
535+
int64_t total_source_particles =
536+
static_cast<int64_t>(settings::n_batches - settings::n_inactive) *
537+
settings::gen_per_batch * settings::n_particles;
538+
// Update with actual count - this overwrites the initial -1 value
539+
g_mcpl_api->hdr_add_stat_sum(
540+
file_id, "openmc_np1", static_cast<double>(total_source_particles));
541+
}
542+
507543
g_mcpl_api->close_outfile(file_id);
508544
}
509545
}

tests/cpp_unit_tests/CMakeLists.txt

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@ set(TEST_NAMES
44
test_tally
55
test_interpolate
66
test_math
7+
test_mcpl_stat_sum
78
# Add additional unit test files here
89
)
910

Lines changed: 108 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,108 @@
1+
#include <catch2/catch_test_macros.hpp>
2+
#include <cstdio>
3+
#include <string>
4+
#include <vector>
5+
6+
#include "openmc/bank.h"
7+
#include "openmc/mcpl_interface.h"
8+
9+
// Test the MCPL stat:sum functionality (issue #3514)
10+
TEST_CASE("MCPL stat:sum field")
11+
{
12+
// Check if MCPL interface is available
13+
if (!openmc::is_mcpl_interface_available()) {
14+
SKIP("MCPL library not available");
15+
}
16+
17+
SECTION("stat:sum field is written to MCPL files")
18+
{
19+
// Create a temporary filename
20+
std::string filename = "test_stat_sum.mcpl";
21+
22+
// Create some test particles
23+
std::vector<openmc::SourceSite> source_bank(100);
24+
std::vector<int64_t> bank_index = {0, 100}; // 100 particles total
25+
26+
// Initialize test particles
27+
for (int i = 0; i < 100; ++i) {
28+
source_bank[i].particle = openmc::ParticleType::neutron;
29+
source_bank[i].r = {i * 0.1, i * 0.2, i * 0.3};
30+
source_bank[i].u = {0.0, 0.0, 1.0};
31+
source_bank[i].E = 2.0e6; // 2 MeV
32+
source_bank[i].time = 0.0;
33+
source_bank[i].wgt = 1.0;
34+
}
35+
36+
// Write the MCPL file
37+
openmc::write_mcpl_source_point(filename.c_str(), source_bank, bank_index);
38+
39+
// Verify the file was created
40+
FILE* f = std::fopen(filename.c_str(), "r");
41+
REQUIRE(f != nullptr);
42+
std::fclose(f);
43+
44+
// Read the file back to check stat:sum
45+
// Note: This would require mcpl_open_file and checking the header
46+
// Since we can't easily read MCPL headers in C++ without the full MCPL API,
47+
// we rely on the Python test to verify the actual content
48+
49+
// Clean up
50+
std::remove(filename.c_str());
51+
}
52+
53+
SECTION("stat:sum uses correct particle count")
54+
{
55+
std::string filename = "test_count.mcpl";
56+
57+
// Test with different particle counts
58+
std::vector<int> test_counts = {1, 10, 100, 1000};
59+
60+
for (int count : test_counts) {
61+
std::vector<openmc::SourceSite> source_bank(count);
62+
std::vector<int64_t> bank_index = {0, count};
63+
64+
// Initialize particles
65+
for (int i = 0; i < count; ++i) {
66+
source_bank[i].particle = openmc::ParticleType::neutron;
67+
source_bank[i].r = {0.0, 0.0, 0.0};
68+
source_bank[i].u = {0.0, 0.0, 1.0};
69+
source_bank[i].E = 1.0e6;
70+
source_bank[i].time = 0.0;
71+
source_bank[i].wgt = 1.0;
72+
}
73+
74+
// Write MCPL file
75+
openmc::write_mcpl_source_point(
76+
filename.c_str(), source_bank, bank_index);
77+
78+
// The stat:sum should equal count (verified by Python test)
79+
// Here we just verify the file was created successfully
80+
FILE* f = std::fopen(filename.c_str(), "r");
81+
REQUIRE(f != nullptr);
82+
std::fclose(f);
83+
84+
// Clean up
85+
std::remove(filename.c_str());
86+
}
87+
}
88+
89+
SECTION("stat:sum handles empty particle bank")
90+
{
91+
std::string filename = "test_empty.mcpl";
92+
93+
// Create empty particle bank
94+
std::vector<openmc::SourceSite> source_bank;
95+
std::vector<int64_t> bank_index = {0};
96+
97+
// This should still create a valid MCPL file with stat:sum = 0
98+
openmc::write_mcpl_source_point(filename.c_str(), source_bank, bank_index);
99+
100+
// Verify file was created
101+
FILE* f = std::fopen(filename.c_str(), "r");
102+
REQUIRE(f != nullptr);
103+
std::fclose(f);
104+
105+
// Clean up
106+
std::remove(filename.c_str());
107+
}
108+
}
Lines changed: 69 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,69 @@
1+
"""Test for MCPL stat:sum functionality"""
2+
3+
from pathlib import Path
4+
import shutil
5+
6+
import pytest
7+
import openmc
8+
9+
10+
@pytest.mark.skipif(shutil.which("mcpl-config") is None, reason="MCPL is not available.")
11+
def test_mcpl_stat_sum_field(run_in_tmpdir):
12+
"""Test that MCPL files contain proper stat:sum field with particle count.
13+
14+
This test verifies that when OpenMC creates MCPL source files, they contain
15+
the stat:sum field. Since MCPL functions are not exposed in the Python API,
16+
this test creates an actual OpenMC simulation to generate MCPL files and
17+
then checks their content.
18+
"""
19+
20+
mcpl = pytest.importorskip("mcpl")
21+
22+
# Create a minimal working model that will generate MCPL files
23+
model = openmc.examples.pwr_pin_cell()
24+
model.settings.batches = 5
25+
model.settings.inactive = 2
26+
model.settings.particles = 1000
27+
model.settings.sourcepoint = {'mcpl': True, 'separate': True}
28+
29+
# Run a short simulation to generate MCPL files
30+
model.run(output=False)
31+
32+
# Find the generated MCPL file (from the last batch)
33+
mcpl_file = Path('source.5.mcpl')
34+
assert mcpl_file.exists(), "No MCPL files were generated"
35+
36+
# Open and verify the stat:sum field exists
37+
with mcpl.MCPLFile(mcpl_file) as f:
38+
# Check if stat:sum field exists using convenience property
39+
if hasattr(f, 'stat_sum'):
40+
# Use the convenience .stat_sum property directly
41+
stat_sum_dict = f.stat_sum
42+
assert 'openmc_np1' in stat_sum_dict, "openmc_np1 key not found in stat_sum"
43+
stat_sum_value = int(stat_sum_dict['openmc_np1'])
44+
else:
45+
# Fallback to checking comments for older MCPL versions
46+
comments = f.comments
47+
48+
# Check for stat:sum in comments (MCPL stores these as comments)
49+
stat_sum_value = None
50+
51+
for comment in comments:
52+
if 'stat:sum:openmc_np1' in comment:
53+
# Extract the value
54+
parts = comment.split(':')
55+
if len(parts) >= 4:
56+
stat_sum_value = int(parts[3].strip())
57+
break
58+
else:
59+
pytest.skip("stat:sum field not found - may be running with MCPL < 2.1.0")
60+
61+
# Verify the stat:sum value is reasonable
62+
assert stat_sum_value != -1, "stat:sum was not updated from initial -1 value"
63+
64+
# In eigenvalue mode, active batches generate source particles
65+
active_batches = model.settings.batches - model.settings.inactive # 3 active batches
66+
expected_particles = active_batches * model.settings.particles # 3000 total
67+
68+
assert stat_sum_value == expected_particles, \
69+
f"stat:sum value {stat_sum_value} doesn't match expected {expected_particles}"

0 commit comments

Comments
 (0)