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

openmc-dev / openmc / 16124808799

07 Jul 2025 06:23PM UTC coverage: 85.255% (+0.004%) from 85.251%
16124808799

Pull #3429

github

web-flow
Merge daad6eb8c into d700d395d
Pull Request #3429: Make MCPL a Runtime Optional Dependency

123 of 192 new or added lines in 1 file covered. (64.06%)

11 existing lines in 1 file now uncovered.

52756 of 61880 relevant lines covered (85.26%)

36323397.34 hits per line

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

62.77
/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 openmc_local_mcpl_particle_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
typedef struct openmc_local_mcpl_particle_t mcpl_particle_repr_t;
50

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

60
typedef struct mcpl_file_t_ mcpl_file_t;
61
typedef struct mcpl_outfile_t_ mcpl_outfile_t;
62

63
// Function pointer types for the dynamically loaded MCPL library
64
typedef mcpl_file_t* (*mcpl_open_file_fpt)(const char* filename);
65
typedef uint64_t (*mcpl_hdr_nparticles_fpt)(mcpl_file_t* file_handle);
66
typedef const mcpl_particle_repr_t* (*mcpl_read_fpt)(mcpl_file_t* file_handle);
67
typedef void (*mcpl_close_file_fpt)(mcpl_file_t* file_handle);
68

69
typedef mcpl_outfile_t* (*mcpl_create_outfile_fpt)(const char* filename);
70
typedef void (*mcpl_hdr_set_srcname_fpt)(
71
  mcpl_outfile_t* outfile_handle, const char* srcname);
72
typedef void (*mcpl_add_particle_fpt)(
73
  mcpl_outfile_t* outfile_handle, const mcpl_particle_repr_t* particle);
74
typedef void (*mcpl_close_outfile_fpt)(mcpl_outfile_t* outfile_handle);
75

