#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 #include // for max, min, max_element #include // for sqrt, exp, log, abs, copysign #include 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(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(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(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(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(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(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(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(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