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

openmc-dev / openmc / 15820849912

23 Jun 2025 09:43AM UTC coverage: 85.175% (-0.06%) from 85.239%
15820849912

Pull #3429

github

web-flow
Merge 93e9522a2 into 3d16d4b10
Pull Request #3429: Make MCPL a Runtime Optional Dependency

130 of 196 new or added lines in 1 file covered. (66.33%)

13 existing lines in 1 file now uncovered.

52595 of 61749 relevant lines covered (85.18%)

36590708.68 hits per line

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

64.68
/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
typedef void* mcpl_file_handle_repr_t;
52
typedef void* mcpl_outfile_handle_repr_t;
53

54
typedef mcpl_file_handle_repr_t (*mcpl_open_file_fpt)(const char* filename);
55
typedef uint64_t (*mcpl_hdr_nparticles_fpt)(
56
  mcpl_file_handle_repr_t file_handle);
57
typedef const mcpl_particle_repr_t* (*mcpl_read_fpt)(
58
  mcpl_file_handle_repr_t file_handle);
59
typedef void (*mcpl_close_file_fpt)(mcpl_file_handle_repr_t file_handle);
60

61
typedef mcpl_outfile_handle_repr_t (*mcpl_create_outfile_fpt)(
62
  const char* filename);
63
typedef void (*mcpl_hdr_set_srcname_fpt)(
64
  mcpl_outfile_handle_repr_t outfile_handle, const char* srcname);
65
typedef void (*mcpl_add_particle_fpt)(mcpl_outfile_handle_repr_t outfile_handle,
66
  const mcpl_particle_repr_t* particle);
67
typedef void (*mcpl_close_outfile_fpt)(
68
  mcpl_outfile_handle_repr_t outfile_handle);
69

