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

openmc-dev / openmc / 16366577751

18 Jul 2025 08:59AM UTC coverage: 85.115% (-0.1%) from 85.251%
16366577751

Pull #3420

github

web-flow
Merge 486047f9a into 659e43af7
Pull Request #3420: Adding material depletion function

28 of 28 new or added lines in 1 file covered. (100.0%)

437 existing lines in 24 files now uncovered.

52652 of 61860 relevant lines covered (85.11%)

36277994.88 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 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

71
namespace openmc {
72

73
#ifdef _WIN32
74
using LibraryHandleType = HMODULE;
75
#else
76
using LibraryHandleType = void*;
77
#endif
78

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

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

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

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

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

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

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

168
void initialize_mcpl_interface_impl()
59✔
169
{
170
  g_mcpl_init_attempted = true;
59✔
171
  g_mcpl_load_error_msg.clear();
59✔
172

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

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

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

238
  if (!g_mcpl_lib_handle) {
59✔
UNCOV
239
    if (mpi::master) {
×
UNCOV
240
      warning(fmt::format("MCPL library could not be loaded. MCPL-dependent "
×
241
                          "features will be unavailable. Load attempts: {}",
UNCOV
242
        g_mcpl_load_error_msg.empty()
×
UNCOV
243
          ? "No specific error during load attempts."
×
244
          : g_mcpl_load_error_msg));
245
    }
UNCOV
246
    g_mcpl_successfully_loaded = false;
×
UNCOV
247
    return;
×
248
  }
249

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

272
void initialize_mcpl_interface_if_needed()
59✔
273
{
274
  std::call_once(g_mcpl_init_flag, initialize_mcpl_interface_impl);
59✔
275
}
59✔
276

UNCOV
277
bool is_mcpl_interface_available()
×
278
{
UNCOV
279
  initialize_mcpl_interface_if_needed();
×
UNCOV
280
  return g_mcpl_successfully_loaded;
×
281
}
282

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

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

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

333
vector<SourceSite> mcpl_source_sites(std::string path)
32✔
334
{
335
  ensure_mcpl_ready_or_fatal();
32✔
336
  vector<SourceSite> sites;
32✔
337

338
  mcpl_file_t* mcpl_file = g_mcpl_api->open_file(path.c_str());
32✔
339
  if (!mcpl_file) {
32✔
UNCOV
340
    fatal_error(fmt::format("MCPL: Could not open file '{}'. It might be "
×
341
                            "missing, inaccessible, or not a valid MCPL file.",
342
      path));
343
  }
344

345
  size_t n_particles_in_file = g_mcpl_api->hdr_nparticles(mcpl_file);
32✔
346
  size_t n_skipped = 0;
32✔
347
  if (n_particles_in_file > 0) {
32✔
348
    sites.reserve(n_particles_in_file);
32✔
349
  }
350

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

367
  g_mcpl_api->close_file(mcpl_file);
32✔
368

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

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

393
void write_mcpl_source_bank_internal(mcpl_outfile_t* file_id,
27✔
394
  span<SourceSite> local_source_bank,
395
  const vector<int64_t>& bank_index_all_ranks)
396
{
397
  if (mpi::master) {
27✔
398
    if (!file_id) {
22✔
UNCOV
399
      fatal_error("MCPL: Internal error - master rank called "
×
400
                  "write_mcpl_source_bank_internal with null file_id.");
401
    }
402
    vector<SourceSite> receive_buffer;
22✔
403

404
    for (int rank_idx = 0; rank_idx < mpi::n_procs; ++rank_idx) {
49✔
405
      size_t num_sites_on_rank = static_cast<size_t>(
406
        bank_index_all_ranks[rank_idx + 1] - bank_index_all_ranks[rank_idx]);
27✔
407
      if (num_sites_on_rank == 0)
27✔
UNCOV
408
        continue;
×
409

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

468
void write_mcpl_source_point(const char* filename, span<SourceSite> source_bank,
27✔
469
  const vector<int64_t>& bank_index)
470
{
471
  ensure_mcpl_ready_or_fatal();
27✔
472

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

483
  mcpl_outfile_t* file_id = nullptr;
27✔
484

485
  if (mpi::master) {
27✔
486
    file_id = g_mcpl_api->create_outfile(filename_.c_str());
22✔
487
    if (!file_id) {
22✔
UNCOV
488
      fatal_error(fmt::format(
×
489
        "MCPL: Failed to create output file '{}'. Check permissions and path.",
490
        filename_));
491
    }
492
    std::string src_line;
22✔
493
    if (VERSION_DEV) {
494
      src_line = fmt::format("OpenMC {}.{}.{}-dev{}", VERSION_MAJOR,
40✔
495
        VERSION_MINOR, VERSION_RELEASE, VERSION_COMMIT_COUNT);
22✔
496
    } else {
497
      src_line = fmt::format(
498
        "OpenMC {}.{}.{}", VERSION_MAJOR, VERSION_MINOR, VERSION_RELEASE);
499
    }
500
    g_mcpl_api->hdr_set_srcname(file_id, src_line.c_str());
22✔
501
  }
22✔
502

503
  write_mcpl_source_bank_internal(file_id, source_bank, bank_index);
27✔
504

505
  if (mpi::master) {
27✔
506
    if (file_id) {
22✔
507
      g_mcpl_api->close_outfile(file_id);
22✔
508
    }
509
  }
510
}
27✔
511

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