• Home
  • Features
  • Pricing
  • Docs
  • Announcements
  • Sign In

openmc-dev / openmc / 16899333750

12 Aug 2025 04:54AM UTC coverage: 85.139% (-0.07%) from 85.205%
16899333750

Pull #3528

github

web-flow
Merge cb1169d6e into a34365396
Pull Request #3528: feature/split plot classes

225 of 293 new or added lines in 2 files covered. (76.79%)

13 existing lines in 1 file now uncovered.

53131 of 62405 relevant lines covered (85.14%)

36922006.74 hits per line

Source File
Press 'n' to go to next uncovered line, 'b' for previous

64.52
/src/mcpl_interface.cpp
1
#include "openmc/mcpl_interface.h"
2

3
#include "openmc/bank.h"
4
#include "openmc/error.h"
5
#include "openmc/file_utils.h"
6
#include "openmc/message_passing.h"
7
#include "openmc/settings.h"
8
#include "openmc/simulation.h"
9
#include "openmc/state_point.h"
10
#include "openmc/vector.h"
11

12
#include <fmt/core.h>
13

14
#include <cctype>
15
#include <cstdint>
16
#include <cstdio>
17
#include <cstdlib>
18
#include <cstring>
19
#include <memory>
20
#include <mutex>
21
#include <stdexcept>
22
#include <string>
23

24
#ifdef _WIN32
25
#define WIN32_LEAN_AND_MEAN
26
#include <windows.h>
27
#else
28
#include <dlfcn.h>
29
#endif
30

31
// WARNING: These declarations MUST EXACTLY MATCH the structure and function
32
// signatures of the libmcpl being loaded at runtime. Any discrepancy will
33
// likely lead to crashes or incorrect behavior. This is a maintenance risk.
34
// MCPL 2.2.0
35

36
#pragma pack(push, 1)
37
struct mcpl_particle_repr_t {
38
  double ekin;
39
  double polarisation[3];
40
  double position[3];
41
  double direction[3];
42
  double time;
43
  double weight;
44
  int32_t pdgcode;
45
  uint32_t userflags;
46
};
47
#pragma pack(pop)
48

49
// Opaque struct definitions replicating the MCPL C-API to ensure ABI
50
// compatibility without including mcpl.h. These must be kept in sync.
51
struct mcpl_file_t {
52
  void* internal;
53
};
54
struct mcpl_outfile_t {
55
  void* internal;
56
};
57

58
// Function pointer types for the dynamically loaded MCPL library
59
using mcpl_open_file_fpt = mcpl_file_t* (*)(const char* filename);
60
using mcpl_hdr_nparticles_fpt = uint64_t (*)(mcpl_file_t* file_handle);
61
using mcpl_read_fpt = const mcpl_particle_repr_t* (*)(mcpl_file_t* file_handle);
62
using mcpl_close_file_fpt = void (*)(mcpl_file_t* file_handle);
63

64
using mcpl_create_outfile_fpt = mcpl_outfile_t* (*)(const char* filename);
65
using mcpl_hdr_set_srcname_fpt = void (*)(
66
  mcpl_outfile_t* outfile_handle, const char* srcname);
67
using mcpl_add_particle_fpt = void (*)(
68
  mcpl_outfile_t* outfile_handle, const mcpl_particle_repr_t* particle);
69
using mcpl_close_outfile_fpt = void (*)(mcpl_outfile_t* outfile_handle);
70
using mcpl_hdr_add_data_fpt = void (*)(mcpl_outfile_t* outfile_handle,
71
  const char* key, uint32_t datalength, const char* data);
72
using mcpl_hdr_add_stat_sum_fpt = void (*)(
73
  mcpl_outfile_t* outfile_handle, const char* key, double value);
74

