From aa005fb2e91d80a00847a46c1bb971567c010a20 Mon Sep 17 00:00:00 2001 From: nuclearkevin <66632997+nuclearkevin@users.noreply.github.com> Date: Sat, 28 Feb 2026 12:17:26 -0600 Subject: [PATCH 01/73] Squash delta tracking commits. --- CMakeLists.txt | 1 + include/openmc/endf.h | 17 + include/openmc/majorant.h | 107 ++++ include/openmc/material.h | 2 + include/openmc/nuclide.h | 4 + include/openmc/particle.h | 3 + include/openmc/particle_data.h | 16 +- include/openmc/settings.h | 1 + include/openmc/simulation.h | 7 + include/openmc/surface.h | 1 + openmc/settings.py | 16 +- src/eigenvalue.cpp | 36 +- src/endf.cpp | 52 ++ src/geometry_aux.cpp | 13 + src/majorant.cpp | 475 ++++++++++++++++++ src/nuclide.cpp | 41 +- src/output.cpp | 4 + src/particle.cpp | 100 +++- src/settings.cpp | 6 + src/simulation.cpp | 73 ++- src/surface.cpp | 1 + .../delta_tracking/__init__.py | 0 tests/regression_tests/delta_tracking/test.py | 53 ++ 23 files changed, 982 insertions(+), 47 deletions(-) create mode 100644 include/openmc/majorant.h create mode 100644 src/majorant.cpp create mode 100644 tests/regression_tests/delta_tracking/__init__.py create mode 100644 tests/regression_tests/delta_tracking/test.py diff --git a/CMakeLists.txt b/CMakeLists.txt index d5af0161081..da0bce04462 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -382,6 +382,7 @@ list(APPEND libopenmc_SOURCES src/ifp.cpp src/initialize.cpp src/lattice.cpp + src/majorant.cpp src/material.cpp src/math_functions.cpp src/mcpl_interface.cpp diff --git a/include/openmc/endf.h b/include/openmc/endf.h index 34fee9758b9..f2cdc6bc872 100644 --- a/include/openmc/endf.h +++ b/include/openmc/endf.h @@ -47,6 +47,8 @@ bool mt_matches(int event_mt, int target_mt); class Function1D { public: virtual double operator()(double x) const = 0; + virtual double max() const = 0; + virtual double max(double min_E, double max_E) const = 0; virtual ~Function1D() = default; }; @@ -69,6 +71,9 @@ class Polynomial : public Function1D { //! \return Polynomial evaluated at x double operator()(double x) const override; + double max() const override; + double max(double min_e, double max_E) const override; + private: vector coef_; //!< Polynomial coefficients }; @@ -90,6 +95,9 @@ class Tabulated1D : public Function1D { //! \return Function evaluated at x double operator()(double x) const override; + double max() const override; + double max(double min_E, double max_E) const override; + // Accessors const vector& x() const { return x_; } const vector& y() const { return y_; } @@ -113,6 +121,9 @@ class CoherentElasticXS : public Function1D { double operator()(double E) const override; + double max() const override; + double max(double min_E, double max_E) const override; + const vector& bragg_edges() const { return bragg_edges_; } const vector& factors() const { return factors_; } @@ -131,6 +142,9 @@ class IncoherentElasticXS : public Function1D { double operator()(double E) const override; + double max() const override; + double max(double min_E, double max_E) const override; + private: double bound_xs_; //!< Characteristic bound xs in [b] double @@ -151,6 +165,9 @@ class Sum1D : public Function1D { //! \return Function evaluated at x double operator()(double E) const override; + double max() const override; + double max(double min_E, double max_E) const override; + const unique_ptr& functions(int i) const { return functions_[i]; } private: diff --git a/include/openmc/majorant.h b/include/openmc/majorant.h new file mode 100644 index 00000000000..d257eb89783 --- /dev/null +++ b/include/openmc/majorant.h @@ -0,0 +1,107 @@ +//! \file majorant.h +//! \brief Majorant cross section type + +#ifndef OPENMC_MAJORANT_H +#define OPENMC_MAJORANT_H + +#include + +#include "openmc/settings.h" +#include "openmc/nuclide.h" + +namespace openmc { + +class Majorant; + +namespace data { + extern std::vector> nuclide_majorants; + extern std::unique_ptr n_majorant; +} + +class Majorant { + +public: + Majorant() = default; + Majorant(const std::vector& energy, const std::vector& xs); + + struct XS { + XS(std::vector energies, + std::vector total_xs) + : energies_(energies), total_xs_(total_xs), idx_(0) + { } + + //! \brief Return the current energy and total cross section values + std::pair get() const; + + //! \brief Return the energy of the current index + double get_e() const; + + //! \brief Return the cross section of the current index + double get_xs() const; + + //! \brief Return the current energy value and advance one + double pop_e(); + + //! \brief Advance the index until past the input energy value + void advance(double energy); + + //! \brief Indicate if all values in the cross section have been visited + bool complete() const; + + //! \brief Return the previous energy, cross section value pair + std::pair prev() const; + + //! \brief Return the previous energy value + double prev_e() const; + + //! \brief Return the previous cross section value + double prev_xs() const; + + //! \brief Increment the cross section index by i + inline + XS& operator +=(size_t i) { this->idx_ += i; return *this; } + + //! \brief Increment the cross section index by one + inline + XS& operator ++(int i) { this->idx_ += 1; return *this; } + + std::vector energies_; + std::vector total_xs_; + size_t idx_; + }; + + //! \brief Determine the intersection of two line segments (p1, p2) and (p3, p4) + static bool intersect_2D(std::pair p1, + std::pair p2, + std::pair p3, + std::pair p4, + std::pair& intersection); + + //! \brief Determine if point p3 is above or below the line segment (p1, p2) + static bool is_above(std::pair p1, + std::pair p2, + std::pair p3); + + public: + void write_ascii(const std::string& filename) const; + + //! \brief Update the majorant using values from another cross section + void update(std::vector energies_other, + std::vector xs_other); + + //! \brief Calculate the microscopic cross section at a given energy + double calculate_xs(double energy) const; + + // data members + public: + std::vector nuclides; // index of nuclides applied + std::vector xs_; // cross section values + Nuclide::EnergyGrid grid_; + constexpr static double safety_factor {1.01}; +}; // class Majorant + + void create_majorant(); + std::vector compute_majorant_energy_grid(); +} + +#endif // OPENMC_MAJORANT_H diff --git a/include/openmc/material.h b/include/openmc/material.h index 3967c36878f..a630d0e0fa1 100644 --- a/include/openmc/material.h +++ b/include/openmc/material.h @@ -237,6 +237,8 @@ class Material { double temperature_ {-1}; }; +void set_majorant_xs(); + //============================================================================== // Non-member functions //============================================================================== diff --git a/include/openmc/nuclide.h b/include/openmc/nuclide.h index ae39a53ddf5..08676f27ffd 100644 --- a/include/openmc/nuclide.h +++ b/include/openmc/nuclide.h @@ -33,6 +33,9 @@ class Nuclide { // Types, aliases using EmissionMode = ReactionProduct::EmissionMode; struct EnergyGrid { + // init method + void init(); + // data members vector grid_index; vector energy; }; @@ -168,6 +171,7 @@ namespace data { // Minimum/maximum transport energy for each particle type. Order corresponds to // transport_index() for supported transport particles. extern array energy_min; +extern array energy_min_rcp; extern array energy_max; //! Minimum temperature in [K] that nuclide data is available at diff --git a/include/openmc/particle.h b/include/openmc/particle.h index 8db9721baa8..2068457fa95 100644 --- a/include/openmc/particle.h +++ b/include/openmc/particle.h @@ -69,6 +69,7 @@ class Particle : public ParticleData { // Coarse-grained particle events void event_calculate_xs(); void event_advance(); + void event_delta_advance(); void event_cross_surface(); void event_collide(); void event_revive_from_secondary(const SourceSite& site); @@ -111,6 +112,8 @@ class Particle : public ParticleData { virtual void mark_as_lost(const char* message) override; using GeometryState::mark_as_lost; + void update_majorant(); + //! create a particle restart HDF5 file void write_restart() const; diff --git a/include/openmc/particle_data.h b/include/openmc/particle_data.h index f72948f6eb4..6169aa6d920 100644 --- a/include/openmc/particle_data.h +++ b/include/openmc/particle_data.h @@ -533,8 +533,9 @@ class ParticleData : public GeometryState { bool write_track_ {false}; - uint64_t seeds_[N_STREAMS]; - int stream_; + // Current PRNG state + uint64_t seeds_[N_STREAMS]; //!< current seeds + int stream_ {STREAM_TRACKING}; //!< current RNG stream vector local_secondary_bank_; @@ -573,6 +574,9 @@ class ParticleData : public GeometryState { int64_t n_progeny_ {0}; + bool delta_tracking_ {false}; // !< Flag to indicate whether or not delta tracking is active + double majorant_ {0.0}; // !< most recent value for the majorant cross section + public: //---------------------------------------------------------------------------- // Constructors @@ -767,6 +771,14 @@ class ParticleData : public GeometryState { // Number of progeny produced by this particle int64_t& n_progeny() { return n_progeny_; } + //! Gets whether the particle is being tracked with delta tracking or not. + bool& delta_tracking() { return delta_tracking_; } + const bool& delta_tracking() const { return delta_tracking_; } + + //! Gets the majorant cross section. + double& majorant() { return majorant_; } + const double& majorant() const { return majorant_; } + //! Gets the pointer to the particle's current PRN seed uint64_t* current_seed() { return seeds_ + stream_; } const uint64_t* current_seed() const { return seeds_ + stream_; } diff --git a/include/openmc/settings.h b/include/openmc/settings.h index 3bba040f5d7..47fb8ccb37f 100644 --- a/include/openmc/settings.h +++ b/include/openmc/settings.h @@ -67,6 +67,7 @@ extern bool create_delayed_neutrons; //!< create delayed fission neutrons? extern "C" bool cmfd_run; //!< is a CMFD run? extern bool delayed_photon_scaling; //!< Scale fission photon yield to include delayed +extern bool delta_tracking; //!< use delta tracking extern "C" bool entropy_on; //!< calculate Shannon entropy? extern "C" bool event_based; //!< use event-based mode (instead of history-based) diff --git a/include/openmc/simulation.h b/include/openmc/simulation.h index 454752cd271..187caf8a21e 100644 --- a/include/openmc/simulation.h +++ b/include/openmc/simulation.h @@ -34,6 +34,7 @@ extern "C" double extern "C" double k_abs_tra; //!< sum over batches of k_absorption * k_tracklength extern double log_spacing; //!< lethargy spacing for energy grid searches +extern double log_spacing_rcp; extern "C" int n_lost_particles; //!< cumulative number of lost particles extern "C" bool need_depletion_rx; //!< need to calculate depletion rx? extern "C" int restart_batch; //!< batch at which a restart job resumed @@ -118,6 +119,12 @@ void transport_history_based(); //! secondary bank void transport_history_based_shared_secondary(); +//! Simulate a single particle history from birth to death using delta tracking +void transport_delta_tracking_single_particle(Particle& p); + +//! Simulate all particle histories using delta tracking +void transport_delta_tracking(); + //! Simulate all particle histories using event-based parallelism void transport_event_based(); diff --git a/include/openmc/surface.h b/include/openmc/surface.h index 2d8580345a4..baeca7771d6 100644 --- a/include/openmc/surface.h +++ b/include/openmc/surface.h @@ -28,6 +28,7 @@ class Surface; namespace model { extern std::unordered_map surface_map; extern vector> surfaces; +extern vector boundary_surfaces; } // namespace model //============================================================================== diff --git a/openmc/settings.py b/openmc/settings.py index 8120eb073e6..9592ef106a2 100644 --- a/openmc/settings.py +++ b/openmc/settings.py @@ -482,7 +482,7 @@ def __init__(self, **kwargs): self._delayed_photon_scaling = None self._material_cell_offsets = None self._log_grid_bins = None - + self._delta_tracking = None self._event_based = None self._max_particles_in_flight = None self._max_particle_events = None @@ -1240,6 +1240,15 @@ def delayed_photon_scaling(self, value: bool): cv.check_type('delayed photon scaling', value, bool) self._delayed_photon_scaling = value + @property + def delta_tracking(self): + return self._delta_tracking + + @delta_tracking.setter + def delta_tracking(self, value): + cv.check_type('event_based', value, bool) + self._delta_tracking = value + @property def material_cell_offsets(self) -> bool: return self._material_cell_offsets @@ -2002,6 +2011,10 @@ def _create_max_tracks_subelement(self, root): if self._max_tracks is not None: elem = ET.SubElement(root, "max_tracks") elem.text = str(self._max_tracks) + def _create_delta_tracking_subelement(self, root): + if self._delta_tracking: + elem = ET.SubElement(root, "delta_tracking") + elem.text = str(self._delta_tracking).lower() def _create_random_ray_subelement(self, root, mesh_memo=None): if self._random_ray: @@ -2637,6 +2650,7 @@ def to_xml_element(self, mesh_memo=None): self._create_use_decay_photons_subelement(element) self._create_source_rejection_fraction_subelement(element) self._create_free_gas_threshold_subelement(element) + self._create_delta_tracking_subelement(element) # Clean the indentation in the file to be user-readable clean_indentation(element) diff --git a/src/eigenvalue.cpp b/src/eigenvalue.cpp index bc8dcc9ea77..31bd201b012 100644 --- a/src/eigenvalue.cpp +++ b/src/eigenvalue.cpp @@ -49,9 +49,16 @@ void calculate_generation_keff() const auto& gt = simulation::global_tallies; // Get keff for this generation by subtracting off the starting value - simulation::keff_generation = - gt(GlobalTally::K_TRACKLENGTH, TallyResult::VALUE) - - simulation::keff_generation; + if (settings::delta_tracking) { + simulation::keff_generation = + gt(GlobalTally::K_COLLISION, TallyResult::VALUE) - + simulation::keff_generation; + } else { + simulation::keff_generation = + gt(GlobalTally::K_TRACKLENGTH, TallyResult::VALUE) - + simulation::keff_generation; + } + double keff_reduced; #ifdef OPENMC_MPI @@ -448,6 +455,21 @@ int openmc_get_keff(double* k_combined) // Copy estimates of k-effective and its variance (not variance of the mean) const auto& gt = simulation::global_tallies; + if (settings::delta_tracking) { + array kv {}; + tensor::Tensor cov = tensor::Tensor({2, 2}); + + kv[0] = gt(GlobalTally::K_COLLISION, TallyResult::SUM) / n; + kv[1] = gt(GlobalTally::K_ABSORPTION, TallyResult::SUM) / n; + + cov(0, 0) = (gt(GlobalTally::K_COLLISION, TallyResult::SUM_SQ) - n*kv[0]*kv[0]) / (n -1); + cov(1, 1) = (gt(GlobalTally::K_ABSORPTION, TallyResult::SUM_SQ) - n*kv[1]*kv[1]) / (n - 1); + + // Calculate covariances based on sums with Bessel's correction + cov(0, 1) = (simulation::k_col_abs - n * kv[0] * kv[1]) / (n - 1); + cov(1, 0) = cov(0, 1); + } + array kv {}; tensor::Tensor cov = tensor::zeros({3, 3}); kv[0] = gt(GlobalTally::K_COLLISION, TallyResult::SUM) / n; @@ -504,6 +526,14 @@ int openmc_get_keff(double* k_combined) use_three = true; } + // if delta tracking is enabled, use the collision and absorption + // estimators only + if (settings::delta_tracking) { + i = 0; + j = 1; + use_three = false; + } + if (use_three) { // Use three estimators as derived in the paper by Urbatsch diff --git a/src/endf.cpp b/src/endf.cpp index 8a9c5e48a85..a3a53dd1b80 100644 --- a/src/endf.cpp +++ b/src/endf.cpp @@ -209,6 +209,14 @@ double Polynomial::operator()(double x) const return y; } +double Polynomial::max() const { + throw(std::runtime_error("Max is not implemented for polynomial functions.")); +} + +double Polynomial::max(double min_E, double max_E) const { + throw(std::runtime_error("Max is not implemented for polynomial functions.")); +} + //============================================================================== // Tabulated1D implementation //============================================================================== @@ -298,6 +306,23 @@ double Tabulated1D::operator()(double x) const } } +double Tabulated1D::max() const { + return *std::max_element(y_.begin(), y_.end()); +} + +double Tabulated1D::max(double min_E, double max_E) const { + std::vector vals; + + for (int i = 0; i <= x().size(); i++) { + double x_val = x()[i]; + if (x_val >= min_E && x_val <= max_E) { + vals.push_back(y()[i]); + } + } + + return *std::max_element(vals.begin(), vals.end()); +} + //============================================================================== // CoherentElasticXS implementation //============================================================================== @@ -330,6 +355,17 @@ double CoherentElasticXS::operator()(double E) const } } +double CoherentElasticXS::max() const { + return *std::max_element(factors_.begin(), factors_.end()); +} + +double CoherentElasticXS::max(double min_E, double max_E) const { + auto i_grid_min = lower_bound_index(bragg_edges().begin(), bragg_edges().end(), min_E); + auto i_grid_max = lower_bound_index(bragg_edges().begin(), bragg_edges().end(), max_E); + + return std::max({factors()[i_grid_min], factors()[i_grid_max]}); +} + //============================================================================== // IncoherentElasticXS implementation //============================================================================== @@ -349,6 +385,14 @@ double IncoherentElasticXS::operator()(double E) const return bound_xs_ / 2.0 * ((1 - std::exp(-4.0 * E * W)) / (2.0 * E * W)); } +double IncoherentElasticXS::max() const { + return (*this)(0.0); +} + +double IncoherentElasticXS::max(double min_E, double max_E) const { + return (*this)(min_E); +} + //============================================================================== // Sum1D implementation //============================================================================== @@ -375,4 +419,12 @@ double Sum1D::operator()(double x) const return result; } +double Sum1D::max() const { + return (*this)(0.0); +} + +double Sum1D::max(double min_E, double max_E) const { + return std::max((*this)(min_E), (*this)(max_E)); +} + } // namespace openmc diff --git a/src/geometry_aux.cpp b/src/geometry_aux.cpp index a740740c1e6..0ef0aeb4acf 100644 --- a/src/geometry_aux.cpp +++ b/src/geometry_aux.cpp @@ -268,6 +268,17 @@ void get_temperatures( } } +void detect_boundary_surfaces() { + for (int i = 0; i < model::surfaces.size(); i++) { + // if the surface has a non-transmission boundary condition, + // add it to the list of surfaces to track during delta tracking + const auto& s = model::surfaces[i]; + if (s->bc_) { + model::boundary_surfaces.push_back(i); + } + } +} + //============================================================================== void finalize_geometry() @@ -280,6 +291,8 @@ void finalize_geometry() // Assign temperatures to cells that don't have temperatures already assigned assign_temperatures(); + detect_boundary_surfaces(); + // Determine number of nested coordinate levels in the geometry model::n_coord_levels = maximum_levels(model::root_universe); } diff --git a/src/majorant.cpp b/src/majorant.cpp new file mode 100644 index 00000000000..df28239295e --- /dev/null +++ b/src/majorant.cpp @@ -0,0 +1,475 @@ +#include + +#include + +#include "openmc/constants.h" +#include "openmc/majorant.h" +#include "openmc/material.h" +#include "openmc/nuclide.h" +#include "openmc/search.h" +#include "openmc/simulation.h" +#include "openmc/thermal.h" + +namespace openmc { + +namespace data { +std::vector> nuclide_majorants; +std::unique_ptr n_majorant; +} + +void create_majorant() { + write_message("Creating majorant cross section..."); + // create a majorant XS for each nuclide + for (const auto& nuclide : data::nuclides) { + data::nuclide_majorants.push_back(std::make_unique()); + auto& majorant = data::nuclide_majorants.back(); + + for (int t = 0; t < nuclide->kTs_.size(); t++) { + auto total = nuclide->xs_[t].slice(openmc::tensor::all, 0); + std::vector xs(total.begin(), total.end()); + auto energies = nuclide->grid_[t].energy; + majorant->update(energies, xs); + + // include unresolved resonance region data + // in the majorant if present + if (nuclide->urr_present_) { + const auto& urr_data = nuclide->urr_data_[t]; + std::vector energies(urr_data.energy_.begin(), urr_data.energy_.end()); + + double max_urr_total {0.0}; + + for (auto xs_vals : urr_data.xs_values_) { + max_urr_total = std::max(max_urr_total, xs_vals.total); + } + + if(urr_data.interp_ == Interpolation::log_log) { + std::cout << fmt::format("Nuclide {} uses log-log interpolation", nuclide->name_) << std::endl; + } + + if (urr_data.multiply_smooth_) { + std::cout << "Multiplied URR: " << nuclide->name_ << std::endl; + majorant->grid_.init(); + std::vector xs_vals; + for (int i = 0; i < energies.size(); i++) { + xs_vals.push_back(majorant->calculate_xs(energies[i]) * max_urr_total); + } + majorant->update(energies, xs_vals); + } else { + std::vector xs_vals(energies.size(), max_urr_total); + majorant->update(energies, xs_vals); + } + // majorant->update_urr(energies, xs, urr_data.interp_); + } + } + // initialize the energy grid for this nuclide + majorant->grid_.init(); + // majorant->write_ascii(nuclide->name_ + "_majorant.txt"); + } + + auto majorant_e_grid = compute_majorant_energy_grid(); + + + std::vector xs_vals; + + std::vector macro_majorants; + + // compute a majorant for every material + for (auto& material : model::materials) { + std::vector material_xs; + for (auto e_val : majorant_e_grid) { + double xs_val = 0.0; + + // See if thermal data is present for any of the material nuclides + bool check_sab = material->thermal_tables_.size() > 0; + int thermal_table_idx = 0; + + int j = 0; + + for (int i = 0; i < material->nuclide_.size(); i++) { + int i_nuc = material->nuclide_[i]; + xs_val += material->atom_density_(i) * data::nuclide_majorants[i_nuc]->calculate_xs(e_val); + + int i_sab = C_NONE; + double sab_frac = 0.0; + + if (check_sab) { + const auto& sab {material->thermal_tables_[j]}; + if (i == sab.index_nuclide) { + // Get index in the sab_tables + i_sab = sab.index_table; + sab_frac = sab.fraction; + + ++j; + if (j == material->thermal_tables_.size()) check_sab = false; + } + } + + // compute the max sab xs if present + if (i_sab >= 0) { + const auto& tdata = data::thermal_scatt[i_sab]; + // compute the maximum xs value over all temperatures + double thermal_xs = 0.0; + for (int k = 0; k < tdata->kTs_.size(); k++) { + // compute the thermal xs at the temperature and energy + double elastic, inelastic; + tdata->data_[k].calculate_xs(e_val, &elastic, &inelastic); + if (elastic + inelastic > thermal_xs) { + thermal_xs = elastic + inelastic; + } + } + // adjust the current xs_val for the thermal component + xs_val += sab_frac * thermal_xs; + } + + } + + material_xs.push_back(xs_val); + } + macro_majorants.emplace_back(Majorant()); + macro_majorants.back().update(majorant_e_grid, material_xs); + macro_majorants.back().write_ascii(fmt::format("mat_{}_majorant.txt", material->id_)); + } + + data::n_majorant = std::make_unique(); + + for (auto& macro_majorant : macro_majorants) { + data::n_majorant->update(macro_majorant.grid_.energy, macro_majorant.xs_); + } + + data::n_majorant->grid_.init(); + data::n_majorant->write_ascii("macro_majorant.txt"); +} + +std::vector +compute_majorant_energy_grid() { + + std::vector common_e_grid; + for (const auto& nuc_maj : data::nuclide_majorants) { + auto& e_grid = nuc_maj->grid_.energy; + // append new points to the current group of points + common_e_grid.insert(common_e_grid.end(), e_grid.begin(), e_grid.end()); + + // remove duplicates + std::unique(common_e_grid.begin(), common_e_grid.end()); + } + std::sort(common_e_grid.begin(), common_e_grid.end()); + + // remove all values below the minimum neutron energy + int neutron = ParticleType::neutron().transport_index(); + auto min_it = common_e_grid.begin(); + while (*min_it < data::energy_min[neutron]) { min_it++; } + common_e_grid.erase(common_e_grid.begin(), min_it + 1); + // insert the minimum neutron energy at the beginning + common_e_grid.insert(common_e_grid.begin(), data::energy_min[neutron]); + + // remove all values above the maximum neutron energy + auto max_it = --common_e_grid.end(); + while (*max_it > data::energy_max[neutron]) { max_it--; } + common_e_grid.erase(max_it - 1, common_e_grid.end()); + // insert the maximum neutron energy at the end + common_e_grid.insert(common_e_grid.end(), data::energy_max[neutron]); + + return common_e_grid; +} + + +Majorant::Majorant(const std::vector& energy, + const std::vector& xs) : xs_(xs) +{ + grid_.energy = energy; + grid_.init(); +} + +double +Majorant::calculate_xs(double energy) const +{ + // Find energy index on energy grid + int neutron = ParticleType::neutron().transport_index(); + int i_log_union = std::log(energy * data::energy_min_rcp[neutron]) * simulation::log_spacing_rcp; + + int i_grid; + if (i_log_union < 0) { + i_grid = 0; + } else if (i_log_union >= (grid_.grid_index.size() - 2)) { + i_grid = grid_.energy.size() - 2; + } else { + // Determine bounding indices based on which equal log-spaced + // interval the energy is in + int i_low = grid_.grid_index[i_log_union]; + int i_high = grid_.grid_index[i_log_union + 1] + 1; + + // Perform binary search over reduced range + i_grid = i_low + lower_bound_index(&grid_.energy[i_low], &grid_.energy[i_high], energy); + } + + // check for rare case where two energy points are the same + if (grid_.energy[i_grid] == grid_.energy[i_grid + 1]) ++i_grid; + + // calculate interpolation factor + double f = (energy - grid_.energy[i_grid]) / + (grid_.energy[i_grid + 1]- grid_.energy[i_grid]); + + double xs = (1.0 - f) * xs_[i_grid] + f * xs_[i_grid + 1]; + + return 1.0 * xs; +} + +bool Majorant::intersect_2D(std::pair p1, + std::pair p2, + std::pair p3, + std::pair p4, + std::pair& intersection) { + + double denominator = (p4.first - p3.first) * (p1.second - p2.second) - + (p1.first - p2.first) * (p4.second - p3.second); + + // if the lines are parallel, return no intersection + if (fabs(denominator) <= FP_PRECISION) { return false; } + + double numerator = (p3.second - p4.second) * (p1.first - p3.first) + + (p4.first - p3.first) * (p1.second - p3.second); + + double t = numerator / denominator; + + if (t < 0.0 || t > 1.0) { return false; } + + // compute intersection location + double x = p1.first + (p2.first - p1.first) * t; + double slope = (p2.second - p1.second) / (p2.first - p1.first); + double y = p1.second + (x - p1.first) * slope; + intersection = {x, y}; + return true; +} + +bool Majorant::is_above(std::pair p1, + std::pair p2, + std::pair p3) { + // if the line is vertical, use + // comparison of x values + if (fabs(p2.first - p1.first) < FP_PRECISION) { + return p3.first < p2.first; + } else { + double slope = (p2.second - p1.second) / (p2.first - p1.first); + double val = p1.second + slope * (p3.first - p1.first); + return val < p3.second; + } + +} + +void Majorant::write_ascii(const std::string& filename) const { + + std::ofstream of(filename); + + for (int i = 0; i < xs_.size(); i++) { + of << grid_.energy[i] << "\t" << xs_[i] << "\n"; + } + + of.close(); +} + +void Majorant::update(std::vector energy_other, + std::vector xs_other) { + + XS xs_a(grid_.energy, xs_); + XS xs_b(energy_other, xs_other); + + // early exit checks + if (xs_b.complete()) { return; } + + if (xs_a.complete()) { + grid_.energy = energy_other; + xs_ = xs_other; + return; + } + + // resulting output of this algorithm + std::vector e_out, xs_out; + std::vector mask; + + // references we'll use to keep track of which + // is currently considered the larger cross section + XS& current_xs = xs_a; + XS& other_xs = xs_b; + + // if the other cross section starts at a lower energy, its + // value is considered to be higher + if (other_xs.get_e() < current_xs.get_e()) { + std::swap(current_xs, other_xs); + } + + // if the two cross sections start at the same energy + // pick the one with the higher xs value + if (other_xs.get_e() == current_xs.get_e() && other_xs.get_xs() > current_xs.get_xs()) { + std::swap(current_xs, other_xs); + } + + // add the first point to the final cross section + e_out.push_back(current_xs.get_e()); + xs_out.push_back(current_xs.get_xs()); + current_xs++; + + // continue adding points until the other xs min + // energy is lower than the output minimum energy + while(current_xs.get_e() < other_xs.get_e() && !current_xs.complete()) { + e_out.push_back(current_xs.get_e()); + xs_out.push_back(current_xs.get_xs()); + current_xs++; + } + + // Corner case: first point of the other xs is higher and nearer + // There is no intersection to find b/c there is no previous point to + // use for interpolation. Here, we'll insert a point at that energy + // in the current cross section, insert the point of the other cross + // section, and swap the two. + // NOTE: possible problem with computing intersections with vertical slopes here + if (current_xs.get_e() <= other_xs.get_e() && is_above({e_out.back(), xs_out.back()}, current_xs.get(), other_xs.get())) { + // insert point on current xs segment + double slope = (current_xs.get_xs() - xs_out.back()) / + (current_xs.get_e() - e_out.back()); + double xs_val = xs_out.back() + slope * (other_xs.get_e() - e_out.back()); + // insert point from other cross section + e_out.push_back(other_xs.get_e()); + xs_out.push_back(other_xs.get_xs()); + // swap the cross sections and advance + std::swap(current_xs, other_xs); + current_xs.advance(e_out.back()); + other_xs.advance(e_out.back()); + } + + // the next point added to the cross section is selected based 4 different cases + // + // above: the other cross section point above the line between the last point added and the current xs's next point + // below: ther other cross section point is below the line betwee the last point added and the current xs's next point + // nearer: the other cross section point is closer in energy to the last point added + // farther: the other cross section point is farther in energy from the last point added + // + // Case 1: other xs value is above and nearer + // + // Check for an intersection with the line segment between last_pnt and current_next. + // If an intersection exists, insert a point in the majorant at that location and + // swap the current and other cross sections. + // + // Case 2: other xs value is above and farther + // + // Check for an intersection with the line segment between last_pnt and current_next. + // Apply the same treatment as Case 1. + // + // Case 3: other xs value is below and nearer + // + // Insert a point at the energy of the other xs value on the line segment between + // last_pnt and current_next. This point is superfluous and purely for the algorithm + // robustness, so the index of this point is added to the mask of indices to be removed + // later. + // + // Case 4: other xs value is below and farther + // + // Insert the location of current_next to the output cross section and continue + + // start looping over values + while (true) { + + if (current_xs.complete() || other_xs.complete()) { break; } + + auto current_next = current_xs.get(); + auto other_next = other_xs.get(); + std::pair last_pnt = {e_out.back(), xs_out.back()}; + + bool above = is_above(last_pnt, current_next, other_next); + bool nearer = other_next.first < current_next.first; + + // for clarity + bool below = !above; + bool farther = !nearer; + + // Cases 1 & 2 + if (above) { + // setup intersection check + auto p1 = last_pnt; + auto p2 = current_next; + auto p3 = other_xs.prev(); + auto p4 = other_next; + + std::pair intersection; + auto intersection_found = intersect_2D(p1, p2, p3, p4, intersection); + + if (intersection_found) { + e_out.push_back(intersection.first); + xs_out.push_back(intersection.second); + std::swap(current_xs, other_xs); + } else { + e_out.push_back(current_next.first); + xs_out.push_back(current_next.second); + } + // Case 3 + } else if (below and nearer) { + // compute the y value + double slope = (current_next.second - last_pnt.second) / + (current_next.first - last_pnt.first); + double xs_val = last_pnt.second + slope * (other_next.first - last_pnt.first); + e_out.push_back(other_next.first); + xs_out.push_back(xs_val); + // add index of this point to the mask + mask.push_back(e_out.size() - 1); + // Case 4 + } else if (below and farther) { + e_out.push_back(current_next.first); + xs_out.push_back(current_next.second); + } + + current_xs.advance(e_out.back()); + other_xs.advance(e_out.back()); + } + + // remove any superfluous points + std::sort(mask.begin(), mask.end(), std::greater()); + for (auto idx : mask) { + e_out.erase(e_out.begin() + idx); + xs_out.erase(xs_out.begin() + idx); + } + + // add any additional data to the output xs + while (!xs_a.complete()) { + e_out.push_back(xs_a.get_e()); + xs_out.push_back(xs_a.get_xs()); + xs_a++; + } + + while (!xs_b.complete()) { + e_out.push_back(xs_b.get_e()); + xs_out.push_back(xs_b.get_xs()); + xs_b++; + } + + // increase all xs values in the majorant by 5 percent + std::transform(xs_out.begin(), xs_out.end(), xs_out.begin(), [](double xs_val) { return xs_val * 1.05; }); + + // update the values of the majorant + grid_.energy = std::move(e_out); + xs_ = std::move(xs_out); + +} + +// Majorant XS definitions + +std::pair +Majorant::XS::get() const { return {energies_.at(idx_), total_xs_.at(idx_)}; } + +double Majorant::XS::get_e() const { return energies_.at(idx_); } + +double Majorant::XS::get_xs() const { return total_xs_.at(idx_); } + +std::pair +Majorant::XS::prev() const { return {energies_.at(idx_ - 1), total_xs_.at(idx_ - 1)}; } + +double Majorant::XS::prev_e() const { return energies_.at(idx_ - 1); } + +double Majorant::XS::prev_xs() const { return total_xs_.at(idx_ - 1); } + +void Majorant::XS::advance(double energy) { + double e = energies_[idx_]; + while (e <= energy && !this->complete()) { e = energies_[++idx_]; } +} + +bool Majorant::XS::complete() const { return idx_ >= energies_.size(); } + +} diff --git a/src/nuclide.cpp b/src/nuclide.cpp index 17d6e952c39..fda2e0e2bbf 100644 --- a/src/nuclide.cpp +++ b/src/nuclide.cpp @@ -31,6 +31,7 @@ namespace openmc { namespace data { array energy_min {0.0, 0.0, 0.0, 0.0}; +array energy_min_rcp{0.0, 0.0, 0.0, 0.0}; array energy_max {INFTY, INFTY, INFTY, INFTY}; double temperature_min {INFTY}; double temperature_max {0.0}; @@ -493,7 +494,7 @@ void Nuclide::create_derived( } } -void Nuclide::init_grid() +void Nuclide::EnergyGrid::init() { int neutron = ParticleType::neutron().transport_index(); double E_min = data::energy_min[neutron]; @@ -506,23 +507,27 @@ void Nuclide::init_grid() // Create equally log-spaced energy grid auto umesh = tensor::linspace(0.0, M * spacing, M + 1); - for (auto& grid : grid_) { - // Resize array for storing grid indices - grid.grid_index.resize(M + 1); - - // Determine corresponding indices in nuclide grid to energies on - // equal-logarithmic grid - int j = 0; - for (int k = 0; k <= M; ++k) { - while (std::log(grid.energy[j + 1] / E_min) <= umesh(k)) { - // Ensure that for isotopes where maxval(grid.energy) << E_max that - // there are no out-of-bounds issues. - if (j + 2 == grid.energy.size()) - break; - ++j; - } - grid.grid_index[k] = j; + grid_index.resize(M + 1); + + // Determine corresponding indices in nuclide grid to energies on + // equal-logarithmic grid + int j = 0; + for (int k = 0; k <= M; ++k) { + while (std::log(energy[j + 1]/E_min) <= umesh(k)) { + // Ensure that for isotopes where maxval(grid.energy) << E_max that + // there are no out-of-bounds issues. + if (j + 2 == energy.size()) break; + ++j; } + grid_index[k] = j; + } +} + +void Nuclide::init_grid() +{ + for (auto& grid : grid_) + { + grid.init(); } } @@ -823,7 +828,7 @@ void Nuclide::calculate_xs( // If the particle is in the unresolved resonance range and there are // probability tables, we need to determine cross sections from the table - if (settings::urr_ptables_on && urr_present_ && !use_mp) { + if (!p.delta_tracking() && settings::urr_ptables_on && urr_present_ && !use_mp) { if (urr_data_[micro.index_temp].energy_in_bounds(p.E())) this->calculate_urr_xs(micro.index_temp, p); } diff --git a/src/output.cpp b/src/output.cpp index 725e8d0693b..807f3b6a1db 100644 --- a/src/output.cpp +++ b/src/output.cpp @@ -553,9 +553,13 @@ void print_results() std::tie(mean, stdev) = mean_stdev(>(GlobalTally::K_COLLISION, 0), n); fmt::print(" k-effective (Collision) = {:.5f} +/- {:.5f}\n", mean, t_n1 * stdev); + if (settings::delta_tracking) { + fmt::print(" k-effective (Track-length) = (Delta-tracking enabled)\n"); + } else { std::tie(mean, stdev) = mean_stdev(>(GlobalTally::K_TRACKLENGTH, 0), n); fmt::print(" k-effective (Track-length) = {:.5f} +/- {:.5f}\n", mean, t_n1 * stdev); + } std::tie(mean, stdev) = mean_stdev(>(GlobalTally::K_ABSORPTION, 0), n); fmt::print(" k-effective (Absorption) = {:.5f} +/- {:.5f}\n", mean, t_n1 * stdev); diff --git a/src/particle.cpp b/src/particle.cpp index 779ae18da9e..5f25a2784cf 100644 --- a/src/particle.cpp +++ b/src/particle.cpp @@ -15,6 +15,7 @@ #include "openmc/geometry.h" #include "openmc/hdf5_interface.h" #include "openmc/lattice.h" +#include "openmc/majorant.h" #include "openmc/material.h" #include "openmc/message_passing.h" #include "openmc/mgxs_interface.h" @@ -152,6 +153,7 @@ void Particle::from_source(const SourceSite* src) material() = C_NONE; n_collision() = 0; fission() = false; + majorant() = 0.0; zero_flux_derivs(); lifetime() = 0.0; #ifdef OPENMC_DAGMC_ENABLED @@ -176,7 +178,8 @@ void Particle::from_source(const SourceSite* src) g_last() = static_cast(src->E); E() = data::mg.energy_bin_avg_[g()]; } - E_last() = E(); + + E_last() = E(); // maybe should be 0.0? time() = src->time; time_last() = src->time; parent_nuclide() = src->parent_nuclide; @@ -191,6 +194,10 @@ void Particle::from_source(const SourceSite* src) wgt_born() = src->wgt_born; wgt_ww_born() = src->wgt_ww_born; n_split() = src->n_split; + + if (delta_tracking()) { + update_majorant(); + } } void Particle::event_calculate_xs() @@ -215,8 +222,12 @@ void Particle::event_calculate_xs() // beginning of the history and again for any secondary particles if (lowest_coord().cell() == C_NONE) { if (!exhaustive_find_cell(*this)) { - mark_as_lost( - "Could not find the cell containing particle " + std::to_string(id())); + if (!delta_tracking()) { + wgt() = 0.0; + } else { + mark_as_lost("Could not find the cell containing particle " + + std::to_string(id())); + } return; } @@ -306,7 +317,7 @@ void Particle::event_advance() } // Score track-length estimate of k-eff - if (settings::run_mode == RunMode::EIGENVALUE && type().is_neutron()) { + if (settings::run_mode == RunMode::EIGENVALUE && type().is_neutron() && !delta_tracking()) { keff_tally_tracklength() += wgt() * distance * macro_xs().nu_fission; } @@ -321,6 +332,61 @@ void Particle::event_advance() } } +void Particle::event_delta_advance() +{ + if (E() != E_last()) { + update_majorant(); + } + + // sample distance to next position + double distance; + if (type() == ParticleType::electron() || type() == ParticleType::positron()) { + distance = 0.0; + } else { + // calculate majorant value for this energy + distance = -std::log(prn(current_seed())) / majorant(); + } + + // Advance the particle (applying boundary conditions) until it either: + // i) Reaches a collision site; + // ii) Or leaks out of a vacuum boundary condition. + while (distance >= 0 && alive()) { + // update distance to problem boundary + boundary().distance() = INFTY; + boundary().surface() = 0; + boundary().coord_level() = 1; + for (auto s_idx : model::boundary_surfaces) { + const auto& s = model::surfaces[s_idx]; + double surf_dist = s->distance(r(), u(), false); + if (surf_dist < boundary().distance()) { + boundary().distance() = surf_dist; + boundary().surface() = s_idx + 1; + if (s->sense(r(), u())) { + boundary().surface() *= -1; + } + } + } + + // We collided before crossing a boundary surface. + // Need to advance the particle to the collision site. + if (distance < boundary().distance()) { + move_distance(distance); + break; + } + + // Advance particle to the boundary surface. + move_distance(boundary().distance()); + event_cross_surface(); + distance -= boundary().distance(); + } + + // Ensure that cross sections will be re-calculated if + // the energy has changed. + if (E() != E_last()) { + material_last() = C_NONE; + } +} + void Particle::event_cross_surface() { // Saving previous cell data @@ -384,6 +450,11 @@ void Particle::event_cross_surface() void Particle::event_collide() { + // Store pre-collision particle properties + wgt_last() = wgt(); + E_last() = E(); + u_last() = u(); + r_last() = r(); // Score collision estimate of keff if (settings::run_mode == RunMode::EIGENVALUE && type().is_neutron()) { @@ -525,8 +596,8 @@ void Particle::event_revive_from_secondary(const SourceSite& site) void Particle::event_check_limit_and_revive() { // If particle has too many events, display warning and kill it - n_event()++; - if (n_event() == settings::max_particle_events) { + ++n_event(); + if (n_event() == settings::max_particle_events && !delta_tracking()) { warning("Particle " + std::to_string(id()) + " underwent maximum number of events."); wgt() = 0.0; @@ -558,8 +629,10 @@ void Particle::event_death() global_tally_absorption += keff_tally_absorption(); #pragma omp atomic global_tally_collision += keff_tally_collision(); + if (!delta_tracking()) { #pragma omp atomic - global_tally_tracklength += keff_tally_tracklength(); + global_tally_tracklength += keff_tally_tracklength(); + } #pragma omp atomic global_tally_leakage += keff_tally_leakage(); @@ -771,10 +844,10 @@ void Particle::cross_reflective_bc(const Surface& surf, Direction new_u) // the lower universes. // (unless we're using a dagmc model, which has exactly one universe) n_coord() = 1; - if (surf.geom_type() != GeometryType::DAG && - !neighbor_list_find_cell(*this)) { - mark_as_lost("Couldn't find particle after reflecting from surface " + - std::to_string(surf.id_) + "."); + if (surf.geom_type() != GeometryType::DAG && !neighbor_list_find_cell(*this)) { + exhaustive_find_cell(*this); + this->mark_as_lost("Couldn't find particle after reflecting from surface " + + std::to_string(surf.id_) + "."); return; } @@ -835,6 +908,11 @@ void Particle::cross_periodic_bc( } } +void Particle::update_majorant() +{ + this->majorant() = 1.000001 * data::n_majorant->calculate_xs(this->E()); +} + void Particle::mark_as_lost(const char* message) { // Print warning and write lost particle file diff --git a/src/settings.cpp b/src/settings.cpp index 8ae252ae1ab..463bc53aa7b 100644 --- a/src/settings.cpp +++ b/src/settings.cpp @@ -53,6 +53,7 @@ bool confidence_intervals {false}; bool create_delayed_neutrons {true}; bool create_fission_neutrons {true}; bool delayed_photon_scaling {true}; +bool delta_tracking {false}; bool entropy_on {false}; bool event_based {false}; bool ifp_on {false}; @@ -1229,6 +1230,11 @@ void read_settings_xml(pugi::xml_node root) event_based = get_node_value_bool(root, "event_based"); } + // Check whether or not to use delta tracking + if (check_for_node(root, "delta_tracking")) { + delta_tracking = get_node_value_bool(root, "delta_tracking"); + } + // Check whether material cell offsets should be generated if (check_for_node(root, "material_cell_offsets")) { material_cell_offsets = get_node_value_bool(root, "material_cell_offsets"); diff --git a/src/simulation.cpp b/src/simulation.cpp index 89aa9ca0ef3..554af24c1a0 100644 --- a/src/simulation.cpp +++ b/src/simulation.cpp @@ -9,6 +9,7 @@ #include "openmc/event.h" #include "openmc/geometry_aux.h" #include "openmc/ifp.h" +#include "openmc/majorant.h" #include "openmc/material.h" #include "openmc/message_passing.h" #include "openmc/nuclide.h" @@ -85,6 +86,8 @@ int openmc_simulation_init() initialize_data(); } + if (settings::delta_tracking) create_majorant(); + // Determine how much work each process should do calculate_work(settings::n_particles); @@ -273,11 +276,16 @@ int openmc_next_batch(int* status) } else { transport_event_based(); } + transport_event_based(); } else { - if (settings::use_shared_secondary_bank) { - transport_history_based_shared_secondary(); + if (settings::delta_tracking) { + transport_delta_tracking(); } else { - transport_history_based(); + if (settings::use_shared_secondary_bank) { + transport_history_based_shared_secondary(); + } else { + transport_history_based(); + } } } @@ -331,6 +339,7 @@ double k_col_abs {0.0}; double k_col_tra {0.0}; double k_abs_tra {0.0}; double log_spacing; +double log_spacing_rcp; int n_lost_particles {0}; bool need_depletion_rx {false}; int restart_batch; @@ -539,8 +548,13 @@ void initialize_generation() ufs_count_sites(); // Store current value of tracklength k - simulation::keff_generation = simulation::global_tallies( - GlobalTally::K_TRACKLENGTH, TallyResult::VALUE); + if (settings::delta_tracking) { + simulation::keff_generation = simulation::global_tallies( + GlobalTally::K_COLLISION, TallyResult::VALUE); + } else { + simulation::keff_generation = simulation::global_tallies( + GlobalTally::K_TRACKLENGTH, TallyResult::VALUE); + } } } @@ -671,6 +685,11 @@ void initialize_particle_track( write_message("Simulating Particle {}", p.id()); } + // Compute the majorant. + if (settings::delta_tracking) { + p.update_majorant(); + } + // Add particle's starting weight to count for normalizing tallies later if (!is_secondary) { #pragma omp atomic @@ -744,10 +763,13 @@ void initialize_data() // Determine minimum/maximum energy for incident neutron/photon data data::energy_max = {INFTY, INFTY, INFTY, INFTY}; data::energy_min = {0.0, 0.0, 0.0, 0.0}; + int neutron = ParticleType::neutron().transport_index(); + int photon = ParticleType::photon().transport_index(); + int electron = ParticleType::electron().transport_index(); + int positron = ParticleType::positron().transport_index(); for (const auto& nuc : data::nuclides) { if (nuc->grid_.size() >= 1) { - int neutron = ParticleType::neutron().transport_index(); data::energy_min[neutron] = std::max(data::energy_min[neutron], nuc->grid_[0].energy.front()); data::energy_max[neutron] = @@ -758,7 +780,6 @@ void initialize_data() if (settings::photon_transport) { for (const auto& elem : data::elements) { if (elem->energy_.size() >= 1) { - int photon = ParticleType::photon().transport_index(); int n = elem->energy_.size(); data::energy_min[photon] = std::max(data::energy_min[photon], std::exp(elem->energy_(1))); @@ -771,9 +792,6 @@ void initialize_data() // Determine if minimum/maximum energy for bremsstrahlung is greater/less // than the current minimum/maximum if (data::ttb_e_grid.size() >= 1) { - int photon = ParticleType::photon().transport_index(); - int electron = ParticleType::electron().transport_index(); - int positron = ParticleType::positron().transport_index(); int n_e = data::ttb_e_grid.size(); const std::vector charged = {electron, positron}; @@ -791,13 +809,16 @@ void initialize_data() } } + // set energy recipricals + data::energy_min_rcp[neutron] = 1.0 / data::energy_min[neutron]; + if (settings::photon_transport) data::energy_min_rcp[photon] = 1.0 / data::energy_min[photon]; + // Show which nuclide results in lowest energy for neutron transport for (const auto& nuc : data::nuclides) { // If a nuclide is present in a material that's not used in the model, its // grid has not been allocated if (nuc->grid_.size() > 0) { double max_E = nuc->grid_[0].energy.back(); - int neutron = ParticleType::neutron().transport_index(); if (max_E == data::energy_max[neutron]) { write_message(7, "Maximum neutron transport energy: {} eV for {}", data::energy_max[neutron], nuc->name_); @@ -814,10 +835,10 @@ void initialize_data() for (auto& nuc : data::nuclides) { nuc->init_grid(); } - int neutron = ParticleType::neutron().transport_index(); simulation::log_spacing = std::log(data::energy_max[neutron] / data::energy_min[neutron]) / settings::n_log_bins; + simulation::log_spacing_rcp = 1.0 / simulation::log_spacing; } #ifdef OPENMC_MPI @@ -890,6 +911,34 @@ void transport_history_based() } } +void transport_delta_tracking_single_particle(Particle& p) +{ + p.delta_tracking() = true; + p.event_calculate_xs(); + while (true) { + p.event_delta_advance(); + if (!p.alive()) + break; + p.event_calculate_xs(); + if (prn(p.current_seed()) < (p.macro_xs().total / p.majorant())) { + p.event_collide(); + } + p.event_check_limit_and_revive(); + if (!p.alive()) + break; + } + p.event_death(); +} + +void transport_delta_tracking() { + #pragma omp parallel for schedule(runtime) + for (int64_t i_work = 1; i_work <= simulation::work_per_rank; ++i_work) { + Particle p; + initialize_particle_track(p, i_work, false); + transport_delta_tracking_single_particle(p); + } +} + // The shared secondary bank transport algorithm works in two phases. In the // first phase, all primary particles are sampled then transported, and their // secondary particles are deposited into a shared secondary bank. The second diff --git a/src/surface.cpp b/src/surface.cpp index 81b756deae7..4ca5b7c807f 100644 --- a/src/surface.cpp +++ b/src/surface.cpp @@ -29,6 +29,7 @@ namespace openmc { namespace model { std::unordered_map surface_map; vector> surfaces; +vector boundary_surfaces; } // namespace model //============================================================================== diff --git a/tests/regression_tests/delta_tracking/__init__.py b/tests/regression_tests/delta_tracking/__init__.py new file mode 100644 index 00000000000..e69de29bb2d diff --git a/tests/regression_tests/delta_tracking/test.py b/tests/regression_tests/delta_tracking/test.py new file mode 100644 index 00000000000..f92f690e9dc --- /dev/null +++ b/tests/regression_tests/delta_tracking/test.py @@ -0,0 +1,53 @@ +import glob +import os + +import openmc +import pytest + +from tests.testing_harness import PyAPITestHarness + +def delta_tracking_model(nuclide): + model = openmc.model.Model() + + model.materials = openmc.Materials() + + # single material + mat = openmc.Material() + mat.add_nuclide(nuclide, 1.0) + mat.set_density('g/cc', 19.1) + mat.depletable = True + + model.materials.append(mat) + + # geometry (infinite cell medium) + cell = openmc.Cell() + cell.fill = mat + cell.temperature = 300 + + prism = openmc.model.rectangular_prism(1000.00, 1000.00, boundary_type='vacuum') + + cell.region = prism + + model.geometry = openmc.Geometry([cell,]) + + settings = model.settings + settings.run_mode = 'fixed source' + settings.particles = 100 + settings.batches = 10 + settings.inactive = 1 + settings.delta_tracking = True + + return model + +nuclides = ('Fe56', 'U238', 'H1', 'O16', 'Zr91', 'Zr90', 'Zr92', 'Zr94', 'Zr96', 'B10', 'B11') +@pytest.mark.parametrize('nuclide', nuclides) +def test_delta_tracking(nuclide): + model = delta_tracking_model(nuclide) + harness = PyAPITestHarness('statepoint.10.h5',model=model) + harness._build_inputs() + harness._run_openmc() + harness._cleanup() + output = glob.glob("*.txt") + for f in output: + if os.path.exists(f): + os.remove(f) From 0ce77aaa95141903ae212d94cd5021cfe731b875 Mon Sep 17 00:00:00 2001 From: nuclearkevin <66632997+nuclearkevin@users.noreply.github.com> Date: Mon, 18 May 2026 14:34:01 -0500 Subject: [PATCH 02/73] Fix non-vacuum BCs. --- src/particle.cpp | 29 ++++++++++++++++++----------- 1 file changed, 18 insertions(+), 11 deletions(-) diff --git a/src/particle.cpp b/src/particle.cpp index 5f25a2784cf..63a71e9f2a2 100644 --- a/src/particle.cpp +++ b/src/particle.cpp @@ -347,9 +347,6 @@ void Particle::event_delta_advance() distance = -std::log(prn(current_seed())) / majorant(); } - // Advance the particle (applying boundary conditions) until it either: - // i) Reaches a collision site; - // ii) Or leaks out of a vacuum boundary condition. while (distance >= 0 && alive()) { // update distance to problem boundary boundary().distance() = INFTY; @@ -367,21 +364,31 @@ void Particle::event_delta_advance() } } - // We collided before crossing a boundary surface. - // Need to advance the particle to the collision site. - if (distance < boundary().distance()) { - move_distance(distance); + if (distance < boundary().distance()) + { break; } - // Advance particle to the boundary surface. - move_distance(boundary().distance()); + // Advance particle to the boundary. + r() += (boundary().distance() - TINY_BIT) * u(); event_cross_surface(); + material() = C_NONE; distance -= boundary().distance(); } - // Ensure that cross sections will be re-calculated if - // the energy has changed. + // Advance particle to the true collision site. + for (int j = 0; j < n_coord(); ++j) { + coord(j).r() += distance * u(); + coord(j).reset(); + } + + // If the particle no longer exists in the geometry, it left through a vacuum BC. + if (!exhaustive_find_cell(*this)) { + keff_tally_leakage() += wgt(); + wgt() = 0.0; + } + + // Force re-calculation of material properties at the collision site. if (E() != E_last()) { material_last() = C_NONE; } From 5cb8cfb282c3910ed46e2a93b1f5c0c4264124a8 Mon Sep 17 00:00:00 2001 From: nuclearkevin <66632997+nuclearkevin@users.noreply.github.com> Date: Tue, 19 May 2026 12:47:24 -0500 Subject: [PATCH 03/73] Make non-vacuum BCs more robust. --- src/particle.cpp | 28 ++++++++++++++++++++++++---- 1 file changed, 24 insertions(+), 4 deletions(-) diff --git a/src/particle.cpp b/src/particle.cpp index 63a71e9f2a2..cf0217d4851 100644 --- a/src/particle.cpp +++ b/src/particle.cpp @@ -364,11 +364,24 @@ void Particle::event_delta_advance() } } - if (distance < boundary().distance()) + if (distance < (boundary().distance() - TINY_BIT)) { break; } + if (coord(n_coord() - 1).cell() == C_NONE && cell_last(n_coord() - 1) == C_NONE) { + // Try to find the particle. + for (int j = 0; j < n_coord(); ++j) { + coord(j).reset(); + } + if (!exhaustive_find_cell(*this)) { + // We've lost this particle. + mark_as_lost("Particle could not be located during the delta tracking loop!"); + wgt() = 0.0; + return; + } + } + // Advance particle to the boundary. r() += (boundary().distance() - TINY_BIT) * u(); event_cross_surface(); @@ -376,16 +389,23 @@ void Particle::event_delta_advance() distance -= boundary().distance(); } + // The particle leaked out of the boundary, no need to perform the subsequent steps. + if (!alive()) + return; + // Advance particle to the true collision site. + r() += distance * u(); for (int j = 0; j < n_coord(); ++j) { - coord(j).r() += distance * u(); + //coord(j).r() += distance * u(); coord(j).reset(); } - // If the particle no longer exists in the geometry, it left through a vacuum BC. + // Need to locate the particle at the collision site. if (!exhaustive_find_cell(*this)) { - keff_tally_leakage() += wgt(); + // We've lost this particle. + mark_as_lost("Particle could not be located at the delta tracking collision site!"); wgt() = 0.0; + return; } // Force re-calculation of material properties at the collision site. From 6320c5ab5f666fc5a83a3d8a2150985a826aae0f Mon Sep 17 00:00:00 2001 From: nuclearkevin <66632997+nuclearkevin@users.noreply.github.com> Date: Fri, 22 May 2026 11:05:59 -0500 Subject: [PATCH 04/73] Allow for the import of majorant cross sections from CSV files. For prototyping --- include/openmc/majorant.h | 2 ++ openmc/settings.py | 24 ++++++++++++++++++++---- src/majorant.cpp | 27 +++++++++++++++++++++++++++ src/particle.cpp | 2 +- 4 files changed, 50 insertions(+), 5 deletions(-) diff --git a/include/openmc/majorant.h b/include/openmc/majorant.h index d257eb89783..7eb111f37df 100644 --- a/include/openmc/majorant.h +++ b/include/openmc/majorant.h @@ -16,6 +16,7 @@ class Majorant; namespace data { extern std::vector> nuclide_majorants; extern std::unique_ptr n_majorant; + extern std::string majorant_file; } class Majorant { @@ -23,6 +24,7 @@ class Majorant { public: Majorant() = default; Majorant(const std::vector& energy, const std::vector& xs); + Majorant(const std::string & majorant_file); struct XS { XS(std::vector energies, diff --git a/openmc/settings.py b/openmc/settings.py index 9592ef106a2..9b7a058947f 100644 --- a/openmc/settings.py +++ b/openmc/settings.py @@ -241,7 +241,7 @@ class Settings: also tends to dampen the convergence rate of the solver, thus requiring more iterations to converge. :adjoint_source: - Source object used to define localized adjoint source/detector response + Source object used to define localized adjoint source/detector response function. .. versionadded:: 0.15.0 @@ -483,6 +483,7 @@ def __init__(self, **kwargs): self._material_cell_offsets = None self._log_grid_bins = None self._delta_tracking = None + self._delta_tracking_majorant_file = None self._event_based = None self._max_particles_in_flight = None self._max_particle_events = None @@ -1244,11 +1245,20 @@ def delayed_photon_scaling(self, value: bool): def delta_tracking(self): return self._delta_tracking + @property + def delta_tracking_majorant_file(self): + return self._delta_tracking_majorant_file + @delta_tracking.setter def delta_tracking(self, value): - cv.check_type('event_based', value, bool) + cv.check_type('delta_tracking', value, bool) self._delta_tracking = value + @delta_tracking_majorant_file.setter + def delta_tracking_majorant_file(self, value): + cv.check_type('delta_tracking_majorant_file', value, str) + self._delta_tracking_majorant_file = value + @property def material_cell_offsets(self) -> bool: return self._material_cell_offsets @@ -2015,6 +2025,11 @@ def _create_delta_tracking_subelement(self, root): if self._delta_tracking: elem = ET.SubElement(root, "delta_tracking") elem.text = str(self._delta_tracking).lower() + def _create_delta_tracking_maj_file_subelement(self, root): + if self._delta_tracking: + if self.delta_tracking_majorant_file != None: + elem = ET.SubElement(root, "delta_tracking_majorant_file") + elem.text = str(self._delta_tracking_majorant_file) def _create_random_ray_subelement(self, root, mesh_memo=None): if self._random_ray: @@ -2044,11 +2059,11 @@ def _create_random_ray_subelement(self, root, mesh_memo=None): path = f"./mesh[@id='{mesh.id}']" if root.find(path) is None: root.append(mesh.to_xml_element()) - if mesh_memo is not None: + if mesh_memo is not None: mesh_memo.add(mesh.id) elif key == 'adjoint_source': subelement = ET.SubElement(element, 'adjoint_source') - # Check that all entries are valid SourceBase instances, in case + # Check that all entries are valid SourceBase instances, in case # the random_ray setter was not used to populate dict entries. if not isinstance(value, MutableSequence): value = [value] @@ -2651,6 +2666,7 @@ def to_xml_element(self, mesh_memo=None): self._create_source_rejection_fraction_subelement(element) self._create_free_gas_threshold_subelement(element) self._create_delta_tracking_subelement(element) + self._create_delta_tracking_maj_file_subelement(element) # Clean the indentation in the file to be user-readable clean_indentation(element) diff --git a/src/majorant.cpp b/src/majorant.cpp index df28239295e..336f034a571 100644 --- a/src/majorant.cpp +++ b/src/majorant.cpp @@ -15,10 +15,20 @@ namespace openmc { namespace data { std::vector> nuclide_majorants; std::unique_ptr n_majorant; +std::string majorant_file; } void create_majorant() { write_message("Creating majorant cross section..."); + if (data::majorant_file != "") { + write_message("Loading majorant from " + data::majorant_file); + // We can load the majorant from a file instead. + data::n_majorant = std::make_unique(data::majorant_file); + data::n_majorant->grid_.init(); + + return; + } + // create a majorant XS for each nuclide for (const auto& nuclide : data::nuclides) { data::nuclide_majorants.push_back(std::make_unique()); @@ -180,6 +190,23 @@ Majorant::Majorant(const std::vector& energy, grid_.init(); } +Majorant::Majorant(const std::string & majorant_file) +{ + std::ifstream majorant_data(majorant_file); + + std::string line; + while (std::getline(majorant_data, line)) { + auto delim_pos = line.find(","); + auto energy = std::stod(line.substr(0, delim_pos)); + if (energy > data::energy_max[ParticleType::neutron().transport_index()]) { + break; + } + grid_.energy.push_back(energy); + xs_.push_back(std::stod(line.substr(delim_pos + 1))); + } + grid_.init(); +} + double Majorant::calculate_xs(double energy) const { diff --git a/src/particle.cpp b/src/particle.cpp index cf0217d4851..09230e1e152 100644 --- a/src/particle.cpp +++ b/src/particle.cpp @@ -937,7 +937,7 @@ void Particle::cross_periodic_bc( void Particle::update_majorant() { - this->majorant() = 1.000001 * data::n_majorant->calculate_xs(this->E()); + this->majorant() = 1.1 * data::n_majorant->calculate_xs(this->E()); } void Particle::mark_as_lost(const char* message) From 4ebbf570e801da2cae890863c7931ea6ec7e7ebb Mon Sep 17 00:00:00 2001 From: nuclearkevin <66632997+nuclearkevin@users.noreply.github.com> Date: Fri, 22 May 2026 14:00:26 -0500 Subject: [PATCH 05/73] Missing setting. --- src/settings.cpp | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/src/settings.cpp b/src/settings.cpp index 463bc53aa7b..90dde938136 100644 --- a/src/settings.cpp +++ b/src/settings.cpp @@ -20,6 +20,7 @@ #include "openmc/eigenvalue.h" #include "openmc/error.h" #include "openmc/file_utils.h" +#include "openmc/majorant.h" #include "openmc/mcpl_interface.h" #include "openmc/mesh.h" #include "openmc/message_passing.h" @@ -1235,6 +1236,10 @@ void read_settings_xml(pugi::xml_node root) delta_tracking = get_node_value_bool(root, "delta_tracking"); } + if (check_for_node(root, "delta_tracking_majorant_file")) { + data::majorant_file = get_node_value(root, "delta_tracking_majorant_file"); + } + // Check whether material cell offsets should be generated if (check_for_node(root, "material_cell_offsets")) { material_cell_offsets = get_node_value_bool(root, "material_cell_offsets"); From c716c85418d8aed6d48890aa926b39b207edc4fa Mon Sep 17 00:00:00 2001 From: nuclearkevin <66632997+nuclearkevin@users.noreply.github.com> Date: Sat, 23 May 2026 16:00:55 -0500 Subject: [PATCH 06/73] Turn ptable sampling on for delta tracking. --- src/nuclide.cpp | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/nuclide.cpp b/src/nuclide.cpp index fda2e0e2bbf..b45c7fb86f5 100644 --- a/src/nuclide.cpp +++ b/src/nuclide.cpp @@ -828,7 +828,7 @@ void Nuclide::calculate_xs( // If the particle is in the unresolved resonance range and there are // probability tables, we need to determine cross sections from the table - if (!p.delta_tracking() && settings::urr_ptables_on && urr_present_ && !use_mp) { + if (settings::urr_ptables_on && urr_present_ && !use_mp) { if (urr_data_[micro.index_temp].energy_in_bounds(p.E())) this->calculate_urr_xs(micro.index_temp, p); } From 79010cbb2390cb8e0840a1f13747c14ea76f385f Mon Sep 17 00:00:00 2001 From: nuclearkevin <66632997+nuclearkevin@users.noreply.github.com> Date: Tue, 26 May 2026 12:19:01 -0500 Subject: [PATCH 07/73] Begin majorant refactor. --- include/openmc/majorant.h | 95 ++----- src/majorant.cpp | 576 ++++++++++++++------------------------ 2 files changed, 236 insertions(+), 435 deletions(-) diff --git a/include/openmc/majorant.h b/include/openmc/majorant.h index 7eb111f37df..badefc45cb4 100644 --- a/include/openmc/majorant.h +++ b/include/openmc/majorant.h @@ -8,13 +8,13 @@ #include "openmc/settings.h" #include "openmc/nuclide.h" +#include "openmc/material.h" namespace openmc { class Majorant; namespace data { - extern std::vector> nuclide_majorants; extern std::unique_ptr n_majorant; extern std::string majorant_file; } @@ -22,88 +22,51 @@ namespace data { class Majorant { public: - Majorant() = default; + Majorant(); Majorant(const std::vector& energy, const std::vector& xs); Majorant(const std::string & majorant_file); - struct XS { - XS(std::vector energies, - std::vector total_xs) - : energies_(energies), total_xs_(total_xs), idx_(0) - { } - - //! \brief Return the current energy and total cross section values - std::pair get() const; - - //! \brief Return the energy of the current index - double get_e() const; - - //! \brief Return the cross section of the current index - double get_xs() const; - - //! \brief Return the current energy value and advance one - double pop_e(); - - //! \brief Advance the index until past the input energy value - void advance(double energy); - - //! \brief Indicate if all values in the cross section have been visited - bool complete() const; - - //! \brief Return the previous energy, cross section value pair - std::pair prev() const; + void write_ascii(const std::string& filename) const; - //! \brief Return the previous energy value - double prev_e() const; + //! \brief Calculate the microscopic cross section at a given energy + double calculate_xs(double energy) const; - //! \brief Return the previous cross section value - double prev_xs() const; + // data members + std::vector nuclides; // index of nuclides applied + std::vector xs_; // cross section values + Nuclide::EnergyGrid grid_; + constexpr static double safety_factor {1.01}; - //! \brief Increment the cross section index by i - inline - XS& operator +=(size_t i) { this->idx_ += i; return *this; } +private: + //! \brief Unionize the smooth and URR cross section grids for all nuclides in the problem. + void compute_unionized_grid(); - //! \brief Increment the cross section index by one - inline - XS& operator ++(int i) { this->idx_ += 1; return *this; } + //! \brief Compute the majorant cross section. + void setup_majorant(); - std::vector energies_; - std::vector total_xs_; - size_t idx_; - }; + //! \brief Compute a per-material majorant cross section. + void fill_material_maj_xs(const Material & mat, const std::vector & to_grid, std::vector & mat_maj); - //! \brief Determine the intersection of two line segments (p1, p2) and (p3, p4) - static bool intersect_2D(std::pair p1, - std::pair p2, - std::pair p3, - std::pair p4, - std::pair& intersection); + //! \brief Compute the maximum smooth cross section for a given energy point. + double calculate_max_smooth_xs(double energy, const Nuclide & nuc); - //! \brief Determine if point p3 is above or below the line segment (p1, p2) - static bool is_above(std::pair p1, - std::pair p2, - std::pair p3); + //! \brief Compute the maximum URR cross section for a given energy point. + double calculate_max_urr_xs(double energy, const Nuclide & nuc, double smooth); - public: - void write_ascii(const std::string& filename) const; + //! \brief Get the grid index for energy interpolation. + int get_i_grid(double energy, const Nuclide::EnergyGrid & grid); - //! \brief Update the majorant using values from another cross section - void update(std::vector energies_other, - std::vector xs_other); + //! \brief Helper function to perform linear-linear interpolation. + double interpolate_lin_1D(double E_0, double E_1, double xs_0, double xs_1, double E); - //! \brief Calculate the microscopic cross section at a given energy - double calculate_xs(double energy) const; + //! \brief Helper function to perform log-log interpolation. + double interpolate_log_1D(double E_0, double E_1, double xs_0, double xs_1, double E); - // data members - public: - std::vector nuclides; // index of nuclides applied - std::vector xs_; // cross section values - Nuclide::EnergyGrid grid_; - constexpr static double safety_factor {1.01}; + //! \brief Compute a union between vectors 'a' and 'b', storing the union in 'result' + void vector_union_1D(const std::vector & a, const std::vector & b, std::vector & result); }; // class Majorant void create_majorant(); - std::vector compute_majorant_energy_grid(); } #endif // OPENMC_MAJORANT_H diff --git a/src/majorant.cpp b/src/majorant.cpp index 336f034a571..f79c22c4630 100644 --- a/src/majorant.cpp +++ b/src/majorant.cpp @@ -18,171 +18,32 @@ std::unique_ptr n_majorant; std::string majorant_file; } -void create_majorant() { +void create_majorant() +{ write_message("Creating majorant cross section..."); if (data::majorant_file != "") { write_message("Loading majorant from " + data::majorant_file); // We can load the majorant from a file instead. data::n_majorant = std::make_unique(data::majorant_file); - data::n_majorant->grid_.init(); - return; } - // create a majorant XS for each nuclide - for (const auto& nuclide : data::nuclides) { - data::nuclide_majorants.push_back(std::make_unique()); - auto& majorant = data::nuclide_majorants.back(); - - for (int t = 0; t < nuclide->kTs_.size(); t++) { - auto total = nuclide->xs_[t].slice(openmc::tensor::all, 0); - std::vector xs(total.begin(), total.end()); - auto energies = nuclide->grid_[t].energy; - majorant->update(energies, xs); - - // include unresolved resonance region data - // in the majorant if present - if (nuclide->urr_present_) { - const auto& urr_data = nuclide->urr_data_[t]; - std::vector energies(urr_data.energy_.begin(), urr_data.energy_.end()); - - double max_urr_total {0.0}; - - for (auto xs_vals : urr_data.xs_values_) { - max_urr_total = std::max(max_urr_total, xs_vals.total); - } - - if(urr_data.interp_ == Interpolation::log_log) { - std::cout << fmt::format("Nuclide {} uses log-log interpolation", nuclide->name_) << std::endl; - } - - if (urr_data.multiply_smooth_) { - std::cout << "Multiplied URR: " << nuclide->name_ << std::endl; - majorant->grid_.init(); - std::vector xs_vals; - for (int i = 0; i < energies.size(); i++) { - xs_vals.push_back(majorant->calculate_xs(energies[i]) * max_urr_total); - } - majorant->update(energies, xs_vals); - } else { - std::vector xs_vals(energies.size(), max_urr_total); - majorant->update(energies, xs_vals); - } - // majorant->update_urr(energies, xs, urr_data.interp_); - } - } - // initialize the energy grid for this nuclide - majorant->grid_.init(); - // majorant->write_ascii(nuclide->name_ + "_majorant.txt"); - } - - auto majorant_e_grid = compute_majorant_energy_grid(); - - - std::vector xs_vals; - - std::vector macro_majorants; - - // compute a majorant for every material - for (auto& material : model::materials) { - std::vector material_xs; - for (auto e_val : majorant_e_grid) { - double xs_val = 0.0; - - // See if thermal data is present for any of the material nuclides - bool check_sab = material->thermal_tables_.size() > 0; - int thermal_table_idx = 0; - - int j = 0; - - for (int i = 0; i < material->nuclide_.size(); i++) { - int i_nuc = material->nuclide_[i]; - xs_val += material->atom_density_(i) * data::nuclide_majorants[i_nuc]->calculate_xs(e_val); - - int i_sab = C_NONE; - double sab_frac = 0.0; - - if (check_sab) { - const auto& sab {material->thermal_tables_[j]}; - if (i == sab.index_nuclide) { - // Get index in the sab_tables - i_sab = sab.index_table; - sab_frac = sab.fraction; - - ++j; - if (j == material->thermal_tables_.size()) check_sab = false; - } - } - - // compute the max sab xs if present - if (i_sab >= 0) { - const auto& tdata = data::thermal_scatt[i_sab]; - // compute the maximum xs value over all temperatures - double thermal_xs = 0.0; - for (int k = 0; k < tdata->kTs_.size(); k++) { - // compute the thermal xs at the temperature and energy - double elastic, inelastic; - tdata->data_[k].calculate_xs(e_val, &elastic, &inelastic); - if (elastic + inelastic > thermal_xs) { - thermal_xs = elastic + inelastic; - } - } - // adjust the current xs_val for the thermal component - xs_val += sab_frac * thermal_xs; - } - - } - - material_xs.push_back(xs_val); - } - macro_majorants.emplace_back(Majorant()); - macro_majorants.back().update(majorant_e_grid, material_xs); - macro_majorants.back().write_ascii(fmt::format("mat_{}_majorant.txt", material->id_)); - } - data::n_majorant = std::make_unique(); + fatal_error("The rest has not been implemented!"); - for (auto& macro_majorant : macro_majorants) { - data::n_majorant->update(macro_majorant.grid_.energy, macro_majorant.xs_); - } - - data::n_majorant->grid_.init(); - data::n_majorant->write_ascii("macro_majorant.txt"); + // data::n_majorant->grid_.init(); + // data::n_majorant->write_ascii("macro_majorant.txt"); } -std::vector -compute_majorant_energy_grid() { - - std::vector common_e_grid; - for (const auto& nuc_maj : data::nuclide_majorants) { - auto& e_grid = nuc_maj->grid_.energy; - // append new points to the current group of points - common_e_grid.insert(common_e_grid.end(), e_grid.begin(), e_grid.end()); - - // remove duplicates - std::unique(common_e_grid.begin(), common_e_grid.end()); - } - std::sort(common_e_grid.begin(), common_e_grid.end()); +Majorant::Majorant() +{ + // Unionize the grid. + compute_unionized_grid(); - // remove all values below the minimum neutron energy - int neutron = ParticleType::neutron().transport_index(); - auto min_it = common_e_grid.begin(); - while (*min_it < data::energy_min[neutron]) { min_it++; } - common_e_grid.erase(common_e_grid.begin(), min_it + 1); - // insert the minimum neutron energy at the beginning - common_e_grid.insert(common_e_grid.begin(), data::energy_min[neutron]); - - // remove all values above the maximum neutron energy - auto max_it = --common_e_grid.end(); - while (*max_it > data::energy_max[neutron]) { max_it--; } - common_e_grid.erase(max_it - 1, common_e_grid.end()); - // insert the maximum neutron energy at the end - common_e_grid.insert(common_e_grid.end(), data::energy_max[neutron]); - - return common_e_grid; + // Setup the majorant given the new grid. + setup_majorant(); } - Majorant::Majorant(const std::vector& energy, const std::vector& xs) : xs_(xs) { @@ -192,18 +53,23 @@ Majorant::Majorant(const std::vector& energy, Majorant::Majorant(const std::string & majorant_file) { + const auto neutron_idx = ParticleType::neutron().transport_index(); std::ifstream majorant_data(majorant_file); std::string line; while (std::getline(majorant_data, line)) { auto delim_pos = line.find(","); auto energy = std::stod(line.substr(0, delim_pos)); - if (energy > data::energy_max[ParticleType::neutron().transport_index()]) { + if (energy < data::energy_min[neutron_idx]) { + continue; + } + if (energy > data::energy_max[neutron_idx]) { break; } grid_.energy.push_back(energy); xs_.push_back(std::stod(line.substr(delim_pos + 1))); } + grid_.init(); } @@ -238,265 +104,237 @@ Majorant::calculate_xs(double energy) const double xs = (1.0 - f) * xs_[i_grid] + f * xs_[i_grid + 1]; - return 1.0 * xs; + return xs; } -bool Majorant::intersect_2D(std::pair p1, - std::pair p2, - std::pair p3, - std::pair p4, - std::pair& intersection) { - - double denominator = (p4.first - p3.first) * (p1.second - p2.second) - - (p1.first - p2.first) * (p4.second - p3.second); - - // if the lines are parallel, return no intersection - if (fabs(denominator) <= FP_PRECISION) { return false; } - - double numerator = (p3.second - p4.second) * (p1.first - p3.first) + - (p4.first - p3.first) * (p1.second - p3.second); - - double t = numerator / denominator; - - if (t < 0.0 || t > 1.0) { return false; } - - // compute intersection location - double x = p1.first + (p2.first - p1.first) * t; - double slope = (p2.second - p1.second) / (p2.first - p1.first); - double y = p1.second + (x - p1.first) * slope; - intersection = {x, y}; - return true; -} - -bool Majorant::is_above(std::pair p1, - std::pair p2, - std::pair p3) { - // if the line is vertical, use - // comparison of x values - if (fabs(p2.first - p1.first) < FP_PRECISION) { - return p3.first < p2.first; - } else { - double slope = (p2.second - p1.second) / (p2.first - p1.first); - double val = p1.second + slope * (p3.first - p1.first); - return val < p3.second; - } - -} - -void Majorant::write_ascii(const std::string& filename) const { - +void Majorant::write_ascii(const std::string& filename) const +{ std::ofstream of(filename); - for (int i = 0; i < xs_.size(); i++) { of << grid_.energy[i] << "\t" << xs_[i] << "\n"; } - of.close(); } -void Majorant::update(std::vector energy_other, - std::vector xs_other) { +void +Majorant::compute_unionized_grid() +{ + write_message("Unionizing nuclide cross section grids."); + + // This function generates a unionized cross section grid between smooth cross + // sections and URR probability table grids. + std::vector grid_copy; + for (const auto & mat : model::materials) { + if (mat->ncrystal_mat_) { + fatal_error("Delta tracking is not supported when using NCrystal!"); + } - XS xs_a(grid_.energy, xs_); - XS xs_b(energy_other, xs_other); + for (auto nuclide_idx : mat->nuclide_) { + const auto & nuclide = data::nuclides[nuclide_idx]; - // early exit checks - if (xs_b.complete()) { return; } + // ====================================================================== + // Unionizing the URR temperature grid. Loop over temperature points. + if (nuclide->urr_present_ && settings::urr_ptables_on) { + for (const auto & nuc_urr : nuclide->urr_data_) { + grid_copy = grid_.energy; + vector_union_1D(grid_copy, nuc_urr.energy_, grid_.energy); + } + } - if (xs_a.complete()) { - grid_.energy = energy_other; - xs_ = xs_other; - return; + // ====================================================================== + // Unionize the smooth cross section grid. Loop over temperature points. + for (const auto & n_grid : nuclide->grid_) { + grid_copy = grid_.energy; + vector_union_1D(grid_copy, n_grid.energy, grid_.energy); + } + } } + grid_.init(); +} - // resulting output of this algorithm - std::vector e_out, xs_out; - std::vector mask; +void +Majorant::setup_majorant() +{ + // Fill with zeros. + xs_.resize(grid_.energy.size(), 0.0); - // references we'll use to keep track of which - // is currently considered the larger cross section - XS& current_xs = xs_a; - XS& other_xs = xs_b; + std::vector material_maj; + for (const auto & mat : model::materials) { + write_message("Computing majorant total cross section for " + mat->name() + "."); + material_maj.clear(); + material_maj.resize(grid_.energy.size(), 0.0); - // if the other cross section starts at a lower energy, its - // value is considered to be higher - if (other_xs.get_e() < current_xs.get_e()) { - std::swap(current_xs, other_xs); + // Populate the per-material majorant cross section. + fill_material_maj_xs((*mat.get()), grid_.energy, material_maj); } +} + +void +Majorant::fill_material_maj_xs(const Material & mat, const std::vector & to_grid, std::vector & mat_maj) +{ + bool check_sab = (mat.thermal_tables_.size() > 0); - // if the two cross sections start at the same energy - // pick the one with the higher xs value - if (other_xs.get_e() == current_xs.get_e() && other_xs.get_xs() > current_xs.get_xs()) { - std::swap(current_xs, other_xs); + for (int e_idx = 0; e_idx < to_grid.size(); ++e_idx) { + for (auto nuclide_idx : mat.nuclide_) { + const auto & nuclide = data::nuclides[nuclide_idx]; + // ====================================================================== + // Start with the smooth cross section. + double micro_smooth_xs = calculate_max_smooth_xs(to_grid[e_idx], (*nuclide.get())); + + // ====================================================================== + // Compute the URR cross section. + double urr_xs = calculate_max_urr_xs(to_grid[e_idx], (*nuclide.get()), micro_smooth_xs); + + // ====================================================================== + // Compute the S(a,b) cross section. + } } +} - // add the first point to the final cross section - e_out.push_back(current_xs.get_e()); - xs_out.push_back(current_xs.get_xs()); - current_xs++; - - // continue adding points until the other xs min - // energy is lower than the output minimum energy - while(current_xs.get_e() < other_xs.get_e() && !current_xs.complete()) { - e_out.push_back(current_xs.get_e()); - xs_out.push_back(current_xs.get_xs()); - current_xs++; +double +Majorant::calculate_max_smooth_xs(double energy, const Nuclide & nuc) +{ + double max_smooth_xs = 0.0; + for (int i = 0; i < nuc.kTs_.size(); ++i) { + int i_grid = get_i_grid(energy, nuc.grid_[i]); + double xs = interpolate_lin_1D(grid_.energy[i_grid], grid_.energy[i_grid + 1], xs_[i_grid], xs_[i_grid + 1], energy); + max_smooth_xs = std::max(max_smooth_xs, xs); } - // Corner case: first point of the other xs is higher and nearer - // There is no intersection to find b/c there is no previous point to - // use for interpolation. Here, we'll insert a point at that energy - // in the current cross section, insert the point of the other cross - // section, and swap the two. - // NOTE: possible problem with computing intersections with vertical slopes here - if (current_xs.get_e() <= other_xs.get_e() && is_above({e_out.back(), xs_out.back()}, current_xs.get(), other_xs.get())) { - // insert point on current xs segment - double slope = (current_xs.get_xs() - xs_out.back()) / - (current_xs.get_e() - e_out.back()); - double xs_val = xs_out.back() + slope * (other_xs.get_e() - e_out.back()); - // insert point from other cross section - e_out.push_back(other_xs.get_e()); - xs_out.push_back(other_xs.get_xs()); - // swap the cross sections and advance - std::swap(current_xs, other_xs); - current_xs.advance(e_out.back()); - other_xs.advance(e_out.back()); + return max_smooth_xs; +} + +double +Majorant::calculate_max_urr_xs(double energy, const Nuclide & nuc, double smooth) +{ + if (!nuc.urr_present_) { + return 0.0; } - // the next point added to the cross section is selected based 4 different cases - // - // above: the other cross section point above the line between the last point added and the current xs's next point - // below: ther other cross section point is below the line betwee the last point added and the current xs's next point - // nearer: the other cross section point is closer in energy to the last point added - // farther: the other cross section point is farther in energy from the last point added - // - // Case 1: other xs value is above and nearer - // - // Check for an intersection with the line segment between last_pnt and current_next. - // If an intersection exists, insert a point in the majorant at that location and - // swap the current and other cross sections. - // - // Case 2: other xs value is above and farther - // - // Check for an intersection with the line segment between last_pnt and current_next. - // Apply the same treatment as Case 1. - // - // Case 3: other xs value is below and nearer - // - // Insert a point at the energy of the other xs value on the line segment between - // last_pnt and current_next. This point is superfluous and purely for the algorithm - // robustness, so the index of this point is added to the mask of indices to be removed - // later. - // - // Case 4: other xs value is below and farther - // - // Insert the location of current_next to the output cross section and continue - - // start looping over values - while (true) { - - if (current_xs.complete() || other_xs.complete()) { break; } - - auto current_next = current_xs.get(); - auto other_next = other_xs.get(); - std::pair last_pnt = {e_out.back(), xs_out.back()}; - - bool above = is_above(last_pnt, current_next, other_next); - bool nearer = other_next.first < current_next.first; - - // for clarity - bool below = !above; - bool farther = !nearer; - - // Cases 1 & 2 - if (above) { - // setup intersection check - auto p1 = last_pnt; - auto p2 = current_next; - auto p3 = other_xs.prev(); - auto p4 = other_next; - - std::pair intersection; - auto intersection_found = intersect_2D(p1, p2, p3, p4, intersection); - - if (intersection_found) { - e_out.push_back(intersection.first); - xs_out.push_back(intersection.second); - std::swap(current_xs, other_xs); - } else { - e_out.push_back(current_next.first); - xs_out.push_back(current_next.second); + double max_urr_xs = 0.0; + for (const auto & urr : nuc.urr_data_) { + if (!urr.energy_in_bounds(energy)) { + continue; + } + // Linear search to find the left and right interpolation points. + // TODO: if this is a problem we can perform binary search in energy + int i_energy; + for (int i = 0; i < urr.energy_.size() - 1; ++i) { + if (urr.energy_[i] <= energy && energy < urr.energy_[i + 1]) { + i_energy = i; + break; } - // Case 3 - } else if (below and nearer) { - // compute the y value - double slope = (current_next.second - last_pnt.second) / - (current_next.first - last_pnt.first); - double xs_val = last_pnt.second + slope * (other_next.first - last_pnt.first); - e_out.push_back(other_next.first); - xs_out.push_back(xs_val); - // add index of this point to the mask - mask.push_back(e_out.size() - 1); - // Case 4 - } else if (below and farther) { - e_out.push_back(current_next.first); - xs_out.push_back(current_next.second); } - current_xs.advance(e_out.back()); - other_xs.advance(e_out.back()); - } + // Find the maximum URR cross sections for the two bounding energy points. + double max_urr_xs_E0 = 0.0; + double max_urr_xs_E1 = 0.0; + for (int i_cdf = 0; i_cdf < urr.n_cdf(); ++i_cdf) { + max_urr_xs_E0 = std::max(max_urr_xs_E0, urr.xs_values_(i_energy, i_cdf).total); + max_urr_xs_E1 = std::max(max_urr_xs_E1, urr.xs_values_(i_energy + 1, i_cdf).total); + } - // remove any superfluous points - std::sort(mask.begin(), mask.end(), std::greater()); - for (auto idx : mask) { - e_out.erase(e_out.begin() + idx); - xs_out.erase(xs_out.begin() + idx); - } + // Interpolate the bounding energy points. + double interp_urr_xs = 0.0; + if (urr.interp_ == Interpolation::lin_lin) { + interp_urr_xs = + interpolate_lin_1D(urr.energy_[i_energy], urr.energy_[i_energy + 1], max_urr_xs_E0, max_urr_xs_E1, energy); + } else if (urr.interp_ == Interpolation::log_log) { + interp_urr_xs = + interpolate_log_1D(urr.energy_[i_energy], urr.energy_[i_energy + 1], max_urr_xs_E0, max_urr_xs_E1, energy); + } - // add any additional data to the output xs - while (!xs_a.complete()) { - e_out.push_back(xs_a.get_e()); - xs_out.push_back(xs_a.get_xs()); - xs_a++; - } + // Multiply by the smooth cross section (after interpolation) if required. + if (urr.multiply_smooth_) { + interp_urr_xs *= smooth; + } - while (!xs_b.complete()) { - e_out.push_back(xs_b.get_e()); - xs_out.push_back(xs_b.get_xs()); - xs_b++; + // Take the maximum over temperature. + max_urr_xs = std::max(max_urr_xs, interp_urr_xs); } - // increase all xs values in the majorant by 5 percent - std::transform(xs_out.begin(), xs_out.end(), xs_out.begin(), [](double xs_val) { return xs_val * 1.05; }); - - // update the values of the majorant - grid_.energy = std::move(e_out); - xs_ = std::move(xs_out); - + return max_urr_xs; } -// Majorant XS definitions - -std::pair -Majorant::XS::get() const { return {energies_.at(idx_), total_xs_.at(idx_)}; } +int +Majorant::get_i_grid(double energy, const Nuclide::EnergyGrid & grid) +{ + // Find energy index on energy grid + int neutron = ParticleType::neutron().transport_index(); + int i_log_union = std::log(energy * data::energy_min_rcp[neutron]) * simulation::log_spacing_rcp; -double Majorant::XS::get_e() const { return energies_.at(idx_); } + int i_grid; + if (i_log_union < 0) { + i_grid = 0; + } else if (i_log_union >= (grid.grid_index.size() - 2)) { + i_grid = grid.energy.size() - 2; + } else { + // Determine bounding indices based on which equal log-spaced + // interval the energy is in + int i_low = grid.grid_index[i_log_union]; + int i_high = grid.grid_index[i_log_union + 1] + 1; -double Majorant::XS::get_xs() const { return total_xs_.at(idx_); } + // Perform binary search over reduced range + i_grid = i_low + lower_bound_index(&grid.energy[i_low], &grid.energy[i_high], energy); + } -std::pair -Majorant::XS::prev() const { return {energies_.at(idx_ - 1), total_xs_.at(idx_ - 1)}; } + // check for rare case where two energy points are the same + if (grid.energy[i_grid] == grid.energy[i_grid + 1]) ++i_grid; -double Majorant::XS::prev_e() const { return energies_.at(idx_ - 1); } + return i_grid; +} -double Majorant::XS::prev_xs() const { return total_xs_.at(idx_ - 1); } +double +Majorant::interpolate_lin_1D(double E_0, double E_1, double xs_0, double xs_1, double E) +{ + double f = (E - E_0) / (E_1 - E_0); + return (1.0 - f) * xs_0 + f * xs_1; +} -void Majorant::XS::advance(double energy) { - double e = energies_[idx_]; - while (e <= energy && !this->complete()) { e = energies_[++idx_]; } +double interpolate_log_1D(double E_0, double E_1, double xs_0, double xs_1, double E) +{ + double f = std::log(E / E_0) / std::log(E_1 / E_0); + return std::exp((1.0 - f) * std::log(xs_0) + f * std::log(xs_1)); } -bool Majorant::XS::complete() const { return idx_ >= energies_.size(); } +void +Majorant::vector_union_1D(const std::vector & a, const std::vector & b, std::vector & result) +{ + int i = 0; + int j = 0; + while (i < a.size() && j < b.size()) { + if (a[i] < b[j]) { + if (result.empty() || result.back() != a[i]) { + result.push_back(a[i]); + } + i++; + } else if (b[j] < a[i]) { + if (result.empty() || result.back() != b[j]) { + result.push_back(b[j]); + } + j++; + } else { + if (result.empty() || result.back() != a[i]) { + result.push_back(a[i]); + } + i++; + j++; + } + } + + while (i < a.size()) { + if (result.empty() || result.back() != a[i]) { + result.push_back(a[i]); + } + i++; + } + + while (j < b.size()) { + if (result.empty() || result.back() != b[j]) { + result.push_back(b[j]); + } + j++; + } } +} // namespace openmc From 89ac45bd19d214d7c3bf757c28625f28b25551f0 Mon Sep 17 00:00:00 2001 From: nuclearkevin <66632997+nuclearkevin@users.noreply.github.com> Date: Tue, 26 May 2026 21:55:05 -0500 Subject: [PATCH 08/73] Finish the "new" majorant implementation. --- include/openmc/majorant.h | 8 +- src/majorant.cpp | 216 ++++++++++++++++++++++++++------------ 2 files changed, 153 insertions(+), 71 deletions(-) diff --git a/include/openmc/majorant.h b/include/openmc/majorant.h index badefc45cb4..f5a20cdd6b4 100644 --- a/include/openmc/majorant.h +++ b/include/openmc/majorant.h @@ -53,6 +53,11 @@ class Majorant { //! \brief Compute the maximum URR cross section for a given energy point. double calculate_max_urr_xs(double energy, const Nuclide & nuc, double smooth); + //! \brief Compute the maximum correction factor for the S(a,b) total cross section. + double calculate_max_sab_tot_xs(double energy, int i_sab, double sab_frac, const Nuclide & nuc); + + //!\brief Compute the max free gas elastic scattering cross section. + //! \brief Get the grid index for energy interpolation. int get_i_grid(double energy, const Nuclide::EnergyGrid & grid); @@ -61,9 +66,6 @@ class Majorant { //! \brief Helper function to perform log-log interpolation. double interpolate_log_1D(double E_0, double E_1, double xs_0, double xs_1, double E); - - //! \brief Compute a union between vectors 'a' and 'b', storing the union in 'result' - void vector_union_1D(const std::vector & a, const std::vector & b, std::vector & result); }; // class Majorant void create_majorant(); diff --git a/src/majorant.cpp b/src/majorant.cpp index f79c22c4630..21c27897abe 100644 --- a/src/majorant.cpp +++ b/src/majorant.cpp @@ -13,7 +13,6 @@ namespace openmc { namespace data { -std::vector> nuclide_majorants; std::unique_ptr n_majorant; std::string majorant_file; } @@ -29,7 +28,7 @@ void create_majorant() } data::n_majorant = std::make_unique(); - fatal_error("The rest has not been implemented!"); + data::n_majorant->write_ascii("macro_majorant.txt"); // data::n_majorant->grid_.init(); // data::n_majorant->write_ascii("macro_majorant.txt"); @@ -123,7 +122,6 @@ Majorant::compute_unionized_grid() // This function generates a unionized cross section grid between smooth cross // sections and URR probability table grids. - std::vector grid_copy; for (const auto & mat : model::materials) { if (mat->ncrystal_mat_) { fatal_error("Delta tracking is not supported when using NCrystal!"); @@ -136,19 +134,35 @@ Majorant::compute_unionized_grid() // Unionizing the URR temperature grid. Loop over temperature points. if (nuclide->urr_present_ && settings::urr_ptables_on) { for (const auto & nuc_urr : nuclide->urr_data_) { - grid_copy = grid_.energy; - vector_union_1D(grid_copy, nuc_urr.energy_, grid_.energy); + grid_.energy.insert(grid_.energy.end(), nuc_urr.energy_.begin(), nuc_urr.energy_.end()); } } // ====================================================================== // Unionize the smooth cross section grid. Loop over temperature points. for (const auto & n_grid : nuclide->grid_) { - grid_copy = grid_.energy; - vector_union_1D(grid_copy, n_grid.energy, grid_.energy); + grid_.energy.insert(grid_.energy.end(), n_grid.energy.begin(),n_grid.energy.end()); } } } + std::sort(grid_.energy.begin(), grid_.energy.end()); + std::unique(grid_.energy.begin(), grid_.energy.end()); + + // remove all values below the minimum neutron energy + int neutron = ParticleType::neutron().transport_index(); + auto min_it = grid_.energy.begin(); + while (*min_it < data::energy_min[neutron]) { min_it++; } + grid_.energy.erase(grid_.energy.begin(), min_it + 1); + // insert the minimum neutron energy at the beginning + grid_.energy.insert(grid_.energy.begin(), data::energy_min[neutron]); + + // remove all values above the maximum neutron energy + auto max_it = --grid_.energy.end(); + while (*max_it > data::energy_max[neutron]) { max_it--; } + grid_.energy.erase(max_it - 1, grid_.energy.end()); + // insert the maximum neutron energy at the end + grid_.energy.insert(grid_.energy.end(), data::energy_max[neutron]); + grid_.init(); } @@ -158,35 +172,83 @@ Majorant::setup_majorant() // Fill with zeros. xs_.resize(grid_.energy.size(), 0.0); - std::vector material_maj; + std::vector material_maj_xs; + material_maj_xs.resize(grid_.energy.size(), 0.0); for (const auto & mat : model::materials) { write_message("Computing majorant total cross section for " + mat->name() + "."); - material_maj.clear(); - material_maj.resize(grid_.energy.size(), 0.0); // Populate the per-material majorant cross section. - fill_material_maj_xs((*mat.get()), grid_.energy, material_maj); + fill_material_maj_xs((*mat.get()), grid_.energy, material_maj_xs); + + // Compute the full majorant by taking the max over each material cross section. + for (int i_energy = 0; i_energy < xs_.size(); ++i_energy) { + xs_[i_energy] = std::max(xs_[i_energy], material_maj_xs[i_energy]); + } + std::fill(material_maj_xs.begin(), material_maj_xs.end(), 0.0); } } void Majorant::fill_material_maj_xs(const Material & mat, const std::vector & to_grid, std::vector & mat_maj) { - bool check_sab = (mat.thermal_tables_.size() > 0); + for (int i_energy = 0; i_energy < to_grid.size(); ++i_energy) { + const double union_energy = to_grid[i_energy]; + int mat_sab_table_idx = 0; + bool check_sab = (mat.thermal_tables_.size() > 0); + for (int i = 0; i < mat.nuclide_.size(); ++i) { + const auto & nuclide = data::nuclides[mat.nuclide_[i]]; + // ====================================================================== + // CHECK FOR S(A,B) TABLE + int i_sab = C_NONE; + double sab_frac = 0.0; + + // Check if this nuclide matches one of the S(a,b) tables specified. + // This relies on thermal_tables_ being sorted by .index_nuclide + if (check_sab) { + const auto& sab {mat.thermal_tables_[mat_sab_table_idx]}; + if (i == sab.index_nuclide) { + // Get index in sab_tables + i_sab = sab.index_table; + sab_frac = sab.fraction; + + // If particle energy is greater than the highest energy for the + // S(a,b) table, then don't use the S(a,b) table + if (union_energy > data::thermal_scatt[i_sab]->energy_max_) { + i_sab = C_NONE; + } + + // Increment position in thermal_tables_ + ++mat_sab_table_idx; + + // Don't check for S(a,b) tables if there are no more left + if (mat_sab_table_idx == mat.thermal_tables_.size()) { + check_sab = false; + } + } + } - for (int e_idx = 0; e_idx < to_grid.size(); ++e_idx) { - for (auto nuclide_idx : mat.nuclide_) { - const auto & nuclide = data::nuclides[nuclide_idx]; // ====================================================================== - // Start with the smooth cross section. - double micro_smooth_xs = calculate_max_smooth_xs(to_grid[e_idx], (*nuclide.get())); + // Compute the maximum smooth total cross section. This is either the + // free gas cross section at energies larger than the Bragg edge, or + // the bound cross section in the thermal scattering region. + double micro_smooth_tot_xs = 0.0; + if (i_sab >= 0) { + // Thermal scattering cross sections using S(a,b) tables. + micro_smooth_tot_xs = calculate_max_sab_tot_xs(union_energy, i_sab, sab_frac, (*nuclide.get())); + } else { + // Free gas smooth cross section + micro_smooth_tot_xs = calculate_max_smooth_xs(union_energy, (*nuclide.get())); + } // ====================================================================== - // Compute the URR cross section. - double urr_xs = calculate_max_urr_xs(to_grid[e_idx], (*nuclide.get()), micro_smooth_xs); + // Compute the URR cross section. This shouldn't intersect with the + // S(a,b) cross section. + double micro_urr_xs = calculate_max_urr_xs(union_energy, (*nuclide.get()), micro_smooth_tot_xs); // ====================================================================== - // Compute the S(a,b) cross section. + // Accumulate the macroscopic cross section. + // TODO: density multipliers for per-cell material densities. + mat_maj[i_energy] += std::max(micro_smooth_tot_xs, micro_urr_xs) * mat.atom_density(i); } } } @@ -194,14 +256,16 @@ Majorant::fill_material_maj_xs(const Material & mat, const std::vector & double Majorant::calculate_max_smooth_xs(double energy, const Nuclide & nuc) { - double max_smooth_xs = 0.0; - for (int i = 0; i < nuc.kTs_.size(); ++i) { - int i_grid = get_i_grid(energy, nuc.grid_[i]); - double xs = interpolate_lin_1D(grid_.energy[i_grid], grid_.energy[i_grid + 1], xs_[i_grid], xs_[i_grid + 1], energy); - max_smooth_xs = std::max(max_smooth_xs, xs); + double max_smooth_tot_xs = 0.0; + for (int i_temp = 0; i_temp < nuc.kTs_.size(); ++i_temp) { + const auto & nuc_grid = nuc.grid_[i_temp]; + int i_grid = get_i_grid(energy, nuc_grid); + auto total = nuc.xs_[i_temp].slice(openmc::tensor::all, 0); + double xs = interpolate_lin_1D(nuc_grid.energy[i_grid], nuc_grid.energy[i_grid + 1], total[i_grid], total[i_grid + 1], energy); + max_smooth_tot_xs = std::max(max_smooth_tot_xs, xs); } - return max_smooth_xs; + return max_smooth_tot_xs; } double @@ -256,6 +320,62 @@ Majorant::calculate_max_urr_xs(double energy, const Nuclide & nuc, double smooth return max_urr_xs; } +double +Majorant::calculate_max_sab_tot_xs(double energy, int i_sab, double sab_frac, const Nuclide & nuc) +{ + const auto & thermal = data::thermal_scatt[i_sab]; + + // Loop over the nuclide's temperature grid to ensure we're consistent. + double max_sab_total = 0.0; + for (int i_nuc_temp = 0; i_nuc_temp < nuc.kTs_.size(); ++i_nuc_temp) { + double nuc_kT = nuc.kTs_[i_nuc_temp] * nuc.kTs_[i_nuc_temp]; + + // Compute the elastic and inelastic scattering cross sections. The S(a,b) + // cross sections are interpolated to match the nuclide temperature point. + double thermal_elastic; + double thermal_inelastic; + const auto & tkTs = thermal->kTs_; + if (tkTs.size() > 1) { + if (nuc_kT < tkTs.front()) { + thermal->data_.front().calculate_xs(energy, &thermal_elastic, &thermal_inelastic); + } else if (nuc_kT > tkTs.back()) { + thermal->data_.back().calculate_xs(energy, &thermal_elastic, &thermal_inelastic); + } else { + // Find temperatures that bound the actual temperature + int i_sab_temp = 0; + while (tkTs[i_sab_temp + 1] < nuc_kT && i_sab_temp + 1 < tkTs.size() - 1) { + ++i_sab_temp; + } + // Interpolate the scattering cross sections to the nuclide temperature grid point. + double T0_elastic; + double T1_elastic; + double T0_inelastic; + double T1_inelastic; + thermal->data_[i_sab_temp].calculate_xs(energy, &T0_elastic, &T0_inelastic); + thermal->data_[i_sab_temp + 1].calculate_xs(energy, &T1_elastic, &T1_inelastic); + thermal_elastic = interpolate_lin_1D(tkTs[i_sab_temp], tkTs[i_sab_temp + 1], T0_elastic, T1_elastic, nuc_kT); + thermal_inelastic = interpolate_lin_1D(tkTs[i_sab_temp], tkTs[i_sab_temp + 1], T0_inelastic, T1_inelastic, nuc_kT); + } + } else { + thermal->data_[0].calculate_xs(energy, &thermal_elastic, &thermal_inelastic); + } + + // Compute the free gas total and elastic cross sections interpolated on the majorant grid. + const auto & nuc_grid = nuc.grid_[i_nuc_temp]; + int i_grid = get_i_grid(energy, nuc_grid); + const auto & free_tot = nuc.xs_[i_nuc_temp].slice(openmc::tensor::all, 0); + const auto & free_ela = nuc.reactions_[0]->xs_[i_nuc_temp].value; + double tot_xs = interpolate_lin_1D(nuc_grid.energy[i_grid], nuc_grid.energy[i_grid + 1], free_tot[i_grid], free_tot[i_grid + 1], energy); + double ela_xs = interpolate_lin_1D(nuc_grid.energy[i_grid], nuc_grid.energy[i_grid + 1], free_ela[i_grid], free_ela[i_grid + 1], energy); + + double thermal_xs = sab_frac * (thermal_elastic + thermal_inelastic); + double sab_corrected_total = tot_xs + thermal_xs - sab_frac * ela_xs; + max_sab_total = std::max(sab_corrected_total, max_sab_total); + } + + return max_sab_total; +} + int Majorant::get_i_grid(double energy, const Nuclide::EnergyGrid & grid) { @@ -291,50 +411,10 @@ Majorant::interpolate_lin_1D(double E_0, double E_1, double xs_0, double xs_1, d return (1.0 - f) * xs_0 + f * xs_1; } -double interpolate_log_1D(double E_0, double E_1, double xs_0, double xs_1, double E) +double +Majorant::interpolate_log_1D(double E_0, double E_1, double xs_0, double xs_1, double E) { double f = std::log(E / E_0) / std::log(E_1 / E_0); return std::exp((1.0 - f) * std::log(xs_0) + f * std::log(xs_1)); } - -void -Majorant::vector_union_1D(const std::vector & a, const std::vector & b, std::vector & result) -{ - int i = 0; - int j = 0; - - while (i < a.size() && j < b.size()) { - if (a[i] < b[j]) { - if (result.empty() || result.back() != a[i]) { - result.push_back(a[i]); - } - i++; - } else if (b[j] < a[i]) { - if (result.empty() || result.back() != b[j]) { - result.push_back(b[j]); - } - j++; - } else { - if (result.empty() || result.back() != a[i]) { - result.push_back(a[i]); - } - i++; - j++; - } - } - - while (i < a.size()) { - if (result.empty() || result.back() != a[i]) { - result.push_back(a[i]); - } - i++; - } - - while (j < b.size()) { - if (result.empty() || result.back() != b[j]) { - result.push_back(b[j]); - } - j++; - } -} } // namespace openmc From 134ac0c2ed2d10efcfc04af33ee3876447904060 Mon Sep 17 00:00:00 2001 From: nuclearkevin <66632997+nuclearkevin@users.noreply.github.com> Date: Tue, 26 May 2026 22:14:18 -0500 Subject: [PATCH 09/73] Better error messages. --- include/openmc/majorant.h | 4 +--- src/majorant.cpp | 3 --- src/particle.cpp | 11 ++++++++--- src/simulation.cpp | 10 ++++++++++ 4 files changed, 19 insertions(+), 9 deletions(-) diff --git a/include/openmc/majorant.h b/include/openmc/majorant.h index f5a20cdd6b4..ac399a4b306 100644 --- a/include/openmc/majorant.h +++ b/include/openmc/majorant.h @@ -35,7 +35,7 @@ class Majorant { std::vector nuclides; // index of nuclides applied std::vector xs_; // cross section values Nuclide::EnergyGrid grid_; - constexpr static double safety_factor {1.01}; + constexpr static double safety_factor {1.1}; private: //! \brief Unionize the smooth and URR cross section grids for all nuclides in the problem. @@ -56,8 +56,6 @@ class Majorant { //! \brief Compute the maximum correction factor for the S(a,b) total cross section. double calculate_max_sab_tot_xs(double energy, int i_sab, double sab_frac, const Nuclide & nuc); - //!\brief Compute the max free gas elastic scattering cross section. - //! \brief Get the grid index for energy interpolation. int get_i_grid(double energy, const Nuclide::EnergyGrid & grid); diff --git a/src/majorant.cpp b/src/majorant.cpp index 21c27897abe..3efc21980e2 100644 --- a/src/majorant.cpp +++ b/src/majorant.cpp @@ -29,9 +29,6 @@ void create_majorant() data::n_majorant = std::make_unique(); data::n_majorant->write_ascii("macro_majorant.txt"); - - // data::n_majorant->grid_.init(); - // data::n_majorant->write_ascii("macro_majorant.txt"); } Majorant::Majorant() diff --git a/src/particle.cpp b/src/particle.cpp index 09230e1e152..52be8502c55 100644 --- a/src/particle.cpp +++ b/src/particle.cpp @@ -376,7 +376,10 @@ void Particle::event_delta_advance() } if (!exhaustive_find_cell(*this)) { // We've lost this particle. - mark_as_lost("Particle could not be located during the delta tracking loop!"); + mark_as_lost( + "Particle " + + std::to_string(id()) + + " could not be located during the delta tracking loop!"); wgt() = 0.0; return; } @@ -403,7 +406,9 @@ void Particle::event_delta_advance() // Need to locate the particle at the collision site. if (!exhaustive_find_cell(*this)) { // We've lost this particle. - mark_as_lost("Particle could not be located at the delta tracking collision site!"); + mark_as_lost( + "Particle " + std::to_string(id()) + + " could not be located at the delta tracking collision site!"); wgt() = 0.0; return; } @@ -937,7 +942,7 @@ void Particle::cross_periodic_bc( void Particle::update_majorant() { - this->majorant() = 1.1 * data::n_majorant->calculate_xs(this->E()); + this->majorant() = Majorant::safety_factor * data::n_majorant->calculate_xs(this->E()); } void Particle::mark_as_lost(const char* message) diff --git a/src/simulation.cpp b/src/simulation.cpp index 554af24c1a0..cce2c14d5b5 100644 --- a/src/simulation.cpp +++ b/src/simulation.cpp @@ -920,6 +920,16 @@ void transport_delta_tracking_single_particle(Particle& p) if (!p.alive()) break; p.event_calculate_xs(); + if (p.macro_xs().total / p.majorant() > 1.0) { + p.mark_as_lost( + "Ratio of the total cross section (" + + std::to_string(p.macro_xs().total) + + ") to the majorant cross section (" + + std::to_string(p.majorant()) + + ") for particle " + std::to_string(p.id()) + + " with energy " + std::to_string(p.E()) + + " is greater than unity!"); + } if (prn(p.current_seed()) < (p.macro_xs().total / p.majorant())) { p.event_collide(); } From 81d58e74fe003f41b06040c8ac957e348987b165 Mon Sep 17 00:00:00 2001 From: nuclearkevin <66632997+nuclearkevin@users.noreply.github.com> Date: Wed, 27 May 2026 11:06:27 -0500 Subject: [PATCH 10/73] Fix issues with URR cross sections. Make error messages better --- include/openmc/majorant.h | 15 +++---- src/majorant.cpp | 89 +++++++++------------------------------ src/particle.cpp | 11 +---- src/simulation.cpp | 15 ++++--- 4 files changed, 35 insertions(+), 95 deletions(-) diff --git a/include/openmc/majorant.h b/include/openmc/majorant.h index ac399a4b306..ddf027c075c 100644 --- a/include/openmc/majorant.h +++ b/include/openmc/majorant.h @@ -35,7 +35,7 @@ class Majorant { std::vector nuclides; // index of nuclides applied std::vector xs_; // cross section values Nuclide::EnergyGrid grid_; - constexpr static double safety_factor {1.1}; + constexpr static double safety_factor {1.01}; private: //! \brief Unionize the smooth and URR cross section grids for all nuclides in the problem. @@ -48,22 +48,19 @@ class Majorant { void fill_material_maj_xs(const Material & mat, const std::vector & to_grid, std::vector & mat_maj); //! \brief Compute the maximum smooth cross section for a given energy point. - double calculate_max_smooth_xs(double energy, const Nuclide & nuc); + double calculate_max_smooth_xs(double energy, const Nuclide & nuc) const; //! \brief Compute the maximum URR cross section for a given energy point. - double calculate_max_urr_xs(double energy, const Nuclide & nuc, double smooth); + double calculate_max_urr_xs(double energy, const Nuclide & nuc, double smooth) const; //! \brief Compute the maximum correction factor for the S(a,b) total cross section. - double calculate_max_sab_tot_xs(double energy, int i_sab, double sab_frac, const Nuclide & nuc); + double calculate_max_sab_tot_xs(double energy, int i_sab, double sab_frac, const Nuclide & nuc) const; //! \brief Get the grid index for energy interpolation. - int get_i_grid(double energy, const Nuclide::EnergyGrid & grid); + int get_i_grid(double energy, const Nuclide::EnergyGrid & grid) const; //! \brief Helper function to perform linear-linear interpolation. - double interpolate_lin_1D(double E_0, double E_1, double xs_0, double xs_1, double E); - - //! \brief Helper function to perform log-log interpolation. - double interpolate_log_1D(double E_0, double E_1, double xs_0, double xs_1, double E); + double interpolate_lin_1D(double x_0, double x_1, double y_0, double y_1, double x) const; }; // class Majorant void create_majorant(); diff --git a/src/majorant.cpp b/src/majorant.cpp index 3efc21980e2..63fe4e9f6e8 100644 --- a/src/majorant.cpp +++ b/src/majorant.cpp @@ -72,27 +72,7 @@ Majorant::Majorant(const std::string & majorant_file) double Majorant::calculate_xs(double energy) const { - // Find energy index on energy grid - int neutron = ParticleType::neutron().transport_index(); - int i_log_union = std::log(energy * data::energy_min_rcp[neutron]) * simulation::log_spacing_rcp; - - int i_grid; - if (i_log_union < 0) { - i_grid = 0; - } else if (i_log_union >= (grid_.grid_index.size() - 2)) { - i_grid = grid_.energy.size() - 2; - } else { - // Determine bounding indices based on which equal log-spaced - // interval the energy is in - int i_low = grid_.grid_index[i_log_union]; - int i_high = grid_.grid_index[i_log_union + 1] + 1; - - // Perform binary search over reduced range - i_grid = i_low + lower_bound_index(&grid_.energy[i_low], &grid_.energy[i_high], energy); - } - - // check for rare case where two energy points are the same - if (grid_.energy[i_grid] == grid_.energy[i_grid + 1]) ++i_grid; + int i_grid = get_i_grid(energy, grid_); // calculate interpolation factor double f = (energy - grid_.energy[i_grid]) / @@ -190,8 +170,10 @@ Majorant::fill_material_maj_xs(const Material & mat, const std::vector & { for (int i_energy = 0; i_energy < to_grid.size(); ++i_energy) { const double union_energy = to_grid[i_energy]; + int mat_sab_table_idx = 0; bool check_sab = (mat.thermal_tables_.size() > 0); + for (int i = 0; i < mat.nuclide_.size(); ++i) { const auto & nuclide = data::nuclides[mat.nuclide_[i]]; // ====================================================================== @@ -210,7 +192,7 @@ Majorant::fill_material_maj_xs(const Material & mat, const std::vector & // If particle energy is greater than the highest energy for the // S(a,b) table, then don't use the S(a,b) table - if (union_energy > data::thermal_scatt[i_sab]->energy_max_) { + if ((union_energy - 1e-6) > data::thermal_scatt[i_sab]->energy_max_) { i_sab = C_NONE; } @@ -251,7 +233,7 @@ Majorant::fill_material_maj_xs(const Material & mat, const std::vector & } double -Majorant::calculate_max_smooth_xs(double energy, const Nuclide & nuc) +Majorant::calculate_max_smooth_xs(double energy, const Nuclide & nuc) const { double max_smooth_tot_xs = 0.0; for (int i_temp = 0; i_temp < nuc.kTs_.size(); ++i_temp) { @@ -266,7 +248,7 @@ Majorant::calculate_max_smooth_xs(double energy, const Nuclide & nuc) } double -Majorant::calculate_max_urr_xs(double energy, const Nuclide & nuc, double smooth) +Majorant::calculate_max_urr_xs(double energy, const Nuclide & nuc, double smooth) const { if (!nuc.urr_present_) { return 0.0; @@ -274,51 +256,25 @@ Majorant::calculate_max_urr_xs(double energy, const Nuclide & nuc, double smooth double max_urr_xs = 0.0; for (const auto & urr : nuc.urr_data_) { - if (!urr.energy_in_bounds(energy)) { + if (!(urr.energy_in_bounds(energy - 1e-6) || urr.energy_in_bounds(energy + 1e-6))) { continue; } - // Linear search to find the left and right interpolation points. - // TODO: if this is a problem we can perform binary search in energy - int i_energy; - for (int i = 0; i < urr.energy_.size() - 1; ++i) { - if (urr.energy_[i] <= energy && energy < urr.energy_[i + 1]) { - i_energy = i; - break; - } - } - - // Find the maximum URR cross sections for the two bounding energy points. - double max_urr_xs_E0 = 0.0; - double max_urr_xs_E1 = 0.0; - for (int i_cdf = 0; i_cdf < urr.n_cdf(); ++i_cdf) { - max_urr_xs_E0 = std::max(max_urr_xs_E0, urr.xs_values_(i_energy, i_cdf).total); - max_urr_xs_E1 = std::max(max_urr_xs_E1, urr.xs_values_(i_energy + 1, i_cdf).total); - } - - // Interpolate the bounding energy points. - double interp_urr_xs = 0.0; - if (urr.interp_ == Interpolation::lin_lin) { - interp_urr_xs = - interpolate_lin_1D(urr.energy_[i_energy], urr.energy_[i_energy + 1], max_urr_xs_E0, max_urr_xs_E1, energy); - } else if (urr.interp_ == Interpolation::log_log) { - interp_urr_xs = - interpolate_log_1D(urr.energy_[i_energy], urr.energy_[i_energy + 1], max_urr_xs_E0, max_urr_xs_E1, energy); - } - // Multiply by the smooth cross section (after interpolation) if required. - if (urr.multiply_smooth_) { - interp_urr_xs *= smooth; + // Take the maximum over all CDF and energy points for the URR range. + for (const auto & urr_xs : urr.xs_values_) { + if (urr.multiply_smooth_) { + max_urr_xs = std::max(max_urr_xs, urr_xs.total * smooth); + } else { + max_urr_xs = std::max(max_urr_xs, urr_xs.total); + } } - - // Take the maximum over temperature. - max_urr_xs = std::max(max_urr_xs, interp_urr_xs); } return max_urr_xs; } double -Majorant::calculate_max_sab_tot_xs(double energy, int i_sab, double sab_frac, const Nuclide & nuc) +Majorant::calculate_max_sab_tot_xs(double energy, int i_sab, double sab_frac, const Nuclide & nuc) const { const auto & thermal = data::thermal_scatt[i_sab]; @@ -374,7 +330,7 @@ Majorant::calculate_max_sab_tot_xs(double energy, int i_sab, double sab_frac, co } int -Majorant::get_i_grid(double energy, const Nuclide::EnergyGrid & grid) +Majorant::get_i_grid(double energy, const Nuclide::EnergyGrid & grid) const { // Find energy index on energy grid int neutron = ParticleType::neutron().transport_index(); @@ -402,16 +358,9 @@ Majorant::get_i_grid(double energy, const Nuclide::EnergyGrid & grid) } double -Majorant::interpolate_lin_1D(double E_0, double E_1, double xs_0, double xs_1, double E) -{ - double f = (E - E_0) / (E_1 - E_0); - return (1.0 - f) * xs_0 + f * xs_1; -} - -double -Majorant::interpolate_log_1D(double E_0, double E_1, double xs_0, double xs_1, double E) +Majorant::interpolate_lin_1D(double x_0, double x_1, double y_0, double y_1, double x) const { - double f = std::log(E / E_0) / std::log(E_1 / E_0); - return std::exp((1.0 - f) * std::log(xs_0) + f * std::log(xs_1)); + double f = (x - x_0) / (x_1 - x_0); + return (1.0 - f) * y_0 + f * y_1; } } // namespace openmc diff --git a/src/particle.cpp b/src/particle.cpp index 52be8502c55..7b841e7d83c 100644 --- a/src/particle.cpp +++ b/src/particle.cpp @@ -376,11 +376,7 @@ void Particle::event_delta_advance() } if (!exhaustive_find_cell(*this)) { // We've lost this particle. - mark_as_lost( - "Particle " - + std::to_string(id()) - + " could not be located during the delta tracking loop!"); - wgt() = 0.0; + mark_as_lost(fmt::format("Particle {} could not be located during the delta tracking loop!", id())); return; } } @@ -406,10 +402,7 @@ void Particle::event_delta_advance() // Need to locate the particle at the collision site. if (!exhaustive_find_cell(*this)) { // We've lost this particle. - mark_as_lost( - "Particle " + std::to_string(id()) - + " could not be located at the delta tracking collision site!"); - wgt() = 0.0; + mark_as_lost(fmt::format("Particle {} could not be located at the delta tracking collision site!", id())); return; } diff --git a/src/simulation.cpp b/src/simulation.cpp index cce2c14d5b5..d5f9c6a0c4f 100644 --- a/src/simulation.cpp +++ b/src/simulation.cpp @@ -922,13 +922,14 @@ void transport_delta_tracking_single_particle(Particle& p) p.event_calculate_xs(); if (p.macro_xs().total / p.majorant() > 1.0) { p.mark_as_lost( - "Ratio of the total cross section (" - + std::to_string(p.macro_xs().total) - + ") to the majorant cross section (" - + std::to_string(p.majorant()) - + ") for particle " + std::to_string(p.id()) - + " with energy " + std::to_string(p.E()) - + " is greater than unity!"); + fmt::format("Ratio of the total cross section ({}) to the majorant " + "cross section ({}) for particle {} with energy {} is " + "greater than unity!", + p.macro_xs().total, + p.majorant(), + p.id(), + p.E())); + break; } if (prn(p.current_seed()) < (p.macro_xs().total / p.majorant())) { p.event_collide(); From 01240eef7aa8136df75b0668b0ae125442098713 Mon Sep 17 00:00:00 2001 From: nuclearkevin <66632997+nuclearkevin@users.noreply.github.com> Date: Wed, 27 May 2026 16:26:07 -0500 Subject: [PATCH 11/73] Prep for photon majorants. --- include/openmc/majorant.h | 137 +++++++++++++++++++++++------ include/openmc/thermal.h | 6 ++ src/majorant.cpp | 177 +++++++++++++++++++++++--------------- src/particle.cpp | 2 +- src/simulation.cpp | 10 +-- src/thermal.cpp | 14 +++ 6 files changed, 244 insertions(+), 102 deletions(-) diff --git a/include/openmc/majorant.h b/include/openmc/majorant.h index ddf027c075c..01adf0dcbde 100644 --- a/include/openmc/majorant.h +++ b/include/openmc/majorant.h @@ -12,58 +12,143 @@ namespace openmc { -class Majorant; +class NeutronMajorant; + +//============================================================================== +// Global variables +//============================================================================== namespace data { - extern std::unique_ptr n_majorant; + extern std::unique_ptr n_majorant; extern std::string majorant_file; -} +} // namespace data -class Majorant { +//============================================================================== +class Majorant { public: - Majorant(); - Majorant(const std::vector& energy, const std::vector& xs); - Majorant(const std::string & majorant_file); + //---------------------------------------------------------------------------- + // Constructors + + Majorant() = default; + Majorant(const std::string & majorant_file, double min_E_transport, double max_E_transport); + + //---------------------------------------------------------------------------- + // Methods + + //! \brief Initialize the majorant cross section. + virtual void init() = 0; + //! \brief Write the majorant cross section to a CSV file for visualization + // TODO: remove this when done prototyping. + // + //! \param[in] filename The path/name for the majorant file void write_ascii(const std::string& filename) const; - //! \brief Calculate the microscopic cross section at a given energy - double calculate_xs(double energy) const; + //---------------------------------------------------------------------------- + // Data members - // data members - std::vector nuclides; // index of nuclides applied - std::vector xs_; // cross section values - Nuclide::EnergyGrid grid_; - constexpr static double safety_factor {1.01}; + constexpr static double safety_factor {1.01}; //!< A dilation factor to ensure floating-point round + //!< off and inexact majorant URR and S(a,b) cross sections + //!< don't bias results + +protected: + //---------------------------------------------------------------------------- + // Protected Methods + + //! \brief Helper function to perform linear interpolation. + // + //! \param[in] x_0 The first x coordinate + //! \param[in] x_1 The second x coordinate + //! \param[in] y_0 The y coordinate associated with x_0 + //! \param[in] y_1 The y coordinate associated with x_1 + //! \param[in] x A x point between x_0 and x_1 to find a y value at + double interpolate_lin_1D(double x_0, double x_1, double y_0, double y_1, double x) const; + + //! \brief Helper function to perform log interpolation. + double interpolate_log_1D(double x_0, double x_1, double y_0, double y_1, double x) const; + + //---------------------------------------------------------------------------- + // Protected data members + + Nuclide::EnergyGrid grid_; //!< The unionized energy grid + std::vector xs_; //!< Macroscopic majorant cross sections at each grid point in grid_ +}; // class Majorant + +//============================================================================== + +class NeutronMajorant : public Majorant { + +public: + //---------------------------------------------------------------------------- + // Constructors + + NeutronMajorant() = default; + NeutronMajorant(const std::string & majorant_file); + + //---------------------------------------------------------------------------- + // Public Methods + + virtual void init() override final; + + //! \brief Calculate the macroscopic majorant cross section in units of [cm^-1] + // + //! \param[in] energy The energy to compute the cross section at in [eV] + double calculate_neutron_xs(double energy) const; private: - //! \brief Unionize the smooth and URR cross section grids for all nuclides in the problem. + //---------------------------------------------------------------------------- + // Private Methods + + //! \brief A function to unionize the smooth and URR cross section grids for all nuclides in the problem. void compute_unionized_grid(); //! \brief Compute the majorant cross section. void setup_majorant(); - //! \brief Compute a per-material majorant cross section. + //! \brief Compute a per-material macroscopic majorant cross section in units of [barn] + // + //! \param[in] mat The material to compute the majorant cross section of + //! \param[in] to_grid The grid points to evaluate the majorant at in [eV] + //! \param[out] mat_maj The array to write the macroscopic majorant to. The resulting cross section has units of [cm^-1] void fill_material_maj_xs(const Material & mat, const std::vector & to_grid, std::vector & mat_maj); - //! \brief Compute the maximum smooth cross section for a given energy point. + //! \brief Compute the maximum smooth microscopic total cross section in units of [barn]. + // + //! \param[in] energy The energy to evaluate the cross section at in [eV] + //! \param[in] nuc The nuclide to compute the microscopic total cross section of double calculate_max_smooth_xs(double energy, const Nuclide & nuc) const; - //! \brief Compute the maximum URR cross section for a given energy point. - double calculate_max_urr_xs(double energy, const Nuclide & nuc, double smooth) const; - - //! \brief Compute the maximum correction factor for the S(a,b) total cross section. + //! \brief Compute the maximum microscopic total URR cross section in units of [barn]. + // + //! \param[in] energy The energy to evaluate the cross section at in [eV] + //! \param[in] nuc The nuclide to compute the microscopic total cross section of + //! \param[in] smooth_xs The smooth total cross section in units of [eV] to use (if needed by the ptable) + double calculate_max_urr_xs(double energy, const Nuclide & nuc, double smooth_xs) const; + + //! \brief Compute the maximum microscopic S(a,b) total cross section in units of [barn] + // + //! \param[in] energy The energy to evaluate the cross section at in [eV] + //! \param[in] i_sab The index into the thermal scattering table array for this nuclide + //! \param[in] sab_frac The fraction of the bound cross section to use vs the free gas cross section + //! \param[in] nuc The nuclide to compute the microscopic total cross section of double calculate_max_sab_tot_xs(double energy, int i_sab, double sab_frac, const Nuclide & nuc) const; - //! \brief Get the grid index for energy interpolation. + //! \brief Get the grid index for energy interpolation + // + //! \param[in] energy The energy to evaluate the cross section at in [eV] + //! \param[in] grid The energy grid to search for an energy grid index int get_i_grid(double energy, const Nuclide::EnergyGrid & grid) const; +}; // class NeutronMajorant - //! \brief Helper function to perform linear-linear interpolation. - double interpolate_lin_1D(double x_0, double x_1, double y_0, double y_1, double x) const; -}; // class Majorant +//============================================================================== + +//============================================================================== +// Non-member functions +//============================================================================== - void create_majorant(); +//! \brief A function to create the global majorant cross section(s). +void create_majorants(); } #endif // OPENMC_MAJORANT_H diff --git a/include/openmc/thermal.h b/include/openmc/thermal.h index c06a2ee0dde..5bd17ce9acd 100644 --- a/include/openmc/thermal.h +++ b/include/openmc/thermal.h @@ -43,6 +43,12 @@ class ThermalData { //! \param[out] inelastic Inelastic scattering cross section in [b] void calculate_xs(double E, double* elastic, double* inelastic) const; + //! Calculate the maximum cross section + // + //! \param[out] elastic Elastic scattering cross section in [b] + //! \param[out] inelastic Inelastic scattering cross section in [b] + void calculate_max_xs(double* elastic, double* inelastic) const; + //! Sample an outgoing energy and angle // //! \param[in] micro_xs Microscopic cross sections diff --git a/src/majorant.cpp b/src/majorant.cpp index 63fe4e9f6e8..857fd098335 100644 --- a/src/majorant.cpp +++ b/src/majorant.cpp @@ -12,54 +12,32 @@ namespace openmc { +//============================================================================== +// Global variables +//============================================================================== + namespace data { -std::unique_ptr n_majorant; +std::unique_ptr n_majorant; std::string majorant_file; -} -void create_majorant() -{ - write_message("Creating majorant cross section..."); - if (data::majorant_file != "") { - write_message("Loading majorant from " + data::majorant_file); - // We can load the majorant from a file instead. - data::n_majorant = std::make_unique(data::majorant_file); - return; - } +} // namespace data - data::n_majorant = std::make_unique(); - data::n_majorant->write_ascii("macro_majorant.txt"); -} +//============================================================================== +// Majorant implementation +//============================================================================== -Majorant::Majorant() +Majorant::Majorant(const std::string & majorant_file, double min_E_transport, double max_E_transport) { - // Unionize the grid. - compute_unionized_grid(); - - // Setup the majorant given the new grid. - setup_majorant(); -} - -Majorant::Majorant(const std::vector& energy, - const std::vector& xs) : xs_(xs) -{ - grid_.energy = energy; - grid_.init(); -} - -Majorant::Majorant(const std::string & majorant_file) -{ - const auto neutron_idx = ParticleType::neutron().transport_index(); std::ifstream majorant_data(majorant_file); std::string line; while (std::getline(majorant_data, line)) { auto delim_pos = line.find(","); auto energy = std::stod(line.substr(0, delim_pos)); - if (energy < data::energy_min[neutron_idx]) { + if (energy < min_E_transport) { continue; } - if (energy > data::energy_max[neutron_idx]) { + if (energy > max_E_transport) { break; } grid_.energy.push_back(energy); @@ -69,8 +47,42 @@ Majorant::Majorant(const std::string & majorant_file) grid_.init(); } +void +Majorant::write_ascii(const std::string& filename) const +{ + std::ofstream of(filename); + for (int i = 0; i < xs_.size(); i++) { + of << grid_.energy[i] << "\t" << xs_[i] << "\n"; + } + of.close(); +} + +double +Majorant::interpolate_lin_1D(double x_0, double x_1, double y_0, double y_1, double x) const +{ + double f = (x - x_0) / (x_1 - x_0); + return (1.0 - f) * y_0 + f * y_1; +} + +double +Majorant::interpolate_log_1D(double x_0, double x_1, double y_0, double y_1, double x) const +{ + double f = std::log(x / x_0) / std::log(x_1 / x_0); + return std::exp((1.0 - f) * std::log(y_0) + f * std::log(y_1)); +} + +//============================================================================== +// NeutronMajorant implementation +//============================================================================== + +NeutronMajorant::NeutronMajorant(const std::string & majorant_file) + : Majorant(majorant_file, + data::energy_min[ParticleType::neutron().transport_index()], + data::energy_max[ParticleType::neutron().transport_index()]) +{ } + double -Majorant::calculate_xs(double energy) const +NeutronMajorant::calculate_neutron_xs(double energy) const { int i_grid = get_i_grid(energy, grid_); @@ -83,32 +95,29 @@ Majorant::calculate_xs(double energy) const return xs; } -void Majorant::write_ascii(const std::string& filename) const +void +NeutronMajorant::init() { - std::ofstream of(filename); - for (int i = 0; i < xs_.size(); i++) { - of << grid_.energy[i] << "\t" << xs_[i] << "\n"; - } - of.close(); + // Unionize the grid. + compute_unionized_grid(); + + // Setup the majorant given the new grid. + setup_majorant(); } void -Majorant::compute_unionized_grid() +NeutronMajorant::compute_unionized_grid() { write_message("Unionizing nuclide cross section grids."); // This function generates a unionized cross section grid between smooth cross // sections and URR probability table grids. for (const auto & mat : model::materials) { - if (mat->ncrystal_mat_) { - fatal_error("Delta tracking is not supported when using NCrystal!"); - } - for (auto nuclide_idx : mat->nuclide_) { const auto & nuclide = data::nuclides[nuclide_idx]; // ====================================================================== - // Unionizing the URR temperature grid. Loop over temperature points. + // Unionizing the URR temperature grid. if (nuclide->urr_present_ && settings::urr_ptables_on) { for (const auto & nuc_urr : nuclide->urr_data_) { grid_.energy.insert(grid_.energy.end(), nuc_urr.energy_.begin(), nuc_urr.energy_.end()); @@ -116,7 +125,7 @@ Majorant::compute_unionized_grid() } // ====================================================================== - // Unionize the smooth cross section grid. Loop over temperature points. + // Unionize the smooth cross section grid. for (const auto & n_grid : nuclide->grid_) { grid_.energy.insert(grid_.energy.end(), n_grid.energy.begin(),n_grid.energy.end()); } @@ -125,8 +134,8 @@ Majorant::compute_unionized_grid() std::sort(grid_.energy.begin(), grid_.energy.end()); std::unique(grid_.energy.begin(), grid_.energy.end()); - // remove all values below the minimum neutron energy int neutron = ParticleType::neutron().transport_index(); + // remove all values below the minimum neutron energy auto min_it = grid_.energy.begin(); while (*min_it < data::energy_min[neutron]) { min_it++; } grid_.energy.erase(grid_.energy.begin(), min_it + 1); @@ -144,7 +153,7 @@ Majorant::compute_unionized_grid() } void -Majorant::setup_majorant() +NeutronMajorant::setup_majorant() { // Fill with zeros. xs_.resize(grid_.energy.size(), 0.0); @@ -166,7 +175,7 @@ Majorant::setup_majorant() } void -Majorant::fill_material_maj_xs(const Material & mat, const std::vector & to_grid, std::vector & mat_maj) +NeutronMajorant::fill_material_maj_xs(const Material & mat, const std::vector & to_grid, std::vector & mat_maj) { for (int i_energy = 0; i_energy < to_grid.size(); ++i_energy) { const double union_energy = to_grid[i_energy]; @@ -233,7 +242,7 @@ Majorant::fill_material_maj_xs(const Material & mat, const std::vector & } double -Majorant::calculate_max_smooth_xs(double energy, const Nuclide & nuc) const +NeutronMajorant::calculate_max_smooth_xs(double energy, const Nuclide & nuc) const { double max_smooth_tot_xs = 0.0; for (int i_temp = 0; i_temp < nuc.kTs_.size(); ++i_temp) { @@ -248,7 +257,7 @@ Majorant::calculate_max_smooth_xs(double energy, const Nuclide & nuc) const } double -Majorant::calculate_max_urr_xs(double energy, const Nuclide & nuc, double smooth) const +NeutronMajorant::calculate_max_urr_xs(double energy, const Nuclide & nuc, double smooth_xs) const { if (!nuc.urr_present_) { return 0.0; @@ -260,21 +269,39 @@ Majorant::calculate_max_urr_xs(double energy, const Nuclide & nuc, double smooth continue; } - // Take the maximum over all CDF and energy points for the URR range. - for (const auto & urr_xs : urr.xs_values_) { - if (urr.multiply_smooth_) { - max_urr_xs = std::max(max_urr_xs, urr_xs.total * smooth); - } else { - max_urr_xs = std::max(max_urr_xs, urr_xs.total); - } + int i_energy = lower_bound_index(&urr.energy_.front(), &urr.energy_.back(), energy); + + // Find the maximum URR cross sections for the two bounding energy points. + double max_urr_xs_E0 = 0.0; + double max_urr_xs_E1 = 0.0; + for (int i_cdf = 0; i_cdf < urr.n_cdf(); ++i_cdf) { + max_urr_xs_E0 = std::max(max_urr_xs_E0, urr.xs_values_(i_energy, i_cdf).total); + max_urr_xs_E1 = std::max(max_urr_xs_E1, urr.xs_values_(i_energy + 1, i_cdf).total); + } + + // Interpolate the bounding energy points. + double interp_urr_xs = 0.0; + if (urr.interp_ == Interpolation::lin_lin) { + interp_urr_xs = + interpolate_lin_1D(urr.energy_[i_energy], urr.energy_[i_energy + 1], max_urr_xs_E0, max_urr_xs_E1, energy); + } else if (urr.interp_ == Interpolation::log_log) { + interp_urr_xs = + interpolate_log_1D(urr.energy_[i_energy], urr.energy_[i_energy + 1], max_urr_xs_E0, max_urr_xs_E1, energy); + } + + // Multiply by the smooth cross section (after interpolation) if required. + if (urr.multiply_smooth_) { + interp_urr_xs *= smooth_xs; } + + max_urr_xs = std::max(max_urr_xs, interp_urr_xs); } return max_urr_xs; } double -Majorant::calculate_max_sab_tot_xs(double energy, int i_sab, double sab_frac, const Nuclide & nuc) const +NeutronMajorant::calculate_max_sab_tot_xs(double energy, int i_sab, double sab_frac, const Nuclide & nuc) const { const auto & thermal = data::thermal_scatt[i_sab]; @@ -300,10 +327,7 @@ Majorant::calculate_max_sab_tot_xs(double energy, int i_sab, double sab_frac, co ++i_sab_temp; } // Interpolate the scattering cross sections to the nuclide temperature grid point. - double T0_elastic; - double T1_elastic; - double T0_inelastic; - double T1_inelastic; + double T0_elastic, T1_elastic, T0_inelastic, T1_inelastic; thermal->data_[i_sab_temp].calculate_xs(energy, &T0_elastic, &T0_inelastic); thermal->data_[i_sab_temp + 1].calculate_xs(energy, &T1_elastic, &T1_inelastic); thermal_elastic = interpolate_lin_1D(tkTs[i_sab_temp], tkTs[i_sab_temp + 1], T0_elastic, T1_elastic, nuc_kT); @@ -330,7 +354,7 @@ Majorant::calculate_max_sab_tot_xs(double energy, int i_sab, double sab_frac, co } int -Majorant::get_i_grid(double energy, const Nuclide::EnergyGrid & grid) const +NeutronMajorant::get_i_grid(double energy, const Nuclide::EnergyGrid & grid) const { // Find energy index on energy grid int neutron = ParticleType::neutron().transport_index(); @@ -357,10 +381,23 @@ Majorant::get_i_grid(double energy, const Nuclide::EnergyGrid & grid) const return i_grid; } -double -Majorant::interpolate_lin_1D(double x_0, double x_1, double y_0, double y_1, double x) const +//============================================================================== +// Static functions +//============================================================================== + +void create_majorants() { - double f = (x - x_0) / (x_1 - x_0); - return (1.0 - f) * y_0 + f * y_1; + if (data::majorant_file != "") { + write_message("Loading majorant from " + data::majorant_file); + // We can load the majorant from a file instead. + data::n_majorant = std::make_unique(data::majorant_file); + return; + } + + write_message("Creating majorant cross section..."); + data::n_majorant = std::make_unique(); + data::n_majorant->init(); + data::n_majorant->write_ascii("macro_majorant.txt"); } + } // namespace openmc diff --git a/src/particle.cpp b/src/particle.cpp index 7b841e7d83c..8c68f6ebcb4 100644 --- a/src/particle.cpp +++ b/src/particle.cpp @@ -935,7 +935,7 @@ void Particle::cross_periodic_bc( void Particle::update_majorant() { - this->majorant() = Majorant::safety_factor * data::n_majorant->calculate_xs(this->E()); + this->majorant() = NeutronMajorant::safety_factor * data::n_majorant->calculate_neutron_xs(this->E()); } void Particle::mark_as_lost(const char* message) diff --git a/src/simulation.cpp b/src/simulation.cpp index d5f9c6a0c4f..565557b2997 100644 --- a/src/simulation.cpp +++ b/src/simulation.cpp @@ -86,7 +86,10 @@ int openmc_simulation_init() initialize_data(); } - if (settings::delta_tracking) create_majorant(); + // Create the majorant cross sections for delta tracking. + if (settings::delta_tracking) { + create_majorants(); + } // Determine how much work each process should do calculate_work(settings::n_particles); @@ -925,10 +928,7 @@ void transport_delta_tracking_single_particle(Particle& p) fmt::format("Ratio of the total cross section ({}) to the majorant " "cross section ({}) for particle {} with energy {} is " "greater than unity!", - p.macro_xs().total, - p.majorant(), - p.id(), - p.E())); + p.macro_xs().total, p.majorant(), p.id(), p.E())); break; } if (prn(p.current_seed()) < (p.macro_xs().total / p.majorant())) { diff --git a/src/thermal.cpp b/src/thermal.cpp index edfbddf23e8..641a6e589e2 100644 --- a/src/thermal.cpp +++ b/src/thermal.cpp @@ -291,6 +291,20 @@ void ThermalData::calculate_xs( *inelastic = (*inelastic_.xs)(E); } +void +ThermalData::calculate_max_xs(double* elastic, double* inelastic) const +{ + // Calculate thermal elastic scattering cross section + if (elastic_.xs) { + *elastic = (*elastic_.xs).max(); + } else { + *elastic = 0.0; + } + + // Calculate thermal inelastic scattering cross section + *inelastic = (*inelastic_.xs).max(); +} + AngleEnergy& ThermalData::sample_dist( const NuclideMicroXS& micro_xs, double E, uint64_t* seed) const { From 907bf8002fe872ce58773384529ef8d8e7f0891b Mon Sep 17 00:00:00 2001 From: nuclearkevin <66632997+nuclearkevin@users.noreply.github.com> Date: Thu, 28 May 2026 18:49:08 -0500 Subject: [PATCH 12/73] Photon majorants & delta tracking for photon transport. --- include/openmc/majorant.h | 114 ++++++++++++--- src/majorant.cpp | 284 +++++++++++++++++++++++++++++--------- src/particle.cpp | 8 +- src/settings.cpp | 8 +- src/simulation.cpp | 32 +++-- 5 files changed, 344 insertions(+), 102 deletions(-) diff --git a/include/openmc/majorant.h b/include/openmc/majorant.h index 01adf0dcbde..7a23b25e300 100644 --- a/include/openmc/majorant.h +++ b/include/openmc/majorant.h @@ -9,10 +9,12 @@ #include "openmc/settings.h" #include "openmc/nuclide.h" #include "openmc/material.h" +#include "openmc/photon.h" namespace openmc { class NeutronMajorant; +class PhotonMajorant; //============================================================================== // Global variables @@ -20,7 +22,9 @@ class NeutronMajorant; namespace data { extern std::unique_ptr n_majorant; - extern std::string majorant_file; + extern std::unique_ptr p_majorant; + extern std::string n_majorant_file; + extern std::string p_majorant_file; } // namespace data //============================================================================== @@ -31,13 +35,16 @@ class Majorant { // Constructors Majorant() = default; - Majorant(const std::string & majorant_file, double min_E_transport, double max_E_transport); + Majorant(const std::string & majorant_file, int p_transport_indx); //---------------------------------------------------------------------------- // Methods - //! \brief Initialize the majorant cross section. - virtual void init() = 0; + //! \brief A function to unionize particle energy grids. + virtual void compute_unionized_grid() = 0; + + //! \brief Populate the majorant cross section. + virtual void compute_majorant(); //! \brief Write the majorant cross section to a CSV file for visualization // TODO: remove this when done prototyping. @@ -45,17 +52,17 @@ class Majorant { //! \param[in] filename The path/name for the majorant file void write_ascii(const std::string& filename) const; - //---------------------------------------------------------------------------- - // Data members - - constexpr static double safety_factor {1.01}; //!< A dilation factor to ensure floating-point round - //!< off and inexact majorant URR and S(a,b) cross sections - //!< don't bias results - protected: //---------------------------------------------------------------------------- // Protected Methods + //! \brief Compute a per-material macroscopic majorant cross section in units of [cm^-1] + // + //! \param[in] mat The material to compute the majorant cross section of + //! \param[in] to_grid The grid points to evaluate the majorant at in [eV] + //! \param[out] mat_maj The array to write the macroscopic majorant to. The resulting cross section has units of [cm^-1] + virtual void fill_material_maj_xs(const Material & mat, const std::vector & to_grid, std::vector & mat_maj) = 0; + //! \brief Helper function to perform linear interpolation. // //! \param[in] x_0 The first x coordinate @@ -89,29 +96,34 @@ class NeutronMajorant : public Majorant { //---------------------------------------------------------------------------- // Public Methods - virtual void init() override final; + virtual void compute_unionized_grid() override final; //! \brief Calculate the macroscopic majorant cross section in units of [cm^-1] // //! \param[in] energy The energy to compute the cross section at in [eV] double calculate_neutron_xs(double energy) const; -private: //---------------------------------------------------------------------------- - // Private Methods + // Data members - //! \brief A function to unionize the smooth and URR cross section grids for all nuclides in the problem. - void compute_unionized_grid(); + constexpr static double safety_factor_ {1.01}; //!< A dilation factor to ensure floating-point round + //!< off and inexact majorant URR and S(a,b) cross sections + //!< don't bias results - //! \brief Compute the majorant cross section. - void setup_majorant(); +protected: + //---------------------------------------------------------------------------- + // Protected Methods - //! \brief Compute a per-material macroscopic majorant cross section in units of [barn] + //! \brief Compute a per-material macroscopic majorant cross section in units of [cm^-1] // //! \param[in] mat The material to compute the majorant cross section of //! \param[in] to_grid The grid points to evaluate the majorant at in [eV] //! \param[out] mat_maj The array to write the macroscopic majorant to. The resulting cross section has units of [cm^-1] - void fill_material_maj_xs(const Material & mat, const std::vector & to_grid, std::vector & mat_maj); + virtual void fill_material_maj_xs(const Material & mat, const std::vector & to_grid, std::vector & mat_maj) override; + +private: + //---------------------------------------------------------------------------- + // Private Methods //! \brief Compute the maximum smooth microscopic total cross section in units of [barn]. // @@ -139,10 +151,72 @@ class NeutronMajorant : public Majorant { //! \param[in] energy The energy to evaluate the cross section at in [eV] //! \param[in] grid The energy grid to search for an energy grid index int get_i_grid(double energy, const Nuclide::EnergyGrid & grid) const; + + //---------------------------------------------------------------------------- + // Private data members + + static constexpr int i_neutron = ParticleType::neutron().transport_index(); }; // class NeutronMajorant //============================================================================== +class PhotonMajorant : public Majorant { +public: + //---------------------------------------------------------------------------- + // Constructors + + PhotonMajorant() = default; + PhotonMajorant(const std::string & majorant_file); + + //---------------------------------------------------------------------------- + // Public Methods + + virtual void compute_unionized_grid() override final; + + //! \brief Calculate the macroscopic majorant cross section in units of [cm^-1] + // + //! \param[in] energy The energy to compute the cross section at in [eV] + double calculate_photon_xs(double energy) const; + + //---------------------------------------------------------------------------- + // Data members + + constexpr static double safety_factor_ {1.01}; //!< A dilation factor to catch interpolation error + +protected: + //---------------------------------------------------------------------------- + // Protected Methods + + //! \brief Compute a per-material macroscopic majorant cross section in units of [cm^-1] + // + //! \param[in] mat The material to compute the majorant cross section of + //! \param[in] to_grid The grid points to evaluate the majorant at in [eV] + //! \param[out] mat_maj The array to write the macroscopic majorant to. The resulting cross section has units of [cm^-1] + virtual void fill_material_maj_xs(const Material & mat, const std::vector & to_grid, std::vector & mat_maj) override; + +private: + //---------------------------------------------------------------------------- + // Private Methods + + //! \brief Compute the maximum smooth microscopic total cross section in units of [barn]. + // + //! \param[in] energy The energy to evaluate the cross section at in [eV] + //! \param[in] nuc The nuclide to compute the microscopic total cross section of + double calculate_elem_tot_xs(double log_energy, const PhotonInteraction & elem) const; + + //! \brief Get the grid index for energy interpolation + // + //! \param[in] energy The energy to evaluate the cross section at in [eV] + //! \param[in] grid The energy grid to search for an energy grid index + int get_i_grid(double log_energy, const std::vector & energy_grid) const; + int get_i_grid(double log_energy, const tensor::Tensor & energy_grid) const; + + //---------------------------------------------------------------------------- + // Private data members + + static constexpr int i_photon_ = ParticleType::photon().transport_index(); +}; // class PhotonMajorant + //============================================================================== // Non-member functions //============================================================================== diff --git a/src/majorant.cpp b/src/majorant.cpp index 857fd098335..4beaf45564c 100644 --- a/src/majorant.cpp +++ b/src/majorant.cpp @@ -8,6 +8,7 @@ #include "openmc/nuclide.h" #include "openmc/search.h" #include "openmc/simulation.h" +#include "openmc/settings.h" #include "openmc/thermal.h" namespace openmc { @@ -18,7 +19,9 @@ namespace openmc { namespace data { std::unique_ptr n_majorant; -std::string majorant_file; +std::unique_ptr p_majorant; +std::string n_majorant_file; +std::string p_majorant_file; } // namespace data @@ -26,7 +29,7 @@ std::string majorant_file; // Majorant implementation //============================================================================== -Majorant::Majorant(const std::string & majorant_file, double min_E_transport, double max_E_transport) +Majorant::Majorant(const std::string & majorant_file, int p_transport_indx) { std::ifstream majorant_data(majorant_file); @@ -34,17 +37,39 @@ Majorant::Majorant(const std::string & majorant_file, double min_E_transport, do while (std::getline(majorant_data, line)) { auto delim_pos = line.find(","); auto energy = std::stod(line.substr(0, delim_pos)); - if (energy < min_E_transport) { + if (energy < data::energy_min[p_transport_indx]) { continue; } - if (energy > max_E_transport) { + if (energy > data::energy_max[p_transport_indx]) { break; } grid_.energy.push_back(energy); xs_.push_back(std::stod(line.substr(delim_pos + 1))); } - grid_.init(); + if (p_transport_indx == ParticleType::neutron().transport_index()) { + grid_.init(); + } +} + +void +Majorant::compute_majorant() +{ + // Fill with zeros. + xs_.resize(grid_.energy.size(), 0.0); + + std::vector material_maj_xs; + material_maj_xs.resize(grid_.energy.size(), 0.0); + for (const auto & mat : model::materials) { + // Populate the per-material majorant cross section. + fill_material_maj_xs(*mat, grid_.energy, material_maj_xs); + + // Compute the full majorant by taking the max over each material cross section. + for (int i_energy = 0; i_energy < xs_.size(); ++i_energy) { + xs_[i_energy] = std::max(xs_[i_energy], material_maj_xs[i_energy]); + } + std::fill(material_maj_xs.begin(), material_maj_xs.end(), 0.0); + } } void @@ -52,7 +77,7 @@ Majorant::write_ascii(const std::string& filename) const { std::ofstream of(filename); for (int i = 0; i < xs_.size(); i++) { - of << grid_.energy[i] << "\t" << xs_[i] << "\n"; + of << grid_.energy[i] << "," << xs_[i] << "\n"; } of.close(); } @@ -76,9 +101,7 @@ Majorant::interpolate_log_1D(double x_0, double x_1, double y_0, double y_1, dou //============================================================================== NeutronMajorant::NeutronMajorant(const std::string & majorant_file) - : Majorant(majorant_file, - data::energy_min[ParticleType::neutron().transport_index()], - data::energy_max[ParticleType::neutron().transport_index()]) + : Majorant(majorant_file, i_neutron) { } double @@ -90,26 +113,12 @@ NeutronMajorant::calculate_neutron_xs(double energy) const double f = (energy - grid_.energy[i_grid]) / (grid_.energy[i_grid + 1]- grid_.energy[i_grid]); - double xs = (1.0 - f) * xs_[i_grid] + f * xs_[i_grid + 1]; - - return xs; -} - -void -NeutronMajorant::init() -{ - // Unionize the grid. - compute_unionized_grid(); - - // Setup the majorant given the new grid. - setup_majorant(); + return (1.0 - f) * xs_[i_grid] + f * xs_[i_grid + 1]; } void NeutronMajorant::compute_unionized_grid() { - write_message("Unionizing nuclide cross section grids."); - // This function generates a unionized cross section grid between smooth cross // sections and URR probability table grids. for (const auto & mat : model::materials) { @@ -127,57 +136,37 @@ NeutronMajorant::compute_unionized_grid() // ====================================================================== // Unionize the smooth cross section grid. for (const auto & n_grid : nuclide->grid_) { - grid_.energy.insert(grid_.energy.end(), n_grid.energy.begin(),n_grid.energy.end()); + grid_.energy.insert(grid_.energy.end(), n_grid.energy.begin(), n_grid.energy.end()); } } } std::sort(grid_.energy.begin(), grid_.energy.end()); - std::unique(grid_.energy.begin(), grid_.energy.end()); + auto unique_end = std::unique(grid_.energy.begin(), grid_.energy.end()); + grid_.energy.resize(std::distance(grid_.energy.begin(), unique_end)); - int neutron = ParticleType::neutron().transport_index(); // remove all values below the minimum neutron energy auto min_it = grid_.energy.begin(); - while (*min_it < data::energy_min[neutron]) { min_it++; } + while (*min_it < data::energy_min[i_neutron]) { min_it++; } grid_.energy.erase(grid_.energy.begin(), min_it + 1); // insert the minimum neutron energy at the beginning - grid_.energy.insert(grid_.energy.begin(), data::energy_min[neutron]); + grid_.energy.insert(grid_.energy.begin(), data::energy_min[i_neutron]); // remove all values above the maximum neutron energy auto max_it = --grid_.energy.end(); - while (*max_it > data::energy_max[neutron]) { max_it--; } + while (*max_it > data::energy_max[i_neutron]) { max_it--; } grid_.energy.erase(max_it - 1, grid_.energy.end()); // insert the maximum neutron energy at the end - grid_.energy.insert(grid_.energy.end(), data::energy_max[neutron]); + grid_.energy.insert(grid_.energy.end(), data::energy_max[i_neutron]); + // Initialize the grid for fast lookups. grid_.init(); } -void -NeutronMajorant::setup_majorant() -{ - // Fill with zeros. - xs_.resize(grid_.energy.size(), 0.0); - - std::vector material_maj_xs; - material_maj_xs.resize(grid_.energy.size(), 0.0); - for (const auto & mat : model::materials) { - write_message("Computing majorant total cross section for " + mat->name() + "."); - - // Populate the per-material majorant cross section. - fill_material_maj_xs((*mat.get()), grid_.energy, material_maj_xs); - - // Compute the full majorant by taking the max over each material cross section. - for (int i_energy = 0; i_energy < xs_.size(); ++i_energy) { - xs_[i_energy] = std::max(xs_[i_energy], material_maj_xs[i_energy]); - } - std::fill(material_maj_xs.begin(), material_maj_xs.end(), 0.0); - } -} - void NeutronMajorant::fill_material_maj_xs(const Material & mat, const std::vector & to_grid, std::vector & mat_maj) { for (int i_energy = 0; i_energy < to_grid.size(); ++i_energy) { + mat_maj[i_energy] = 0.0; const double union_energy = to_grid[i_energy]; int mat_sab_table_idx = 0; @@ -222,16 +211,16 @@ NeutronMajorant::fill_material_maj_xs(const Material & mat, const std::vector= 0) { // Thermal scattering cross sections using S(a,b) tables. - micro_smooth_tot_xs = calculate_max_sab_tot_xs(union_energy, i_sab, sab_frac, (*nuclide.get())); + micro_smooth_tot_xs = calculate_max_sab_tot_xs(union_energy, i_sab, sab_frac, *nuclide); } else { // Free gas smooth cross section - micro_smooth_tot_xs = calculate_max_smooth_xs(union_energy, (*nuclide.get())); + micro_smooth_tot_xs = calculate_max_smooth_xs(union_energy, *nuclide); } // ====================================================================== // Compute the URR cross section. This shouldn't intersect with the // S(a,b) cross section. - double micro_urr_xs = calculate_max_urr_xs(union_energy, (*nuclide.get()), micro_smooth_tot_xs); + double micro_urr_xs = calculate_max_urr_xs(union_energy, *nuclide, micro_smooth_tot_xs); // ====================================================================== // Accumulate the macroscopic cross section. @@ -357,8 +346,7 @@ int NeutronMajorant::get_i_grid(double energy, const Nuclide::EnergyGrid & grid) const { // Find energy index on energy grid - int neutron = ParticleType::neutron().transport_index(); - int i_log_union = std::log(energy * data::energy_min_rcp[neutron]) * simulation::log_spacing_rcp; + int i_log_union = std::log(energy * data::energy_min_rcp[i_neutron]) * simulation::log_spacing_rcp; int i_grid; if (i_log_union < 0) { @@ -381,23 +369,185 @@ NeutronMajorant::get_i_grid(double energy, const Nuclide::EnergyGrid & grid) con return i_grid; } +//============================================================================== +// PhotonMajorant implementation +//============================================================================== + +PhotonMajorant::PhotonMajorant(const std::string & majorant_file) + : Majorant(majorant_file, i_photon_) +{ } + +void +PhotonMajorant::compute_unionized_grid() +{ + // This function generates a unionized cross section grid for all elements. + for (const auto & mat : model::materials) { + for (int i = 0; i < mat->nuclide_.size(); ++i) { + const auto & element = data::elements[mat->element_[i]]; + grid_.energy.insert(grid_.energy.end(), element->energy_.begin(), element->energy_.end()); + } + } + std::sort(grid_.energy.begin(), grid_.energy.end()); + auto unique_end = std::unique(grid_.energy.begin(), grid_.energy.end()); + grid_.energy.resize(std::distance(grid_.energy.begin(), unique_end)); + + // remove all values below the minimum photon energy + auto min_it = grid_.energy.begin(); + while (*min_it < std::log(data::energy_min[i_photon_])) { min_it++; } + grid_.energy.erase(grid_.energy.begin(), min_it + 1); + // insert the minimum photon energy at the beginning + grid_.energy.insert(grid_.energy.begin(), std::log(data::energy_min[i_photon_])); + + // remove all values above the maximum photon energy + auto max_it = --grid_.energy.end(); + while (*max_it > std::log(data::energy_max[i_photon_])) { max_it--; } + grid_.energy.erase(max_it - 1, grid_.energy.end()); + // insert the maximum photon energy at the end + grid_.energy.insert(grid_.energy.end(), std::log(data::energy_max[i_photon_])); +} + +double +PhotonMajorant::calculate_photon_xs(double energy) const +{ + double log_energy = std::log(energy); + int i_grid = get_i_grid(log_energy, grid_.energy); + + // calculate interpolation factor + double f = + (log_energy - grid_.energy[i_grid]) / (grid_.energy[i_grid + 1] - grid_.energy[i_grid]); + + // interpolate the total cross section + return std::exp(xs_[i_grid] + f * (xs_[i_grid + 1] - xs_[i_grid])); +} + +void +PhotonMajorant::fill_material_maj_xs(const Material & mat, const std::vector & to_grid, std::vector & mat_maj) +{ + for (int i_energy = 0; i_energy < to_grid.size(); ++i_energy) { + mat_maj[i_energy] = 0.0; + const double union_log_energy = to_grid[i_energy]; + + for (int i = 0; i < mat.nuclide_.size(); ++i) { + const int i_element = mat.element_[i]; + + mat_maj[i_energy] += calculate_elem_tot_xs(union_log_energy, *data::elements[i_element]) * mat.atom_density(i); + } + mat_maj[i_energy] = std::log(mat_maj[i_energy]); + } +} + +double +PhotonMajorant::calculate_elem_tot_xs(double log_energy, const PhotonInteraction & elem) const +{ + int i_grid = get_i_grid(log_energy, elem.energy_); + + // calculate interpolation factor + double f = + (log_energy - elem.energy_(i_grid)) / (elem.energy_(i_grid + 1) - elem.energy_(i_grid)); + + // Calculate microscopic coherent cross section + double coherent = std::exp( + elem.coherent_(i_grid) + f * (elem.coherent_(i_grid + 1) - elem.coherent_(i_grid))); + + // Calculate microscopic incoherent cross section + double incoherent = std::exp( + elem.incoherent_(i_grid) + f * (elem.incoherent_(i_grid + 1) - elem.incoherent_(i_grid))); + + // Calculate microscopic photoelectric cross section + double photoelectric = 0.0; + tensor::View xs_lower = elem.cross_sections_.slice(i_grid); + tensor::View xs_upper = elem.cross_sections_.slice(i_grid + 1); + + for (int i = 0; i < xs_upper.size(); ++i) + if (xs_lower(i) != 0) + photoelectric += + std::exp(xs_lower(i) + f * (xs_upper(i) - xs_lower(i))); + + // Calculate microscopic pair production cross section + double pair_production = std::exp( + elem.pair_production_total_(i_grid) + + f * (elem.pair_production_total_(i_grid + 1) - elem.pair_production_total_(i_grid))); + + // Calculate microscopic total cross section + double total = + coherent + incoherent + photoelectric + pair_production; + return total; +} + +int +PhotonMajorant::get_i_grid(double log_energy, const std::vector & energy_grid) const +{ + int n_grid = energy_grid.size(); + int i_grid; + if (log_energy <= energy_grid[0]) { + i_grid = 0; + } else if (log_energy > energy_grid[n_grid - 1]) { + i_grid = n_grid - 2; + } else { + // We use upper_bound_index here because sometimes photons are created with + // energies that exactly match a grid point + i_grid = upper_bound_index(energy_grid.cbegin(), energy_grid.cend(), log_energy); + } + + // check for case where two energy points are the same + if (energy_grid[i_grid] == energy_grid[i_grid + 1]) + ++i_grid; + + return i_grid; +} + +int +PhotonMajorant::get_i_grid(double log_energy, const tensor::Tensor & energy_grid) const +{ + int n_grid = energy_grid.size(); + int i_grid; + if (log_energy <= energy_grid[0]) { + i_grid = 0; + } else if (log_energy > energy_grid(n_grid - 1)) { + i_grid = n_grid - 2; + } else { + // We use upper_bound_index here because sometimes photons are created with + // energies that exactly match a grid point + i_grid = upper_bound_index(energy_grid.cbegin(), energy_grid.cend(), log_energy); + } + + // check for case where two energy points are the same + if (energy_grid(i_grid) == energy_grid(i_grid + 1)) + ++i_grid; + + return i_grid; +} + //============================================================================== // Static functions //============================================================================== void create_majorants() { - if (data::majorant_file != "") { - write_message("Loading majorant from " + data::majorant_file); - // We can load the majorant from a file instead. - data::n_majorant = std::make_unique(data::majorant_file); - return; + if (data::n_majorant_file != "") { + write_message("Loading neutron majorant from " + data::n_majorant_file); + data::n_majorant = std::make_unique(data::n_majorant_file); + } + if (settings::photon_transport && data::p_majorant_file != "") { + write_message("Loading photon majorant from " + data::p_majorant_file); + data::p_majorant = std::make_unique(data::p_majorant_file); } - write_message("Creating majorant cross section..."); - data::n_majorant = std::make_unique(); - data::n_majorant->init(); - data::n_majorant->write_ascii("macro_majorant.txt"); + if (data::n_majorant_file == "") { + write_message("Creating a neutron majorant cross section..."); + data::n_majorant = std::make_unique(); + data::n_majorant->compute_unionized_grid(); + data::n_majorant->compute_majorant(); + data::n_majorant->write_ascii("neutron_majorant.txt"); + } + + if (settings::photon_transport && data::p_majorant_file == "") { + write_message("Creating a photon majorant cross section..."); + data::p_majorant = std::make_unique(); + data::p_majorant->compute_unionized_grid(); + data::p_majorant->compute_majorant(); + data::p_majorant->write_ascii("photon_majorant.txt"); + } } } // namespace openmc diff --git a/src/particle.cpp b/src/particle.cpp index 8c68f6ebcb4..b47d0ad0712 100644 --- a/src/particle.cpp +++ b/src/particle.cpp @@ -347,7 +347,7 @@ void Particle::event_delta_advance() distance = -std::log(prn(current_seed())) / majorant(); } - while (distance >= 0 && alive()) { + while (distance > 0.0 && alive()) { // update distance to problem boundary boundary().distance() = INFTY; boundary().surface() = 0; @@ -935,7 +935,11 @@ void Particle::cross_periodic_bc( void Particle::update_majorant() { - this->majorant() = NeutronMajorant::safety_factor * data::n_majorant->calculate_neutron_xs(this->E()); + if (type().is_neutron()) { + majorant() = NeutronMajorant::safety_factor_ * data::n_majorant->calculate_neutron_xs(E()); + } else if (type().is_photon()) { + majorant() = PhotonMajorant::safety_factor_ * data::p_majorant->calculate_photon_xs(E()); + } } void Particle::mark_as_lost(const char* message) diff --git a/src/settings.cpp b/src/settings.cpp index 90dde938136..79c8895a3de 100644 --- a/src/settings.cpp +++ b/src/settings.cpp @@ -1236,8 +1236,12 @@ void read_settings_xml(pugi::xml_node root) delta_tracking = get_node_value_bool(root, "delta_tracking"); } - if (check_for_node(root, "delta_tracking_majorant_file")) { - data::majorant_file = get_node_value(root, "delta_tracking_majorant_file"); + if (check_for_node(root, "delta_tracking_n_majorant_file")) { + data::n_majorant_file = get_node_value(root, "delta_tracking_n_majorant_file"); + } + + if (check_for_node(root, "delta_tracking_p_majorant_file")) { + data::p_majorant_file = get_node_value(root, "delta_tracking_p_majorant_file"); } // Check whether material cell offsets should be generated diff --git a/src/simulation.cpp b/src/simulation.cpp index 565557b2997..88120df2c8e 100644 --- a/src/simulation.cpp +++ b/src/simulation.cpp @@ -920,24 +920,34 @@ void transport_delta_tracking_single_particle(Particle& p) p.event_calculate_xs(); while (true) { p.event_delta_advance(); - if (!p.alive()) - break; - p.event_calculate_xs(); - if (p.macro_xs().total / p.majorant() > 1.0) { - p.mark_as_lost( - fmt::format("Ratio of the total cross section ({}) to the majorant " - "cross section ({}) for particle {} with energy {} is " - "greater than unity!", - p.macro_xs().total, p.majorant(), p.id(), p.E())); + if (!p.alive()) { break; } - if (prn(p.current_seed()) < (p.macro_xs().total / p.majorant())) { + + if (p.type() == ParticleType::electron() || p.type() == ParticleType::positron()) { + // Electrons and positrons collide in-place, no need to rejection sample. p.event_collide(); + } else { + // Rejection sample the total to majorant ratio for photons and neutrons. + p.event_calculate_xs(); + if (p.macro_xs().total / p.majorant() > 1.0) { + p.mark_as_lost( + fmt::format("Ratio of the total cross section ({}) to the majorant " + "cross section ({}) for particle {} ({}) with energy {} is " + "greater than unity!", + p.macro_xs().total, p.majorant(), p.id(), p.type().str(), p.E())); + break; + } + if (prn(p.current_seed()) < (p.macro_xs().total / p.majorant())) { + p.event_collide(); + } } p.event_check_limit_and_revive(); - if (!p.alive()) + if (!p.alive()) { break; + } } + p.event_death(); } From fc4d6042703ce96feecfffefe4cbed4286e847d3 Mon Sep 17 00:00:00 2001 From: nuclearkevin <66632997+nuclearkevin@users.noreply.github.com> Date: Thu, 28 May 2026 18:59:53 -0500 Subject: [PATCH 13/73] Errors for settings unsupported by delta tracking (for now). --- src/material.cpp | 3 +++ src/settings.cpp | 4 ++++ 2 files changed, 7 insertions(+) diff --git a/src/material.cpp b/src/material.cpp index 21b11b8b9ce..6ae3f786ce7 100644 --- a/src/material.cpp +++ b/src/material.cpp @@ -60,6 +60,9 @@ Material::Material(pugi::xml_node node) } if (check_for_node(node, "cfg")) { + if (settings::delta_tracking) { + fatal_error("NCrystal materials are presently not supported when running with delta tracking!"); + } auto cfg = get_node_value(node, "cfg"); write_message( 5, "NCrystal config string for material #{}: '{}'", this->id(), cfg); diff --git a/src/settings.cpp b/src/settings.cpp index 79c8895a3de..11e34f60c5e 100644 --- a/src/settings.cpp +++ b/src/settings.cpp @@ -1234,6 +1234,10 @@ void read_settings_xml(pugi::xml_node root) // Check whether or not to use delta tracking if (check_for_node(root, "delta_tracking")) { delta_tracking = get_node_value_bool(root, "delta_tracking"); + + if (temperature_multipole && delta_tracking) { + fatal_error("Delta tracking cannot be used with a windowed multipole temperature treatment."); + } } if (check_for_node(root, "delta_tracking_n_majorant_file")) { From 69dd41221d5591b34c1e07ad5e67d11e1a931ac3 Mon Sep 17 00:00:00 2001 From: nuclearkevin <66632997+nuclearkevin@users.noreply.github.com> Date: Fri, 29 May 2026 11:23:01 -0500 Subject: [PATCH 14/73] Compute majorants for all materials contained in a universe. --- include/openmc/majorant.h | 41 ++++++++++---- src/majorant.cpp | 112 ++++++++++++++++++++++++++++---------- 2 files changed, 114 insertions(+), 39 deletions(-) diff --git a/include/openmc/majorant.h b/include/openmc/majorant.h index 7a23b25e300..163427213dd 100644 --- a/include/openmc/majorant.h +++ b/include/openmc/majorant.h @@ -34,7 +34,7 @@ class Majorant { //---------------------------------------------------------------------------- // Constructors - Majorant() = default; + Majorant(int i_universe); Majorant(const std::string & majorant_file, int p_transport_indx); //---------------------------------------------------------------------------- @@ -44,7 +44,7 @@ class Majorant { virtual void compute_unionized_grid() = 0; //! \brief Populate the majorant cross section. - virtual void compute_majorant(); + void compute_majorant(); //! \brief Write the majorant cross section to a CSV file for visualization // TODO: remove this when done prototyping. @@ -56,6 +56,10 @@ class Majorant { //---------------------------------------------------------------------------- // Protected Methods + //! \brief Find materials in maj_universe_ by traversing the geometry tree. + // + void discover_contained_materials(); + //! \brief Compute a per-material macroscopic majorant cross section in units of [cm^-1] // //! \param[in] mat The material to compute the majorant cross section of @@ -69,17 +73,34 @@ class Majorant { //! \param[in] x_1 The second x coordinate //! \param[in] y_0 The y coordinate associated with x_0 //! \param[in] y_1 The y coordinate associated with x_1 - //! \param[in] x A x point between x_0 and x_1 to find a y value at - double interpolate_lin_1D(double x_0, double x_1, double y_0, double y_1, double x) const; + //! \param[in] x The point between x_0 and x_1 to find a y value at + inline double interpolate_lin_1D(double x_0, double x_1, double y_0, double y_1, double x) const + { + const double f = (x - x_0) / (x_1 - x_0); + return (1.0 - f) * y_0 + f * y_1; + } //! \brief Helper function to perform log interpolation. - double interpolate_log_1D(double x_0, double x_1, double y_0, double y_1, double x) const; + // + //! \param[in] x_0 The first x coordinate + //! \param[in] x_1 The second x coordinate + //! \param[in] y_0 The y coordinate associated with x_0 + //! \param[in] y_1 The y coordinate associated with x_1 + //! \param[in] x The point between x_0 and x_1 to find a y value at + inline double interpolate_log_1D(double x_0, double x_1, double y_0, double y_1, double x) const + { + const double f = std::log(x / x_0) / std::log(x_1 / x_0); + return std::exp((1.0 - f) * std::log(y_0) + f * std::log(y_1)); + } //---------------------------------------------------------------------------- // Protected data members - Nuclide::EnergyGrid grid_; //!< The unionized energy grid - std::vector xs_; //!< Macroscopic majorant cross sections at each grid point in grid_ + int maj_universe_ = C_NONE; //!< Index into the universe array for the universe which this + // majorant uses to fetch material properties. + std::vector contained_materials_; //!< A vector of materials contained in maj_universe_ + Nuclide::EnergyGrid grid_; //!< The unionized energy grid + std::vector xs_; //!< Macroscopic majorant cross sections at each grid point in grid_ }; // class Majorant //============================================================================== @@ -90,7 +111,7 @@ class NeutronMajorant : public Majorant { //---------------------------------------------------------------------------- // Constructors - NeutronMajorant() = default; + NeutronMajorant(int i_universe); NeutronMajorant(const std::string & majorant_file); //---------------------------------------------------------------------------- @@ -155,7 +176,7 @@ class NeutronMajorant : public Majorant { //---------------------------------------------------------------------------- // Private data members - static constexpr int i_neutron = ParticleType::neutron().transport_index(); + static constexpr int i_neutron_ = ParticleType::neutron().transport_index(); }; // class NeutronMajorant //============================================================================== @@ -165,7 +186,7 @@ class PhotonMajorant : public Majorant { //---------------------------------------------------------------------------- // Constructors - PhotonMajorant() = default; + PhotonMajorant(int i_universe); PhotonMajorant(const std::string & majorant_file); //---------------------------------------------------------------------------- diff --git a/src/majorant.cpp b/src/majorant.cpp index 4beaf45564c..941f47fbf76 100644 --- a/src/majorant.cpp +++ b/src/majorant.cpp @@ -3,6 +3,7 @@ #include #include "openmc/constants.h" +#include "openmc/geometry.h" #include "openmc/majorant.h" #include "openmc/material.h" #include "openmc/nuclide.h" @@ -10,6 +11,7 @@ #include "openmc/simulation.h" #include "openmc/settings.h" #include "openmc/thermal.h" +#include "openmc/universe.h" namespace openmc { @@ -29,6 +31,12 @@ std::string p_majorant_file; // Majorant implementation //============================================================================== +Majorant::Majorant(int i_universe) + : maj_universe_(i_universe) +{ + discover_contained_materials(); +} + Majorant::Majorant(const std::string & majorant_file, int p_transport_indx) { std::ifstream majorant_data(majorant_file); @@ -82,49 +90,76 @@ Majorant::write_ascii(const std::string& filename) const of.close(); } -double -Majorant::interpolate_lin_1D(double x_0, double x_1, double y_0, double y_1, double x) const +void +Majorant::discover_contained_materials() { - double f = (x - x_0) / (x_1 - x_0); - return (1.0 - f) * y_0 + f * y_1; -} + std::set unique_materials; + if (maj_universe_ == C_NONE || maj_universe_ >= model::universes.size()) { + fatal_error( fmt::format("Invalid majorant universe: {}", maj_universe_)); + } -double -Majorant::interpolate_log_1D(double x_0, double x_1, double y_0, double y_1, double x) const -{ - double f = std::log(x / x_0) / std::log(x_1 / x_0); - return std::exp((1.0 - f) * std::log(y_0) + f * std::log(y_1)); + const auto & maj_uni = model::universes[maj_universe_]; + for (int i_cell : maj_uni->cells_) { + const auto & cell = model::cells[i_cell]; + if (cell->type_ == Fill::MATERIAL) { + for (auto i_mat : cell->material_) { + unique_materials.emplace(i_mat); + } + } else { + const auto contained_cells = cell->get_contained_cells(); + for (const auto & [i_contained_cell, contained_instances] : contained_cells) { + const auto & contained_cell = model::cells[i_contained_cell]; + for (auto i_mat : contained_cell->material_) { + unique_materials.emplace(i_mat); + } + } + } + } + + contained_materials_.clear(); + for (auto i_mat : unique_materials) { + contained_materials_.push_back(i_mat); + } } //============================================================================== // NeutronMajorant implementation //============================================================================== +NeutronMajorant::NeutronMajorant(int i_universe) + : Majorant(i_universe) +{ } + NeutronMajorant::NeutronMajorant(const std::string & majorant_file) - : Majorant(majorant_file, i_neutron) + : Majorant(majorant_file, i_neutron_) { } double NeutronMajorant::calculate_neutron_xs(double energy) const { - int i_grid = get_i_grid(energy, grid_); - - // calculate interpolation factor - double f = (energy - grid_.energy[i_grid]) / - (grid_.energy[i_grid + 1]- grid_.energy[i_grid]); - - return (1.0 - f) * xs_[i_grid] + f * xs_[i_grid + 1]; + const int i_grid = get_i_grid(energy, grid_); + return interpolate_lin_1D(grid_.energy[i_grid], grid_.energy[i_grid + 1], xs_[i_grid], xs_[i_grid + 1], energy); } void NeutronMajorant::compute_unionized_grid() { + // In the event the majorant needs to be re-generated (e.g. in-memory for + // multiphysics), we need to reset the unionized grid. + grid_.energy.clear(); + // This function generates a unionized cross section grid between smooth cross // sections and URR probability table grids. - for (const auto & mat : model::materials) { - for (auto nuclide_idx : mat->nuclide_) { - const auto & nuclide = data::nuclides[nuclide_idx]; + std::set processed_nuclides; + for (int i_mat : contained_materials_) { + const auto & mat = model::materials[i_mat]; + for (auto i_nuclide : mat->nuclide_) { + // Only unionize nuclides we haven't checked yet. + if (processed_nuclides.count(i_nuclide) > 0) { + continue; + } + const auto & nuclide = data::nuclides[i_nuclide]; // ====================================================================== // Unionizing the URR temperature grid. if (nuclide->urr_present_ && settings::urr_ptables_on) { @@ -138,6 +173,8 @@ NeutronMajorant::compute_unionized_grid() for (const auto & n_grid : nuclide->grid_) { grid_.energy.insert(grid_.energy.end(), n_grid.energy.begin(), n_grid.energy.end()); } + + processed_nuclides.insert(i_nuclide); } } std::sort(grid_.energy.begin(), grid_.energy.end()); @@ -146,17 +183,17 @@ NeutronMajorant::compute_unionized_grid() // remove all values below the minimum neutron energy auto min_it = grid_.energy.begin(); - while (*min_it < data::energy_min[i_neutron]) { min_it++; } + while (*min_it < data::energy_min[i_neutron_]) { min_it++; } grid_.energy.erase(grid_.energy.begin(), min_it + 1); // insert the minimum neutron energy at the beginning - grid_.energy.insert(grid_.energy.begin(), data::energy_min[i_neutron]); + grid_.energy.insert(grid_.energy.begin(), data::energy_min[i_neutron_]); // remove all values above the maximum neutron energy auto max_it = --grid_.energy.end(); - while (*max_it > data::energy_max[i_neutron]) { max_it--; } + while (*max_it > data::energy_max[i_neutron_]) { max_it--; } grid_.energy.erase(max_it - 1, grid_.energy.end()); // insert the maximum neutron energy at the end - grid_.energy.insert(grid_.energy.end(), data::energy_max[i_neutron]); + grid_.energy.insert(grid_.energy.end(), data::energy_max[i_neutron_]); // Initialize the grid for fast lookups. grid_.init(); @@ -346,7 +383,7 @@ int NeutronMajorant::get_i_grid(double energy, const Nuclide::EnergyGrid & grid) const { // Find energy index on energy grid - int i_log_union = std::log(energy * data::energy_min_rcp[i_neutron]) * simulation::log_spacing_rcp; + int i_log_union = std::log(energy * data::energy_min_rcp[i_neutron_]) * simulation::log_spacing_rcp; int i_grid; if (i_log_union < 0) { @@ -373,6 +410,10 @@ NeutronMajorant::get_i_grid(double energy, const Nuclide::EnergyGrid & grid) con // PhotonMajorant implementation //============================================================================== +PhotonMajorant::PhotonMajorant(int i_universe) + : Majorant(i_universe) +{ } + PhotonMajorant::PhotonMajorant(const std::string & majorant_file) : Majorant(majorant_file, i_photon_) { } @@ -380,11 +421,24 @@ PhotonMajorant::PhotonMajorant(const std::string & majorant_file) void PhotonMajorant::compute_unionized_grid() { + // In the event the majorant needs to be re-generated (e.g. in-memory for + // multiphysics), we need to reset the unionized grid. + grid_.energy.clear(); + // This function generates a unionized cross section grid for all elements. - for (const auto & mat : model::materials) { + std::set processed_elements; + for (int i_mat : contained_materials_) { + const auto & mat = model::materials[i_mat]; for (int i = 0; i < mat->nuclide_.size(); ++i) { + // Only unionize elements we haven't checked yet. + if (processed_elements.count(mat->element_[i]) > 0) { + continue; + } + const auto & element = data::elements[mat->element_[i]]; grid_.energy.insert(grid_.energy.end(), element->energy_.begin(), element->energy_.end()); + + processed_elements.insert(mat->element_[i]); } } std::sort(grid_.energy.begin(), grid_.energy.end()); @@ -535,7 +589,7 @@ void create_majorants() if (data::n_majorant_file == "") { write_message("Creating a neutron majorant cross section..."); - data::n_majorant = std::make_unique(); + data::n_majorant = std::make_unique(model::root_universe); data::n_majorant->compute_unionized_grid(); data::n_majorant->compute_majorant(); data::n_majorant->write_ascii("neutron_majorant.txt"); @@ -543,7 +597,7 @@ void create_majorants() if (settings::photon_transport && data::p_majorant_file == "") { write_message("Creating a photon majorant cross section..."); - data::p_majorant = std::make_unique(); + data::p_majorant = std::make_unique(model::root_universe); data::p_majorant->compute_unionized_grid(); data::p_majorant->compute_majorant(); data::p_majorant->write_ascii("photon_majorant.txt"); From 033d9766606abba8da87065f8bb2bb0df69cf10d Mon Sep 17 00:00:00 2001 From: nuclearkevin <66632997+nuclearkevin@users.noreply.github.com> Date: Fri, 29 May 2026 11:23:10 -0500 Subject: [PATCH 15/73] Fix delta tracking photon loop. --- src/simulation.cpp | 41 +++++++++++++++++++---------------------- 1 file changed, 19 insertions(+), 22 deletions(-) diff --git a/src/simulation.cpp b/src/simulation.cpp index 88120df2c8e..8bb7b393747 100644 --- a/src/simulation.cpp +++ b/src/simulation.cpp @@ -918,34 +918,31 @@ void transport_delta_tracking_single_particle(Particle& p) { p.delta_tracking() = true; p.event_calculate_xs(); - while (true) { + while (p.alive()) { p.event_delta_advance(); - if (!p.alive()) { - break; - } - if (p.type() == ParticleType::electron() || p.type() == ParticleType::positron()) { - // Electrons and positrons collide in-place, no need to rejection sample. - p.event_collide(); - } else { - // Rejection sample the total to majorant ratio for photons and neutrons. - p.event_calculate_xs(); - if (p.macro_xs().total / p.majorant() > 1.0) { - p.mark_as_lost( - fmt::format("Ratio of the total cross section ({}) to the majorant " - "cross section ({}) for particle {} ({}) with energy {} is " - "greater than unity!", - p.macro_xs().total, p.majorant(), p.id(), p.type().str(), p.E())); - break; - } - if (prn(p.current_seed()) < (p.macro_xs().total / p.majorant())) { + if (p.alive()) { + if (p.type() == ParticleType::electron() || p.type() == ParticleType::positron()) { + // Electrons and positrons collide in-place, no need to rejection sample. p.event_collide(); + } else { + // Rejection sample the total to majorant ratio for photons and neutrons. + p.event_calculate_xs(); + if (p.macro_xs().total / p.majorant() > 1.0) { + p.mark_as_lost( + fmt::format("Ratio of the total cross section ({}) to the majorant " + "cross section ({}) for particle {} ({}) with energy {} is " + "greater than unity!", + p.macro_xs().total, p.majorant(), p.id(), p.type().str(), p.E())); + break; + } + if (prn(p.current_seed()) < (p.macro_xs().total / p.majorant())) { + p.event_collide(); + } } } + p.event_check_limit_and_revive(); - if (!p.alive()) { - break; - } } p.event_death(); From eb521189058172db69e84f42795b695ee277de96 Mon Sep 17 00:00:00 2001 From: nuclearkevin <66632997+nuclearkevin@users.noreply.github.com> Date: Fri, 29 May 2026 11:43:02 -0500 Subject: [PATCH 16/73] Minor changes. --- src/majorant.cpp | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/majorant.cpp b/src/majorant.cpp index 941f47fbf76..c105258aff7 100644 --- a/src/majorant.cpp +++ b/src/majorant.cpp @@ -588,7 +588,7 @@ void create_majorants() } if (data::n_majorant_file == "") { - write_message("Creating a neutron majorant cross section..."); + write_message("Creating a neutron majorant cross section"); data::n_majorant = std::make_unique(model::root_universe); data::n_majorant->compute_unionized_grid(); data::n_majorant->compute_majorant(); @@ -596,7 +596,7 @@ void create_majorants() } if (settings::photon_transport && data::p_majorant_file == "") { - write_message("Creating a photon majorant cross section..."); + write_message("Creating a photon majorant cross section"); data::p_majorant = std::make_unique(model::root_universe); data::p_majorant->compute_unionized_grid(); data::p_majorant->compute_majorant(); From 48fd58ea2325a4c20c92799d92077a47cd952d6e Mon Sep 17 00:00:00 2001 From: nuclearkevin <66632997+nuclearkevin@users.noreply.github.com> Date: Sat, 30 May 2026 12:36:47 -0500 Subject: [PATCH 17/73] Fix bug with universe majorants & reset majorants when finalizing a simulation. --- include/openmc/majorant.h | 12 +++--- src/majorant.cpp | 85 +++++++++++++++++++-------------------- src/simulation.cpp | 3 ++ 3 files changed, 51 insertions(+), 49 deletions(-) diff --git a/include/openmc/majorant.h b/include/openmc/majorant.h index 163427213dd..1dfbc1b0015 100644 --- a/include/openmc/majorant.h +++ b/include/openmc/majorant.h @@ -6,9 +6,8 @@ #include -#include "openmc/settings.h" -#include "openmc/nuclide.h" #include "openmc/material.h" +#include "openmc/nuclide.h" #include "openmc/photon.h" namespace openmc { @@ -239,11 +238,14 @@ class PhotonMajorant : public Majorant { }; // class PhotonMajorant //============================================================================== -// Non-member functions +// Static functions //============================================================================== -//! \brief A function to create the global majorant cross section(s). +//! \brief A function to create majorant cross sections. void create_majorants(); -} + +//! \brief A function to reset majorant cross sectiosn. +void reset_majorants(); +} // namespace openmc #endif // OPENMC_MAJORANT_H diff --git a/src/majorant.cpp b/src/majorant.cpp index c105258aff7..753d07de234 100644 --- a/src/majorant.cpp +++ b/src/majorant.cpp @@ -2,6 +2,7 @@ #include +#include "openmc/capi.h" #include "openmc/constants.h" #include "openmc/geometry.h" #include "openmc/majorant.h" @@ -60,17 +61,16 @@ Majorant::Majorant(const std::string & majorant_file, int p_transport_indx) } } -void -Majorant::compute_majorant() +void Majorant::compute_majorant() { // Fill with zeros. xs_.resize(grid_.energy.size(), 0.0); std::vector material_maj_xs; material_maj_xs.resize(grid_.energy.size(), 0.0); - for (const auto & mat : model::materials) { + for (int i_material : contained_materials_) { // Populate the per-material majorant cross section. - fill_material_maj_xs(*mat, grid_.energy, material_maj_xs); + fill_material_maj_xs(*model::materials[i_material], grid_.energy, material_maj_xs); // Compute the full majorant by taking the max over each material cross section. for (int i_energy = 0; i_energy < xs_.size(); ++i_energy) { @@ -80,8 +80,7 @@ Majorant::compute_majorant() } } -void -Majorant::write_ascii(const std::string& filename) const +void Majorant::write_ascii(const std::string& filename) const { std::ofstream of(filename); for (int i = 0; i < xs_.size(); i++) { @@ -90,8 +89,7 @@ Majorant::write_ascii(const std::string& filename) const of.close(); } -void -Majorant::discover_contained_materials() +void Majorant::discover_contained_materials() { std::set unique_materials; if (maj_universe_ == C_NONE || maj_universe_ >= model::universes.size()) { @@ -134,15 +132,13 @@ NeutronMajorant::NeutronMajorant(const std::string & majorant_file) : Majorant(majorant_file, i_neutron_) { } -double -NeutronMajorant::calculate_neutron_xs(double energy) const +double NeutronMajorant::calculate_neutron_xs(double energy) const { const int i_grid = get_i_grid(energy, grid_); return interpolate_lin_1D(grid_.energy[i_grid], grid_.energy[i_grid + 1], xs_[i_grid], xs_[i_grid + 1], energy); } -void -NeutronMajorant::compute_unionized_grid() +void NeutronMajorant::compute_unionized_grid() { // In the event the majorant needs to be re-generated (e.g. in-memory for // multiphysics), we need to reset the unionized grid. @@ -199,8 +195,7 @@ NeutronMajorant::compute_unionized_grid() grid_.init(); } -void -NeutronMajorant::fill_material_maj_xs(const Material & mat, const std::vector & to_grid, std::vector & mat_maj) +void NeutronMajorant::fill_material_maj_xs(const Material & mat, const std::vector & to_grid, std::vector & mat_maj) { for (int i_energy = 0; i_energy < to_grid.size(); ++i_energy) { mat_maj[i_energy] = 0.0; @@ -267,8 +262,7 @@ NeutronMajorant::fill_material_maj_xs(const Material & mat, const std::vector & to_grid, std::vector & mat_maj) +void PhotonMajorant::fill_material_maj_xs(const Material & mat, const std::vector & to_grid, std::vector & mat_maj) { for (int i_energy = 0; i_energy < to_grid.size(); ++i_energy) { mat_maj[i_energy] = 0.0; @@ -490,8 +478,7 @@ PhotonMajorant::fill_material_maj_xs(const Material & mat, const std::vector & energy_grid) const +int PhotonMajorant::get_i_grid(double log_energy, const std::vector & energy_grid) const { int n_grid = energy_grid.size(); int i_grid; @@ -550,8 +536,7 @@ PhotonMajorant::get_i_grid(double log_energy, const std::vector & energy return i_grid; } -int -PhotonMajorant::get_i_grid(double log_energy, const tensor::Tensor & energy_grid) const +int PhotonMajorant::get_i_grid(double log_energy, const tensor::Tensor & energy_grid) const { int n_grid = energy_grid.size(); int i_grid; @@ -572,19 +557,25 @@ PhotonMajorant::get_i_grid(double log_energy, const tensor::Tensor & ene return i_grid; } -//============================================================================== -// Static functions -//============================================================================== - +//! Create/load a majorant cross section for photons or neutrons. Errors if they +// exist already. void create_majorants() { - if (data::n_majorant_file != "") { - write_message("Loading neutron majorant from " + data::n_majorant_file); - data::n_majorant = std::make_unique(data::n_majorant_file); + try { + if (data::n_majorant_file != "") { + write_message("Loading neutron majorant from " + data::n_majorant_file); + data::n_majorant = std::make_unique(data::n_majorant_file); + } + } catch (const std::exception& e) { + fatal_error(fmt::format("Failed to load neutron majorant with error: {}", e.what()).c_str()); } - if (settings::photon_transport && data::p_majorant_file != "") { - write_message("Loading photon majorant from " + data::p_majorant_file); - data::p_majorant = std::make_unique(data::p_majorant_file); + try { + if (settings::photon_transport && data::p_majorant_file != "") { + write_message("Loading photon majorant from " + data::p_majorant_file); + data::p_majorant = std::make_unique(data::p_majorant_file); + } + } catch (const std::exception& e) { + fatal_error(fmt::format("Failed to load photon majorant with error: {}", e.what()).c_str()); } if (data::n_majorant_file == "") { @@ -604,4 +595,10 @@ void create_majorants() } } +//! Reset the photon and neutron majorant cross sections. +void reset_majorants() +{ + openmc::data::n_majorant.reset(nullptr); + openmc::data::p_majorant.reset(nullptr); +} } // namespace openmc diff --git a/src/simulation.cpp b/src/simulation.cpp index 8bb7b393747..8b97307920d 100644 --- a/src/simulation.cpp +++ b/src/simulation.cpp @@ -245,6 +245,9 @@ int openmc_simulation_finalize() if (settings::check_overlaps) print_overlap_check(); + // Clear majorants as they could change if OpenMC is run again. + reset_majorants(); + // Reset flags simulation::initialized = false; return 0; From 95157ac3f4aee657714f63646cbc1dd9a538f409 Mon Sep 17 00:00:00 2001 From: nuclearkevin <66632997+nuclearkevin@users.noreply.github.com> Date: Sat, 30 May 2026 13:06:36 -0500 Subject: [PATCH 18/73] Support distrib densities. --- include/openmc/majorant.h | 28 +++---- src/majorant.cpp | 155 +++++++++++++++++--------------------- 2 files changed, 82 insertions(+), 101 deletions(-) diff --git a/include/openmc/majorant.h b/include/openmc/majorant.h index 1dfbc1b0015..eff6ef6884a 100644 --- a/include/openmc/majorant.h +++ b/include/openmc/majorant.h @@ -31,10 +31,9 @@ namespace data { class Majorant { public: //---------------------------------------------------------------------------- - // Constructors + // Constructor Majorant(int i_universe); - Majorant(const std::string & majorant_file, int p_transport_indx); //---------------------------------------------------------------------------- // Methods @@ -55,16 +54,13 @@ class Majorant { //---------------------------------------------------------------------------- // Protected Methods - //! \brief Find materials in maj_universe_ by traversing the geometry tree. - // - void discover_contained_materials(); - //! \brief Compute a per-material macroscopic majorant cross section in units of [cm^-1] // //! \param[in] mat The material to compute the majorant cross section of //! \param[in] to_grid The grid points to evaluate the majorant at in [eV] //! \param[out] mat_maj The array to write the macroscopic majorant to. The resulting cross section has units of [cm^-1] - virtual void fill_material_maj_xs(const Material & mat, const std::vector & to_grid, std::vector & mat_maj) = 0; + virtual void fill_material_maj_xs(const Material & mat, double max_density_mult, + const std::vector & to_grid, std::vector & mat_maj) = 0; //! \brief Helper function to perform linear interpolation. // @@ -95,11 +91,13 @@ class Majorant { //---------------------------------------------------------------------------- // Protected data members - int maj_universe_ = C_NONE; //!< Index into the universe array for the universe which this - // majorant uses to fetch material properties. - std::vector contained_materials_; //!< A vector of materials contained in maj_universe_ - Nuclide::EnergyGrid grid_; //!< The unionized energy grid - std::vector xs_; //!< Macroscopic majorant cross sections at each grid point in grid_ + int maj_universe_ = C_NONE; //!< Index into the universe array for the universe which this + // majorant uses to fetch material properties. + std::vector contained_materials_; //!< A vector of materials contained in maj_universe_ + std::unordered_map max_density_mult_; //!< A map of each material index and the corresponding + // maximum density multiplier applied to that material by a cell + Nuclide::EnergyGrid grid_; //!< The unionized energy grid + std::vector xs_; //!< Macroscopic majorant cross sections at each grid point in grid_ }; // class Majorant //============================================================================== @@ -139,7 +137,8 @@ class NeutronMajorant : public Majorant { //! \param[in] mat The material to compute the majorant cross section of //! \param[in] to_grid The grid points to evaluate the majorant at in [eV] //! \param[out] mat_maj The array to write the macroscopic majorant to. The resulting cross section has units of [cm^-1] - virtual void fill_material_maj_xs(const Material & mat, const std::vector & to_grid, std::vector & mat_maj) override; + virtual void fill_material_maj_xs(const Material & mat, double max_density_mult, + const std::vector & to_grid, std::vector & mat_maj) override; private: //---------------------------------------------------------------------------- @@ -212,7 +211,8 @@ class PhotonMajorant : public Majorant { //! \param[in] mat The material to compute the majorant cross section of //! \param[in] to_grid The grid points to evaluate the majorant at in [eV] //! \param[out] mat_maj The array to write the macroscopic majorant to. The resulting cross section has units of [cm^-1] - virtual void fill_material_maj_xs(const Material & mat, const std::vector & to_grid, std::vector & mat_maj) override; + virtual void fill_material_maj_xs(const Material & mat, double max_density_mult, + const std::vector & to_grid, std::vector & mat_maj) override; private: //---------------------------------------------------------------------------- diff --git a/src/majorant.cpp b/src/majorant.cpp index 753d07de234..ddb849361a2 100644 --- a/src/majorant.cpp +++ b/src/majorant.cpp @@ -35,29 +35,65 @@ std::string p_majorant_file; Majorant::Majorant(int i_universe) : maj_universe_(i_universe) { - discover_contained_materials(); -} + // Find all materials contained in the majorant's universe. This also obtains + // the maximum density multiplier applied to that material. + std::set unique_materials; + if (maj_universe_ == C_NONE || maj_universe_ >= model::universes.size()) { + fatal_error( fmt::format("Invalid majorant universe: {}", maj_universe_)); + } -Majorant::Majorant(const std::string & majorant_file, int p_transport_indx) -{ - std::ifstream majorant_data(majorant_file); + const auto & maj_uni = model::universes[maj_universe_]; + for (int i_cell : maj_uni->cells_) { + const auto & cell = model::cells[i_cell]; - std::string line; - while (std::getline(majorant_data, line)) { - auto delim_pos = line.find(","); - auto energy = std::stod(line.substr(0, delim_pos)); - if (energy < data::energy_min[p_transport_indx]) { - continue; - } - if (energy > data::energy_max[p_transport_indx]) { - break; + // If the cell is filled with a material, it won't have any sub-cells. + if (cell->type_ == Fill::MATERIAL) { + // Loop over instances. TODO: confirm if this is unecessary and use 0 instead? + for (int instance = 0; instance < cell->n_instances(); ++instance) { + int i_material = cell->material(instance); + + // Check to see if we've found the contained material yet. If not, add to the set + // of materials discovered and add to the map of density multipliers. + if (unique_materials.count(i_material) == 0) { + unique_materials.emplace(i_material); + max_density_mult_[i_material] = cell->density_mult(instance); + } else { + // We've found this material already. Need to take the maximum density multiplier. + max_density_mult_.at(i_material) = + std::max(max_density_mult_.at(i_material), + cell->density_mult(instance)); + } + } + } else { + // This cell is filled with a universe or lattice. Need to get the list of cells and + // cell instances. + const auto contained_cells = cell->get_contained_cells(); + for (const auto & [i_con_cell, contained_instances] : contained_cells) { + const auto & contained_cell = model::cells[i_con_cell]; + + // Loop over contained cell instances. + for (auto instance : contained_instances) { + // Check to see if we've found the contained material instance yet. If not, add + // to the set of materials discovered and add to the map of density multipliers. + int i_material = contained_cell->material(instance); + if (unique_materials.count(i_material) == 0) { + unique_materials.emplace(i_material); + max_density_mult_[i_material] = cell->density_mult(instance); + } else { + // We've found this material already. Need to take the maximum density multiplier + // for the contained instance. + max_density_mult_.at(i_material) = + std::max(max_density_mult_.at(i_material), cell->density_mult(instance)); + } + } + } } - grid_.energy.push_back(energy); - xs_.push_back(std::stod(line.substr(delim_pos + 1))); } - if (p_transport_indx == ParticleType::neutron().transport_index()) { - grid_.init(); + // Clear the contained materials vector and insert the elements from the set. + contained_materials_.clear(); + for (auto i_mat : unique_materials) { + contained_materials_.push_back(i_mat); } } @@ -70,7 +106,8 @@ void Majorant::compute_majorant() material_maj_xs.resize(grid_.energy.size(), 0.0); for (int i_material : contained_materials_) { // Populate the per-material majorant cross section. - fill_material_maj_xs(*model::materials[i_material], grid_.energy, material_maj_xs); + fill_material_maj_xs(*model::materials[i_material], max_density_mult_.at(i_material), + grid_.energy, material_maj_xs); // Compute the full majorant by taking the max over each material cross section. for (int i_energy = 0; i_energy < xs_.size(); ++i_energy) { @@ -89,37 +126,6 @@ void Majorant::write_ascii(const std::string& filename) const of.close(); } -void Majorant::discover_contained_materials() -{ - std::set unique_materials; - if (maj_universe_ == C_NONE || maj_universe_ >= model::universes.size()) { - fatal_error( fmt::format("Invalid majorant universe: {}", maj_universe_)); - } - - const auto & maj_uni = model::universes[maj_universe_]; - for (int i_cell : maj_uni->cells_) { - const auto & cell = model::cells[i_cell]; - if (cell->type_ == Fill::MATERIAL) { - for (auto i_mat : cell->material_) { - unique_materials.emplace(i_mat); - } - } else { - const auto contained_cells = cell->get_contained_cells(); - for (const auto & [i_contained_cell, contained_instances] : contained_cells) { - const auto & contained_cell = model::cells[i_contained_cell]; - for (auto i_mat : contained_cell->material_) { - unique_materials.emplace(i_mat); - } - } - } - } - - contained_materials_.clear(); - for (auto i_mat : unique_materials) { - contained_materials_.push_back(i_mat); - } -} - //============================================================================== // NeutronMajorant implementation //============================================================================== @@ -128,10 +134,6 @@ NeutronMajorant::NeutronMajorant(int i_universe) : Majorant(i_universe) { } -NeutronMajorant::NeutronMajorant(const std::string & majorant_file) - : Majorant(majorant_file, i_neutron_) -{ } - double NeutronMajorant::calculate_neutron_xs(double energy) const { const int i_grid = get_i_grid(energy, grid_); @@ -195,7 +197,8 @@ void NeutronMajorant::compute_unionized_grid() grid_.init(); } -void NeutronMajorant::fill_material_maj_xs(const Material & mat, const std::vector & to_grid, std::vector & mat_maj) +void NeutronMajorant::fill_material_maj_xs(const Material & mat, double max_density_mult, + const std::vector & to_grid, std::vector & mat_maj) { for (int i_energy = 0; i_energy < to_grid.size(); ++i_energy) { mat_maj[i_energy] = 0.0; @@ -257,7 +260,7 @@ void NeutronMajorant::fill_material_maj_xs(const Material & mat, const std::vect // ====================================================================== // Accumulate the macroscopic cross section. // TODO: density multipliers for per-cell material densities. - mat_maj[i_energy] += std::max(micro_smooth_tot_xs, micro_urr_xs) * mat.atom_density(i); + mat_maj[i_energy] += std::max(micro_smooth_tot_xs, micro_urr_xs) * mat.atom_density(i, max_density_mult); } } } @@ -405,10 +408,6 @@ PhotonMajorant::PhotonMajorant(int i_universe) : Majorant(i_universe) { } -PhotonMajorant::PhotonMajorant(const std::string & majorant_file) - : Majorant(majorant_file, i_photon_) -{ } - void PhotonMajorant::compute_unionized_grid() { // In the event the majorant needs to be re-generated (e.g. in-memory for @@ -463,7 +462,8 @@ double PhotonMajorant::calculate_photon_xs(double energy) const return std::exp(xs_[i_grid] + f * (xs_[i_grid + 1] - xs_[i_grid])); } -void PhotonMajorant::fill_material_maj_xs(const Material & mat, const std::vector & to_grid, std::vector & mat_maj) +void PhotonMajorant::fill_material_maj_xs(const Material & mat, double max_density_mult, + const std::vector & to_grid, std::vector & mat_maj) { for (int i_energy = 0; i_energy < to_grid.size(); ++i_energy) { mat_maj[i_energy] = 0.0; @@ -472,7 +472,7 @@ void PhotonMajorant::fill_material_maj_xs(const Material & mat, const std::vecto for (int i = 0; i < mat.nuclide_.size(); ++i) { const int i_element = mat.element_[i]; - mat_maj[i_energy] += calculate_elem_tot_xs(union_log_energy, *data::elements[i_element]) * mat.atom_density(i); + mat_maj[i_energy] += calculate_elem_tot_xs(union_log_energy, *data::elements[i_element]) * mat.atom_density(i, max_density_mult); } mat_maj[i_energy] = std::log(mat_maj[i_energy]); } @@ -561,32 +561,13 @@ int PhotonMajorant::get_i_grid(double log_energy, const tensor::Tensor & // exist already. void create_majorants() { - try { - if (data::n_majorant_file != "") { - write_message("Loading neutron majorant from " + data::n_majorant_file); - data::n_majorant = std::make_unique(data::n_majorant_file); - } - } catch (const std::exception& e) { - fatal_error(fmt::format("Failed to load neutron majorant with error: {}", e.what()).c_str()); - } - try { - if (settings::photon_transport && data::p_majorant_file != "") { - write_message("Loading photon majorant from " + data::p_majorant_file); - data::p_majorant = std::make_unique(data::p_majorant_file); - } - } catch (const std::exception& e) { - fatal_error(fmt::format("Failed to load photon majorant with error: {}", e.what()).c_str()); - } - - if (data::n_majorant_file == "") { - write_message("Creating a neutron majorant cross section"); - data::n_majorant = std::make_unique(model::root_universe); - data::n_majorant->compute_unionized_grid(); - data::n_majorant->compute_majorant(); - data::n_majorant->write_ascii("neutron_majorant.txt"); - } + write_message("Creating a neutron majorant cross section"); + data::n_majorant = std::make_unique(model::root_universe); + data::n_majorant->compute_unionized_grid(); + data::n_majorant->compute_majorant(); + data::n_majorant->write_ascii("neutron_majorant.txt"); - if (settings::photon_transport && data::p_majorant_file == "") { + if (settings::photon_transport) { write_message("Creating a photon majorant cross section"); data::p_majorant = std::make_unique(model::root_universe); data::p_majorant->compute_unionized_grid(); From 17cc190bffcf18b3d98a8ba33510b1b591a85193 Mon Sep 17 00:00:00 2001 From: nuclearkevin <66632997+nuclearkevin@users.noreply.github.com> Date: Sat, 30 May 2026 13:34:01 -0500 Subject: [PATCH 19/73] Rename delta tracking functions. --- include/openmc/simulation.h | 4 ++-- src/simulation.cpp | 8 ++++---- 2 files changed, 6 insertions(+), 6 deletions(-) diff --git a/include/openmc/simulation.h b/include/openmc/simulation.h index 187caf8a21e..55fe29f441d 100644 --- a/include/openmc/simulation.h +++ b/include/openmc/simulation.h @@ -120,10 +120,10 @@ void transport_history_based(); void transport_history_based_shared_secondary(); //! Simulate a single particle history from birth to death using delta tracking -void transport_delta_tracking_single_particle(Particle& p); +void transport_delta_history_based_single_particle(Particle& p); //! Simulate all particle histories using delta tracking -void transport_delta_tracking(); +void transport_delta_history_based(); //! Simulate all particle histories using event-based parallelism void transport_event_based(); diff --git a/src/simulation.cpp b/src/simulation.cpp index 8b97307920d..b08f8269c81 100644 --- a/src/simulation.cpp +++ b/src/simulation.cpp @@ -285,7 +285,7 @@ int openmc_next_batch(int* status) transport_event_based(); } else { if (settings::delta_tracking) { - transport_delta_tracking(); + transport_delta_history_based(); } else { if (settings::use_shared_secondary_bank) { transport_history_based_shared_secondary(); @@ -917,7 +917,7 @@ void transport_history_based() } } -void transport_delta_tracking_single_particle(Particle& p) +void transport_delta_history_based_single_particle(Particle& p) { p.delta_tracking() = true; p.event_calculate_xs(); @@ -951,12 +951,12 @@ void transport_delta_tracking_single_particle(Particle& p) p.event_death(); } -void transport_delta_tracking() { +void transport_delta_history_based() { #pragma omp parallel for schedule(runtime) for (int64_t i_work = 1; i_work <= simulation::work_per_rank; ++i_work) { Particle p; initialize_particle_track(p, i_work, false); - transport_delta_tracking_single_particle(p); + transport_delta_history_based_single_particle(p); } } From a2e2d7f0eb4418413e06c01c0fe56fdc3df3958f Mon Sep 17 00:00:00 2001 From: nuclearkevin <66632997+nuclearkevin@users.noreply.github.com> Date: Sat, 30 May 2026 13:49:13 -0500 Subject: [PATCH 20/73] Support delta tracking with a shared secondary bank. --- include/openmc/simulation.h | 3 --- src/simulation.cpp | 43 ++++++++++++++++++------------------- 2 files changed, 21 insertions(+), 25 deletions(-) diff --git a/include/openmc/simulation.h b/include/openmc/simulation.h index 55fe29f441d..77c917d82a5 100644 --- a/include/openmc/simulation.h +++ b/include/openmc/simulation.h @@ -122,9 +122,6 @@ void transport_history_based_shared_secondary(); //! Simulate a single particle history from birth to death using delta tracking void transport_delta_history_based_single_particle(Particle& p); -//! Simulate all particle histories using delta tracking -void transport_delta_history_based(); - //! Simulate all particle histories using event-based parallelism void transport_event_based(); diff --git a/src/simulation.cpp b/src/simulation.cpp index b08f8269c81..3b5054a015b 100644 --- a/src/simulation.cpp +++ b/src/simulation.cpp @@ -284,14 +284,10 @@ int openmc_next_batch(int* status) } transport_event_based(); } else { - if (settings::delta_tracking) { - transport_delta_history_based(); + if (settings::use_shared_secondary_bank) { + transport_history_based_shared_secondary(); } else { - if (settings::use_shared_secondary_bank) { - transport_history_based_shared_secondary(); - } else { - transport_history_based(); - } + transport_history_based(); } } @@ -907,16 +903,6 @@ void transport_history_based_single_particle(Particle& p) p.event_death(); } -void transport_history_based() -{ -#pragma omp parallel for schedule(runtime) - for (int64_t i_work = 1; i_work <= simulation::work_per_rank; ++i_work) { - Particle p; - initialize_particle_track(p, i_work, false); - transport_history_based_single_particle(p); - } -} - void transport_delta_history_based_single_particle(Particle& p) { p.delta_tracking() = true; @@ -951,12 +937,17 @@ void transport_delta_history_based_single_particle(Particle& p) p.event_death(); } -void transport_delta_history_based() { - #pragma omp parallel for schedule(runtime) +void transport_history_based() +{ +#pragma omp parallel for schedule(runtime) for (int64_t i_work = 1; i_work <= simulation::work_per_rank; ++i_work) { Particle p; initialize_particle_track(p, i_work, false); - transport_delta_history_based_single_particle(p); + if (settings::delta_tracking) { + transport_delta_history_based_single_particle(p); + } else { + transport_history_based_single_particle(p); + } } } @@ -994,7 +985,11 @@ void transport_history_based_shared_secondary() for (int64_t i = 1; i <= simulation::work_per_rank; i++) { Particle p; initialize_particle_track(p, i, false); - transport_history_based_single_particle(p); + if (settings::delta_tracking) { + transport_delta_history_based_single_particle(p); + } else { + transport_history_based_single_particle(p); + } for (auto& site : p.local_secondary_bank()) { thread_bank.push_back(site); } @@ -1062,7 +1057,11 @@ void transport_history_based_shared_secondary() initialize_particle_track(p, i, true); SourceSite& site = simulation::shared_secondary_bank_read[i - 1]; p.event_revive_from_secondary(site); - transport_history_based_single_particle(p); + if (settings::delta_tracking) { + transport_delta_history_based_single_particle(p); + } else { + transport_history_based_single_particle(p); + } for (auto& secondary_site : p.local_secondary_bank()) { thread_bank.push_back(secondary_site); } From 2a7de820fd992a7b844ea63c9c2bae199ce87df2 Mon Sep 17 00:00:00 2001 From: nuclearkevin <66632997+nuclearkevin@users.noreply.github.com> Date: Mon, 1 Jun 2026 09:29:21 -0500 Subject: [PATCH 21/73] Remove CSV majorant cross sections. --- include/openmc/majorant.h | 2 -- openmc/settings.py | 16 ---------------- src/majorant.cpp | 2 -- src/settings.cpp | 8 -------- src/simulation.cpp | 9 ++++++--- 5 files changed, 6 insertions(+), 31 deletions(-) diff --git a/include/openmc/majorant.h b/include/openmc/majorant.h index eff6ef6884a..384487c12e0 100644 --- a/include/openmc/majorant.h +++ b/include/openmc/majorant.h @@ -22,8 +22,6 @@ class PhotonMajorant; namespace data { extern std::unique_ptr n_majorant; extern std::unique_ptr p_majorant; - extern std::string n_majorant_file; - extern std::string p_majorant_file; } // namespace data //============================================================================== diff --git a/openmc/settings.py b/openmc/settings.py index 9b7a058947f..f5b3325dd4d 100644 --- a/openmc/settings.py +++ b/openmc/settings.py @@ -483,7 +483,6 @@ def __init__(self, **kwargs): self._material_cell_offsets = None self._log_grid_bins = None self._delta_tracking = None - self._delta_tracking_majorant_file = None self._event_based = None self._max_particles_in_flight = None self._max_particle_events = None @@ -1245,20 +1244,11 @@ def delayed_photon_scaling(self, value: bool): def delta_tracking(self): return self._delta_tracking - @property - def delta_tracking_majorant_file(self): - return self._delta_tracking_majorant_file - @delta_tracking.setter def delta_tracking(self, value): cv.check_type('delta_tracking', value, bool) self._delta_tracking = value - @delta_tracking_majorant_file.setter - def delta_tracking_majorant_file(self, value): - cv.check_type('delta_tracking_majorant_file', value, str) - self._delta_tracking_majorant_file = value - @property def material_cell_offsets(self) -> bool: return self._material_cell_offsets @@ -2025,11 +2015,6 @@ def _create_delta_tracking_subelement(self, root): if self._delta_tracking: elem = ET.SubElement(root, "delta_tracking") elem.text = str(self._delta_tracking).lower() - def _create_delta_tracking_maj_file_subelement(self, root): - if self._delta_tracking: - if self.delta_tracking_majorant_file != None: - elem = ET.SubElement(root, "delta_tracking_majorant_file") - elem.text = str(self._delta_tracking_majorant_file) def _create_random_ray_subelement(self, root, mesh_memo=None): if self._random_ray: @@ -2666,7 +2651,6 @@ def to_xml_element(self, mesh_memo=None): self._create_source_rejection_fraction_subelement(element) self._create_free_gas_threshold_subelement(element) self._create_delta_tracking_subelement(element) - self._create_delta_tracking_maj_file_subelement(element) # Clean the indentation in the file to be user-readable clean_indentation(element) diff --git a/src/majorant.cpp b/src/majorant.cpp index ddb849361a2..67954ac2b57 100644 --- a/src/majorant.cpp +++ b/src/majorant.cpp @@ -23,8 +23,6 @@ namespace openmc { namespace data { std::unique_ptr n_majorant; std::unique_ptr p_majorant; -std::string n_majorant_file; -std::string p_majorant_file; } // namespace data diff --git a/src/settings.cpp b/src/settings.cpp index 11e34f60c5e..e38de2697da 100644 --- a/src/settings.cpp +++ b/src/settings.cpp @@ -1240,14 +1240,6 @@ void read_settings_xml(pugi::xml_node root) } } - if (check_for_node(root, "delta_tracking_n_majorant_file")) { - data::n_majorant_file = get_node_value(root, "delta_tracking_n_majorant_file"); - } - - if (check_for_node(root, "delta_tracking_p_majorant_file")) { - data::p_majorant_file = get_node_value(root, "delta_tracking_p_majorant_file"); - } - // Check whether material cell offsets should be generated if (check_for_node(root, "material_cell_offsets")) { material_cell_offsets = get_node_value_bool(root, "material_cell_offsets"); diff --git a/src/simulation.cpp b/src/simulation.cpp index 3b5054a015b..4ee321c5711 100644 --- a/src/simulation.cpp +++ b/src/simulation.cpp @@ -939,11 +939,12 @@ void transport_delta_history_based_single_particle(Particle& p) void transport_history_based() { + const bool using_delta_tracking = settings::delta_tracking; #pragma omp parallel for schedule(runtime) for (int64_t i_work = 1; i_work <= simulation::work_per_rank; ++i_work) { Particle p; initialize_particle_track(p, i_work, false); - if (settings::delta_tracking) { + if (using_delta_tracking) { transport_delta_history_based_single_particle(p); } else { transport_history_based_single_particle(p); @@ -961,6 +962,8 @@ void transport_history_based() // continues until there are no more secondary tracks left to transport. void transport_history_based_shared_secondary() { + const bool using_delta_tracking = settings::delta_tracking; + // Clear shared secondary banks from any prior use simulation::shared_secondary_bank_read.clear(); simulation::shared_secondary_bank_write.clear(); @@ -985,7 +988,7 @@ void transport_history_based_shared_secondary() for (int64_t i = 1; i <= simulation::work_per_rank; i++) { Particle p; initialize_particle_track(p, i, false); - if (settings::delta_tracking) { + if (using_delta_tracking) { transport_delta_history_based_single_particle(p); } else { transport_history_based_single_particle(p); @@ -1057,7 +1060,7 @@ void transport_history_based_shared_secondary() initialize_particle_track(p, i, true); SourceSite& site = simulation::shared_secondary_bank_read[i - 1]; p.event_revive_from_secondary(site); - if (settings::delta_tracking) { + if (using_delta_tracking) { transport_delta_history_based_single_particle(p); } else { transport_history_based_single_particle(p); From 9f621b11b0cb33a33aa666254d6b5ed1c35cae1a Mon Sep 17 00:00:00 2001 From: nuclearkevin <66632997+nuclearkevin@users.noreply.github.com> Date: Mon, 1 Jun 2026 13:43:58 -0500 Subject: [PATCH 22/73] Revert to most conservative URR treatment. --- src/majorant.cpp | 26 +++++++------------------- 1 file changed, 7 insertions(+), 19 deletions(-) diff --git a/src/majorant.cpp b/src/majorant.cpp index 67954ac2b57..c236aa3921d 100644 --- a/src/majorant.cpp +++ b/src/majorant.cpp @@ -289,32 +289,20 @@ double NeutronMajorant::calculate_max_urr_xs(double energy, const Nuclide & nuc, continue; } - int i_energy = lower_bound_index(&urr.energy_.front(), &urr.energy_.back(), energy); - - // Find the maximum URR cross sections for the two bounding energy points. - double max_urr_xs_E0 = 0.0; - double max_urr_xs_E1 = 0.0; + // Find the maximum URR cross section. + double max_urr_xs_at_temp = 0.0; for (int i_cdf = 0; i_cdf < urr.n_cdf(); ++i_cdf) { - max_urr_xs_E0 = std::max(max_urr_xs_E0, urr.xs_values_(i_energy, i_cdf).total); - max_urr_xs_E1 = std::max(max_urr_xs_E1, urr.xs_values_(i_energy + 1, i_cdf).total); - } - - // Interpolate the bounding energy points. - double interp_urr_xs = 0.0; - if (urr.interp_ == Interpolation::lin_lin) { - interp_urr_xs = - interpolate_lin_1D(urr.energy_[i_energy], urr.energy_[i_energy + 1], max_urr_xs_E0, max_urr_xs_E1, energy); - } else if (urr.interp_ == Interpolation::log_log) { - interp_urr_xs = - interpolate_log_1D(urr.energy_[i_energy], urr.energy_[i_energy + 1], max_urr_xs_E0, max_urr_xs_E1, energy); + for (int i_energy = 0; i_energy < urr.energy_.size(); ++i_energy) { + max_urr_xs_at_temp = std::max(max_urr_xs_at_temp, urr.xs_values_(i_energy, i_cdf).total); + } } // Multiply by the smooth cross section (after interpolation) if required. if (urr.multiply_smooth_) { - interp_urr_xs *= smooth_xs; + max_urr_xs_at_temp *= smooth_xs; } - max_urr_xs = std::max(max_urr_xs, interp_urr_xs); + max_urr_xs = std::max(max_urr_xs, max_urr_xs_at_temp); } return max_urr_xs; From 7d82cb98d37c4405456b494b17b23b21995d4c49 Mon Sep 17 00:00:00 2001 From: nuclearkevin <66632997+nuclearkevin@users.noreply.github.com> Date: Tue, 2 Jun 2026 11:20:39 -0500 Subject: [PATCH 23/73] Honestly, I don't know anymore... --- include/openmc/majorant.h | 2 +- src/majorant.cpp | 35 +++++++++++++++++++++++------------ 2 files changed, 24 insertions(+), 13 deletions(-) diff --git a/include/openmc/majorant.h b/include/openmc/majorant.h index 384487c12e0..a6048b00deb 100644 --- a/include/openmc/majorant.h +++ b/include/openmc/majorant.h @@ -43,7 +43,7 @@ class Majorant { void compute_majorant(); //! \brief Write the majorant cross section to a CSV file for visualization - // TODO: remove this when done prototyping. + // TODO: remove this when done prototyping. // //! \param[in] filename The path/name for the majorant file void write_ascii(const std::string& filename) const; diff --git a/src/majorant.cpp b/src/majorant.cpp index c236aa3921d..767c63c46ff 100644 --- a/src/majorant.cpp +++ b/src/majorant.cpp @@ -157,7 +157,7 @@ void NeutronMajorant::compute_unionized_grid() const auto & nuclide = data::nuclides[i_nuclide]; // ====================================================================== - // Unionizing the URR temperature grid. + // Unionizing the URR energy grids. if (nuclide->urr_present_ && settings::urr_ptables_on) { for (const auto & nuc_urr : nuclide->urr_data_) { grid_.energy.insert(grid_.energy.end(), nuc_urr.energy_.begin(), nuc_urr.energy_.end()); @@ -165,9 +165,9 @@ void NeutronMajorant::compute_unionized_grid() } // ====================================================================== - // Unionize the smooth cross section grid. - for (const auto & n_grid : nuclide->grid_) { - grid_.energy.insert(grid_.energy.end(), n_grid.energy.begin(), n_grid.energy.end()); + // Unionize the smooth cross section energy grids. + for (const auto & nuc_grid : nuclide->grid_) { + grid_.energy.insert(grid_.energy.end(), nuc_grid.energy.begin(), nuc_grid.energy.end()); } processed_nuclides.insert(i_nuclide); @@ -257,7 +257,6 @@ void NeutronMajorant::fill_material_maj_xs(const Material & mat, double max_dens // ====================================================================== // Accumulate the macroscopic cross section. - // TODO: density multipliers for per-cell material densities. mat_maj[i_energy] += std::max(micro_smooth_tot_xs, micro_urr_xs) * mat.atom_density(i, max_density_mult); } } @@ -289,20 +288,32 @@ double NeutronMajorant::calculate_max_urr_xs(double energy, const Nuclide & nuc, continue; } - // Find the maximum URR cross section. - double max_urr_xs_at_temp = 0.0; + int i_energy = lower_bound_index(&urr.energy_.front(), &urr.energy_.back(), energy); + + // Find the maximum URR cross sections for the two bounding energy points. + double max_urr_xs_E0 = 0.0; + double max_urr_xs_E1 = 0.0; for (int i_cdf = 0; i_cdf < urr.n_cdf(); ++i_cdf) { - for (int i_energy = 0; i_energy < urr.energy_.size(); ++i_energy) { - max_urr_xs_at_temp = std::max(max_urr_xs_at_temp, urr.xs_values_(i_energy, i_cdf).total); - } + max_urr_xs_E0 = std::max(max_urr_xs_E0, urr.xs_values_(i_energy, i_cdf).total); + max_urr_xs_E1 = std::max(max_urr_xs_E1, urr.xs_values_(i_energy + 1, i_cdf).total); + } + + // Interpolate the bounding energy points. + double interp_urr_xs = 0.0; + if (urr.interp_ == Interpolation::lin_lin) { + interp_urr_xs = + interpolate_lin_1D(urr.energy_[i_energy], urr.energy_[i_energy + 1], max_urr_xs_E0, max_urr_xs_E1, energy); + } else if (urr.interp_ == Interpolation::log_log) { + interp_urr_xs = + interpolate_log_1D(urr.energy_[i_energy], urr.energy_[i_energy + 1], max_urr_xs_E0, max_urr_xs_E1, energy); } // Multiply by the smooth cross section (after interpolation) if required. if (urr.multiply_smooth_) { - max_urr_xs_at_temp *= smooth_xs; + interp_urr_xs *= smooth_xs; } - max_urr_xs = std::max(max_urr_xs, max_urr_xs_at_temp); + max_urr_xs = std::max(max_urr_xs, interp_urr_xs); } return max_urr_xs; From 1dc0646245ea1771b10d08f2204810d9aa672722 Mon Sep 17 00:00:00 2001 From: nuclearkevin <66632997+nuclearkevin@users.noreply.github.com> Date: Tue, 2 Jun 2026 14:47:31 -0500 Subject: [PATCH 24/73] Refactor history-based delta tracking to prepare for event-based. --- include/openmc/geometry.h | 6 ++++ src/geometry.cpp | 24 +++++++++++++ src/particle.cpp | 73 +++++++-------------------------------- src/simulation.cpp | 17 +++++---- 4 files changed, 54 insertions(+), 66 deletions(-) diff --git a/include/openmc/geometry.h b/include/openmc/geometry.h index 107cc7d1f3e..487d62b4a3a 100644 --- a/include/openmc/geometry.h +++ b/include/openmc/geometry.h @@ -77,6 +77,12 @@ void cross_lattice( BoundaryInfo distance_to_boundary(GeometryState& p); +//============================================================================== +//! Find the next external boundary a particle will intersect. +//============================================================================== + +BoundaryInfo distance_to_external_boundary(GeometryState& p); + } // namespace openmc #endif // OPENMC_GEOMETRY_H diff --git a/src/geometry.cpp b/src/geometry.cpp index ddb61385f18..2a68eb728dc 100644 --- a/src/geometry.cpp +++ b/src/geometry.cpp @@ -452,6 +452,30 @@ BoundaryInfo distance_to_boundary(GeometryState& p) return info; } +//============================================================================== + +BoundaryInfo distance_to_external_boundary(GeometryState& p) +{ + BoundaryInfo info; + + info.distance() = INFTY; + info.surface() = 0; + info.coord_level() = 1; + for (auto s_idx : model::boundary_surfaces) { + const auto& s = model::surfaces[s_idx]; + double surf_dist = s->distance(p.r(), p.u(), false); + if (surf_dist < info.distance()) { + info.distance() = surf_dist; + info.surface() = s_idx + 1; + if (s->sense(p.r(), p.u())) { + info.surface() *= -1; + } + } + } + + return info; +} + //============================================================================== // C API //============================================================================== diff --git a/src/particle.cpp b/src/particle.cpp index b47d0ad0712..e2c3b5a3cfe 100644 --- a/src/particle.cpp +++ b/src/particle.cpp @@ -338,78 +338,31 @@ void Particle::event_delta_advance() update_majorant(); } - // sample distance to next position - double distance; + // Sample distance to next position if (type() == ParticleType::electron() || type() == ParticleType::positron()) { - distance = 0.0; + // Electrons/positrons don't move + collision_distance() = 0.0; } else { - // calculate majorant value for this energy - distance = -std::log(prn(current_seed())) / majorant(); + // Sample collision distance based on the majorant for this energy. + collision_distance() = -std::log(prn(current_seed())) / majorant(); } - while (distance > 0.0 && alive()) { - // update distance to problem boundary - boundary().distance() = INFTY; - boundary().surface() = 0; - boundary().coord_level() = 1; - for (auto s_idx : model::boundary_surfaces) { - const auto& s = model::surfaces[s_idx]; - double surf_dist = s->distance(r(), u(), false); - if (surf_dist < boundary().distance()) { - boundary().distance() = surf_dist; - boundary().surface() = s_idx + 1; - if (s->sense(r(), u())) { - boundary().surface() *= -1; - } - } - } - - if (distance < (boundary().distance() - TINY_BIT)) - { - break; - } - - if (coord(n_coord() - 1).cell() == C_NONE && cell_last(n_coord() - 1) == C_NONE) { - // Try to find the particle. - for (int j = 0; j < n_coord(); ++j) { - coord(j).reset(); - } - if (!exhaustive_find_cell(*this)) { - // We've lost this particle. - mark_as_lost(fmt::format("Particle {} could not be located during the delta tracking loop!", id())); - return; - } - } - - // Advance particle to the boundary. - r() += (boundary().distance() - TINY_BIT) * u(); - event_cross_surface(); - material() = C_NONE; - distance -= boundary().distance(); - } - - // The particle leaked out of the boundary, no need to perform the subsequent steps. - if (!alive()) - return; + // Update distance to problem boundary + boundary() = distance_to_external_boundary(*this); - // Advance particle to the true collision site. - r() += distance * u(); - for (int j = 0; j < n_coord(); ++j) { - //coord(j).r() += distance * u(); - coord(j).reset(); - } + // Move to the external boundary or delta tracking collision site. + double distance = std::min(collision_distance(), boundary().distance()); + r() += (distance - TINY_BIT) * u(); - // Need to locate the particle at the collision site. + // Need to locate the particle at the collision site or boundary. if (!exhaustive_find_cell(*this)) { // We've lost this particle. - mark_as_lost(fmt::format("Particle {} could not be located at the delta tracking collision site!", id())); + mark_as_lost(fmt::format("Particle {} could not be located while running delta tracking!", id())); return; } // Force re-calculation of material properties at the collision site. - if (E() != E_last()) { - material_last() = C_NONE; - } + material_last() = C_NONE; } void Particle::event_cross_surface() diff --git a/src/simulation.cpp b/src/simulation.cpp index 4ee321c5711..a78d312fd52 100644 --- a/src/simulation.cpp +++ b/src/simulation.cpp @@ -907,17 +907,20 @@ void transport_delta_history_based_single_particle(Particle& p) { p.delta_tracking() = true; p.event_calculate_xs(); + while (p.alive()) { p.event_delta_advance(); if (p.alive()) { + // Electrons and positrons collide in-place, no need to rejection sample. if (p.type() == ParticleType::electron() || p.type() == ParticleType::positron()) { - // Electrons and positrons collide in-place, no need to rejection sample. p.event_collide(); - } else { - // Rejection sample the total to majorant ratio for photons and neutrons. + } + + if (p.collision_distance() < p.boundary().distance()) { + // Collided before hitting an external boundary. Rejection sample the majorant. p.event_calculate_xs(); - if (p.macro_xs().total / p.majorant() > 1.0) { + if (p.alive() && p.macro_xs().total / p.majorant() > 1.0) { p.mark_as_lost( fmt::format("Ratio of the total cross section ({}) to the majorant " "cross section ({}) for particle {} ({}) with energy {} is " @@ -925,15 +928,17 @@ void transport_delta_history_based_single_particle(Particle& p) p.macro_xs().total, p.majorant(), p.id(), p.type().str(), p.E())); break; } - if (prn(p.current_seed()) < (p.macro_xs().total / p.majorant())) { + if (p.alive() && (prn(p.current_seed()) < (p.macro_xs().total / p.majorant()))) { p.event_collide(); } + } else { + // Crossed an external boundary before colliding. + p.event_cross_surface(); } } p.event_check_limit_and_revive(); } - p.event_death(); } From df758b126cc4805946b2e6d4b52a5cc0b6c33abb Mon Sep 17 00:00:00 2001 From: nuclearkevin <66632997+nuclearkevin@users.noreply.github.com> Date: Tue, 2 Jun 2026 16:55:13 -0500 Subject: [PATCH 25/73] First cut of event-based transport. Works for neutrons --- include/openmc/event.h | 31 +++++++ src/event.cpp | 181 +++++++++++++++++++++++++++++++++++++++++ src/simulation.cpp | 33 ++++++-- 3 files changed, 236 insertions(+), 9 deletions(-) diff --git a/include/openmc/event.h b/include/openmc/event.h index 48a009f2b4c..0ea7abb89d9 100644 --- a/include/openmc/event.h +++ b/include/openmc/event.h @@ -107,6 +107,26 @@ void process_surface_crossing_events(); //! Execute the collision event for all particles in this event's buffer void process_collision_events(); +//! Specialization of process_init_events() for delta tracking. +// +//! \param n_particles The number of particles in the particle buffer +//! \param source_offset The offset index in the source bank to use +void process_delta_init_events(int64_t n_particles, int64_t source_offset); + +//! Specialization of process_calculate_xs_events() for delta tracking. +// +//! \param queue A reference to the desired XS lookup queue +void process_delta_calculate_xs_events(SharedArray& queue); + +//! Execute the delta advance particle event for all particles in this advance buffer +void process_delta_advance_particle_events(); + +//! Specialization of process_surface_crossing_events() for delta tracking. +void process_delta_surface_crossing_events(); + +//! Execute the delta tracking collision event for all particles in this event's buffer +void process_delta_collision_events(); + //! Execute the death event for all particles // //! \param n_particles The number of particles in the particle buffer @@ -116,6 +136,9 @@ void process_death_events(int64_t n_particles); //! longest queue first to maximize vectorization efficiency. void process_transport_events(); +//! Specialization of process_transport_events() for delta tracking. +void process_delta_transport_events(); + //! Initialize secondary particles from a shared secondary bank for //! event-based transport // @@ -125,6 +148,14 @@ void process_transport_events(); void process_init_secondary_events(int64_t n_particles, int64_t offset, const SharedArray& shared_secondary_bank); +//! Specialization of process_init_secondary_events() for delta tracking. +// +//! \param n_particles The number of particles to initialize +//! \param offset The offset index in the shared secondary bank +//! \param shared_secondary_bank The shared secondary bank to read from +void process_delta_init_secondary_events(int64_t n_particles, int64_t offset, + const SharedArray& shared_secondary_bank); + } // namespace openmc #endif // OPENMC_EVENT_H diff --git a/src/event.cpp b/src/event.cpp index 2d436bb9dcf..ee6f78fce23 100644 --- a/src/event.cpp +++ b/src/event.cpp @@ -169,6 +169,143 @@ void process_collision_events() simulation::time_event_collision.stop(); } +void process_delta_init_events(int64_t n_particles, int64_t source_offset) +{ + simulation::time_event_init.start(); +#pragma omp parallel for schedule(runtime) + for (int64_t i = 0; i < n_particles; i++) { + simulation::particles[i].delta_tracking() = true; + initialize_particle_track( + simulation::particles[i], source_offset + i + 1, false); + simulation::particles[i].event_calculate_xs(); + + simulation::advance_particle_queue[i] = {simulation::particles[i], i}; + } + simulation::advance_particle_queue.resize(n_particles); + simulation::time_event_init.stop(); +} + +void process_delta_calculate_xs_events(SharedArray& queue) +{ + simulation::time_event_calculate_xs.start(); + + // TODO: If using C++17, we could perform a parallel sort of the queue by + // particle type, material type, and then energy, in order to improve cache + // locality and reduce thread divergence on GPU. However, the parallel + // algorithms typically require linking against an additional library (Intel + // TBB). Prior to C++17, std::sort is a serial only operation, which in this + // case makes it too slow to be practical for most test problems. + // + // std::sort(std::execution::par_unseq, queue.data(), queue.data() + + // queue.size()); + + int64_t offset = simulation::collision_queue.size(); + +#pragma omp parallel for schedule(runtime) + for (int64_t i = 0; i < queue.size(); i++) { + Particle* p = &simulation::particles[queue[i].idx]; + p->event_calculate_xs(); + + // After executing a calculate_xs event in delta tracking, particles will + // always require a collision event. Therefore, we don't need to use + // the protected enqueuing function. + simulation::collision_queue[offset + i] = queue[i]; + } + + simulation::collision_queue.resize(offset + queue.size()); + + queue.resize(0); + + simulation::time_event_calculate_xs.stop(); +} + +void process_delta_advance_particle_events() +{ + simulation::time_event_advance_particle.start(); + + #pragma omp parallel for schedule(runtime) + for (int64_t i = 0; i < simulation::advance_particle_queue.size(); i++) { + int64_t buffer_idx = simulation::advance_particle_queue[i].idx; + Particle& p = simulation::particles[buffer_idx]; + p.event_delta_advance(); + if (!p.alive()) + continue; + + if (p.type() == ParticleType::electron() || p.type() == ParticleType::positron()) { + simulation::collision_queue.thread_safe_append({p, buffer_idx}); + } + + if (p.collision_distance() < p.boundary().distance()) { + // We need to compute cross sections prior to processing a collision. + dispatch_xs_event(buffer_idx); + } else { + simulation::surface_crossing_queue.thread_safe_append({p, buffer_idx}); + } + } + + simulation::advance_particle_queue.resize(0); + + simulation::time_event_advance_particle.stop(); +} + +void process_delta_surface_crossing_events() +{ + simulation::time_event_surface_crossing.start(); + +#pragma omp parallel for schedule(runtime) + for (int64_t i = 0; i < simulation::surface_crossing_queue.size(); i++) { + int64_t buffer_idx = simulation::surface_crossing_queue[i].idx; + Particle& p = simulation::particles[buffer_idx]; + p.event_cross_surface(); + p.event_check_limit_and_revive(); + if (p.alive()) + simulation::advance_particle_queue.thread_safe_append({p, buffer_idx}); + } + + simulation::surface_crossing_queue.resize(0); + + simulation::time_event_surface_crossing.stop(); +} + +void process_delta_collision_events() +{ + simulation::time_event_collision.start(); + +#pragma omp parallel for schedule(runtime) + for (int64_t i = 0; i < simulation::collision_queue.size(); i++) { + int64_t buffer_idx = simulation::collision_queue[i].idx; + Particle& p = simulation::particles[buffer_idx]; + + if (p.type() == ParticleType::electron() || p.type() == ParticleType::positron()) { + p.event_collide(); + } else { + if (p.macro_xs().total / p.majorant() > 1.0) { + p.mark_as_lost( + fmt::format("Ratio of the total cross section ({}) to the majorant " + "cross section ({}) for particle {} ({}) with energy {} is " + "greater than unity!", + p.macro_xs().total, p.majorant(), p.id(), p.type().str(), p.E())); + continue; + } + + if (prn(p.current_seed()) < (p.macro_xs().total / p.majorant())) { + // Real collision, need to process the collision prior to enqueuing an + // advance event. + p.event_collide(); + } + } + + p.event_check_limit_and_revive(); + if (p.alive()) { + simulation::advance_particle_queue.thread_safe_append({p, buffer_idx}); + } + } + + simulation::collision_queue.resize(0); + + simulation::time_event_collision.stop(); +} + void process_death_events(int64_t n_particles) { simulation::time_event_death.start(); @@ -205,6 +342,31 @@ void process_transport_events() } } +void process_delta_transport_events() +{ + while (true) { + int64_t max = std::max({simulation::calculate_fuel_xs_queue.size(), + simulation::calculate_nonfuel_xs_queue.size(), + simulation::advance_particle_queue.size(), + simulation::surface_crossing_queue.size(), + simulation::collision_queue.size()}); + + if (max == 0) { + break; + } else if (max == simulation::calculate_fuel_xs_queue.size()) { + process_delta_calculate_xs_events(simulation::calculate_fuel_xs_queue); + } else if (max == simulation::calculate_nonfuel_xs_queue.size()) { + process_delta_calculate_xs_events(simulation::calculate_nonfuel_xs_queue); + } else if (max == simulation::advance_particle_queue.size()) { + process_delta_advance_particle_events(); + } else if (max == simulation::surface_crossing_queue.size()) { + process_delta_surface_crossing_events(); + } else if (max == simulation::collision_queue.size()) { + process_delta_collision_events(); + } + } +} + void process_init_secondary_events(int64_t n_particles, int64_t offset, const SharedArray& shared_secondary_bank) { @@ -221,4 +383,23 @@ void process_init_secondary_events(int64_t n_particles, int64_t offset, simulation::time_event_init.stop(); } +void process_delta_init_secondary_events(int64_t n_particles, int64_t offset, + const SharedArray& shared_secondary_bank) +{ + simulation::time_event_init.start(); +#pragma omp parallel for schedule(runtime) + for (int64_t i = 0; i < n_particles; i++) { + simulation::particles[i].delta_tracking() = true; + initialize_particle_track(simulation::particles[i], offset + i + 1, true); + const SourceSite& site = shared_secondary_bank[offset + i]; + simulation::particles[i].event_revive_from_secondary(site); + + if (simulation::particles[i].alive()) { + simulation::particles[i].event_calculate_xs(); + simulation::advance_particle_queue.thread_safe_append({simulation::particles[i], i}); + } + } + simulation::time_event_init.stop(); +} + } // namespace openmc diff --git a/src/simulation.cpp b/src/simulation.cpp index a78d312fd52..244042be5bb 100644 --- a/src/simulation.cpp +++ b/src/simulation.cpp @@ -282,7 +282,6 @@ int openmc_next_batch(int* status) } else { transport_event_based(); } - transport_event_based(); } else { if (settings::use_shared_secondary_bank) { transport_history_based_shared_secondary(); @@ -920,7 +919,7 @@ void transport_delta_history_based_single_particle(Particle& p) if (p.collision_distance() < p.boundary().distance()) { // Collided before hitting an external boundary. Rejection sample the majorant. p.event_calculate_xs(); - if (p.alive() && p.macro_xs().total / p.majorant() > 1.0) { + if (p.alive() && (p.macro_xs().total / p.majorant() > 1.0)) { p.mark_as_lost( fmt::format("Ratio of the total cross section ({}) to the majorant " "cross section ({}) for particle {} ({}) with energy {} is " @@ -1109,8 +1108,13 @@ void transport_event_based() std::min(remaining_work, settings::max_particles_in_flight); // Initialize all particle histories for this subiteration - process_init_events(n_particles, source_offset); - process_transport_events(); + if (settings::delta_tracking) { + process_delta_init_events(n_particles, source_offset); + process_delta_transport_events(); + } else { + process_init_events(n_particles, source_offset); + process_transport_events(); + } process_death_events(n_particles); // Adjust remaining work and source offset variables @@ -1144,8 +1148,13 @@ void transport_event_based_shared_secondary() int64_t n_particles = std::min(remaining_work, settings::max_particles_in_flight); - process_init_events(n_particles, source_offset); - process_transport_events(); + if (settings::delta_tracking) { + process_delta_init_events(n_particles, source_offset); + process_delta_transport_events(); + } else { + process_init_events(n_particles, source_offset); + process_transport_events(); + } process_death_events(n_particles); // Collect secondaries from all particle buffers into shared bank @@ -1213,9 +1222,15 @@ void transport_event_based_shared_secondary() int64_t n_particles = std::min(sec_remaining, settings::max_particles_in_flight); - process_init_secondary_events( - n_particles, sec_offset, simulation::shared_secondary_bank_read); - process_transport_events(); + if (settings::delta_tracking) { + process_delta_init_secondary_events( + n_particles, sec_offset, simulation::shared_secondary_bank_read); + process_delta_transport_events(); + } else { + process_init_secondary_events( + n_particles, sec_offset, simulation::shared_secondary_bank_read); + process_transport_events(); + } process_death_events(n_particles); // Collect secondaries from all particle buffers into shared bank From e5efe724ee942b30570116430e54bcf84568b3e7 Mon Sep 17 00:00:00 2001 From: nuclearkevin <66632997+nuclearkevin@users.noreply.github.com> Date: Wed, 3 Jun 2026 09:38:20 -0500 Subject: [PATCH 26/73] Fixes for event-based delta tracking photon transport. --- include/openmc/event.h | 42 ++++++++-------- src/event.cpp | 107 +++++++++++++++++++++-------------------- src/particle.cpp | 4 +- 3 files changed, 80 insertions(+), 73 deletions(-) diff --git a/include/openmc/event.h b/include/openmc/event.h index 0ea7abb89d9..a9d446a27f2 100644 --- a/include/openmc/event.h +++ b/include/openmc/event.h @@ -71,7 +71,7 @@ extern vector particles; } // namespace simulation //============================================================================== -// Functions +// Surface tracking functions //============================================================================== //! Allocate space for the event queues and particle buffer @@ -107,6 +107,28 @@ void process_surface_crossing_events(); //! Execute the collision event for all particles in this event's buffer void process_collision_events(); +//! Execute the death event for all particles +// +//! \param n_particles The number of particles in the particle buffer +void process_death_events(int64_t n_particles); + +//! Process event queues until all are empty. Each iteration processes the +//! longest queue first to maximize vectorization efficiency. +void process_transport_events(); + +//! Initialize secondary particles from a shared secondary bank for +//! event-based transport +// +//! \param n_particles The number of particles to initialize +//! \param offset The offset index in the shared secondary bank +//! \param shared_secondary_bank The shared secondary bank to read from +void process_init_secondary_events(int64_t n_particles, int64_t offset, + const SharedArray& shared_secondary_bank); + +//============================================================================== +// Delta tracking functions +//============================================================================== + //! Specialization of process_init_events() for delta tracking. // //! \param n_particles The number of particles in the particle buffer @@ -127,27 +149,9 @@ void process_delta_surface_crossing_events(); //! Execute the delta tracking collision event for all particles in this event's buffer void process_delta_collision_events(); -//! Execute the death event for all particles -// -//! \param n_particles The number of particles in the particle buffer -void process_death_events(int64_t n_particles); - -//! Process event queues until all are empty. Each iteration processes the -//! longest queue first to maximize vectorization efficiency. -void process_transport_events(); - //! Specialization of process_transport_events() for delta tracking. void process_delta_transport_events(); -//! Initialize secondary particles from a shared secondary bank for -//! event-based transport -// -//! \param n_particles The number of particles to initialize -//! \param offset The offset index in the shared secondary bank -//! \param shared_secondary_bank The shared secondary bank to read from -void process_init_secondary_events(int64_t n_particles, int64_t offset, - const SharedArray& shared_secondary_bank); - //! Specialization of process_init_secondary_events() for delta tracking. // //! \param n_particles The number of particles to initialize diff --git a/src/event.cpp b/src/event.cpp index ee6f78fce23..3080db32e15 100644 --- a/src/event.cpp +++ b/src/event.cpp @@ -169,6 +169,58 @@ void process_collision_events() simulation::time_event_collision.stop(); } +void process_death_events(int64_t n_particles) +{ + simulation::time_event_death.start(); +#pragma omp parallel for schedule(runtime) + for (int64_t i = 0; i < n_particles; i++) { + Particle& p = simulation::particles[i]; + p.event_death(); + } + simulation::time_event_death.stop(); +} + +void process_transport_events() +{ + while (true) { + int64_t max = std::max({simulation::calculate_fuel_xs_queue.size(), + simulation::calculate_nonfuel_xs_queue.size(), + simulation::advance_particle_queue.size(), + simulation::surface_crossing_queue.size(), + simulation::collision_queue.size()}); + + if (max == 0) { + break; + } else if (max == simulation::calculate_fuel_xs_queue.size()) { + process_calculate_xs_events(simulation::calculate_fuel_xs_queue); + } else if (max == simulation::calculate_nonfuel_xs_queue.size()) { + process_calculate_xs_events(simulation::calculate_nonfuel_xs_queue); + } else if (max == simulation::advance_particle_queue.size()) { + process_advance_particle_events(); + } else if (max == simulation::surface_crossing_queue.size()) { + process_surface_crossing_events(); + } else if (max == simulation::collision_queue.size()) { + process_collision_events(); + } + } +} + +void process_init_secondary_events(int64_t n_particles, int64_t offset, + const SharedArray& shared_secondary_bank) +{ + simulation::time_event_init.start(); +#pragma omp parallel for schedule(runtime) + for (int64_t i = 0; i < n_particles; i++) { + initialize_particle_track(simulation::particles[i], offset + i + 1, true); + const SourceSite& site = shared_secondary_bank[offset + i]; + simulation::particles[i].event_revive_from_secondary(site); + if (simulation::particles[i].alive()) { + dispatch_xs_event(i); + } + } + simulation::time_event_init.stop(); +} + void process_delta_init_events(int64_t n_particles, int64_t source_offset) { simulation::time_event_init.start(); @@ -232,7 +284,10 @@ void process_delta_advance_particle_events() continue; if (p.type() == ParticleType::electron() || p.type() == ParticleType::positron()) { + // Electrons / positrons collide in place and don't require cross section calculations. + // Can append to the collision queue directly. simulation::collision_queue.thread_safe_append({p, buffer_idx}); + continue; } if (p.collision_distance() < p.boundary().distance()) { @@ -306,42 +361,6 @@ void process_delta_collision_events() simulation::time_event_collision.stop(); } -void process_death_events(int64_t n_particles) -{ - simulation::time_event_death.start(); -#pragma omp parallel for schedule(runtime) - for (int64_t i = 0; i < n_particles; i++) { - Particle& p = simulation::particles[i]; - p.event_death(); - } - simulation::time_event_death.stop(); -} - -void process_transport_events() -{ - while (true) { - int64_t max = std::max({simulation::calculate_fuel_xs_queue.size(), - simulation::calculate_nonfuel_xs_queue.size(), - simulation::advance_particle_queue.size(), - simulation::surface_crossing_queue.size(), - simulation::collision_queue.size()}); - - if (max == 0) { - break; - } else if (max == simulation::calculate_fuel_xs_queue.size()) { - process_calculate_xs_events(simulation::calculate_fuel_xs_queue); - } else if (max == simulation::calculate_nonfuel_xs_queue.size()) { - process_calculate_xs_events(simulation::calculate_nonfuel_xs_queue); - } else if (max == simulation::advance_particle_queue.size()) { - process_advance_particle_events(); - } else if (max == simulation::surface_crossing_queue.size()) { - process_surface_crossing_events(); - } else if (max == simulation::collision_queue.size()) { - process_collision_events(); - } - } -} - void process_delta_transport_events() { while (true) { @@ -367,22 +386,6 @@ void process_delta_transport_events() } } -void process_init_secondary_events(int64_t n_particles, int64_t offset, - const SharedArray& shared_secondary_bank) -{ - simulation::time_event_init.start(); -#pragma omp parallel for schedule(runtime) - for (int64_t i = 0; i < n_particles; i++) { - initialize_particle_track(simulation::particles[i], offset + i + 1, true); - const SourceSite& site = shared_secondary_bank[offset + i]; - simulation::particles[i].event_revive_from_secondary(site); - if (simulation::particles[i].alive()) { - dispatch_xs_event(i); - } - } - simulation::time_event_init.stop(); -} - void process_delta_init_secondary_events(int64_t n_particles, int64_t offset, const SharedArray& shared_secondary_bank) { diff --git a/src/particle.cpp b/src/particle.cpp index e2c3b5a3cfe..9ece2fa180b 100644 --- a/src/particle.cpp +++ b/src/particle.cpp @@ -351,8 +351,8 @@ void Particle::event_delta_advance() boundary() = distance_to_external_boundary(*this); // Move to the external boundary or delta tracking collision site. - double distance = std::min(collision_distance(), boundary().distance()); - r() += (distance - TINY_BIT) * u(); + double distance = std::min(collision_distance(), boundary().distance() - TINY_BIT); + r() += distance * u(); // Need to locate the particle at the collision site or boundary. if (!exhaustive_find_cell(*this)) { From a691f3e31bcfed94c608fa3c24f35a5a1992411d Mon Sep 17 00:00:00 2001 From: nuclearkevin <66632997+nuclearkevin@users.noreply.github.com> Date: Wed, 3 Jun 2026 09:42:16 -0500 Subject: [PATCH 27/73] More shuffling, add some extra division comments. --- include/openmc/event.h | 18 +++++++++++------- src/event.cpp | 30 +++++++++++++++++++----------- 2 files changed, 30 insertions(+), 18 deletions(-) diff --git a/include/openmc/event.h b/include/openmc/event.h index a9d446a27f2..efb366d97a2 100644 --- a/include/openmc/event.h +++ b/include/openmc/event.h @@ -71,7 +71,7 @@ extern vector particles; } // namespace simulation //============================================================================== -// Surface tracking functions +// Functions //============================================================================== //! Allocate space for the event queues and particle buffer @@ -87,6 +87,15 @@ void free_event_queues(void); //! \param buffer_idx The particle's actual index in the particle buffer void dispatch_xs_event(int64_t buffer_idx); +//! Execute the death event for all particles +// +//! \param n_particles The number of particles in the particle buffer +void process_death_events(int64_t n_particles); + +//============================================================================== +// Surface tracking +//============================================================================== + //! Execute the initialization event for all particles // //! \param n_particles The number of particles in the particle buffer @@ -107,11 +116,6 @@ void process_surface_crossing_events(); //! Execute the collision event for all particles in this event's buffer void process_collision_events(); -//! Execute the death event for all particles -// -//! \param n_particles The number of particles in the particle buffer -void process_death_events(int64_t n_particles); - //! Process event queues until all are empty. Each iteration processes the //! longest queue first to maximize vectorization efficiency. void process_transport_events(); @@ -126,7 +130,7 @@ void process_init_secondary_events(int64_t n_particles, int64_t offset, const SharedArray& shared_secondary_bank); //============================================================================== -// Delta tracking functions +// Delta tracking //============================================================================== //! Specialization of process_init_events() for delta tracking. diff --git a/src/event.cpp b/src/event.cpp index 3080db32e15..9dda88e99cc 100644 --- a/src/event.cpp +++ b/src/event.cpp @@ -62,6 +62,21 @@ void dispatch_xs_event(int64_t buffer_idx) } } +void process_death_events(int64_t n_particles) +{ + simulation::time_event_death.start(); +#pragma omp parallel for schedule(runtime) + for (int64_t i = 0; i < n_particles; i++) { + Particle& p = simulation::particles[i]; + p.event_death(); + } + simulation::time_event_death.stop(); +} + +//============================================================================== +// Functions for surface tracking +//============================================================================== + void process_init_events(int64_t n_particles, int64_t source_offset) { simulation::time_event_init.start(); @@ -169,17 +184,6 @@ void process_collision_events() simulation::time_event_collision.stop(); } -void process_death_events(int64_t n_particles) -{ - simulation::time_event_death.start(); -#pragma omp parallel for schedule(runtime) - for (int64_t i = 0; i < n_particles; i++) { - Particle& p = simulation::particles[i]; - p.event_death(); - } - simulation::time_event_death.stop(); -} - void process_transport_events() { while (true) { @@ -221,6 +225,10 @@ void process_init_secondary_events(int64_t n_particles, int64_t offset, simulation::time_event_init.stop(); } +//============================================================================== +// Functions for delta tracking +//============================================================================== + void process_delta_init_events(int64_t n_particles, int64_t source_offset) { simulation::time_event_init.start(); From 1cd0ba0276cd8fb8bdc31e08c9b62647d156cc5d Mon Sep 17 00:00:00 2001 From: nuclearkevin <66632997+nuclearkevin@users.noreply.github.com> Date: Wed, 3 Jun 2026 11:16:32 -0500 Subject: [PATCH 28/73] Remove unnecessary cross section calculation event on particle initialization. --- src/event.cpp | 1 - src/simulation.cpp | 1 - 2 files changed, 2 deletions(-) diff --git a/src/event.cpp b/src/event.cpp index 9dda88e99cc..f904bc64107 100644 --- a/src/event.cpp +++ b/src/event.cpp @@ -237,7 +237,6 @@ void process_delta_init_events(int64_t n_particles, int64_t source_offset) simulation::particles[i].delta_tracking() = true; initialize_particle_track( simulation::particles[i], source_offset + i + 1, false); - simulation::particles[i].event_calculate_xs(); simulation::advance_particle_queue[i] = {simulation::particles[i], i}; } diff --git a/src/simulation.cpp b/src/simulation.cpp index 244042be5bb..46a31b17034 100644 --- a/src/simulation.cpp +++ b/src/simulation.cpp @@ -905,7 +905,6 @@ void transport_history_based_single_particle(Particle& p) void transport_delta_history_based_single_particle(Particle& p) { p.delta_tracking() = true; - p.event_calculate_xs(); while (p.alive()) { p.event_delta_advance(); From 826b3a06f30b004311d06232e8804322fc7b5d04 Mon Sep 17 00:00:00 2001 From: nuclearkevin <66632997+nuclearkevin@users.noreply.github.com> Date: Wed, 3 Jun 2026 13:08:28 -0500 Subject: [PATCH 29/73] Forgot the coord reset. --- src/particle.cpp | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/src/particle.cpp b/src/particle.cpp index 9ece2fa180b..fa72c8e330c 100644 --- a/src/particle.cpp +++ b/src/particle.cpp @@ -355,6 +355,10 @@ void Particle::event_delta_advance() r() += distance * u(); // Need to locate the particle at the collision site or boundary. + for (int j = 0; j < n_coord(); ++j) { + coord(j).reset(); + } + if (!exhaustive_find_cell(*this)) { // We've lost this particle. mark_as_lost(fmt::format("Particle {} could not be located while running delta tracking!", id())); From b2e9a2642eba92c4bc418965765ca82f0d43693a Mon Sep 17 00:00:00 2001 From: nuclearkevin <66632997+nuclearkevin@users.noreply.github.com> Date: Thu, 4 Jun 2026 10:46:15 -0500 Subject: [PATCH 30/73] Use FP_REL_PRECISION when advancing to boundaries. --- src/particle.cpp | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/src/particle.cpp b/src/particle.cpp index fa72c8e330c..ed8bc7ccc6f 100644 --- a/src/particle.cpp +++ b/src/particle.cpp @@ -351,14 +351,13 @@ void Particle::event_delta_advance() boundary() = distance_to_external_boundary(*this); // Move to the external boundary or delta tracking collision site. - double distance = std::min(collision_distance(), boundary().distance() - TINY_BIT); + double distance = std::min(collision_distance(), boundary().distance() - FP_REL_PRECISION); r() += distance * u(); // Need to locate the particle at the collision site or boundary. for (int j = 0; j < n_coord(); ++j) { coord(j).reset(); } - if (!exhaustive_find_cell(*this)) { // We've lost this particle. mark_as_lost(fmt::format("Particle {} could not be located while running delta tracking!", id())); From f1489e4f311497615b6ca85084e94890a27b7496 Mon Sep 17 00:00:00 2001 From: nuclearkevin <66632997+nuclearkevin@users.noreply.github.com> Date: Thu, 4 Jun 2026 16:11:18 -0500 Subject: [PATCH 31/73] Fix grid lookup. --- src/majorant.cpp | 14 ++++++++------ 1 file changed, 8 insertions(+), 6 deletions(-) diff --git a/src/majorant.cpp b/src/majorant.cpp index 767c63c46ff..56a30a71d87 100644 --- a/src/majorant.cpp +++ b/src/majorant.cpp @@ -374,25 +374,27 @@ double NeutronMajorant::calculate_max_sab_tot_xs(double energy, int i_sab, doubl int NeutronMajorant::get_i_grid(double energy, const Nuclide::EnergyGrid & grid) const { // Find energy index on energy grid - int i_log_union = std::log(energy * data::energy_min_rcp[i_neutron_]) * simulation::log_spacing_rcp; + int i_log_union = std::log(energy / data::energy_min[i_neutron_]) / simulation::log_spacing; int i_grid; - if (i_log_union < 0) { + if (energy < grid.energy.front()) { i_grid = 0; - } else if (i_log_union >= (grid.grid_index.size() - 2)) { + } else if (energy > grid.energy.back()) { i_grid = grid.energy.size() - 2; } else { // Determine bounding indices based on which equal log-spaced // interval the energy is in - int i_low = grid.grid_index[i_log_union]; + int i_low = grid.grid_index[i_log_union]; int i_high = grid.grid_index[i_log_union + 1] + 1; // Perform binary search over reduced range - i_grid = i_low + lower_bound_index(&grid.energy[i_low], &grid.energy[i_high], energy); + i_grid = i_low + lower_bound_index( + &grid.energy[i_low], &grid.energy[i_high], energy); } // check for rare case where two energy points are the same - if (grid.energy[i_grid] == grid.energy[i_grid + 1]) ++i_grid; + if (grid.energy[i_grid] == grid.energy[i_grid + 1]) + ++i_grid; return i_grid; } From aca3f3f27b686025c278a390c0ee1c4afd9d6cf2 Mon Sep 17 00:00:00 2001 From: nuclearkevin <66632997+nuclearkevin@users.noreply.github.com> Date: Thu, 4 Jun 2026 18:06:50 -0500 Subject: [PATCH 32/73] Fix majorants, again... --- src/majorant.cpp | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/majorant.cpp b/src/majorant.cpp index 56a30a71d87..182425e0d7e 100644 --- a/src/majorant.cpp +++ b/src/majorant.cpp @@ -379,7 +379,7 @@ int NeutronMajorant::get_i_grid(double energy, const Nuclide::EnergyGrid & grid) int i_grid; if (energy < grid.energy.front()) { i_grid = 0; - } else if (energy > grid.energy.back()) { + } else if (energy >= grid.energy.back()) { i_grid = grid.energy.size() - 2; } else { // Determine bounding indices based on which equal log-spaced From 8d176a6b698968c80271206c45bd5e423e030f45 Mon Sep 17 00:00:00 2001 From: nuclearkevin <66632997+nuclearkevin@users.noreply.github.com> Date: Mon, 8 Jun 2026 13:35:11 -0500 Subject: [PATCH 33/73] Remove unused code. --- include/openmc/endf.h | 17 ------------- include/openmc/majorant.h | 2 -- include/openmc/nuclide.h | 1 - src/endf.cpp | 52 --------------------------------------- src/nuclide.cpp | 1 - src/simulation.cpp | 4 --- src/thermal.cpp | 14 ----------- 7 files changed, 91 deletions(-) diff --git a/include/openmc/endf.h b/include/openmc/endf.h index f2cdc6bc872..34fee9758b9 100644 --- a/include/openmc/endf.h +++ b/include/openmc/endf.h @@ -47,8 +47,6 @@ bool mt_matches(int event_mt, int target_mt); class Function1D { public: virtual double operator()(double x) const = 0; - virtual double max() const = 0; - virtual double max(double min_E, double max_E) const = 0; virtual ~Function1D() = default; }; @@ -71,9 +69,6 @@ class Polynomial : public Function1D { //! \return Polynomial evaluated at x double operator()(double x) const override; - double max() const override; - double max(double min_e, double max_E) const override; - private: vector coef_; //!< Polynomial coefficients }; @@ -95,9 +90,6 @@ class Tabulated1D : public Function1D { //! \return Function evaluated at x double operator()(double x) const override; - double max() const override; - double max(double min_E, double max_E) const override; - // Accessors const vector& x() const { return x_; } const vector& y() const { return y_; } @@ -121,9 +113,6 @@ class CoherentElasticXS : public Function1D { double operator()(double E) const override; - double max() const override; - double max(double min_E, double max_E) const override; - const vector& bragg_edges() const { return bragg_edges_; } const vector& factors() const { return factors_; } @@ -142,9 +131,6 @@ class IncoherentElasticXS : public Function1D { double operator()(double E) const override; - double max() const override; - double max(double min_E, double max_E) const override; - private: double bound_xs_; //!< Characteristic bound xs in [b] double @@ -165,9 +151,6 @@ class Sum1D : public Function1D { //! \return Function evaluated at x double operator()(double E) const override; - double max() const override; - double max(double min_E, double max_E) const override; - const unique_ptr& functions(int i) const { return functions_[i]; } private: diff --git a/include/openmc/majorant.h b/include/openmc/majorant.h index a6048b00deb..e0a40fe751f 100644 --- a/include/openmc/majorant.h +++ b/include/openmc/majorant.h @@ -107,7 +107,6 @@ class NeutronMajorant : public Majorant { // Constructors NeutronMajorant(int i_universe); - NeutronMajorant(const std::string & majorant_file); //---------------------------------------------------------------------------- // Public Methods @@ -183,7 +182,6 @@ class PhotonMajorant : public Majorant { // Constructors PhotonMajorant(int i_universe); - PhotonMajorant(const std::string & majorant_file); //---------------------------------------------------------------------------- // Public Methods diff --git a/include/openmc/nuclide.h b/include/openmc/nuclide.h index 08676f27ffd..c6b9e37c5fa 100644 --- a/include/openmc/nuclide.h +++ b/include/openmc/nuclide.h @@ -171,7 +171,6 @@ namespace data { // Minimum/maximum transport energy for each particle type. Order corresponds to // transport_index() for supported transport particles. extern array energy_min; -extern array energy_min_rcp; extern array energy_max; //! Minimum temperature in [K] that nuclide data is available at diff --git a/src/endf.cpp b/src/endf.cpp index a3a53dd1b80..8a9c5e48a85 100644 --- a/src/endf.cpp +++ b/src/endf.cpp @@ -209,14 +209,6 @@ double Polynomial::operator()(double x) const return y; } -double Polynomial::max() const { - throw(std::runtime_error("Max is not implemented for polynomial functions.")); -} - -double Polynomial::max(double min_E, double max_E) const { - throw(std::runtime_error("Max is not implemented for polynomial functions.")); -} - //============================================================================== // Tabulated1D implementation //============================================================================== @@ -306,23 +298,6 @@ double Tabulated1D::operator()(double x) const } } -double Tabulated1D::max() const { - return *std::max_element(y_.begin(), y_.end()); -} - -double Tabulated1D::max(double min_E, double max_E) const { - std::vector vals; - - for (int i = 0; i <= x().size(); i++) { - double x_val = x()[i]; - if (x_val >= min_E && x_val <= max_E) { - vals.push_back(y()[i]); - } - } - - return *std::max_element(vals.begin(), vals.end()); -} - //============================================================================== // CoherentElasticXS implementation //============================================================================== @@ -355,17 +330,6 @@ double CoherentElasticXS::operator()(double E) const } } -double CoherentElasticXS::max() const { - return *std::max_element(factors_.begin(), factors_.end()); -} - -double CoherentElasticXS::max(double min_E, double max_E) const { - auto i_grid_min = lower_bound_index(bragg_edges().begin(), bragg_edges().end(), min_E); - auto i_grid_max = lower_bound_index(bragg_edges().begin(), bragg_edges().end(), max_E); - - return std::max({factors()[i_grid_min], factors()[i_grid_max]}); -} - //============================================================================== // IncoherentElasticXS implementation //============================================================================== @@ -385,14 +349,6 @@ double IncoherentElasticXS::operator()(double E) const return bound_xs_ / 2.0 * ((1 - std::exp(-4.0 * E * W)) / (2.0 * E * W)); } -double IncoherentElasticXS::max() const { - return (*this)(0.0); -} - -double IncoherentElasticXS::max(double min_E, double max_E) const { - return (*this)(min_E); -} - //============================================================================== // Sum1D implementation //============================================================================== @@ -419,12 +375,4 @@ double Sum1D::operator()(double x) const return result; } -double Sum1D::max() const { - return (*this)(0.0); -} - -double Sum1D::max(double min_E, double max_E) const { - return std::max((*this)(min_E), (*this)(max_E)); -} - } // namespace openmc diff --git a/src/nuclide.cpp b/src/nuclide.cpp index b45c7fb86f5..3bec328bda9 100644 --- a/src/nuclide.cpp +++ b/src/nuclide.cpp @@ -31,7 +31,6 @@ namespace openmc { namespace data { array energy_min {0.0, 0.0, 0.0, 0.0}; -array energy_min_rcp{0.0, 0.0, 0.0, 0.0}; array energy_max {INFTY, INFTY, INFTY, INFTY}; double temperature_min {INFTY}; double temperature_max {0.0}; diff --git a/src/simulation.cpp b/src/simulation.cpp index 46a31b17034..351a0824d62 100644 --- a/src/simulation.cpp +++ b/src/simulation.cpp @@ -810,10 +810,6 @@ void initialize_data() } } - // set energy recipricals - data::energy_min_rcp[neutron] = 1.0 / data::energy_min[neutron]; - if (settings::photon_transport) data::energy_min_rcp[photon] = 1.0 / data::energy_min[photon]; - // Show which nuclide results in lowest energy for neutron transport for (const auto& nuc : data::nuclides) { // If a nuclide is present in a material that's not used in the model, its diff --git a/src/thermal.cpp b/src/thermal.cpp index 641a6e589e2..edfbddf23e8 100644 --- a/src/thermal.cpp +++ b/src/thermal.cpp @@ -291,20 +291,6 @@ void ThermalData::calculate_xs( *inelastic = (*inelastic_.xs)(E); } -void -ThermalData::calculate_max_xs(double* elastic, double* inelastic) const -{ - // Calculate thermal elastic scattering cross section - if (elastic_.xs) { - *elastic = (*elastic_.xs).max(); - } else { - *elastic = 0.0; - } - - // Calculate thermal inelastic scattering cross section - *inelastic = (*inelastic_.xs).max(); -} - AngleEnergy& ThermalData::sample_dist( const NuclideMicroXS& micro_xs, double E, uint64_t* seed) const { From 581e6e0915d9ebba6fa69f2776cd42029cd1ecaa Mon Sep 17 00:00:00 2001 From: nuclearkevin <66632997+nuclearkevin@users.noreply.github.com> Date: Mon, 8 Jun 2026 13:53:47 -0500 Subject: [PATCH 34/73] More cleanup. --- src/majorant.cpp | 16 +++------------- src/particle.cpp | 6 +----- 2 files changed, 4 insertions(+), 18 deletions(-) diff --git a/src/majorant.cpp b/src/majorant.cpp index 182425e0d7e..d69122ccfee 100644 --- a/src/majorant.cpp +++ b/src/majorant.cpp @@ -89,7 +89,6 @@ Majorant::Majorant(int i_universe) } // Clear the contained materials vector and insert the elements from the set. - contained_materials_.clear(); for (auto i_mat : unique_materials) { contained_materials_.push_back(i_mat); } @@ -140,10 +139,6 @@ double NeutronMajorant::calculate_neutron_xs(double energy) const void NeutronMajorant::compute_unionized_grid() { - // In the event the majorant needs to be re-generated (e.g. in-memory for - // multiphysics), we need to reset the unionized grid. - grid_.energy.clear(); - // This function generates a unionized cross section grid between smooth cross // sections and URR probability table grids. std::set processed_nuclides; @@ -223,7 +218,7 @@ void NeutronMajorant::fill_material_maj_xs(const Material & mat, double max_dens // If particle energy is greater than the highest energy for the // S(a,b) table, then don't use the S(a,b) table - if ((union_energy - 1e-6) > data::thermal_scatt[i_sab]->energy_max_) { + if (union_energy > data::thermal_scatt[i_sab]->energy_max_) { i_sab = C_NONE; } @@ -284,7 +279,7 @@ double NeutronMajorant::calculate_max_urr_xs(double energy, const Nuclide & nuc, double max_urr_xs = 0.0; for (const auto & urr : nuc.urr_data_) { - if (!(urr.energy_in_bounds(energy - 1e-6) || urr.energy_in_bounds(energy + 1e-6))) { + if (!urr.energy_in_bounds(energy)) { continue; } @@ -409,10 +404,6 @@ PhotonMajorant::PhotonMajorant(int i_universe) void PhotonMajorant::compute_unionized_grid() { - // In the event the majorant needs to be re-generated (e.g. in-memory for - // multiphysics), we need to reset the unionized grid. - grid_.energy.clear(); - // This function generates a unionized cross section grid for all elements. std::set processed_elements; for (int i_mat : contained_materials_) { @@ -556,8 +547,7 @@ int PhotonMajorant::get_i_grid(double log_energy, const tensor::Tensor & return i_grid; } -//! Create/load a majorant cross section for photons or neutrons. Errors if they -// exist already. +//! Create a majorant cross section for photons or neutrons. void create_majorants() { write_message("Creating a neutron majorant cross section"); diff --git a/src/particle.cpp b/src/particle.cpp index ed8bc7ccc6f..6b6dc2ff886 100644 --- a/src/particle.cpp +++ b/src/particle.cpp @@ -178,8 +178,8 @@ void Particle::from_source(const SourceSite* src) g_last() = static_cast(src->E); E() = data::mg.energy_bin_avg_[g()]; } + E_last() = E(); - E_last() = E(); // maybe should be 0.0? time() = src->time; time_last() = src->time; parent_nuclide() = src->parent_nuclide; @@ -222,12 +222,8 @@ void Particle::event_calculate_xs() // beginning of the history and again for any secondary particles if (lowest_coord().cell() == C_NONE) { if (!exhaustive_find_cell(*this)) { - if (!delta_tracking()) { - wgt() = 0.0; - } else { mark_as_lost("Could not find the cell containing particle " + std::to_string(id())); - } return; } From 70b7cbab4756526bb802723965dd620030d551f7 Mon Sep 17 00:00:00 2001 From: nuclearkevin <66632997+nuclearkevin@users.noreply.github.com> Date: Mon, 8 Jun 2026 14:12:00 -0500 Subject: [PATCH 35/73] More cleanup. --- include/openmc/material.h | 2 -- include/openmc/particle_data.h | 5 ++--- include/openmc/simulation.h | 1 - include/openmc/thermal.h | 6 ------ src/particle.cpp | 16 ++++++++-------- src/simulation.cpp | 2 -- 6 files changed, 10 insertions(+), 22 deletions(-) diff --git a/include/openmc/material.h b/include/openmc/material.h index a630d0e0fa1..3967c36878f 100644 --- a/include/openmc/material.h +++ b/include/openmc/material.h @@ -237,8 +237,6 @@ class Material { double temperature_ {-1}; }; -void set_majorant_xs(); - //============================================================================== // Non-member functions //============================================================================== diff --git a/include/openmc/particle_data.h b/include/openmc/particle_data.h index 6169aa6d920..2b604255740 100644 --- a/include/openmc/particle_data.h +++ b/include/openmc/particle_data.h @@ -533,9 +533,8 @@ class ParticleData : public GeometryState { bool write_track_ {false}; - // Current PRNG state - uint64_t seeds_[N_STREAMS]; //!< current seeds - int stream_ {STREAM_TRACKING}; //!< current RNG stream + uint64_t seeds_[N_STREAMS]; + int stream_ {STREAM_TRACKING}; vector local_secondary_bank_; diff --git a/include/openmc/simulation.h b/include/openmc/simulation.h index 77c917d82a5..6f5f3366057 100644 --- a/include/openmc/simulation.h +++ b/include/openmc/simulation.h @@ -34,7 +34,6 @@ extern "C" double extern "C" double k_abs_tra; //!< sum over batches of k_absorption * k_tracklength extern double log_spacing; //!< lethargy spacing for energy grid searches -extern double log_spacing_rcp; extern "C" int n_lost_particles; //!< cumulative number of lost particles extern "C" bool need_depletion_rx; //!< need to calculate depletion rx? extern "C" int restart_batch; //!< batch at which a restart job resumed diff --git a/include/openmc/thermal.h b/include/openmc/thermal.h index 5bd17ce9acd..c06a2ee0dde 100644 --- a/include/openmc/thermal.h +++ b/include/openmc/thermal.h @@ -43,12 +43,6 @@ class ThermalData { //! \param[out] inelastic Inelastic scattering cross section in [b] void calculate_xs(double E, double* elastic, double* inelastic) const; - //! Calculate the maximum cross section - // - //! \param[out] elastic Elastic scattering cross section in [b] - //! \param[out] inelastic Inelastic scattering cross section in [b] - void calculate_max_xs(double* elastic, double* inelastic) const; - //! Sample an outgoing energy and angle // //! \param[in] micro_xs Microscopic cross sections diff --git a/src/particle.cpp b/src/particle.cpp index 6b6dc2ff886..5783839f0eb 100644 --- a/src/particle.cpp +++ b/src/particle.cpp @@ -179,7 +179,6 @@ void Particle::from_source(const SourceSite* src) E() = data::mg.energy_bin_avg_[g()]; } E_last() = E(); - time() = src->time; time_last() = src->time; parent_nuclide() = src->parent_nuclide; @@ -222,8 +221,8 @@ void Particle::event_calculate_xs() // beginning of the history and again for any secondary particles if (lowest_coord().cell() == C_NONE) { if (!exhaustive_find_cell(*this)) { - mark_as_lost("Could not find the cell containing particle " + - std::to_string(id())); + mark_as_lost( + "Could not find the cell containing particle " + std::to_string(id())); return; } @@ -573,7 +572,7 @@ void Particle::event_revive_from_secondary(const SourceSite& site) void Particle::event_check_limit_and_revive() { // If particle has too many events, display warning and kill it - ++n_event(); + n_event()++; if (n_event() == settings::max_particle_events && !delta_tracking()) { warning("Particle " + std::to_string(id()) + " underwent maximum number of events."); @@ -822,10 +821,11 @@ void Particle::cross_reflective_bc(const Surface& surf, Direction new_u) // (unless we're using a dagmc model, which has exactly one universe) n_coord() = 1; if (surf.geom_type() != GeometryType::DAG && !neighbor_list_find_cell(*this)) { - exhaustive_find_cell(*this); - this->mark_as_lost("Couldn't find particle after reflecting from surface " + - std::to_string(surf.id_) + "."); - return; + if (!exhaustive_find_cell(*this)) { + mark_as_lost("Couldn't find particle after reflecting from surface " + + std::to_string(surf.id_) + "."); + return; + } } // Set previous coordinate going slightly past surface crossing diff --git a/src/simulation.cpp b/src/simulation.cpp index 351a0824d62..4044c7618f0 100644 --- a/src/simulation.cpp +++ b/src/simulation.cpp @@ -340,7 +340,6 @@ double k_col_abs {0.0}; double k_col_tra {0.0}; double k_abs_tra {0.0}; double log_spacing; -double log_spacing_rcp; int n_lost_particles {0}; bool need_depletion_rx {false}; int restart_batch; @@ -835,7 +834,6 @@ void initialize_data() simulation::log_spacing = std::log(data::energy_max[neutron] / data::energy_min[neutron]) / settings::n_log_bins; - simulation::log_spacing_rcp = 1.0 / simulation::log_spacing; } #ifdef OPENMC_MPI From 0c1f0c6382b35095e19dd87765a1babf2c476ddd Mon Sep 17 00:00:00 2001 From: nuclearkevin <66632997+nuclearkevin@users.noreply.github.com> Date: Mon, 8 Jun 2026 14:19:44 -0500 Subject: [PATCH 36/73] URR fix. --- include/openmc/nuclide.h | 5 +++-- src/majorant.cpp | 5 ++++- 2 files changed, 7 insertions(+), 3 deletions(-) diff --git a/include/openmc/nuclide.h b/include/openmc/nuclide.h index c6b9e37c5fa..299a134756c 100644 --- a/include/openmc/nuclide.h +++ b/include/openmc/nuclide.h @@ -33,9 +33,10 @@ class Nuclide { // Types, aliases using EmissionMode = ReactionProduct::EmissionMode; struct EnergyGrid { - // init method + // Init method. void init(); - // data members + + // Data members. vector grid_index; vector energy; }; diff --git a/src/majorant.cpp b/src/majorant.cpp index d69122ccfee..0f8662784c6 100644 --- a/src/majorant.cpp +++ b/src/majorant.cpp @@ -273,13 +273,16 @@ double NeutronMajorant::calculate_max_smooth_xs(double energy, const Nuclide & n double NeutronMajorant::calculate_max_urr_xs(double energy, const Nuclide & nuc, double smooth_xs) const { + // A tolerance on the URR check to make sure we include the URR energy grid bounds. + constexpr double URR_FUZZY_CHECK = 1e-6; + if (!nuc.urr_present_) { return 0.0; } double max_urr_xs = 0.0; for (const auto & urr : nuc.urr_data_) { - if (!urr.energy_in_bounds(energy)) { + if (!(urr.energy_in_bounds(energy - URR_FUZZY_CHECK) || urr.energy_in_bounds(energy + URR_FUZZY_CHECK))) { continue; } From d78524899dfe615650f10a135a2da3623f8b8fcc Mon Sep 17 00:00:00 2001 From: nuclearkevin <66632997+nuclearkevin@users.noreply.github.com> Date: Mon, 8 Jun 2026 16:12:48 -0500 Subject: [PATCH 37/73] Have majorants conform to OpenMC's indexing approach. Also add a timer for majorant construction. --- include/openmc/majorant.h | 26 ++++++++--------- include/openmc/timer.h | 1 + src/majorant.cpp | 59 ++++++++++++++++++++++++--------------- src/output.cpp | 3 ++ src/timer.cpp | 2 ++ 5 files changed, 55 insertions(+), 36 deletions(-) diff --git a/include/openmc/majorant.h b/include/openmc/majorant.h index e0a40fe751f..47d50b77cf0 100644 --- a/include/openmc/majorant.h +++ b/include/openmc/majorant.h @@ -6,9 +6,7 @@ #include -#include "openmc/material.h" #include "openmc/nuclide.h" -#include "openmc/photon.h" namespace openmc { @@ -57,7 +55,7 @@ class Majorant { //! \param[in] mat The material to compute the majorant cross section of //! \param[in] to_grid The grid points to evaluate the majorant at in [eV] //! \param[out] mat_maj The array to write the macroscopic majorant to. The resulting cross section has units of [cm^-1] - virtual void fill_material_maj_xs(const Material & mat, double max_density_mult, + virtual void fill_material_maj_xs(int i_material, double max_density_mult, const std::vector & to_grid, std::vector & mat_maj) = 0; //! \brief Helper function to perform linear interpolation. @@ -131,10 +129,10 @@ class NeutronMajorant : public Majorant { //! \brief Compute a per-material macroscopic majorant cross section in units of [cm^-1] // - //! \param[in] mat The material to compute the majorant cross section of + //! \param[in] i_material Index into the materials array //! \param[in] to_grid The grid points to evaluate the majorant at in [eV] //! \param[out] mat_maj The array to write the macroscopic majorant to. The resulting cross section has units of [cm^-1] - virtual void fill_material_maj_xs(const Material & mat, double max_density_mult, + virtual void fill_material_maj_xs(int i_material, double max_density_mult, const std::vector & to_grid, std::vector & mat_maj) override; private: @@ -143,16 +141,16 @@ class NeutronMajorant : public Majorant { //! \brief Compute the maximum smooth microscopic total cross section in units of [barn]. // + //! \param[in] i_nuclide Index into the nuclides array //! \param[in] energy The energy to evaluate the cross section at in [eV] - //! \param[in] nuc The nuclide to compute the microscopic total cross section of - double calculate_max_smooth_xs(double energy, const Nuclide & nuc) const; + double calculate_max_smooth_xs(int i_nuclide, double energy) const; //! \brief Compute the maximum microscopic total URR cross section in units of [barn]. // + //! \param[in] i_nuclide Index into the nuclides array //! \param[in] energy The energy to evaluate the cross section at in [eV] - //! \param[in] nuc The nuclide to compute the microscopic total cross section of //! \param[in] smooth_xs The smooth total cross section in units of [eV] to use (if needed by the ptable) - double calculate_max_urr_xs(double energy, const Nuclide & nuc, double smooth_xs) const; + double calculate_max_urr_xs(int i_nuclide, double energy, double smooth_xs) const; //! \brief Compute the maximum microscopic S(a,b) total cross section in units of [barn] // @@ -160,7 +158,7 @@ class NeutronMajorant : public Majorant { //! \param[in] i_sab The index into the thermal scattering table array for this nuclide //! \param[in] sab_frac The fraction of the bound cross section to use vs the free gas cross section //! \param[in] nuc The nuclide to compute the microscopic total cross section of - double calculate_max_sab_tot_xs(double energy, int i_sab, double sab_frac, const Nuclide & nuc) const; + double calculate_max_sab_tot_xs(int i_nuclide, int i_sab, double sab_frac, double energy) const; //! \brief Get the grid index for energy interpolation // @@ -204,10 +202,10 @@ class PhotonMajorant : public Majorant { //! \brief Compute a per-material macroscopic majorant cross section in units of [cm^-1] // - //! \param[in] mat The material to compute the majorant cross section of + //! \param[in] i_material Index into the materials array //! \param[in] to_grid The grid points to evaluate the majorant at in [eV] //! \param[out] mat_maj The array to write the macroscopic majorant to. The resulting cross section has units of [cm^-1] - virtual void fill_material_maj_xs(const Material & mat, double max_density_mult, + virtual void fill_material_maj_xs(int i_material, double max_density_mult, const std::vector & to_grid, std::vector & mat_maj) override; private: @@ -216,9 +214,9 @@ class PhotonMajorant : public Majorant { //! \brief Compute the maximum smooth microscopic total cross section in units of [barn]. // + //! \param[in] i_element Index in the elements array //! \param[in] energy The energy to evaluate the cross section at in [eV] - //! \param[in] nuc The nuclide to compute the microscopic total cross section of - double calculate_elem_tot_xs(double log_energy, const PhotonInteraction & elem) const; + double calculate_elem_tot_xs(int i_element, double log_energy) const; //! \brief Get the grid index for energy interpolation // diff --git a/include/openmc/timer.h b/include/openmc/timer.h index d928aad4560..83d1eb4d2a1 100644 --- a/include/openmc/timer.h +++ b/include/openmc/timer.h @@ -21,6 +21,7 @@ extern Timer time_finalize; extern Timer time_inactive; extern Timer time_initialize; extern Timer time_read_xs; +extern Timer time_build_majorant; extern Timer time_statepoint; extern Timer time_tallies; extern Timer time_total; diff --git a/src/majorant.cpp b/src/majorant.cpp index 0f8662784c6..d6baf759a32 100644 --- a/src/majorant.cpp +++ b/src/majorant.cpp @@ -8,10 +8,12 @@ #include "openmc/majorant.h" #include "openmc/material.h" #include "openmc/nuclide.h" +#include "openmc/photon.h" #include "openmc/search.h" #include "openmc/simulation.h" #include "openmc/settings.h" #include "openmc/thermal.h" +#include "openmc/timer.h" #include "openmc/universe.h" namespace openmc { @@ -103,7 +105,7 @@ void Majorant::compute_majorant() material_maj_xs.resize(grid_.energy.size(), 0.0); for (int i_material : contained_materials_) { // Populate the per-material majorant cross section. - fill_material_maj_xs(*model::materials[i_material], max_density_mult_.at(i_material), + fill_material_maj_xs(i_material, max_density_mult_.at(i_material), grid_.energy, material_maj_xs); // Compute the full majorant by taking the max over each material cross section. @@ -190,9 +192,11 @@ void NeutronMajorant::compute_unionized_grid() grid_.init(); } -void NeutronMajorant::fill_material_maj_xs(const Material & mat, double max_density_mult, +void NeutronMajorant::fill_material_maj_xs(int i_material, double max_density_mult, const std::vector & to_grid, std::vector & mat_maj) { + const auto & mat = *model::materials[i_material]; + for (int i_energy = 0; i_energy < to_grid.size(); ++i_energy) { mat_maj[i_energy] = 0.0; const double union_energy = to_grid[i_energy]; @@ -201,7 +205,6 @@ void NeutronMajorant::fill_material_maj_xs(const Material & mat, double max_dens bool check_sab = (mat.thermal_tables_.size() > 0); for (int i = 0; i < mat.nuclide_.size(); ++i) { - const auto & nuclide = data::nuclides[mat.nuclide_[i]]; // ====================================================================== // CHECK FOR S(A,B) TABLE int i_sab = C_NONE; @@ -239,16 +242,16 @@ void NeutronMajorant::fill_material_maj_xs(const Material & mat, double max_dens double micro_smooth_tot_xs = 0.0; if (i_sab >= 0) { // Thermal scattering cross sections using S(a,b) tables. - micro_smooth_tot_xs = calculate_max_sab_tot_xs(union_energy, i_sab, sab_frac, *nuclide); + micro_smooth_tot_xs = calculate_max_sab_tot_xs(mat.nuclide_[i], i_sab, sab_frac, union_energy); } else { // Free gas smooth cross section - micro_smooth_tot_xs = calculate_max_smooth_xs(union_energy, *nuclide); + micro_smooth_tot_xs = calculate_max_smooth_xs(mat.nuclide_[i], union_energy); } // ====================================================================== // Compute the URR cross section. This shouldn't intersect with the // S(a,b) cross section. - double micro_urr_xs = calculate_max_urr_xs(union_energy, *nuclide, micro_smooth_tot_xs); + double micro_urr_xs = calculate_max_urr_xs(mat.nuclide_[i], union_energy, micro_smooth_tot_xs); // ====================================================================== // Accumulate the macroscopic cross section. @@ -257,8 +260,10 @@ void NeutronMajorant::fill_material_maj_xs(const Material & mat, double max_dens } } -double NeutronMajorant::calculate_max_smooth_xs(double energy, const Nuclide & nuc) const +double NeutronMajorant::calculate_max_smooth_xs(int i_nuclide, double energy) const { + const auto & nuc = *data::nuclides[i_nuclide]; + double max_smooth_tot_xs = 0.0; for (int i_temp = 0; i_temp < nuc.kTs_.size(); ++i_temp) { const auto & nuc_grid = nuc.grid_[i_temp]; @@ -271,18 +276,20 @@ double NeutronMajorant::calculate_max_smooth_xs(double energy, const Nuclide & n return max_smooth_tot_xs; } -double NeutronMajorant::calculate_max_urr_xs(double energy, const Nuclide & nuc, double smooth_xs) const +double NeutronMajorant::calculate_max_urr_xs(int i_nuclide, double energy, double smooth_xs) const { // A tolerance on the URR check to make sure we include the URR energy grid bounds. constexpr double URR_FUZZY_CHECK = 1e-6; + const auto & nuc = *data::nuclides[i_nuclide]; if (!nuc.urr_present_) { return 0.0; } double max_urr_xs = 0.0; for (const auto & urr : nuc.urr_data_) { - if (!(urr.energy_in_bounds(energy - URR_FUZZY_CHECK) || urr.energy_in_bounds(energy + URR_FUZZY_CHECK))) { + if (!(urr.energy_in_bounds(energy - URR_FUZZY_CHECK) + || urr.energy_in_bounds(energy + URR_FUZZY_CHECK))) { continue; } @@ -317,9 +324,10 @@ double NeutronMajorant::calculate_max_urr_xs(double energy, const Nuclide & nuc, return max_urr_xs; } -double NeutronMajorant::calculate_max_sab_tot_xs(double energy, int i_sab, double sab_frac, const Nuclide & nuc) const +double NeutronMajorant::calculate_max_sab_tot_xs(int i_nuclide, int i_sab, double sab_frac, double energy) const { - const auto & thermal = data::thermal_scatt[i_sab]; + const auto & nuc = *data::nuclides[i_nuclide]; + const auto & thermal = *data::thermal_scatt[i_sab]; // Loop over the nuclide's temperature grid to ensure we're consistent. double max_sab_total = 0.0; @@ -330,12 +338,12 @@ double NeutronMajorant::calculate_max_sab_tot_xs(double energy, int i_sab, doubl // cross sections are interpolated to match the nuclide temperature point. double thermal_elastic; double thermal_inelastic; - const auto & tkTs = thermal->kTs_; + const auto & tkTs = thermal.kTs_; if (tkTs.size() > 1) { if (nuc_kT < tkTs.front()) { - thermal->data_.front().calculate_xs(energy, &thermal_elastic, &thermal_inelastic); + thermal.data_.front().calculate_xs(energy, &thermal_elastic, &thermal_inelastic); } else if (nuc_kT > tkTs.back()) { - thermal->data_.back().calculate_xs(energy, &thermal_elastic, &thermal_inelastic); + thermal.data_.back().calculate_xs(energy, &thermal_elastic, &thermal_inelastic); } else { // Find temperatures that bound the actual temperature int i_sab_temp = 0; @@ -344,13 +352,13 @@ double NeutronMajorant::calculate_max_sab_tot_xs(double energy, int i_sab, doubl } // Interpolate the scattering cross sections to the nuclide temperature grid point. double T0_elastic, T1_elastic, T0_inelastic, T1_inelastic; - thermal->data_[i_sab_temp].calculate_xs(energy, &T0_elastic, &T0_inelastic); - thermal->data_[i_sab_temp + 1].calculate_xs(energy, &T1_elastic, &T1_inelastic); + thermal.data_[i_sab_temp].calculate_xs(energy, &T0_elastic, &T0_inelastic); + thermal.data_[i_sab_temp + 1].calculate_xs(energy, &T1_elastic, &T1_inelastic); thermal_elastic = interpolate_lin_1D(tkTs[i_sab_temp], tkTs[i_sab_temp + 1], T0_elastic, T1_elastic, nuc_kT); thermal_inelastic = interpolate_lin_1D(tkTs[i_sab_temp], tkTs[i_sab_temp + 1], T0_inelastic, T1_inelastic, nuc_kT); } } else { - thermal->data_[0].calculate_xs(energy, &thermal_elastic, &thermal_inelastic); + thermal.data_[0].calculate_xs(energy, &thermal_elastic, &thermal_inelastic); } // Compute the free gas total and elastic cross sections interpolated on the majorant grid. @@ -455,9 +463,11 @@ double PhotonMajorant::calculate_photon_xs(double energy) const return std::exp(xs_[i_grid] + f * (xs_[i_grid + 1] - xs_[i_grid])); } -void PhotonMajorant::fill_material_maj_xs(const Material & mat, double max_density_mult, +void PhotonMajorant::fill_material_maj_xs(int i_material, double max_density_mult, const std::vector & to_grid, std::vector & mat_maj) { + const auto & mat = *model::materials[i_material]; + for (int i_energy = 0; i_energy < to_grid.size(); ++i_energy) { mat_maj[i_energy] = 0.0; const double union_log_energy = to_grid[i_energy]; @@ -465,14 +475,15 @@ void PhotonMajorant::fill_material_maj_xs(const Material & mat, double max_densi for (int i = 0; i < mat.nuclide_.size(); ++i) { const int i_element = mat.element_[i]; - mat_maj[i_energy] += calculate_elem_tot_xs(union_log_energy, *data::elements[i_element]) * mat.atom_density(i, max_density_mult); + mat_maj[i_energy] += calculate_elem_tot_xs(i_element, union_log_energy) * mat.atom_density(i, max_density_mult); } mat_maj[i_energy] = std::log(mat_maj[i_energy]); } } -double PhotonMajorant::calculate_elem_tot_xs(double log_energy, const PhotonInteraction & elem) const +double PhotonMajorant::calculate_elem_tot_xs(int i_element, double log_energy) const { + const auto & elem = *data::elements[i_element]; int i_grid = get_i_grid(log_energy, elem.energy_); // calculate interpolation factor @@ -553,19 +564,23 @@ int PhotonMajorant::get_i_grid(double log_energy, const tensor::Tensor & //! Create a majorant cross section for photons or neutrons. void create_majorants() { - write_message("Creating a neutron majorant cross section"); + simulation::time_build_majorant.start(); + + write_message("Constructing a neutron majorant cross section"); data::n_majorant = std::make_unique(model::root_universe); data::n_majorant->compute_unionized_grid(); data::n_majorant->compute_majorant(); data::n_majorant->write_ascii("neutron_majorant.txt"); if (settings::photon_transport) { - write_message("Creating a photon majorant cross section"); + write_message("Constructing a photon majorant cross section"); data::p_majorant = std::make_unique(model::root_universe); data::p_majorant->compute_unionized_grid(); data::p_majorant->compute_majorant(); data::p_majorant->write_ascii("photon_majorant.txt"); } + + simulation::time_build_majorant.stop(); } //! Reset the photon and neutron majorant cross sections. diff --git a/src/output.cpp b/src/output.cpp index 807f3b6a1db..7dcec247882 100644 --- a/src/output.cpp +++ b/src/output.cpp @@ -439,6 +439,9 @@ void print_runtime() // display time elapsed for various sections show_time("Total time for initialization", time_initialize.elapsed()); show_time("Reading cross sections", time_read_xs.elapsed(), 1); + if (settings::delta_tracking) { + show_time("Constructing majorants", time_build_majorant.elapsed()); + } show_time("Total time in simulation", time_inactive.elapsed() + time_active.elapsed()); show_time("Time in transport only", time_transport.elapsed(), 1); diff --git a/src/timer.cpp b/src/timer.cpp index 6d692d4fbf6..f4a827c8734 100644 --- a/src/timer.cpp +++ b/src/timer.cpp @@ -16,6 +16,7 @@ Timer time_finalize; Timer time_inactive; Timer time_initialize; Timer time_read_xs; +Timer time_build_majorant; Timer time_statepoint; Timer time_tallies; Timer time_total; @@ -75,6 +76,7 @@ void reset_timers() simulation::time_finalize.reset(); simulation::time_inactive.reset(); simulation::time_initialize.reset(); + simulation::time_build_majorant.reset(); simulation::time_read_xs.reset(); simulation::time_statepoint.reset(); simulation::time_tallies.reset(); From 163c067d9588a29c9078a019504fa6e8bc61f678 Mon Sep 17 00:00:00 2001 From: nuclearkevin <66632997+nuclearkevin@users.noreply.github.com> Date: Mon, 8 Jun 2026 16:13:39 -0500 Subject: [PATCH 38/73] Remove ascii file writes. --- include/openmc/majorant.h | 6 ------ src/majorant.cpp | 13 ------------- 2 files changed, 19 deletions(-) diff --git a/include/openmc/majorant.h b/include/openmc/majorant.h index 47d50b77cf0..da55eb70833 100644 --- a/include/openmc/majorant.h +++ b/include/openmc/majorant.h @@ -40,12 +40,6 @@ class Majorant { //! \brief Populate the majorant cross section. void compute_majorant(); - //! \brief Write the majorant cross section to a CSV file for visualization - // TODO: remove this when done prototyping. - // - //! \param[in] filename The path/name for the majorant file - void write_ascii(const std::string& filename) const; - protected: //---------------------------------------------------------------------------- // Protected Methods diff --git a/src/majorant.cpp b/src/majorant.cpp index d6baf759a32..4ce6afcb848 100644 --- a/src/majorant.cpp +++ b/src/majorant.cpp @@ -1,5 +1,3 @@ -#include - #include #include "openmc/capi.h" @@ -116,15 +114,6 @@ void Majorant::compute_majorant() } } -void Majorant::write_ascii(const std::string& filename) const -{ - std::ofstream of(filename); - for (int i = 0; i < xs_.size(); i++) { - of << grid_.energy[i] << "," << xs_[i] << "\n"; - } - of.close(); -} - //============================================================================== // NeutronMajorant implementation //============================================================================== @@ -570,14 +559,12 @@ void create_majorants() data::n_majorant = std::make_unique(model::root_universe); data::n_majorant->compute_unionized_grid(); data::n_majorant->compute_majorant(); - data::n_majorant->write_ascii("neutron_majorant.txt"); if (settings::photon_transport) { write_message("Constructing a photon majorant cross section"); data::p_majorant = std::make_unique(model::root_universe); data::p_majorant->compute_unionized_grid(); data::p_majorant->compute_majorant(); - data::p_majorant->write_ascii("photon_majorant.txt"); } simulation::time_build_majorant.stop(); From 85c83e555434ec3a1c3d61089be09322fe82787f Mon Sep 17 00:00:00 2001 From: nuclearkevin <66632997+nuclearkevin@users.noreply.github.com> Date: Mon, 8 Jun 2026 16:18:20 -0500 Subject: [PATCH 39/73] Pretty formatting. --- src/output.cpp | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/output.cpp b/src/output.cpp index 7dcec247882..117b56bab1c 100644 --- a/src/output.cpp +++ b/src/output.cpp @@ -440,7 +440,7 @@ void print_runtime() show_time("Total time for initialization", time_initialize.elapsed()); show_time("Reading cross sections", time_read_xs.elapsed(), 1); if (settings::delta_tracking) { - show_time("Constructing majorants", time_build_majorant.elapsed()); + show_time("Time spent building majorants", time_build_majorant.elapsed()); } show_time("Total time in simulation", time_inactive.elapsed() + time_active.elapsed()); From 5095a581ef1d632f8b91280a93b0c8c293fc768a Mon Sep 17 00:00:00 2001 From: nuclearkevin <66632997+nuclearkevin@users.noreply.github.com> Date: Wed, 10 Jun 2026 11:30:25 -0500 Subject: [PATCH 40/73] Restrict delta tracking tallies to collision estimators. --- src/output.cpp | 2 +- src/tallies/tally.cpp | 6 ++++++ 2 files changed, 7 insertions(+), 1 deletion(-) diff --git a/src/output.cpp b/src/output.cpp index 117b56bab1c..0d1639eb22f 100644 --- a/src/output.cpp +++ b/src/output.cpp @@ -440,7 +440,7 @@ void print_runtime() show_time("Total time for initialization", time_initialize.elapsed()); show_time("Reading cross sections", time_read_xs.elapsed(), 1); if (settings::delta_tracking) { - show_time("Time spent building majorants", time_build_majorant.elapsed()); + show_time("Total time building majorants", time_build_majorant.elapsed()); } show_time("Total time in simulation", time_inactive.elapsed() + time_active.elapsed()); diff --git a/src/tallies/tally.cpp b/src/tallies/tally.cpp index e4ea56e5976..10f0fbaf3aa 100644 --- a/src/tallies/tally.cpp +++ b/src/tallies/tally.cpp @@ -350,6 +350,12 @@ Tally::Tally(pugi::xml_node node) this->init_triggers(node); } + // If we're running with delta tracking, the estimator must be set to + // collision or analog. + if (settings::delta_tracking && estimator_ == TallyEstimator::TRACKLENGTH) { + estimator_ = TallyEstimator::COLLISION; + } + // ======================================================================= // SET TALLY ESTIMATOR From 851d5c21b400afb9d4d273159c70016c70740e0d Mon Sep 17 00:00:00 2001 From: nuclearkevin <66632997+nuclearkevin@users.noreply.github.com> Date: Wed, 10 Jun 2026 12:00:03 -0500 Subject: [PATCH 41/73] Remove unnecessary coord reset. --- src/particle.cpp | 3 --- 1 file changed, 3 deletions(-) diff --git a/src/particle.cpp b/src/particle.cpp index 5783839f0eb..f5c56d160c1 100644 --- a/src/particle.cpp +++ b/src/particle.cpp @@ -350,9 +350,6 @@ void Particle::event_delta_advance() r() += distance * u(); // Need to locate the particle at the collision site or boundary. - for (int j = 0; j < n_coord(); ++j) { - coord(j).reset(); - } if (!exhaustive_find_cell(*this)) { // We've lost this particle. mark_as_lost(fmt::format("Particle {} could not be located while running delta tracking!", id())); From 182d0f0e8954846860c7e9c4463e92bc45a36f52 Mon Sep 17 00:00:00 2001 From: nuclearkevin <66632997+nuclearkevin@users.noreply.github.com> Date: Wed, 10 Jun 2026 12:22:55 -0500 Subject: [PATCH 42/73] Clean up combined estimate for k. --- src/eigenvalue.cpp | 66 ++++++++++++++++++---------------------------- 1 file changed, 25 insertions(+), 41 deletions(-) diff --git a/src/eigenvalue.cpp b/src/eigenvalue.cpp index 31bd201b012..50eb28b9e1a 100644 --- a/src/eigenvalue.cpp +++ b/src/eigenvalue.cpp @@ -455,21 +455,6 @@ int openmc_get_keff(double* k_combined) // Copy estimates of k-effective and its variance (not variance of the mean) const auto& gt = simulation::global_tallies; - if (settings::delta_tracking) { - array kv {}; - tensor::Tensor cov = tensor::Tensor({2, 2}); - - kv[0] = gt(GlobalTally::K_COLLISION, TallyResult::SUM) / n; - kv[1] = gt(GlobalTally::K_ABSORPTION, TallyResult::SUM) / n; - - cov(0, 0) = (gt(GlobalTally::K_COLLISION, TallyResult::SUM_SQ) - n*kv[0]*kv[0]) / (n -1); - cov(1, 1) = (gt(GlobalTally::K_ABSORPTION, TallyResult::SUM_SQ) - n*kv[1]*kv[1]) / (n - 1); - - // Calculate covariances based on sums with Bessel's correction - cov(0, 1) = (simulation::k_col_abs - n * kv[0] * kv[1]) / (n - 1); - cov(1, 0) = cov(0, 1); - } - array kv {}; tensor::Tensor cov = tensor::zeros({3, 3}); kv[0] = gt(GlobalTally::K_COLLISION, TallyResult::SUM) / n; @@ -500,38 +485,37 @@ int openmc_get_keff(double* k_combined) // exceptions and an expression specifically derived for the combination of // two estimators (vice three) should be used instead. - // First we will identify if there are any matching estimators + // If delta tracking is enabled, use the collision and absorption estimators only. + // Otherwise, we will identify if there are any matching estimators. If none match, + // all three estimates are used. int i, j; bool use_three = false; - if ((std::abs(kv[0] - kv[1]) / kv[0] < FP_REL_PRECISION) && - (std::abs(cov(0, 0) - cov(1, 1)) / cov(0, 0) < FP_REL_PRECISION)) { - // 0 and 1 match, so only use 0 and 2 in our comparisons - i = 0; - j = 2; - - } else if ((std::abs(kv[0] - kv[2]) / kv[0] < FP_REL_PRECISION) && - (std::abs(cov(0, 0) - cov(2, 2)) / cov(0, 0) < FP_REL_PRECISION)) { - // 0 and 2 match, so only use 0 and 1 in our comparisons - i = 0; - j = 1; - - } else if ((std::abs(kv[1] - kv[2]) / kv[1] < FP_REL_PRECISION) && - (std::abs(cov(1, 1) - cov(2, 2)) / cov(1, 1) < FP_REL_PRECISION)) { - // 1 and 2 match, so only use 0 and 1 in our comparisons + if (settings::delta_tracking) { i = 0; j = 1; - } else { - // No two estimators match, so set boolean to use all three estimators. - use_three = true; - } + if ((std::abs(kv[0] - kv[1]) / kv[0] < FP_REL_PRECISION) && + (std::abs(cov(0, 0) - cov(1, 1)) / cov(0, 0) < FP_REL_PRECISION)) { + // 0 and 1 match, so only use 0 and 2 in our comparisons + i = 0; + j = 2; + + } else if ((std::abs(kv[0] - kv[2]) / kv[0] < FP_REL_PRECISION) && + (std::abs(cov(0, 0) - cov(2, 2)) / cov(0, 0) < FP_REL_PRECISION)) { + // 0 and 2 match, so only use 0 and 1 in our comparisons + i = 0; + j = 1; + + } else if ((std::abs(kv[1] - kv[2]) / kv[1] < FP_REL_PRECISION) && + (std::abs(cov(1, 1) - cov(2, 2)) / cov(1, 1) < FP_REL_PRECISION)) { + // 1 and 2 match, so only use 0 and 1 in our comparisons + i = 0; + j = 1; - // if delta tracking is enabled, use the collision and absorption - // estimators only - if (settings::delta_tracking) { - i = 0; - j = 1; - use_three = false; + } else { + // No two estimators match, so set boolean to use all three estimators. + use_three = true; + } } if (use_three) { From f24a075c5059647efd3ca17fe3e985a8c8610972 Mon Sep 17 00:00:00 2001 From: nuclearkevin <66632997+nuclearkevin@users.noreply.github.com> Date: Wed, 10 Jun 2026 13:00:12 -0500 Subject: [PATCH 43/73] Clean up majorant and remove random duplicate code. --- include/openmc/majorant.h | 16 +++++++-- include/openmc/particle.h | 1 + src/majorant.cpp | 71 ++++++++++++++++++++------------------- 3 files changed, 51 insertions(+), 37 deletions(-) diff --git a/include/openmc/majorant.h b/include/openmc/majorant.h index da55eb70833..5485d7c8e2f 100644 --- a/include/openmc/majorant.h +++ b/include/openmc/majorant.h @@ -44,14 +44,26 @@ class Majorant { //---------------------------------------------------------------------------- // Protected Methods - //! \brief Compute a per-material macroscopic majorant cross section in units of [cm^-1] + //! \brief Compute a per-material macroscopic majorant cross section in units + //! of [cm^-1] // //! \param[in] mat The material to compute the majorant cross section of //! \param[in] to_grid The grid points to evaluate the majorant at in [eV] - //! \param[out] mat_maj The array to write the macroscopic majorant to. The resulting cross section has units of [cm^-1] + //! \param[out] mat_maj The array to write the macroscopic majorant to. + //! The resulting cross section has units of [cm^-1] virtual void fill_material_maj_xs(int i_material, double max_density_mult, const std::vector & to_grid, std::vector & mat_maj) = 0; + //! \brief Post-processes the energy grid by calling std::sort(), std::unique(). + //! This also removes all energy values below the transport minimum and + //! above the transport maximum for a given particle type. + //! + //! \param[in] particle_type the particle type transport index to use when + //! fetching transport minimum / maximum energies + //! \param[out] grid the energy grid to post-process. This is performed + //! in-place. + void post_process_grid(int particle_type, Nuclide::EnergyGrid & grid); + //! \brief Helper function to perform linear interpolation. // //! \param[in] x_0 The first x coordinate diff --git a/include/openmc/particle.h b/include/openmc/particle.h index 2068457fa95..f64f4db1913 100644 --- a/include/openmc/particle.h +++ b/include/openmc/particle.h @@ -112,6 +112,7 @@ class Particle : public ParticleData { virtual void mark_as_lost(const char* message) override; using GeometryState::mark_as_lost; + //! Update the cached majorant cross section for this particle. void update_majorant(); //! create a particle restart HDF5 file diff --git a/src/majorant.cpp b/src/majorant.cpp index 4ce6afcb848..f2d5a68f34f 100644 --- a/src/majorant.cpp +++ b/src/majorant.cpp @@ -100,7 +100,6 @@ void Majorant::compute_majorant() xs_.resize(grid_.energy.size(), 0.0); std::vector material_maj_xs; - material_maj_xs.resize(grid_.energy.size(), 0.0); for (int i_material : contained_materials_) { // Populate the per-material majorant cross section. fill_material_maj_xs(i_material, max_density_mult_.at(i_material), @@ -110,10 +109,30 @@ void Majorant::compute_majorant() for (int i_energy = 0; i_energy < xs_.size(); ++i_energy) { xs_[i_energy] = std::max(xs_[i_energy], material_maj_xs[i_energy]); } - std::fill(material_maj_xs.begin(), material_maj_xs.end(), 0.0); } } +void Majorant::post_process_grid(int particle_type, Nuclide::EnergyGrid & grid) +{ + std::sort(grid_.energy.begin(), grid_.energy.end()); + auto unique_end = std::unique(grid_.energy.begin(), grid_.energy.end()); + grid_.energy.resize(std::distance(grid_.energy.begin(), unique_end)); + + // remove all values below the minimum neutron energy + auto min_it = grid_.energy.begin(); + while (*min_it < data::energy_min[particle_type]) { min_it++; } + grid_.energy.erase(grid_.energy.begin(), min_it + 1); + // insert the minimum neutron energy at the beginning + grid_.energy.insert(grid_.energy.begin(), data::energy_min[particle_type]); + + // remove all values above the maximum neutron energy + auto max_it = --grid_.energy.end(); + while (*max_it > data::energy_max[particle_type]) { max_it--; } + grid_.energy.erase(max_it - 1, grid_.energy.end()); + // insert the maximum neutron energy at the end + grid_.energy.insert(grid_.energy.end(), data::energy_max[particle_type]); +} + //============================================================================== // NeutronMajorant implementation //============================================================================== @@ -159,25 +178,13 @@ void NeutronMajorant::compute_unionized_grid() processed_nuclides.insert(i_nuclide); } } - std::sort(grid_.energy.begin(), grid_.energy.end()); - auto unique_end = std::unique(grid_.energy.begin(), grid_.energy.end()); - grid_.energy.resize(std::distance(grid_.energy.begin(), unique_end)); - // remove all values below the minimum neutron energy - auto min_it = grid_.energy.begin(); - while (*min_it < data::energy_min[i_neutron_]) { min_it++; } - grid_.energy.erase(grid_.energy.begin(), min_it + 1); - // insert the minimum neutron energy at the beginning - grid_.energy.insert(grid_.energy.begin(), data::energy_min[i_neutron_]); - - // remove all values above the maximum neutron energy - auto max_it = --grid_.energy.end(); - while (*max_it > data::energy_max[i_neutron_]) { max_it--; } - grid_.energy.erase(max_it - 1, grid_.energy.end()); - // insert the maximum neutron energy at the end - grid_.energy.insert(grid_.energy.end(), data::energy_max[i_neutron_]); + // Post-process the energy grid now that all points from nuclides are included. This sorts + // the energy points, removes duplicates, and removes all energies exceeding neutron + // transport bounds. + post_process_grid(i_neutron_, grid_); - // Initialize the grid for fast lookups. + // Initialize the grid for fast lookups. This only applies to neutrons. grid_.init(); } @@ -186,6 +193,9 @@ void NeutronMajorant::fill_material_maj_xs(int i_material, double max_density_mu { const auto & mat = *model::materials[i_material]; + mat_maj.resize(to_grid.size()); + std::fill(mat_maj.begin(), mat_maj.end(), 0.0); + for (int i_energy = 0; i_energy < to_grid.size(); ++i_energy) { mat_maj[i_energy] = 0.0; const double union_energy = to_grid[i_energy]; @@ -420,23 +430,11 @@ void PhotonMajorant::compute_unionized_grid() processed_elements.insert(mat->element_[i]); } } - std::sort(grid_.energy.begin(), grid_.energy.end()); - auto unique_end = std::unique(grid_.energy.begin(), grid_.energy.end()); - grid_.energy.resize(std::distance(grid_.energy.begin(), unique_end)); - // remove all values below the minimum photon energy - auto min_it = grid_.energy.begin(); - while (*min_it < std::log(data::energy_min[i_photon_])) { min_it++; } - grid_.energy.erase(grid_.energy.begin(), min_it + 1); - // insert the minimum photon energy at the beginning - grid_.energy.insert(grid_.energy.begin(), std::log(data::energy_min[i_photon_])); - - // remove all values above the maximum photon energy - auto max_it = --grid_.energy.end(); - while (*max_it > std::log(data::energy_max[i_photon_])) { max_it--; } - grid_.energy.erase(max_it - 1, grid_.energy.end()); - // insert the maximum photon energy at the end - grid_.energy.insert(grid_.energy.end(), std::log(data::energy_max[i_photon_])); + // Post-process the energy grid now that all points from photon interactions are included. + // This sorts the energy points, removes duplicates, and removes all energies exceeding + // photon transport bounds. + post_process_grid(i_photon_, grid_); } double PhotonMajorant::calculate_photon_xs(double energy) const @@ -457,6 +455,9 @@ void PhotonMajorant::fill_material_maj_xs(int i_material, double max_density_mul { const auto & mat = *model::materials[i_material]; + mat_maj.resize(to_grid.size()); + std::fill(mat_maj.begin(), mat_maj.end(), 0.0); + for (int i_energy = 0; i_energy < to_grid.size(); ++i_energy) { mat_maj[i_energy] = 0.0; const double union_log_energy = to_grid[i_energy]; From 85dd343263c45ed95b181bc9d4a138cffedb1aff Mon Sep 17 00:00:00 2001 From: nuclearkevin <66632997+nuclearkevin@users.noreply.github.com> Date: Wed, 10 Jun 2026 13:05:09 -0500 Subject: [PATCH 44/73] Shouldn't need this going into a boundary crossing. --- src/particle.cpp | 11 +++++------ 1 file changed, 5 insertions(+), 6 deletions(-) diff --git a/src/particle.cpp b/src/particle.cpp index f5c56d160c1..4c224426b61 100644 --- a/src/particle.cpp +++ b/src/particle.cpp @@ -817,12 +817,11 @@ void Particle::cross_reflective_bc(const Surface& surf, Direction new_u) // the lower universes. // (unless we're using a dagmc model, which has exactly one universe) n_coord() = 1; - if (surf.geom_type() != GeometryType::DAG && !neighbor_list_find_cell(*this)) { - if (!exhaustive_find_cell(*this)) { - mark_as_lost("Couldn't find particle after reflecting from surface " + - std::to_string(surf.id_) + "."); - return; - } + if (surf.geom_type() != GeometryType::DAG && + !neighbor_list_find_cell(*this)) { + mark_as_lost("Couldn't find particle after reflecting from surface " + + std::to_string(surf.id_) + "."); + return; } // Set previous coordinate going slightly past surface crossing From c832cbc2433d1f59a2229f5e7cf1526b0afb0754 Mon Sep 17 00:00:00 2001 From: nuclearkevin <66632997+nuclearkevin@users.noreply.github.com> Date: Wed, 10 Jun 2026 13:15:19 -0500 Subject: [PATCH 45/73] Remove more duplicate flagging. --- src/particle.cpp | 6 ------ 1 file changed, 6 deletions(-) diff --git a/src/particle.cpp b/src/particle.cpp index 4c224426b61..d88aa06b674 100644 --- a/src/particle.cpp +++ b/src/particle.cpp @@ -423,12 +423,6 @@ void Particle::event_cross_surface() void Particle::event_collide() { - // Store pre-collision particle properties - wgt_last() = wgt(); - E_last() = E(); - u_last() = u(); - r_last() = r(); - // Score collision estimate of keff if (settings::run_mode == RunMode::EIGENVALUE && type().is_neutron()) { keff_tally_collision() += wgt() * macro_xs().nu_fission / macro_xs().total; From 54a6eb9d5ef1c468ea85c3027de8849fe98494f7 Mon Sep 17 00:00:00 2001 From: nuclearkevin <66632997+nuclearkevin@users.noreply.github.com> Date: Wed, 10 Jun 2026 14:39:54 -0500 Subject: [PATCH 46/73] More cleanup. --- include/openmc/geometry.h | 5 + include/openmc/majorant.h | 182 ++++++++++-------- src/settings.cpp | 4 + src/simulation.cpp | 9 +- .../delta_tracking/__init__.py | 0 tests/regression_tests/delta_tracking/test.py | 53 ----- 6 files changed, 114 insertions(+), 139 deletions(-) delete mode 100644 tests/regression_tests/delta_tracking/__init__.py delete mode 100644 tests/regression_tests/delta_tracking/test.py diff --git a/include/openmc/geometry.h b/include/openmc/geometry.h index 487d62b4a3a..1d8ac297eb8 100644 --- a/include/openmc/geometry.h +++ b/include/openmc/geometry.h @@ -73,12 +73,17 @@ void cross_lattice( //============================================================================== //! Find the next boundary a particle will intersect. +//! \param p A geometry state to compute distances with. +//! \return Boundary information corresponding to the nearest surface. //============================================================================== BoundaryInfo distance_to_boundary(GeometryState& p); //============================================================================== //! Find the next external boundary a particle will intersect. +//! +//! \param p A geometry state to compute distances with. +//! \return Boundary information corresponding to the nearest external boundary. //============================================================================== BoundaryInfo distance_to_external_boundary(GeometryState& p); diff --git a/include/openmc/majorant.h b/include/openmc/majorant.h index 5485d7c8e2f..1e9f28be432 100644 --- a/include/openmc/majorant.h +++ b/include/openmc/majorant.h @@ -1,5 +1,5 @@ //! \file majorant.h -//! \brief Majorant cross section type +//! Majorant cross section type #ifndef OPENMC_MAJORANT_H #define OPENMC_MAJORANT_H @@ -34,56 +34,58 @@ class Majorant { //---------------------------------------------------------------------------- // Methods - //! \brief A function to unionize particle energy grids. + //! A function to unionize particle energy grids. virtual void compute_unionized_grid() = 0; - //! \brief Populate the majorant cross section. + //! A function to populate the majorant cross section. void compute_majorant(); protected: //---------------------------------------------------------------------------- // Protected Methods - //! \brief Compute a per-material macroscopic majorant cross section in units - //! of [cm^-1] - // + //! Compute a per-material macroscopic majorant cross section in units + //! of [cm^-1] + //! //! \param[in] mat The material to compute the majorant cross section of //! \param[in] to_grid The grid points to evaluate the majorant at in [eV] //! \param[out] mat_maj The array to write the macroscopic majorant to. - //! The resulting cross section has units of [cm^-1] + //! The resulting cross section has units of [cm^-1] virtual void fill_material_maj_xs(int i_material, double max_density_mult, const std::vector & to_grid, std::vector & mat_maj) = 0; - //! \brief Post-processes the energy grid by calling std::sort(), std::unique(). - //! This also removes all energy values below the transport minimum and - //! above the transport maximum for a given particle type. + //! Post-processes the energy grid by calling std::sort(), std::unique(). + //! This also removes all energy values below the transport minimum and + //! above the transport maximum for a given particle type. //! - //! \param[in] particle_type the particle type transport index to use when - //! fetching transport minimum / maximum energies - //! \param[out] grid the energy grid to post-process. This is performed - //! in-place. + //! \param[in] particle_type The particle type transport index to use when + //! fetching transport minimum / maximum energies + //! \param[out] grid The energy grid to post-process. This is performed + //! in-place. void post_process_grid(int particle_type, Nuclide::EnergyGrid & grid); - //! \brief Helper function to perform linear interpolation. - // - //! \param[in] x_0 The first x coordinate - //! \param[in] x_1 The second x coordinate - //! \param[in] y_0 The y coordinate associated with x_0 - //! \param[in] y_1 The y coordinate associated with x_1 - //! \param[in] x The point between x_0 and x_1 to find a y value at + //! Helper function to perform linear interpolation. + //! + //! \param[in] x_0 The first x coordinate. + //! \param[in] x_1 The second x coordinate. + //! \param[in] y_0 The y coordinate associated with x_0. + //! \param[in] y_1 The y coordinate associated with x_1. + //! \param[in] x The point between x_0 and x_1 to find a y value at. + //! \return The linear interpolant of the given points. inline double interpolate_lin_1D(double x_0, double x_1, double y_0, double y_1, double x) const { const double f = (x - x_0) / (x_1 - x_0); return (1.0 - f) * y_0 + f * y_1; } - //! \brief Helper function to perform log interpolation. - // - //! \param[in] x_0 The first x coordinate - //! \param[in] x_1 The second x coordinate - //! \param[in] y_0 The y coordinate associated with x_0 - //! \param[in] y_1 The y coordinate associated with x_1 - //! \param[in] x The point between x_0 and x_1 to find a y value at + //! Helper function to perform log interpolation. + //! + //! \param[in] x_0 The first x coordinate. + //! \param[in] x_1 The second x coordinate. + //! \param[in] y_0 The y coordinate associated with x_0. + //! \param[in] y_1 The y coordinate associated with x_1. + //! \param[in] x The point between x_0 and x_1 to find a y value at. + //! \return The log interpolant of the given points. inline double interpolate_log_1D(double x_0, double x_1, double y_0, double y_1, double x) const { const double f = std::log(x / x_0) / std::log(x_1 / x_0); @@ -93,13 +95,18 @@ class Majorant { //---------------------------------------------------------------------------- // Protected data members - int maj_universe_ = C_NONE; //!< Index into the universe array for the universe which this - // majorant uses to fetch material properties. - std::vector contained_materials_; //!< A vector of materials contained in maj_universe_ - std::unordered_map max_density_mult_; //!< A map of each material index and the corresponding - // maximum density multiplier applied to that material by a cell - Nuclide::EnergyGrid grid_; //!< The unionized energy grid - std::vector xs_; //!< Macroscopic majorant cross sections at each grid point in grid_ + //! Index into the universe array for the universe which this majorant uses + //! to fetch material properties. + int maj_universe_ = C_NONE; + //!< A vector of materials contained in maj_universe_. + std::vector contained_materials_; + //! A map of each material index and the corresponding maximum density + //! multiplier applied to that material by a cell. + std::unordered_map max_density_mult_; + //! The unionized energy grid. + Nuclide::EnergyGrid grid_; + //! Macroscopic majorant cross sections at each energy point in grid_. + std::vector xs_; }; // class Majorant //============================================================================== @@ -117,27 +124,29 @@ class NeutronMajorant : public Majorant { virtual void compute_unionized_grid() override final; - //! \brief Calculate the macroscopic majorant cross section in units of [cm^-1] - // + //! Calculate the macroscopic majorant cross section. + //! //! \param[in] energy The energy to compute the cross section at in [eV] + //! \return The neutron majorant cross section at the given energy in [cm^-1] double calculate_neutron_xs(double energy) const; //---------------------------------------------------------------------------- // Data members - constexpr static double safety_factor_ {1.01}; //!< A dilation factor to ensure floating-point round - //!< off and inexact majorant URR and S(a,b) cross sections - //!< don't bias results + //! A dilation factor to ensure floating-point round off and inexact majorant + //! URR and S(a,b) cross sections don't bias results. + constexpr static double safety_factor_ {1.01}; protected: //---------------------------------------------------------------------------- // Protected Methods - //! \brief Compute a per-material macroscopic majorant cross section in units of [cm^-1] - // - //! \param[in] i_material Index into the materials array - //! \param[in] to_grid The grid points to evaluate the majorant at in [eV] - //! \param[out] mat_maj The array to write the macroscopic majorant to. The resulting cross section has units of [cm^-1] + //! Compute a per-material macroscopic majorant cross section. + //! + //! \param[in] i_material Index into the materials array. + //! \param[in] to_grid The grid points to evaluate the majorant at in [eV]. + //! \param[out] mat_maj The array to write the macroscopic majorant to. + //! The resulting cross section has units of [cm^-1]. virtual void fill_material_maj_xs(int i_material, double max_density_mult, const std::vector & to_grid, std::vector & mat_maj) override; @@ -145,31 +154,39 @@ class NeutronMajorant : public Majorant { //---------------------------------------------------------------------------- // Private Methods - //! \brief Compute the maximum smooth microscopic total cross section in units of [barn]. - // - //! \param[in] i_nuclide Index into the nuclides array - //! \param[in] energy The energy to evaluate the cross section at in [eV] + //! Compute the maximum smooth microscopic total cross section. + //! + //! \param[in] i_nuclide Index into the nuclides array. + //! \param[in] energy The energy to evaluate the cross section at in [eV]. + //! \return The maximum smooth cross section in [barn]. double calculate_max_smooth_xs(int i_nuclide, double energy) const; - //! \brief Compute the maximum microscopic total URR cross section in units of [barn]. - // - //! \param[in] i_nuclide Index into the nuclides array - //! \param[in] energy The energy to evaluate the cross section at in [eV] - //! \param[in] smooth_xs The smooth total cross section in units of [eV] to use (if needed by the ptable) + //! Compute the maximum microscopic total URR cross section. + //! + //! \param[in] i_nuclide Index into the nuclides array. + //! \param[in] energy The energy to evaluate the cross section at in [eV]. + //! \param[in] smooth_xs The smooth total cross section in units of [eV] to + //! use (if needed by the ptable). + //! \return The maximum URR total cross section in [barn]. double calculate_max_urr_xs(int i_nuclide, double energy, double smooth_xs) const; - //! \brief Compute the maximum microscopic S(a,b) total cross section in units of [barn] - // - //! \param[in] energy The energy to evaluate the cross section at in [eV] - //! \param[in] i_sab The index into the thermal scattering table array for this nuclide - //! \param[in] sab_frac The fraction of the bound cross section to use vs the free gas cross section - //! \param[in] nuc The nuclide to compute the microscopic total cross section of + //! Compute the maximum microscopic S(a,b) total cross section. + //! + //! \param[in] energy The energy to evaluate the cross section at in [eV]. + //! \param[in] i_sab The index into the thermal scattering table array for + //! this nuclide. + //! \param[in] sab_frac The fraction of the bound cross section to use vs the + //! free gas cross section. + //! \param[in] nuc The nuclide to compute the microscopic total cross section + //! of. + //! \return The maximum S(a,b) total cross section in [barn]. double calculate_max_sab_tot_xs(int i_nuclide, int i_sab, double sab_frac, double energy) const; - //! \brief Get the grid index for energy interpolation - // - //! \param[in] energy The energy to evaluate the cross section at in [eV] - //! \param[in] grid The energy grid to search for an energy grid index + //! Get the grid index for energy interpolation. + //! + //! \param[in] energy The energy to evaluate the cross section at in [eV]. + //! \param[in] grid The energy grid to search for an energy grid index. + //! \return The grid index. int get_i_grid(double energy, const Nuclide::EnergyGrid & grid) const; //---------------------------------------------------------------------------- @@ -192,25 +209,28 @@ class PhotonMajorant : public Majorant { virtual void compute_unionized_grid() override final; - //! \brief Calculate the macroscopic majorant cross section in units of [cm^-1] - // + //! Calculate the macroscopic majorant cross section. + //! //! \param[in] energy The energy to compute the cross section at in [eV] + //! \return The photon majorant cross section at the given energy in [cm^-1] double calculate_photon_xs(double energy) const; //---------------------------------------------------------------------------- // Data members - constexpr static double safety_factor_ {1.01}; //!< A dilation factor to catch interpolation error + //! A dilation factor to catch interpolation error. + constexpr static double safety_factor_ {1.01}; protected: //---------------------------------------------------------------------------- // Protected Methods - //! \brief Compute a per-material macroscopic majorant cross section in units of [cm^-1] - // + //! Compute a per-material macroscopic majorant cross section. + //! //! \param[in] i_material Index into the materials array //! \param[in] to_grid The grid points to evaluate the majorant at in [eV] - //! \param[out] mat_maj The array to write the macroscopic majorant to. The resulting cross section has units of [cm^-1] + //! \param[out] mat_maj The array to write the macroscopic majorant to. The + //! resulting cross section has units of [cm^-1] virtual void fill_material_maj_xs(int i_material, double max_density_mult, const std::vector & to_grid, std::vector & mat_maj) override; @@ -218,16 +238,18 @@ class PhotonMajorant : public Majorant { //---------------------------------------------------------------------------- // Private Methods - //! \brief Compute the maximum smooth microscopic total cross section in units of [barn]. - // - //! \param[in] i_element Index in the elements array - //! \param[in] energy The energy to evaluate the cross section at in [eV] + //! Compute the maximum smooth microscopic total cross section. + //! + //! \param[in] i_element Index in the elements array. + //! \param[in] energy The energy to evaluate the cross section at in [eV]. + //! \return The maximum microscopic total cross section in [barn]. double calculate_elem_tot_xs(int i_element, double log_energy) const; - //! \brief Get the grid index for energy interpolation - // - //! \param[in] energy The energy to evaluate the cross section at in [eV] - //! \param[in] grid The energy grid to search for an energy grid index + //! Get the grid index for energy interpolation. + //! + //! \param[in] energy The energy to evaluate the cross section at in [eV]. + //! \param[in] grid The energy grid to search for an energy grid index. + //! \return The grid index. int get_i_grid(double log_energy, const std::vector & energy_grid) const; int get_i_grid(double log_energy, const tensor::Tensor & energy_grid) const; @@ -241,10 +263,10 @@ class PhotonMajorant : public Majorant { // Static functions //============================================================================== -//! \brief A function to create majorant cross sections. +//! A function to create majorant cross sections. void create_majorants(); -//! \brief A function to reset majorant cross sectiosn. +//! A function to reset majorant cross sections. void reset_majorants(); } // namespace openmc diff --git a/src/settings.cpp b/src/settings.cpp index e38de2697da..77e3b7ac03c 100644 --- a/src/settings.cpp +++ b/src/settings.cpp @@ -1238,6 +1238,10 @@ void read_settings_xml(pugi::xml_node root) if (temperature_multipole && delta_tracking) { fatal_error("Delta tracking cannot be used with a windowed multipole temperature treatment."); } + + if (!run_CE && delta_tracking) { + fatal_error("At present, delta tracking can only be used in continuous energy simulations."); + } } // Check whether material cell offsets should be generated diff --git a/src/simulation.cpp b/src/simulation.cpp index 4044c7618f0..e6b428db1b3 100644 --- a/src/simulation.cpp +++ b/src/simulation.cpp @@ -936,12 +936,11 @@ void transport_delta_history_based_single_particle(Particle& p) void transport_history_based() { - const bool using_delta_tracking = settings::delta_tracking; #pragma omp parallel for schedule(runtime) for (int64_t i_work = 1; i_work <= simulation::work_per_rank; ++i_work) { Particle p; initialize_particle_track(p, i_work, false); - if (using_delta_tracking) { + if (settings::delta_tracking) { transport_delta_history_based_single_particle(p); } else { transport_history_based_single_particle(p); @@ -959,8 +958,6 @@ void transport_history_based() // continues until there are no more secondary tracks left to transport. void transport_history_based_shared_secondary() { - const bool using_delta_tracking = settings::delta_tracking; - // Clear shared secondary banks from any prior use simulation::shared_secondary_bank_read.clear(); simulation::shared_secondary_bank_write.clear(); @@ -985,7 +982,7 @@ void transport_history_based_shared_secondary() for (int64_t i = 1; i <= simulation::work_per_rank; i++) { Particle p; initialize_particle_track(p, i, false); - if (using_delta_tracking) { + if (settings::delta_tracking) { transport_delta_history_based_single_particle(p); } else { transport_history_based_single_particle(p); @@ -1057,7 +1054,7 @@ void transport_history_based_shared_secondary() initialize_particle_track(p, i, true); SourceSite& site = simulation::shared_secondary_bank_read[i - 1]; p.event_revive_from_secondary(site); - if (using_delta_tracking) { + if (settings::delta_tracking) { transport_delta_history_based_single_particle(p); } else { transport_history_based_single_particle(p); diff --git a/tests/regression_tests/delta_tracking/__init__.py b/tests/regression_tests/delta_tracking/__init__.py deleted file mode 100644 index e69de29bb2d..00000000000 diff --git a/tests/regression_tests/delta_tracking/test.py b/tests/regression_tests/delta_tracking/test.py deleted file mode 100644 index f92f690e9dc..00000000000 --- a/tests/regression_tests/delta_tracking/test.py +++ /dev/null @@ -1,53 +0,0 @@ -import glob -import os - -import openmc -import pytest - -from tests.testing_harness import PyAPITestHarness - -def delta_tracking_model(nuclide): - model = openmc.model.Model() - - model.materials = openmc.Materials() - - # single material - mat = openmc.Material() - mat.add_nuclide(nuclide, 1.0) - mat.set_density('g/cc', 19.1) - mat.depletable = True - - model.materials.append(mat) - - # geometry (infinite cell medium) - cell = openmc.Cell() - cell.fill = mat - cell.temperature = 300 - - prism = openmc.model.rectangular_prism(1000.00, 1000.00, boundary_type='vacuum') - - cell.region = prism - - model.geometry = openmc.Geometry([cell,]) - - settings = model.settings - settings.run_mode = 'fixed source' - settings.particles = 100 - settings.batches = 10 - settings.inactive = 1 - settings.delta_tracking = True - - return model - -nuclides = ('Fe56', 'U238', 'H1', 'O16', 'Zr91', 'Zr90', 'Zr92', 'Zr94', 'Zr96', 'B10', 'B11') -@pytest.mark.parametrize('nuclide', nuclides) -def test_delta_tracking(nuclide): - model = delta_tracking_model(nuclide) - harness = PyAPITestHarness('statepoint.10.h5',model=model) - harness._build_inputs() - harness._run_openmc() - harness._cleanup() - output = glob.glob("*.txt") - for f in output: - if os.path.exists(f): - os.remove(f) From a730bdc8362509b0a7074498b8cdd2f8e6606a3e Mon Sep 17 00:00:00 2001 From: nuclearkevin <66632997+nuclearkevin@users.noreply.github.com> Date: Wed, 10 Jun 2026 14:51:36 -0500 Subject: [PATCH 47/73] Round trip delta_tracking alongside the majorant build time in the SP. --- openmc/statepoint.py | 6 ++++++ src/state_point.cpp | 5 +++++ 2 files changed, 11 insertions(+) diff --git a/openmc/statepoint.py b/openmc/statepoint.py index a10ec3a839d..5f0568c9ec1 100644 --- a/openmc/statepoint.py +++ b/openmc/statepoint.py @@ -97,6 +97,8 @@ class StatePoint: Working directory for simulation photon_transport : bool Indicate whether photon transport is active + delta_tracking : bool + Indicate whether delta tracking is active run_mode : str Simulation run mode, e.g. 'eigenvalue' runtime : dict @@ -350,6 +352,10 @@ def path(self): def photon_transport(self): return self._f.attrs['photon_transport'] > 0 + @property + def delta_tracking(self): + return self._f.attrs['delta_tracking'] > 0 + @property def run_mode(self): return self._f['run_mode'][()].decode() diff --git a/src/state_point.cpp b/src/state_point.cpp index da1c141a230..0970648178f 100644 --- a/src/state_point.cpp +++ b/src/state_point.cpp @@ -107,6 +107,7 @@ extern "C" int openmc_statepoint_write(const char* filename, bool* write_source) break; } write_attribute(file_id, "photon_transport", settings::photon_transport); + write_attribute(file_id, "delta_tracking", settings::delta_tracking); write_dataset(file_id, "n_particles", settings::n_particles); write_dataset(file_id, "n_batches", settings::n_batches); @@ -306,6 +307,10 @@ extern "C" int openmc_statepoint_write(const char* filename, bool* write_source) hid_t runtime_group = create_group(file_id, "runtime"); write_dataset( runtime_group, "total initialization", time_initialize.elapsed()); + if (settings::delta_tracking) { + write_dataset( + runtime_group, "build majorant", time_build_majorant.elapsed()); + } write_dataset( runtime_group, "reading cross sections", time_read_xs.elapsed()); write_dataset(runtime_group, "simulation", From d008b79e181822e59fcc1f99bf51c7d5af5f9555 Mon Sep 17 00:00:00 2001 From: nuclearkevin <66632997+nuclearkevin@users.noreply.github.com> Date: Wed, 10 Jun 2026 15:07:38 -0500 Subject: [PATCH 48/73] Remove event limit. --- src/particle.cpp | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/particle.cpp b/src/particle.cpp index d88aa06b674..3fe2bc49e1d 100644 --- a/src/particle.cpp +++ b/src/particle.cpp @@ -564,7 +564,7 @@ void Particle::event_check_limit_and_revive() { // If particle has too many events, display warning and kill it n_event()++; - if (n_event() == settings::max_particle_events && !delta_tracking()) { + if (n_event() == settings::max_particle_events) { warning("Particle " + std::to_string(id()) + " underwent maximum number of events."); wgt() = 0.0; From e1ee20e8404678818aa3baae9ad1b90f617b4276 Mon Sep 17 00:00:00 2001 From: nuclearkevin <66632997+nuclearkevin@users.noreply.github.com> Date: Thu, 11 Jun 2026 13:22:42 -0500 Subject: [PATCH 49/73] Delta tracking theory. --- docs/source/methods/neutron_physics.rst | 116 ++++++++++++++++++++++++ 1 file changed, 116 insertions(+) diff --git a/docs/source/methods/neutron_physics.rst b/docs/source/methods/neutron_physics.rst index 2b797e3dbcf..dca91b3a269 100644 --- a/docs/source/methods/neutron_physics.rst +++ b/docs/source/methods/neutron_physics.rst @@ -52,6 +52,106 @@ the formula usually used to calculate the distance to next collision is \ell = -\frac{\ln \xi}{\Sigma_t} +.. _surface_tracking: + +~~~~~~~~~~~~~~~~~~~~~~~~~ +Surface Tracking +~~~~~~~~~~~~~~~~~~~~~~~~~ + +The development of Equation :eq:`sample-distance-2` requires the assumption +that the medium under consideration is homogeneous. To accomodate heterogeneous +geometries, a resampling scheme is used. First, the distance to the next +collision is sampled with Equation :eq:`sample-distance-2`. Then, the distance +to the nearest surface from the particle position along its current trajectory +is computed (discussed in the :ref:`methods_geometry` section). If the distance +to the nearest surface is smaller than the distance to the next collision, the +sampled distance is not statistically valid and the particle is moved to the +surface and is considered to be contained by the next geometric region. Cross +sections are recomputed, and a new distance to the next collision is sampled +with Equation :eq:`sample-distance-2`. This process repeats until the distance +to the next collision is smaller than the distance to the nearest surface, +which is when a collision is accepted. This procedure is known as surface +tracking. + +Surface tracking is quite efficient when used in problems with short mean free +paths relative to the size of individual regions in the problem geometry. +Surface tracking also admits the use of the track length estimator (discussed +in the :ref:`methods_tallies` section). In problems with many geometric regions +across particle flights, surface tracking will require a large number of surface +distance calculations and collision distance samples. The cost of finding the +nearest surface is also non-trivial for problems that contain many geometric +regions at the same cell level (e.g. TRISO-fueled fission reactors). + +.. _delta_tracking: + +~~~~~~~~~~~~~~~~~~~~~~~~~ +Delta Tracking +~~~~~~~~~~~~~~~~~~~~~~~~~ + +The disadvantages of surface tracking for certain classes of problems motivates +the development of alternative approaches which do not require distance +to surface checks. Delta tracking (also known by Woodcock tracking, +delta scattering, and null scattering) is one approach to +avoid surface geometry queries [Woodcock]_. In delta tracking, the domain is +homogenized by adding a fictitious delta scattering cross sections +:math:`\Sigma_{\delta}(\mathbf{r}, E)` to the medium. The sum of the delta +scattering cross section and the total cross section is known as the majorant +cross section (:math:`\Sigma_{maj}(E)`) + +.. math:: + :label: majorant-xs-1 + + \Sigma_{maj}(E) = \Sigma_{t}(\mathbf{r}, E) + \Sigma_{\delta}(\mathbf{r}, + E), + +which is computed as the maximum macroscopic total cross section over the +spatial domain + +.. math:: + :label: majorant-xs-2 + + \Sigma_{maj}(E) = \max_{\mathbf{r}}\left(\Sigma_{t}(\mathbf{r}, E)\right). + +The delta tracking procedure computes the distance to the next collision +with the majorant cross section instead of the total cross section + +.. math:: + :label: sample-distance-maj + + \ell = -\frac{\ln \xi}{\Sigma_{maj}(E)}. + +This is followed by a rejection sampling test to determine if a real +collision or a delta scattering collision occured at this delta collision +point. A random number :math:`\xi` on the interval :math:`[0,1)` is drawn and +used to check + +.. math:: + :label: delta-real-collision + + \xi < \frac{\Sigma_t (\mathbf{r}, E)}{\Sigma_{maj} (E)}. + +If the condition is true, the collision is accepted as real. If the condition +is false, a delta scatter event has occured and the particle continues along +its trajectory with the same energy and direction. Boundary conditions +are applied by testing the distance to the nearest external boundary and +comparing this to the distance sampled with Equation :eq:`sample-distance-maj`. +If the distance to the nearest boundary is less than the sampled distance to +the next collision, the particle crosses the external boundary. + +Delta tracking is advantageous compared to surface tracking as it allows +for continuously-varying material properties due to the use of pointwise +cross section samples in Equation :eq:`delta-real-collision`. Delta tracking +often performs better than surface tracking in problems where the particle +mean free path is larger than the distance between surfaces. Problems +containing small regions with large total cross sections (such as burnable +absorbers) will have majorant cross sections several orders of magnitude in +larger than the total cross section over the majority of the domain +[Leppänen]_. This decreases the number of accepted collisions, and therefore +the effectiveness of delta tracking. Material discontinuities are not +considered in delta tracking, which prohibits the use of track length +estimators and forces the use of the higher- variance collision estimator +(discussed in detail in the :ref:`methods_tallies` section). + ---------------------------------------------------- :math:`(n,\gamma)` and Other Disappearance Reactions ---------------------------------------------------- @@ -1631,6 +1731,13 @@ the unresolved range to get the actual cross sections. Lastly, the total cross section is calculated as the sum of the elastic, fission, capture, and inelastic cross sections. +Unresolved resonance probability tables pose a challenge when computing a majorant +cross section for :ref:`delta_tracking`. OpenMC implements a conservative approach: +the maximum total cross section is computed over all bands, which is then +interpolated to the corresponding energy using either linear or logarithmic +interpolation. This ensures the majorant bounds the total cross section at the +cost of increasing the number of delta scatters in the unresolved range. + ----------------------------- Variance Reduction Techniques ----------------------------- @@ -1735,6 +1842,10 @@ types. .. [Gelbard] Ely M. Gelbard, "Epithermal Scattering in VIM," FRA-TM-123, Argonne National Laboratory (1979). +.. [Leppänen] J. Leppänen. "Performance of Woodcock Delta-Tracking in Lattice + Physics Applications using the Serpent Monte Carlo Reactor Physics Burnup + Calculation Code", *Annals of Nuclear Energy*, 37:715-722, 2010. + .. [Squires] G. L. Squires, *Introduction to the Theory of Thermal Neutron Scattering*, Cambridge University Press (1978). @@ -1742,6 +1853,11 @@ types. Neutrons*, North-Holland Publishing Co., Amsterdam (1966). **Note:** This book can be obtained for free from the OECD_. +.. [Woodcock] E.R. Woodcock, T. Murphy, P.J. Hemmings, and T.C. Longworth. + "Techniques used in the GEM Code for Monte Carlo Neutronics Calculations + in Reactors and other Systems of Complex Geometry", ANL-7050, + Argonne National Laboratory (1965). + .. |sab| replace:: S(:math:`\alpha,\beta,T`) .. _SIGMA1 method: https://doi.org/10.13182/NSE76-1 From 8377feecf88a7f81fadcb690e898280396779cdf Mon Sep 17 00:00:00 2001 From: nuclearkevin <66632997+nuclearkevin@users.noreply.github.com> Date: Thu, 11 Jun 2026 13:41:59 -0500 Subject: [PATCH 50/73] Test round tripping the delta tracking setting. --- openmc/settings.py | 19 +++++++++++++++---- tests/unit_tests/test_settings.py | 2 ++ 2 files changed, 17 insertions(+), 4 deletions(-) diff --git a/openmc/settings.py b/openmc/settings.py index f5b3325dd4d..dac168b0e06 100644 --- a/openmc/settings.py +++ b/openmc/settings.py @@ -94,6 +94,10 @@ class Settings: release of delayed photons. .. versionadded:: 0.12 + delta_tracking : bool + Whether transport should be performed with delta tracking or not. + + .. versionadded:: 0.15.4 electron_treatment : {'led', 'ttb'} Whether to deposit all energy from electrons locally ('led') or create secondary bremsstrahlung photons ('ttb'). @@ -2011,10 +2015,6 @@ def _create_max_tracks_subelement(self, root): if self._max_tracks is not None: elem = ET.SubElement(root, "max_tracks") elem.text = str(self._max_tracks) - def _create_delta_tracking_subelement(self, root): - if self._delta_tracking: - elem = ET.SubElement(root, "delta_tracking") - elem.text = str(self._delta_tracking).lower() def _create_random_ray_subelement(self, root, mesh_memo=None): if self._random_ray: @@ -2075,6 +2075,11 @@ def _create_free_gas_threshold_subelement(self, root): element = ET.SubElement(root, "free_gas_threshold") element.text = str(self._free_gas_threshold) + def _create_delta_tracking_subelement(self, root): + if self._delta_tracking: + elem = ET.SubElement(root, "delta_tracking") + elem.text = str(self._delta_tracking).lower() + def _eigenvalue_from_xml_element(self, root): elem = root.find('eigenvalue') if elem is not None: @@ -2573,6 +2578,11 @@ def _free_gas_threshold_from_xml_element(self, root): if text is not None: self.free_gas_threshold = float(text) + def _delta_tracking_from_xml_element(self, root): + text = get_text(root, 'delta_tracking') + if text is not None: + self.delta_tracking = text in ('true', '1') + def to_xml_element(self, mesh_memo=None): """Create a 'settings' element to be written to an XML file. @@ -2769,6 +2779,7 @@ def from_xml_element(cls, elem, meshes=None): settings._use_decay_photons_from_xml_element(elem) settings._source_rejection_fraction_from_xml_element(elem) settings._free_gas_threshold_from_xml_element(elem) + settings._delta_tracking_from_xml_element(elem) return settings diff --git a/tests/unit_tests/test_settings.py b/tests/unit_tests/test_settings.py index bdb3ea8fe9f..18aa9f6cffa 100644 --- a/tests/unit_tests/test_settings.py +++ b/tests/unit_tests/test_settings.py @@ -94,6 +94,7 @@ def test_export_to_xml(run_in_tmpdir): s.max_secondaries = 1_000_000 s.source_rejection_fraction = 0.01 s.free_gas_threshold = 800.0 + s.delta_tracking = True # Make sure exporting XML works s.export_to_xml() @@ -190,6 +191,7 @@ def test_export_to_xml(run_in_tmpdir): assert s.max_secondaries == 1_000_000 assert s.source_rejection_fraction == 0.01 assert s.free_gas_threshold == 800.0 + assert s.delta_tracking == True def test_properties_file_load(tmp_path, mpi_intracomm): From 5af0f003b42b01fa8824c3e39b53859d5ab713d2 Mon Sep 17 00:00:00 2001 From: nuclearkevin <66632997+nuclearkevin@users.noreply.github.com> Date: Thu, 11 Jun 2026 14:29:00 -0500 Subject: [PATCH 51/73] Fix distributed density majorant bug. --- src/majorant.cpp | 18 +++++++++--------- 1 file changed, 9 insertions(+), 9 deletions(-) diff --git a/src/majorant.cpp b/src/majorant.cpp index f2d5a68f34f..286605597b4 100644 --- a/src/majorant.cpp +++ b/src/majorant.cpp @@ -42,30 +42,30 @@ Majorant::Majorant(int i_universe) const auto & maj_uni = model::universes[maj_universe_]; for (int i_cell : maj_uni->cells_) { - const auto & cell = model::cells[i_cell]; + const auto & uni_cell = model::cells[i_cell]; // If the cell is filled with a material, it won't have any sub-cells. - if (cell->type_ == Fill::MATERIAL) { + if (uni_cell->type_ == Fill::MATERIAL) { // Loop over instances. TODO: confirm if this is unecessary and use 0 instead? - for (int instance = 0; instance < cell->n_instances(); ++instance) { - int i_material = cell->material(instance); + for (int instance = 0; instance < uni_cell->n_instances(); ++instance) { + int i_material = uni_cell->material(instance); // Check to see if we've found the contained material yet. If not, add to the set // of materials discovered and add to the map of density multipliers. if (unique_materials.count(i_material) == 0) { unique_materials.emplace(i_material); - max_density_mult_[i_material] = cell->density_mult(instance); + max_density_mult_[i_material] = uni_cell->density_mult(instance); } else { // We've found this material already. Need to take the maximum density multiplier. max_density_mult_.at(i_material) = std::max(max_density_mult_.at(i_material), - cell->density_mult(instance)); + uni_cell->density_mult(instance)); } } } else { // This cell is filled with a universe or lattice. Need to get the list of cells and // cell instances. - const auto contained_cells = cell->get_contained_cells(); + const auto contained_cells = uni_cell->get_contained_cells(); for (const auto & [i_con_cell, contained_instances] : contained_cells) { const auto & contained_cell = model::cells[i_con_cell]; @@ -76,12 +76,12 @@ Majorant::Majorant(int i_universe) int i_material = contained_cell->material(instance); if (unique_materials.count(i_material) == 0) { unique_materials.emplace(i_material); - max_density_mult_[i_material] = cell->density_mult(instance); + max_density_mult_[i_material] = contained_cell->density_mult(instance); } else { // We've found this material already. Need to take the maximum density multiplier // for the contained instance. max_density_mult_.at(i_material) = - std::max(max_density_mult_.at(i_material), cell->density_mult(instance)); + std::max(max_density_mult_.at(i_material), contained_cell->density_mult(instance)); } } } From c1c05479d5b2245b6654fa0de76f5d53508f77a7 Mon Sep 17 00:00:00 2001 From: nuclearkevin <66632997+nuclearkevin@users.noreply.github.com> Date: Thu, 11 Jun 2026 15:05:28 -0500 Subject: [PATCH 52/73] More majorant fixes. --- include/openmc/majorant.h | 2 +- src/majorant.cpp | 50 +++++++++++++++++++++++++-------------- 2 files changed, 33 insertions(+), 19 deletions(-) diff --git a/include/openmc/majorant.h b/include/openmc/majorant.h index 1e9f28be432..a2b958c37c4 100644 --- a/include/openmc/majorant.h +++ b/include/openmc/majorant.h @@ -62,7 +62,7 @@ class Majorant { //! fetching transport minimum / maximum energies //! \param[out] grid The energy grid to post-process. This is performed //! in-place. - void post_process_grid(int particle_type, Nuclide::EnergyGrid & grid); + void post_process_grid(int particle_type, Nuclide::EnergyGrid & grid) const; //! Helper function to perform linear interpolation. //! diff --git a/src/majorant.cpp b/src/majorant.cpp index 286605597b4..0ee9e191bb0 100644 --- a/src/majorant.cpp +++ b/src/majorant.cpp @@ -112,25 +112,39 @@ void Majorant::compute_majorant() } } -void Majorant::post_process_grid(int particle_type, Nuclide::EnergyGrid & grid) +void Majorant::post_process_grid(int particle_type, Nuclide::EnergyGrid & grid) const { - std::sort(grid_.energy.begin(), grid_.energy.end()); - auto unique_end = std::unique(grid_.energy.begin(), grid_.energy.end()); - grid_.energy.resize(std::distance(grid_.energy.begin(), unique_end)); - - // remove all values below the minimum neutron energy - auto min_it = grid_.energy.begin(); - while (*min_it < data::energy_min[particle_type]) { min_it++; } - grid_.energy.erase(grid_.energy.begin(), min_it + 1); - // insert the minimum neutron energy at the beginning - grid_.energy.insert(grid_.energy.begin(), data::energy_min[particle_type]); - - // remove all values above the maximum neutron energy - auto max_it = --grid_.energy.end(); - while (*max_it > data::energy_max[particle_type]) { max_it--; } - grid_.energy.erase(max_it - 1, grid_.energy.end()); - // insert the maximum neutron energy at the end - grid_.energy.insert(grid_.energy.end(), data::energy_max[particle_type]); + // Photons use a logarithmic energy grid. + double E_min = data::energy_min[particle_type]; + double E_max = data::energy_max[particle_type]; + if (particle_type == ParticleType::photon().transport_index()) { + E_min = std::log(E_min); + E_max = std::log(E_max); + } + + std::sort(grid.energy.begin(), grid.energy.end()); + auto unique_end = std::unique(grid.energy.begin(), grid.energy.end()); + grid.energy.resize(std::distance(grid.energy.begin(), unique_end)); + + // Remove all values below the minimum neutron energy. + auto min_it = grid.energy.begin(); + while (*min_it < E_min) { + min_it++; + } + grid.energy.erase(grid.energy.begin(), min_it + 1); + + // Insert the minimum neutron energy at the beginning. + grid.energy.insert(grid.energy.begin(), E_min); + + // Remove all values above the maximum neutron energy. + auto max_it = --grid.energy.end(); + while (*max_it > E_max) { + max_it--; + } + grid.energy.erase(max_it - 1, grid.energy.end()); + + // Insert the maximum neutron energy at the end. + grid.energy.insert(grid.energy.end(), E_max); } //============================================================================== From 77ad917d7c82b82b396204bad8cd905d22ce86af Mon Sep 17 00:00:00 2001 From: nuclearkevin <66632997+nuclearkevin@users.noreply.github.com> Date: Thu, 11 Jun 2026 15:35:08 -0500 Subject: [PATCH 53/73] Test for distributed densities with delta tracking. --- .../False/inputs_true.dat | 62 +++++++++++++++ .../False/results_true.dat | 11 +++ .../True/inputs_true.dat | 78 +++++++++++++++++++ .../True/results_true.dat | 20 +++++ .../delta_tracking_distribrho/__init__.py | 0 .../delta_tracking_distribrho/test.py | 77 ++++++++++++++++++ 6 files changed, 248 insertions(+) create mode 100644 tests/regression_tests/delta_tracking_distribrho/False/inputs_true.dat create mode 100644 tests/regression_tests/delta_tracking_distribrho/False/results_true.dat create mode 100644 tests/regression_tests/delta_tracking_distribrho/True/inputs_true.dat create mode 100644 tests/regression_tests/delta_tracking_distribrho/True/results_true.dat create mode 100644 tests/regression_tests/delta_tracking_distribrho/__init__.py create mode 100644 tests/regression_tests/delta_tracking_distribrho/test.py diff --git a/tests/regression_tests/delta_tracking_distribrho/False/inputs_true.dat b/tests/regression_tests/delta_tracking_distribrho/False/inputs_true.dat new file mode 100644 index 00000000000..6bd03229746 --- /dev/null +++ b/tests/regression_tests/delta_tracking_distribrho/False/inputs_true.dat @@ -0,0 +1,62 @@ + + + + + + + + + + + + + + + + + + 10.0 20.0 10.0 20.0 + + + + + 1.0 1.0 + 2 2 + -1.0 -1.0 + +1 1 +1 1 + + + + + + + + + eigenvalue + 1000 + 10 + 5 + false + true + + + + 2 2 + -1.0 -1.0 + 1.0 1.0 + + + neutron + + + 0 + + + 1 2 + total + collision + + + diff --git a/tests/regression_tests/delta_tracking_distribrho/False/results_true.dat b/tests/regression_tests/delta_tracking_distribrho/False/results_true.dat new file mode 100644 index 00000000000..3bd172ea252 --- /dev/null +++ b/tests/regression_tests/delta_tracking_distribrho/False/results_true.dat @@ -0,0 +1,11 @@ +k-combined: +1.108089E-01 1.861559E-03 +tally 1: +2.237440E+02 +1.002088E+04 +2.261060E+02 +1.023153E+04 +2.226120E+02 +9.912659E+03 +2.252070E+02 +1.014833E+04 diff --git a/tests/regression_tests/delta_tracking_distribrho/True/inputs_true.dat b/tests/regression_tests/delta_tracking_distribrho/True/inputs_true.dat new file mode 100644 index 00000000000..d3b594f2a78 --- /dev/null +++ b/tests/regression_tests/delta_tracking_distribrho/True/inputs_true.dat @@ -0,0 +1,78 @@ + + + + + + + + + + + + + + + + + + 10.0 20.0 10.0 20.0 + + + + + 1.0 1.0 + 2 2 + -1.0 -1.0 + +1 1 +1 1 + + + + + + + + + eigenvalue + 1000 + 10 + 5 + true + true + + + + 2 2 + -1.0 -1.0 + 1.0 1.0 + + + 2 2 + -1.0 -1.0 + 1.0 1.0 + + + neutron + + + 0 + + + photon + + + 1 + + + 1 2 + total + collision + + + 3 4 + total + collision + + + diff --git a/tests/regression_tests/delta_tracking_distribrho/True/results_true.dat b/tests/regression_tests/delta_tracking_distribrho/True/results_true.dat new file mode 100644 index 00000000000..fb37f56563c --- /dev/null +++ b/tests/regression_tests/delta_tracking_distribrho/True/results_true.dat @@ -0,0 +1,20 @@ +k-combined: +1.098307E-01 2.729028E-04 +tally 1: +2.217840E+02 +9.850848E+03 +2.214880E+02 +9.834344E+03 +2.221800E+02 +9.878536E+03 +2.211110E+02 +9.785553E+03 +tally 2: +5.309872E+00 +5.695613E+00 +6.294508E+00 +8.036467E+00 +5.316642E+00 +5.711778E+00 +6.414998E+00 +8.319784E+00 diff --git a/tests/regression_tests/delta_tracking_distribrho/__init__.py b/tests/regression_tests/delta_tracking_distribrho/__init__.py new file mode 100644 index 00000000000..e69de29bb2d diff --git a/tests/regression_tests/delta_tracking_distribrho/test.py b/tests/regression_tests/delta_tracking_distribrho/test.py new file mode 100644 index 00000000000..f609a12f5e2 --- /dev/null +++ b/tests/regression_tests/delta_tracking_distribrho/test.py @@ -0,0 +1,77 @@ +import openmc +import pytest +from openmc.utility_funcs import change_directory + +from tests.testing_harness import PyAPITestHarness + + +@pytest.fixture +def model(): + openmc.reset_auto_ids() + model = openmc.Model() + + uo2 = openmc.Material(name='UO2') + uo2.set_density('g/cm3', 10.0) + uo2.add_nuclide('U235', 1.0) + uo2.add_nuclide('O16', 2.0) + water = openmc.Material(name='light water') + water.add_nuclide('H1', 2.0) + water.add_nuclide('O16', 1.0) + water.set_density('g/cm3', 1.0) + water.add_s_alpha_beta('c_H_in_H2O') + model.materials.extend([uo2, water]) + + cyl = openmc.ZCylinder(r=0.4) + pin = openmc.model.pin([cyl], [uo2, water]) + d = 1.0 + + lattice = openmc.RectLattice() + lattice.lower_left = (-d, -d) + lattice.pitch = (d, d) + lattice.universes = [[pin, pin], + [pin, pin]] + box = openmc.model.RectangularPrism( + 2.0 * d, 2.0 * d, + origin=(0.0, 0.0), + boundary_type='reflective' + ) + + pin.cells[1].density = [10.0, 20.0, 10.0, 20.0] + + model.geometry = openmc.Geometry([openmc.Cell(fill=lattice, region=-box)]) + model.geometry.merge_surfaces = True + + msh = openmc.RegularMesh(mesh_id=0) + msh.lower_left = (-d, -d) + msh.upper_right = (d, d) + msh.dimension = (2, 2) + t = openmc.Tally() + t.filters = [openmc.ParticleFilter(bins='neutron'), openmc.MeshFilter(mesh=msh)] + t.scores = ['total'] + t.estimator = 'collision' + model.tallies.append(t) + + model.settings.batches = 10 + model.settings.inactive = 5 + model.settings.particles = 1000 + model.settings.delta_tracking = True + + return model + + +@pytest.mark.parametrize("photon", [False, True]) +def test_lattice_checkerboard(model, photon): + with change_directory(str(photon)): + model.settings.photon_transport = photon + if photon: + msh = openmc.RegularMesh(mesh_id=1) + msh.lower_left = (-1.0, -1.0) + msh.upper_right = (1.0, 1.0) + msh.dimension = (2, 2) + t = openmc.Tally() + t.filters = [openmc.ParticleFilter(bins='photon'), openmc.MeshFilter(mesh=msh)] + t.scores = ['total'] + t.estimator = 'collision' + model.tallies.append(t) + harness = PyAPITestHarness('statepoint.10.h5', model) + harness.main() From dd05863eb648710720103920a1f18bb35340d172 Mon Sep 17 00:00:00 2001 From: nuclearkevin <66632997+nuclearkevin@users.noreply.github.com> Date: Thu, 11 Jun 2026 15:46:01 -0500 Subject: [PATCH 54/73] Add history-based and event-based tests with/without photons. --- .../False/inputs_true.dat | 61 +++++++++++++++ .../False/results_true.dat | 11 +++ .../delta_tracking_event/True/inputs_true.dat | 77 +++++++++++++++++++ .../True/results_true.dat | 20 +++++ .../delta_tracking_event/__init__.py | 0 .../delta_tracking_event/test.py | 76 ++++++++++++++++++ .../False/inputs_true.dat | 60 +++++++++++++++ .../False/results_true.dat | 11 +++ .../True/inputs_true.dat | 76 ++++++++++++++++++ .../True/results_true.dat | 20 +++++ .../delta_tracking_history/__init__.py | 0 .../delta_tracking_history/test.py | 75 ++++++++++++++++++ 12 files changed, 487 insertions(+) create mode 100644 tests/regression_tests/delta_tracking_event/False/inputs_true.dat create mode 100644 tests/regression_tests/delta_tracking_event/False/results_true.dat create mode 100644 tests/regression_tests/delta_tracking_event/True/inputs_true.dat create mode 100644 tests/regression_tests/delta_tracking_event/True/results_true.dat create mode 100644 tests/regression_tests/delta_tracking_event/__init__.py create mode 100644 tests/regression_tests/delta_tracking_event/test.py create mode 100644 tests/regression_tests/delta_tracking_history/False/inputs_true.dat create mode 100644 tests/regression_tests/delta_tracking_history/False/results_true.dat create mode 100644 tests/regression_tests/delta_tracking_history/True/inputs_true.dat create mode 100644 tests/regression_tests/delta_tracking_history/True/results_true.dat create mode 100644 tests/regression_tests/delta_tracking_history/__init__.py create mode 100644 tests/regression_tests/delta_tracking_history/test.py diff --git a/tests/regression_tests/delta_tracking_event/False/inputs_true.dat b/tests/regression_tests/delta_tracking_event/False/inputs_true.dat new file mode 100644 index 00000000000..e302eef7bdf --- /dev/null +++ b/tests/regression_tests/delta_tracking_event/False/inputs_true.dat @@ -0,0 +1,61 @@ + + + + + + + + + + + + + + + + + + + + + 1.0 1.0 + 2 2 + -1.0 -1.0 + +1 1 +1 1 + + + + + + + + + eigenvalue + 1000 + 10 + 5 + false + true + true + + + + 2 2 + -1.0 -1.0 + 1.0 1.0 + + + neutron + + + 0 + + + 1 2 + total + collision + + + diff --git a/tests/regression_tests/delta_tracking_event/False/results_true.dat b/tests/regression_tests/delta_tracking_event/False/results_true.dat new file mode 100644 index 00000000000..59df0c152d0 --- /dev/null +++ b/tests/regression_tests/delta_tracking_event/False/results_true.dat @@ -0,0 +1,11 @@ +k-combined: +6.550198E-02 7.372609E-04 +tally 1: +2.169960E+02 +9.430305E+03 +2.203220E+02 +9.713679E+03 +2.205600E+02 +9.733107E+03 +2.242590E+02 +1.006066E+04 diff --git a/tests/regression_tests/delta_tracking_event/True/inputs_true.dat b/tests/regression_tests/delta_tracking_event/True/inputs_true.dat new file mode 100644 index 00000000000..9c8a39f3cfe --- /dev/null +++ b/tests/regression_tests/delta_tracking_event/True/inputs_true.dat @@ -0,0 +1,77 @@ + + + + + + + + + + + + + + + + + + + + + 1.0 1.0 + 2 2 + -1.0 -1.0 + +1 1 +1 1 + + + + + + + + + eigenvalue + 1000 + 10 + 5 + true + true + true + + + + 2 2 + -1.0 -1.0 + 1.0 1.0 + + + 2 2 + -1.0 -1.0 + 1.0 1.0 + + + neutron + + + 0 + + + photon + + + 1 + + + 1 2 + total + collision + + + 3 4 + total + collision + + + diff --git a/tests/regression_tests/delta_tracking_event/True/results_true.dat b/tests/regression_tests/delta_tracking_event/True/results_true.dat new file mode 100644 index 00000000000..3e2c4e19b1c --- /dev/null +++ b/tests/regression_tests/delta_tracking_event/True/results_true.dat @@ -0,0 +1,20 @@ +k-combined: +6.389784E-02 7.457490E-04 +tally 1: +2.255080E+02 +1.018236E+04 +2.238190E+02 +1.003496E+04 +2.257940E+02 +1.020553E+04 +2.233470E+02 +9.988555E+03 +tally 2: +3.386434E+00 +2.303892E+00 +3.208627E+00 +2.068319E+00 +3.404065E+00 +2.318857E+00 +3.266091E+00 +2.137819E+00 diff --git a/tests/regression_tests/delta_tracking_event/__init__.py b/tests/regression_tests/delta_tracking_event/__init__.py new file mode 100644 index 00000000000..e69de29bb2d diff --git a/tests/regression_tests/delta_tracking_event/test.py b/tests/regression_tests/delta_tracking_event/test.py new file mode 100644 index 00000000000..a6e9be89090 --- /dev/null +++ b/tests/regression_tests/delta_tracking_event/test.py @@ -0,0 +1,76 @@ +import openmc +import pytest +from openmc.utility_funcs import change_directory + +from tests.testing_harness import PyAPITestHarness + + +@pytest.fixture +def model(): + openmc.reset_auto_ids() + model = openmc.Model() + + uo2 = openmc.Material(name='UO2') + uo2.set_density('g/cm3', 10.0) + uo2.add_nuclide('U235', 1.0) + uo2.add_nuclide('O16', 2.0) + water = openmc.Material(name='light water') + water.add_nuclide('H1', 2.0) + water.add_nuclide('O16', 1.0) + water.set_density('g/cm3', 1.0) + water.add_s_alpha_beta('c_H_in_H2O') + model.materials.extend([uo2, water]) + + cyl = openmc.ZCylinder(r=0.4) + pin = openmc.model.pin([cyl], [uo2, water]) + d = 1.0 + + lattice = openmc.RectLattice() + lattice.lower_left = (-d, -d) + lattice.pitch = (d, d) + lattice.universes = [[pin, pin], + [pin, pin]] + box = openmc.model.RectangularPrism( + 2.0 * d, 2.0 * d, + origin=(0.0, 0.0), + boundary_type='reflective' + ) + + model.geometry = openmc.Geometry([openmc.Cell(fill=lattice, region=-box)]) + model.geometry.merge_surfaces = True + + msh = openmc.RegularMesh(mesh_id=0) + msh.lower_left = (-d, -d) + msh.upper_right = (d, d) + msh.dimension = (2, 2) + t = openmc.Tally() + t.filters = [openmc.ParticleFilter(bins='neutron'), openmc.MeshFilter(mesh=msh)] + t.scores = ['total'] + t.estimator = 'collision' + model.tallies.append(t) + + model.settings.batches = 10 + model.settings.inactive = 5 + model.settings.particles = 1000 + model.settings.delta_tracking = True + model.settings.event_based = True + + return model + + +@pytest.mark.parametrize("photon", [False, True]) +def test_lattice(model, photon): + with change_directory(str(photon)): + model.settings.photon_transport = photon + if photon: + msh = openmc.RegularMesh(mesh_id=1) + msh.lower_left = (-1.0, -1.0) + msh.upper_right = (1.0, 1.0) + msh.dimension = (2, 2) + t = openmc.Tally() + t.filters = [openmc.ParticleFilter(bins='photon'), openmc.MeshFilter(mesh=msh)] + t.scores = ['total'] + t.estimator = 'collision' + model.tallies.append(t) + harness = PyAPITestHarness('statepoint.10.h5', model) + harness.main() diff --git a/tests/regression_tests/delta_tracking_history/False/inputs_true.dat b/tests/regression_tests/delta_tracking_history/False/inputs_true.dat new file mode 100644 index 00000000000..2177b2a8d5e --- /dev/null +++ b/tests/regression_tests/delta_tracking_history/False/inputs_true.dat @@ -0,0 +1,60 @@ + + + + + + + + + + + + + + + + + + + + + 1.0 1.0 + 2 2 + -1.0 -1.0 + +1 1 +1 1 + + + + + + + + + eigenvalue + 1000 + 10 + 5 + false + true + + + + 2 2 + -1.0 -1.0 + 1.0 1.0 + + + neutron + + + 0 + + + 1 2 + total + collision + + + diff --git a/tests/regression_tests/delta_tracking_history/False/results_true.dat b/tests/regression_tests/delta_tracking_history/False/results_true.dat new file mode 100644 index 00000000000..59df0c152d0 --- /dev/null +++ b/tests/regression_tests/delta_tracking_history/False/results_true.dat @@ -0,0 +1,11 @@ +k-combined: +6.550198E-02 7.372609E-04 +tally 1: +2.169960E+02 +9.430305E+03 +2.203220E+02 +9.713679E+03 +2.205600E+02 +9.733107E+03 +2.242590E+02 +1.006066E+04 diff --git a/tests/regression_tests/delta_tracking_history/True/inputs_true.dat b/tests/regression_tests/delta_tracking_history/True/inputs_true.dat new file mode 100644 index 00000000000..7e2b5896073 --- /dev/null +++ b/tests/regression_tests/delta_tracking_history/True/inputs_true.dat @@ -0,0 +1,76 @@ + + + + + + + + + + + + + + + + + + + + + 1.0 1.0 + 2 2 + -1.0 -1.0 + +1 1 +1 1 + + + + + + + + + eigenvalue + 1000 + 10 + 5 + true + true + + + + 2 2 + -1.0 -1.0 + 1.0 1.0 + + + 2 2 + -1.0 -1.0 + 1.0 1.0 + + + neutron + + + 0 + + + photon + + + 1 + + + 1 2 + total + collision + + + 3 4 + total + collision + + + diff --git a/tests/regression_tests/delta_tracking_history/True/results_true.dat b/tests/regression_tests/delta_tracking_history/True/results_true.dat new file mode 100644 index 00000000000..3e2c4e19b1c --- /dev/null +++ b/tests/regression_tests/delta_tracking_history/True/results_true.dat @@ -0,0 +1,20 @@ +k-combined: +6.389784E-02 7.457490E-04 +tally 1: +2.255080E+02 +1.018236E+04 +2.238190E+02 +1.003496E+04 +2.257940E+02 +1.020553E+04 +2.233470E+02 +9.988555E+03 +tally 2: +3.386434E+00 +2.303892E+00 +3.208627E+00 +2.068319E+00 +3.404065E+00 +2.318857E+00 +3.266091E+00 +2.137819E+00 diff --git a/tests/regression_tests/delta_tracking_history/__init__.py b/tests/regression_tests/delta_tracking_history/__init__.py new file mode 100644 index 00000000000..e69de29bb2d diff --git a/tests/regression_tests/delta_tracking_history/test.py b/tests/regression_tests/delta_tracking_history/test.py new file mode 100644 index 00000000000..f74c718f2c4 --- /dev/null +++ b/tests/regression_tests/delta_tracking_history/test.py @@ -0,0 +1,75 @@ +import openmc +import pytest +from openmc.utility_funcs import change_directory + +from tests.testing_harness import PyAPITestHarness + + +@pytest.fixture +def model(): + openmc.reset_auto_ids() + model = openmc.Model() + + uo2 = openmc.Material(name='UO2') + uo2.set_density('g/cm3', 10.0) + uo2.add_nuclide('U235', 1.0) + uo2.add_nuclide('O16', 2.0) + water = openmc.Material(name='light water') + water.add_nuclide('H1', 2.0) + water.add_nuclide('O16', 1.0) + water.set_density('g/cm3', 1.0) + water.add_s_alpha_beta('c_H_in_H2O') + model.materials.extend([uo2, water]) + + cyl = openmc.ZCylinder(r=0.4) + pin = openmc.model.pin([cyl], [uo2, water]) + d = 1.0 + + lattice = openmc.RectLattice() + lattice.lower_left = (-d, -d) + lattice.pitch = (d, d) + lattice.universes = [[pin, pin], + [pin, pin]] + box = openmc.model.RectangularPrism( + 2.0 * d, 2.0 * d, + origin=(0.0, 0.0), + boundary_type='reflective' + ) + + model.geometry = openmc.Geometry([openmc.Cell(fill=lattice, region=-box)]) + model.geometry.merge_surfaces = True + + msh = openmc.RegularMesh(mesh_id=0) + msh.lower_left = (-d, -d) + msh.upper_right = (d, d) + msh.dimension = (2, 2) + t = openmc.Tally() + t.filters = [openmc.ParticleFilter(bins='neutron'), openmc.MeshFilter(mesh=msh)] + t.scores = ['total'] + t.estimator = 'collision' + model.tallies.append(t) + + model.settings.batches = 10 + model.settings.inactive = 5 + model.settings.particles = 1000 + model.settings.delta_tracking = True + + return model + + +@pytest.mark.parametrize("photon", [False, True]) +def test_lattice_checkerboard(model, photon): + with change_directory(str(photon)): + model.settings.photon_transport = photon + if photon: + msh = openmc.RegularMesh(mesh_id=1) + msh.lower_left = (-1.0, -1.0) + msh.upper_right = (1.0, 1.0) + msh.dimension = (2, 2) + t = openmc.Tally() + t.filters = [openmc.ParticleFilter(bins='photon'), openmc.MeshFilter(mesh=msh)] + t.scores = ['total'] + t.estimator = 'collision' + model.tallies.append(t) + harness = PyAPITestHarness('statepoint.10.h5', model) + harness.main() From aefcb3d01cd86f7b8885cc5d9b31a1aef6cc6e6a Mon Sep 17 00:00:00 2001 From: nuclearkevin <66632997+nuclearkevin@users.noreply.github.com> Date: Thu, 11 Jun 2026 15:59:51 -0500 Subject: [PATCH 55/73] Revert "Remove unnecessary coord reset." This reverts commit ab346b268b72eb3f5c6fa303911f5c80a103dfb6. --- src/particle.cpp | 3 +++ 1 file changed, 3 insertions(+) diff --git a/src/particle.cpp b/src/particle.cpp index 3fe2bc49e1d..5a29613ef48 100644 --- a/src/particle.cpp +++ b/src/particle.cpp @@ -350,6 +350,9 @@ void Particle::event_delta_advance() r() += distance * u(); // Need to locate the particle at the collision site or boundary. + for (int j = 0; j < n_coord(); ++j) { + coord(j).reset(); + } if (!exhaustive_find_cell(*this)) { // We've lost this particle. mark_as_lost(fmt::format("Particle {} could not be located while running delta tracking!", id())); From 0d56c4155487cc76f824444ae8f9269d78c6d186 Mon Sep 17 00:00:00 2001 From: nuclearkevin <66632997+nuclearkevin@users.noreply.github.com> Date: Thu, 11 Jun 2026 16:04:58 -0500 Subject: [PATCH 56/73] Regold to account for coord fix. --- .../False/results_true.dat | 18 +++++----- .../True/results_true.dat | 34 +++++++++---------- .../False/results_true.dat | 18 +++++----- .../True/results_true.dat | 34 +++++++++---------- .../False/results_true.dat | 18 +++++----- .../True/results_true.dat | 34 +++++++++---------- 6 files changed, 78 insertions(+), 78 deletions(-) diff --git a/tests/regression_tests/delta_tracking_distribrho/False/results_true.dat b/tests/regression_tests/delta_tracking_distribrho/False/results_true.dat index 3bd172ea252..014825376b2 100644 --- a/tests/regression_tests/delta_tracking_distribrho/False/results_true.dat +++ b/tests/regression_tests/delta_tracking_distribrho/False/results_true.dat @@ -1,11 +1,11 @@ k-combined: -1.108089E-01 1.861559E-03 +1.902488E+00 3.242884E-03 tally 1: -2.237440E+02 -1.002088E+04 -2.261060E+02 -1.023153E+04 -2.226120E+02 -9.912659E+03 -2.252070E+02 -1.014833E+04 +1.246500E+01 +3.108325E+01 +1.684800E+01 +5.679135E+01 +1.239600E+01 +3.082090E+01 +1.640800E+01 +5.386842E+01 diff --git a/tests/regression_tests/delta_tracking_distribrho/True/results_true.dat b/tests/regression_tests/delta_tracking_distribrho/True/results_true.dat index fb37f56563c..502f7b7c1fc 100644 --- a/tests/regression_tests/delta_tracking_distribrho/True/results_true.dat +++ b/tests/regression_tests/delta_tracking_distribrho/True/results_true.dat @@ -1,20 +1,20 @@ k-combined: -1.098307E-01 2.729028E-04 +1.905169E+00 2.601649E-03 tally 1: -2.217840E+02 -9.850848E+03 -2.214880E+02 -9.834344E+03 -2.221800E+02 -9.878536E+03 -2.211110E+02 -9.785553E+03 +1.280100E+01 +3.280666E+01 +1.668400E+01 +5.572759E+01 +1.270200E+01 +3.227630E+01 +1.645400E+01 +5.417179E+01 tally 2: -5.309872E+00 -5.695613E+00 -6.294508E+00 -8.036467E+00 -5.316642E+00 -5.711778E+00 -6.414998E+00 -8.319784E+00 +6.522825E+01 +8.512465E+02 +1.239644E+02 +3.074655E+03 +6.644431E+01 +8.835009E+02 +1.228901E+02 +3.022436E+03 diff --git a/tests/regression_tests/delta_tracking_event/False/results_true.dat b/tests/regression_tests/delta_tracking_event/False/results_true.dat index 59df0c152d0..a388688876f 100644 --- a/tests/regression_tests/delta_tracking_event/False/results_true.dat +++ b/tests/regression_tests/delta_tracking_event/False/results_true.dat @@ -1,11 +1,11 @@ k-combined: -6.550198E-02 7.372609E-04 +1.869636E+00 4.475641E-03 tally 1: -2.169960E+02 -9.430305E+03 -2.203220E+02 -9.713679E+03 -2.205600E+02 -9.733107E+03 -2.242590E+02 -1.006066E+04 +1.558300E+01 +4.857891E+01 +1.578200E+01 +4.982111E+01 +1.489300E+01 +4.438229E+01 +1.544300E+01 +4.771581E+01 diff --git a/tests/regression_tests/delta_tracking_event/True/results_true.dat b/tests/regression_tests/delta_tracking_event/True/results_true.dat index 3e2c4e19b1c..5d33cb06d5d 100644 --- a/tests/regression_tests/delta_tracking_event/True/results_true.dat +++ b/tests/regression_tests/delta_tracking_event/True/results_true.dat @@ -1,20 +1,20 @@ k-combined: -6.389784E-02 7.457490E-04 +1.863579E+00 4.987651E-03 tally 1: -2.255080E+02 -1.018236E+04 -2.238190E+02 -1.003496E+04 -2.257940E+02 -1.020553E+04 -2.233470E+02 -9.988555E+03 +1.532500E+01 +4.698978E+01 +1.517100E+01 +4.606297E+01 +1.531800E+01 +4.694604E+01 +1.547900E+01 +4.801024E+01 tally 2: -3.386434E+00 -2.303892E+00 -3.208627E+00 -2.068319E+00 -3.404065E+00 -2.318857E+00 -3.266091E+00 -2.137819E+00 +9.001423E+01 +1.621357E+03 +9.138405E+01 +1.671319E+03 +9.036599E+01 +1.634452E+03 +9.160015E+01 +1.681369E+03 diff --git a/tests/regression_tests/delta_tracking_history/False/results_true.dat b/tests/regression_tests/delta_tracking_history/False/results_true.dat index 59df0c152d0..a388688876f 100644 --- a/tests/regression_tests/delta_tracking_history/False/results_true.dat +++ b/tests/regression_tests/delta_tracking_history/False/results_true.dat @@ -1,11 +1,11 @@ k-combined: -6.550198E-02 7.372609E-04 +1.869636E+00 4.475641E-03 tally 1: -2.169960E+02 -9.430305E+03 -2.203220E+02 -9.713679E+03 -2.205600E+02 -9.733107E+03 -2.242590E+02 -1.006066E+04 +1.558300E+01 +4.857891E+01 +1.578200E+01 +4.982111E+01 +1.489300E+01 +4.438229E+01 +1.544300E+01 +4.771581E+01 diff --git a/tests/regression_tests/delta_tracking_history/True/results_true.dat b/tests/regression_tests/delta_tracking_history/True/results_true.dat index 3e2c4e19b1c..5d33cb06d5d 100644 --- a/tests/regression_tests/delta_tracking_history/True/results_true.dat +++ b/tests/regression_tests/delta_tracking_history/True/results_true.dat @@ -1,20 +1,20 @@ k-combined: -6.389784E-02 7.457490E-04 +1.863579E+00 4.987651E-03 tally 1: -2.255080E+02 -1.018236E+04 -2.238190E+02 -1.003496E+04 -2.257940E+02 -1.020553E+04 -2.233470E+02 -9.988555E+03 +1.532500E+01 +4.698978E+01 +1.517100E+01 +4.606297E+01 +1.531800E+01 +4.694604E+01 +1.547900E+01 +4.801024E+01 tally 2: -3.386434E+00 -2.303892E+00 -3.208627E+00 -2.068319E+00 -3.404065E+00 -2.318857E+00 -3.266091E+00 -2.137819E+00 +9.001423E+01 +1.621357E+03 +9.138405E+01 +1.671319E+03 +9.036599E+01 +1.634452E+03 +9.160015E+01 +1.681369E+03 From 2e1796ac8ded4b68a855907d5ef5c1b9229dc58f Mon Sep 17 00:00:00 2001 From: nuclearkevin <66632997+nuclearkevin@users.noreply.github.com> Date: Thu, 11 Jun 2026 17:20:12 -0500 Subject: [PATCH 57/73] Fix periodic boundary conditions. --- src/particle.cpp | 13 ++++++++++--- 1 file changed, 10 insertions(+), 3 deletions(-) diff --git a/src/particle.cpp b/src/particle.cpp index 5a29613ef48..26b393aed76 100644 --- a/src/particle.cpp +++ b/src/particle.cpp @@ -345,7 +345,9 @@ void Particle::event_delta_advance() // Update distance to problem boundary boundary() = distance_to_external_boundary(*this); - // Move to the external boundary or delta tracking collision site. + // Move to the external boundary or delta tracking collision site. Particles with + // large majorant cross sections will tunnel out of the domain if a floating point + // tolerance is not specified on the boundary distance calculation. double distance = std::min(collision_distance(), boundary().distance() - FP_REL_PRECISION); r() += distance * u(); @@ -862,10 +864,15 @@ void Particle::cross_periodic_bc( // Figure out what cell particle is in now n_coord() = 1; + // Need to nudge the particle back into the domain as event_delta_advance() + // does not go right up to the surface to fix tunneling. + if (delta_tracking()) { + r() += FP_REL_PRECISION * u(); + } if (!neighbor_list_find_cell(*this)) { mark_as_lost("Couldn't find particle after hitting periodic " - "boundary on surface " + - std::to_string(surf.id_) + "."); + "boundary on surface " + + std::to_string(surf.id_) + "."); return; } From bbee3ee91132480d21a333f29d9ae70df50a3a8b Mon Sep 17 00:00:00 2001 From: nuclearkevin <66632997+nuclearkevin@users.noreply.github.com> Date: Thu, 11 Jun 2026 17:22:17 -0500 Subject: [PATCH 58/73] Add tests for delta tracking boundary conditions. --- .../delta_tracking_bcs/__init__.py | 0 .../periodicFalse/inputs_true.dat | 60 +++++++++++++++ .../periodicFalse/results_true.dat | 11 +++ .../periodicTrue/inputs_true.dat | 76 +++++++++++++++++++ .../periodicTrue/results_true.dat | 20 +++++ .../reflectiveFalse/inputs_true.dat | 60 +++++++++++++++ .../reflectiveFalse/results_true.dat | 11 +++ .../reflectiveTrue/inputs_true.dat | 76 +++++++++++++++++++ .../reflectiveTrue/results_true.dat | 20 +++++ .../delta_tracking_bcs/test.py | 76 +++++++++++++++++++ .../vacuumFalse/inputs_true.dat | 60 +++++++++++++++ .../vacuumFalse/results_true.dat | 11 +++ .../vacuumTrue/inputs_true.dat | 76 +++++++++++++++++++ .../vacuumTrue/results_true.dat | 20 +++++ .../whiteFalse/inputs_true.dat | 60 +++++++++++++++ .../whiteFalse/results_true.dat | 11 +++ .../whiteTrue/inputs_true.dat | 76 +++++++++++++++++++ .../whiteTrue/results_true.dat | 20 +++++ .../delta_tracking_history/test.py | 2 +- 19 files changed, 745 insertions(+), 1 deletion(-) create mode 100644 tests/regression_tests/delta_tracking_bcs/__init__.py create mode 100644 tests/regression_tests/delta_tracking_bcs/periodicFalse/inputs_true.dat create mode 100644 tests/regression_tests/delta_tracking_bcs/periodicFalse/results_true.dat create mode 100644 tests/regression_tests/delta_tracking_bcs/periodicTrue/inputs_true.dat create mode 100644 tests/regression_tests/delta_tracking_bcs/periodicTrue/results_true.dat create mode 100644 tests/regression_tests/delta_tracking_bcs/reflectiveFalse/inputs_true.dat create mode 100644 tests/regression_tests/delta_tracking_bcs/reflectiveFalse/results_true.dat create mode 100644 tests/regression_tests/delta_tracking_bcs/reflectiveTrue/inputs_true.dat create mode 100644 tests/regression_tests/delta_tracking_bcs/reflectiveTrue/results_true.dat create mode 100644 tests/regression_tests/delta_tracking_bcs/test.py create mode 100644 tests/regression_tests/delta_tracking_bcs/vacuumFalse/inputs_true.dat create mode 100644 tests/regression_tests/delta_tracking_bcs/vacuumFalse/results_true.dat create mode 100644 tests/regression_tests/delta_tracking_bcs/vacuumTrue/inputs_true.dat create mode 100644 tests/regression_tests/delta_tracking_bcs/vacuumTrue/results_true.dat create mode 100644 tests/regression_tests/delta_tracking_bcs/whiteFalse/inputs_true.dat create mode 100644 tests/regression_tests/delta_tracking_bcs/whiteFalse/results_true.dat create mode 100644 tests/regression_tests/delta_tracking_bcs/whiteTrue/inputs_true.dat create mode 100644 tests/regression_tests/delta_tracking_bcs/whiteTrue/results_true.dat diff --git a/tests/regression_tests/delta_tracking_bcs/__init__.py b/tests/regression_tests/delta_tracking_bcs/__init__.py new file mode 100644 index 00000000000..e69de29bb2d diff --git a/tests/regression_tests/delta_tracking_bcs/periodicFalse/inputs_true.dat b/tests/regression_tests/delta_tracking_bcs/periodicFalse/inputs_true.dat new file mode 100644 index 00000000000..e01569b98a4 --- /dev/null +++ b/tests/regression_tests/delta_tracking_bcs/periodicFalse/inputs_true.dat @@ -0,0 +1,60 @@ + + + + + + + + + + + + + + + + + + + + + 1.0 1.0 + 2 2 + -1.0 -1.0 + +1 1 +1 1 + + + + + + + + + eigenvalue + 1000 + 10 + 5 + false + true + + + + 2 2 + -1.0 -1.0 + 1.0 1.0 + + + neutron + + + 0 + + + 1 2 + total + collision + + + diff --git a/tests/regression_tests/delta_tracking_bcs/periodicFalse/results_true.dat b/tests/regression_tests/delta_tracking_bcs/periodicFalse/results_true.dat new file mode 100644 index 00000000000..aaddfe9b0fd --- /dev/null +++ b/tests/regression_tests/delta_tracking_bcs/periodicFalse/results_true.dat @@ -0,0 +1,11 @@ +k-combined: +1.862405E+00 3.970186E-03 +tally 1: +1.590300E+01 +5.059012E+01 +1.556600E+01 +4.848489E+01 +1.592000E+01 +5.071728E+01 +1.562800E+01 +4.886343E+01 diff --git a/tests/regression_tests/delta_tracking_bcs/periodicTrue/inputs_true.dat b/tests/regression_tests/delta_tracking_bcs/periodicTrue/inputs_true.dat new file mode 100644 index 00000000000..c300a297da8 --- /dev/null +++ b/tests/regression_tests/delta_tracking_bcs/periodicTrue/inputs_true.dat @@ -0,0 +1,76 @@ + + + + + + + + + + + + + + + + + + + + + 1.0 1.0 + 2 2 + -1.0 -1.0 + +1 1 +1 1 + + + + + + + + + eigenvalue + 1000 + 10 + 5 + true + true + + + + 2 2 + -1.0 -1.0 + 1.0 1.0 + + + 2 2 + -1.0 -1.0 + 1.0 1.0 + + + neutron + + + 0 + + + photon + + + 1 + + + 1 2 + total + collision + + + 3 4 + total + collision + + + diff --git a/tests/regression_tests/delta_tracking_bcs/periodicTrue/results_true.dat b/tests/regression_tests/delta_tracking_bcs/periodicTrue/results_true.dat new file mode 100644 index 00000000000..089934cc199 --- /dev/null +++ b/tests/regression_tests/delta_tracking_bcs/periodicTrue/results_true.dat @@ -0,0 +1,20 @@ +k-combined: +1.865102E+00 1.598906E-03 +tally 1: +1.547100E+01 +4.788337E+01 +1.540300E+01 +4.746582E+01 +1.532000E+01 +4.695871E+01 +1.546300E+01 +4.786204E+01 +tally 2: +9.316751E+01 +1.737859E+03 +9.255290E+01 +1.713493E+03 +9.233657E+01 +1.705814E+03 +9.158787E+01 +1.678355E+03 diff --git a/tests/regression_tests/delta_tracking_bcs/reflectiveFalse/inputs_true.dat b/tests/regression_tests/delta_tracking_bcs/reflectiveFalse/inputs_true.dat new file mode 100644 index 00000000000..2177b2a8d5e --- /dev/null +++ b/tests/regression_tests/delta_tracking_bcs/reflectiveFalse/inputs_true.dat @@ -0,0 +1,60 @@ + + + + + + + + + + + + + + + + + + + + + 1.0 1.0 + 2 2 + -1.0 -1.0 + +1 1 +1 1 + + + + + + + + + eigenvalue + 1000 + 10 + 5 + false + true + + + + 2 2 + -1.0 -1.0 + 1.0 1.0 + + + neutron + + + 0 + + + 1 2 + total + collision + + + diff --git a/tests/regression_tests/delta_tracking_bcs/reflectiveFalse/results_true.dat b/tests/regression_tests/delta_tracking_bcs/reflectiveFalse/results_true.dat new file mode 100644 index 00000000000..a388688876f --- /dev/null +++ b/tests/regression_tests/delta_tracking_bcs/reflectiveFalse/results_true.dat @@ -0,0 +1,11 @@ +k-combined: +1.869636E+00 4.475641E-03 +tally 1: +1.558300E+01 +4.857891E+01 +1.578200E+01 +4.982111E+01 +1.489300E+01 +4.438229E+01 +1.544300E+01 +4.771581E+01 diff --git a/tests/regression_tests/delta_tracking_bcs/reflectiveTrue/inputs_true.dat b/tests/regression_tests/delta_tracking_bcs/reflectiveTrue/inputs_true.dat new file mode 100644 index 00000000000..7e2b5896073 --- /dev/null +++ b/tests/regression_tests/delta_tracking_bcs/reflectiveTrue/inputs_true.dat @@ -0,0 +1,76 @@ + + + + + + + + + + + + + + + + + + + + + 1.0 1.0 + 2 2 + -1.0 -1.0 + +1 1 +1 1 + + + + + + + + + eigenvalue + 1000 + 10 + 5 + true + true + + + + 2 2 + -1.0 -1.0 + 1.0 1.0 + + + 2 2 + -1.0 -1.0 + 1.0 1.0 + + + neutron + + + 0 + + + photon + + + 1 + + + 1 2 + total + collision + + + 3 4 + total + collision + + + diff --git a/tests/regression_tests/delta_tracking_bcs/reflectiveTrue/results_true.dat b/tests/regression_tests/delta_tracking_bcs/reflectiveTrue/results_true.dat new file mode 100644 index 00000000000..5d33cb06d5d --- /dev/null +++ b/tests/regression_tests/delta_tracking_bcs/reflectiveTrue/results_true.dat @@ -0,0 +1,20 @@ +k-combined: +1.863579E+00 4.987651E-03 +tally 1: +1.532500E+01 +4.698978E+01 +1.517100E+01 +4.606297E+01 +1.531800E+01 +4.694604E+01 +1.547900E+01 +4.801024E+01 +tally 2: +9.001423E+01 +1.621357E+03 +9.138405E+01 +1.671319E+03 +9.036599E+01 +1.634452E+03 +9.160015E+01 +1.681369E+03 diff --git a/tests/regression_tests/delta_tracking_bcs/test.py b/tests/regression_tests/delta_tracking_bcs/test.py new file mode 100644 index 00000000000..cf3205cfa43 --- /dev/null +++ b/tests/regression_tests/delta_tracking_bcs/test.py @@ -0,0 +1,76 @@ +import openmc +import pytest +from openmc.utility_funcs import change_directory + +from tests.testing_harness import PyAPITestHarness + + +def model(boundary_type): + openmc.reset_auto_ids() + model = openmc.Model() + + uo2 = openmc.Material(name='UO2') + uo2.set_density('g/cm3', 10.0) + uo2.add_nuclide('U235', 1.0) + uo2.add_nuclide('O16', 2.0) + water = openmc.Material(name='light water') + water.add_nuclide('H1', 2.0) + water.add_nuclide('O16', 1.0) + water.set_density('g/cm3', 1.0) + water.add_s_alpha_beta('c_H_in_H2O') + model.materials.extend([uo2, water]) + + cyl = openmc.ZCylinder(r=0.4) + pin = openmc.model.pin([cyl], [uo2, water]) + d = 1.0 + + lattice = openmc.RectLattice() + lattice.lower_left = (-d, -d) + lattice.pitch = (d, d) + lattice.universes = [[pin, pin], + [pin, pin]] + box = openmc.model.RectangularPrism( + 2.0 * d, 2.0 * d, + origin=(0.0, 0.0), + boundary_type=boundary_type + ) + + model.geometry = openmc.Geometry([openmc.Cell(fill=lattice, region=-box)]) + model.geometry.merge_surfaces = True + + msh = openmc.RegularMesh(mesh_id=0) + msh.lower_left = (-d, -d) + msh.upper_right = (d, d) + msh.dimension = (2, 2) + t = openmc.Tally() + t.filters = [openmc.ParticleFilter(bins='neutron'), openmc.MeshFilter(mesh=msh)] + t.scores = ['total'] + t.estimator = 'collision' + model.tallies.append(t) + + model.settings.batches = 10 + model.settings.inactive = 5 + model.settings.particles = 1000 + model.settings.delta_tracking = True + + return model + + +@pytest.mark.parametrize("photon", [False, True]) +@pytest.mark.parametrize("boundary", ['vacuum', 'reflective', 'periodic', 'white']) +def test_lattice(photon, boundary): + m = model(boundary) + with change_directory(boundary+str(photon)): + m.settings.photon_transport = photon + if photon: + msh = openmc.RegularMesh(mesh_id=1) + msh.lower_left = (-1.0, -1.0) + msh.upper_right = (1.0, 1.0) + msh.dimension = (2, 2) + t = openmc.Tally() + t.filters = [openmc.ParticleFilter(bins='photon'), openmc.MeshFilter(mesh=msh)] + t.scores = ['total'] + t.estimator = 'collision' + m.tallies.append(t) + harness = PyAPITestHarness('statepoint.10.h5', m) + harness.main() diff --git a/tests/regression_tests/delta_tracking_bcs/vacuumFalse/inputs_true.dat b/tests/regression_tests/delta_tracking_bcs/vacuumFalse/inputs_true.dat new file mode 100644 index 00000000000..adc875d6aca --- /dev/null +++ b/tests/regression_tests/delta_tracking_bcs/vacuumFalse/inputs_true.dat @@ -0,0 +1,60 @@ + + + + + + + + + + + + + + + + + + + + + 1.0 1.0 + 2 2 + -1.0 -1.0 + +1 1 +1 1 + + + + + + + + + eigenvalue + 1000 + 10 + 5 + false + true + + + + 2 2 + -1.0 -1.0 + 1.0 1.0 + + + neutron + + + 0 + + + 1 2 + total + collision + + + diff --git a/tests/regression_tests/delta_tracking_bcs/vacuumFalse/results_true.dat b/tests/regression_tests/delta_tracking_bcs/vacuumFalse/results_true.dat new file mode 100644 index 00000000000..1082fc6086c --- /dev/null +++ b/tests/regression_tests/delta_tracking_bcs/vacuumFalse/results_true.dat @@ -0,0 +1,11 @@ +k-combined: +6.509655E-02 1.331677E-03 +tally 1: +7.010000E-01 +9.943700E-02 +5.960000E-01 +7.183800E-02 +6.110000E-01 +7.488700E-02 +6.430000E-01 +8.386500E-02 diff --git a/tests/regression_tests/delta_tracking_bcs/vacuumTrue/inputs_true.dat b/tests/regression_tests/delta_tracking_bcs/vacuumTrue/inputs_true.dat new file mode 100644 index 00000000000..f1d4ee6c5b5 --- /dev/null +++ b/tests/regression_tests/delta_tracking_bcs/vacuumTrue/inputs_true.dat @@ -0,0 +1,76 @@ + + + + + + + + + + + + + + + + + + + + + 1.0 1.0 + 2 2 + -1.0 -1.0 + +1 1 +1 1 + + + + + + + + + eigenvalue + 1000 + 10 + 5 + true + true + + + + 2 2 + -1.0 -1.0 + 1.0 1.0 + + + 2 2 + -1.0 -1.0 + 1.0 1.0 + + + neutron + + + 0 + + + photon + + + 1 + + + 1 2 + total + collision + + + 3 4 + total + collision + + + diff --git a/tests/regression_tests/delta_tracking_bcs/vacuumTrue/results_true.dat b/tests/regression_tests/delta_tracking_bcs/vacuumTrue/results_true.dat new file mode 100644 index 00000000000..37fae217a06 --- /dev/null +++ b/tests/regression_tests/delta_tracking_bcs/vacuumTrue/results_true.dat @@ -0,0 +1,20 @@ +k-combined: +6.507844E-02 1.185162E-03 +tally 1: +6.370000E-01 +8.195500E-02 +6.700000E-01 +9.225800E-02 +6.200000E-01 +7.868400E-02 +6.610000E-01 +8.829700E-02 +tally 2: +3.337187E-01 +2.408676E-02 +3.443390E-01 +2.629035E-02 +3.168400E-01 +2.197952E-02 +3.345920E-01 +2.320579E-02 diff --git a/tests/regression_tests/delta_tracking_bcs/whiteFalse/inputs_true.dat b/tests/regression_tests/delta_tracking_bcs/whiteFalse/inputs_true.dat new file mode 100644 index 00000000000..0f5a0ab6cec --- /dev/null +++ b/tests/regression_tests/delta_tracking_bcs/whiteFalse/inputs_true.dat @@ -0,0 +1,60 @@ + + + + + + + + + + + + + + + + + + + + + 1.0 1.0 + 2 2 + -1.0 -1.0 + +1 1 +1 1 + + + + + + + + + eigenvalue + 1000 + 10 + 5 + false + true + + + + 2 2 + -1.0 -1.0 + 1.0 1.0 + + + neutron + + + 0 + + + 1 2 + total + collision + + + diff --git a/tests/regression_tests/delta_tracking_bcs/whiteFalse/results_true.dat b/tests/regression_tests/delta_tracking_bcs/whiteFalse/results_true.dat new file mode 100644 index 00000000000..7ef4e04cdd8 --- /dev/null +++ b/tests/regression_tests/delta_tracking_bcs/whiteFalse/results_true.dat @@ -0,0 +1,11 @@ +k-combined: +1.874386E+00 5.037177E-03 +tally 1: +1.577100E+01 +4.976951E+01 +1.571100E+01 +4.942012E+01 +1.567900E+01 +4.922117E+01 +1.533300E+01 +4.705902E+01 diff --git a/tests/regression_tests/delta_tracking_bcs/whiteTrue/inputs_true.dat b/tests/regression_tests/delta_tracking_bcs/whiteTrue/inputs_true.dat new file mode 100644 index 00000000000..32da22c58c8 --- /dev/null +++ b/tests/regression_tests/delta_tracking_bcs/whiteTrue/inputs_true.dat @@ -0,0 +1,76 @@ + + + + + + + + + + + + + + + + + + + + + 1.0 1.0 + 2 2 + -1.0 -1.0 + +1 1 +1 1 + + + + + + + + + eigenvalue + 1000 + 10 + 5 + true + true + + + + 2 2 + -1.0 -1.0 + 1.0 1.0 + + + 2 2 + -1.0 -1.0 + 1.0 1.0 + + + neutron + + + 0 + + + photon + + + 1 + + + 1 2 + total + collision + + + 3 4 + total + collision + + + diff --git a/tests/regression_tests/delta_tracking_bcs/whiteTrue/results_true.dat b/tests/regression_tests/delta_tracking_bcs/whiteTrue/results_true.dat new file mode 100644 index 00000000000..e5cfa717a8f --- /dev/null +++ b/tests/regression_tests/delta_tracking_bcs/whiteTrue/results_true.dat @@ -0,0 +1,20 @@ +k-combined: +1.862942E+00 2.642610E-03 +tally 1: +1.530500E+01 +4.685260E+01 +1.548200E+01 +4.799607E+01 +1.569500E+01 +4.937413E+01 +1.600300E+01 +5.128737E+01 +tally 2: +9.263828E+01 +1.716912E+03 +9.379487E+01 +1.762199E+03 +9.156886E+01 +1.679125E+03 +9.441403E+01 +1.784070E+03 diff --git a/tests/regression_tests/delta_tracking_history/test.py b/tests/regression_tests/delta_tracking_history/test.py index f74c718f2c4..53b58bdcc50 100644 --- a/tests/regression_tests/delta_tracking_history/test.py +++ b/tests/regression_tests/delta_tracking_history/test.py @@ -58,7 +58,7 @@ def model(): @pytest.mark.parametrize("photon", [False, True]) -def test_lattice_checkerboard(model, photon): +def test_lattice(model, photon): with change_directory(str(photon)): model.settings.photon_transport = photon if photon: From 86274de1efc9acd53675d2f503062480b2097610 Mon Sep 17 00:00:00 2001 From: nuclearkevin <66632997+nuclearkevin@users.noreply.github.com> Date: Fri, 12 Jun 2026 11:43:49 -0500 Subject: [PATCH 59/73] Only need the tolerance on the translational periodic BC. --- src/boundary_condition.cpp | 6 ++++++ src/particle.cpp | 5 ----- 2 files changed, 6 insertions(+), 5 deletions(-) diff --git a/src/boundary_condition.cpp b/src/boundary_condition.cpp index 5bbda483059..ed8b1ce469b 100644 --- a/src/boundary_condition.cpp +++ b/src/boundary_condition.cpp @@ -132,6 +132,12 @@ void TranslationalPeriodicBC::handle_particle( auto new_r = p.r() + translation_; int new_surface = p.surface() > 0 ? j_surf_ + 1 : -(j_surf_ + 1); + // Need to nudge the particle back into the domain when using delta tracking + // as event_delta_advance() does not go right up to the surface to fix tunneling. + if (p.delta_tracking()) { + new_r += FP_REL_PRECISION * p.u(); + } + // Handle the effects of the surface albedo on the particle's weight. BoundaryCondition::handle_albedo(p, surf); diff --git a/src/particle.cpp b/src/particle.cpp index 26b393aed76..7ace3d3fd12 100644 --- a/src/particle.cpp +++ b/src/particle.cpp @@ -864,11 +864,6 @@ void Particle::cross_periodic_bc( // Figure out what cell particle is in now n_coord() = 1; - // Need to nudge the particle back into the domain as event_delta_advance() - // does not go right up to the surface to fix tunneling. - if (delta_tracking()) { - r() += FP_REL_PRECISION * u(); - } if (!neighbor_list_find_cell(*this)) { mark_as_lost("Couldn't find particle after hitting periodic " "boundary on surface " + From 43b97443767c166ef513a64de981d13af568d95a Mon Sep 17 00:00:00 2001 From: nuclearkevin <66632997+nuclearkevin@users.noreply.github.com> Date: Fri, 12 Jun 2026 11:45:45 -0500 Subject: [PATCH 60/73] Update tests to account for the removal of duplicate code & consolidation. --- openmc/examples.py | 134 +++++++++++++++--- .../periodic/inputs_true.dat | 60 ++++++++ .../periodic/results_true.dat | 11 ++ .../periodicFalse/inputs_true.dat | 60 -------- .../periodicFalse/results_true.dat | 11 -- .../periodicTrue/inputs_true.dat | 76 ---------- .../periodicTrue/results_true.dat | 20 --- .../reflective/inputs_true.dat | 60 ++++++++ .../reflective/results_true.dat | 11 ++ .../reflectiveFalse/inputs_true.dat | 60 -------- .../reflectiveFalse/results_true.dat | 11 -- .../reflectiveTrue/inputs_true.dat | 76 ---------- .../reflectiveTrue/results_true.dat | 20 --- .../delta_tracking_bcs/test.py | 72 +--------- .../{vacuumFalse => vacuum}/inputs_true.dat | 20 +-- .../vacuum/results_true.dat | 11 ++ .../vacuumFalse/results_true.dat | 11 -- .../vacuumTrue/inputs_true.dat | 76 ---------- .../vacuumTrue/results_true.dat | 20 --- .../delta_tracking_bcs/white/inputs_true.dat | 60 ++++++++ .../delta_tracking_bcs/white/results_true.dat | 11 ++ .../whiteFalse/inputs_true.dat | 60 -------- .../whiteFalse/results_true.dat | 11 -- .../whiteTrue/inputs_true.dat | 76 ---------- .../whiteTrue/results_true.dat | 20 --- .../False/inputs_true.dat | 20 +-- .../False/results_true.dat | 18 +-- .../True/inputs_true.dat | 28 ++-- .../True/results_true.dat | 34 ++--- .../delta_tracking_distribrho/test.py | 69 +-------- .../False/inputs_true.dat | 20 +-- .../False/results_true.dat | 18 +-- .../delta_tracking_event/True/inputs_true.dat | 28 ++-- .../True/results_true.dat | 34 ++--- .../delta_tracking_event/test.py | 69 +-------- .../False/inputs_true.dat | 20 +-- .../False/results_true.dat | 18 +-- .../True/inputs_true.dat | 28 ++-- .../True/results_true.dat | 34 ++--- .../delta_tracking_history/test.py | 67 +-------- 40 files changed, 500 insertions(+), 1063 deletions(-) create mode 100644 tests/regression_tests/delta_tracking_bcs/periodic/inputs_true.dat create mode 100644 tests/regression_tests/delta_tracking_bcs/periodic/results_true.dat delete mode 100644 tests/regression_tests/delta_tracking_bcs/periodicFalse/inputs_true.dat delete mode 100644 tests/regression_tests/delta_tracking_bcs/periodicFalse/results_true.dat delete mode 100644 tests/regression_tests/delta_tracking_bcs/periodicTrue/inputs_true.dat delete mode 100644 tests/regression_tests/delta_tracking_bcs/periodicTrue/results_true.dat create mode 100644 tests/regression_tests/delta_tracking_bcs/reflective/inputs_true.dat create mode 100644 tests/regression_tests/delta_tracking_bcs/reflective/results_true.dat delete mode 100644 tests/regression_tests/delta_tracking_bcs/reflectiveFalse/inputs_true.dat delete mode 100644 tests/regression_tests/delta_tracking_bcs/reflectiveFalse/results_true.dat delete mode 100644 tests/regression_tests/delta_tracking_bcs/reflectiveTrue/inputs_true.dat delete mode 100644 tests/regression_tests/delta_tracking_bcs/reflectiveTrue/results_true.dat rename tests/regression_tests/delta_tracking_bcs/{vacuumFalse => vacuum}/inputs_true.dat (85%) create mode 100644 tests/regression_tests/delta_tracking_bcs/vacuum/results_true.dat delete mode 100644 tests/regression_tests/delta_tracking_bcs/vacuumFalse/results_true.dat delete mode 100644 tests/regression_tests/delta_tracking_bcs/vacuumTrue/inputs_true.dat delete mode 100644 tests/regression_tests/delta_tracking_bcs/vacuumTrue/results_true.dat create mode 100644 tests/regression_tests/delta_tracking_bcs/white/inputs_true.dat create mode 100644 tests/regression_tests/delta_tracking_bcs/white/results_true.dat delete mode 100644 tests/regression_tests/delta_tracking_bcs/whiteFalse/inputs_true.dat delete mode 100644 tests/regression_tests/delta_tracking_bcs/whiteFalse/results_true.dat delete mode 100644 tests/regression_tests/delta_tracking_bcs/whiteTrue/inputs_true.dat delete mode 100644 tests/regression_tests/delta_tracking_bcs/whiteTrue/results_true.dat diff --git a/openmc/examples.py b/openmc/examples.py index 6895bd94b53..9cd9c1c85db 100644 --- a/openmc/examples.py +++ b/openmc/examples.py @@ -6,6 +6,98 @@ PINCELL_PITCH = 1.26 # cm +def delta_tracking_lattice( + run_photon: bool = False, + boundary_type: str = 'reflective', + densities: list[float] | None = None) -> openmc.Model: + """Create a simple PWR-style lattice for testing delta tracking. + + Parameters + ---------- + run_photon : bool, optional + If coupled neutron-photon transport should be run or not. + boundary_type : str, optional + The boundary to apply to the outer surface of the infinite lattice. + densities : list of float, optional + The distributed cell densities to apply to cell 1 (the fuel) in the + infinite lattice. + + Returns + ------- + model : openmc.Model + A PWR-style 2x2 infinite assembly model + + """ + openmc.reset_auto_ids() + model = openmc.Model() + + DELTA_PIN_RADIUS = 0.4 + + # Create some simple materials. UO2 fuel for the inner cylinder in the pin, + # and water for the remainder of the domain. + uo2 = openmc.Material(name='UO2') + uo2.set_density('g/cm3', 10.0) + uo2.add_nuclide('U235', 1.0) + uo2.add_nuclide('O16', 2.0) + water = openmc.Material(name='light water') + water.add_nuclide('H1', 2.0) + water.add_nuclide('O16', 1.0) + water.set_density('g/cm3', 1.0) + water.add_s_alpha_beta('c_H_in_H2O') + model.materials.extend([uo2, water]) + + # Create the geometry, starting with the fuel pincell. + cyl = openmc.ZCylinder(r=DELTA_PIN_RADIUS) + pin = openmc.model.pin([cyl], [uo2, water]) + + # Create a 2x2 lattice to allow for distributed properties. + lattice = openmc.RectLattice() + lattice.lower_left = (-PINCELL_PITCH, -PINCELL_PITCH) + lattice.pitch = (PINCELL_PITCH, PINCELL_PITCH) + lattice.universes = [[pin, pin], + [pin, pin]] + box = openmc.model.RectangularPrism( + 2.0 * PINCELL_PITCH, 2.0 * PINCELL_PITCH, + origin=(0.0, 0.0), + boundary_type=boundary_type + ) + + # Set distributed densities if required. + if densities != None: + pin.cells[1].density = densities + + # Finally, save the geometry. + model.geometry = openmc.Geometry([openmc.Cell(fill=lattice, region=-box)]) + model.geometry.merge_surfaces = True + + # Add a mesh tally for the neutron total reaction rate. + msh = openmc.RegularMesh() + msh.lower_left = (-PINCELL_PITCH, -PINCELL_PITCH) + msh.upper_right = (PINCELL_PITCH, PINCELL_PITCH) + msh.dimension = (2, 2) + t = openmc.Tally() + t.filters = [openmc.ParticleFilter(bins='neutron'), openmc.MeshFilter(mesh=msh)] + t.scores = ['total'] + t.estimator = 'collision' + model.tallies.append(t) + + # If photon transport is required, add a mesh tally for the photon total reaction rate. + if run_photon: + t = openmc.Tally() + t.filters = [openmc.ParticleFilter(bins='photon'), openmc.MeshFilter(mesh=msh)] + t.scores = ['total'] + t.estimator = 'collision' + model.tallies.append(t) + + # Set some simulation settings. + model.settings.batches = 10 + model.settings.inactive = 5 + model.settings.particles = 1000 + model.settings.delta_tracking = True + model.settings.photon_transport = run_photon + + return model + def pwr_pin_cell() -> openmc.Model: """Create a PWR pin-cell model. @@ -968,7 +1060,7 @@ def random_ray_lattice(second_temp = False) -> openmc.Model: Whether or not the cross sections should contain two temperature datapoints. The first data point is the C5G7 cross sections, which corresponds to a temperature of 294 K. The second data point is the C5G7 cross sections multiplied by 1/2, - which corresponds to a temperature of 3934 K. This temperature dependence is + which corresponds to a temperature of 394 K. This temperature dependence is fictitious; it is used for testing temperature feedback in the random ray solver. Returns @@ -1314,17 +1406,17 @@ def fill_cube(N, n_1, n_2, fill_1, fill_2, fill_3): def random_ray_three_region_cube_with_detectors() -> openmc.Model: """Create a three region cube model with two external tally regions. - This is an adaptation of the simple monoenergetic problem of a cube with - three concentric cubic regions. The innermost region is near void (with - Sigma_t around 10^-5) and contains an external isotropic source term, the - middle region is a mild scatterer (with Sigma_t around 10^-3), and the - outer region of the cube is a scatterer and absorber (with Sigma_t around + This is an adaptation of the simple monoenergetic problem of a cube with + three concentric cubic regions. The innermost region is near void (with + Sigma_t around 10^-5) and contains an external isotropic source term, the + middle region is a mild scatterer (with Sigma_t around 10^-3), and the + outer region of the cube is a scatterer and absorber (with Sigma_t around 1). - Two cubic "detector" regions are found outside this geometry, one along the - y-axis near z=0, and the other in the upper right corner of the system. - The size of each detector is scaled to be equal to that of the source - region. The model returned by this function contains cell tallies on each + Two cubic "detector" regions are found outside this geometry, one along the + y-axis near z=0, and the other in the upper right corner of the system. + The size of each detector is scaled to be equal to that of the source + region. The model returned by this function contains cell tallies on each detector. Returns @@ -1498,29 +1590,29 @@ def fill_cube(N, n_1, n_2, fill_1, fill_2, fill_3): fill=absorber_mat, region=detector2_region ) - + external_x = ( - +x_high & +y_low & +z_low & -x_outer & + +x_high & +y_low & +z_low & -x_outer & ((-y_outer & -z_high) | (-y_high & +z_high & -z_outer)) ) external_y = ( - +y_high & -y_outer & + +y_high & -y_outer & ( - (+detector1_right & -x_high & +z_low & -z_outer) | - (-detector1_right & +x_low & +detector1_top & -z_outer) | + (+detector1_right & -x_high & +z_low & -z_outer) | + (-detector1_right & +x_low & +detector1_top & -z_outer) | (+x_high & -x_outer & +z_low & -z_high) ) ) external_z = ( - +x_low & +y_low & +z_high & -z_outer & + +x_low & +y_low & +z_high & -z_outer & ((-y_outer & -x_high) | (-y_high & +x_high & -x_outer)) ) - external_cell = openmc.Cell(fill=cavity_mat, - region=(external_x | external_y | external_z), + external_cell = openmc.Cell(fill=cavity_mat, + region=(external_x | external_y | external_z), name='outside cube') root = openmc.Universe( - name='root universe', + name='root universe', cells=[cube_domain, detector1, detector2, external_cell] ) @@ -1604,8 +1696,8 @@ def fill_cube(N, n_1, n_2, fill_1, fill_2, fill_3): source_tally.estimator = estimator # Instantiate a Tallies collection and export to XML - tallies = openmc.Tallies([detector1_tally, - detector2_tally, + tallies = openmc.Tallies([detector1_tally, + detector2_tally, absorber_tally, cavity_tally, source_tally]) diff --git a/tests/regression_tests/delta_tracking_bcs/periodic/inputs_true.dat b/tests/regression_tests/delta_tracking_bcs/periodic/inputs_true.dat new file mode 100644 index 00000000000..29f406a342c --- /dev/null +++ b/tests/regression_tests/delta_tracking_bcs/periodic/inputs_true.dat @@ -0,0 +1,60 @@ + + + + + + + + + + + + + + + + + + + + + 1.26 1.26 + 2 2 + -1.26 -1.26 + +7 7 +7 7 + + + + + + + + + eigenvalue + 1000 + 10 + 5 + false + true + + + + 2 2 + -1.26 -1.26 + 1.26 1.26 + + + neutron + + + 3 + + + 5 6 + total + collision + + + diff --git a/tests/regression_tests/delta_tracking_bcs/periodic/results_true.dat b/tests/regression_tests/delta_tracking_bcs/periodic/results_true.dat new file mode 100644 index 00000000000..dd6eca2dee8 --- /dev/null +++ b/tests/regression_tests/delta_tracking_bcs/periodic/results_true.dat @@ -0,0 +1,11 @@ +k-combined: +1.849018E+00 2.929263E-03 +tally 1: +1.874900E+01 +7.040473E+01 +1.847800E+01 +6.831298E+01 +1.859800E+01 +6.926442E+01 +1.834400E+01 +6.735645E+01 diff --git a/tests/regression_tests/delta_tracking_bcs/periodicFalse/inputs_true.dat b/tests/regression_tests/delta_tracking_bcs/periodicFalse/inputs_true.dat deleted file mode 100644 index e01569b98a4..00000000000 --- a/tests/regression_tests/delta_tracking_bcs/periodicFalse/inputs_true.dat +++ /dev/null @@ -1,60 +0,0 @@ - - - - - - - - - - - - - - - - - - - - - 1.0 1.0 - 2 2 - -1.0 -1.0 - -1 1 -1 1 - - - - - - - - - eigenvalue - 1000 - 10 - 5 - false - true - - - - 2 2 - -1.0 -1.0 - 1.0 1.0 - - - neutron - - - 0 - - - 1 2 - total - collision - - - diff --git a/tests/regression_tests/delta_tracking_bcs/periodicFalse/results_true.dat b/tests/regression_tests/delta_tracking_bcs/periodicFalse/results_true.dat deleted file mode 100644 index aaddfe9b0fd..00000000000 --- a/tests/regression_tests/delta_tracking_bcs/periodicFalse/results_true.dat +++ /dev/null @@ -1,11 +0,0 @@ -k-combined: -1.862405E+00 3.970186E-03 -tally 1: -1.590300E+01 -5.059012E+01 -1.556600E+01 -4.848489E+01 -1.592000E+01 -5.071728E+01 -1.562800E+01 -4.886343E+01 diff --git a/tests/regression_tests/delta_tracking_bcs/periodicTrue/inputs_true.dat b/tests/regression_tests/delta_tracking_bcs/periodicTrue/inputs_true.dat deleted file mode 100644 index c300a297da8..00000000000 --- a/tests/regression_tests/delta_tracking_bcs/periodicTrue/inputs_true.dat +++ /dev/null @@ -1,76 +0,0 @@ - - - - - - - - - - - - - - - - - - - - - 1.0 1.0 - 2 2 - -1.0 -1.0 - -1 1 -1 1 - - - - - - - - - eigenvalue - 1000 - 10 - 5 - true - true - - - - 2 2 - -1.0 -1.0 - 1.0 1.0 - - - 2 2 - -1.0 -1.0 - 1.0 1.0 - - - neutron - - - 0 - - - photon - - - 1 - - - 1 2 - total - collision - - - 3 4 - total - collision - - - diff --git a/tests/regression_tests/delta_tracking_bcs/periodicTrue/results_true.dat b/tests/regression_tests/delta_tracking_bcs/periodicTrue/results_true.dat deleted file mode 100644 index 089934cc199..00000000000 --- a/tests/regression_tests/delta_tracking_bcs/periodicTrue/results_true.dat +++ /dev/null @@ -1,20 +0,0 @@ -k-combined: -1.865102E+00 1.598906E-03 -tally 1: -1.547100E+01 -4.788337E+01 -1.540300E+01 -4.746582E+01 -1.532000E+01 -4.695871E+01 -1.546300E+01 -4.786204E+01 -tally 2: -9.316751E+01 -1.737859E+03 -9.255290E+01 -1.713493E+03 -9.233657E+01 -1.705814E+03 -9.158787E+01 -1.678355E+03 diff --git a/tests/regression_tests/delta_tracking_bcs/reflective/inputs_true.dat b/tests/regression_tests/delta_tracking_bcs/reflective/inputs_true.dat new file mode 100644 index 00000000000..cdcfcc3e70d --- /dev/null +++ b/tests/regression_tests/delta_tracking_bcs/reflective/inputs_true.dat @@ -0,0 +1,60 @@ + + + + + + + + + + + + + + + + + + + + + 1.26 1.26 + 2 2 + -1.26 -1.26 + +4 4 +4 4 + + + + + + + + + eigenvalue + 1000 + 10 + 5 + false + true + + + + 2 2 + -1.26 -1.26 + 1.26 1.26 + + + neutron + + + 2 + + + 3 4 + total + collision + + + diff --git a/tests/regression_tests/delta_tracking_bcs/reflective/results_true.dat b/tests/regression_tests/delta_tracking_bcs/reflective/results_true.dat new file mode 100644 index 00000000000..bcf614974d6 --- /dev/null +++ b/tests/regression_tests/delta_tracking_bcs/reflective/results_true.dat @@ -0,0 +1,11 @@ +k-combined: +1.850729E+00 2.180813E-03 +tally 1: +1.828800E+01 +6.694305E+01 +1.823900E+01 +6.658124E+01 +1.894000E+01 +7.184607E+01 +1.852200E+01 +6.869212E+01 diff --git a/tests/regression_tests/delta_tracking_bcs/reflectiveFalse/inputs_true.dat b/tests/regression_tests/delta_tracking_bcs/reflectiveFalse/inputs_true.dat deleted file mode 100644 index 2177b2a8d5e..00000000000 --- a/tests/regression_tests/delta_tracking_bcs/reflectiveFalse/inputs_true.dat +++ /dev/null @@ -1,60 +0,0 @@ - - - - - - - - - - - - - - - - - - - - - 1.0 1.0 - 2 2 - -1.0 -1.0 - -1 1 -1 1 - - - - - - - - - eigenvalue - 1000 - 10 - 5 - false - true - - - - 2 2 - -1.0 -1.0 - 1.0 1.0 - - - neutron - - - 0 - - - 1 2 - total - collision - - - diff --git a/tests/regression_tests/delta_tracking_bcs/reflectiveFalse/results_true.dat b/tests/regression_tests/delta_tracking_bcs/reflectiveFalse/results_true.dat deleted file mode 100644 index a388688876f..00000000000 --- a/tests/regression_tests/delta_tracking_bcs/reflectiveFalse/results_true.dat +++ /dev/null @@ -1,11 +0,0 @@ -k-combined: -1.869636E+00 4.475641E-03 -tally 1: -1.558300E+01 -4.857891E+01 -1.578200E+01 -4.982111E+01 -1.489300E+01 -4.438229E+01 -1.544300E+01 -4.771581E+01 diff --git a/tests/regression_tests/delta_tracking_bcs/reflectiveTrue/inputs_true.dat b/tests/regression_tests/delta_tracking_bcs/reflectiveTrue/inputs_true.dat deleted file mode 100644 index 7e2b5896073..00000000000 --- a/tests/regression_tests/delta_tracking_bcs/reflectiveTrue/inputs_true.dat +++ /dev/null @@ -1,76 +0,0 @@ - - - - - - - - - - - - - - - - - - - - - 1.0 1.0 - 2 2 - -1.0 -1.0 - -1 1 -1 1 - - - - - - - - - eigenvalue - 1000 - 10 - 5 - true - true - - - - 2 2 - -1.0 -1.0 - 1.0 1.0 - - - 2 2 - -1.0 -1.0 - 1.0 1.0 - - - neutron - - - 0 - - - photon - - - 1 - - - 1 2 - total - collision - - - 3 4 - total - collision - - - diff --git a/tests/regression_tests/delta_tracking_bcs/reflectiveTrue/results_true.dat b/tests/regression_tests/delta_tracking_bcs/reflectiveTrue/results_true.dat deleted file mode 100644 index 5d33cb06d5d..00000000000 --- a/tests/regression_tests/delta_tracking_bcs/reflectiveTrue/results_true.dat +++ /dev/null @@ -1,20 +0,0 @@ -k-combined: -1.863579E+00 4.987651E-03 -tally 1: -1.532500E+01 -4.698978E+01 -1.517100E+01 -4.606297E+01 -1.531800E+01 -4.694604E+01 -1.547900E+01 -4.801024E+01 -tally 2: -9.001423E+01 -1.621357E+03 -9.138405E+01 -1.671319E+03 -9.036599E+01 -1.634452E+03 -9.160015E+01 -1.681369E+03 diff --git a/tests/regression_tests/delta_tracking_bcs/test.py b/tests/regression_tests/delta_tracking_bcs/test.py index cf3205cfa43..13e93393f96 100644 --- a/tests/regression_tests/delta_tracking_bcs/test.py +++ b/tests/regression_tests/delta_tracking_bcs/test.py @@ -1,76 +1,14 @@ import openmc import pytest from openmc.utility_funcs import change_directory +from openmc.examples import delta_tracking_lattice from tests.testing_harness import PyAPITestHarness -def model(boundary_type): - openmc.reset_auto_ids() - model = openmc.Model() - - uo2 = openmc.Material(name='UO2') - uo2.set_density('g/cm3', 10.0) - uo2.add_nuclide('U235', 1.0) - uo2.add_nuclide('O16', 2.0) - water = openmc.Material(name='light water') - water.add_nuclide('H1', 2.0) - water.add_nuclide('O16', 1.0) - water.set_density('g/cm3', 1.0) - water.add_s_alpha_beta('c_H_in_H2O') - model.materials.extend([uo2, water]) - - cyl = openmc.ZCylinder(r=0.4) - pin = openmc.model.pin([cyl], [uo2, water]) - d = 1.0 - - lattice = openmc.RectLattice() - lattice.lower_left = (-d, -d) - lattice.pitch = (d, d) - lattice.universes = [[pin, pin], - [pin, pin]] - box = openmc.model.RectangularPrism( - 2.0 * d, 2.0 * d, - origin=(0.0, 0.0), - boundary_type=boundary_type - ) - - model.geometry = openmc.Geometry([openmc.Cell(fill=lattice, region=-box)]) - model.geometry.merge_surfaces = True - - msh = openmc.RegularMesh(mesh_id=0) - msh.lower_left = (-d, -d) - msh.upper_right = (d, d) - msh.dimension = (2, 2) - t = openmc.Tally() - t.filters = [openmc.ParticleFilter(bins='neutron'), openmc.MeshFilter(mesh=msh)] - t.scores = ['total'] - t.estimator = 'collision' - model.tallies.append(t) - - model.settings.batches = 10 - model.settings.inactive = 5 - model.settings.particles = 1000 - model.settings.delta_tracking = True - - return model - - -@pytest.mark.parametrize("photon", [False, True]) @pytest.mark.parametrize("boundary", ['vacuum', 'reflective', 'periodic', 'white']) -def test_lattice(photon, boundary): - m = model(boundary) - with change_directory(boundary+str(photon)): - m.settings.photon_transport = photon - if photon: - msh = openmc.RegularMesh(mesh_id=1) - msh.lower_left = (-1.0, -1.0) - msh.upper_right = (1.0, 1.0) - msh.dimension = (2, 2) - t = openmc.Tally() - t.filters = [openmc.ParticleFilter(bins='photon'), openmc.MeshFilter(mesh=msh)] - t.scores = ['total'] - t.estimator = 'collision' - m.tallies.append(t) - harness = PyAPITestHarness('statepoint.10.h5', m) +def test_lattice(boundary): + with change_directory(boundary): + model = delta_tracking_lattice(False, boundary) + harness = PyAPITestHarness('statepoint.10.h5', model) harness.main() diff --git a/tests/regression_tests/delta_tracking_bcs/vacuumFalse/inputs_true.dat b/tests/regression_tests/delta_tracking_bcs/vacuum/inputs_true.dat similarity index 85% rename from tests/regression_tests/delta_tracking_bcs/vacuumFalse/inputs_true.dat rename to tests/regression_tests/delta_tracking_bcs/vacuum/inputs_true.dat index adc875d6aca..bb003376a1d 100644 --- a/tests/regression_tests/delta_tracking_bcs/vacuumFalse/inputs_true.dat +++ b/tests/regression_tests/delta_tracking_bcs/vacuum/inputs_true.dat @@ -18,18 +18,18 @@ - 1.0 1.0 + 1.26 1.26 2 2 - -1.0 -1.0 + -1.26 -1.26 1 1 1 1 - - - - + + + + eigenvalue @@ -40,16 +40,16 @@ true - + 2 2 - -1.0 -1.0 - 1.0 1.0 + -1.26 -1.26 + 1.26 1.26 neutron - 0 + 1 1 2 diff --git a/tests/regression_tests/delta_tracking_bcs/vacuum/results_true.dat b/tests/regression_tests/delta_tracking_bcs/vacuum/results_true.dat new file mode 100644 index 00000000000..d3a9260b537 --- /dev/null +++ b/tests/regression_tests/delta_tracking_bcs/vacuum/results_true.dat @@ -0,0 +1,11 @@ +k-combined: +6.926354E-02 2.083939E-03 +tally 1: +9.270000E-01 +1.739570E-01 +1.017000E+00 +2.077250E-01 +8.920000E-01 +1.607780E-01 +9.290000E-01 +1.727250E-01 diff --git a/tests/regression_tests/delta_tracking_bcs/vacuumFalse/results_true.dat b/tests/regression_tests/delta_tracking_bcs/vacuumFalse/results_true.dat deleted file mode 100644 index 1082fc6086c..00000000000 --- a/tests/regression_tests/delta_tracking_bcs/vacuumFalse/results_true.dat +++ /dev/null @@ -1,11 +0,0 @@ -k-combined: -6.509655E-02 1.331677E-03 -tally 1: -7.010000E-01 -9.943700E-02 -5.960000E-01 -7.183800E-02 -6.110000E-01 -7.488700E-02 -6.430000E-01 -8.386500E-02 diff --git a/tests/regression_tests/delta_tracking_bcs/vacuumTrue/inputs_true.dat b/tests/regression_tests/delta_tracking_bcs/vacuumTrue/inputs_true.dat deleted file mode 100644 index f1d4ee6c5b5..00000000000 --- a/tests/regression_tests/delta_tracking_bcs/vacuumTrue/inputs_true.dat +++ /dev/null @@ -1,76 +0,0 @@ - - - - - - - - - - - - - - - - - - - - - 1.0 1.0 - 2 2 - -1.0 -1.0 - -1 1 -1 1 - - - - - - - - - eigenvalue - 1000 - 10 - 5 - true - true - - - - 2 2 - -1.0 -1.0 - 1.0 1.0 - - - 2 2 - -1.0 -1.0 - 1.0 1.0 - - - neutron - - - 0 - - - photon - - - 1 - - - 1 2 - total - collision - - - 3 4 - total - collision - - - diff --git a/tests/regression_tests/delta_tracking_bcs/vacuumTrue/results_true.dat b/tests/regression_tests/delta_tracking_bcs/vacuumTrue/results_true.dat deleted file mode 100644 index 37fae217a06..00000000000 --- a/tests/regression_tests/delta_tracking_bcs/vacuumTrue/results_true.dat +++ /dev/null @@ -1,20 +0,0 @@ -k-combined: -6.507844E-02 1.185162E-03 -tally 1: -6.370000E-01 -8.195500E-02 -6.700000E-01 -9.225800E-02 -6.200000E-01 -7.868400E-02 -6.610000E-01 -8.829700E-02 -tally 2: -3.337187E-01 -2.408676E-02 -3.443390E-01 -2.629035E-02 -3.168400E-01 -2.197952E-02 -3.345920E-01 -2.320579E-02 diff --git a/tests/regression_tests/delta_tracking_bcs/white/inputs_true.dat b/tests/regression_tests/delta_tracking_bcs/white/inputs_true.dat new file mode 100644 index 00000000000..6643f01d596 --- /dev/null +++ b/tests/regression_tests/delta_tracking_bcs/white/inputs_true.dat @@ -0,0 +1,60 @@ + + + + + + + + + + + + + + + + + + + + + 1.26 1.26 + 2 2 + -1.26 -1.26 + +10 10 +10 10 + + + + + + + + + eigenvalue + 1000 + 10 + 5 + false + true + + + + 2 2 + -1.26 -1.26 + 1.26 1.26 + + + neutron + + + 4 + + + 7 8 + total + collision + + + diff --git a/tests/regression_tests/delta_tracking_bcs/white/results_true.dat b/tests/regression_tests/delta_tracking_bcs/white/results_true.dat new file mode 100644 index 00000000000..92ddc2dee65 --- /dev/null +++ b/tests/regression_tests/delta_tracking_bcs/white/results_true.dat @@ -0,0 +1,11 @@ +k-combined: +1.843368E+00 1.587102E-03 +tally 1: +1.835700E+01 +6.749374E+01 +1.936700E+01 +7.503086E+01 +1.872400E+01 +7.013084E+01 +1.857200E+01 +6.902069E+01 diff --git a/tests/regression_tests/delta_tracking_bcs/whiteFalse/inputs_true.dat b/tests/regression_tests/delta_tracking_bcs/whiteFalse/inputs_true.dat deleted file mode 100644 index 0f5a0ab6cec..00000000000 --- a/tests/regression_tests/delta_tracking_bcs/whiteFalse/inputs_true.dat +++ /dev/null @@ -1,60 +0,0 @@ - - - - - - - - - - - - - - - - - - - - - 1.0 1.0 - 2 2 - -1.0 -1.0 - -1 1 -1 1 - - - - - - - - - eigenvalue - 1000 - 10 - 5 - false - true - - - - 2 2 - -1.0 -1.0 - 1.0 1.0 - - - neutron - - - 0 - - - 1 2 - total - collision - - - diff --git a/tests/regression_tests/delta_tracking_bcs/whiteFalse/results_true.dat b/tests/regression_tests/delta_tracking_bcs/whiteFalse/results_true.dat deleted file mode 100644 index 7ef4e04cdd8..00000000000 --- a/tests/regression_tests/delta_tracking_bcs/whiteFalse/results_true.dat +++ /dev/null @@ -1,11 +0,0 @@ -k-combined: -1.874386E+00 5.037177E-03 -tally 1: -1.577100E+01 -4.976951E+01 -1.571100E+01 -4.942012E+01 -1.567900E+01 -4.922117E+01 -1.533300E+01 -4.705902E+01 diff --git a/tests/regression_tests/delta_tracking_bcs/whiteTrue/inputs_true.dat b/tests/regression_tests/delta_tracking_bcs/whiteTrue/inputs_true.dat deleted file mode 100644 index 32da22c58c8..00000000000 --- a/tests/regression_tests/delta_tracking_bcs/whiteTrue/inputs_true.dat +++ /dev/null @@ -1,76 +0,0 @@ - - - - - - - - - - - - - - - - - - - - - 1.0 1.0 - 2 2 - -1.0 -1.0 - -1 1 -1 1 - - - - - - - - - eigenvalue - 1000 - 10 - 5 - true - true - - - - 2 2 - -1.0 -1.0 - 1.0 1.0 - - - 2 2 - -1.0 -1.0 - 1.0 1.0 - - - neutron - - - 0 - - - photon - - - 1 - - - 1 2 - total - collision - - - 3 4 - total - collision - - - diff --git a/tests/regression_tests/delta_tracking_bcs/whiteTrue/results_true.dat b/tests/regression_tests/delta_tracking_bcs/whiteTrue/results_true.dat deleted file mode 100644 index e5cfa717a8f..00000000000 --- a/tests/regression_tests/delta_tracking_bcs/whiteTrue/results_true.dat +++ /dev/null @@ -1,20 +0,0 @@ -k-combined: -1.862942E+00 2.642610E-03 -tally 1: -1.530500E+01 -4.685260E+01 -1.548200E+01 -4.799607E+01 -1.569500E+01 -4.937413E+01 -1.600300E+01 -5.128737E+01 -tally 2: -9.263828E+01 -1.716912E+03 -9.379487E+01 -1.762199E+03 -9.156886E+01 -1.679125E+03 -9.441403E+01 -1.784070E+03 diff --git a/tests/regression_tests/delta_tracking_distribrho/False/inputs_true.dat b/tests/regression_tests/delta_tracking_distribrho/False/inputs_true.dat index 6bd03229746..1485d929ae9 100644 --- a/tests/regression_tests/delta_tracking_distribrho/False/inputs_true.dat +++ b/tests/regression_tests/delta_tracking_distribrho/False/inputs_true.dat @@ -20,18 +20,18 @@ - 1.0 1.0 + 1.26 1.26 2 2 - -1.0 -1.0 + -1.26 -1.26 1 1 1 1 - - - - + + + + eigenvalue @@ -42,16 +42,16 @@ true - + 2 2 - -1.0 -1.0 - 1.0 1.0 + -1.26 -1.26 + 1.26 1.26 neutron - 0 + 1 1 2 diff --git a/tests/regression_tests/delta_tracking_distribrho/False/results_true.dat b/tests/regression_tests/delta_tracking_distribrho/False/results_true.dat index 014825376b2..23b3eb24804 100644 --- a/tests/regression_tests/delta_tracking_distribrho/False/results_true.dat +++ b/tests/regression_tests/delta_tracking_distribrho/False/results_true.dat @@ -1,11 +1,11 @@ k-combined: -1.902488E+00 3.242884E-03 +1.868055E+00 4.212977E-03 tally 1: -1.246500E+01 -3.108325E+01 -1.684800E+01 -5.679135E+01 -1.239600E+01 -3.082090E+01 -1.640800E+01 -5.386842E+01 +1.649200E+01 +5.445790E+01 +1.884800E+01 +7.109906E+01 +1.611600E+01 +5.202239E+01 +1.841400E+01 +6.788397E+01 diff --git a/tests/regression_tests/delta_tracking_distribrho/True/inputs_true.dat b/tests/regression_tests/delta_tracking_distribrho/True/inputs_true.dat index d3b594f2a78..c4367581ff0 100644 --- a/tests/regression_tests/delta_tracking_distribrho/True/inputs_true.dat +++ b/tests/regression_tests/delta_tracking_distribrho/True/inputs_true.dat @@ -20,18 +20,18 @@ - 1.0 1.0 + 1.26 1.26 2 2 - -1.0 -1.0 + -1.26 -1.26 1 1 1 1 - - - - + + + + eigenvalue @@ -42,35 +42,27 @@ true - - 2 2 - -1.0 -1.0 - 1.0 1.0 - 2 2 - -1.0 -1.0 - 1.0 1.0 + -1.26 -1.26 + 1.26 1.26 neutron - 0 + 1 photon - - 1 - 1 2 total collision - 3 4 + 3 2 total collision diff --git a/tests/regression_tests/delta_tracking_distribrho/True/results_true.dat b/tests/regression_tests/delta_tracking_distribrho/True/results_true.dat index 502f7b7c1fc..d395c34a762 100644 --- a/tests/regression_tests/delta_tracking_distribrho/True/results_true.dat +++ b/tests/regression_tests/delta_tracking_distribrho/True/results_true.dat @@ -1,20 +1,20 @@ k-combined: -1.905169E+00 2.601649E-03 +1.863429E+00 3.189404E-03 tally 1: -1.280100E+01 -3.280666E+01 -1.668400E+01 -5.572759E+01 -1.270200E+01 -3.227630E+01 -1.645400E+01 -5.417179E+01 +1.638600E+01 +5.380010E+01 +1.836200E+01 +6.749015E+01 +1.596600E+01 +5.106840E+01 +1.903000E+01 +7.262313E+01 tally 2: -6.522825E+01 -8.512465E+02 -1.239644E+02 -3.074655E+03 -6.644431E+01 -8.835009E+02 -1.228901E+02 -3.022436E+03 +6.548566E+01 +8.595674E+02 +1.145647E+02 +2.625219E+03 +6.537393E+01 +8.552263E+02 +1.154873E+02 +2.672011E+03 diff --git a/tests/regression_tests/delta_tracking_distribrho/test.py b/tests/regression_tests/delta_tracking_distribrho/test.py index f609a12f5e2..1687447dd5e 100644 --- a/tests/regression_tests/delta_tracking_distribrho/test.py +++ b/tests/regression_tests/delta_tracking_distribrho/test.py @@ -1,77 +1,14 @@ import openmc import pytest from openmc.utility_funcs import change_directory +from openmc.examples import delta_tracking_lattice from tests.testing_harness import PyAPITestHarness -@pytest.fixture -def model(): - openmc.reset_auto_ids() - model = openmc.Model() - - uo2 = openmc.Material(name='UO2') - uo2.set_density('g/cm3', 10.0) - uo2.add_nuclide('U235', 1.0) - uo2.add_nuclide('O16', 2.0) - water = openmc.Material(name='light water') - water.add_nuclide('H1', 2.0) - water.add_nuclide('O16', 1.0) - water.set_density('g/cm3', 1.0) - water.add_s_alpha_beta('c_H_in_H2O') - model.materials.extend([uo2, water]) - - cyl = openmc.ZCylinder(r=0.4) - pin = openmc.model.pin([cyl], [uo2, water]) - d = 1.0 - - lattice = openmc.RectLattice() - lattice.lower_left = (-d, -d) - lattice.pitch = (d, d) - lattice.universes = [[pin, pin], - [pin, pin]] - box = openmc.model.RectangularPrism( - 2.0 * d, 2.0 * d, - origin=(0.0, 0.0), - boundary_type='reflective' - ) - - pin.cells[1].density = [10.0, 20.0, 10.0, 20.0] - - model.geometry = openmc.Geometry([openmc.Cell(fill=lattice, region=-box)]) - model.geometry.merge_surfaces = True - - msh = openmc.RegularMesh(mesh_id=0) - msh.lower_left = (-d, -d) - msh.upper_right = (d, d) - msh.dimension = (2, 2) - t = openmc.Tally() - t.filters = [openmc.ParticleFilter(bins='neutron'), openmc.MeshFilter(mesh=msh)] - t.scores = ['total'] - t.estimator = 'collision' - model.tallies.append(t) - - model.settings.batches = 10 - model.settings.inactive = 5 - model.settings.particles = 1000 - model.settings.delta_tracking = True - - return model - - @pytest.mark.parametrize("photon", [False, True]) -def test_lattice_checkerboard(model, photon): +def test_lattice_checkerboard(photon): with change_directory(str(photon)): - model.settings.photon_transport = photon - if photon: - msh = openmc.RegularMesh(mesh_id=1) - msh.lower_left = (-1.0, -1.0) - msh.upper_right = (1.0, 1.0) - msh.dimension = (2, 2) - t = openmc.Tally() - t.filters = [openmc.ParticleFilter(bins='photon'), openmc.MeshFilter(mesh=msh)] - t.scores = ['total'] - t.estimator = 'collision' - model.tallies.append(t) + model = delta_tracking_lattice(photon, densities=[10.0, 20.0, 10.0, 20.0]) harness = PyAPITestHarness('statepoint.10.h5', model) harness.main() diff --git a/tests/regression_tests/delta_tracking_event/False/inputs_true.dat b/tests/regression_tests/delta_tracking_event/False/inputs_true.dat index e302eef7bdf..15399916e6f 100644 --- a/tests/regression_tests/delta_tracking_event/False/inputs_true.dat +++ b/tests/regression_tests/delta_tracking_event/False/inputs_true.dat @@ -18,18 +18,18 @@ - 1.0 1.0 + 1.26 1.26 2 2 - -1.0 -1.0 + -1.26 -1.26 1 1 1 1 - - - - + + + + eigenvalue @@ -41,16 +41,16 @@ true - + 2 2 - -1.0 -1.0 - 1.0 1.0 + -1.26 -1.26 + 1.26 1.26 neutron - 0 + 1 1 2 diff --git a/tests/regression_tests/delta_tracking_event/False/results_true.dat b/tests/regression_tests/delta_tracking_event/False/results_true.dat index a388688876f..bcf614974d6 100644 --- a/tests/regression_tests/delta_tracking_event/False/results_true.dat +++ b/tests/regression_tests/delta_tracking_event/False/results_true.dat @@ -1,11 +1,11 @@ k-combined: -1.869636E+00 4.475641E-03 +1.850729E+00 2.180813E-03 tally 1: -1.558300E+01 -4.857891E+01 -1.578200E+01 -4.982111E+01 -1.489300E+01 -4.438229E+01 -1.544300E+01 -4.771581E+01 +1.828800E+01 +6.694305E+01 +1.823900E+01 +6.658124E+01 +1.894000E+01 +7.184607E+01 +1.852200E+01 +6.869212E+01 diff --git a/tests/regression_tests/delta_tracking_event/True/inputs_true.dat b/tests/regression_tests/delta_tracking_event/True/inputs_true.dat index 9c8a39f3cfe..2adbabe8601 100644 --- a/tests/regression_tests/delta_tracking_event/True/inputs_true.dat +++ b/tests/regression_tests/delta_tracking_event/True/inputs_true.dat @@ -18,18 +18,18 @@ - 1.0 1.0 + 1.26 1.26 2 2 - -1.0 -1.0 + -1.26 -1.26 1 1 1 1 - - - - + + + + eigenvalue @@ -41,35 +41,27 @@ true - - 2 2 - -1.0 -1.0 - 1.0 1.0 - 2 2 - -1.0 -1.0 - 1.0 1.0 + -1.26 -1.26 + 1.26 1.26 neutron - 0 + 1 photon - - 1 - 1 2 total collision - 3 4 + 3 2 total collision diff --git a/tests/regression_tests/delta_tracking_event/True/results_true.dat b/tests/regression_tests/delta_tracking_event/True/results_true.dat index 5d33cb06d5d..27e7afcefb7 100644 --- a/tests/regression_tests/delta_tracking_event/True/results_true.dat +++ b/tests/regression_tests/delta_tracking_event/True/results_true.dat @@ -1,20 +1,20 @@ k-combined: -1.863579E+00 4.987651E-03 +1.841093E+00 4.770249E-03 tally 1: -1.532500E+01 -4.698978E+01 -1.517100E+01 -4.606297E+01 -1.531800E+01 -4.694604E+01 -1.547900E+01 -4.801024E+01 +1.878600E+01 +7.065042E+01 +1.829400E+01 +6.696300E+01 +1.883000E+01 +7.095247E+01 +1.854300E+01 +6.883357E+01 tally 2: -9.001423E+01 -1.621357E+03 -9.138405E+01 -1.671319E+03 -9.036599E+01 -1.634452E+03 -9.160015E+01 -1.681369E+03 +8.714295E+01 +1.519366E+03 +8.757274E+01 +1.533958E+03 +8.827597E+01 +1.558930E+03 +8.794806E+01 +1.547220E+03 diff --git a/tests/regression_tests/delta_tracking_event/test.py b/tests/regression_tests/delta_tracking_event/test.py index a6e9be89090..fded025f57d 100644 --- a/tests/regression_tests/delta_tracking_event/test.py +++ b/tests/regression_tests/delta_tracking_event/test.py @@ -1,76 +1,15 @@ import openmc import pytest from openmc.utility_funcs import change_directory +from openmc.examples import delta_tracking_lattice from tests.testing_harness import PyAPITestHarness -@pytest.fixture -def model(): - openmc.reset_auto_ids() - model = openmc.Model() - - uo2 = openmc.Material(name='UO2') - uo2.set_density('g/cm3', 10.0) - uo2.add_nuclide('U235', 1.0) - uo2.add_nuclide('O16', 2.0) - water = openmc.Material(name='light water') - water.add_nuclide('H1', 2.0) - water.add_nuclide('O16', 1.0) - water.set_density('g/cm3', 1.0) - water.add_s_alpha_beta('c_H_in_H2O') - model.materials.extend([uo2, water]) - - cyl = openmc.ZCylinder(r=0.4) - pin = openmc.model.pin([cyl], [uo2, water]) - d = 1.0 - - lattice = openmc.RectLattice() - lattice.lower_left = (-d, -d) - lattice.pitch = (d, d) - lattice.universes = [[pin, pin], - [pin, pin]] - box = openmc.model.RectangularPrism( - 2.0 * d, 2.0 * d, - origin=(0.0, 0.0), - boundary_type='reflective' - ) - - model.geometry = openmc.Geometry([openmc.Cell(fill=lattice, region=-box)]) - model.geometry.merge_surfaces = True - - msh = openmc.RegularMesh(mesh_id=0) - msh.lower_left = (-d, -d) - msh.upper_right = (d, d) - msh.dimension = (2, 2) - t = openmc.Tally() - t.filters = [openmc.ParticleFilter(bins='neutron'), openmc.MeshFilter(mesh=msh)] - t.scores = ['total'] - t.estimator = 'collision' - model.tallies.append(t) - - model.settings.batches = 10 - model.settings.inactive = 5 - model.settings.particles = 1000 - model.settings.delta_tracking = True - model.settings.event_based = True - - return model - - @pytest.mark.parametrize("photon", [False, True]) -def test_lattice(model, photon): +def test_lattice(photon): with change_directory(str(photon)): - model.settings.photon_transport = photon - if photon: - msh = openmc.RegularMesh(mesh_id=1) - msh.lower_left = (-1.0, -1.0) - msh.upper_right = (1.0, 1.0) - msh.dimension = (2, 2) - t = openmc.Tally() - t.filters = [openmc.ParticleFilter(bins='photon'), openmc.MeshFilter(mesh=msh)] - t.scores = ['total'] - t.estimator = 'collision' - model.tallies.append(t) + model = delta_tracking_lattice(photon) + model.settings.event_based = True harness = PyAPITestHarness('statepoint.10.h5', model) harness.main() diff --git a/tests/regression_tests/delta_tracking_history/False/inputs_true.dat b/tests/regression_tests/delta_tracking_history/False/inputs_true.dat index 2177b2a8d5e..f0f5477d2eb 100644 --- a/tests/regression_tests/delta_tracking_history/False/inputs_true.dat +++ b/tests/regression_tests/delta_tracking_history/False/inputs_true.dat @@ -18,18 +18,18 @@ - 1.0 1.0 + 1.26 1.26 2 2 - -1.0 -1.0 + -1.26 -1.26 1 1 1 1 - - - - + + + + eigenvalue @@ -40,16 +40,16 @@ true - + 2 2 - -1.0 -1.0 - 1.0 1.0 + -1.26 -1.26 + 1.26 1.26 neutron - 0 + 1 1 2 diff --git a/tests/regression_tests/delta_tracking_history/False/results_true.dat b/tests/regression_tests/delta_tracking_history/False/results_true.dat index a388688876f..bcf614974d6 100644 --- a/tests/regression_tests/delta_tracking_history/False/results_true.dat +++ b/tests/regression_tests/delta_tracking_history/False/results_true.dat @@ -1,11 +1,11 @@ k-combined: -1.869636E+00 4.475641E-03 +1.850729E+00 2.180813E-03 tally 1: -1.558300E+01 -4.857891E+01 -1.578200E+01 -4.982111E+01 -1.489300E+01 -4.438229E+01 -1.544300E+01 -4.771581E+01 +1.828800E+01 +6.694305E+01 +1.823900E+01 +6.658124E+01 +1.894000E+01 +7.184607E+01 +1.852200E+01 +6.869212E+01 diff --git a/tests/regression_tests/delta_tracking_history/True/inputs_true.dat b/tests/regression_tests/delta_tracking_history/True/inputs_true.dat index 7e2b5896073..49dcfb3d432 100644 --- a/tests/regression_tests/delta_tracking_history/True/inputs_true.dat +++ b/tests/regression_tests/delta_tracking_history/True/inputs_true.dat @@ -18,18 +18,18 @@ - 1.0 1.0 + 1.26 1.26 2 2 - -1.0 -1.0 + -1.26 -1.26 1 1 1 1 - - - - + + + + eigenvalue @@ -40,35 +40,27 @@ true - - 2 2 - -1.0 -1.0 - 1.0 1.0 - 2 2 - -1.0 -1.0 - 1.0 1.0 + -1.26 -1.26 + 1.26 1.26 neutron - 0 + 1 photon - - 1 - 1 2 total collision - 3 4 + 3 2 total collision diff --git a/tests/regression_tests/delta_tracking_history/True/results_true.dat b/tests/regression_tests/delta_tracking_history/True/results_true.dat index 5d33cb06d5d..27e7afcefb7 100644 --- a/tests/regression_tests/delta_tracking_history/True/results_true.dat +++ b/tests/regression_tests/delta_tracking_history/True/results_true.dat @@ -1,20 +1,20 @@ k-combined: -1.863579E+00 4.987651E-03 +1.841093E+00 4.770249E-03 tally 1: -1.532500E+01 -4.698978E+01 -1.517100E+01 -4.606297E+01 -1.531800E+01 -4.694604E+01 -1.547900E+01 -4.801024E+01 +1.878600E+01 +7.065042E+01 +1.829400E+01 +6.696300E+01 +1.883000E+01 +7.095247E+01 +1.854300E+01 +6.883357E+01 tally 2: -9.001423E+01 -1.621357E+03 -9.138405E+01 -1.671319E+03 -9.036599E+01 -1.634452E+03 -9.160015E+01 -1.681369E+03 +8.714295E+01 +1.519366E+03 +8.757274E+01 +1.533958E+03 +8.827597E+01 +1.558930E+03 +8.794806E+01 +1.547220E+03 diff --git a/tests/regression_tests/delta_tracking_history/test.py b/tests/regression_tests/delta_tracking_history/test.py index 53b58bdcc50..f4d6768ee3d 100644 --- a/tests/regression_tests/delta_tracking_history/test.py +++ b/tests/regression_tests/delta_tracking_history/test.py @@ -1,75 +1,14 @@ import openmc import pytest from openmc.utility_funcs import change_directory +from openmc.examples import delta_tracking_lattice from tests.testing_harness import PyAPITestHarness -@pytest.fixture -def model(): - openmc.reset_auto_ids() - model = openmc.Model() - - uo2 = openmc.Material(name='UO2') - uo2.set_density('g/cm3', 10.0) - uo2.add_nuclide('U235', 1.0) - uo2.add_nuclide('O16', 2.0) - water = openmc.Material(name='light water') - water.add_nuclide('H1', 2.0) - water.add_nuclide('O16', 1.0) - water.set_density('g/cm3', 1.0) - water.add_s_alpha_beta('c_H_in_H2O') - model.materials.extend([uo2, water]) - - cyl = openmc.ZCylinder(r=0.4) - pin = openmc.model.pin([cyl], [uo2, water]) - d = 1.0 - - lattice = openmc.RectLattice() - lattice.lower_left = (-d, -d) - lattice.pitch = (d, d) - lattice.universes = [[pin, pin], - [pin, pin]] - box = openmc.model.RectangularPrism( - 2.0 * d, 2.0 * d, - origin=(0.0, 0.0), - boundary_type='reflective' - ) - - model.geometry = openmc.Geometry([openmc.Cell(fill=lattice, region=-box)]) - model.geometry.merge_surfaces = True - - msh = openmc.RegularMesh(mesh_id=0) - msh.lower_left = (-d, -d) - msh.upper_right = (d, d) - msh.dimension = (2, 2) - t = openmc.Tally() - t.filters = [openmc.ParticleFilter(bins='neutron'), openmc.MeshFilter(mesh=msh)] - t.scores = ['total'] - t.estimator = 'collision' - model.tallies.append(t) - - model.settings.batches = 10 - model.settings.inactive = 5 - model.settings.particles = 1000 - model.settings.delta_tracking = True - - return model - - @pytest.mark.parametrize("photon", [False, True]) -def test_lattice(model, photon): +def test_lattice(photon): with change_directory(str(photon)): - model.settings.photon_transport = photon - if photon: - msh = openmc.RegularMesh(mesh_id=1) - msh.lower_left = (-1.0, -1.0) - msh.upper_right = (1.0, 1.0) - msh.dimension = (2, 2) - t = openmc.Tally() - t.filters = [openmc.ParticleFilter(bins='photon'), openmc.MeshFilter(mesh=msh)] - t.scores = ['total'] - t.estimator = 'collision' - model.tallies.append(t) + model = delta_tracking_lattice(photon) harness = PyAPITestHarness('statepoint.10.h5', model) harness.main() From 5bac6ad7b3692d6dfaae3430fc37a0b2a427141c Mon Sep 17 00:00:00 2001 From: nuclearkevin <66632997+nuclearkevin@users.noreply.github.com> Date: Fri, 12 Jun 2026 11:53:12 -0500 Subject: [PATCH 61/73] Fix gold inputs. --- .../periodic/inputs_true.dat | 38 +++++++++---------- .../reflective/inputs_true.dat | 38 +++++++++---------- .../delta_tracking_bcs/white/inputs_true.dat | 38 +++++++++---------- 3 files changed, 57 insertions(+), 57 deletions(-) diff --git a/tests/regression_tests/delta_tracking_bcs/periodic/inputs_true.dat b/tests/regression_tests/delta_tracking_bcs/periodic/inputs_true.dat index 29f406a342c..5aef1d9d108 100644 --- a/tests/regression_tests/delta_tracking_bcs/periodic/inputs_true.dat +++ b/tests/regression_tests/delta_tracking_bcs/periodic/inputs_true.dat @@ -1,12 +1,12 @@ - + - + @@ -14,22 +14,22 @@ - - - - + + + + 1.26 1.26 2 2 -1.26 -1.26 -7 7 -7 7 +1 1 +1 1 - - - - - + + + + + eigenvalue @@ -40,19 +40,19 @@ true - + 2 2 -1.26 -1.26 1.26 1.26 - + neutron - - 3 + + 1 - - 5 6 + + 1 2 total collision diff --git a/tests/regression_tests/delta_tracking_bcs/reflective/inputs_true.dat b/tests/regression_tests/delta_tracking_bcs/reflective/inputs_true.dat index cdcfcc3e70d..f0f5477d2eb 100644 --- a/tests/regression_tests/delta_tracking_bcs/reflective/inputs_true.dat +++ b/tests/regression_tests/delta_tracking_bcs/reflective/inputs_true.dat @@ -1,12 +1,12 @@ - + - + @@ -14,22 +14,22 @@ - - - - + + + + 1.26 1.26 2 2 -1.26 -1.26 -4 4 -4 4 +1 1 +1 1 - - - - - + + + + + eigenvalue @@ -40,19 +40,19 @@ true - + 2 2 -1.26 -1.26 1.26 1.26 - + neutron - - 2 + + 1 - - 3 4 + + 1 2 total collision diff --git a/tests/regression_tests/delta_tracking_bcs/white/inputs_true.dat b/tests/regression_tests/delta_tracking_bcs/white/inputs_true.dat index 6643f01d596..4cf01d3a47e 100644 --- a/tests/regression_tests/delta_tracking_bcs/white/inputs_true.dat +++ b/tests/regression_tests/delta_tracking_bcs/white/inputs_true.dat @@ -1,12 +1,12 @@ - + - + @@ -14,22 +14,22 @@ - - - - + + + + 1.26 1.26 2 2 -1.26 -1.26 -10 10 -10 10 +1 1 +1 1 - - - - - + + + + + eigenvalue @@ -40,19 +40,19 @@ true - + 2 2 -1.26 -1.26 1.26 1.26 - + neutron - - 4 + + 1 - - 7 8 + + 1 2 total collision From c743933dcf7c53dce478a0fae51efaa3c4f2c173 Mon Sep 17 00:00:00 2001 From: nuclearkevin <66632997+nuclearkevin@users.noreply.github.com> Date: Fri, 12 Jun 2026 14:59:44 -0500 Subject: [PATCH 62/73] Final review. --- docs/source/methods/neutron_physics.rst | 2 +- include/openmc/majorant.h | 6 +++--- include/openmc/particle_data.h | 6 ++++-- src/geometry_aux.cpp | 4 ++++ src/majorant.cpp | 4 ++-- 5 files changed, 14 insertions(+), 8 deletions(-) diff --git a/docs/source/methods/neutron_physics.rst b/docs/source/methods/neutron_physics.rst index dca91b3a269..c1af34bada3 100644 --- a/docs/source/methods/neutron_physics.rst +++ b/docs/source/methods/neutron_physics.rst @@ -65,7 +65,7 @@ collision is sampled with Equation :eq:`sample-distance-2`. Then, the distance to the nearest surface from the particle position along its current trajectory is computed (discussed in the :ref:`methods_geometry` section). If the distance to the nearest surface is smaller than the distance to the next collision, the -sampled distance is not statistically valid and the particle is moved to the +sampled distance is not statistically valid. The particle is moved to the surface and is considered to be contained by the next geometric region. Cross sections are recomputed, and a new distance to the next collision is sampled with Equation :eq:`sample-distance-2`. This process repeats until the distance diff --git a/include/openmc/majorant.h b/include/openmc/majorant.h index a2b958c37c4..4621f2a3275 100644 --- a/include/openmc/majorant.h +++ b/include/openmc/majorant.h @@ -52,7 +52,7 @@ class Majorant { //! \param[out] mat_maj The array to write the macroscopic majorant to. //! The resulting cross section has units of [cm^-1] virtual void fill_material_maj_xs(int i_material, double max_density_mult, - const std::vector & to_grid, std::vector & mat_maj) = 0; + const std::vector & to_grid, std::vector & mat_maj) const = 0; //! Post-processes the energy grid by calling std::sort(), std::unique(). //! This also removes all energy values below the transport minimum and @@ -148,7 +148,7 @@ class NeutronMajorant : public Majorant { //! \param[out] mat_maj The array to write the macroscopic majorant to. //! The resulting cross section has units of [cm^-1]. virtual void fill_material_maj_xs(int i_material, double max_density_mult, - const std::vector & to_grid, std::vector & mat_maj) override; + const std::vector & to_grid, std::vector & mat_maj) const override; private: //---------------------------------------------------------------------------- @@ -232,7 +232,7 @@ class PhotonMajorant : public Majorant { //! \param[out] mat_maj The array to write the macroscopic majorant to. The //! resulting cross section has units of [cm^-1] virtual void fill_material_maj_xs(int i_material, double max_density_mult, - const std::vector & to_grid, std::vector & mat_maj) override; + const std::vector & to_grid, std::vector & mat_maj) const override; private: //---------------------------------------------------------------------------- diff --git a/include/openmc/particle_data.h b/include/openmc/particle_data.h index 2b604255740..3b663e35278 100644 --- a/include/openmc/particle_data.h +++ b/include/openmc/particle_data.h @@ -573,8 +573,10 @@ class ParticleData : public GeometryState { int64_t n_progeny_ {0}; - bool delta_tracking_ {false}; // !< Flag to indicate whether or not delta tracking is active - double majorant_ {0.0}; // !< most recent value for the majorant cross section + //! Flag to indicate whether or not delta tracking is active. + bool delta_tracking_ {false}; + //! Most recent value of the majorant cross section. + double majorant_ {0.0}; public: //---------------------------------------------------------------------------- diff --git a/src/geometry_aux.cpp b/src/geometry_aux.cpp index 0ef0aeb4acf..798a9d3c0e7 100644 --- a/src/geometry_aux.cpp +++ b/src/geometry_aux.cpp @@ -268,6 +268,8 @@ void get_temperatures( } } +//============================================================================== + void detect_boundary_surfaces() { for (int i = 0; i < model::surfaces.size(); i++) { // if the surface has a non-transmission boundary condition, @@ -291,6 +293,8 @@ void finalize_geometry() // Assign temperatures to cells that don't have temperatures already assigned assign_temperatures(); + // Find all boundary surfaces. Used in delta tracking to trace through the + // geometry. detect_boundary_surfaces(); // Determine number of nested coordinate levels in the geometry diff --git a/src/majorant.cpp b/src/majorant.cpp index 0ee9e191bb0..e69636ebe2b 100644 --- a/src/majorant.cpp +++ b/src/majorant.cpp @@ -203,7 +203,7 @@ void NeutronMajorant::compute_unionized_grid() } void NeutronMajorant::fill_material_maj_xs(int i_material, double max_density_mult, - const std::vector & to_grid, std::vector & mat_maj) + const std::vector & to_grid, std::vector & mat_maj) const { const auto & mat = *model::materials[i_material]; @@ -465,7 +465,7 @@ double PhotonMajorant::calculate_photon_xs(double energy) const } void PhotonMajorant::fill_material_maj_xs(int i_material, double max_density_mult, - const std::vector & to_grid, std::vector & mat_maj) + const std::vector & to_grid, std::vector & mat_maj) const { const auto & mat = *model::materials[i_material]; From f4ce014d50efbdb48ca612ef3f2f00cf88fe1b1b Mon Sep 17 00:00:00 2001 From: nuclearkevin <66632997+nuclearkevin@users.noreply.github.com> Date: Fri, 12 Jun 2026 15:05:17 -0500 Subject: [PATCH 63/73] Style changes. --- include/openmc/event.h | 6 +- include/openmc/majorant.h | 34 +++-- src/boundary_condition.cpp | 3 +- src/eigenvalue.cpp | 13 +- src/event.cpp | 25 ++-- src/geometry_aux.cpp | 3 +- src/majorant.cpp | 279 +++++++++++++++++++++---------------- src/material.cpp | 3 +- src/nuclide.cpp | 8 +- src/output.cpp | 9 +- src/particle.cpp | 29 ++-- src/settings.cpp | 6 +- src/simulation.cpp | 19 +-- 13 files changed, 255 insertions(+), 182 deletions(-) diff --git a/include/openmc/event.h b/include/openmc/event.h index efb366d97a2..7b043d6831c 100644 --- a/include/openmc/event.h +++ b/include/openmc/event.h @@ -144,13 +144,15 @@ void process_delta_init_events(int64_t n_particles, int64_t source_offset); //! \param queue A reference to the desired XS lookup queue void process_delta_calculate_xs_events(SharedArray& queue); -//! Execute the delta advance particle event for all particles in this advance buffer +//! Execute the delta advance particle event for all particles in this advance +//! buffer void process_delta_advance_particle_events(); //! Specialization of process_surface_crossing_events() for delta tracking. void process_delta_surface_crossing_events(); -//! Execute the delta tracking collision event for all particles in this event's buffer +//! Execute the delta tracking collision event for all particles in this event's +//! buffer void process_delta_collision_events(); //! Specialization of process_transport_events() for delta tracking. diff --git a/include/openmc/majorant.h b/include/openmc/majorant.h index 4621f2a3275..35d782aee22 100644 --- a/include/openmc/majorant.h +++ b/include/openmc/majorant.h @@ -18,8 +18,8 @@ class PhotonMajorant; //============================================================================== namespace data { - extern std::unique_ptr n_majorant; - extern std::unique_ptr p_majorant; +extern std::unique_ptr n_majorant; +extern std::unique_ptr p_majorant; } // namespace data //============================================================================== @@ -52,7 +52,7 @@ class Majorant { //! \param[out] mat_maj The array to write the macroscopic majorant to. //! The resulting cross section has units of [cm^-1] virtual void fill_material_maj_xs(int i_material, double max_density_mult, - const std::vector & to_grid, std::vector & mat_maj) const = 0; + const std::vector& to_grid, std::vector& mat_maj) const = 0; //! Post-processes the energy grid by calling std::sort(), std::unique(). //! This also removes all energy values below the transport minimum and @@ -62,7 +62,7 @@ class Majorant { //! fetching transport minimum / maximum energies //! \param[out] grid The energy grid to post-process. This is performed //! in-place. - void post_process_grid(int particle_type, Nuclide::EnergyGrid & grid) const; + void post_process_grid(int particle_type, Nuclide::EnergyGrid& grid) const; //! Helper function to perform linear interpolation. //! @@ -72,7 +72,8 @@ class Majorant { //! \param[in] y_1 The y coordinate associated with x_1. //! \param[in] x The point between x_0 and x_1 to find a y value at. //! \return The linear interpolant of the given points. - inline double interpolate_lin_1D(double x_0, double x_1, double y_0, double y_1, double x) const + inline double interpolate_lin_1D( + double x_0, double x_1, double y_0, double y_1, double x) const { const double f = (x - x_0) / (x_1 - x_0); return (1.0 - f) * y_0 + f * y_1; @@ -86,7 +87,8 @@ class Majorant { //! \param[in] y_1 The y coordinate associated with x_1. //! \param[in] x The point between x_0 and x_1 to find a y value at. //! \return The log interpolant of the given points. - inline double interpolate_log_1D(double x_0, double x_1, double y_0, double y_1, double x) const + inline double interpolate_log_1D( + double x_0, double x_1, double y_0, double y_1, double x) const { const double f = std::log(x / x_0) / std::log(x_1 / x_0); return std::exp((1.0 - f) * std::log(y_0) + f * std::log(y_1)); @@ -148,7 +150,8 @@ class NeutronMajorant : public Majorant { //! \param[out] mat_maj The array to write the macroscopic majorant to. //! The resulting cross section has units of [cm^-1]. virtual void fill_material_maj_xs(int i_material, double max_density_mult, - const std::vector & to_grid, std::vector & mat_maj) const override; + const std::vector& to_grid, + std::vector& mat_maj) const override; private: //---------------------------------------------------------------------------- @@ -168,7 +171,8 @@ class NeutronMajorant : public Majorant { //! \param[in] smooth_xs The smooth total cross section in units of [eV] to //! use (if needed by the ptable). //! \return The maximum URR total cross section in [barn]. - double calculate_max_urr_xs(int i_nuclide, double energy, double smooth_xs) const; + double calculate_max_urr_xs( + int i_nuclide, double energy, double smooth_xs) const; //! Compute the maximum microscopic S(a,b) total cross section. //! @@ -180,14 +184,15 @@ class NeutronMajorant : public Majorant { //! \param[in] nuc The nuclide to compute the microscopic total cross section //! of. //! \return The maximum S(a,b) total cross section in [barn]. - double calculate_max_sab_tot_xs(int i_nuclide, int i_sab, double sab_frac, double energy) const; + double calculate_max_sab_tot_xs( + int i_nuclide, int i_sab, double sab_frac, double energy) const; //! Get the grid index for energy interpolation. //! //! \param[in] energy The energy to evaluate the cross section at in [eV]. //! \param[in] grid The energy grid to search for an energy grid index. //! \return The grid index. - int get_i_grid(double energy, const Nuclide::EnergyGrid & grid) const; + int get_i_grid(double energy, const Nuclide::EnergyGrid& grid) const; //---------------------------------------------------------------------------- // Private data members @@ -232,7 +237,8 @@ class PhotonMajorant : public Majorant { //! \param[out] mat_maj The array to write the macroscopic majorant to. The //! resulting cross section has units of [cm^-1] virtual void fill_material_maj_xs(int i_material, double max_density_mult, - const std::vector & to_grid, std::vector & mat_maj) const override; + const std::vector& to_grid, + std::vector& mat_maj) const override; private: //---------------------------------------------------------------------------- @@ -250,8 +256,10 @@ class PhotonMajorant : public Majorant { //! \param[in] energy The energy to evaluate the cross section at in [eV]. //! \param[in] grid The energy grid to search for an energy grid index. //! \return The grid index. - int get_i_grid(double log_energy, const std::vector & energy_grid) const; - int get_i_grid(double log_energy, const tensor::Tensor & energy_grid) const; + int get_i_grid( + double log_energy, const std::vector& energy_grid) const; + int get_i_grid( + double log_energy, const tensor::Tensor& energy_grid) const; //---------------------------------------------------------------------------- // Private data members diff --git a/src/boundary_condition.cpp b/src/boundary_condition.cpp index ed8b1ce469b..e2d135a1c49 100644 --- a/src/boundary_condition.cpp +++ b/src/boundary_condition.cpp @@ -133,7 +133,8 @@ void TranslationalPeriodicBC::handle_particle( int new_surface = p.surface() > 0 ? j_surf_ + 1 : -(j_surf_ + 1); // Need to nudge the particle back into the domain when using delta tracking - // as event_delta_advance() does not go right up to the surface to fix tunneling. + // as event_delta_advance() does not go right up to the surface to fix + // tunneling. if (p.delta_tracking()) { new_r += FP_REL_PRECISION * p.u(); } diff --git a/src/eigenvalue.cpp b/src/eigenvalue.cpp index 50eb28b9e1a..7e6767f51db 100644 --- a/src/eigenvalue.cpp +++ b/src/eigenvalue.cpp @@ -59,7 +59,6 @@ void calculate_generation_keff() simulation::keff_generation; } - double keff_reduced; #ifdef OPENMC_MPI if (settings::solver_type != SolverType::RANDOM_RAY) { @@ -485,9 +484,9 @@ int openmc_get_keff(double* k_combined) // exceptions and an expression specifically derived for the combination of // two estimators (vice three) should be used instead. - // If delta tracking is enabled, use the collision and absorption estimators only. - // Otherwise, we will identify if there are any matching estimators. If none match, - // all three estimates are used. + // If delta tracking is enabled, use the collision and absorption estimators + // only. Otherwise, we will identify if there are any matching estimators. If + // none match, all three estimates are used. int i, j; bool use_three = false; if (settings::delta_tracking) { @@ -501,13 +500,15 @@ int openmc_get_keff(double* k_combined) j = 2; } else if ((std::abs(kv[0] - kv[2]) / kv[0] < FP_REL_PRECISION) && - (std::abs(cov(0, 0) - cov(2, 2)) / cov(0, 0) < FP_REL_PRECISION)) { + (std::abs(cov(0, 0) - cov(2, 2)) / cov(0, 0) < + FP_REL_PRECISION)) { // 0 and 2 match, so only use 0 and 1 in our comparisons i = 0; j = 1; } else if ((std::abs(kv[1] - kv[2]) / kv[1] < FP_REL_PRECISION) && - (std::abs(cov(1, 1) - cov(2, 2)) / cov(1, 1) < FP_REL_PRECISION)) { + (std::abs(cov(1, 1) - cov(2, 2)) / cov(1, 1) < + FP_REL_PRECISION)) { // 1 and 2 match, so only use 0 and 1 in our comparisons i = 0; j = 1; diff --git a/src/event.cpp b/src/event.cpp index f904bc64107..65aebd95a31 100644 --- a/src/event.cpp +++ b/src/event.cpp @@ -282,7 +282,7 @@ void process_delta_advance_particle_events() { simulation::time_event_advance_particle.start(); - #pragma omp parallel for schedule(runtime) +#pragma omp parallel for schedule(runtime) for (int64_t i = 0; i < simulation::advance_particle_queue.size(); i++) { int64_t buffer_idx = simulation::advance_particle_queue[i].idx; Particle& p = simulation::particles[buffer_idx]; @@ -290,9 +290,10 @@ void process_delta_advance_particle_events() if (!p.alive()) continue; - if (p.type() == ParticleType::electron() || p.type() == ParticleType::positron()) { - // Electrons / positrons collide in place and don't require cross section calculations. - // Can append to the collision queue directly. + if (p.type() == ParticleType::electron() || + p.type() == ParticleType::positron()) { + // Electrons / positrons collide in place and don't require cross section + // calculations. Can append to the collision queue directly. simulation::collision_queue.thread_safe_append({p, buffer_idx}); continue; } @@ -338,15 +339,16 @@ void process_delta_collision_events() int64_t buffer_idx = simulation::collision_queue[i].idx; Particle& p = simulation::particles[buffer_idx]; - if (p.type() == ParticleType::electron() || p.type() == ParticleType::positron()) { + if (p.type() == ParticleType::electron() || + p.type() == ParticleType::positron()) { p.event_collide(); } else { if (p.macro_xs().total / p.majorant() > 1.0) { - p.mark_as_lost( - fmt::format("Ratio of the total cross section ({}) to the majorant " - "cross section ({}) for particle {} ({}) with energy {} is " - "greater than unity!", - p.macro_xs().total, p.majorant(), p.id(), p.type().str(), p.E())); + p.mark_as_lost(fmt::format( + "Ratio of the total cross section ({}) to the majorant " + "cross section ({}) for particle {} ({}) with energy {} is " + "greater than unity!", + p.macro_xs().total, p.majorant(), p.id(), p.type().str(), p.E())); continue; } @@ -406,7 +408,8 @@ void process_delta_init_secondary_events(int64_t n_particles, int64_t offset, if (simulation::particles[i].alive()) { simulation::particles[i].event_calculate_xs(); - simulation::advance_particle_queue.thread_safe_append({simulation::particles[i], i}); + simulation::advance_particle_queue.thread_safe_append( + {simulation::particles[i], i}); } } simulation::time_event_init.stop(); diff --git a/src/geometry_aux.cpp b/src/geometry_aux.cpp index 798a9d3c0e7..35cc16fa79b 100644 --- a/src/geometry_aux.cpp +++ b/src/geometry_aux.cpp @@ -270,7 +270,8 @@ void get_temperatures( //============================================================================== -void detect_boundary_surfaces() { +void detect_boundary_surfaces() +{ for (int i = 0; i < model::surfaces.size(); i++) { // if the surface has a non-transmission boundary condition, // add it to the list of surfaces to track during delta tracking diff --git a/src/majorant.cpp b/src/majorant.cpp index e69636ebe2b..da36c4dfa87 100644 --- a/src/majorant.cpp +++ b/src/majorant.cpp @@ -8,8 +8,8 @@ #include "openmc/nuclide.h" #include "openmc/photon.h" #include "openmc/search.h" -#include "openmc/simulation.h" #include "openmc/settings.h" +#include "openmc/simulation.h" #include "openmc/thermal.h" #include "openmc/timer.h" #include "openmc/universe.h" @@ -30,58 +30,62 @@ std::unique_ptr p_majorant; // Majorant implementation //============================================================================== -Majorant::Majorant(int i_universe) - : maj_universe_(i_universe) +Majorant::Majorant(int i_universe) : maj_universe_(i_universe) { // Find all materials contained in the majorant's universe. This also obtains // the maximum density multiplier applied to that material. std::set unique_materials; if (maj_universe_ == C_NONE || maj_universe_ >= model::universes.size()) { - fatal_error( fmt::format("Invalid majorant universe: {}", maj_universe_)); + fatal_error(fmt::format("Invalid majorant universe: {}", maj_universe_)); } - const auto & maj_uni = model::universes[maj_universe_]; + const auto& maj_uni = model::universes[maj_universe_]; for (int i_cell : maj_uni->cells_) { - const auto & uni_cell = model::cells[i_cell]; + const auto& uni_cell = model::cells[i_cell]; // If the cell is filled with a material, it won't have any sub-cells. if (uni_cell->type_ == Fill::MATERIAL) { - // Loop over instances. TODO: confirm if this is unecessary and use 0 instead? + // Loop over instances. TODO: confirm if this is unecessary and use 0 + // instead? for (int instance = 0; instance < uni_cell->n_instances(); ++instance) { int i_material = uni_cell->material(instance); - // Check to see if we've found the contained material yet. If not, add to the set - // of materials discovered and add to the map of density multipliers. + // Check to see if we've found the contained material yet. If not, add + // to the set of materials discovered and add to the map of density + // multipliers. if (unique_materials.count(i_material) == 0) { unique_materials.emplace(i_material); max_density_mult_[i_material] = uni_cell->density_mult(instance); } else { - // We've found this material already. Need to take the maximum density multiplier. - max_density_mult_.at(i_material) = - std::max(max_density_mult_.at(i_material), - uni_cell->density_mult(instance)); + // We've found this material already. Need to take the maximum density + // multiplier. + max_density_mult_.at(i_material) = std::max( + max_density_mult_.at(i_material), uni_cell->density_mult(instance)); } } } else { - // This cell is filled with a universe or lattice. Need to get the list of cells and - // cell instances. + // This cell is filled with a universe or lattice. Need to get the list of + // cells and cell instances. const auto contained_cells = uni_cell->get_contained_cells(); - for (const auto & [i_con_cell, contained_instances] : contained_cells) { - const auto & contained_cell = model::cells[i_con_cell]; + for (const auto& [i_con_cell, contained_instances] : contained_cells) { + const auto& contained_cell = model::cells[i_con_cell]; // Loop over contained cell instances. for (auto instance : contained_instances) { - // Check to see if we've found the contained material instance yet. If not, add - // to the set of materials discovered and add to the map of density multipliers. + // Check to see if we've found the contained material instance yet. If + // not, add to the set of materials discovered and add to the map of + // density multipliers. int i_material = contained_cell->material(instance); if (unique_materials.count(i_material) == 0) { unique_materials.emplace(i_material); - max_density_mult_[i_material] = contained_cell->density_mult(instance); + max_density_mult_[i_material] = + contained_cell->density_mult(instance); } else { - // We've found this material already. Need to take the maximum density multiplier - // for the contained instance. + // We've found this material already. Need to take the maximum + // density multiplier for the contained instance. max_density_mult_.at(i_material) = - std::max(max_density_mult_.at(i_material), contained_cell->density_mult(instance)); + std::max(max_density_mult_.at(i_material), + contained_cell->density_mult(instance)); } } } @@ -105,14 +109,16 @@ void Majorant::compute_majorant() fill_material_maj_xs(i_material, max_density_mult_.at(i_material), grid_.energy, material_maj_xs); - // Compute the full majorant by taking the max over each material cross section. + // Compute the full majorant by taking the max over each material cross + // section. for (int i_energy = 0; i_energy < xs_.size(); ++i_energy) { xs_[i_energy] = std::max(xs_[i_energy], material_maj_xs[i_energy]); } } } -void Majorant::post_process_grid(int particle_type, Nuclide::EnergyGrid & grid) const +void Majorant::post_process_grid( + int particle_type, Nuclide::EnergyGrid& grid) const { // Photons use a logarithmic energy grid. double E_min = data::energy_min[particle_type]; @@ -151,14 +157,13 @@ void Majorant::post_process_grid(int particle_type, Nuclide::EnergyGrid & grid) // NeutronMajorant implementation //============================================================================== -NeutronMajorant::NeutronMajorant(int i_universe) - : Majorant(i_universe) -{ } +NeutronMajorant::NeutronMajorant(int i_universe) : Majorant(i_universe) {} double NeutronMajorant::calculate_neutron_xs(double energy) const { const int i_grid = get_i_grid(energy, grid_); - return interpolate_lin_1D(grid_.energy[i_grid], grid_.energy[i_grid + 1], xs_[i_grid], xs_[i_grid + 1], energy); + return interpolate_lin_1D(grid_.energy[i_grid], grid_.energy[i_grid + 1], + xs_[i_grid], xs_[i_grid + 1], energy); } void NeutronMajorant::compute_unionized_grid() @@ -167,45 +172,48 @@ void NeutronMajorant::compute_unionized_grid() // sections and URR probability table grids. std::set processed_nuclides; for (int i_mat : contained_materials_) { - const auto & mat = model::materials[i_mat]; + const auto& mat = model::materials[i_mat]; for (auto i_nuclide : mat->nuclide_) { // Only unionize nuclides we haven't checked yet. if (processed_nuclides.count(i_nuclide) > 0) { continue; } - const auto & nuclide = data::nuclides[i_nuclide]; + const auto& nuclide = data::nuclides[i_nuclide]; // ====================================================================== // Unionizing the URR energy grids. if (nuclide->urr_present_ && settings::urr_ptables_on) { - for (const auto & nuc_urr : nuclide->urr_data_) { - grid_.energy.insert(grid_.energy.end(), nuc_urr.energy_.begin(), nuc_urr.energy_.end()); + for (const auto& nuc_urr : nuclide->urr_data_) { + grid_.energy.insert( + grid_.energy.end(), nuc_urr.energy_.begin(), nuc_urr.energy_.end()); } } // ====================================================================== // Unionize the smooth cross section energy grids. - for (const auto & nuc_grid : nuclide->grid_) { - grid_.energy.insert(grid_.energy.end(), nuc_grid.energy.begin(), nuc_grid.energy.end()); + for (const auto& nuc_grid : nuclide->grid_) { + grid_.energy.insert( + grid_.energy.end(), nuc_grid.energy.begin(), nuc_grid.energy.end()); } processed_nuclides.insert(i_nuclide); } } - // Post-process the energy grid now that all points from nuclides are included. This sorts - // the energy points, removes duplicates, and removes all energies exceeding neutron - // transport bounds. + // Post-process the energy grid now that all points from nuclides are + // included. This sorts the energy points, removes duplicates, and removes all + // energies exceeding neutron transport bounds. post_process_grid(i_neutron_, grid_); // Initialize the grid for fast lookups. This only applies to neutrons. grid_.init(); } -void NeutronMajorant::fill_material_maj_xs(int i_material, double max_density_mult, - const std::vector & to_grid, std::vector & mat_maj) const +void NeutronMajorant::fill_material_maj_xs(int i_material, + double max_density_mult, const std::vector& to_grid, + std::vector& mat_maj) const { - const auto & mat = *model::materials[i_material]; + const auto& mat = *model::materials[i_material]; mat_maj.resize(to_grid.size()); std::fill(mat_maj.begin(), mat_maj.end(), 0.0); @@ -255,75 +263,86 @@ void NeutronMajorant::fill_material_maj_xs(int i_material, double max_density_mu double micro_smooth_tot_xs = 0.0; if (i_sab >= 0) { // Thermal scattering cross sections using S(a,b) tables. - micro_smooth_tot_xs = calculate_max_sab_tot_xs(mat.nuclide_[i], i_sab, sab_frac, union_energy); + micro_smooth_tot_xs = calculate_max_sab_tot_xs( + mat.nuclide_[i], i_sab, sab_frac, union_energy); } else { // Free gas smooth cross section - micro_smooth_tot_xs = calculate_max_smooth_xs(mat.nuclide_[i], union_energy); + micro_smooth_tot_xs = + calculate_max_smooth_xs(mat.nuclide_[i], union_energy); } // ====================================================================== // Compute the URR cross section. This shouldn't intersect with the // S(a,b) cross section. - double micro_urr_xs = calculate_max_urr_xs(mat.nuclide_[i], union_energy, micro_smooth_tot_xs); + double micro_urr_xs = calculate_max_urr_xs( + mat.nuclide_[i], union_energy, micro_smooth_tot_xs); // ====================================================================== // Accumulate the macroscopic cross section. - mat_maj[i_energy] += std::max(micro_smooth_tot_xs, micro_urr_xs) * mat.atom_density(i, max_density_mult); + mat_maj[i_energy] += std::max(micro_smooth_tot_xs, micro_urr_xs) * + mat.atom_density(i, max_density_mult); } } } -double NeutronMajorant::calculate_max_smooth_xs(int i_nuclide, double energy) const +double NeutronMajorant::calculate_max_smooth_xs( + int i_nuclide, double energy) const { - const auto & nuc = *data::nuclides[i_nuclide]; + const auto& nuc = *data::nuclides[i_nuclide]; double max_smooth_tot_xs = 0.0; for (int i_temp = 0; i_temp < nuc.kTs_.size(); ++i_temp) { - const auto & nuc_grid = nuc.grid_[i_temp]; + const auto& nuc_grid = nuc.grid_[i_temp]; int i_grid = get_i_grid(energy, nuc_grid); auto total = nuc.xs_[i_temp].slice(openmc::tensor::all, 0); - double xs = interpolate_lin_1D(nuc_grid.energy[i_grid], nuc_grid.energy[i_grid + 1], total[i_grid], total[i_grid + 1], energy); + double xs = interpolate_lin_1D(nuc_grid.energy[i_grid], + nuc_grid.energy[i_grid + 1], total[i_grid], total[i_grid + 1], energy); max_smooth_tot_xs = std::max(max_smooth_tot_xs, xs); } return max_smooth_tot_xs; } -double NeutronMajorant::calculate_max_urr_xs(int i_nuclide, double energy, double smooth_xs) const +double NeutronMajorant::calculate_max_urr_xs( + int i_nuclide, double energy, double smooth_xs) const { - // A tolerance on the URR check to make sure we include the URR energy grid bounds. + // A tolerance on the URR check to make sure we include the URR energy grid + // bounds. constexpr double URR_FUZZY_CHECK = 1e-6; - const auto & nuc = *data::nuclides[i_nuclide]; + const auto& nuc = *data::nuclides[i_nuclide]; if (!nuc.urr_present_) { return 0.0; } double max_urr_xs = 0.0; - for (const auto & urr : nuc.urr_data_) { - if (!(urr.energy_in_bounds(energy - URR_FUZZY_CHECK) - || urr.energy_in_bounds(energy + URR_FUZZY_CHECK))) { + for (const auto& urr : nuc.urr_data_) { + if (!(urr.energy_in_bounds(energy - URR_FUZZY_CHECK) || + urr.energy_in_bounds(energy + URR_FUZZY_CHECK))) { continue; } - int i_energy = lower_bound_index(&urr.energy_.front(), &urr.energy_.back(), energy); + int i_energy = + lower_bound_index(&urr.energy_.front(), &urr.energy_.back(), energy); // Find the maximum URR cross sections for the two bounding energy points. double max_urr_xs_E0 = 0.0; double max_urr_xs_E1 = 0.0; for (int i_cdf = 0; i_cdf < urr.n_cdf(); ++i_cdf) { - max_urr_xs_E0 = std::max(max_urr_xs_E0, urr.xs_values_(i_energy, i_cdf).total); - max_urr_xs_E1 = std::max(max_urr_xs_E1, urr.xs_values_(i_energy + 1, i_cdf).total); + max_urr_xs_E0 = + std::max(max_urr_xs_E0, urr.xs_values_(i_energy, i_cdf).total); + max_urr_xs_E1 = + std::max(max_urr_xs_E1, urr.xs_values_(i_energy + 1, i_cdf).total); } // Interpolate the bounding energy points. double interp_urr_xs = 0.0; if (urr.interp_ == Interpolation::lin_lin) { - interp_urr_xs = - interpolate_lin_1D(urr.energy_[i_energy], urr.energy_[i_energy + 1], max_urr_xs_E0, max_urr_xs_E1, energy); + interp_urr_xs = interpolate_lin_1D(urr.energy_[i_energy], + urr.energy_[i_energy + 1], max_urr_xs_E0, max_urr_xs_E1, energy); } else if (urr.interp_ == Interpolation::log_log) { - interp_urr_xs = - interpolate_log_1D(urr.energy_[i_energy], urr.energy_[i_energy + 1], max_urr_xs_E0, max_urr_xs_E1, energy); + interp_urr_xs = interpolate_log_1D(urr.energy_[i_energy], + urr.energy_[i_energy + 1], max_urr_xs_E0, max_urr_xs_E1, energy); } // Multiply by the smooth cross section (after interpolation) if required. @@ -337,10 +356,11 @@ double NeutronMajorant::calculate_max_urr_xs(int i_nuclide, double energy, doubl return max_urr_xs; } -double NeutronMajorant::calculate_max_sab_tot_xs(int i_nuclide, int i_sab, double sab_frac, double energy) const +double NeutronMajorant::calculate_max_sab_tot_xs( + int i_nuclide, int i_sab, double sab_frac, double energy) const { - const auto & nuc = *data::nuclides[i_nuclide]; - const auto & thermal = *data::thermal_scatt[i_sab]; + const auto& nuc = *data::nuclides[i_nuclide]; + const auto& thermal = *data::thermal_scatt[i_sab]; // Loop over the nuclide's temperature grid to ensure we're consistent. double max_sab_total = 0.0; @@ -351,36 +371,50 @@ double NeutronMajorant::calculate_max_sab_tot_xs(int i_nuclide, int i_sab, doubl // cross sections are interpolated to match the nuclide temperature point. double thermal_elastic; double thermal_inelastic; - const auto & tkTs = thermal.kTs_; + const auto& tkTs = thermal.kTs_; if (tkTs.size() > 1) { if (nuc_kT < tkTs.front()) { - thermal.data_.front().calculate_xs(energy, &thermal_elastic, &thermal_inelastic); + thermal.data_.front().calculate_xs( + energy, &thermal_elastic, &thermal_inelastic); } else if (nuc_kT > tkTs.back()) { - thermal.data_.back().calculate_xs(energy, &thermal_elastic, &thermal_inelastic); + thermal.data_.back().calculate_xs( + energy, &thermal_elastic, &thermal_inelastic); } else { // Find temperatures that bound the actual temperature int i_sab_temp = 0; - while (tkTs[i_sab_temp + 1] < nuc_kT && i_sab_temp + 1 < tkTs.size() - 1) { + while ( + tkTs[i_sab_temp + 1] < nuc_kT && i_sab_temp + 1 < tkTs.size() - 1) { ++i_sab_temp; } - // Interpolate the scattering cross sections to the nuclide temperature grid point. + // Interpolate the scattering cross sections to the nuclide temperature + // grid point. double T0_elastic, T1_elastic, T0_inelastic, T1_inelastic; - thermal.data_[i_sab_temp].calculate_xs(energy, &T0_elastic, &T0_inelastic); - thermal.data_[i_sab_temp + 1].calculate_xs(energy, &T1_elastic, &T1_inelastic); - thermal_elastic = interpolate_lin_1D(tkTs[i_sab_temp], tkTs[i_sab_temp + 1], T0_elastic, T1_elastic, nuc_kT); - thermal_inelastic = interpolate_lin_1D(tkTs[i_sab_temp], tkTs[i_sab_temp + 1], T0_inelastic, T1_inelastic, nuc_kT); + thermal.data_[i_sab_temp].calculate_xs( + energy, &T0_elastic, &T0_inelastic); + thermal.data_[i_sab_temp + 1].calculate_xs( + energy, &T1_elastic, &T1_inelastic); + thermal_elastic = interpolate_lin_1D(tkTs[i_sab_temp], + tkTs[i_sab_temp + 1], T0_elastic, T1_elastic, nuc_kT); + thermal_inelastic = interpolate_lin_1D(tkTs[i_sab_temp], + tkTs[i_sab_temp + 1], T0_inelastic, T1_inelastic, nuc_kT); } } else { - thermal.data_[0].calculate_xs(energy, &thermal_elastic, &thermal_inelastic); + thermal.data_[0].calculate_xs( + energy, &thermal_elastic, &thermal_inelastic); } - // Compute the free gas total and elastic cross sections interpolated on the majorant grid. - const auto & nuc_grid = nuc.grid_[i_nuc_temp]; + // Compute the free gas total and elastic cross sections interpolated on the + // majorant grid. + const auto& nuc_grid = nuc.grid_[i_nuc_temp]; int i_grid = get_i_grid(energy, nuc_grid); - const auto & free_tot = nuc.xs_[i_nuc_temp].slice(openmc::tensor::all, 0); - const auto & free_ela = nuc.reactions_[0]->xs_[i_nuc_temp].value; - double tot_xs = interpolate_lin_1D(nuc_grid.energy[i_grid], nuc_grid.energy[i_grid + 1], free_tot[i_grid], free_tot[i_grid + 1], energy); - double ela_xs = interpolate_lin_1D(nuc_grid.energy[i_grid], nuc_grid.energy[i_grid + 1], free_ela[i_grid], free_ela[i_grid + 1], energy); + const auto& free_tot = nuc.xs_[i_nuc_temp].slice(openmc::tensor::all, 0); + const auto& free_ela = nuc.reactions_[0]->xs_[i_nuc_temp].value; + double tot_xs = + interpolate_lin_1D(nuc_grid.energy[i_grid], nuc_grid.energy[i_grid + 1], + free_tot[i_grid], free_tot[i_grid + 1], energy); + double ela_xs = + interpolate_lin_1D(nuc_grid.energy[i_grid], nuc_grid.energy[i_grid + 1], + free_ela[i_grid], free_ela[i_grid + 1], energy); double thermal_xs = sab_frac * (thermal_elastic + thermal_inelastic); double sab_corrected_total = tot_xs + thermal_xs - sab_frac * ela_xs; @@ -390,10 +424,12 @@ double NeutronMajorant::calculate_max_sab_tot_xs(int i_nuclide, int i_sab, doubl return max_sab_total; } -int NeutronMajorant::get_i_grid(double energy, const Nuclide::EnergyGrid & grid) const +int NeutronMajorant::get_i_grid( + double energy, const Nuclide::EnergyGrid& grid) const { // Find energy index on energy grid - int i_log_union = std::log(energy / data::energy_min[i_neutron_]) / simulation::log_spacing; + int i_log_union = + std::log(energy / data::energy_min[i_neutron_]) / simulation::log_spacing; int i_grid; if (energy < grid.energy.front()) { @@ -408,7 +444,7 @@ int NeutronMajorant::get_i_grid(double energy, const Nuclide::EnergyGrid & grid) // Perform binary search over reduced range i_grid = i_low + lower_bound_index( - &grid.energy[i_low], &grid.energy[i_high], energy); + &grid.energy[i_low], &grid.energy[i_high], energy); } // check for rare case where two energy points are the same @@ -422,32 +458,31 @@ int NeutronMajorant::get_i_grid(double energy, const Nuclide::EnergyGrid & grid) // PhotonMajorant implementation //============================================================================== -PhotonMajorant::PhotonMajorant(int i_universe) - : Majorant(i_universe) -{ } +PhotonMajorant::PhotonMajorant(int i_universe) : Majorant(i_universe) {} void PhotonMajorant::compute_unionized_grid() { // This function generates a unionized cross section grid for all elements. std::set processed_elements; for (int i_mat : contained_materials_) { - const auto & mat = model::materials[i_mat]; + const auto& mat = model::materials[i_mat]; for (int i = 0; i < mat->nuclide_.size(); ++i) { // Only unionize elements we haven't checked yet. if (processed_elements.count(mat->element_[i]) > 0) { continue; } - const auto & element = data::elements[mat->element_[i]]; - grid_.energy.insert(grid_.energy.end(), element->energy_.begin(), element->energy_.end()); + const auto& element = data::elements[mat->element_[i]]; + grid_.energy.insert( + grid_.energy.end(), element->energy_.begin(), element->energy_.end()); processed_elements.insert(mat->element_[i]); } } - // Post-process the energy grid now that all points from photon interactions are included. - // This sorts the energy points, removes duplicates, and removes all energies exceeding - // photon transport bounds. + // Post-process the energy grid now that all points from photon interactions + // are included. This sorts the energy points, removes duplicates, and removes + // all energies exceeding photon transport bounds. post_process_grid(i_photon_, grid_); } @@ -457,17 +492,18 @@ double PhotonMajorant::calculate_photon_xs(double energy) const int i_grid = get_i_grid(log_energy, grid_.energy); // calculate interpolation factor - double f = - (log_energy - grid_.energy[i_grid]) / (grid_.energy[i_grid + 1] - grid_.energy[i_grid]); + double f = (log_energy - grid_.energy[i_grid]) / + (grid_.energy[i_grid + 1] - grid_.energy[i_grid]); // interpolate the total cross section return std::exp(xs_[i_grid] + f * (xs_[i_grid + 1] - xs_[i_grid])); } -void PhotonMajorant::fill_material_maj_xs(int i_material, double max_density_mult, - const std::vector & to_grid, std::vector & mat_maj) const +void PhotonMajorant::fill_material_maj_xs(int i_material, + double max_density_mult, const std::vector& to_grid, + std::vector& mat_maj) const { - const auto & mat = *model::materials[i_material]; + const auto& mat = *model::materials[i_material]; mat_maj.resize(to_grid.size()); std::fill(mat_maj.begin(), mat_maj.end(), 0.0); @@ -479,28 +515,32 @@ void PhotonMajorant::fill_material_maj_xs(int i_material, double max_density_mul for (int i = 0; i < mat.nuclide_.size(); ++i) { const int i_element = mat.element_[i]; - mat_maj[i_energy] += calculate_elem_tot_xs(i_element, union_log_energy) * mat.atom_density(i, max_density_mult); + mat_maj[i_energy] += calculate_elem_tot_xs(i_element, union_log_energy) * + mat.atom_density(i, max_density_mult); } mat_maj[i_energy] = std::log(mat_maj[i_energy]); } } -double PhotonMajorant::calculate_elem_tot_xs(int i_element, double log_energy) const +double PhotonMajorant::calculate_elem_tot_xs( + int i_element, double log_energy) const { - const auto & elem = *data::elements[i_element]; + const auto& elem = *data::elements[i_element]; int i_grid = get_i_grid(log_energy, elem.energy_); // calculate interpolation factor - double f = - (log_energy - elem.energy_(i_grid)) / (elem.energy_(i_grid + 1) - elem.energy_(i_grid)); + double f = (log_energy - elem.energy_(i_grid)) / + (elem.energy_(i_grid + 1) - elem.energy_(i_grid)); // Calculate microscopic coherent cross section - double coherent = std::exp( - elem.coherent_(i_grid) + f * (elem.coherent_(i_grid + 1) - elem.coherent_(i_grid))); + double coherent = + std::exp(elem.coherent_(i_grid) + + f * (elem.coherent_(i_grid + 1) - elem.coherent_(i_grid))); // Calculate microscopic incoherent cross section - double incoherent = std::exp( - elem.incoherent_(i_grid) + f * (elem.incoherent_(i_grid + 1) - elem.incoherent_(i_grid))); + double incoherent = + std::exp(elem.incoherent_(i_grid) + + f * (elem.incoherent_(i_grid + 1) - elem.incoherent_(i_grid))); // Calculate microscopic photoelectric cross section double photoelectric = 0.0; @@ -509,21 +549,21 @@ double PhotonMajorant::calculate_elem_tot_xs(int i_element, double log_energy) c for (int i = 0; i < xs_upper.size(); ++i) if (xs_lower(i) != 0) - photoelectric += - std::exp(xs_lower(i) + f * (xs_upper(i) - xs_lower(i))); + photoelectric += std::exp(xs_lower(i) + f * (xs_upper(i) - xs_lower(i))); // Calculate microscopic pair production cross section - double pair_production = std::exp( - elem.pair_production_total_(i_grid) + - f * (elem.pair_production_total_(i_grid + 1) - elem.pair_production_total_(i_grid))); + double pair_production = + std::exp(elem.pair_production_total_(i_grid) + + f * (elem.pair_production_total_(i_grid + 1) - + elem.pair_production_total_(i_grid))); // Calculate microscopic total cross section - double total = - coherent + incoherent + photoelectric + pair_production; + double total = coherent + incoherent + photoelectric + pair_production; return total; } -int PhotonMajorant::get_i_grid(double log_energy, const std::vector & energy_grid) const +int PhotonMajorant::get_i_grid( + double log_energy, const std::vector& energy_grid) const { int n_grid = energy_grid.size(); int i_grid; @@ -534,7 +574,8 @@ int PhotonMajorant::get_i_grid(double log_energy, const std::vector & en } else { // We use upper_bound_index here because sometimes photons are created with // energies that exactly match a grid point - i_grid = upper_bound_index(energy_grid.cbegin(), energy_grid.cend(), log_energy); + i_grid = + upper_bound_index(energy_grid.cbegin(), energy_grid.cend(), log_energy); } // check for case where two energy points are the same @@ -544,7 +585,8 @@ int PhotonMajorant::get_i_grid(double log_energy, const std::vector & en return i_grid; } -int PhotonMajorant::get_i_grid(double log_energy, const tensor::Tensor & energy_grid) const +int PhotonMajorant::get_i_grid( + double log_energy, const tensor::Tensor& energy_grid) const { int n_grid = energy_grid.size(); int i_grid; @@ -555,7 +597,8 @@ int PhotonMajorant::get_i_grid(double log_energy, const tensor::Tensor & } else { // We use upper_bound_index here because sometimes photons are created with // energies that exactly match a grid point - i_grid = upper_bound_index(energy_grid.cbegin(), energy_grid.cend(), log_energy); + i_grid = + upper_bound_index(energy_grid.cbegin(), energy_grid.cend(), log_energy); } // check for case where two energy points are the same diff --git a/src/material.cpp b/src/material.cpp index 6ae3f786ce7..ccfdc5cbe21 100644 --- a/src/material.cpp +++ b/src/material.cpp @@ -61,7 +61,8 @@ Material::Material(pugi::xml_node node) if (check_for_node(node, "cfg")) { if (settings::delta_tracking) { - fatal_error("NCrystal materials are presently not supported when running with delta tracking!"); + fatal_error("NCrystal materials are presently not supported when running " + "with delta tracking!"); } auto cfg = get_node_value(node, "cfg"); write_message( diff --git a/src/nuclide.cpp b/src/nuclide.cpp index 3bec328bda9..26635b86db4 100644 --- a/src/nuclide.cpp +++ b/src/nuclide.cpp @@ -512,10 +512,11 @@ void Nuclide::EnergyGrid::init() // equal-logarithmic grid int j = 0; for (int k = 0; k <= M; ++k) { - while (std::log(energy[j + 1]/E_min) <= umesh(k)) { + while (std::log(energy[j + 1] / E_min) <= umesh(k)) { // Ensure that for isotopes where maxval(grid.energy) << E_max that // there are no out-of-bounds issues. - if (j + 2 == energy.size()) break; + if (j + 2 == energy.size()) + break; ++j; } grid_index[k] = j; @@ -524,8 +525,7 @@ void Nuclide::EnergyGrid::init() void Nuclide::init_grid() { - for (auto& grid : grid_) - { + for (auto& grid : grid_) { grid.init(); } } diff --git a/src/output.cpp b/src/output.cpp index 0d1639eb22f..c4b56bec02d 100644 --- a/src/output.cpp +++ b/src/output.cpp @@ -557,11 +557,12 @@ void print_results() fmt::print(" k-effective (Collision) = {:.5f} +/- {:.5f}\n", mean, t_n1 * stdev); if (settings::delta_tracking) { - fmt::print(" k-effective (Track-length) = (Delta-tracking enabled)\n"); + fmt::print(" k-effective (Track-length) = (Delta-tracking enabled)\n"); } else { - std::tie(mean, stdev) = mean_stdev(>(GlobalTally::K_TRACKLENGTH, 0), n); - fmt::print(" k-effective (Track-length) = {:.5f} +/- {:.5f}\n", mean, - t_n1 * stdev); + std::tie(mean, stdev) = + mean_stdev(>(GlobalTally::K_TRACKLENGTH, 0), n); + fmt::print(" k-effective (Track-length) = {:.5f} +/- {:.5f}\n", mean, + t_n1 * stdev); } std::tie(mean, stdev) = mean_stdev(>(GlobalTally::K_ABSORPTION, 0), n); fmt::print(" k-effective (Absorption) = {:.5f} +/- {:.5f}\n", mean, diff --git a/src/particle.cpp b/src/particle.cpp index 7ace3d3fd12..7a68dfe70b9 100644 --- a/src/particle.cpp +++ b/src/particle.cpp @@ -312,7 +312,8 @@ void Particle::event_advance() } // Score track-length estimate of k-eff - if (settings::run_mode == RunMode::EIGENVALUE && type().is_neutron() && !delta_tracking()) { + if (settings::run_mode == RunMode::EIGENVALUE && type().is_neutron() && + !delta_tracking()) { keff_tally_tracklength() += wgt() * distance * macro_xs().nu_fission; } @@ -334,7 +335,8 @@ void Particle::event_delta_advance() } // Sample distance to next position - if (type() == ParticleType::electron() || type() == ParticleType::positron()) { + if (type() == ParticleType::electron() || + type() == ParticleType::positron()) { // Electrons/positrons don't move collision_distance() = 0.0; } else { @@ -345,10 +347,12 @@ void Particle::event_delta_advance() // Update distance to problem boundary boundary() = distance_to_external_boundary(*this); - // Move to the external boundary or delta tracking collision site. Particles with - // large majorant cross sections will tunnel out of the domain if a floating point - // tolerance is not specified on the boundary distance calculation. - double distance = std::min(collision_distance(), boundary().distance() - FP_REL_PRECISION); + // Move to the external boundary or delta tracking collision site. Particles + // with large majorant cross sections will tunnel out of the domain if a + // floating point tolerance is not specified on the boundary distance + // calculation. + double distance = + std::min(collision_distance(), boundary().distance() - FP_REL_PRECISION); r() += distance * u(); // Need to locate the particle at the collision site or boundary. @@ -357,7 +361,8 @@ void Particle::event_delta_advance() } if (!exhaustive_find_cell(*this)) { // We've lost this particle. - mark_as_lost(fmt::format("Particle {} could not be located while running delta tracking!", id())); + mark_as_lost(fmt::format( + "Particle {} could not be located while running delta tracking!", id())); return; } @@ -866,8 +871,8 @@ void Particle::cross_periodic_bc( if (!neighbor_list_find_cell(*this)) { mark_as_lost("Couldn't find particle after hitting periodic " - "boundary on surface " + - std::to_string(surf.id_) + "."); + "boundary on surface " + + std::to_string(surf.id_) + "."); return; } @@ -883,9 +888,11 @@ void Particle::cross_periodic_bc( void Particle::update_majorant() { if (type().is_neutron()) { - majorant() = NeutronMajorant::safety_factor_ * data::n_majorant->calculate_neutron_xs(E()); + majorant() = NeutronMajorant::safety_factor_ * + data::n_majorant->calculate_neutron_xs(E()); } else if (type().is_photon()) { - majorant() = PhotonMajorant::safety_factor_ * data::p_majorant->calculate_photon_xs(E()); + majorant() = PhotonMajorant::safety_factor_ * + data::p_majorant->calculate_photon_xs(E()); } } diff --git a/src/settings.cpp b/src/settings.cpp index 77e3b7ac03c..a3b6e8f133d 100644 --- a/src/settings.cpp +++ b/src/settings.cpp @@ -1236,11 +1236,13 @@ void read_settings_xml(pugi::xml_node root) delta_tracking = get_node_value_bool(root, "delta_tracking"); if (temperature_multipole && delta_tracking) { - fatal_error("Delta tracking cannot be used with a windowed multipole temperature treatment."); + fatal_error("Delta tracking cannot be used with a windowed multipole " + "temperature treatment."); } if (!run_CE && delta_tracking) { - fatal_error("At present, delta tracking can only be used in continuous energy simulations."); + fatal_error("At present, delta tracking can only be used in continuous " + "energy simulations."); } } diff --git a/src/simulation.cpp b/src/simulation.cpp index e6b428db1b3..95df734d192 100644 --- a/src/simulation.cpp +++ b/src/simulation.cpp @@ -905,22 +905,25 @@ void transport_delta_history_based_single_particle(Particle& p) if (p.alive()) { // Electrons and positrons collide in-place, no need to rejection sample. - if (p.type() == ParticleType::electron() || p.type() == ParticleType::positron()) { + if (p.type() == ParticleType::electron() || + p.type() == ParticleType::positron()) { p.event_collide(); } if (p.collision_distance() < p.boundary().distance()) { - // Collided before hitting an external boundary. Rejection sample the majorant. + // Collided before hitting an external boundary. Rejection sample the + // majorant. p.event_calculate_xs(); if (p.alive() && (p.macro_xs().total / p.majorant() > 1.0)) { - p.mark_as_lost( - fmt::format("Ratio of the total cross section ({}) to the majorant " - "cross section ({}) for particle {} ({}) with energy {} is " - "greater than unity!", - p.macro_xs().total, p.majorant(), p.id(), p.type().str(), p.E())); + p.mark_as_lost(fmt::format( + "Ratio of the total cross section ({}) to the majorant " + "cross section ({}) for particle {} ({}) with energy {} is " + "greater than unity!", + p.macro_xs().total, p.majorant(), p.id(), p.type().str(), p.E())); break; } - if (p.alive() && (prn(p.current_seed()) < (p.macro_xs().total / p.majorant()))) { + if (p.alive() && + (prn(p.current_seed()) < (p.macro_xs().total / p.majorant()))) { p.event_collide(); } } else { From df740850b4e36d99e9a70105c6e7698f8373baaa Mon Sep 17 00:00:00 2001 From: nuclearkevin <66632997+nuclearkevin@users.noreply.github.com> Date: Sat, 13 Jun 2026 12:01:52 -0500 Subject: [PATCH 64/73] URR grid index fix. --- src/majorant.cpp | 15 +++++++++------ 1 file changed, 9 insertions(+), 6 deletions(-) diff --git a/src/majorant.cpp b/src/majorant.cpp index da36c4dfa87..971fc8329d4 100644 --- a/src/majorant.cpp +++ b/src/majorant.cpp @@ -216,8 +216,6 @@ void NeutronMajorant::fill_material_maj_xs(int i_material, const auto& mat = *model::materials[i_material]; mat_maj.resize(to_grid.size()); - std::fill(mat_maj.begin(), mat_maj.end(), 0.0); - for (int i_energy = 0; i_energy < to_grid.size(); ++i_energy) { mat_maj[i_energy] = 0.0; const double union_energy = to_grid[i_energy]; @@ -322,8 +320,15 @@ double NeutronMajorant::calculate_max_urr_xs( continue; } - int i_energy = - lower_bound_index(&urr.energy_.front(), &urr.energy_.back(), energy); + int i_energy; + if (energy < urr.energy_.front()) { + i_energy = 0; + } else if (energy > urr.energy_.back()) { + i_energy = urr.energy_.size() - 2; + } else { + i_energy = + lower_bound_index(&urr.energy_.front(), &urr.energy_.back(), energy); + } // Find the maximum URR cross sections for the two bounding energy points. double max_urr_xs_E0 = 0.0; @@ -506,8 +511,6 @@ void PhotonMajorant::fill_material_maj_xs(int i_material, const auto& mat = *model::materials[i_material]; mat_maj.resize(to_grid.size()); - std::fill(mat_maj.begin(), mat_maj.end(), 0.0); - for (int i_energy = 0; i_energy < to_grid.size(); ++i_energy) { mat_maj[i_energy] = 0.0; const double union_log_energy = to_grid[i_energy]; From 58348adff976962366fa29c115a5d827684a007a Mon Sep 17 00:00:00 2001 From: nuclearkevin <66632997+nuclearkevin@users.noreply.github.com> Date: Sat, 13 Jun 2026 13:37:14 -0500 Subject: [PATCH 65/73] More fixes for rare bugs that happen when finding a grid index. --- src/majorant.cpp | 17 +++++++++++------ 1 file changed, 11 insertions(+), 6 deletions(-) diff --git a/src/majorant.cpp b/src/majorant.cpp index 971fc8329d4..fd6d7dea330 100644 --- a/src/majorant.cpp +++ b/src/majorant.cpp @@ -321,9 +321,9 @@ double NeutronMajorant::calculate_max_urr_xs( } int i_energy; - if (energy < urr.energy_.front()) { + if (energy <= urr.energy_.front()) { i_energy = 0; - } else if (energy > urr.energy_.back()) { + } else if (energy >= urr.energy_.back()) { i_energy = urr.energy_.size() - 2; } else { i_energy = @@ -437,7 +437,7 @@ int NeutronMajorant::get_i_grid( std::log(energy / data::energy_min[i_neutron_]) / simulation::log_spacing; int i_grid; - if (energy < grid.energy.front()) { + if (energy <= grid.energy.front()) { i_grid = 0; } else if (energy >= grid.energy.back()) { i_grid = grid.energy.size() - 2; @@ -447,9 +447,14 @@ int NeutronMajorant::get_i_grid( int i_low = grid.grid_index[i_log_union]; int i_high = grid.grid_index[i_log_union + 1] + 1; - // Perform binary search over reduced range - i_grid = i_low + lower_bound_index( - &grid.energy[i_low], &grid.energy[i_high], energy); + // This catches the very rare case where floating point comparisons fail. + if (i_low >= grid.energy.size() || i_high >= grid.energy.size()) { + i_grid = grid.energy.size() - 2; + } else { + // Perform binary search over reduced range + i_grid = i_low + lower_bound_index( + &grid.energy[i_low], &grid.energy[i_high], energy); + } } // check for rare case where two energy points are the same From 9ebd64c9062ea16a2cd2f69e114a0dac64a6c411 Mon Sep 17 00:00:00 2001 From: nuclearkevin <66632997+nuclearkevin@users.noreply.github.com> Date: Tue, 16 Jun 2026 11:43:28 -0500 Subject: [PATCH 66/73] Catch void majorants, clean up majorant killswitch. --- include/openmc/particle.h | 7 +++++++ src/event.cpp | 7 +------ src/particle.cpp | 17 +++++++++++++++++ src/simulation.cpp | 11 +++-------- 4 files changed, 28 insertions(+), 14 deletions(-) diff --git a/include/openmc/particle.h b/include/openmc/particle.h index f64f4db1913..9f7e81f800d 100644 --- a/include/openmc/particle.h +++ b/include/openmc/particle.h @@ -115,6 +115,13 @@ class Particle : public ParticleData { //! Update the cached majorant cross section for this particle. void update_majorant(); + //! Check if the majorant is valid. If its not, the particle is killed and + //! a restart file is written. + // + //! \return true if the majorant is invalid, false if not. If true, + //! the particle is also killed. + bool kill_invalid_maj(); + //! create a particle restart HDF5 file void write_restart() const; diff --git a/src/event.cpp b/src/event.cpp index 65aebd95a31..26fef4e5e75 100644 --- a/src/event.cpp +++ b/src/event.cpp @@ -343,12 +343,7 @@ void process_delta_collision_events() p.type() == ParticleType::positron()) { p.event_collide(); } else { - if (p.macro_xs().total / p.majorant() > 1.0) { - p.mark_as_lost(fmt::format( - "Ratio of the total cross section ({}) to the majorant " - "cross section ({}) for particle {} ({}) with energy {} is " - "greater than unity!", - p.macro_xs().total, p.majorant(), p.id(), p.type().str(), p.E())); + if (p.kill_invalid_maj()) { continue; } diff --git a/src/particle.cpp b/src/particle.cpp index 7a68dfe70b9..b1c974df5e7 100644 --- a/src/particle.cpp +++ b/src/particle.cpp @@ -339,6 +339,10 @@ void Particle::event_delta_advance() type() == ParticleType::positron()) { // Electrons/positrons don't move collision_distance() = 0.0; + } else if (majorant() == 0.0) { + // For a void majorant (rare but possible for a source in a void), + // the collision distance is infinity. + collision_distance() = INFINITY; } else { // Sample collision distance based on the majorant for this energy. collision_distance() = -std::log(prn(current_seed())) / majorant(); @@ -896,6 +900,19 @@ void Particle::update_majorant() } } +bool Particle::kill_invalid_maj() +{ + if (alive() && (macro_xs().total / majorant() > 1.0)) { + mark_as_lost( + fmt::format("Ratio of the total cross section ({}) to the majorant " + "cross section ({}) for particle {} ({}) with energy {} is " + "greater than unity!", + macro_xs().total, majorant(), id(), type().str(), E())); + return true; + } + return false; +} + void Particle::mark_as_lost(const char* message) { // Print warning and write lost particle file diff --git a/src/simulation.cpp b/src/simulation.cpp index 95df734d192..d1301173f3c 100644 --- a/src/simulation.cpp +++ b/src/simulation.cpp @@ -910,23 +910,18 @@ void transport_delta_history_based_single_particle(Particle& p) p.event_collide(); } - if (p.collision_distance() < p.boundary().distance()) { + if (p.alive() && p.collision_distance() < p.boundary().distance()) { // Collided before hitting an external boundary. Rejection sample the // majorant. p.event_calculate_xs(); - if (p.alive() && (p.macro_xs().total / p.majorant() > 1.0)) { - p.mark_as_lost(fmt::format( - "Ratio of the total cross section ({}) to the majorant " - "cross section ({}) for particle {} ({}) with energy {} is " - "greater than unity!", - p.macro_xs().total, p.majorant(), p.id(), p.type().str(), p.E())); + if (p.kill_invalid_maj()) { break; } if (p.alive() && (prn(p.current_seed()) < (p.macro_xs().total / p.majorant()))) { p.event_collide(); } - } else { + } else if (p.alive()) { // Crossed an external boundary before colliding. p.event_cross_surface(); } From 05780f98851a3c5a92cec089da19f7dd36d7da69 Mon Sep 17 00:00:00 2001 From: nuclearkevin <66632997+nuclearkevin@users.noreply.github.com> Date: Tue, 16 Jun 2026 14:57:42 -0500 Subject: [PATCH 67/73] Build majorant in parallel with OpenMP threads. --- src/majorant.cpp | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/src/majorant.cpp b/src/majorant.cpp index fd6d7dea330..7790d2024c3 100644 --- a/src/majorant.cpp +++ b/src/majorant.cpp @@ -216,6 +216,8 @@ void NeutronMajorant::fill_material_maj_xs(int i_material, const auto& mat = *model::materials[i_material]; mat_maj.resize(to_grid.size()); + + #pragma omp parallel for for (int i_energy = 0; i_energy < to_grid.size(); ++i_energy) { mat_maj[i_energy] = 0.0; const double union_energy = to_grid[i_energy]; @@ -516,6 +518,8 @@ void PhotonMajorant::fill_material_maj_xs(int i_material, const auto& mat = *model::materials[i_material]; mat_maj.resize(to_grid.size()); + + #pragma omp parallel for for (int i_energy = 0; i_energy < to_grid.size(); ++i_energy) { mat_maj[i_energy] = 0.0; const double union_log_energy = to_grid[i_energy]; From d4417fdcbda6f9f8aad63db67f9b8f147a131ab8 Mon Sep 17 00:00:00 2001 From: nuclearkevin <66632997+nuclearkevin@users.noreply.github.com> Date: Tue, 16 Jun 2026 15:00:39 -0500 Subject: [PATCH 68/73] Style changes. --- src/majorant.cpp | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/majorant.cpp b/src/majorant.cpp index 7790d2024c3..15e76fd980d 100644 --- a/src/majorant.cpp +++ b/src/majorant.cpp @@ -217,7 +217,7 @@ void NeutronMajorant::fill_material_maj_xs(int i_material, mat_maj.resize(to_grid.size()); - #pragma omp parallel for +#pragma omp parallel for for (int i_energy = 0; i_energy < to_grid.size(); ++i_energy) { mat_maj[i_energy] = 0.0; const double union_energy = to_grid[i_energy]; @@ -519,7 +519,7 @@ void PhotonMajorant::fill_material_maj_xs(int i_material, mat_maj.resize(to_grid.size()); - #pragma omp parallel for +#pragma omp parallel for for (int i_energy = 0; i_energy < to_grid.size(); ++i_energy) { mat_maj[i_energy] = 0.0; const double union_log_energy = to_grid[i_energy]; From e118baaab385a368ed39873768b6e6e5cf60fad4 Mon Sep 17 00:00:00 2001 From: nuclearkevin <66632997+nuclearkevin@users.noreply.github.com> Date: Wed, 17 Jun 2026 09:21:58 -0500 Subject: [PATCH 69/73] Consistent BC tolerance application. --- src/particle.cpp | 12 +++---- .../periodic/results_true.dat | 18 +++++----- .../delta_tracking_bcs/white/results_true.dat | 18 +++++----- .../False/results_true.dat | 18 +++++----- .../True/results_true.dat | 16 ++++----- .../True/results_true.dat | 34 +++++++++---------- .../True/results_true.dat | 34 +++++++++---------- 7 files changed, 75 insertions(+), 75 deletions(-) diff --git a/src/particle.cpp b/src/particle.cpp index b1c974df5e7..25e48ef9329 100644 --- a/src/particle.cpp +++ b/src/particle.cpp @@ -348,15 +348,15 @@ void Particle::event_delta_advance() collision_distance() = -std::log(prn(current_seed())) / majorant(); } - // Update distance to problem boundary + // Update distance to problem boundary. Particles with large majorant + // cross sections will tunnel out of the domain if a floating point + // tolerance is not specified on the boundary distance calculation. boundary() = distance_to_external_boundary(*this); + boundary().distance() -= FP_REL_PRECISION; - // Move to the external boundary or delta tracking collision site. Particles - // with large majorant cross sections will tunnel out of the domain if a - // floating point tolerance is not specified on the boundary distance - // calculation. + // Move to the external boundary or delta tracking collision site. double distance = - std::min(collision_distance(), boundary().distance() - FP_REL_PRECISION); + std::min(collision_distance(), boundary().distance()); r() += distance * u(); // Need to locate the particle at the collision site or boundary. diff --git a/tests/regression_tests/delta_tracking_bcs/periodic/results_true.dat b/tests/regression_tests/delta_tracking_bcs/periodic/results_true.dat index dd6eca2dee8..53e0735cda8 100644 --- a/tests/regression_tests/delta_tracking_bcs/periodic/results_true.dat +++ b/tests/regression_tests/delta_tracking_bcs/periodic/results_true.dat @@ -1,11 +1,11 @@ k-combined: -1.849018E+00 2.929263E-03 +1.837808E+00 3.691605E-03 tally 1: -1.874900E+01 -7.040473E+01 -1.847800E+01 -6.831298E+01 -1.859800E+01 -6.926442E+01 -1.834400E+01 -6.735645E+01 +1.874300E+01 +7.031590E+01 +1.879200E+01 +7.068119E+01 +1.870100E+01 +7.000963E+01 +1.852700E+01 +6.868985E+01 diff --git a/tests/regression_tests/delta_tracking_bcs/white/results_true.dat b/tests/regression_tests/delta_tracking_bcs/white/results_true.dat index 92ddc2dee65..1fc2acdeeae 100644 --- a/tests/regression_tests/delta_tracking_bcs/white/results_true.dat +++ b/tests/regression_tests/delta_tracking_bcs/white/results_true.dat @@ -1,11 +1,11 @@ k-combined: -1.843368E+00 1.587102E-03 +1.842649E+00 3.286845E-03 tally 1: -1.835700E+01 -6.749374E+01 -1.936700E+01 -7.503086E+01 -1.872400E+01 -7.013084E+01 -1.857200E+01 -6.902069E+01 +1.844200E+01 +6.808763E+01 +1.903200E+01 +7.249322E+01 +1.858800E+01 +6.913862E+01 +1.866100E+01 +6.966707E+01 diff --git a/tests/regression_tests/delta_tracking_distribrho/False/results_true.dat b/tests/regression_tests/delta_tracking_distribrho/False/results_true.dat index 23b3eb24804..6e6698cae69 100644 --- a/tests/regression_tests/delta_tracking_distribrho/False/results_true.dat +++ b/tests/regression_tests/delta_tracking_distribrho/False/results_true.dat @@ -1,11 +1,11 @@ k-combined: -1.868055E+00 4.212977E-03 +1.860197E+00 1.196913E-03 tally 1: -1.649200E+01 -5.445790E+01 -1.884800E+01 -7.109906E+01 -1.611600E+01 -5.202239E+01 -1.841400E+01 -6.788397E+01 +1.633700E+01 +5.350220E+01 +1.886400E+01 +7.123130E+01 +1.617900E+01 +5.238011E+01 +1.833900E+01 +6.734011E+01 diff --git a/tests/regression_tests/delta_tracking_distribrho/True/results_true.dat b/tests/regression_tests/delta_tracking_distribrho/True/results_true.dat index d395c34a762..c8934bf8cf9 100644 --- a/tests/regression_tests/delta_tracking_distribrho/True/results_true.dat +++ b/tests/regression_tests/delta_tracking_distribrho/True/results_true.dat @@ -10,11 +10,11 @@ tally 1: 1.903000E+01 7.262313E+01 tally 2: -6.548566E+01 -8.595674E+02 -1.145647E+02 -2.625219E+03 -6.537393E+01 -8.552263E+02 -1.154873E+02 -2.672011E+03 +6.547989E+01 +8.594162E+02 +1.145708E+02 +2.625502E+03 +6.537182E+01 +8.551704E+02 +1.154863E+02 +2.671966E+03 diff --git a/tests/regression_tests/delta_tracking_event/True/results_true.dat b/tests/regression_tests/delta_tracking_event/True/results_true.dat index 27e7afcefb7..fb0fe02b56c 100644 --- a/tests/regression_tests/delta_tracking_event/True/results_true.dat +++ b/tests/regression_tests/delta_tracking_event/True/results_true.dat @@ -1,20 +1,20 @@ k-combined: -1.841093E+00 4.770249E-03 +1.837786E+00 2.176497E-03 tally 1: -1.878600E+01 -7.065042E+01 -1.829400E+01 -6.696300E+01 -1.883000E+01 -7.095247E+01 -1.854300E+01 -6.883357E+01 +1.864400E+01 +6.959490E+01 +1.872400E+01 +7.018429E+01 +1.855700E+01 +6.894280E+01 +1.811100E+01 +6.571011E+01 tally 2: -8.714295E+01 -1.519366E+03 -8.757274E+01 -1.533958E+03 -8.827597E+01 -1.558930E+03 -8.794806E+01 -1.547220E+03 +8.578019E+01 +1.473018E+03 +8.729418E+01 +1.526265E+03 +8.810551E+01 +1.553484E+03 +8.716879E+01 +1.520479E+03 diff --git a/tests/regression_tests/delta_tracking_history/True/results_true.dat b/tests/regression_tests/delta_tracking_history/True/results_true.dat index 27e7afcefb7..fb0fe02b56c 100644 --- a/tests/regression_tests/delta_tracking_history/True/results_true.dat +++ b/tests/regression_tests/delta_tracking_history/True/results_true.dat @@ -1,20 +1,20 @@ k-combined: -1.841093E+00 4.770249E-03 +1.837786E+00 2.176497E-03 tally 1: -1.878600E+01 -7.065042E+01 -1.829400E+01 -6.696300E+01 -1.883000E+01 -7.095247E+01 -1.854300E+01 -6.883357E+01 +1.864400E+01 +6.959490E+01 +1.872400E+01 +7.018429E+01 +1.855700E+01 +6.894280E+01 +1.811100E+01 +6.571011E+01 tally 2: -8.714295E+01 -1.519366E+03 -8.757274E+01 -1.533958E+03 -8.827597E+01 -1.558930E+03 -8.794806E+01 -1.547220E+03 +8.578019E+01 +1.473018E+03 +8.729418E+01 +1.526265E+03 +8.810551E+01 +1.553484E+03 +8.716879E+01 +1.520479E+03 From 1b5a0a39066f3963d1c24cf2055bf221aed64354 Mon Sep 17 00:00:00 2001 From: nuclearkevin <66632997+nuclearkevin@users.noreply.github.com> Date: Wed, 17 Jun 2026 09:26:46 -0500 Subject: [PATCH 70/73] Advancing in time. --- src/particle.cpp | 22 +++++++++++++++++++--- 1 file changed, 19 insertions(+), 3 deletions(-) diff --git a/src/particle.cpp b/src/particle.cpp index 25e48ef9329..e78163a6c08 100644 --- a/src/particle.cpp +++ b/src/particle.cpp @@ -354,10 +354,21 @@ void Particle::event_delta_advance() boundary() = distance_to_external_boundary(*this); boundary().distance() -= FP_REL_PRECISION; - // Move to the external boundary or delta tracking collision site. + double speed = this->speed(); + double time_cutoff = settings::time_cutoff[type().transport_index()]; + double distance_cutoff = + (time_cutoff < INFTY) ? (time_cutoff - time()) * speed : INFTY; + + // Move to the external boundary, delta tracking collision site, or time + // cutoff distance. double distance = - std::min(collision_distance(), boundary().distance()); - r() += distance * u(); + std::min({collision_distance(), boundary().distance(), distance_cutoff}); + move_distance(distance); + + // Advance particle in time. + double dt = distance / speed; + time() += dt; + lifetime() += dt; // Need to locate the particle at the collision site or boundary. for (int j = 0; j < n_coord(); ++j) { @@ -372,6 +383,11 @@ void Particle::event_delta_advance() // Force re-calculation of material properties at the collision site. material_last() = C_NONE; + + // Set particle weight to zero if it hit the time boundary + if (distance == distance_cutoff) { + wgt() = 0.0; + } } void Particle::event_cross_surface() From 844b7ee420317d65cab6c0f29817db38c866b310 Mon Sep 17 00:00:00 2001 From: nuclearkevin <66632997+nuclearkevin@users.noreply.github.com> Date: Thu, 18 Jun 2026 14:15:56 -0500 Subject: [PATCH 71/73] Catch void materials. --- src/majorant.cpp | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/src/majorant.cpp b/src/majorant.cpp index 15e76fd980d..b31c78ed871 100644 --- a/src/majorant.cpp +++ b/src/majorant.cpp @@ -94,7 +94,9 @@ Majorant::Majorant(int i_universe) : maj_universe_(i_universe) // Clear the contained materials vector and insert the elements from the set. for (auto i_mat : unique_materials) { - contained_materials_.push_back(i_mat); + if (i_mat != MATERIAL_VOID) { + contained_materials_.push_back(i_mat); + } } } From 82ce365d34bc5ea0477232435b5d681299464136 Mon Sep 17 00:00:00 2001 From: nuclearkevin <66632997+nuclearkevin@users.noreply.github.com> Date: Wed, 24 Jun 2026 14:40:18 -0500 Subject: [PATCH 72/73] Apply more maxes to URRs. --- src/majorant.cpp | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/src/majorant.cpp b/src/majorant.cpp index b31c78ed871..4de799f37aa 100644 --- a/src/majorant.cpp +++ b/src/majorant.cpp @@ -343,6 +343,9 @@ double NeutronMajorant::calculate_max_urr_xs( max_urr_xs_E1 = std::max(max_urr_xs_E1, urr.xs_values_(i_energy + 1, i_cdf).total); } + // Handle the rare case where the points could be negative. + max_urr_xs_E0 = std::max(max_urr_xs_E0, 0.0); + max_urr_xs_E1 = std::max(max_urr_xs_E1, 0.0); // Interpolate the bounding energy points. double interp_urr_xs = 0.0; @@ -359,7 +362,7 @@ double NeutronMajorant::calculate_max_urr_xs( interp_urr_xs *= smooth_xs; } - max_urr_xs = std::max(max_urr_xs, interp_urr_xs); + max_urr_xs = std::max({max_urr_xs, interp_urr_xs, smooth_xs}); } return max_urr_xs; From 137e0b82fad7bebdc1e055993c730ca8da241022 Mon Sep 17 00:00:00 2001 From: nuclearkevin <66632997+nuclearkevin@users.noreply.github.com> Date: Mon, 6 Jul 2026 15:03:40 -0500 Subject: [PATCH 73/73] Minor improvements, document statepoint changes. --- docs/source/io_formats/statepoint.rst | 6 ++++++ docs/source/methods/neutron_physics.rst | 4 ++-- src/majorant.cpp | 3 +-- 3 files changed, 9 insertions(+), 4 deletions(-) diff --git a/docs/source/io_formats/statepoint.rst b/docs/source/io_formats/statepoint.rst index 7d7765b849d..91fe746b82a 100644 --- a/docs/source/io_formats/statepoint.rst +++ b/docs/source/io_formats/statepoint.rst @@ -28,6 +28,10 @@ The current version of the statepoint file format is 18.2. 'continuous-energy' or 'multi-group'. - **run_mode** (*char[]*) -- Run mode used, either 'eigenvalue' or 'fixed source'. + - **photon_transport** (*bool*) -- Whether photon transport was enabled + or not. + - **delta_tracking** (*bool*) -- Whether delta tracking was enabled + or not. - **n_particles** (*int8_t*) -- Number of particles used per generation. - **n_batches** (*int*) -- Number of batches to simulate. - **current_batch** (*int*) -- The number of batches already simulated. @@ -178,6 +182,8 @@ All values are given in seconds and are measured on the master process. allocating arrays, etc. - **reading cross sections** (*double*) -- Time spent loading cross section libraries (this is a subset of initialization). + - **build majorant** (*double*) -- Time spent constructing the majorant + cross sections. This is only saved if running with delta tracking. - **simulation** (*double*) -- Time spent between initialization and finalization. - **transport** (*double*) -- Time spent transporting particles. diff --git a/docs/source/methods/neutron_physics.rst b/docs/source/methods/neutron_physics.rst index c1af34bada3..cd6e0cf3bc8 100644 --- a/docs/source/methods/neutron_physics.rst +++ b/docs/source/methods/neutron_physics.rst @@ -144,12 +144,12 @@ cross section samples in Equation :eq:`delta-real-collision`. Delta tracking often performs better than surface tracking in problems where the particle mean free path is larger than the distance between surfaces. Problems containing small regions with large total cross sections (such as burnable -absorbers) will have majorant cross sections several orders of magnitude in +absorbers) will have majorant cross sections several orders of magnitude larger than the total cross section over the majority of the domain [Leppänen]_. This decreases the number of accepted collisions, and therefore the effectiveness of delta tracking. Material discontinuities are not considered in delta tracking, which prohibits the use of track length -estimators and forces the use of the higher- variance collision estimator +estimators and forces the use of the higher-variance collision estimator (discussed in detail in the :ref:`methods_tallies` section). ---------------------------------------------------- diff --git a/src/majorant.cpp b/src/majorant.cpp index 4de799f37aa..410e00540ce 100644 --- a/src/majorant.cpp +++ b/src/majorant.cpp @@ -575,8 +575,7 @@ double PhotonMajorant::calculate_elem_tot_xs( elem.pair_production_total_(i_grid))); // Calculate microscopic total cross section - double total = coherent + incoherent + photoelectric + pair_production; - return total; + return coherent + incoherent + photoelectric + pair_production; } int PhotonMajorant::get_i_grid(