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

openmc-dev / openmc / 29508759883

16 Jul 2026 02:54PM UTC coverage: 81.348% (+0.03%) from 81.315%
29508759883

push

github

web-flow
Native parametric tokamak source (#3999)

Co-authored-by: Paul Romano <paul.k.romano@gmail.com>

18327 of 26569 branches covered (68.98%)

Branch coverage included in aggregate %.

394 of 427 new or added lines in 4 files covered. (92.27%)

59841 of 69522 relevant lines covered (86.07%)

47916192.67 hits per line

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

81.45
/src/source.cpp
1
#include "openmc/source.h"
2

3
#if defined(__unix__) || (defined(__APPLE__) && defined(__MACH__))
4
#define HAS_DYNAMIC_LINKING
5
#endif
6

7
#include <algorithm> // for max
8
#include <cmath>     // for sin, cos, abs
9
#include <utility>   // for move
10

11
#ifdef HAS_DYNAMIC_LINKING
12
#include <dlfcn.h> // for dlopen, dlsym, dlclose, dlerror
13
#endif
14

15
#include "openmc/tensor.h"
16
#include <fmt/core.h>
17

18
#include "openmc/bank.h"
19
#include "openmc/capi.h"
20
#include "openmc/cell.h"
21
#include "openmc/constants.h"
22
#include "openmc/container_util.h"
23
#include "openmc/error.h"
24
#include "openmc/file_utils.h"
25
#include "openmc/geometry.h"
26
#include "openmc/hdf5_interface.h"
27
#include "openmc/material.h"
28
#include "openmc/math_functions.h"
29
#include "openmc/mcpl_interface.h"
30
#include "openmc/memory.h"
31
#include "openmc/message_passing.h"
32
#include "openmc/mgxs_interface.h"
33
#include "openmc/nuclide.h"
34
#include "openmc/random_dist.h"
35
#include "openmc/random_lcg.h"
36
#include "openmc/search.h"
37
#include "openmc/settings.h"
38
#include "openmc/simulation.h"
39
#include "openmc/state_point.h"
40
#include "openmc/string_utils.h"
41
#include "openmc/xml_interface.h"
42

43
namespace openmc {
44

45
std::atomic<int64_t> source_n_accept {0};
46
std::atomic<int64_t> source_n_reject {0};
47

48
namespace {
49

50
void validate_particle_type(ParticleType type, const std::string& context)
79,079✔
51
{
52
  if (type.is_transportable())
79,079!
53
    return;
79,079✔
54

55
  fatal_error(
×
56
    fmt::format("Unsupported source particle type '{}' (PDG {}) in {}.",
×
57
      type.str(), type.pdg_number(), context));
×
58
}
59

60
} // namespace
61

62
//==============================================================================
63
// Global variables
64
//==============================================================================
65

66
namespace model {
67

68
vector<unique_ptr<Source>> external_sources;
69

70
vector<unique_ptr<Source>> adjoint_sources;
71

72
DiscreteIndex external_sources_probability;
73

74
} // namespace model
75

76
//==============================================================================
77
// Source implementation
78
//==============================================================================
79

80
Source::Source(pugi::xml_node node)
4,563✔
81
{
82
  // Check for source strength
83
  if (check_for_node(node, "strength")) {
4,563✔
84
    strength_ = std::stod(get_node_value(node, "strength"));
8,586✔
85
    if (strength_ < 0.0) {
4,293!
86
      fatal_error("Source strength is negative.");
×
87
    }
88
  }
89

90
  // Check for additional defined constraints
91
  read_constraints(node);
4,563✔
92
}
4,563✔
93

94
unique_ptr<Source> Source::create(pugi::xml_node node)
4,563✔
95
{
96
  // if the source type is present, use it to determine the type
97
  // of object to create
98
  if (check_for_node(node, "type")) {
4,563✔
99
    std::string source_type = get_node_value(node, "type");
4,200✔
100
    if (source_type == "independent") {
4,200✔
101
      return make_unique<IndependentSource>(node);
4,058✔
102
    } else if (source_type == "file") {
142✔
103
      return make_unique<FileSource>(node);
20✔
104
    } else if (source_type == "compiled") {
122✔
105
      return make_unique<CompiledSourceWrapper>(node);
12✔
106
    } else if (source_type == "mesh") {
110✔
107
      return make_unique<MeshSource>(node);
90✔
108
    } else if (source_type == "tokamak") {
20!
109
      return make_unique<TokamakSource>(node);
20✔
110
    } else {
111
      fatal_error(fmt::format("Invalid source type '{}' found.", source_type));
×
112
    }
113
  } else {
4,195✔
114
    // support legacy source format
115
    if (check_for_node(node, "file")) {
363✔
116
      return make_unique<FileSource>(node);
12✔
117
    } else if (check_for_node(node, "library")) {
351!
118
      return make_unique<CompiledSourceWrapper>(node);
×
119
    } else {
120
      return make_unique<IndependentSource>(node);
351✔
121
    }
122
  }
123
}
124

125
void Source::read_constraints(pugi::xml_node node)
4,563✔
126
{
127
  // Check for constraints node. For backwards compatibility, if no constraints
128
  // node is given, still try searching for domain constraints from top-level
129
  // node.
130
  pugi::xml_node constraints_node = node.child("constraints");
4,563✔
131
  if (constraints_node) {
4,563✔
132
    node = constraints_node;
906✔
133
  }
134

135
  // Check for domains to reject from
136
  if (check_for_node(node, "domain_type")) {
4,563✔
137
    std::string domain_type = get_node_value(node, "domain_type");
222✔
138
    if (domain_type == "cell") {
222✔
139
      domain_type_ = DomainType::CELL;
45✔
140
    } else if (domain_type == "material") {
177✔
141
      domain_type_ = DomainType::MATERIAL;
27✔
142
    } else if (domain_type == "universe") {
150!
143
      domain_type_ = DomainType::UNIVERSE;
150✔
144
    } else {
145
      fatal_error(
×
146
        std::string("Unrecognized domain type for constraint: " + domain_type));
×
147
    }
148

149
    auto ids = get_node_array<int>(node, "domain_ids");
222✔
150
    domain_ids_.insert(ids.begin(), ids.end());
222✔
151
  }
222✔
152

153
  if (check_for_node(node, "time_bounds")) {
4,563✔
154
    auto ids = get_node_array<double>(node, "time_bounds");
5✔
155
    if (ids.size() != 2) {
5!
156
      fatal_error("Time bounds must be represented by two numbers.");
×
157
    }
158
    time_bounds_ = std::make_pair(ids[0], ids[1]);
5✔
159
  }
5✔
160
  if (check_for_node(node, "energy_bounds")) {
4,563✔
161
    auto ids = get_node_array<double>(node, "energy_bounds");
5✔
162
    if (ids.size() != 2) {
5!
163
      fatal_error("Energy bounds must be represented by two numbers.");
×
164
    }
165
    energy_bounds_ = std::make_pair(ids[0], ids[1]);
5✔
166
  }
5✔
167

168
  if (check_for_node(node, "fissionable")) {
4,563✔
169
    only_fissionable_ = get_node_value_bool(node, "fissionable");
679✔
170
  }
171

172
  // Check for how to handle rejected particles
173
  if (check_for_node(node, "rejection_strategy")) {
4,563!
174
    std::string rejection_strategy = get_node_value(node, "rejection_strategy");
×
175
    if (rejection_strategy == "kill") {
×
176
      rejection_strategy_ = RejectionStrategy::KILL;
×
177
    } else if (rejection_strategy == "resample") {
×
178
      rejection_strategy_ = RejectionStrategy::RESAMPLE;
×
179
    } else {
180
      fatal_error(std::string(
×
181
        "Unrecognized strategy source rejection: " + rejection_strategy));
182
    }
183
  }
×
184
}
4,563✔
185

186
void check_rejection_fraction(int64_t n_reject, int64_t n_accept)
16,258,525✔
187
{
188
  // Don't check unless we've hit a minimum number of total sites rejected
189
  if (n_reject < EXTSRC_REJECT_THRESHOLD)
16,258,525✔
190
    return;
191

192
  // Compute fraction of accepted sites and compare against minimum
193
  double fraction = static_cast<double>(n_accept) / n_reject;
607,565✔
194
  if (fraction <= settings::source_rejection_fraction) {
607,565✔
195
    fatal_error(fmt::format(
4✔
196
      "Too few source sites satisfied the constraints (minimum source "
197
      "rejection fraction = {}). Please check your source definition or "
198
      "set a lower value of Settings.source_rejection_fraction.",
199
      settings::source_rejection_fraction));
200
  }
201
}
202

203
SourceSite Source::sample_with_constraints(uint64_t* seed) const
16,258,525✔
204
{
205
  bool accepted = false;
16,258,525✔
206
  int64_t n_local_reject = 0;
16,258,525✔
207
  SourceSite site {};
16,258,525✔
208

209
  while (!accepted) {
49,393,456✔
210
    // Sample a source site without considering constraints yet
211
    site = this->sample(seed);
16,876,406✔
212

213
    if (constraints_applied()) {
16,876,406✔
214
      accepted = true;
215
    } else {
216
      // Check whether sampled site satisfies constraints
217
      accepted = satisfies_spatial_constraints(site.r) &&
20,825,067✔
218
                 satisfies_energy_constraints(site.E) &&
2,833,454✔
219
                 satisfies_time_constraints(site.time);
1,110,273✔
220
      if (!accepted) {
617,881✔
221
        ++n_local_reject;
617,881✔
222

223
        // Check per-particle rejection limit
224
        if (n_local_reject >= MAX_SOURCE_REJECTIONS_PER_SAMPLE) {
617,881!
225
          fatal_error("Exceeded maximum number of source rejections per "
×
226
                      "sample. Please check your source definition.");
227
        }
228

229
        // For the "kill" strategy, accept particle but set weight to 0 so that
230
        // it is terminated immediately
231
        if (rejection_strategy_ == RejectionStrategy::KILL) {
617,881!
232
          accepted = true;
×
233
          site.wgt = 0.0;
×
234
        }
235
      }
236
    }
237
  }
238

239
  // Flush local rejection count, update accept counter, and check overall
240
  // rejection fraction
241
  if (n_local_reject > 0) {
16,258,525✔
242
    source_n_reject += n_local_reject;
8,686✔
243
  }
244
  ++source_n_accept;
16,258,525✔
245
  check_rejection_fraction(source_n_reject, source_n_accept);
16,258,525✔
246

247
  return site;
16,258,521✔
248
}
249

250
bool Source::satisfies_energy_constraints(double E) const
16,273,405✔
251
{
252
  return E > energy_bounds_.first && E < energy_bounds_.second;
16,273,405!
253
}
254

255
bool Source::satisfies_time_constraints(double time) const
1,110,273✔
256
{
257
  return time > time_bounds_.first && time < time_bounds_.second;
1,110,273✔
258
}
259

260
bool Source::satisfies_spatial_constraints(Position r) const
18,597,902✔
261
{
262
  GeometryState geom_state;
18,597,902✔
263
  geom_state.r() = r;
18,597,902✔
264
  geom_state.u() = {0.0, 0.0, 1.0};
18,597,902✔
265

266
  // Reject particle if it's not in the geometry at all
267
  bool found = exhaustive_find_cell(geom_state);
18,597,902✔
268
  if (!found)
18,597,902✔
269
    return false;
270

271
  // Check the geometry state against specified domains
272
  bool accepted = true;
18,372,782✔
273
  if (!domain_ids_.empty()) {
18,372,782✔
274
    if (domain_type_ == DomainType::MATERIAL) {
952,001✔
275
      auto mat_index = geom_state.material();
5,000✔
276
      if (mat_index == MATERIAL_VOID) {
5,000!
277
        accepted = false;
278
      } else {
279
        accepted = contains(domain_ids_, model::materials[mat_index]->id());
10,000✔
280
      }
281
    } else {
282
      for (int i = 0; i < geom_state.n_coord(); i++) {
1,871,938✔
283
        auto id =
947,001✔
284
          (domain_type_ == DomainType::CELL)
285
            ? model::cells[geom_state.coord(i).cell()].get()->id_
947,001!
286
            : model::universes[geom_state.coord(i).universe()].get()->id_;
×
287
        if ((accepted = contains(domain_ids_, id)))
1,894,002✔
288
          break;
289
      }
290
    }
291
  }
292

293
  // Check if spatial site is in fissionable material
294
  if (accepted && only_fissionable_) {
18,372,782✔
295
    // Determine material
296
    auto mat_index = geom_state.material();
491,955✔
297
    if (mat_index == MATERIAL_VOID) {
491,955!
298
      accepted = false;
299
    } else {
300
      accepted = model::materials[mat_index]->fissionable();
491,955✔
301
    }
302
  }
303

304
  return accepted;
305
}
18,597,902✔
306

307
//==============================================================================
308
// IndependentSource implementation
309
//==============================================================================
310

311
IndependentSource::IndependentSource(
980✔
312
  UPtrSpace space, UPtrAngle angle, UPtrDist energy, UPtrDist time)
980✔
313
  : space_ {std::move(space)}, angle_ {std::move(angle)},
980✔
314
    energy_ {std::move(energy)}, time_ {std::move(time)}
980✔
315
{}
980✔
316

317
IndependentSource::IndependentSource(pugi::xml_node node) : Source(node)
4,409✔
318
{
319
  // Check for particle type
320
  if (check_for_node(node, "particle")) {
4,409✔
321
    auto temp_str = get_node_value(node, "particle", false, true);
4,058✔
322
    particle_ = ParticleType(temp_str);
4,058✔
323
    if (particle_ == ParticleType::photon() ||
4,058✔
324
        particle_ == ParticleType::electron() ||
4,058✔
325
        particle_ == ParticleType::positron()) {
3,940!
326
      settings::photon_transport = true;
118✔
327
    }
328
  }
4,058✔
329
  validate_particle_type(particle_, "IndependentSource");
4,409✔
330

331
  // Check for external source file
332
  if (check_for_node(node, "file")) {
4,409!
333

334
  } else {
335

336
    // Spatial distribution for external source
337
    if (check_for_node(node, "space")) {
4,409✔
338
      space_ = SpatialDistribution::create(node.child("space"));
3,389✔
339
    } else {
340
      // If no spatial distribution specified, make it a point source
341
      space_ = UPtrSpace {new SpatialPoint()};
1,020✔
342
    }
343

344
    // For backwards compatibility, check for only fissionable setting on box
345
    // source
346
    auto space_box = dynamic_cast<SpatialBox*>(space_.get());
4,409!
347
    if (space_box) {
4,409✔
348
      if (!only_fissionable_) {
1,799✔
349
        only_fissionable_ = space_box->only_fissionable();
1,120✔
350
      }
351
    }
352

353
    // Determine external source angular distribution
354
    if (check_for_node(node, "angle")) {
4,409✔
355
      angle_ = UnitSphereDistribution::create(node.child("angle"));
1,545✔
356
    } else {
357
      angle_ = UPtrAngle {new Isotropic()};
2,864✔
358
    }
359

360
    // Determine external source energy distribution
361
    if (check_for_node(node, "energy")) {
4,409✔
362
      pugi::xml_node node_dist = node.child("energy");
2,241✔
363
      energy_ = distribution_from_xml(node_dist);
2,241✔
364

365
      // For decay photon sources, use the absolute photon emission rate in
366
      // [photons/s] as the source strength
367
      if (dynamic_cast<DecaySpectrum*>(energy_.get())) {
2,241!
368
        if (strength_ != 1.0) {
25!
369
          warning(fmt::format(
×
370
            "Source strength of {} is ignored because the source uses a "
371
            "DecaySpectrum energy distribution. The source strength will be "
372
            "set from the DecaySpectrum emission rate.",
373
            strength_));
×
374
        }
375
        strength_ = energy_->integral();
25✔
376
      }
377
    } else {
378
      // Default to a Watt spectrum with parameters 0.988 MeV and 2.249 MeV^-1
379
      energy_ = UPtrDist {new Watt(0.988e6, 2.249e-6)};
2,168✔
380
    }
381

382
    // Determine external source time distribution
383
    if (check_for_node(node, "time")) {
4,409✔
384
      pugi::xml_node node_dist = node.child("time");
17✔
385
      time_ = distribution_from_xml(node_dist);
17✔
386
    } else {
387
      // Default to a Constant time T=0
388
      double T[] {0.0};
4,392✔
389
      double p[] {1.0};
4,392✔
390
      time_ = UPtrDist {new Discrete {T, p, 1}};
4,392✔
391
    }
392
  }
393
}
4,409✔
394

395
SourceSite IndependentSource::sample(uint64_t* seed) const
16,186,225✔
396
{
397
  SourceSite site {};
16,186,225✔
398
  site.particle = particle_;
16,186,225✔
399
  double r_wgt = 1.0;
16,186,225✔
400
  double E_wgt = 1.0;
16,186,225✔
401

402
  // Repeat sampling source location until a good site has been accepted
403
  bool accepted = false;
16,186,225✔
404
  int64_t n_local_reject = 0;
16,186,225✔
405

406
  while (!accepted) {
33,060,946✔
407

408
    // Sample spatial distribution
409
    auto [r, r_wgt_temp] = space_->sample(seed);
16,874,721✔
410
    site.r = r;
16,874,721✔
411
    r_wgt = r_wgt_temp;
16,874,721✔
412

413
    // Check if sampled position satisfies spatial constraints
414
    accepted = satisfies_spatial_constraints(site.r);
16,874,721✔
415

416
    // Check for rejection
417
    if (!accepted) {
16,874,721✔
418
      ++n_local_reject;
688,496✔
419
      if (n_local_reject >= MAX_SOURCE_REJECTIONS_PER_SAMPLE) {
688,496!
420
        fatal_error("Exceeded maximum number of source rejections per "
×
421
                    "sample. Please check your source definition.");
422
      }
423
    }
424
  }
425

426
  // Sample angle
427
  auto [u, u_wgt] = angle_->sample(seed);
16,186,225✔
428
  site.u = u;
16,186,225✔
429

430
  site.wgt = r_wgt * u_wgt;
16,186,225✔
431

432
  // Sample energy and time for neutron and photon sources
433
  if (settings::solver_type != SolverType::RANDOM_RAY) {
16,186,225✔
434
    // Check for monoenergetic source above maximum particle energy
435
    auto p = particle_.transport_index();
15,153,225✔
436
    auto energy_ptr = dynamic_cast<Discrete*>(energy_.get());
15,153,225!
437
    auto decay_spectrum = dynamic_cast<DecaySpectrum*>(energy_.get());
15,153,225!
438
    if (energy_ptr) {
15,153,225✔
439
      auto energies =
7,988,760✔
440
        tensor::Tensor<double>(energy_ptr->x().data(), energy_ptr->x().size());
7,988,760✔
441
      if ((energies > data::energy_max[p]).any()) {
23,966,280!
442
        fatal_error("Source energy above range of energies of at least "
×
443
                    "one cross section table");
444
      }
445
    }
7,988,760✔
446

447
    while (true) {
15,153,225✔
448
      // Sample energy spectrum. For decay photon sources, also get the parent
449
      // nuclide index to store in the source site for tallying purposes.
450
      if (decay_spectrum) {
15,153,225✔
451
        auto sample = decay_spectrum->sample_with_parent(seed);
32,500✔
452
        site.E = sample.energy;
32,500✔
453
        E_wgt = sample.weight;
32,500✔
454
        site.parent_nuclide = sample.parent_nuclide;
32,500✔
455
      } else {
456
        auto [E, E_wgt_temp] = energy_->sample(seed);
15,120,725✔
457
        site.E = E;
15,120,725✔
458
        E_wgt = E_wgt_temp;
15,120,725✔
459
      }
460

461
      // Resample if energy falls above maximum particle energy
462
      if (site.E < data::energy_max[p] &&
30,306,450!
463
          (satisfies_energy_constraints(site.E)))
15,153,225✔
464
        break;
465

466
      ++n_local_reject;
×
467
      if (n_local_reject >= MAX_SOURCE_REJECTIONS_PER_SAMPLE) {
×
468
        fatal_error("Exceeded maximum number of source rejections per "
×
469
                    "sample. Please check your source definition.");
470
      }
471
    }
472

473
    // Sample particle creation time
474
    auto [time, time_wgt] = time_->sample(seed);
15,153,225✔
475
    site.time = time;
15,153,225✔
476

477
    site.wgt *= (E_wgt * time_wgt);
15,153,225✔
478
  }
479

480
  // Flush local rejection count into global counter
481
  if (n_local_reject > 0) {
16,186,225✔
482
    source_n_reject += n_local_reject;
162,394✔
483
  }
484

485
  return site;
16,186,225✔
486
}
487

488
//==============================================================================
489
// FileSource implementation
490
//==============================================================================
491

492
FileSource::FileSource(pugi::xml_node node) : Source(node)
32✔
493
{
494
  auto path = get_node_value(node, "file", false, true);
32✔
495
  load_sites_from_file(path);
32✔
496
}
27✔
497

498
FileSource::FileSource(const std::string& path)
12✔
499
{
500
  load_sites_from_file(path);
12✔
501
}
12✔
502

503
void FileSource::load_sites_from_file(const std::string& path)
44✔
504
{
505
  // If MCPL file, use the dedicated file reader
506
  if (ends_with(path, ".mcpl") || ends_with(path, ".mcpl.gz")) {
76!
507
    sites_ = mcpl_source_sites(path);
12✔
508
  } else {
509
    // Check if source file exists
510
    if (!file_exists(path)) {
32!
511
      fatal_error(fmt::format("Source file '{}' does not exist.", path));
×
512
    }
513

514
    write_message(6, "Reading source file from {}...", path);
32✔
515

516
    // Open the binary file
517
    hid_t file_id = file_open(path, 'r', true);
32✔
518

519
    // Check to make sure this is a source file
520
    std::string filetype;
32✔
521
    read_attribute(file_id, "filetype", filetype);
32✔
522
    if (filetype != "source" && filetype != "statepoint") {
32!
523
      fatal_error("Specified starting source file not a source file type.");
×
524
    }
525

526
    // Read in the source particles
527
    read_source_bank(file_id, sites_, false);
32✔
528

529
    // Close file
530
    file_close(file_id);
27✔
531
  }
27✔
532

533
  // Make sure particles in source file have valid types. If any particle is a
534
  // photon, electron, or positron, enable photon transport so that the
535
  // appropriate cross sections are loaded.
536
  for (const auto& site : this->sites_) {
74,049✔
537
    validate_particle_type(site.particle, "FileSource");
74,010✔
538
    if (site.particle == ParticleType::photon() ||
74,010✔
539
        site.particle == ParticleType::electron() ||
74,010!
540
        site.particle == ParticleType::positron()) {
74,005!
541
      settings::photon_transport = true;
5✔
542
    }
543
  }
544
}
39✔
545

546
SourceSite FileSource::sample(uint64_t* seed) const
130,180✔
547
{
548
  // Sample a particle randomly from list
549
  size_t i_site = sites_.size() * prn(seed);
130,180✔
550
  return sites_[i_site];
130,180✔
551
}
552

553
//==============================================================================
554
// CompiledSourceWrapper implementation
555
//==============================================================================
556

557
CompiledSourceWrapper::CompiledSourceWrapper(pugi::xml_node node) : Source(node)
12✔
558
{
559
  // Get shared library path and parameters
560
  auto path = get_node_value(node, "library", false, true);
12✔
561
  std::string parameters;
12✔
562
  if (check_for_node(node, "parameters")) {
12✔
563
    parameters = get_node_value(node, "parameters", false, true);
6✔
564
  }
565
  setup(path, parameters);
12✔
566
}
12✔
567

568
void CompiledSourceWrapper::setup(
12✔
569
  const std::string& path, const std::string& parameters)
570
{
571
#ifdef HAS_DYNAMIC_LINKING
572
  // Open the library
573
  shared_library_ = dlopen(path.c_str(), RTLD_LAZY);
12✔
574
  if (!shared_library_) {
12!
575
    fatal_error("Couldn't open source library " + path);
×
576
  }
577

578
  // reset errors
579
  dlerror();
12✔
580

581
  // get the function to create the custom source from the library
582
  auto create_compiled_source = reinterpret_cast<create_compiled_source_t*>(
12✔
583
    dlsym(shared_library_, "openmc_create_source"));
12✔
584

585
  // check for any dlsym errors
586
  auto dlsym_error = dlerror();
12✔
587
  if (dlsym_error) {
12!
588
    std::string error_msg = fmt::format(
×
589
      "Couldn't open the openmc_create_source symbol: {}", dlsym_error);
×
590
    dlclose(shared_library_);
×
591
    fatal_error(error_msg);
×
592
  }
×
593

594
  // create a pointer to an instance of the custom source
595
  compiled_source_ = create_compiled_source(parameters);
12✔
596

597
#else
598
  fatal_error("Custom source libraries have not yet been implemented for "
599
              "non-POSIX systems");
600
#endif
601
}
12✔
602

603
CompiledSourceWrapper::~CompiledSourceWrapper()
24✔
604
{
605
  // Make sure custom source is cleared before closing shared library
606
  if (compiled_source_.get())
12!
607
    compiled_source_.reset();
12✔
608

609
#ifdef HAS_DYNAMIC_LINKING
610
  dlclose(shared_library_);
12✔
611
#else
612
  fatal_error("Custom source libraries have not yet been implemented for "
613
              "non-POSIX systems");
614
#endif
615
}
24✔
616

617
//==============================================================================
618
// MeshElementSpatial implementation
619
//==============================================================================
620

621
std::pair<Position, double> MeshElementSpatial::sample(uint64_t* seed) const
700,426✔
622
{
623
  return {model::meshes[mesh_index_]->sample_element(elem_index_, seed), 1.0};
700,426✔
624
}
625

626
//==============================================================================
627
// MeshSource implementation
628
//==============================================================================
629

630
MeshSource::MeshSource(pugi::xml_node node) : Source(node)
90✔
631
{
632
  int32_t mesh_id = stoi(get_node_value(node, "mesh"));
180✔
633
  int32_t mesh_idx = model::mesh_map.at(mesh_id);
90✔
634
  const auto& mesh = model::meshes[mesh_idx];
90✔
635

636
  std::vector<double> strengths;
90✔
637
  // read all source distributions and populate strengths vector for MeshSpatial
638
  // object
639
  for (auto source_node : node.children("source")) {
750✔
640
    auto src = Source::create(source_node);
660✔
641
    if (auto ptr = dynamic_cast<IndependentSource*>(src.get())) {
660!
642
      src.release();
660✔
643
      sources_.emplace_back(ptr);
660✔
644
    } else {
645
      fatal_error(
×
646
        "The source assigned to each element must be an IndependentSource.");
647
    }
648
    strengths.push_back(sources_.back()->strength());
660✔
649
  }
660✔
650

651
  // Set spatial distributions for each mesh element
652
  for (int elem_index = 0; elem_index < sources_.size(); ++elem_index) {
750✔
653
    sources_[elem_index]->set_space(
660✔
654
      std::make_unique<MeshElementSpatial>(mesh_idx, elem_index));
1,320✔
655
  }
656

657
  // Make sure sources use valid particle types
658
  for (const auto& src : sources_) {
750✔
659
    validate_particle_type(src->particle_type(), "MeshSource");
1,320✔
660
  }
661

662
  // the number of source distributions should either be one or equal to the
663
  // number of mesh elements
664
  if (sources_.size() > 1 && sources_.size() != mesh->n_bins()) {
90!
665
    fatal_error(fmt::format("Incorrect number of source distributions ({}) for "
×
666
                            "mesh source with {} elements.",
667
      sources_.size(), mesh->n_bins()));
×
668
  }
669

670
  space_ = std::make_unique<MeshSpatial>(mesh_idx, strengths);
90✔
671
}
90✔
672

673
SourceSite MeshSource::sample(uint64_t* seed) const
693,001✔
674
{
675
  // Sample a mesh element based on the relative strengths
676
  int32_t element = space_->sample_element_index(seed);
693,001✔
677

678
  // Sample the distribution for the specific mesh element; note that the
679
  // spatial distribution has been set for each element using MeshElementSpatial
680
  return source(element)->sample_with_constraints(seed);
1,386,002!
681
}
682

683
//==============================================================================
684
// TokamakSource implementation
685
//==============================================================================
686

687
TokamakSource::TokamakSource(pugi::xml_node node) : Source(node)
20✔
688
{
689
  // Read geometry parameters
690
  major_radius_ = std::stod(get_node_value(node, "major_radius"));
40✔
691
  minor_radius_ = std::stod(get_node_value(node, "minor_radius"));
40✔
692
  elongation_ = std::stod(get_node_value(node, "elongation"));
40✔
693
  triangularity_ = std::stod(get_node_value(node, "triangularity"));
40✔
694
  shafranov_shift_ = std::stod(get_node_value(node, "shafranov_shift"));
40✔
695

696
  // Read optional vertical shift
697
  if (check_for_node(node, "vertical_shift")) {
20✔
698
    vertical_shift_ = std::stod(get_node_value(node, "vertical_shift"));
10✔
699
  } else {
700
    vertical_shift_ = 0.0;
15✔
701
  }
702

703
  // Read optional toroidal angle bounds
704
  if (check_for_node(node, "phi_start")) {
20!
705
    phi_start_ = std::stod(get_node_value(node, "phi_start"));
40✔
706
  } else {
NEW
707
    phi_start_ = 0.0;
×
708
  }
709
  if (check_for_node(node, "phi_extent")) {
20!
710
    phi_extent_ = std::stod(get_node_value(node, "phi_extent"));
40✔
711
  } else {
NEW
712
    phi_extent_ = 2.0 * PI;
×
713
  }
714
  if (check_for_node(node, "n_alpha")) {
20!
715
    n_alpha_ = std::stoi(get_node_value(node, "n_alpha"));
40✔
716
  } else {
NEW
717
    n_alpha_ = 101; // Default
×
718
  }
719

720
  // Read emission profile
721
  r_over_a_ = get_node_array<double>(node, "r_over_a");
20✔
722
  emission_density_ = get_node_array<double>(node, "emission_density");
20✔
723

724
  // Read energy distribution(s)
725
  for (auto energy_node : node.children("energy")) {
40✔
726
    energy_dists_.push_back(distribution_from_xml(energy_node));
40✔
727
  }
728

729
  // Read optional time distribution; default to a delta distribution at t=0
730
  // for the same behavior as IndependentSource
731
  if (check_for_node(node, "time")) {
20!
NEW
732
    time_ = distribution_from_xml(node.child("time"));
×
733
  } else {
734
    double T[] {0.0};
20✔
735
    double p[] {1.0};
20✔
736
    time_ = UPtrDist {new Discrete {T, p, 1}};
20✔
737
  }
738

739
  // Validate inputs
740
  if (emission_density_.size() != r_over_a_.size()) {
20!
NEW
741
    fatal_error("TokamakSource: emission_density and r_over_a must have the "
×
742
                "same length.");
743
  }
744
  if (r_over_a_.size() < 2) {
20!
NEW
745
    fatal_error(
×
746
      "TokamakSource: At least 2 radial points are required for profiles.");
747
  }
748
  if (r_over_a_.front() != 0.0) {
20!
NEW
749
    fatal_error("TokamakSource: r_over_a must start at 0.");
×
750
  }
751
  if (r_over_a_.back() != 1.0) {
20!
NEW
752
    fatal_error("TokamakSource: r_over_a must end at 1.");
×
753
  }
754
  for (size_t i = 1; i < r_over_a_.size(); ++i) {
1,070✔
755
    if (r_over_a_[i] <= r_over_a_[i - 1]) {
1,050!
NEW
756
      fatal_error("TokamakSource: r_over_a must be strictly increasing.");
×
757
    }
758
  }
759
  for (size_t i = 0; i < emission_density_.size(); ++i) {
1,090✔
760
    if (emission_density_[i] < 0.0) {
1,070!
NEW
761
      fatal_error("TokamakSource: emission_density values cannot be negative.");
×
762
    }
763
  }
764
  if (major_radius_ <= 0.0) {
20!
NEW
765
    fatal_error("TokamakSource: major_radius must be > 0.");
×
766
  }
767
  if (minor_radius_ <= 0.0) {
20!
NEW
768
    fatal_error("TokamakSource: minor_radius must be > 0.");
×
769
  }
770
  if (minor_radius_ >= major_radius_) {
20!
NEW
771
    fatal_error("TokamakSource: minor_radius must be less than major_radius.");
×
772
  }
773
  if (elongation_ <= 0.0) {
20!
NEW
774
    fatal_error("TokamakSource: elongation must be > 0.");
×
775
  }
776
  if (triangularity_ < -1.0 || triangularity_ > 1.0) {
20!
NEW
777
    fatal_error("TokamakSource: triangularity must be in the range [-1, 1].");
×
778
  }
779
  if (shafranov_shift_ < 0.0) {
20!
NEW
780
    fatal_error("TokamakSource: shafranov_shift must be >= 0.");
×
781
  }
782
  if (shafranov_shift_ >= 0.5 * minor_radius_) {
20!
NEW
783
    fatal_error("TokamakSource: shafranov_shift must be less than half the "
×
784
                "minor radius.");
785
  }
786
  if (phi_extent_ <= 0.0 || phi_extent_ > 2.0 * PI) {
20!
NEW
787
    fatal_error("TokamakSource: phi_extent must be > 0 and <= 2*pi.");
×
788
  }
789
  if (n_alpha_ <= 2) {
20!
NEW
790
    fatal_error("TokamakSource: n_alpha must be > 2.");
×
791
  }
792
  if (n_alpha_ < 51) {
20✔
793
    warning("TokamakSource: n_alpha values below 51 may introduce noticeable "
10✔
794
            "discretization bias in source sampling.");
795
  }
796
  if (energy_dists_.empty()) {
20!
NEW
797
    fatal_error("TokamakSource: At least one energy distribution is required.");
×
798
  }
799
  if (energy_dists_.size() != 1 && energy_dists_.size() != r_over_a_.size()) {
20!
NEW
800
    fatal_error("TokamakSource: energy distributions must be either 1 (for all "
×
801
                "r) or match the number of r_over_a points.");
802
  }
803

804
  // Compute normalized geometry parameters
805
  epsilon_ = minor_radius_ / major_radius_;
20✔
806
  delta_tilde_ = shafranov_shift_ / minor_radius_;
20✔
807

808
  // Initialize isotropic angular distribution
809
  angle_ = UPtrAngle {new Isotropic()};
20✔
810

811
  precompute_sampling_distributions();
20✔
812
}
20✔
813

814
void TokamakSource::precompute_sampling_distributions()
20✔
815
{
816
  // Use precomputed normalized geometry parameters
817
  double eps = epsilon_;    // Inverse aspect ratio (a/R0)
20✔
818
  double Dt = delta_tilde_; // Normalized Shafranov shift (Delta/a)
20✔
819
  double delta = triangularity_;
20✔
820

821
  //==========================================================================
822
  // RADIAL CDF (computed first since it's simpler and sampled first)
823
  //==========================================================================
824
  // The marginal radial PDF is obtained by analytically integrating the joint
825
  // distribution f(r_tilde, alpha) over alpha. The result is:
826
  //
827
  //   p(r_tilde) ~ S(r_tilde) * [(1 + eps*Dt)*r_tilde
828
  //                              - (3/8)*c1*eps*r_tilde^2
829
  //                              - 2*eps*Dt*r_tilde^3]
830
  //
831
  // where the Bessel function coefficients are:
832
  //   c0 = J_0(delta) + J_2(delta)
833
  //   c1 = (J_1(2*delta) + J_3(2*delta)) / c0
834
  //
835
  // For delta -> 0, c0 -> 1 and c1 -> 0, giving the circular cross-section
836
  // limit.
837

838
  // Compute Bessel function coefficients. openmc::cyl_bessel_j handles
839
  // negative arguments (negative triangularity) via the parity relation
840
  // J_n(-x) = (-1)^n * J_n(x).
841
  double J0_d = cyl_bessel_j(0, delta);
20✔
842
  double J2_d = cyl_bessel_j(2, delta);
20✔
843
  double J1_2d = cyl_bessel_j(1, 2.0 * delta);
20✔
844
  double J3_2d = cyl_bessel_j(3, 2.0 * delta);
20✔
845
  double c0 = J0_d + J2_d;
20✔
846
  double c1 = (J1_2d + J3_2d) / c0;
20✔
847

848
  // Coefficients for the radial polynomial: A*r - B*r^2 - C*r^3
849
  radial_poly_a_ = 1.0 + eps * Dt;
20✔
850
  radial_poly_b_ = 0.375 * c1 * eps; // 3/8 * c1 * eps
20✔
851
  radial_poly_c_ = 2.0 * eps * Dt;
20✔
852

853
  // Build a refined radial grid that retains the user-specified grid points.
854
  // The emission density is interpreted as linear-linear between those points.
855
  constexpr int MIN_SUBINTERVALS = 8;
20✔
856
  constexpr double MAX_GRID_SPACING = 1.0e-3;
20✔
857
  vector<double> radial_grid {r_over_a_.front()};
20✔
858
  vector<double> radial_emission {emission_density_.front()};
20✔
859
  for (size_t i = 1; i < r_over_a_.size(); ++i) {
1,070✔
860
    double r_lo = r_over_a_[i - 1];
1,050✔
861
    double r_hi = r_over_a_[i];
1,050✔
862
    double s_lo = emission_density_[i - 1];
1,050✔
863
    double s_hi = emission_density_[i];
1,050✔
864
    int n_subintervals = std::max(MIN_SUBINTERVALS,
2,100✔
865
      static_cast<int>(std::ceil((r_hi - r_lo) / MAX_GRID_SPACING)));
1,050✔
866
    for (int j = 1; j <= n_subintervals; ++j) {
24,050✔
867
      double t = static_cast<double>(j) / n_subintervals;
23,000✔
868
      radial_grid.push_back(r_lo + t * (r_hi - r_lo));
23,000✔
869
      radial_emission.push_back(s_lo + t * (s_hi - s_lo));
23,000✔
870
    }
871
  }
872

873
  vector<double> radial_pdf(radial_grid.size());
20✔
874
  for (size_t i = 0; i < radial_grid.size(); ++i) {
23,040✔
875
    double r = radial_grid[i];
23,020✔
876
    // p(r) ~ S(r) * [A*r - B*r^2 - C*r^3]
877
    double geometric_factor =
23,020✔
878
      radial_poly_a_ * r - radial_poly_b_ * r * r - radial_poly_c_ * r * r * r;
23,020✔
879
    radial_pdf[i] = radial_emission[i] * std::max(0.0, geometric_factor);
46,020✔
880
  }
881

882
  // Check that the refined profile contains positive probability mass before
883
  // constructing the normalized tabular distribution.
884
  double total = 0.0;
885
  for (size_t i = 1; i < radial_grid.size(); ++i) {
23,020✔
886
    total += 0.5 * (radial_pdf[i - 1] + radial_pdf[i]) *
23,000✔
887
             (radial_grid[i] - radial_grid[i - 1]);
23,000✔
888
  }
889
  if (total <= 0.0) {
20!
NEW
890
    fatal_error(
×
891
      "TokamakSource: Integrated emission density is zero or negative. "
892
      "Check emission_density profile.");
893
  }
894
  radial_dist_ = make_unique<Tabular>(radial_grid.data(), radial_pdf.data(),
60✔
895
    radial_grid.size(), Interpolation::lin_lin);
20✔
896

897
  //==========================================================================
898
  // POLOIDAL CDFs (for conditional sampling of alpha given r)
899
  //==========================================================================
900
  // The conditional distribution P(alpha | r) is a mixture:
901
  //   P(alpha | r) ~ sum_k w_k(r) * I_hat_k * p_k(alpha)
902
  // where:
903
  //   - w_k(r) are the "dynamic" Bernstein weight functions (depend on r)
904
  //   - I_hat_k are the "static" normalized integrals (precomputed constants)
905
  //   - p_k(alpha) are the normalized basis distributions (precomputed CDFs)
906
  //
907
  // The static weights I_hat_k = I_k / (2*pi*c0) are:
908
  //   I_hat_0 = 1 + eps*Dt
909
  //   I_hat_1 = 1 + eps*Dt - (3/16)*c1*eps
910
  //   I_hat_2 = 1 - (3/8)*c1*eps
911
  //   I_hat_3 = 1 + eps*Dt
912
  //   I_hat_4 = 1 + (1/2)*eps*Dt - (3/16)*c1*eps
913
  //   I_hat_5 = 1 - eps*Dt - (3/8)*c1*eps
914

915
  // Compute static weights analytically
916
  poloidal_integrals_[0] = 1.0 + eps * Dt;
20✔
917
  poloidal_integrals_[1] = 1.0 + eps * Dt - 0.1875 * c1 * eps; // 3/16 = 0.1875
20✔
918
  poloidal_integrals_[2] = 1.0 - 0.375 * c1 * eps;             // 3/8 = 0.375
20✔
919
  poloidal_integrals_[3] = 1.0 + eps * Dt;
20✔
920
  poloidal_integrals_[4] = 1.0 + 0.5 * eps * Dt - 0.1875 * c1 * eps;
20✔
921
  poloidal_integrals_[5] = 1.0 - eps * Dt - 0.375 * c1 * eps;
20✔
922

923
  // Build the alpha grid on [0, pi] (half domain due to up-down symmetry)
924
  int n_alpha = n_alpha_;
20✔
925
  vector<double> alpha_grid(n_alpha);
20✔
926
  double dalpha = PI / (n_alpha - 1);
20✔
927
  for (int i = 0; i < n_alpha; ++i) {
2,050✔
928
    alpha_grid[i] = i * dalpha;
2,030✔
929
  }
930

931
  // Compute basis function values g_k(alpha) for tabular distributions
932
  // Using Bernstein form:
933
  //   R_tilde = b0*(1-r)^2 + 2*b1*r*(1-r) + b2*r^2
934
  //   J_tilde = b3*(1-r) + b4*r
935
  // with:
936
  //   b0(alpha) = 1 + eps*Dt
937
  //   b1(alpha) = b0 + (eps/2)*cos(psi),  psi = alpha + delta*sin(alpha)
938
  //   b2(alpha) = 1 + eps*cos(psi)
939
  //   b3(alpha) = cos(delta*sin(alpha))
940
  //               + (delta/4)*(cos(alpha - delta*sin(alpha))
941
  //                          - cos(3*alpha + delta*sin(alpha)))
942
  //   b4(alpha) = b3(alpha) - 2*Dt*cos(alpha)
943

944
  array<vector<double>, N_POLOIDAL_BASIS> basis;
945
  for (int k = 0; k < N_POLOIDAL_BASIS; ++k) {
140✔
946
    basis[k].resize(n_alpha);
120✔
947
  }
948

949
  for (int i = 0; i < n_alpha; ++i) {
2,050✔
950
    double alpha = alpha_grid[i];
2,030✔
951
    double sin_alpha = std::sin(alpha);
2,030✔
952
    double cos_alpha = std::cos(alpha);
2,030✔
953
    double delta_sin_alpha = delta * sin_alpha;
2,030✔
954
    double psi = alpha + delta_sin_alpha;
2,030✔
955
    double cos_psi = std::cos(psi);
2,030✔
956

957
    // Bernstein coefficients b0-b4
958
    double b0 = 1.0 + eps * Dt;
2,030✔
959
    double b1 = b0 + 0.5 * eps * cos_psi;
2,030✔
960
    double b2 = 1.0 + eps * cos_psi;
2,030✔
961
    double b3 =
2,030✔
962
      std::cos(delta_sin_alpha) + 0.25 * delta *
2,030✔
963
                                    (std::cos(alpha - delta_sin_alpha) -
2,030✔
964
                                      std::cos(3.0 * alpha + delta_sin_alpha));
2,030✔
965
    double b4 = b3 - 2.0 * Dt * cos_alpha;
2,030✔
966

967
    // 6 basis functions g_k(alpha) = b_i * b_j
968
    basis[0][i] = b0 * b3; // w0 = (1-r)^3
2,030✔
969
    basis[1][i] = b1 * b3; // w1 = 2*r*(1-r)^2
2,030✔
970
    basis[2][i] = b2 * b3; // w2 = r^2*(1-r)
2,030✔
971
    basis[3][i] = b0 * b4; // w3 = r*(1-r)^2
2,030✔
972
    basis[4][i] = b1 * b4; // w4 = 2*r^2*(1-r)
2,030✔
973
    basis[5][i] = b2 * b4; // w5 = r^3
2,030✔
974
  }
975

976
  // Build a linear-linear distribution for each basis function p_k(alpha)
977
  for (int k = 0; k < N_POLOIDAL_BASIS; ++k) {
140✔
978
    poloidal_dists_[k] = make_unique<Tabular>(
120✔
979
      alpha_grid.data(), basis[k].data(), n_alpha, Interpolation::lin_lin);
240✔
980
  }
981
}
20✔
982

983
double TokamakSource::sample_r_over_a(uint64_t* seed) const
800,000✔
984
{
985
  return radial_dist_->sample(seed).first;
800,000✔
986
}
987

988
double TokamakSource::mixture_weight(int k, double r) const
2,726,764✔
989
{
990
  double s = 1.0 - r;
2,726,764✔
991
  switch (k) {
2,726,764!
992
  case 0:
800,000✔
993
    return s * s * s * poloidal_integrals_[0];
800,000✔
994
  case 1:
702,685✔
995
    return 2.0 * r * s * s * poloidal_integrals_[1];
702,685✔
996
  case 2:
80,297✔
997
    return r * r * s * poloidal_integrals_[2];
80,297✔
998
  case 3:
139,793✔
999
    return r * s * s * poloidal_integrals_[3];
139,793✔
1000
  case 4:
582,328✔
1001
    return 2.0 * r * r * s * poloidal_integrals_[4];
582,328✔
1002
  case 5:
421,661✔
1003
    return r * r * r * poloidal_integrals_[5];
421,661✔
NEW
1004
  default:
×
NEW
1005
    UNREACHABLE();
×
1006
  }
1007
}
1008

1009
double TokamakSource::sample_poloidal_angle(double r_norm, uint64_t* seed) const
800,000✔
1010
{
1011
  // Sample from the conditional distribution P(alpha | r_tilde) using
1012
  // mixture sampling with 6 precomputed basis distributions.
1013
  //
1014
  // The conditional is: P(alpha | r) ~ sum_k w_k(r) * I_hat_k * p_k(alpha)
1015
  // where:
1016
  //   - w_k(r) are the "dynamic" Bernstein weight functions
1017
  //   - I_hat_k are the "static" normalized integrals (precomputed in
1018
  //   poloidal_integrals_)
1019
  //   - p_k(alpha) are the normalized, precomputed basis distributions
1020
  //
1021
  // The normalization sum_k w_k(r) * I_hat_k equals the radial geometric
1022
  // polynomial evaluated at r, which is known analytically.
1023
  //
1024
  // Algorithm:
1025
  // 1. Compute total from analytical normalization
1026
  // 2. Lazily evaluate mixture weights with early exit to select component k
1027
  // 3. Sample alpha from the selected basis distribution
1028

1029
  // Analytical normalization: sum_k w_k(r) * I_hat_k
1030
  double total =
800,000✔
1031
    radial_poly_a_ - radial_poly_b_ * r_norm - radial_poly_c_ * r_norm * r_norm;
800,000✔
1032
  double xi = prn(seed) * total;
800,000✔
1033

1034
  // Sample component via lazy evaluation with early exit
1035
  // Order optimized for peaked emission profiles: 0, 1, 4, 5, 3, 2
1036
  constexpr int order[] = {0, 1, 4, 5, 3, 2};
800,000✔
1037
  double cumsum = 0.0;
800,000✔
1038
  int component = order[N_POLOIDAL_BASIS - 1];
800,000✔
1039
  for (int i = 0; i < N_POLOIDAL_BASIS; ++i) {
2,726,764!
1040
    cumsum += mixture_weight(order[i], r_norm);
2,726,764✔
1041
    if (xi < cumsum) {
2,726,764✔
1042
      component = order[i];
1043
      break;
1044
    }
1045
  }
1046

1047
  // Sample alpha from [0, pi]
1048
  double alpha = poloidal_dists_[component]->sample(seed).first;
800,000✔
1049

1050
  // Exploit up-down symmetry: randomly flip to [pi, 2*pi] with 50% probability
1051
  // This is equivalent to flipping the sign of Z in the final position
1052
  if (prn(seed) >= 0.5) {
800,000✔
1053
    alpha = 2.0 * PI - alpha;
400,360✔
1054
  }
1055
  return alpha;
800,000✔
1056
}
1057

1058
std::pair<double, double> TokamakSource::sample_energy(
800,000✔
1059
  double r_norm, uint64_t* seed) const
1060
{
1061
  if (energy_dists_.size() == 1) {
800,000!
1062
    // Single distribution for all r
1063
    return energy_dists_[0]->sample(seed);
800,000✔
1064
  }
1065

1066
  // Multiple distributions: stochastic selection between bracketing r points
1067
  // Find the interval containing r_norm
NEW
1068
  size_t i = lower_bound_index(r_over_a_.begin(), r_over_a_.end(), r_norm);
×
1069

1070
  // Handle boundary cases
NEW
1071
  if (i >= energy_dists_.size() - 1) {
×
NEW
1072
    return energy_dists_.back()->sample(seed);
×
1073
  }
1074

1075
  // Stochastic interpolation: randomly select one of the two bracketing
1076
  // distributions based on distance to each
NEW
1077
  double t = (r_norm - r_over_a_[i]) / (r_over_a_[i + 1] - r_over_a_[i]);
×
NEW
1078
  size_t idx = (prn(seed) < t) ? i + 1 : i;
×
NEW
1079
  return energy_dists_[idx]->sample(seed);
×
1080
}
1081

1082
Position TokamakSource::flux_to_cartesian(
800,000✔
1083
  double r, double alpha, double phi) const
1084
{
1085
  // Flux surface parameterization:
1086
  // R = R0 + r*cos(alpha + delta*sin(alpha)) + Delta*(1 - (r/a)^2)
1087
  // Z = kappa * r * sin(alpha)
1088
  // x = R * cos(phi)
1089
  // y = R * sin(phi)
1090
  // z = Z
1091

1092
  double psi = alpha + triangularity_ * std::sin(alpha);
800,000✔
1093
  double r_over_a_sq = (r * r) / (minor_radius_ * minor_radius_);
800,000✔
1094

1095
  double R =
800,000✔
1096
    major_radius_ + r * std::cos(psi) + shafranov_shift_ * (1.0 - r_over_a_sq);
800,000✔
1097
  double Z = elongation_ * r * std::sin(alpha);
800,000✔
1098

1099
  double x = R * std::cos(phi);
800,000✔
1100
  double y = R * std::sin(phi);
800,000✔
1101
  double z = Z;
800,000✔
1102

1103
  return {x, y, z};
800,000✔
1104
}
1105

1106
SourceSite TokamakSource::sample(uint64_t* seed) const
800,000✔
1107
{
1108
  SourceSite site;
800,000✔
1109
  site.particle = ParticleType::neutron();
800,000✔
1110
  site.wgt = 1.0;
800,000✔
1111
  site.delayed_group = 0;
800,000✔
1112

1113
  // 1. Sample r/a from radial CDF
1114
  double r_norm = sample_r_over_a(seed);
800,000✔
1115
  double r = r_norm * minor_radius_;
800,000✔
1116

1117
  // 2. Sample poloidal angle from conditional distribution P(alpha|r)
1118
  double alpha = sample_poloidal_angle(r_norm, seed);
800,000✔
1119

1120
  // 3. Sample toroidal angle uniformly in [phi_start, phi_start + phi_extent]
1121
  double phi = phi_start_ + phi_extent_ * prn(seed);
800,000✔
1122

1123
  // 4. Convert to Cartesian coordinates
1124
  site.r = flux_to_cartesian(r, alpha, phi);
800,000✔
1125

1126
  // 4a. Apply vertical shift if non-zero
1127
  if (vertical_shift_ != 0.0) {
800,000✔
1128
    site.r.z += vertical_shift_;
100,000✔
1129
  }
1130

1131
  // 5. Sample isotropic direction
1132
  site.u = angle_->sample(seed).first;
800,000✔
1133

1134
  // 6. Sample energy from distribution(s), applying the importance weight so
1135
  // that biased distributions are handled correctly
1136
  auto [E, E_wgt] = sample_energy(r_norm, seed);
800,000✔
1137
  site.E = E;
800,000✔
1138

1139
  // 7. Sample particle creation time
1140
  auto [time, time_wgt] = time_->sample(seed);
800,000✔
1141
  site.time = time;
800,000✔
1142

1143
  site.wgt *= E_wgt * time_wgt;
800,000✔
1144

1145
  return site;
800,000✔
1146
}
1147

1148
//==============================================================================
1149
// Non-member functions
1150
//==============================================================================
1151

1152
void initialize_source()
1,722✔
1153
{
1154
  write_message("Initializing source particles...", 5);
1,722✔
1155

1156
// Generation source sites from specified distribution in user input
1157
#pragma omp parallel for
1158
  for (int64_t i = 0; i < simulation::work_per_rank; ++i) {
1,290,187✔
1159
    // initialize random number seed
1160
    int64_t id = simulation::total_gen * settings::n_particles +
1,288,465✔
1161
                 simulation::work_index[mpi::rank] + i + 1;
1,288,465✔
1162
    uint64_t seed = init_seed(id, STREAM_SOURCE);
1,288,465✔
1163

1164
    // sample external source distribution
1165
    simulation::source_bank[i] = sample_external_source(&seed);
1,288,465✔
1166
  }
1167

1168
  // Write out initial source
1169
  if (settings::write_initial_source) {
1,722!
1170
    write_message("Writing out initial source...", 5);
×
1171
    std::string filename = settings::path_output + "initial_source.h5";
×
1172
    hid_t file_id = file_open(filename, 'w', true);
×
1173
    write_source_bank(file_id, simulation::source_bank, simulation::work_index);
×
1174
    file_close(file_id);
×
1175
  }
×
1176
}
1,722✔
1177

1178
SourceSite sample_external_source(uint64_t* seed)
15,565,524✔
1179
{
1180
  // Sample from among multiple source distributions
1181
  int i = 0;
15,565,524✔
1182
  int n_sources = model::external_sources.size();
15,565,524✔
1183
  if (n_sources > 1) {
15,565,524✔
1184
    if (settings::uniform_source_sampling) {
1,569,000✔
1185
      i = prn(seed) * n_sources;
1,000✔
1186
    } else {
1187
      i = model::external_sources_probability.sample(seed);
1,568,000✔
1188
    }
1189
  }
1190

1191
  // Sample source site from i-th source distribution
1192
  SourceSite site {model::external_sources[i]->sample_with_constraints(seed)};
15,565,524✔
1193

1194
  // For uniform source sampling, multiply the weight by the ratio of the actual
1195
  // probability of sampling source i to the biased probability of sampling
1196
  // source i, which is (strength_i / total_strength) / (1 / n)
1197
  if (n_sources > 1 && settings::uniform_source_sampling) {
15,565,520✔
1198
    double total_strength = model::external_sources_probability.integral();
1,000✔
1199
    site.wgt *=
2,000✔
1200
      model::external_sources[i]->strength() * n_sources / total_strength;
1,000✔
1201
  }
1202

1203
  // If running in MG, convert site.E to group
1204
  if (!settings::run_CE) {
15,565,520✔
1205
    site.E = lower_bound_index(data::mg.rev_energy_bins_.begin(),
793,650✔
1206
      data::mg.rev_energy_bins_.end(), site.E);
1207
    site.E = data::mg.num_energy_groups_ - site.E - 1.;
793,650✔
1208
  }
1209

1210
  return site;
15,565,520✔
1211
}
1212

1213
void free_memory_source()
3,880✔
1214
{
1215
  model::external_sources.clear();
3,880✔
1216
  model::adjoint_sources.clear();
3,880✔
1217
  reset_source_rejection_counters();
3,880✔
1218
}
3,880✔
1219

1220
void reset_source_rejection_counters()
7,222✔
1221
{
1222
  source_n_accept = 0;
7,222✔
1223
  source_n_reject = 0;
7,222✔
1224
}
7,222✔
1225

1226
//==============================================================================
1227
// C API
1228
//==============================================================================
1229

1230
extern "C" int openmc_sample_external_source(
185✔
1231
  size_t n, uint64_t* seed, void* sites)
1232
{
1233
  if (!sites || !seed) {
185!
1234
    set_errmsg("Received null pointer.");
×
1235
    return OPENMC_E_INVALID_ARGUMENT;
×
1236
  }
1237

1238
  if (model::external_sources.empty()) {
185!
1239
    set_errmsg("No external sources have been defined.");
×
1240
    return OPENMC_E_OUT_OF_BOUNDS;
×
1241
  }
1242

1243
  auto sites_array = static_cast<SourceSite*>(sites);
185✔
1244

1245
  // Derive independent per-particle seeds from the base seed so that
1246
  // each iteration has its own RNG state for thread-safe parallel sampling.
1247
  uint64_t base_seed = *seed;
185✔
1248

1249
#pragma omp parallel for schedule(static)
1250
  for (size_t i = 0; i < n; ++i) {
1,871,505✔
1251
    uint64_t particle_seed = init_seed(base_seed + i, STREAM_SOURCE);
1,871,320✔
1252
    sites_array[i] = sample_external_source(&particle_seed);
1,871,320✔
1253
  }
1254
  return 0;
1255
}
1256

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