75
namespace openmc {
76

77
#ifdef _WIN32
78
using LibraryHandleType = HMODULE;
79
#else
80
using LibraryHandleType = void*;
81
#endif
82

83
std::string get_last_library_error()
×
84
{
85
#ifdef _WIN32
86
  DWORD error_code = GetLastError();
87
  if (error_code == 0)
88
    return "No error reported by system."; // More accurate than "No error."
89
  LPSTR message_buffer = nullptr;
90
  size_t size =
91
    FormatMessageA(FORMAT_MESSAGE_ALLOCATE_BUFFER | FORMAT_MESSAGE_FROM_SYSTEM |
92
                     FORMAT_MESSAGE_IGNORE_INSERTS,
93
      NULL, error_code, MAKELANGID(LANG_NEUTRAL, SUBLANG_DEFAULT),
94
      (LPSTR)&message_buffer, 0, NULL);
95
  std::string message(message_buffer, size);
96
  LocalFree(message_buffer);
97
  while (
98
    !message.empty() && (message.back() == '\n' || message.back() == '\r')) {
99
    message.pop_back();
100
  }
101
  return message;
102
#else
103
  const char* err = dlerror();
×
104
  return err ? std::string(err) : "No error reported by dlerror.";
×
105
#endif
106
}
107

108
struct McplApi {
109
  mcpl_open_file_fpt open_file;
110
  mcpl_hdr_nparticles_fpt hdr_nparticles;
111
  mcpl_read_fpt read;
112
  mcpl_close_file_fpt close_file;
113
  mcpl_create_outfile_fpt create_outfile;
114
  mcpl_hdr_set_srcname_fpt hdr_set_srcname;
115
  mcpl_add_particle_fpt add_particle;
116
  mcpl_close_outfile_fpt close_outfile;
117
  mcpl_hdr_add_data_fpt hdr_add_data;
118
  mcpl_hdr_add_stat_sum_fpt hdr_add_stat_sum;
119

120
  explicit McplApi(LibraryHandleType lib_handle)
81✔
121
  {
81✔
122
    if (!lib_handle)
81✔
123
      throw std::runtime_error(
×
124
        "MCPL library handle is null during API binding.");
×
125

126
    auto load_symbol_platform = [lib_handle](const char* name) {
810✔
127
      void* sym = nullptr;
810✔
128
#ifdef _WIN32
129
      sym = (void*)GetProcAddress(lib_handle, name);
130
#else
131
      sym = dlsym(lib_handle, name);
810✔
132
#endif
133
      if (!sym) {
810✔
134
        throw std::runtime_error(
×
135
          fmt::format("Failed to load MCPL symbol '{}': {}", name,
×
136
            get_last_library_error()));
×
137
      }
138
      return sym;
810✔
139
    };
81✔
140

141
    open_file = reinterpret_cast<mcpl_open_file_fpt>(
81✔
142
      load_symbol_platform("mcpl_open_file"));
81✔
143
    hdr_nparticles = reinterpret_cast<mcpl_hdr_nparticles_fpt>(
81✔
144
      load_symbol_platform("mcpl_hdr_nparticles"));
81✔
145
    read = reinterpret_cast<mcpl_read_fpt>(load_symbol_platform("mcpl_read"));
81✔
146
    close_file = reinterpret_cast<mcpl_close_file_fpt>(
81✔
147
      load_symbol_platform("mcpl_close_file"));
81✔
148
    create_outfile = reinterpret_cast<mcpl_create_outfile_fpt>(
81✔
149
      load_symbol_platform("mcpl_create_outfile"));
81✔
150
    hdr_set_srcname = reinterpret_cast<mcpl_hdr_set_srcname_fpt>(
81✔
151
      load_symbol_platform("mcpl_hdr_set_srcname"));
81✔
152
    add_particle = reinterpret_cast<mcpl_add_particle_fpt>(
81✔
153
      load_symbol_platform("mcpl_add_particle"));
81✔
154
    close_outfile = reinterpret_cast<mcpl_close_outfile_fpt>(
81✔
155
      load_symbol_platform("mcpl_close_outfile"));
81✔
156

157
    // Try to load mcpl_hdr_add_data (available in MCPL >= 2.1.0)
158
    // Set to nullptr if not available for graceful fallback
159
    try {
160
      hdr_add_data = reinterpret_cast<mcpl_hdr_add_data_fpt>(
81✔
161
        load_symbol_platform("mcpl_hdr_add_data"));
81✔
NEW
162
    } catch (const std::runtime_error&) {
×
NEW
163
      hdr_add_data = nullptr;
×
NEW
164
    }
×
165

166
    // Try to load mcpl_hdr_add_stat_sum (available in MCPL >= 2.1.0)
167
    // Set to nullptr if not available for graceful fallback
168
    try {
169
      hdr_add_stat_sum = reinterpret_cast<mcpl_hdr_add_stat_sum_fpt>(
81✔
170
        load_symbol_platform("mcpl_hdr_add_stat_sum"));
81✔
171
    } catch (const std::runtime_error&) {
×
172
      hdr_add_stat_sum = nullptr;
×
173
    }
×
174
  }
81✔
175
};
176

177
static LibraryHandleType g_mcpl_lib_handle = nullptr;
178
static std::unique_ptr<McplApi> g_mcpl_api;
179
static bool g_mcpl_init_attempted = false;
180
static bool g_mcpl_successfully_loaded = false;
181
static std::string g_mcpl_load_error_msg;
182
static std::once_flag g_mcpl_init_flag;
183

184
void append_error(std::string& existing_msg, const std::string& new_error)
×
185
{
186
  if (!existing_msg.empty()) {
×
187
    existing_msg += "; ";
×
188
  }
189
  existing_msg += new_error;
×
190
}
191

192
void initialize_mcpl_interface_impl()
81✔
193
{
194
  g_mcpl_init_attempted = true;
81✔
195
  g_mcpl_load_error_msg.clear();
81✔
196

197
  // Try mcpl-config
198
  if (!g_mcpl_lib_handle) {
81✔
199
    FILE* pipe = nullptr;
81✔
200
#ifdef _WIN32
201
    pipe = _popen("mcpl-config --show libpath", "r");
202
#else
203
    pipe = popen("mcpl-config --show libpath 2>/dev/null", "r");
81✔
204
#endif
205
    if (pipe) {
81✔
206
      char buffer[512];
207
      if (fgets(buffer, sizeof(buffer), pipe) != nullptr) {
81✔
208
        std::string shlibpath = buffer;
81✔
209
        // Remove trailing whitespace
210
        while (!shlibpath.empty() &&
324✔
211
               std::isspace(static_cast<unsigned char>(shlibpath.back()))) {
162✔
212
          shlibpath.pop_back();
81✔
213
        }
214

215
        if (!shlibpath.empty()) {
81✔
216
#ifdef _WIN32
217
          g_mcpl_lib_handle = LoadLibraryA(shlibpath.c_str());
218
#else
219
          g_mcpl_lib_handle = dlopen(shlibpath.c_str(), RTLD_LAZY);
81✔
220
#endif
221
          if (!g_mcpl_lib_handle) {
81✔
222
            append_error(
×
223
              g_mcpl_load_error_msg, fmt::format("From mcpl-config ({}): {}",
×
224
                                       shlibpath, get_last_library_error()));
×
225
          }
226
        }
227
      }
81✔
228
#ifdef _WIN32
229
      _pclose(pipe);
230
#else
231
      pclose(pipe);
81✔
232
#endif
233
    } else { // pipe failed to open
234
      append_error(g_mcpl_load_error_msg,
×
235
        "mcpl-config command not found or failed to execute");
236
    }
237
  }
238

239
  // Try standard library names
240
  if (!g_mcpl_lib_handle) {
81✔
241
#ifdef _WIN32
242
    const char* standard_names[] = {"mcpl.dll", "libmcpl.dll"};
243
#else
244
    const char* standard_names[] = {"libmcpl.so", "libmcpl.dylib"};
×
245
#endif
246
    for (const char* name : standard_names) {
×
247
#ifdef _WIN32
248
      g_mcpl_lib_handle = LoadLibraryA(name);
249
#else
250
      g_mcpl_lib_handle = dlopen(name, RTLD_LAZY);
×
251
#endif
252
      if (g_mcpl_lib_handle)
×
253
        break;
×
254
    }
255
    if (!g_mcpl_lib_handle) {
×
256
      append_error(
×
257
        g_mcpl_load_error_msg, fmt::format("Using standard names (e.g. {}): {}",
×
258
                                 standard_names[0], get_last_library_error()));
×
259
    }
260
  }
261

262
  if (!g_mcpl_lib_handle) {
81✔
263
    if (mpi::master) {
×
264
      warning(fmt::format("MCPL library could not be loaded. MCPL-dependent "
×
265
                          "features will be unavailable. Load attempts: {}",
266
        g_mcpl_load_error_msg.empty()
×
267
          ? "No specific error during load attempts."
×
268
          : g_mcpl_load_error_msg));
269
    }
270
    g_mcpl_successfully_loaded = false;
×
271
    return;
×
272
  }
273

274
  try {
275
    g_mcpl_api = std::make_unique<McplApi>(g_mcpl_lib_handle);
81✔
276
    g_mcpl_successfully_loaded = true;
81✔
277
    // Do not call dlclose/FreeLibrary at exit. Leaking the handle is safer
278
    // and standard practice for libraries used for the application's lifetime.
279
  } catch (const std::runtime_error& e) {
×
280
    append_error(g_mcpl_load_error_msg,
×
281
      fmt::format(
×
282
        "MCPL library loaded, but failed to bind symbols: {}", e.what()));
×
283
    if (mpi::master) {
×
284
      warning(g_mcpl_load_error_msg);
×
285
    }
286
#ifdef _WIN32
287
    FreeLibrary(g_mcpl_lib_handle);
288
#else
289
    dlclose(g_mcpl_lib_handle);
×
290
#endif
291
    g_mcpl_lib_handle = nullptr;
×
292
    g_mcpl_successfully_loaded = false;
×
293
  }
×
294
}
295

296
void initialize_mcpl_interface_if_needed()
169✔
297
{
298
  std::call_once(g_mcpl_init_flag, initialize_mcpl_interface_impl);
169✔
299
}
169✔
300

301
bool is_mcpl_interface_available()
33✔
302
{
303
  initialize_mcpl_interface_if_needed();
33✔
304
  return g_mcpl_successfully_loaded;
33✔
305
}
306

307
inline void ensure_mcpl_ready_or_fatal()
136✔
308
{
309
  initialize_mcpl_interface_if_needed();
136✔
310
  if (!g_mcpl_successfully_loaded) {
136✔
311
    fatal_error("MCPL functionality is required, but the MCPL library is not "
×
312
                "available or failed to initialize. Please ensure MCPL is "
313
                "installed and its library can be found (e.g., via PATH on "
314
                "Windows, LD_LIBRARY_PATH on Linux, or DYLD_LIBRARY_PATH on "
315
                "macOS). You can often install MCPL with 'pip install mcpl' or "
316
                "'conda install mcpl'.");
317
  }
318
}
136✔
319

320
SourceSite mcpl_particle_to_site(const mcpl_particle_repr_t* particle_repr)
32,000✔
321
{
322
  SourceSite site;
32,000✔
323
  switch (particle_repr->pdgcode) {
32,000✔
324
  case 2112:
32,000✔
325
    site.particle = ParticleType::neutron;
32,000✔
326
    break;
32,000✔
327
  case 22:
×
328
    site.particle = ParticleType::photon;
×
329
    break;
×
330
  case 11:
×
331
    site.particle = ParticleType::electron;
×
332
    break;
×
333
  case -11:
×
334
    site.particle = ParticleType::positron;
×
335
    break;
×
336
  default:
×
337
    fatal_error(fmt::format(
×
338
      "MCPL: Encountered unexpected PDG code {} when converting to SourceSite.",
339
      particle_repr->pdgcode));
×
340
    break;
341
  }
342

343
  // Copy position and direction
344
  site.r.x = particle_repr->position[0];
32,000✔
345
  site.r.y = particle_repr->position[1];
32,000✔
346
  site.r.z = particle_repr->position[2];
32,000✔
347
  site.u.x = particle_repr->direction[0];
32,000✔
348
  site.u.y = particle_repr->direction[1];
32,000✔
349
  site.u.z = particle_repr->direction[2];
32,000✔
350
  // MCPL stores kinetic energy in [MeV], time in [ms]
351
  site.E = particle_repr->ekin * 1e6;
32,000✔
352
  site.time = particle_repr->time * 1e-3;
32,000✔
353
  site.wgt = particle_repr->weight;
32,000✔
354
  return site;
32,000✔
355
}
356

357
vector<SourceSite> mcpl_source_sites(std::string path)
32✔
358
{
359
  ensure_mcpl_ready_or_fatal();
32✔
360
  vector<SourceSite> sites;
32✔
361

362
  mcpl_file_t* mcpl_file = g_mcpl_api->open_file(path.c_str());
32✔
363
  if (!mcpl_file) {
32✔
364
    fatal_error(fmt::format("MCPL: Could not open file '{}'. It might be "
×
365
                            "missing, inaccessible, or not a valid MCPL file.",
366
      path));
367
  }
368

369
  size_t n_particles_in_file = g_mcpl_api->hdr_nparticles(mcpl_file);
32✔
370
  size_t n_skipped = 0;
32✔
371
  if (n_particles_in_file > 0) {
32✔
372
    sites.reserve(n_particles_in_file);
32✔
373
  }
374

375
  for (size_t i = 0; i < n_particles_in_file; ++i) {
32,032✔
376
    const mcpl_particle_repr_t* p_repr = g_mcpl_api->read(mcpl_file);
32,000✔
377
    if (!p_repr) {
32,000✔
378
      warning(fmt::format("MCPL: Read error or unexpected end of file '{}' "
×
379
                          "after reading {} of {} expected particles.",
380
        path, sites.size(), n_particles_in_file));
×
381
      break;
×
382
    }
383
    if (p_repr->pdgcode == 2112 || p_repr->pdgcode == 22 ||
32,000✔
384
        p_repr->pdgcode == 11 || p_repr->pdgcode == -11) {
×
385
      sites.push_back(mcpl_particle_to_site(p_repr));
32,000✔
386
    } else {
387
      n_skipped++;
×
388
    }
389
  }
390

391
  g_mcpl_api->close_file(mcpl_file);
32✔
392

393
  if (n_skipped > 0 && n_particles_in_file > 0) {
32✔
394
    double percent_skipped =
×
395
      100.0 * static_cast<double>(n_skipped) / n_particles_in_file;
×
396
    warning(fmt::format(
×
397
      "MCPL: Skipped {} of {} total particles ({:.1f}%) in file '{}' because "
398
      "their type is not supported by OpenMC.",
399
      n_skipped, n_particles_in_file, percent_skipped, path));
400
  }
401

402
  if (sites.empty()) {
32✔
403
    if (n_particles_in_file > 0) {
×
404
      fatal_error(fmt::format(
×
405
        "MCPL file '{}' contained {} particles, but none were of the supported "
406
        "types (neutron, photon, electron, positron). OpenMC cannot proceed "
407
        "without source particles.",
408
        path, n_particles_in_file));
409
    } else {
410
      fatal_error(fmt::format(
×
411
        "MCPL file '{}' is empty or contains no particle data.", path));
412
    }
413
  }
414
  return sites;
64✔
415
}
×
416

417
void write_mcpl_source_bank_internal(mcpl_outfile_t* file_id,
104✔
418
  span<SourceSite> local_source_bank,
419
  const vector<int64_t>& bank_index_all_ranks)
420
{
421
  if (mpi::master) {
104✔
422
    if (!file_id) {
99✔
423
      fatal_error("MCPL: Internal error - master rank called "
×
424
                  "write_mcpl_source_bank_internal with null file_id.");
425
    }
426
    vector<SourceSite> receive_buffer;
99✔
427

428
    for (int rank_idx = 0; rank_idx < mpi::n_procs; ++rank_idx) {
203✔
429
      size_t num_sites_on_rank = static_cast<size_t>(
430
        bank_index_all_ranks[rank_idx + 1] - bank_index_all_ranks[rank_idx]);
104✔
431
      if (num_sites_on_rank == 0)
104✔
432
        continue;
11✔
433

434
      span<const SourceSite> sites_to_write;
93✔
435
#ifdef OPENMC_MPI
436
      if (rank_idx == mpi::rank) {
45✔
437
        sites_to_write = openmc::span<const SourceSite>(
40✔
438
          local_source_bank.data(), num_sites_on_rank);
40✔
439
      } else {
440
        if (receive_buffer.size() < num_sites_on_rank) {
5✔
441
          receive_buffer.resize(num_sites_on_rank);
5✔
442
        }
443
        MPI_Recv(receive_buffer.data(), num_sites_on_rank, mpi::source_site,
5✔
444
          rank_idx, rank_idx, mpi::intracomm, MPI_STATUS_IGNORE);
445
        sites_to_write = openmc::span<const SourceSite>(
5✔
446
          receive_buffer.data(), num_sites_on_rank);
5✔
447
      }
448
#else
449
      sites_to_write = openmc::span<const SourceSite>(
48✔
450
        local_source_bank.data(), num_sites_on_rank);
48✔
451
#endif
452
      for (const auto& site : sites_to_write) {
46,414✔
453
        mcpl_particle_repr_t p_repr {};
46,321✔
454
        p_repr.position[0] = site.r.x;
46,321✔
455
        p_repr.position[1] = site.r.y;
46,321✔
456
        p_repr.position[2] = site.r.z;
46,321✔
457
        p_repr.direction[0] = site.u.x;
46,321✔
458
        p_repr.direction[1] = site.u.y;
46,321✔
459
        p_repr.direction[2] = site.u.z;
46,321✔
460
        p_repr.ekin = site.E * 1e-6;
46,321✔
461
        p_repr.time = site.time * 1e3;
46,321✔
462
        p_repr.weight = site.wgt;
46,321✔
463
        switch (site.particle) {
46,321✔
464
        case ParticleType::neutron:
46,321✔
465
          p_repr.pdgcode = 2112;
46,321✔
466
          break;
46,321✔
467
        case ParticleType::photon:
×
468
          p_repr.pdgcode = 22;
×
469
          break;
×
470
        case ParticleType::electron:
×
471
          p_repr.pdgcode = 11;
×
472
          break;
×
473
        case ParticleType::positron:
×
474
          p_repr.pdgcode = -11;
×
475
          break;
×
476
        default:
×
477
          continue;
×
478
        }
479
        g_mcpl_api->add_particle(file_id, &p_repr);
46,321✔
480
      }
481
    }
482
  } else {
99✔
483
#ifdef OPENMC_MPI
484
    if (!local_source_bank.empty()) {
5✔
485
      MPI_Send(local_source_bank.data(), local_source_bank.size(),
5✔
486
        mpi::source_site, 0, mpi::rank, mpi::intracomm);
487
    }
488
#endif
489
  }
490
}
104✔
491

492
void write_mcpl_source_point(const char* filename, span<SourceSite> source_bank,
104✔
493
  const vector<int64_t>& bank_index)
494
{
495
  ensure_mcpl_ready_or_fatal();
104✔
496

497
  std::string filename_(filename);
104✔
498
  const auto extension = get_file_extension(filename_);
104✔
499
  if (extension.empty()) {
104✔
500
    filename_.append(".mcpl");
×
501
  } else if (extension != "mcpl") {
104✔
502
    warning(fmt::format("Specified filename '{}' has an extension '.{}', but "
×
503
                        "an MCPL file (.mcpl) will be written using this name.",
504
      filename, extension));
505
  }
506

507
  mcpl_outfile_t* file_id = nullptr;
104✔
508

509
  if (mpi::master) {
104✔
510
    file_id = g_mcpl_api->create_outfile(filename_.c_str());
99✔
511
    if (!file_id) {
99✔
512
      fatal_error(fmt::format(
×
513
        "MCPL: Failed to create output file '{}'. Check permissions and path.",
514
        filename_));
515
    }
516
    std::string src_line;
99✔
517
    if (VERSION_DEV) {
518
      src_line = fmt::format("OpenMC {}.{}.{}-dev{}", VERSION_MAJOR,
180✔
519
        VERSION_MINOR, VERSION_RELEASE, VERSION_COMMIT_COUNT);
99✔
520
    } else {
521
      src_line = fmt::format(
522
        "OpenMC {}.{}.{}", VERSION_MAJOR, VERSION_MINOR, VERSION_RELEASE);
523
    }
524
    g_mcpl_api->hdr_set_srcname(file_id, src_line.c_str());
99✔
525

526
    // Initialize stat:sum with -1 to indicate incomplete file (issue #3514)
527
    // This follows MCPL >= 2.1.0 convention for tracking simulation statistics
528
    // The -1 value indicates "not available" if file creation is interrupted
529
    if (g_mcpl_api->hdr_add_stat_sum) {
99✔
530
      // Using key "openmc_np1" following tkittel's recommendation
531
      // Initial value of -1 prevents misleading values in case of crashes
532
      g_mcpl_api->hdr_add_stat_sum(file_id, "openmc_np1", -1.0);
99✔
533
    }
534
  }
99✔
535

536
  write_mcpl_source_bank_internal(file_id, source_bank, bank_index);
104✔
537

538
  if (mpi::master) {
104✔
539
    if (file_id) {
99✔
540
      // Update stat:sum with actual particle count before closing (issue #3514)
541
      // This represents the original number of source particles in the
542
      // simulation (not the number of particles in the file)
543
      if (g_mcpl_api->hdr_add_stat_sum) {
99✔
544
        // Calculate total source particles from active batches
545
        // Per issue #3514: this should be the original number of source
546
        // particles, not the number written to the file
547
        int64_t total_source_particles =
99✔
548
          static_cast<int64_t>(settings::n_batches - settings::n_inactive) *
99✔
549
          settings::gen_per_batch * settings::n_particles;
99✔
550
        // Update with actual count - this overwrites the initial -1 value
551
        g_mcpl_api->hdr_add_stat_sum(
99✔
552
          file_id, "openmc_np1", static_cast<double>(total_source_particles));
553
      }
554

555
      g_mcpl_api->close_outfile(file_id);
99✔
556
    }
557
  }
558
}
104✔
559

560
} // namespace openmc
STATUS · Troubleshooting · Open an Issue · Sales · Support · CAREERS · ENTERPRISE · START FREE · SCHEDULE DEMO
ANNOUNCEMENTS · TWITTER · TOS & SLA · Supported CI Services · What's a CI service? · Automated Testing

© 2025 Coveralls, Inc