mirror of
https://github.com/openmc-dev/openmc.git
synced 2026-07-26 13:15:39 -04:00
Co-authored-by: Patrick Shriwise <pshriwise@gmail.com> Co-authored-by: Paul Romano <paul.k.romano@gmail.com>
1186 lines
38 KiB
C++
1186 lines
38 KiB
C++
#include "openmc/physics.h"
|
|
|
|
#include "openmc/bank.h"
|
|
#include "openmc/bremsstrahlung.h"
|
|
#include "openmc/constants.h"
|
|
#include "openmc/distribution_multi.h"
|
|
#include "openmc/eigenvalue.h"
|
|
#include "openmc/endf.h"
|
|
#include "openmc/error.h"
|
|
#include "openmc/material.h"
|
|
#include "openmc/math_functions.h"
|
|
#include "openmc/message_passing.h"
|
|
#include "openmc/ncrystal_interface.h"
|
|
#include "openmc/nuclide.h"
|
|
#include "openmc/photon.h"
|
|
#include "openmc/physics_common.h"
|
|
#include "openmc/random_dist.h"
|
|
#include "openmc/random_lcg.h"
|
|
#include "openmc/reaction.h"
|
|
#include "openmc/search.h"
|
|
#include "openmc/secondary_uncorrelated.h"
|
|
#include "openmc/settings.h"
|
|
#include "openmc/simulation.h"
|
|
#include "openmc/string_utils.h"
|
|
#include "openmc/tallies/tally.h"
|
|
#include "openmc/thermal.h"
|
|
#include "openmc/weight_windows.h"
|
|
|
|
#include <fmt/core.h>
|
|
|
|
#include <algorithm> // for max, min, max_element
|
|
#include <cmath> // for sqrt, exp, log, abs, copysign
|
|
#include <xtensor/xview.hpp>
|
|
|
|
namespace openmc {
|
|
|
|
//==============================================================================
|
|
// Non-member functions
|
|
//==============================================================================
|
|
|
|
void collision(Particle& p)
|
|
{
|
|
// Add to collision counter for particle
|
|
++(p.n_collision());
|
|
|
|
// Sample reaction for the material the particle is in
|
|
switch (p.type()) {
|
|
case ParticleType::neutron:
|
|
sample_neutron_reaction(p);
|
|
break;
|
|
case ParticleType::photon:
|
|
sample_photon_reaction(p);
|
|
break;
|
|
case ParticleType::electron:
|
|
sample_electron_reaction(p);
|
|
break;
|
|
case ParticleType::positron:
|
|
sample_positron_reaction(p);
|
|
break;
|
|
}
|
|
|
|
if (settings::weight_window_checkpoint_collision)
|
|
apply_weight_windows(p);
|
|
|
|
// Kill particle if energy falls below cutoff
|
|
int type = static_cast<int>(p.type());
|
|
if (p.E() < settings::energy_cutoff[type]) {
|
|
p.wgt() = 0.0;
|
|
}
|
|
|
|
// Display information about collision
|
|
if (settings::verbosity >= 10 || p.trace()) {
|
|
std::string msg;
|
|
if (p.event() == TallyEvent::KILL) {
|
|
msg = fmt::format(" Killed. Energy = {} eV.", p.E());
|
|
} else if (p.type() == ParticleType::neutron) {
|
|
msg = fmt::format(" {} with {}. Energy = {} eV.",
|
|
reaction_name(p.event_mt()), data::nuclides[p.event_nuclide()]->name_,
|
|
p.E());
|
|
} else if (p.type() == ParticleType::photon) {
|
|
msg = fmt::format(" {} with {}. Energy = {} eV.",
|
|
reaction_name(p.event_mt()),
|
|
to_element(data::nuclides[p.event_nuclide()]->name_), p.E());
|
|
} else {
|
|
msg = fmt::format(" Disappeared. Energy = {} eV.", p.E());
|
|
}
|
|
write_message(msg, 1);
|
|
}
|
|
}
|
|
|
|
void sample_neutron_reaction(Particle& p)
|
|
{
|
|
// Sample a nuclide within the material
|
|
int i_nuclide = sample_nuclide(p);
|
|
|
|
// Save which nuclide particle had collision with
|
|
p.event_nuclide() = i_nuclide;
|
|
|
|
// Create fission bank sites. Note that while a fission reaction is sampled,
|
|
// it never actually "happens", i.e. the weight of the particle does not
|
|
// change when sampling fission sites. The following block handles all
|
|
// absorption (including fission)
|
|
|
|
const auto& nuc {data::nuclides[i_nuclide]};
|
|
|
|
if (nuc->fissionable_ && p.neutron_xs(i_nuclide).fission > 0.0) {
|
|
auto& rx = sample_fission(i_nuclide, p);
|
|
if (settings::run_mode == RunMode::EIGENVALUE) {
|
|
create_fission_sites(p, i_nuclide, rx);
|
|
} else if (settings::run_mode == RunMode::FIXED_SOURCE &&
|
|
settings::create_fission_neutrons) {
|
|
create_fission_sites(p, i_nuclide, rx);
|
|
|
|
// Make sure particle population doesn't grow out of control for
|
|
// subcritical multiplication problems.
|
|
if (p.secondary_bank().size() >= 10000) {
|
|
fatal_error(
|
|
"The secondary particle bank appears to be growing without "
|
|
"bound. You are likely running a subcritical multiplication problem "
|
|
"with k-effective close to or greater than one.");
|
|
}
|
|
}
|
|
}
|
|
|
|
// Create secondary photons
|
|
if (settings::photon_transport) {
|
|
sample_secondary_photons(p, i_nuclide);
|
|
}
|
|
|
|
// If survival biasing is being used, the following subroutine adjusts the
|
|
// weight of the particle. Otherwise, it checks to see if absorption occurs
|
|
|
|
if (p.neutron_xs(i_nuclide).absorption > 0.0) {
|
|
absorption(p, i_nuclide);
|
|
}
|
|
if (!p.alive())
|
|
return;
|
|
|
|
// Sample a scattering reaction and determine the secondary energy of the
|
|
// exiting neutron
|
|
const auto& ncrystal_mat = model::materials[p.material()]->ncrystal_mat();
|
|
if (ncrystal_mat && p.E() < NCRYSTAL_MAX_ENERGY) {
|
|
ncrystal_mat.scatter(p);
|
|
} else {
|
|
scatter(p, i_nuclide);
|
|
}
|
|
|
|
// Advance URR seed stream 'N' times after energy changes
|
|
if (p.E() != p.E_last()) {
|
|
advance_prn_seed(data::nuclides.size(), &p.seeds(STREAM_URR_PTABLE));
|
|
}
|
|
|
|
// Play russian roulette if survival biasing is turned on
|
|
if (settings::survival_biasing) {
|
|
if (p.wgt() < settings::weight_cutoff) {
|
|
russian_roulette(p, settings::weight_survive);
|
|
}
|
|
}
|
|
}
|
|
|
|
void create_fission_sites(Particle& p, int i_nuclide, const Reaction& rx)
|
|
{
|
|
// If uniform fission source weighting is turned on, we increase or decrease
|
|
// the expected number of fission sites produced
|
|
double weight = settings::ufs_on ? ufs_get_weight(p) : 1.0;
|
|
|
|
// Determine the expected number of neutrons produced
|
|
double nu_t = p.wgt() / simulation::keff * weight *
|
|
p.neutron_xs(i_nuclide).nu_fission /
|
|
p.neutron_xs(i_nuclide).total;
|
|
|
|
// Sample the number of neutrons produced
|
|
int nu = static_cast<int>(nu_t);
|
|
if (prn(p.current_seed()) <= (nu_t - nu))
|
|
++nu;
|
|
|
|
// If no neutrons were produced then don't continue
|
|
if (nu == 0)
|
|
return;
|
|
|
|
// Initialize the counter of delayed neutrons encountered for each delayed
|
|
// group.
|
|
double nu_d[MAX_DELAYED_GROUPS] = {0.};
|
|
|
|
// Clear out particle's nu fission bank
|
|
p.nu_bank().clear();
|
|
|
|
p.fission() = true;
|
|
|
|
// Determine whether to place fission sites into the shared fission bank
|
|
// or the secondary particle bank.
|
|
bool use_fission_bank = (settings::run_mode == RunMode::EIGENVALUE);
|
|
|
|
// Counter for the number of fission sites successfully stored to the shared
|
|
// fission bank or the secondary particle bank
|
|
int n_sites_stored;
|
|
|
|
for (n_sites_stored = 0; n_sites_stored < nu; n_sites_stored++) {
|
|
// Initialize fission site object with particle data
|
|
SourceSite site;
|
|
site.r = p.r();
|
|
site.particle = ParticleType::neutron;
|
|
site.time = p.time();
|
|
site.wgt = 1. / weight;
|
|
site.parent_id = p.id();
|
|
site.progeny_id = p.n_progeny()++;
|
|
site.surf_id = 0;
|
|
|
|
// Sample delayed group and angle/energy for fission reaction
|
|
sample_fission_neutron(i_nuclide, rx, &site, p);
|
|
|
|
// Store fission site in bank
|
|
if (use_fission_bank) {
|
|
int64_t idx = simulation::fission_bank.thread_safe_append(site);
|
|
if (idx == -1) {
|
|
warning(
|
|
"The shared fission bank is full. Additional fission sites created "
|
|
"in this generation will not be banked. Results may be "
|
|
"non-deterministic.");
|
|
|
|
// Decrement number of particle progeny as storage was unsuccessful.
|
|
// This step is needed so that the sum of all progeny is equal to the
|
|
// size of the shared fission bank.
|
|
p.n_progeny()--;
|
|
|
|
// Break out of loop as no more sites can be added to fission bank
|
|
break;
|
|
}
|
|
} else {
|
|
p.secondary_bank().push_back(site);
|
|
}
|
|
|
|
// Set the delayed group on the particle as well
|
|
p.delayed_group() = site.delayed_group;
|
|
|
|
// Increment the number of neutrons born delayed
|
|
if (p.delayed_group() > 0) {
|
|
nu_d[p.delayed_group() - 1]++;
|
|
}
|
|
|
|
// Write fission particles to nuBank
|
|
p.nu_bank().emplace_back();
|
|
NuBank* nu_bank_entry = &p.nu_bank().back();
|
|
nu_bank_entry->wgt = site.wgt;
|
|
nu_bank_entry->E = site.E;
|
|
nu_bank_entry->delayed_group = site.delayed_group;
|
|
}
|
|
|
|
// If shared fission bank was full, and no fissions could be added,
|
|
// set the particle fission flag to false.
|
|
if (n_sites_stored == 0) {
|
|
p.fission() = false;
|
|
return;
|
|
}
|
|
|
|
// Set nu to the number of fission sites successfully stored. If the fission
|
|
// bank was not found to be full then these values are already equivalent.
|
|
nu = n_sites_stored;
|
|
|
|
// Store the total weight banked for analog fission tallies
|
|
p.n_bank() = nu;
|
|
p.wgt_bank() = nu / weight;
|
|
for (size_t d = 0; d < MAX_DELAYED_GROUPS; d++) {
|
|
p.n_delayed_bank(d) = nu_d[d];
|
|
}
|
|
}
|
|
|
|
void sample_photon_reaction(Particle& p)
|
|
{
|
|
// Kill photon if below energy cutoff -- an extra check is made here because
|
|
// photons with energy below the cutoff may have been produced by neutrons
|
|
// reactions or atomic relaxation
|
|
int photon = static_cast<int>(ParticleType::photon);
|
|
if (p.E() < settings::energy_cutoff[photon]) {
|
|
p.E() = 0.0;
|
|
p.wgt() = 0.0;
|
|
return;
|
|
}
|
|
|
|
// Sample element within material
|
|
int i_element = sample_element(p);
|
|
const auto& micro {p.photon_xs(i_element)};
|
|
const auto& element {*data::elements[i_element]};
|
|
|
|
// Calculate photon energy over electron rest mass equivalent
|
|
double alpha = p.E() / MASS_ELECTRON_EV;
|
|
|
|
// For tallying purposes, this routine might be called directly. In that
|
|
// case, we need to sample a reaction via the cutoff variable
|
|
double prob = 0.0;
|
|
double cutoff = prn(p.current_seed()) * micro.total;
|
|
|
|
// Coherent (Rayleigh) scattering
|
|
prob += micro.coherent;
|
|
if (prob > cutoff) {
|
|
double mu = element.rayleigh_scatter(alpha, p.current_seed());
|
|
p.u() = rotate_angle(p.u(), mu, nullptr, p.current_seed());
|
|
p.event() = TallyEvent::SCATTER;
|
|
p.event_mt() = COHERENT;
|
|
return;
|
|
}
|
|
|
|
// Incoherent (Compton) scattering
|
|
prob += micro.incoherent;
|
|
if (prob > cutoff) {
|
|
double alpha_out, mu;
|
|
int i_shell;
|
|
element.compton_scatter(
|
|
alpha, true, &alpha_out, &mu, &i_shell, p.current_seed());
|
|
|
|
// Determine binding energy of shell. The binding energy is 0.0 if
|
|
// doppler broadening is not used.
|
|
double e_b;
|
|
if (i_shell == -1) {
|
|
e_b = 0.0;
|
|
} else {
|
|
e_b = element.binding_energy_[i_shell];
|
|
}
|
|
|
|
// Create Compton electron
|
|
double phi = uniform_distribution(0., 2.0 * PI, p.current_seed());
|
|
double E_electron = (alpha - alpha_out) * MASS_ELECTRON_EV - e_b;
|
|
int electron = static_cast<int>(ParticleType::electron);
|
|
if (E_electron >= settings::energy_cutoff[electron]) {
|
|
double mu_electron = (alpha - alpha_out * mu) /
|
|
std::sqrt(alpha * alpha + alpha_out * alpha_out -
|
|
2.0 * alpha * alpha_out * mu);
|
|
Direction u = rotate_angle(p.u(), mu_electron, &phi, p.current_seed());
|
|
p.create_secondary(p.wgt(), u, E_electron, ParticleType::electron);
|
|
}
|
|
|
|
// TODO: Compton subshell data does not match atomic relaxation data
|
|
// Allow electrons to fill orbital and produce auger electrons
|
|
// and fluorescent photons
|
|
if (i_shell >= 0) {
|
|
element.atomic_relaxation(i_shell, p);
|
|
}
|
|
|
|
phi += PI;
|
|
p.E() = alpha_out * MASS_ELECTRON_EV;
|
|
p.u() = rotate_angle(p.u(), mu, &phi, p.current_seed());
|
|
p.event() = TallyEvent::SCATTER;
|
|
p.event_mt() = INCOHERENT;
|
|
return;
|
|
}
|
|
|
|
// Photoelectric effect
|
|
double prob_after = prob + micro.photoelectric;
|
|
|
|
if (prob_after > cutoff) {
|
|
// Get grid index, interpolation factor, and bounding subshell
|
|
// cross sections
|
|
int i_grid = micro.index_grid;
|
|
double f = micro.interp_factor;
|
|
const auto& xs_lower = xt::row(element.cross_sections_, i_grid);
|
|
const auto& xs_upper = xt::row(element.cross_sections_, i_grid + 1);
|
|
|
|
for (int i_shell = 0; i_shell < element.shells_.size(); ++i_shell) {
|
|
const auto& shell {element.shells_[i_shell]};
|
|
|
|
// Check threshold of reaction
|
|
if (xs_lower(i_shell) == 0)
|
|
continue;
|
|
|
|
// Evaluation subshell photoionization cross section
|
|
prob += std::exp(
|
|
xs_lower(i_shell) + f * (xs_upper(i_shell) - xs_lower(i_shell)));
|
|
|
|
if (prob > cutoff) {
|
|
// Determine binding energy based on whether atomic relaxation data is
|
|
// present (if not, use value from Compton profile data)
|
|
double binding_energy = element.has_atomic_relaxation_
|
|
? shell.binding_energy
|
|
: element.binding_energy_[i_shell];
|
|
|
|
// Determine energy of secondary electron
|
|
double E_electron = p.E() - binding_energy;
|
|
|
|
// Sample mu using non-relativistic Sauter distribution.
|
|
// See Eqns 3.19 and 3.20 in "Implementing a photon physics
|
|
// model in Serpent 2" by Toni Kaltiaisenaho
|
|
double mu;
|
|
while (true) {
|
|
double r = prn(p.current_seed());
|
|
if (4.0 * (1.0 - r) * r >= prn(p.current_seed())) {
|
|
double rel_vel =
|
|
std::sqrt(E_electron * (E_electron + 2.0 * MASS_ELECTRON_EV)) /
|
|
(E_electron + MASS_ELECTRON_EV);
|
|
mu =
|
|
(2.0 * r + rel_vel - 1.0) / (2.0 * rel_vel * r - rel_vel + 1.0);
|
|
break;
|
|
}
|
|
}
|
|
|
|
double phi = uniform_distribution(0., 2.0 * PI, p.current_seed());
|
|
Direction u;
|
|
u.x = mu;
|
|
u.y = std::sqrt(1.0 - mu * mu) * std::cos(phi);
|
|
u.z = std::sqrt(1.0 - mu * mu) * std::sin(phi);
|
|
|
|
// Create secondary electron
|
|
p.create_secondary(p.wgt(), u, E_electron, ParticleType::electron);
|
|
|
|
// Allow electrons to fill orbital and produce auger electrons
|
|
// and fluorescent photons
|
|
element.atomic_relaxation(i_shell, p);
|
|
p.event() = TallyEvent::ABSORB;
|
|
p.event_mt() = 533 + shell.index_subshell;
|
|
p.wgt() = 0.0;
|
|
p.E() = 0.0;
|
|
return;
|
|
}
|
|
}
|
|
}
|
|
prob = prob_after;
|
|
|
|
// Pair production
|
|
prob += micro.pair_production;
|
|
if (prob > cutoff) {
|
|
double E_electron, E_positron;
|
|
double mu_electron, mu_positron;
|
|
element.pair_production(alpha, &E_electron, &E_positron, &mu_electron,
|
|
&mu_positron, p.current_seed());
|
|
|
|
// Create secondary electron
|
|
Direction u = rotate_angle(p.u(), mu_electron, nullptr, p.current_seed());
|
|
p.create_secondary(p.wgt(), u, E_electron, ParticleType::electron);
|
|
|
|
// Create secondary positron
|
|
u = rotate_angle(p.u(), mu_positron, nullptr, p.current_seed());
|
|
p.create_secondary(p.wgt(), u, E_positron, ParticleType::positron);
|
|
|
|
p.event() = TallyEvent::ABSORB;
|
|
p.event_mt() = PAIR_PROD;
|
|
p.wgt() = 0.0;
|
|
p.E() = 0.0;
|
|
}
|
|
}
|
|
|
|
void sample_electron_reaction(Particle& p)
|
|
{
|
|
// TODO: create reaction types
|
|
|
|
if (settings::electron_treatment == ElectronTreatment::TTB) {
|
|
double E_lost;
|
|
thick_target_bremsstrahlung(p, &E_lost);
|
|
}
|
|
|
|
p.E() = 0.0;
|
|
p.wgt() = 0.0;
|
|
p.event() = TallyEvent::ABSORB;
|
|
}
|
|
|
|
void sample_positron_reaction(Particle& p)
|
|
{
|
|
// TODO: create reaction types
|
|
|
|
if (settings::electron_treatment == ElectronTreatment::TTB) {
|
|
double E_lost;
|
|
thick_target_bremsstrahlung(p, &E_lost);
|
|
}
|
|
|
|
// Sample angle isotropically
|
|
Direction u = isotropic_direction(p.current_seed());
|
|
|
|
// Create annihilation photon pair traveling in opposite directions
|
|
p.create_secondary(p.wgt(), u, MASS_ELECTRON_EV, ParticleType::photon);
|
|
p.create_secondary(p.wgt(), -u, MASS_ELECTRON_EV, ParticleType::photon);
|
|
|
|
p.E() = 0.0;
|
|
p.wgt() = 0.0;
|
|
p.event() = TallyEvent::ABSORB;
|
|
}
|
|
|
|
int sample_nuclide(Particle& p)
|
|
{
|
|
// Sample cumulative distribution function
|
|
double cutoff = prn(p.current_seed()) * p.macro_xs().total;
|
|
|
|
// Get pointers to nuclide/density arrays
|
|
const auto& mat {model::materials[p.material()]};
|
|
int n = mat->nuclide_.size();
|
|
|
|
double prob = 0.0;
|
|
for (int i = 0; i < n; ++i) {
|
|
// Get atom density
|
|
int i_nuclide = mat->nuclide_[i];
|
|
double atom_density = mat->atom_density_[i];
|
|
|
|
// Increment probability to compare to cutoff
|
|
prob += atom_density * p.neutron_xs(i_nuclide).total;
|
|
if (prob >= cutoff)
|
|
return i_nuclide;
|
|
}
|
|
|
|
// If we reach here, no nuclide was sampled
|
|
p.write_restart();
|
|
throw std::runtime_error {"Did not sample any nuclide during collision."};
|
|
}
|
|
|
|
int sample_element(Particle& p)
|
|
{
|
|
// Sample cumulative distribution function
|
|
double cutoff = prn(p.current_seed()) * p.macro_xs().total;
|
|
|
|
// Get pointers to elements, densities
|
|
const auto& mat {model::materials[p.material()]};
|
|
|
|
double prob = 0.0;
|
|
for (int i = 0; i < mat->element_.size(); ++i) {
|
|
// Find atom density
|
|
int i_element = mat->element_[i];
|
|
double atom_density = mat->atom_density_[i];
|
|
|
|
// Determine microscopic cross section
|
|
double sigma = atom_density * p.photon_xs(i_element).total;
|
|
|
|
// Increment probability to compare to cutoff
|
|
prob += sigma;
|
|
if (prob > cutoff) {
|
|
// Save which nuclide particle had collision with for tally purpose
|
|
p.event_nuclide() = mat->nuclide_[i];
|
|
|
|
return i_element;
|
|
}
|
|
}
|
|
|
|
// If we made it here, no element was sampled
|
|
p.write_restart();
|
|
fatal_error("Did not sample any element during collision.");
|
|
}
|
|
|
|
Reaction& sample_fission(int i_nuclide, Particle& p)
|
|
{
|
|
// Get pointer to nuclide
|
|
const auto& nuc {data::nuclides[i_nuclide]};
|
|
|
|
// If we're in the URR, by default use the first fission reaction. We also
|
|
// default to the first reaction if we know that there are no partial fission
|
|
// reactions
|
|
if (p.neutron_xs(i_nuclide).use_ptable || !nuc->has_partial_fission_) {
|
|
return *nuc->fission_rx_[0];
|
|
}
|
|
|
|
// Check to see if we are in a windowed multipole range. WMP only supports
|
|
// the first fission reaction.
|
|
if (nuc->multipole_) {
|
|
if (p.E() >= nuc->multipole_->E_min_ && p.E() <= nuc->multipole_->E_max_) {
|
|
return *nuc->fission_rx_[0];
|
|
}
|
|
}
|
|
|
|
// Get grid index and interpolation factor and sample fission cdf
|
|
const auto& micro = p.neutron_xs(i_nuclide);
|
|
double cutoff = prn(p.current_seed()) * p.neutron_xs(i_nuclide).fission;
|
|
double prob = 0.0;
|
|
|
|
// Loop through each partial fission reaction type
|
|
for (auto& rx : nuc->fission_rx_) {
|
|
// add to cumulative probability
|
|
prob += rx->xs(micro);
|
|
|
|
// Create fission bank sites if fission occurs
|
|
if (prob > cutoff)
|
|
return *rx;
|
|
}
|
|
|
|
// If we reached here, no reaction was sampled
|
|
throw std::runtime_error {
|
|
"No fission reaction was sampled for " + nuc->name_};
|
|
}
|
|
|
|
void sample_photon_product(
|
|
int i_nuclide, Particle& p, int* i_rx, int* i_product)
|
|
{
|
|
// Get grid index and interpolation factor and sample photon production cdf
|
|
const auto& micro = p.neutron_xs(i_nuclide);
|
|
double cutoff = prn(p.current_seed()) * micro.photon_prod;
|
|
double prob = 0.0;
|
|
|
|
// Loop through each reaction type
|
|
const auto& nuc {data::nuclides[i_nuclide]};
|
|
for (int i = 0; i < nuc->reactions_.size(); ++i) {
|
|
// Evaluate neutron cross section
|
|
const auto& rx = nuc->reactions_[i];
|
|
double xs = rx->xs(micro);
|
|
|
|
// if cross section is zero for this reaction, skip it
|
|
if (xs == 0.0)
|
|
continue;
|
|
|
|
for (int j = 0; j < rx->products_.size(); ++j) {
|
|
if (rx->products_[j].particle_ == ParticleType::photon) {
|
|
// For fission, artificially increase the photon yield to account
|
|
// for delayed photons
|
|
double f = 1.0;
|
|
if (settings::delayed_photon_scaling) {
|
|
if (is_fission(rx->mt_)) {
|
|
if (nuc->prompt_photons_ && nuc->delayed_photons_) {
|
|
double energy_prompt = (*nuc->prompt_photons_)(p.E());
|
|
double energy_delayed = (*nuc->delayed_photons_)(p.E());
|
|
f = (energy_prompt + energy_delayed) / (energy_prompt);
|
|
}
|
|
}
|
|
}
|
|
|
|
// add to cumulative probability
|
|
prob += f * (*rx->products_[j].yield_)(p.E()) * xs;
|
|
|
|
*i_rx = i;
|
|
*i_product = j;
|
|
if (prob > cutoff)
|
|
return;
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
void absorption(Particle& p, int i_nuclide)
|
|
{
|
|
if (settings::survival_biasing) {
|
|
// Determine weight absorbed in survival biasing
|
|
const double wgt_absorb = p.wgt() * p.neutron_xs(i_nuclide).absorption /
|
|
p.neutron_xs(i_nuclide).total;
|
|
|
|
// Adjust weight of particle by probability of absorption
|
|
p.wgt() -= wgt_absorb;
|
|
|
|
// Score implicit absorption estimate of keff
|
|
if (settings::run_mode == RunMode::EIGENVALUE) {
|
|
p.keff_tally_absorption() += wgt_absorb *
|
|
p.neutron_xs(i_nuclide).nu_fission /
|
|
p.neutron_xs(i_nuclide).absorption;
|
|
}
|
|
} else {
|
|
// See if disappearance reaction happens
|
|
if (p.neutron_xs(i_nuclide).absorption >
|
|
prn(p.current_seed()) * p.neutron_xs(i_nuclide).total) {
|
|
// Score absorption estimate of keff
|
|
if (settings::run_mode == RunMode::EIGENVALUE) {
|
|
p.keff_tally_absorption() += p.wgt() *
|
|
p.neutron_xs(i_nuclide).nu_fission /
|
|
p.neutron_xs(i_nuclide).absorption;
|
|
}
|
|
|
|
p.wgt() = 0.0;
|
|
p.event() = TallyEvent::ABSORB;
|
|
p.event_mt() = N_DISAPPEAR;
|
|
}
|
|
}
|
|
}
|
|
|
|
void scatter(Particle& p, int i_nuclide)
|
|
{
|
|
// copy incoming direction
|
|
Direction u_old {p.u()};
|
|
|
|
// Get pointer to nuclide and grid index/interpolation factor
|
|
const auto& nuc {data::nuclides[i_nuclide]};
|
|
const auto& micro {p.neutron_xs(i_nuclide)};
|
|
int i_temp = micro.index_temp;
|
|
|
|
// For tallying purposes, this routine might be called directly. In that
|
|
// case, we need to sample a reaction via the cutoff variable
|
|
double cutoff = prn(p.current_seed()) * (micro.total - micro.absorption);
|
|
bool sampled = false;
|
|
|
|
// Calculate elastic cross section if it wasn't precalculated
|
|
if (micro.elastic == CACHE_INVALID) {
|
|
nuc->calculate_elastic_xs(p);
|
|
}
|
|
|
|
double prob = micro.elastic - micro.thermal;
|
|
if (prob > cutoff) {
|
|
// =======================================================================
|
|
// NON-S(A,B) ELASTIC SCATTERING
|
|
|
|
// Determine temperature
|
|
double kT = nuc->multipole_ ? p.sqrtkT() * p.sqrtkT() : nuc->kTs_[i_temp];
|
|
|
|
// Perform collision physics for elastic scattering
|
|
elastic_scatter(i_nuclide, *nuc->reactions_[0], kT, p);
|
|
|
|
p.event_mt() = ELASTIC;
|
|
sampled = true;
|
|
}
|
|
|
|
prob = micro.elastic;
|
|
if (prob > cutoff && !sampled) {
|
|
// =======================================================================
|
|
// S(A,B) SCATTERING
|
|
|
|
sab_scatter(i_nuclide, micro.index_sab, p);
|
|
|
|
p.event_mt() = ELASTIC;
|
|
sampled = true;
|
|
}
|
|
|
|
if (!sampled) {
|
|
// =======================================================================
|
|
// INELASTIC SCATTERING
|
|
|
|
int n = nuc->index_inelastic_scatter_.size();
|
|
int i = 0;
|
|
for (int j = 0; j < n && prob < cutoff; ++j) {
|
|
i = nuc->index_inelastic_scatter_[j];
|
|
|
|
// add to cumulative probability
|
|
prob += nuc->reactions_[i]->xs(micro);
|
|
}
|
|
|
|
// Perform collision physics for inelastic scattering
|
|
const auto& rx {nuc->reactions_[i]};
|
|
inelastic_scatter(*nuc, *rx, p);
|
|
p.event_mt() = rx->mt_;
|
|
}
|
|
|
|
// Set event component
|
|
p.event() = TallyEvent::SCATTER;
|
|
|
|
// Sample new outgoing angle for isotropic-in-lab scattering
|
|
const auto& mat {model::materials[p.material()]};
|
|
if (!mat->p0_.empty()) {
|
|
int i_nuc_mat = mat->mat_nuclide_index_[i_nuclide];
|
|
if (mat->p0_[i_nuc_mat]) {
|
|
// Sample isotropic-in-lab outgoing direction
|
|
p.u() = isotropic_direction(p.current_seed());
|
|
p.mu() = u_old.dot(p.u());
|
|
}
|
|
}
|
|
}
|
|
|
|
void elastic_scatter(int i_nuclide, const Reaction& rx, double kT, Particle& p)
|
|
{
|
|
// get pointer to nuclide
|
|
const auto& nuc {data::nuclides[i_nuclide]};
|
|
|
|
double vel = std::sqrt(p.E());
|
|
double awr = nuc->awr_;
|
|
|
|
// Neutron velocity in LAB
|
|
Direction v_n = vel * p.u();
|
|
|
|
// Sample velocity of target nucleus
|
|
Direction v_t {};
|
|
if (!p.neutron_xs(i_nuclide).use_ptable) {
|
|
v_t = sample_target_velocity(*nuc, p.E(), p.u(), v_n,
|
|
p.neutron_xs(i_nuclide).elastic, kT, p.current_seed());
|
|
}
|
|
|
|
// Velocity of center-of-mass
|
|
Direction v_cm = (v_n + awr * v_t) / (awr + 1.0);
|
|
|
|
// Transform to CM frame
|
|
v_n -= v_cm;
|
|
|
|
// Find speed of neutron in CM
|
|
vel = v_n.norm();
|
|
|
|
// Sample scattering angle, checking if angle distribution is present (assume
|
|
// isotropic otherwise)
|
|
double mu_cm;
|
|
auto& d = rx.products_[0].distribution_[0];
|
|
auto d_ = dynamic_cast<UncorrelatedAngleEnergy*>(d.get());
|
|
if (!d_->angle().empty()) {
|
|
mu_cm = d_->angle().sample(p.E(), p.current_seed());
|
|
} else {
|
|
mu_cm = uniform_distribution(-1., 1., p.current_seed());
|
|
}
|
|
|
|
// Determine direction cosines in CM
|
|
Direction u_cm = v_n / vel;
|
|
|
|
// Rotate neutron velocity vector to new angle -- note that the speed of the
|
|
// neutron in CM does not change in elastic scattering. However, the speed
|
|
// will change when we convert back to LAB
|
|
v_n = vel * rotate_angle(u_cm, mu_cm, nullptr, p.current_seed());
|
|
|
|
// Transform back to LAB frame
|
|
v_n += v_cm;
|
|
|
|
p.E() = v_n.dot(v_n);
|
|
vel = std::sqrt(p.E());
|
|
|
|
// compute cosine of scattering angle in LAB frame by taking dot product of
|
|
// neutron's pre- and post-collision angle
|
|
p.mu() = p.u().dot(v_n) / vel;
|
|
|
|
// Set energy and direction of particle in LAB frame
|
|
p.u() = v_n / vel;
|
|
|
|
// Because of floating-point roundoff, it may be possible for mu_lab to be
|
|
// outside of the range [-1,1). In these cases, we just set mu_lab to exactly
|
|
// -1 or 1
|
|
if (std::abs(p.mu()) > 1.0)
|
|
p.mu() = std::copysign(1.0, p.mu());
|
|
}
|
|
|
|
void sab_scatter(int i_nuclide, int i_sab, Particle& p)
|
|
{
|
|
// Determine temperature index
|
|
const auto& micro {p.neutron_xs(i_nuclide)};
|
|
int i_temp = micro.index_temp_sab;
|
|
|
|
// Sample energy and angle
|
|
double E_out;
|
|
data::thermal_scatt[i_sab]->data_[i_temp].sample(
|
|
micro, p.E(), &E_out, &p.mu(), p.current_seed());
|
|
|
|
// Set energy to outgoing, change direction of particle
|
|
p.E() = E_out;
|
|
p.u() = rotate_angle(p.u(), p.mu(), nullptr, p.current_seed());
|
|
}
|
|
|
|
Direction sample_target_velocity(const Nuclide& nuc, double E, Direction u,
|
|
Direction v_neut, double xs_eff, double kT, uint64_t* seed)
|
|
{
|
|
// check if nuclide is a resonant scatterer
|
|
ResScatMethod sampling_method;
|
|
if (nuc.resonant_) {
|
|
|
|
// sampling method to use
|
|
sampling_method = settings::res_scat_method;
|
|
|
|
// upper resonance scattering energy bound (target is at rest above this E)
|
|
if (E > settings::res_scat_energy_max) {
|
|
return {};
|
|
|
|
// lower resonance scattering energy bound (should be no resonances below)
|
|
} else if (E < settings::res_scat_energy_min) {
|
|
sampling_method = ResScatMethod::cxs;
|
|
}
|
|
|
|
// otherwise, use free gas model
|
|
} else {
|
|
if (E >= FREE_GAS_THRESHOLD * kT && nuc.awr_ > 1.0) {
|
|
return {};
|
|
} else {
|
|
sampling_method = ResScatMethod::cxs;
|
|
}
|
|
}
|
|
|
|
// use appropriate target velocity sampling method
|
|
switch (sampling_method) {
|
|
case ResScatMethod::cxs:
|
|
|
|
// sample target velocity with the constant cross section (cxs) approx.
|
|
return sample_cxs_target_velocity(nuc.awr_, E, u, kT, seed);
|
|
|
|
case ResScatMethod::dbrc:
|
|
case ResScatMethod::rvs: {
|
|
double E_red = std::sqrt(nuc.awr_ * E / kT);
|
|
double E_low = std::pow(std::max(0.0, E_red - 4.0), 2) * kT / nuc.awr_;
|
|
double E_up = (E_red + 4.0) * (E_red + 4.0) * kT / nuc.awr_;
|
|
|
|
// find lower and upper energy bound indices
|
|
// lower index
|
|
int i_E_low;
|
|
if (E_low < nuc.energy_0K_.front()) {
|
|
i_E_low = 0;
|
|
} else if (E_low > nuc.energy_0K_.back()) {
|
|
i_E_low = nuc.energy_0K_.size() - 2;
|
|
} else {
|
|
i_E_low =
|
|
lower_bound_index(nuc.energy_0K_.begin(), nuc.energy_0K_.end(), E_low);
|
|
}
|
|
|
|
// upper index
|
|
int i_E_up;
|
|
if (E_up < nuc.energy_0K_.front()) {
|
|
i_E_up = 0;
|
|
} else if (E_up > nuc.energy_0K_.back()) {
|
|
i_E_up = nuc.energy_0K_.size() - 2;
|
|
} else {
|
|
i_E_up =
|
|
lower_bound_index(nuc.energy_0K_.begin(), nuc.energy_0K_.end(), E_up);
|
|
}
|
|
|
|
if (i_E_up == i_E_low) {
|
|
// Handle degenerate case -- if the upper/lower bounds occur for the same
|
|
// index, then using cxs is probably a good approximation
|
|
return sample_cxs_target_velocity(nuc.awr_, E, u, kT, seed);
|
|
}
|
|
|
|
if (sampling_method == ResScatMethod::dbrc) {
|
|
// interpolate xs since we're not exactly at the energy indices
|
|
double xs_low = nuc.elastic_0K_[i_E_low];
|
|
double m = (nuc.elastic_0K_[i_E_low + 1] - xs_low) /
|
|
(nuc.energy_0K_[i_E_low + 1] - nuc.energy_0K_[i_E_low]);
|
|
xs_low += m * (E_low - nuc.energy_0K_[i_E_low]);
|
|
double xs_up = nuc.elastic_0K_[i_E_up];
|
|
m = (nuc.elastic_0K_[i_E_up + 1] - xs_up) /
|
|
(nuc.energy_0K_[i_E_up + 1] - nuc.energy_0K_[i_E_up]);
|
|
xs_up += m * (E_up - nuc.energy_0K_[i_E_up]);
|
|
|
|
// get max 0K xs value over range of practical relative energies
|
|
double xs_max = *std::max_element(
|
|
&nuc.elastic_0K_[i_E_low + 1], &nuc.elastic_0K_[i_E_up + 1]);
|
|
xs_max = std::max({xs_low, xs_max, xs_up});
|
|
|
|
while (true) {
|
|
double E_rel;
|
|
Direction v_target;
|
|
while (true) {
|
|
// sample target velocity with the constant cross section (cxs)
|
|
// approx.
|
|
v_target = sample_cxs_target_velocity(nuc.awr_, E, u, kT, seed);
|
|
Direction v_rel = v_neut - v_target;
|
|
E_rel = v_rel.dot(v_rel);
|
|
if (E_rel < E_up)
|
|
break;
|
|
}
|
|
|
|
// perform Doppler broadening rejection correction (dbrc)
|
|
double xs_0K = nuc.elastic_xs_0K(E_rel);
|
|
double R = xs_0K / xs_max;
|
|
if (prn(seed) < R)
|
|
return v_target;
|
|
}
|
|
|
|
} else if (sampling_method == ResScatMethod::rvs) {
|
|
// interpolate xs CDF since we're not exactly at the energy indices
|
|
// cdf value at lower bound attainable energy
|
|
double cdf_low = 0.0;
|
|
if (E_low > nuc.energy_0K_.front()) {
|
|
double m = (nuc.xs_cdf_[i_E_low + 1] - nuc.xs_cdf_[i_E_low]) /
|
|
(nuc.energy_0K_[i_E_low + 1] - nuc.energy_0K_[i_E_low]);
|
|
cdf_low = nuc.xs_cdf_[i_E_low] + m * (E_low - nuc.energy_0K_[i_E_low]);
|
|
}
|
|
|
|
// cdf value at upper bound attainable energy
|
|
double m = (nuc.xs_cdf_[i_E_up + 1] - nuc.xs_cdf_[i_E_up]) /
|
|
(nuc.energy_0K_[i_E_up + 1] - nuc.energy_0K_[i_E_up]);
|
|
double cdf_up = nuc.xs_cdf_[i_E_up] + m * (E_up - nuc.energy_0K_[i_E_up]);
|
|
|
|
while (true) {
|
|
// directly sample Maxwellian
|
|
double E_t = -kT * std::log(prn(seed));
|
|
|
|
// sample a relative energy using the xs cdf
|
|
double cdf_rel = cdf_low + prn(seed) * (cdf_up - cdf_low);
|
|
int i_E_rel = lower_bound_index(nuc.xs_cdf_.begin() + i_E_low,
|
|
nuc.xs_cdf_.begin() + i_E_up + 2, cdf_rel);
|
|
double E_rel = nuc.energy_0K_[i_E_low + i_E_rel];
|
|
double m = (nuc.xs_cdf_[i_E_low + i_E_rel + 1] -
|
|
nuc.xs_cdf_[i_E_low + i_E_rel]) /
|
|
(nuc.energy_0K_[i_E_low + i_E_rel + 1] -
|
|
nuc.energy_0K_[i_E_low + i_E_rel]);
|
|
E_rel += (cdf_rel - nuc.xs_cdf_[i_E_low + i_E_rel]) / m;
|
|
|
|
// perform rejection sampling on cosine between
|
|
// neutron and target velocities
|
|
double mu = (E_t + nuc.awr_ * (E - E_rel)) /
|
|
(2.0 * std::sqrt(nuc.awr_ * E * E_t));
|
|
|
|
if (std::abs(mu) < 1.0) {
|
|
// set and accept target velocity
|
|
E_t /= nuc.awr_;
|
|
return std::sqrt(E_t) * rotate_angle(u, mu, nullptr, seed);
|
|
}
|
|
}
|
|
}
|
|
} // case RVS, DBRC
|
|
} // switch (sampling_method)
|
|
|
|
UNREACHABLE();
|
|
}
|
|
|
|
Direction sample_cxs_target_velocity(
|
|
double awr, double E, Direction u, double kT, uint64_t* seed)
|
|
{
|
|
double beta_vn = std::sqrt(awr * E / kT);
|
|
double alpha = 1.0 / (1.0 + std::sqrt(PI) * beta_vn / 2.0);
|
|
|
|
double beta_vt_sq;
|
|
double mu;
|
|
while (true) {
|
|
// Sample two random numbers
|
|
double r1 = prn(seed);
|
|
double r2 = prn(seed);
|
|
|
|
if (prn(seed) < alpha) {
|
|
// With probability alpha, we sample the distribution p(y) =
|
|
// y*e^(-y). This can be done with sampling scheme C45 from the Monte
|
|
// Carlo sampler
|
|
|
|
beta_vt_sq = -std::log(r1 * r2);
|
|
|
|
} else {
|
|
// With probability 1-alpha, we sample the distribution p(y) = y^2 *
|
|
// e^(-y^2). This can be done with sampling scheme C61 from the Monte
|
|
// Carlo sampler
|
|
|
|
double c = std::cos(PI / 2.0 * prn(seed));
|
|
beta_vt_sq = -std::log(r1) - std::log(r2) * c * c;
|
|
}
|
|
|
|
// Determine beta * vt
|
|
double beta_vt = std::sqrt(beta_vt_sq);
|
|
|
|
// Sample cosine of angle between neutron and target velocity
|
|
mu = uniform_distribution(-1., 1., seed);
|
|
|
|
// Determine rejection probability
|
|
double accept_prob =
|
|
std::sqrt(beta_vn * beta_vn + beta_vt_sq - 2 * beta_vn * beta_vt * mu) /
|
|
(beta_vn + beta_vt);
|
|
|
|
// Perform rejection sampling on vt and mu
|
|
if (prn(seed) < accept_prob)
|
|
break;
|
|
}
|
|
|
|
// Determine speed of target nucleus
|
|
double vt = std::sqrt(beta_vt_sq * kT / awr);
|
|
|
|
// Determine velocity vector of target nucleus based on neutron's velocity
|
|
// and the sampled angle between them
|
|
return vt * rotate_angle(u, mu, nullptr, seed);
|
|
}
|
|
|
|
void sample_fission_neutron(
|
|
int i_nuclide, const Reaction& rx, SourceSite* site, Particle& p)
|
|
{
|
|
// Get attributes of particle
|
|
double E_in = p.E();
|
|
uint64_t* seed = p.current_seed();
|
|
|
|
// Determine total nu, delayed nu, and delayed neutron fraction
|
|
const auto& nuc {data::nuclides[i_nuclide]};
|
|
double nu_t = nuc->nu(E_in, Nuclide::EmissionMode::total);
|
|
double nu_d = nuc->nu(E_in, Nuclide::EmissionMode::delayed);
|
|
double beta = nu_d / nu_t;
|
|
|
|
if (prn(seed) < beta) {
|
|
// ====================================================================
|
|
// DELAYED NEUTRON SAMPLED
|
|
|
|
// sampled delayed precursor group
|
|
double xi = prn(seed) * nu_d;
|
|
double prob = 0.0;
|
|
int group;
|
|
for (group = 1; group < nuc->n_precursor_; ++group) {
|
|
// determine delayed neutron precursor yield for group j
|
|
double yield = (*rx.products_[group].yield_)(E_in);
|
|
|
|
// Check if this group is sampled
|
|
prob += yield;
|
|
if (xi < prob)
|
|
break;
|
|
}
|
|
|
|
// if the sum of the probabilities is slightly less than one and the
|
|
// random number is greater, j will be greater than nuc %
|
|
// n_precursor -- check for this condition
|
|
group = std::min(group, nuc->n_precursor_);
|
|
|
|
// set the delayed group for the particle born from fission
|
|
site->delayed_group = group;
|
|
|
|
} else {
|
|
// ====================================================================
|
|
// PROMPT NEUTRON SAMPLED
|
|
|
|
// set the delayed group for the particle born from fission to 0
|
|
site->delayed_group = 0;
|
|
}
|
|
|
|
// sample from prompt neutron energy distribution
|
|
int n_sample = 0;
|
|
double mu;
|
|
while (true) {
|
|
rx.products_[site->delayed_group].sample(E_in, site->E, mu, seed);
|
|
|
|
// resample if energy is greater than maximum neutron energy
|
|
constexpr int neutron = static_cast<int>(ParticleType::neutron);
|
|
if (site->E < data::energy_max[neutron])
|
|
break;
|
|
|
|
// check for large number of resamples
|
|
++n_sample;
|
|
if (n_sample == MAX_SAMPLE) {
|
|
// particle_write_restart(p)
|
|
fatal_error("Resampled energy distribution maximum number of times "
|
|
"for nuclide " +
|
|
nuc->name_);
|
|
}
|
|
}
|
|
|
|
// Sample azimuthal angle uniformly in [0, 2*pi) and assign angle
|
|
site->u = rotate_angle(p.u(), mu, nullptr, seed);
|
|
}
|
|
|
|
void inelastic_scatter(const Nuclide& nuc, const Reaction& rx, Particle& p)
|
|
{
|
|
// copy energy of neutron
|
|
double E_in = p.E();
|
|
|
|
// sample outgoing energy and scattering cosine
|
|
double E;
|
|
double mu;
|
|
rx.products_[0].sample(E_in, E, mu, p.current_seed());
|
|
|
|
// if scattering system is in center-of-mass, transfer cosine of scattering
|
|
// angle and outgoing energy from CM to LAB
|
|
if (rx.scatter_in_cm_) {
|
|
double E_cm = E;
|
|
|
|
// determine outgoing energy in lab
|
|
double A = nuc.awr_;
|
|
E = E_cm + (E_in + 2.0 * mu * (A + 1.0) * std::sqrt(E_in * E_cm)) /
|
|
((A + 1.0) * (A + 1.0));
|
|
|
|
// determine outgoing angle in lab
|
|
mu = mu * std::sqrt(E_cm / E) + 1.0 / (A + 1.0) * std::sqrt(E_in / E);
|
|
}
|
|
|
|
// Because of floating-point roundoff, it may be possible for mu to be
|
|
// outside of the range [-1,1). In these cases, we just set mu to exactly -1
|
|
// or 1
|
|
if (std::abs(mu) > 1.0)
|
|
mu = std::copysign(1.0, mu);
|
|
|
|
// Set outgoing energy and scattering angle
|
|
p.E() = E;
|
|
p.mu() = mu;
|
|
|
|
// change direction of particle
|
|
p.u() = rotate_angle(p.u(), mu, nullptr, p.current_seed());
|
|
|
|
// evaluate yield
|
|
double yield = (*rx.products_[0].yield_)(E_in);
|
|
if (std::floor(yield) == yield && yield > 0) {
|
|
// If yield is integral, create exactly that many secondary particles
|
|
for (int i = 0; i < static_cast<int>(std::round(yield)) - 1; ++i) {
|
|
p.create_secondary(p.wgt(), p.u(), p.E(), ParticleType::neutron);
|
|
}
|
|
} else {
|
|
// Otherwise, change weight of particle based on yield
|
|
p.wgt() *= yield;
|
|
}
|
|
}
|
|
|
|
void sample_secondary_photons(Particle& p, int i_nuclide)
|
|
{
|
|
// Sample the number of photons produced
|
|
double y_t =
|
|
p.neutron_xs(i_nuclide).photon_prod / p.neutron_xs(i_nuclide).total;
|
|
int y = static_cast<int>(y_t);
|
|
if (prn(p.current_seed()) <= y_t - y)
|
|
++y;
|
|
|
|
// Sample each secondary photon
|
|
for (int i = 0; i < y; ++i) {
|
|
// Sample the reaction and product
|
|
int i_rx;
|
|
int i_product;
|
|
sample_photon_product(i_nuclide, p, &i_rx, &i_product);
|
|
|
|
// Sample the outgoing energy and angle
|
|
auto& rx = data::nuclides[i_nuclide]->reactions_[i_rx];
|
|
double E;
|
|
double mu;
|
|
rx->products_[i_product].sample(p.E(), E, mu, p.current_seed());
|
|
|
|
// Sample the new direction
|
|
Direction u = rotate_angle(p.u(), mu, nullptr, p.current_seed());
|
|
|
|
// In a k-eigenvalue simulation, it's necessary to provide higher weight to
|
|
// secondary photons from non-fission reactions to properly balance energy
|
|
// release and deposition. See D. P. Griesheimer, S. J. Douglass, and M. H.
|
|
// Stedry, "Self-consistent energy normalization for quasistatic reactor
|
|
// calculations", Proc. PHYSOR, Cambridge, UK, Mar 29-Apr 2, 2020.
|
|
double wgt;
|
|
if (settings::run_mode == RunMode::EIGENVALUE && !is_fission(rx->mt_)) {
|
|
wgt = simulation::keff * p.wgt();
|
|
} else {
|
|
wgt = p.wgt();
|
|
}
|
|
|
|
// Create the secondary photon
|
|
p.create_secondary(wgt, u, E, ParticleType::photon);
|
|
}
|
|
}
|
|
|
|
} // namespace openmc
|