76
namespace openmc {
77

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

NEW
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
NEW
104
  const char* err = dlerror();
×
NEW
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_add_particle_fpt add_particle;
117
  mcpl_close_outfile_fpt close_outfile;
118

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

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

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

158
static LibraryHandleType g_mcpl_lib_handle = nullptr;
159
static std::unique_ptr<McplApi> g_mcpl_api;
160
static bool g_mcpl_init_attempted = false;
161
static bool g_mcpl_successfully_loaded = false;
162
static std::string g_mcpl_load_error_msg;
163
static std::once_flag g_mcpl_init_flag;
164

NEW
165
void append_error(std::string& existing_msg, const std::string& new_error)
×
166
{
NEW
167
  if (!existing_msg.empty()) {
×
NEW
168
    existing_msg += "; ";
×
169
  }
NEW
170
  existing_msg += new_error;
×
171
}
172

173
void initialize_mcpl_interface_impl()
32✔
174
{
175
  g_mcpl_init_attempted = true;
32✔
176
  g_mcpl_load_error_msg.clear();
32✔
177

178
  // Try mcpl-config
179
  if (!g_mcpl_lib_handle) {
32✔
180
    FILE* pipe = nullptr;
32✔
181
#ifdef _WIN32
182
    pipe = _popen("mcpl-config --show libpath", "r");
183
#else
184
    pipe = popen("mcpl-config --show libpath 2>/dev/null", "r");
32✔
185
#endif
186
    if (pipe) {
32✔
187
      char buffer[512];
188
      if (fgets(buffer, sizeof(buffer), pipe) != nullptr) {
32✔
189
        std::string shlibpath = buffer;
32✔
190
        // Remove trailing whitespace
191
        while (!shlibpath.empty() &&
128✔
192
               std::isspace(static_cast<unsigned char>(shlibpath.back()))) {
64✔
193
          shlibpath.pop_back();
32✔
194
        }
195

196
        if (!shlibpath.empty()) {
32✔
197
#ifdef _WIN32
198
          g_mcpl_lib_handle = LoadLibraryA(shlibpath.c_str());
199
#else
200
          g_mcpl_lib_handle = dlopen(shlibpath.c_str(), RTLD_LAZY);
32✔
201
#endif
202
          if (!g_mcpl_lib_handle) {
32✔
NEW
203
            append_error(
×
NEW
204
              g_mcpl_load_error_msg, fmt::format("From mcpl-config ({}): {}",
×
NEW
205
                                       shlibpath, get_last_library_error()));
×
206
          }
207
        }
208
      }
32✔
209
#ifdef _WIN32
210
      _pclose(pipe);
211
#else
212
      pclose(pipe);
32✔
213
#endif
214
    } else { // pipe failed to open
NEW
215
      append_error(g_mcpl_load_error_msg,
×
216
        "mcpl-config command not found or failed to execute");
217
    }
218
  }
219

220
  // Try standard library names
221
  if (!g_mcpl_lib_handle) {
32✔
222
#ifdef _WIN32
223
    const char* standard_names[] = {"mcpl.dll", "libmcpl.dll"};
224
#else
NEW
225
    const char* standard_names[] = {"libmcpl.so", "libmcpl.dylib"};
×
226
#endif
NEW
227
    for (const char* name : standard_names) {
×
228
#ifdef _WIN32
229
      g_mcpl_lib_handle = LoadLibraryA(name);
230
#else
NEW
231
      g_mcpl_lib_handle = dlopen(name, RTLD_LAZY);
×
232
#endif
NEW
233
      if (g_mcpl_lib_handle)
×
NEW
234
        break;
×
235
    }
NEW
236
    if (!g_mcpl_lib_handle) {
×
NEW
237
      append_error(
×
NEW
238
        g_mcpl_load_error_msg, fmt::format("Using standard names (e.g. {}): {}",
×
NEW
239
                                 standard_names[0], get_last_library_error()));
×
240
    }
241
  }
242

243
  if (!g_mcpl_lib_handle) {
32✔
NEW
244
    if (mpi::master) {
×
NEW
245
      warning(fmt::format("MCPL library could not be loaded. MCPL-dependent "
×
246
                          "features will be unavailable. Load attempts: {}",
NEW
247
        g_mcpl_load_error_msg.empty()
×
NEW
248
          ? "No specific error during load attempts."
×
249
          : g_mcpl_load_error_msg));
250
    }
NEW
251
    g_mcpl_successfully_loaded = false;
×
NEW
252
    return;
×
253
  }
254

255
  try {
256
    g_mcpl_api = std::make_unique<McplApi>(g_mcpl_lib_handle);
32✔
257
    g_mcpl_successfully_loaded = true;
32✔
258
    // Do not call dlclose/FreeLibrary at exit. Leaking the handle is safer
259
    // and standard practice for libraries used for the application's lifetime.
NEW
260
  } catch (const std::runtime_error& e) {
×
NEW
261
    append_error(g_mcpl_load_error_msg,
×
NEW
262
      fmt::format(
×
NEW
263
        "MCPL library loaded, but failed to bind symbols: {}", e.what()));
×
NEW
264
    if (mpi::master) {
×
NEW
265
      warning(g_mcpl_load_error_msg);
×
266
    }
267
#ifdef _WIN32
268
    FreeLibrary(g_mcpl_lib_handle);
269
#else
NEW
270
    dlclose(g_mcpl_lib_handle);
×
271
#endif
NEW
272
    g_mcpl_lib_handle = nullptr;
×
NEW
273
    g_mcpl_successfully_loaded = false;
×
NEW
274
  }
×
275
}
276

277
void initialize_mcpl_interface_if_needed()
32✔
278
{
279
  std::call_once(g_mcpl_init_flag, initialize_mcpl_interface_impl);
32✔
280
}
32✔
281

NEW
282
bool is_mcpl_interface_available()
×
283
{
NEW
284
  initialize_mcpl_interface_if_needed();
×
NEW
285
  return g_mcpl_successfully_loaded;
×
286
}
287

288
inline void ensure_mcpl_ready_or_fatal()
32✔
289
{
290
  initialize_mcpl_interface_if_needed();
32✔
291
  if (!g_mcpl_successfully_loaded) {
32✔
NEW
292
    fatal_error("MCPL functionality is required, but the MCPL library is not "
×
293
                "available or failed to initialize. Please ensure MCPL is "
294
                "installed and its library can be found (e.g., via PATH on "
295
                "Windows, LD_LIBRARY_PATH on Linux, or DYLD_LIBRARY_PATH on "
296
                "macOS). You can often install MCPL with 'pip install mcpl' or "
297
                "'conda install mcpl'.");
298
  }
299
}
32✔
300

301
SourceSite mcpl_particle_to_site(const mcpl_particle_repr_t* particle_repr)
16,000✔
302
{
303
  SourceSite site;
16,000✔
304
  switch (particle_repr->pdgcode) {
16,000✔
305
  case 2112:
16,000✔
306
    site.particle = ParticleType::neutron;
16,000✔
307
    break;
16,000✔
308
  case 22:
×
309
    site.particle = ParticleType::photon;
×
310
    break;
×
311
  case 11:
×
312
    site.particle = ParticleType::electron;
×
313
    break;
×
314
  case -11:
×
315
    site.particle = ParticleType::positron;
×
316
    break;
×
NEW
UNCOV
317
  default:
×
NEW
UNCOV
318
    fatal_error(fmt::format(
×
319
      "MCPL: Encountered unexpected PDG code {} when converting to SourceSite.",
NEW
320
      particle_repr->pdgcode));
×
321
    break;
322
  }
323

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

338
vector<SourceSite> mcpl_source_sites(std::string path)
16✔
339
{
340
  ensure_mcpl_ready_or_fatal();
16✔
341
  vector<SourceSite> sites;
16✔
342

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

350
  size_t n_particles_in_file = g_mcpl_api->hdr_nparticles(mcpl_file);
16✔
351
  size_t n_skipped = 0;
16✔
352
  if (n_particles_in_file > 0) {
16✔
353
    sites.reserve(n_particles_in_file);
16✔
354
  }
355

356
  for (size_t i = 0; i < n_particles_in_file; ++i) {
16,016✔
357
    const mcpl_particle_repr_t* p_repr = g_mcpl_api->read(mcpl_file);
16,000✔
358
    if (!p_repr) {
16,000✔
NEW
359
      warning(fmt::format("MCPL: Read error or unexpected end of file '{}' "
×
360
                          "after reading {} of {} expected particles.",
NEW
361
        path, sites.size(), n_particles_in_file));
×
NEW
362
      break;
×
363
    }
364
    if (p_repr->pdgcode == 2112 || p_repr->pdgcode == 22 ||
16,000✔
NEW
UNCOV
365
        p_repr->pdgcode == 11 || p_repr->pdgcode == -11) {
×
366
      sites.push_back(mcpl_particle_to_site(p_repr));
16,000✔
367
    } else {
NEW
368
      n_skipped++;
×
369
    }
370
  }
371

372
  g_mcpl_api->close_file(mcpl_file);
16✔
373

374
  if (n_skipped > 0 && n_particles_in_file > 0) {
16✔
NEW
375
    double percent_skipped =
×
NEW
376
      100.0 * static_cast<double>(n_skipped) / n_particles_in_file;
×
NEW
UNCOV
377
    warning(fmt::format(
×
378
      "MCPL: Skipped {} of {} total particles ({:.1f}%) in file '{}' because "
379
      "their type is not supported by OpenMC.",
380
      n_skipped, n_particles_in_file, percent_skipped, path));
381
  }
382

383
  if (sites.empty()) {
16✔
NEW
384
    if (n_particles_in_file > 0) {
×
NEW
385
      fatal_error(fmt::format(
×
386
        "MCPL file '{}' contained {} particles, but none were of the supported "
387
        "types (neutron, photon, electron, positron). OpenMC cannot proceed "
388
        "without source particles.",
389
        path, n_particles_in_file));
390
    } else {
NEW
391
      fatal_error(fmt::format(
×
392
        "MCPL file '{}' is empty or contains no particle data.", path));
393
    }
394
  }
395
  return sites;
32✔
UNCOV
396
}
×
397

398
void write_mcpl_source_bank_internal(mcpl_outfile_t* file_id,
16✔
399
  span<SourceSite> local_source_bank,
400
  const vector<int64_t>& bank_index_all_ranks)
401
{
402
  if (mpi::master) {
16✔
403
    if (!file_id) {
11✔
NEW
404
      fatal_error("MCPL: Internal error - master rank called "
×
405
                  "write_mcpl_source_bank_internal with null file_id.");
406
    }
407
    vector<SourceSite> receive_buffer;
11✔
408

409
    for (int rank_idx = 0; rank_idx < mpi::n_procs; ++rank_idx) {
27✔
410
      size_t num_sites_on_rank = static_cast<size_t>(
411
        bank_index_all_ranks[rank_idx + 1] - bank_index_all_ranks[rank_idx]);
16✔
412
      if (num_sites_on_rank == 0)
16✔
NEW
413
        continue;
×
414

415
      span<const SourceSite> sites_to_write;
16✔
416
#ifdef OPENMC_MPI
417
      if (rank_idx == mpi::rank) {
10✔
418
        sites_to_write = openmc::span<const SourceSite>(
5✔
419
          local_source_bank.data(), num_sites_on_rank);
5✔
420
      } else {
421
        if (receive_buffer.size() < num_sites_on_rank) {
5✔
422
          receive_buffer.resize(num_sites_on_rank);
5✔
423
        }
424
        MPI_Recv(receive_buffer.data(), num_sites_on_rank, mpi::source_site,
5✔
425
          rank_idx, rank_idx, mpi::intracomm, MPI_STATUS_IGNORE);
426
        sites_to_write = openmc::span<const SourceSite>(
5✔
427
          receive_buffer.data(), num_sites_on_rank);
5✔
428
      }
429
#else
430
      sites_to_write = openmc::span<const SourceSite>(
6✔
431
        local_source_bank.data(), num_sites_on_rank);
6✔
432
#endif
433
      for (const auto& site : sites_to_write) {
11,016✔
434
        mcpl_particle_repr_t p_repr {};
11,000✔
435
        p_repr.position[0] = site.r.x;
11,000✔
436
        p_repr.position[1] = site.r.y;
11,000✔
437
        p_repr.position[2] = site.r.z;
11,000✔
438
        p_repr.direction[0] = site.u.x;
11,000✔
439
        p_repr.direction[1] = site.u.y;
11,000✔
440
        p_repr.direction[2] = site.u.z;
11,000✔
441
        p_repr.ekin = site.E * 1e-6;
11,000✔
442
        p_repr.time = site.time * 1e3;
11,000✔
443
        p_repr.weight = site.wgt;
11,000✔
444
        switch (site.particle) {
11,000✔
445
        case ParticleType::neutron:
11,000✔
446
          p_repr.pdgcode = 2112;
11,000✔
447
          break;
11,000✔
448
        case ParticleType::photon:
×
NEW
449
          p_repr.pdgcode = 22;
×
450
          break;
×
451
        case ParticleType::electron:
×
NEW
452
          p_repr.pdgcode = 11;
×
453
          break;
×
UNCOV
454
        case ParticleType::positron:
×
NEW
455
          p_repr.pdgcode = -11;
×
UNCOV
456
          break;
×
NEW
UNCOV
457
        default:
×
NEW
UNCOV
458
          continue;
×
459
        }
460
        g_mcpl_api->add_particle(file_id, &p_repr);
11,000✔
461
      }
462
    }
463
  } else {
11✔
464
#ifdef OPENMC_MPI
465
    if (!local_source_bank.empty()) {
5✔
466
      MPI_Send(local_source_bank.data(), local_source_bank.size(),
5✔
467
        mpi::source_site, 0, mpi::rank, mpi::intracomm);
468
    }
469
#endif
470
  }
471
}
16✔
472

473
void write_mcpl_source_point(const char* filename, span<SourceSite> source_bank,
16✔
474
  const vector<int64_t>& bank_index)
475
{
476
  ensure_mcpl_ready_or_fatal();
16✔
477

478
  std::string filename_(filename);
16✔
479
  const auto extension = get_file_extension(filename_);
16✔
480
  if (extension.empty()) {
16✔
UNCOV
481
    filename_.append(".mcpl");
×
482
  } else if (extension != "mcpl") {
16✔
NEW
483
    warning(fmt::format("Specified filename '{}' has an extension '.{}', but "
×
484
                        "an MCPL file (.mcpl) will be written using this name.",
485
      filename, extension));
486
  }
487

488
  mcpl_outfile_t* file_id = nullptr;
16✔
489

490
  if (mpi::master) {
16✔
491
    file_id = g_mcpl_api->create_outfile(filename_.c_str());
11✔
492
    if (!file_id) {
11✔
NEW
UNCOV
493
      fatal_error(fmt::format(
×
494
        "MCPL: Failed to create output file '{}'. Check permissions and path.",
495
        filename_));
496
    }
497
    std::string src_line;
11✔
498
    if (VERSION_DEV) {
499
      src_line = fmt::format("OpenMC {}.{}.{}-dev{}", VERSION_MAJOR,
20✔
500
        VERSION_MINOR, VERSION_RELEASE, VERSION_COMMIT_COUNT);
11✔
501
    } else {
502
      src_line = fmt::format(
503
        "OpenMC {}.{}.{}", VERSION_MAJOR, VERSION_MINOR, VERSION_RELEASE);
504
    }
505
    g_mcpl_api->hdr_set_srcname(file_id, src_line.c_str());
11✔
506
  }
11✔
507

508
  write_mcpl_source_bank_internal(file_id, source_bank, bank_index);
16✔
509

510
  if (mpi::master) {
16✔
511
    if (file_id) {
11✔
512
      g_mcpl_api->close_outfile(file_id);
11✔
513
    }
514
  }
515
}
16✔
516

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