70
namespace openmc {
71

72
#ifdef _WIN32
73
typedef HMODULE LibraryHandleType;
74
#else
75
typedef void* LibraryHandleType;
76
#endif
77

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

103
struct McplApi {
104
  mcpl_open_file_fpt open_file;
105
  mcpl_hdr_nparticles_fpt hdr_nparticles;
106
  mcpl_read_fpt read;
107
  mcpl_close_file_fpt close_file;
108
  mcpl_create_outfile_fpt create_outfile;
109
  mcpl_hdr_set_srcname_fpt hdr_set_srcname;
110
  mcpl_add_particle_fpt add_particle;
111
  mcpl_close_outfile_fpt close_outfile;
112

113
  explicit McplApi(LibraryHandleType lib_handle)
32✔
114
  {
32✔
115
    if (!lib_handle)
32✔
NEW
116
      throw std::runtime_error(
×
NEW
117
        "MCPL library handle is null during API binding.");
×
118

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

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

152
static LibraryHandleType g_mcpl_lib_handle = nullptr;
153
static std::unique_ptr<McplApi> g_mcpl_api;
154
static bool g_mcpl_init_attempted = false;
155
static bool g_mcpl_successfully_loaded = false;
156
static std::string g_mcpl_load_error_msg;
157
static std::once_flag g_mcpl_init_flag;
158

NEW
UNCOV
159
void append_error(std::string& existing_msg, const std::string& new_error)
×
160
{
NEW
UNCOV
161
  if (!existing_msg.empty()) {
×
NEW
162
    existing_msg += "; ";
×
163
  }
NEW
164
  existing_msg += new_error;
×
165
}
166

167
void mcpl_library_cleanup()
32✔
168
{
169
  g_mcpl_api.reset();
32✔
170
  if (g_mcpl_lib_handle) {
32✔
171
#ifdef _WIN32
172
    FreeLibrary(g_mcpl_lib_handle);
173
#else
174
    dlclose(g_mcpl_lib_handle);
32✔
175
#endif
176
    g_mcpl_lib_handle = nullptr;
32✔
177
  }
178
  g_mcpl_successfully_loaded = false;
32✔
179
  g_mcpl_init_attempted = false;
32✔
180
}
32✔
181

182
void initialize_mcpl_interface_impl()
32✔
183
{
184
  g_mcpl_init_attempted = true;
32✔
185
  g_mcpl_load_error_msg.clear();
32✔
186

187
  // Try mcpl-config
188
  if (!g_mcpl_lib_handle) {
32✔
189
    FILE* pipe = nullptr;
32✔
190
#ifdef _WIN32
191
    pipe = _popen("mcpl-config --show libpath", "r");
192
#else
193
    pipe = popen("mcpl-config --show libpath 2>/dev/null", "r");
32✔
194
#endif
195
    if (pipe) {
32✔
196
      char buffer[512];
197
      if (fgets(buffer, sizeof(buffer), pipe) != nullptr) {
32✔
198
        std::string shlibpath = buffer;
32✔
199
        // Remove trailing whitespace
200
        while (!shlibpath.empty() &&
128✔
201
               std::isspace(static_cast<unsigned char>(shlibpath.back()))) {
64✔
202
          shlibpath.pop_back();
32✔
203
        }
204

205
        if (!shlibpath.empty()) {
32✔
206
#ifdef _WIN32
207
          g_mcpl_lib_handle = LoadLibraryA(shlibpath.c_str());
208
#else
209
          g_mcpl_lib_handle = dlopen(shlibpath.c_str(), RTLD_LAZY);
32✔
210
#endif
211
          if (!g_mcpl_lib_handle) {
32✔
NEW
212
            append_error(
×
NEW
213
              g_mcpl_load_error_msg, fmt::format("From mcpl-config ({}): {}",
×
NEW
214
                                       shlibpath, get_last_library_error()));
×
215
          }
216
        }
217
      }
32✔
218
#ifdef _WIN32
219
      _pclose(pipe);
220
#else
221
      pclose(pipe);
32✔
222
#endif
223
    } else { // pipe failed to open
NEW
224
      append_error(g_mcpl_load_error_msg,
×
225
        "mcpl-config command not found or failed to execute");
226
    }
227
  }
228

229
  // Try standard library names
230
  if (!g_mcpl_lib_handle) {
32✔
231
#ifdef _WIN32
232
    const char* standard_names[] = {"mcpl.dll", "libmcpl.dll"};
233
#else
NEW
234
    const char* standard_names[] = {"libmcpl.so", "libmcpl.dylib"};
×
235
#endif
NEW
236
    for (const char* name : standard_names) {
×
237
#ifdef _WIN32
238
      g_mcpl_lib_handle = LoadLibraryA(name);
239
#else
NEW
240
      g_mcpl_lib_handle = dlopen(name, RTLD_LAZY);
×
241
#endif
NEW
242
      if (g_mcpl_lib_handle)
×
NEW
243
        break;
×
244
    }
NEW
245
    if (!g_mcpl_lib_handle) {
×
NEW
246
      append_error(
×
NEW
247
        g_mcpl_load_error_msg, fmt::format("Using standard names (e.g. {}): {}",
×
NEW
248
                                 standard_names[0], get_last_library_error()));
×
249
    }
250
  }
251

252
  if (!g_mcpl_lib_handle) {
32✔
NEW
253
    if (mpi::master) {
×
NEW
254
      warning(fmt::format("MCPL library could not be loaded. MCPL-dependent "
×
255
                          "features will be unavailable. Load attempts: {}",
NEW
256
        g_mcpl_load_error_msg.empty()
×
NEW
257
          ? "No specific error during load attempts."
×
258
          : g_mcpl_load_error_msg));
259
    }
NEW
260
    g_mcpl_successfully_loaded = false;
×
NEW
261
    return;
×
262
  }
263

264
  try {
265
    g_mcpl_api = std::make_unique<McplApi>(g_mcpl_lib_handle);
32✔
266
    g_mcpl_successfully_loaded = true;
32✔
267
    std::atexit(mcpl_library_cleanup);
32✔
NEW
268
  } catch (const std::runtime_error& e) {
×
NEW
269
    append_error(g_mcpl_load_error_msg,
×
NEW
270
      fmt::format(
×
NEW
271
        "MCPL library loaded, but failed to bind symbols: {}", e.what()));
×
NEW
272
    if (mpi::master) {
×
NEW
273
      warning(g_mcpl_load_error_msg);
×
274
    }
275
#ifdef _WIN32
276
    FreeLibrary(g_mcpl_lib_handle);
277
#else
NEW
278
    dlclose(g_mcpl_lib_handle);
×
279
#endif
NEW
280
    g_mcpl_lib_handle = nullptr;
×
NEW
281
    g_mcpl_successfully_loaded = false;
×
NEW
282
  }
×
283
}
284

285
void initialize_mcpl_interface_if_needed()
32✔
286
{
287
  std::call_once(g_mcpl_init_flag, initialize_mcpl_interface_impl);
32✔
288
}
32✔
289

NEW
290
bool is_mcpl_interface_available()
×
291
{
NEW
292
  initialize_mcpl_interface_if_needed();
×
NEW
293
  return g_mcpl_successfully_loaded;
×
294
}
295

296
inline void ensure_mcpl_ready_or_fatal()
32✔
297
{
298
  initialize_mcpl_interface_if_needed();
32✔
299
  if (!g_mcpl_successfully_loaded) {
32✔
NEW
UNCOV
300
    fatal_error(fmt::format(
×
301
      "MCPL functionality is required, but the MCPL library is not available "
302
      "or failed to initialize. "
303
      "Please ensure MCPL is installed and its library can be found (e.g., via "
304
      "PATH on Windows, LD_LIBRARY_PATH on Linux, or DYLD_LIBRARY_PATH on "
305
      "macOS). "
306
      "You can often install MCPL with 'pip install mcpl'. "
307
      "Last error(s): {}",
NEW
308
      g_mcpl_load_error_msg.empty() ? "No specific error during load."
×
309
                                    : g_mcpl_load_error_msg));
310
  }
311
}
32✔
312

313
SourceSite mcpl_particle_to_site(const mcpl_particle_repr_t* particle_repr)
16,000✔
314
{
315
  SourceSite site;
16,000✔
316
  switch (particle_repr->pdgcode) {
16,000✔
317
  case 2112:
16,000✔
318
    site.particle = ParticleType::neutron;
16,000✔
319
    break;
16,000✔
320
  case 22:
×
321
    site.particle = ParticleType::photon;
×
322
    break;
×
323
  case 11:
×
324
    site.particle = ParticleType::electron;
×
325
    break;
×
UNCOV
326
  case -11:
×
327
    site.particle = ParticleType::positron;
×
328
    break;
×
NEW
329
  default:
×
NEW
UNCOV
330
    fatal_error(fmt::format(
×
331
      "MCPL: Encountered unexpected PDG code {} when converting to SourceSite.",
NEW
UNCOV
332
      particle_repr->pdgcode));
×
333
    break;
334
  }
335

336
  // Copy position and direction
337
  site.r.x = particle_repr->position[0];
16,000✔
338
  site.r.y = particle_repr->position[1];
16,000✔
339
  site.r.z = particle_repr->position[2];
16,000✔
340
  site.u.x = particle_repr->direction[0];
16,000✔
341
  site.u.y = particle_repr->direction[1];
16,000✔
342
  site.u.z = particle_repr->direction[2];
16,000✔
343
  // MCPL stores kinetic energy in [MeV], time in [ms]
344
  site.E = particle_repr->ekin * 1e6;
16,000✔
345
  site.time = particle_repr->time * 1e-3;
16,000✔
346
  site.wgt = particle_repr->weight;
16,000✔
347
  return site;
16,000✔
348
}
349

350
vector<SourceSite> mcpl_source_sites(std::string path)
16✔
351
{
352
  ensure_mcpl_ready_or_fatal();
16✔
353
  vector<SourceSite> sites;
16✔
354

355
  mcpl_file_handle_repr_t mcpl_file = g_mcpl_api->open_file(path.c_str());
16✔
356
  if (!mcpl_file) {
16✔
NEW
357
    fatal_error(fmt::format("MCPL: Could not open file '{}'. It might be "
×
358
                            "missing, inaccessible, or not a valid MCPL file.",
359
      path));
360
  }
361

362
  size_t n_particles_in_file = g_mcpl_api->hdr_nparticles(mcpl_file);
16✔
363
  if (n_particles_in_file > 0) {
16✔
364
    sites.reserve(n_particles_in_file);
16✔
365
  }
366

367
  for (size_t i = 0; i < n_particles_in_file; ++i) {
16,016✔
368
    const mcpl_particle_repr_t* p_repr = g_mcpl_api->read(mcpl_file);
16,000✔
369
    if (!p_repr) {
16,000✔
NEW
370
      warning(fmt::format("MCPL: Read error or unexpected end of file '{}' "
×
371
                          "after reading {} of {} expected particles.",
NEW
372
        path, sites.size(), n_particles_in_file));
×
NEW
373
      break;
×
374
    }
375
    if (p_repr->pdgcode == 2112 || p_repr->pdgcode == 22 ||
16,000✔
NEW
376
        p_repr->pdgcode == 11 || p_repr->pdgcode == -11) {
×
377
      sites.push_back(mcpl_particle_to_site(p_repr));
16,000✔
378
    }
379
  }
380

381
  g_mcpl_api->close_file(mcpl_file);
16✔
382

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

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

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

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

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

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

487
  mcpl_outfile_handle_repr_t file_id = nullptr;
16✔
488

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

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

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

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