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

openmc-dev / openmc / 21887062786

10 Feb 2026 11:55PM UTC coverage: 81.899% (-0.1%) from 82.009%
21887062786

Pull #3493

github

web-flow
Merge 320b68711 into 3f20a5e22
Pull Request #3493: Implement vector fitting to replace external `vectfit` package

17361 of 24292 branches covered (71.47%)

Branch coverage included in aggregate %.

185 of 218 new or added lines in 3 files covered. (84.86%)

2770 existing lines in 69 files now uncovered.

56369 of 65733 relevant lines covered (85.75%)

51144917.16 hits per line

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

63.41
/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 <sstream>
22
#include <stdexcept>
23
#include <string>
24

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

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

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

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

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

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

76
namespace openmc {
77

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

321
SourceSite mcpl_particle_to_site(const mcpl_particle_repr_t* particle_repr)
28,000✔
322
{
323
  SourceSite site;
28,000✔
324
  site.particle = ParticleType {particle_repr->pdgcode};
28,000✔
325

326
  // Copy position and direction
327
  site.r.x = particle_repr->position[0];
28,000✔
328
  site.r.y = particle_repr->position[1];
28,000✔
329
  site.r.z = particle_repr->position[2];
28,000✔
330
  site.u.x = particle_repr->direction[0];
28,000✔
331
  site.u.y = particle_repr->direction[1];
28,000✔
332
  site.u.z = particle_repr->direction[2];
28,000✔
333
  // MCPL stores kinetic energy in [MeV], time in [ms]
334
  site.E = particle_repr->ekin * 1e6;
28,000✔
335
  site.time = particle_repr->time * 1e-3;
28,000✔
336
  site.wgt = particle_repr->weight;
28,000✔
337
  return site;
28,000✔
338
}
339

340
vector<SourceSite> mcpl_source_sites(std::string path)
28✔
341
{
342
  ensure_mcpl_ready_or_fatal();
28✔
343
  vector<SourceSite> sites;
28✔
344

345
  mcpl_file_t* mcpl_file = g_mcpl_api->open_file(path.c_str());
28✔
346
  if (!mcpl_file) {
28!
UNCOV
347
    fatal_error(fmt::format("MCPL: Could not open file '{}'. It might be "
×
348
                            "missing, inaccessible, or not a valid MCPL file.",
349
      path));
350
  }
351

352
  size_t n_particles_in_file = g_mcpl_api->hdr_nparticles(mcpl_file);
28✔
353
  if (n_particles_in_file > 0) {
28!
354
    sites.reserve(n_particles_in_file);
28✔
355
  }
356

357
  for (size_t i = 0; i < n_particles_in_file; ++i) {
28,028✔
358
    const mcpl_particle_repr_t* p_repr = g_mcpl_api->read(mcpl_file);
28,000✔
359
    if (!p_repr) {
28,000!
UNCOV
360
      warning(fmt::format("MCPL: Read error or unexpected end of file '{}' "
×
361
                          "after reading {} of {} expected particles.",
UNCOV
362
        path, sites.size(), n_particles_in_file));
×
UNCOV
363
      break;
×
364
    }
365
    sites.push_back(mcpl_particle_to_site(p_repr));
28,000✔
366
  }
367

368
  g_mcpl_api->close_file(mcpl_file);
28✔
369

370
  if (sites.empty()) {
28!
UNCOV
371
    if (n_particles_in_file > 0) {
×
UNCOV
372
      fatal_error(fmt::format(
×
373
        "MCPL file '{}' contained {} particles, but no particles could be "
374
        "read.",
375
        path, n_particles_in_file));
376
    } else {
UNCOV
377
      fatal_error(fmt::format(
×
378
        "MCPL file '{}' is empty or contains no particle data.", path));
379
    }
380
  }
381
  return sites;
56✔
382
}
×
383

384
void write_mcpl_source_bank_internal(mcpl_outfile_t* file_id,
94✔
385
  span<SourceSite> local_source_bank,
386
  const vector<int64_t>& bank_index_all_ranks)
387
{
388
  if (mpi::master) {
94✔
389
    if (!file_id) {
90!
UNCOV
390
      fatal_error("MCPL: Internal error - master rank called "
×
391
                  "write_mcpl_source_bank_internal with null file_id.");
392
    }
393
    vector<SourceSite> receive_buffer;
90✔
394

395
    for (int rank_idx = 0; rank_idx < mpi::n_procs; ++rank_idx) {
184✔
396
      size_t num_sites_on_rank = static_cast<size_t>(
397
        bank_index_all_ranks[rank_idx + 1] - bank_index_all_ranks[rank_idx]);
94✔
398
      if (num_sites_on_rank == 0)
94✔
399
        continue;
10✔
400

401
      span<const SourceSite> sites_to_write;
84✔
402
#ifdef OPENMC_MPI
403
      if (rank_idx == mpi::rank) {
36✔
404
        sites_to_write = openmc::span<const SourceSite>(
32✔
405
          local_source_bank.data(), num_sites_on_rank);
32✔
406
      } else {
407
        if (receive_buffer.size() < num_sites_on_rank) {
4!
408
          receive_buffer.resize(num_sites_on_rank);
4✔
409
        }
410
        MPI_Recv(receive_buffer.data(), num_sites_on_rank, mpi::source_site,
4✔
411
          rank_idx, rank_idx, mpi::intracomm, MPI_STATUS_IGNORE);
412
        sites_to_write = openmc::span<const SourceSite>(
4✔
413
          receive_buffer.data(), num_sites_on_rank);
4✔
414
      }
415
#else
416
      sites_to_write = openmc::span<const SourceSite>(
48✔
417
        local_source_bank.data(), num_sites_on_rank);
48✔
418
#endif
419
      for (const auto& site : sites_to_write) {
42,194✔
420
        mcpl_particle_repr_t p_repr {};
42,110✔
421
        p_repr.position[0] = site.r.x;
42,110✔
422
        p_repr.position[1] = site.r.y;
42,110✔
423
        p_repr.position[2] = site.r.z;
42,110✔
424
        p_repr.direction[0] = site.u.x;
42,110✔
425
        p_repr.direction[1] = site.u.y;
42,110✔
426
        p_repr.direction[2] = site.u.z;
42,110✔
427
        p_repr.ekin = site.E * 1e-6;
42,110✔
428
        p_repr.time = site.time * 1e3;
42,110✔
429
        p_repr.weight = site.wgt;
42,110✔
430
        p_repr.pdgcode = site.particle.pdg_number();
42,110✔
431
        g_mcpl_api->add_particle(file_id, &p_repr);
42,110✔
432
      }
433
    }
434
  } else {
90✔
435
#ifdef OPENMC_MPI
436
    if (!local_source_bank.empty()) {
4!
437
      MPI_Send(local_source_bank.data(), local_source_bank.size(),
4✔
438
        mpi::source_site, 0, mpi::rank, mpi::intracomm);
439
    }
440
#endif
441
  }
442
}
94✔
443

444
void write_mcpl_source_point(const char* filename, span<SourceSite> source_bank,
94✔
445
  const vector<int64_t>& bank_index)
446
{
447
  ensure_mcpl_ready_or_fatal();
94✔
448

449
  std::string filename_(filename);
94✔
450
  const auto extension = get_file_extension(filename_);
94✔
451
  if (extension.empty()) {
94!
UNCOV
452
    filename_.append(".mcpl");
×
453
  } else if (extension != "mcpl") {
94!
UNCOV
454
    warning(fmt::format("Specified filename '{}' has an extension '.{}', but "
×
455
                        "an MCPL file (.mcpl) will be written using this name.",
456
      filename, extension));
457
  }
458

459
  mcpl_outfile_t* file_id = nullptr;
94✔
460

461
  if (mpi::master) {
94✔
462
    file_id = g_mcpl_api->create_outfile(filename_.c_str());
90✔
463
    if (!file_id) {
90!
UNCOV
464
      fatal_error(fmt::format(
×
465
        "MCPL: Failed to create output file '{}'. Check permissions and path.",
466
        filename_));
467
    }
468
    std::string src_line;
90✔
469
    if (VERSION_DEV) {
470
      src_line = fmt::format("OpenMC {}.{}.{}-dev{}", VERSION_MAJOR,
162✔
471
        VERSION_MINOR, VERSION_RELEASE, VERSION_COMMIT_COUNT);
90✔
472
    } else {
473
      src_line = fmt::format(
474
        "OpenMC {}.{}.{}", VERSION_MAJOR, VERSION_MINOR, VERSION_RELEASE);
475
    }
476
    g_mcpl_api->hdr_set_srcname(file_id, src_line.c_str());
90✔
477

478
    // Initialize stat:sum with -1 to indicate incomplete file (issue #3514)
479
    // This follows MCPL >= 2.1.0 convention for tracking simulation statistics
480
    // The -1 value indicates "not available" if file creation is interrupted
481
    if (g_mcpl_api->hdr_add_stat_sum) {
90!
482
      // Using key "openmc_np1" following tkittel's recommendation
483
      // Initial value of -1 prevents misleading values in case of crashes
484
      g_mcpl_api->hdr_add_stat_sum(file_id, "openmc_np1", -1.0);
90✔
485
    }
486
  }
90✔
487

488
  write_mcpl_source_bank_internal(file_id, source_bank, bank_index);
94✔
489

490
  if (mpi::master) {
94✔
491
    if (file_id) {
90!
492
      // Update stat:sum with actual particle count before closing (issue #3514)
493
      // This represents the original number of source particles in the
494
      // simulation (not the number of particles in the file)
495
      if (g_mcpl_api->hdr_add_stat_sum) {
90!
496
        // Calculate total source particles from active batches
497
        // Per issue #3514: this should be the original number of source
498
        // particles, not the number written to the file
499
        int64_t total_source_particles =
90✔
500
          static_cast<int64_t>(settings::n_batches - settings::n_inactive) *
90✔
501
          settings::gen_per_batch * settings::n_particles;
90✔
502
        // Update with actual count - this overwrites the initial -1 value
503
        g_mcpl_api->hdr_add_stat_sum(
90✔
504
          file_id, "openmc_np1", static_cast<double>(total_source_particles));
505
      }
506

507
      g_mcpl_api->close_outfile(file_id);
90✔
508
    }
509
  }
510
}
94✔
511

512
// Collision track feature with MCPL
513
void write_mcpl_collision_track_internal(mcpl_outfile_t* file_id,
10✔
514
  span<CollisionTrackSite> collision_track_bank,
515
  const vector<int64_t>& bank_index_all_ranks)
516
{
517
  if (mpi::master) {
10!
518
    if (!file_id) {
10!
UNCOV
519
      fatal_error("MCPL: Internal error - master rank called "
×
520
                  "write_mcpl_source_bank_internal with null file_id.");
521
    }
522
    vector<CollisionTrackSite> receive_buffer;
10✔
523
    vector<CollisionTrackSite> all_sites;
10✔
524
    all_sites.reserve(static_cast<size_t>(bank_index_all_ranks.back()));
10✔
525
    vector<std::string> all_blobs;
10✔
526
    all_blobs.reserve(static_cast<size_t>(bank_index_all_ranks.back()));
10✔
527

528
    for (int rank_idx = 0; rank_idx < mpi::n_procs; ++rank_idx) {
20✔
529
      size_t num_sites_on_rank = static_cast<size_t>(
530
        bank_index_all_ranks[rank_idx + 1] - bank_index_all_ranks[rank_idx]);
10✔
531
      if (num_sites_on_rank == 0)
10!
UNCOV
532
        continue;
×
533

534
      span<const CollisionTrackSite> sites_to_process;
10✔
535
#ifdef OPENMC_MPI
536
      if (rank_idx == mpi::rank) {
4!
537
        sites_to_process = openmc::span<const CollisionTrackSite>(
4✔
538
          collision_track_bank.data(), num_sites_on_rank);
4✔
539
      } else {
540
        receive_buffer.resize(num_sites_on_rank);
×
541
        MPI_Recv(receive_buffer.data(), num_sites_on_rank,
×
542
          mpi::collision_track_site, rank_idx, rank_idx, mpi::intracomm,
543
          MPI_STATUS_IGNORE);
544
        sites_to_process = openmc::span<const CollisionTrackSite>(
545
          receive_buffer.data(), num_sites_on_rank);
546
      }
547
#else
548
      sites_to_process = openmc::span<const CollisionTrackSite>(
6✔
549
        collision_track_bank.data(), num_sites_on_rank);
6✔
550
#endif
551

552
      for (const auto& site : sites_to_process) {
610✔
553
        std::ostringstream custom_data_stream;
600✔
554
        custom_data_stream << " dE : " << site.dE
600✔
555
                           << " ; event_mt : " << site.event_mt
600✔
556
                           << " ; delayed_group : " << site.delayed_group
600✔
557
                           << " ; cell_id : " << site.cell_id
600✔
558
                           << " ; nuclide_id : " << site.nuclide_id
600✔
559
                           << " ; material_id : " << site.material_id
600✔
560
                           << " ; universe_id : " << site.universe_id
600✔
561
                           << " ; n_collision : " << site.n_collision
600✔
562
                           << " ; parent_id : " << site.parent_id
600✔
563
                           << " ; progeny_id : " << site.progeny_id;
600✔
564

565
        all_blobs.push_back(custom_data_stream.str());
600✔
566
        all_sites.push_back(site);
600✔
567
      }
600✔
568
    }
569

570
    for (size_t idx = 0; idx < all_blobs.size(); ++idx) {
610✔
571
      const auto& blob = all_blobs[idx];
600✔
572
      std::string key = "blob_" + std::to_string(idx);
600✔
573
      g_mcpl_api->hdr_add_data(file_id, key.c_str(), blob.size(), blob.c_str());
600✔
574
    }
600✔
575

576
    for (const auto& site : all_sites) {
610✔
577
      mcpl_particle_repr_t p_repr {};
600✔
578
      p_repr.position[0] = site.r.x;
600✔
579
      p_repr.position[1] = site.r.y;
600✔
580
      p_repr.position[2] = site.r.z;
600✔
581
      p_repr.direction[0] = site.u.x;
600✔
582
      p_repr.direction[1] = site.u.y;
600✔
583
      p_repr.direction[2] = site.u.z;
600✔
584
      p_repr.ekin = site.E * 1e-6;
600✔
585
      p_repr.time = site.time * 1e3;
600✔
586
      p_repr.weight = site.wgt;
600✔
587
      p_repr.pdgcode = site.particle.pdg_number();
600✔
588
      g_mcpl_api->add_particle(file_id, &p_repr);
600✔
589
    }
590
  } else {
10✔
591
#ifdef OPENMC_MPI
592
    if (!collision_track_bank.empty()) {
×
593
      MPI_Send(collision_track_bank.data(), collision_track_bank.size(),
594
        mpi::collision_track_site, 0, mpi::rank, mpi::intracomm);
595
    }
596
#endif
597
  }
598
}
10✔
599

600
void write_mcpl_collision_track(const char* filename,
10✔
601
  span<CollisionTrackSite> collision_track_bank,
602
  const vector<int64_t>& bank_index)
603
{
604
  ensure_mcpl_ready_or_fatal();
10✔
605

606
  std::string filename_(filename);
10✔
607
  const auto extension = get_file_extension(filename_);
10✔
608
  if (extension.empty()) {
10!
UNCOV
609
    filename_.append(".mcpl");
×
610
  } else if (extension != "mcpl") {
10!
UNCOV
611
    warning(fmt::format("Specified filename '{}' has an extension '.{}', but "
×
612
                        "an MCPL file (.mcpl) will be written using this name.",
613
      filename, extension));
614
  }
615

616
  mcpl_outfile_t* file_id = nullptr;
10✔
617

618
  if (mpi::master) {
10!
619
    file_id = g_mcpl_api->create_outfile(filename_.c_str());
10✔
620
    if (!file_id) {
10!
UNCOV
621
      fatal_error(fmt::format(
×
622
        "MCPL: Failed to create output file '{}'. Check permissions and path.",
623
        filename_));
624
    }
625
    std::string src_line;
10✔
626
    if (VERSION_DEV) {
627
      src_line = fmt::format("OpenMC {}.{}.{}-dev{}", VERSION_MAJOR,
18✔
628
        VERSION_MINOR, VERSION_RELEASE, VERSION_COMMIT_COUNT);
10✔
629
    } else {
630
      src_line = fmt::format(
631
        "OpenMC {}.{}.{}", VERSION_MAJOR, VERSION_MINOR, VERSION_RELEASE);
632
    }
633

634
    g_mcpl_api->hdr_set_srcname(file_id, src_line.c_str());
10✔
635
  }
10✔
636
  write_mcpl_collision_track_internal(
10✔
637
    file_id, collision_track_bank, bank_index);
638

639
  if (mpi::master) {
10!
640
    if (file_id) {
10!
641
      g_mcpl_api->close_outfile(file_id);
10✔
642
    }
643
  }
644
}
10✔
645

646
} // 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

© 2026 Coveralls, Inc