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

openmc-dev / openmc / 18299973882

07 Oct 2025 02:14AM UTC coverage: 81.92% (-3.3%) from 85.194%
18299973882

push

github

web-flow
Switch to using coveralls github action for reporting (#3594)

16586 of 23090 branches covered (71.83%)

Branch coverage included in aggregate %.

53703 of 62712 relevant lines covered (85.63%)

43428488.21 hits per line

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

51.59
/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_stat_sum_fpt = void (*)(
71
  mcpl_outfile_t* outfile_handle, const char* key, double value);
72

73
namespace openmc {
74

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

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

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

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

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

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

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

165
static LibraryHandleType g_mcpl_lib_handle = nullptr;
166
static std::unique_ptr<McplApi> g_mcpl_api;
167
static bool g_mcpl_init_attempted = false;
168
static bool g_mcpl_successfully_loaded = false;
169
static std::string g_mcpl_load_error_msg;
170
static std::once_flag g_mcpl_init_flag;
171

172
void append_error(std::string& existing_msg, const std::string& new_error)
×
173
{
174
  if (!existing_msg.empty()) {
×
175
    existing_msg += "; ";
×
176
  }
177
  existing_msg += new_error;
×
178
}
×
179

180
void initialize_mcpl_interface_impl()
81✔
181
{
182
  g_mcpl_init_attempted = true;
81✔
183
  g_mcpl_load_error_msg.clear();
81✔
184

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

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

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

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

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

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

289
bool is_mcpl_interface_available()
33✔
290
{
291
  initialize_mcpl_interface_if_needed();
33✔
292
  return g_mcpl_successfully_loaded;
33✔
293
}
294

295
inline void ensure_mcpl_ready_or_fatal()
136✔
296
{
297
  initialize_mcpl_interface_if_needed();
136✔
298
  if (!g_mcpl_successfully_loaded) {
136!
299
    fatal_error("MCPL functionality is required, but the MCPL library is not "
×
300
                "available or failed to initialize. Please ensure MCPL is "
301
                "installed and its library can be found (e.g., via PATH on "
302
                "Windows, LD_LIBRARY_PATH on Linux, or DYLD_LIBRARY_PATH on "
303
                "macOS). You can often install MCPL with 'pip install mcpl' or "
304
                "'conda install mcpl'.");
305
  }
306
}
136✔
307

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

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

345
vector<SourceSite> mcpl_source_sites(std::string path)
32✔
346
{
347
  ensure_mcpl_ready_or_fatal();
32✔
348
  vector<SourceSite> sites;
32✔
349

350
  mcpl_file_t* mcpl_file = g_mcpl_api->open_file(path.c_str());
32✔
351
  if (!mcpl_file) {
32!
352
    fatal_error(fmt::format("MCPL: Could not open file '{}'. It might be "
×
353
                            "missing, inaccessible, or not a valid MCPL file.",
354
      path));
355
  }
356

357
  size_t n_particles_in_file = g_mcpl_api->hdr_nparticles(mcpl_file);
32✔
358
  size_t n_skipped = 0;
32✔
359
  if (n_particles_in_file > 0) {
32!
360
    sites.reserve(n_particles_in_file);
32✔
361
  }
362

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

379
  g_mcpl_api->close_file(mcpl_file);
32✔
380

381
  if (n_skipped > 0 && n_particles_in_file > 0) {
32!
382
    double percent_skipped =
×
383
      100.0 * static_cast<double>(n_skipped) / n_particles_in_file;
×
384
    warning(fmt::format(
×
385
      "MCPL: Skipped {} of {} total particles ({:.1f}%) in file '{}' because "
386
      "their type is not supported by OpenMC.",
387
      n_skipped, n_particles_in_file, percent_skipped, path));
388
  }
389

390
  if (sites.empty()) {
32!
391
    if (n_particles_in_file > 0) {
×
392
      fatal_error(fmt::format(
×
393
        "MCPL file '{}' contained {} particles, but none were of the supported "
394
        "types (neutron, photon, electron, positron). OpenMC cannot proceed "
395
        "without source particles.",
396
        path, n_particles_in_file));
397
    } else {
398
      fatal_error(fmt::format(
×
399
        "MCPL file '{}' is empty or contains no particle data.", path));
400
    }
401
  }
402
  return sites;
64✔
403
}
×
404

405
void write_mcpl_source_bank_internal(mcpl_outfile_t* file_id,
104✔
406
  span<SourceSite> local_source_bank,
407
  const vector<int64_t>& bank_index_all_ranks)
408
{
409
  if (mpi::master) {
104✔
410
    if (!file_id) {
99!
411
      fatal_error("MCPL: Internal error - master rank called "
×
412
                  "write_mcpl_source_bank_internal with null file_id.");
413
    }
414
    vector<SourceSite> receive_buffer;
99✔
415

416
    for (int rank_idx = 0; rank_idx < mpi::n_procs; ++rank_idx) {
203✔
417
      size_t num_sites_on_rank = static_cast<size_t>(
418
        bank_index_all_ranks[rank_idx + 1] - bank_index_all_ranks[rank_idx]);
104✔
419
      if (num_sites_on_rank == 0)
104✔
420
        continue;
11✔
421

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

480
void write_mcpl_source_point(const char* filename, span<SourceSite> source_bank,
104✔
481
  const vector<int64_t>& bank_index)
482
{
483
  ensure_mcpl_ready_or_fatal();
104✔
484

485
  std::string filename_(filename);
104✔
486
  const auto extension = get_file_extension(filename_);
104✔
487
  if (extension.empty()) {
104!
488
    filename_.append(".mcpl");
×
489
  } else if (extension != "mcpl") {
104!
490
    warning(fmt::format("Specified filename '{}' has an extension '.{}', but "
×
491
                        "an MCPL file (.mcpl) will be written using this name.",
492
      filename, extension));
493
  }
494

495
  mcpl_outfile_t* file_id = nullptr;
104✔
496

497
  if (mpi::master) {
104✔
498
    file_id = g_mcpl_api->create_outfile(filename_.c_str());
99✔
499
    if (!file_id) {
99!
500
      fatal_error(fmt::format(
×
501
        "MCPL: Failed to create output file '{}'. Check permissions and path.",
502
        filename_));
503
    }
504
    std::string src_line;
99✔
505
    if (VERSION_DEV) {
506
      src_line = fmt::format("OpenMC {}.{}.{}-dev{}", VERSION_MAJOR,
180✔
507
        VERSION_MINOR, VERSION_RELEASE, VERSION_COMMIT_COUNT);
99✔
508
    } else {
509
      src_line = fmt::format(
510
        "OpenMC {}.{}.{}", VERSION_MAJOR, VERSION_MINOR, VERSION_RELEASE);
511
    }
512
    g_mcpl_api->hdr_set_srcname(file_id, src_line.c_str());
99✔
513

514
    // Initialize stat:sum with -1 to indicate incomplete file (issue #3514)
515
    // This follows MCPL >= 2.1.0 convention for tracking simulation statistics
516
    // The -1 value indicates "not available" if file creation is interrupted
517
    if (g_mcpl_api->hdr_add_stat_sum) {
99!
518
      // Using key "openmc_np1" following tkittel's recommendation
519
      // Initial value of -1 prevents misleading values in case of crashes
520
      g_mcpl_api->hdr_add_stat_sum(file_id, "openmc_np1", -1.0);
99✔
521
    }
522
  }
99✔
523

524
  write_mcpl_source_bank_internal(file_id, source_bank, bank_index);
104✔
525

526
  if (mpi::master) {
104✔
527
    if (file_id) {
99!
528
      // Update stat:sum with actual particle count before closing (issue #3514)
529
      // This represents the original number of source particles in the
530
      // simulation (not the number of particles in the file)
531
      if (g_mcpl_api->hdr_add_stat_sum) {
99!
532
        // Calculate total source particles from active batches
533
        // Per issue #3514: this should be the original number of source
534
        // particles, not the number written to the file
535
        int64_t total_source_particles =
99✔
536
          static_cast<int64_t>(settings::n_batches - settings::n_inactive) *
99✔
537
          settings::gen_per_batch * settings::n_particles;
99✔
538
        // Update with actual count - this overwrites the initial -1 value
539
        g_mcpl_api->hdr_add_stat_sum(
99✔
540
          file_id, "openmc_np1", static_cast<double>(total_source_particles));
541
      }
542

543
      g_mcpl_api->close_outfile(file_id);
99✔
544
    }
545
  }
546
}
104✔
547

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