From 270eace12c6d7dcf7614b6e07e53f8c770bf601c Mon Sep 17 00:00:00 2001 From: John Tramm Date: Thu, 9 Jul 2026 21:18:42 +0000 Subject: [PATCH 01/35] Add adaptive random ray volume estimator and make it the default The adaptive estimator generalizes hybrid: it uses the simulation-averaged volume by default and demotes individual source regions to the naive (iteration) volume plus a previous-iteration miss treatment wherever the simulation-averaged estimator is at risk. Demotion is triggered by (1) a strong inhomogeneous source -- any group whose reduced source q/Sigma_t is negative or exceeds ADAPTIVE_VOLUME_KAPPA times the previous iteration's scalar flux, the condition under which the flux update is a near-cancellation that requires volume-consistent terms (this subsumes hybrid's external-source heuristic and also catches optically thin in-scatter-fed regions that hybrid misses); (2) hit starvation (the existing small-region criterion); and (3) a converged-negative flux: the unmodified simulation-averaged estimator runs through the inactive phase while each region's flux is accumulated, and any region whose accumulated flux is negative at the end of the inactive phase is demoted for all active batches. Deciding on the sign of the accumulated estimate rather than reacting to per-iteration fluctuations avoids clipping the lower tail of the noise distribution, so merely-noisy regions keep the unbiased estimator. Under linear sources, strong-source regions also fall back to a flat source representation, since their gradient terms carry per-iteration noise at the q/Sigma_t scale that the volume choice cannot cancel; the general gradient (tilt) limiter remains a separate follow-up. The adaptive estimator becomes the default (previously hybrid), both at static initialization and in openmc_finalize_random_ray(), which restores built-in defaults between in-process runs. The eigenvalue fw-adjoint double solve now clears the accumulated forward flux before the adjoint solve (in fixed source mode set_fw_adjoint_sources already consumes and zeroes it), so the adjoint solve's demotion decision operates on clean adjoint statistics. An end-of-run report breaks down the naive-volume treatment by cause. naive/simulation_averaged/hybrid behavior is unchanged (the estimator application is refactored into two per-region policy decisions that reproduce the legacy estimators exactly). Co-Authored-By: Claude Fable 5 --- include/openmc/constants.h | 17 +- .../openmc/random_ray/flat_source_domain.h | 19 ++ include/openmc/random_ray/source_region.h | 18 +- openmc/settings.py | 13 +- src/random_ray/flat_source_domain.cpp | 215 +++++++++++++++--- src/random_ray/linear_source_domain.cpp | 17 ++ src/random_ray/random_ray_simulation.cpp | 44 +++- src/random_ray/source_region.cpp | 7 +- src/settings.cpp | 3 + 9 files changed, 312 insertions(+), 41 deletions(-) diff --git a/include/openmc/constants.h b/include/openmc/constants.h index 26b4a224c0e..d9d510a873b 100644 --- a/include/openmc/constants.h +++ b/include/openmc/constants.h @@ -65,6 +65,16 @@ constexpr int MAX_SAMPLE {100000}; // source region in the random ray solver constexpr double MIN_HITS_PER_BATCH {1.5}; +// Strong-source ratio threshold for the adaptive volume estimator. A source +// region is treated as having a "strong" inhomogeneous source -- and is given +// the naive volume and previous-flux miss treatment -- in any group where the +// reduced source q/Sigma_t exceeds this multiple of the region's scalar flux, +// indicating a source sustained by an external or in-scatter contribution +// rather than by the local flux. The value sits well inside the range over +// which benign problems remain untriggered while pathological cells are still +// caught. +constexpr double ADAPTIVE_VOLUME_KAPPA {4.0}; + // The minimum flux value to be considered non-zero when computing adjoint // sources. Positive values below this cutoff will be treated as zero, so as to // prevent extremely large adjoint source terms from being generated. @@ -365,7 +375,12 @@ enum class RunMode { enum class SolverType { MONTE_CARLO, RANDOM_RAY }; -enum class RandomRayVolumeEstimator { NAIVE, SIMULATION_AVERAGED, HYBRID }; +enum class RandomRayVolumeEstimator { + NAIVE, + SIMULATION_AVERAGED, + HYBRID, + ADAPTIVE +}; enum class RandomRaySourceShape { FLAT, LINEAR, LINEAR_XY }; enum class RandomRaySampleMethod { PRNG, HALTON, S2 }; enum class RandomRaySolve { FORWARD, FORWARD_FOR_ADJOINT, ADJOINT }; diff --git a/include/openmc/random_ray/flat_source_domain.h b/include/openmc/random_ray/flat_source_domain.h index 09414fd4465..d60e6868eb0 100644 --- a/include/openmc/random_ray/flat_source_domain.h +++ b/include/openmc/random_ray/flat_source_domain.h @@ -39,6 +39,7 @@ class FlatSourceDomain { void reset_tally_volumes(); void random_ray_tally(); virtual void accumulate_iteration_flux(); + void inactive_demotion_step(); void output_to_vtk() const; void convert_external_sources(bool use_adjoint_sources); void count_external_source_regions(); @@ -103,6 +104,15 @@ class FlatSourceDomain { int64_t n_external_source_regions_ {0}; // Total number of source regions with // non-zero external source terms + // Final-iteration snapshot of the naive volume treatment, classified by + // cause with mutually exclusive attribution (the cause lines sum to the + // total), for end-of-simulation reporting + int64_t n_final_naive_ {0}; + int64_t n_final_strong_ {0}; + int64_t n_final_demoted_ {0}; + int64_t n_final_small_ {0}; + bool final_stats_valid_ {false}; + // 1D array representing source region starting offset for each OpenMC Cell // in model::cells vector source_region_offsets_; @@ -171,6 +181,15 @@ class FlatSourceDomain { void set_flux_to_source(int64_t sr, int g); virtual void set_flux_to_old_flux(int64_t sr, int g); + //! Adaptive-estimator "strong source" test: true if, in any group, the + //! region's reduced source q/Sigma_t is negative or exceeds + //! ADAPTIVE_VOLUME_KAPPA times the (non-negative) previous-iteration scalar + //! flux. Shared by the flat volume switch (add_source_to_scalar_flux) and the + //! linear gradient fallback (update_single_neutron_source); the region's + //! per-group reduced-source and previous-flux arrays are passed directly. + bool region_has_strong_source( + const float* reduced_source, const double* flux_old) const; + //---------------------------------------------------------------------------- // Private data members int negroups_; // Number of energy groups in simulation diff --git a/include/openmc/random_ray/source_region.h b/include/openmc/random_ray/source_region.h index 1d2bbe1e8dc..dec5eaab330 100644 --- a/include/openmc/random_ray/source_region.h +++ b/include/openmc/random_ray/source_region.h @@ -149,6 +149,7 @@ class SourceRegionHandle { int* temperature_idx_; double* density_mult_; int* is_small_; + int* n_negative_fluxes_; int* n_hits_; int* birthday_; OpenMPMutex* lock_; @@ -205,6 +206,8 @@ class SourceRegionHandle { int& is_small() { return *is_small_; } const int is_small() const { return *is_small_; } + int& n_negative_fluxes() { return *n_negative_fluxes_; } + const int n_negative_fluxes() const { return *n_negative_fluxes_; } int& n_hits() { return *n_hits_; } const int n_hits() const { return *n_hits_; } @@ -337,8 +340,13 @@ class SourceRegion { double volume_naive_ {0.0}; //!< Volume as integrated from this iteration only int position_recorded_ {0}; //!< Has the position been recorded yet? int external_source_present_ { - 0}; //!< Is an external source present in this region? - int is_small_ {0}; //!< Is it "small", receiving < 1.5 hits per iteration? + 0}; //!< Is an external source present in this region? + int is_small_ {0}; //!< Is it "small", receiving < 1.5 hits per iteration? + int n_negative_fluxes_ { + 0}; //!< One-shot demotion flag (adaptive estimator only): set to 1 at the + //!< end of the inactive phase when this region's accumulated flux was + //!< negative, demoting it to the naive volume estimator for the active + //!< phase. int n_hits_ {0}; //!< Number of total hits (ray crossings) // Mesh that subdivides this source region int mesh_ {C_NONE}; //!< Index in openmc::model::meshes array that subdivides @@ -413,6 +421,11 @@ class SourceRegionContainer { int& is_small(int64_t sr) { return is_small_[sr]; } const int is_small(int64_t sr) const { return is_small_[sr]; } + int& n_negative_fluxes(int64_t sr) { return n_negative_fluxes_[sr]; } + const int n_negative_fluxes(int64_t sr) const + { + return n_negative_fluxes_[sr]; + } int& n_hits(int64_t sr) { return n_hits_[sr]; } const int n_hits(int64_t sr) const { return n_hits_[sr]; } @@ -645,6 +658,7 @@ class SourceRegionContainer { vector temperature_idx_; vector density_mult_; vector is_small_; + vector n_negative_fluxes_; vector n_hits_; vector mesh_; vector parent_sr_; diff --git a/openmc/settings.py b/openmc/settings.py index 8120eb073e6..984fe8a8755 100644 --- a/openmc/settings.py +++ b/openmc/settings.py @@ -202,8 +202,15 @@ class Settings: specified by a :class:`openmc.SourceBase` object. :volume_estimator: Choice of volume estimator for the random ray solver. Options are - 'naive', 'simulation_averaged', or 'hybrid'. - The default is 'hybrid'. + 'naive', 'simulation_averaged', 'hybrid', or 'adaptive'. The default + is 'adaptive'. The 'adaptive' estimator generalizes 'hybrid': it uses + the simulation-averaged volume by default but falls back to the + naive (iteration) volume in regions with a strong inhomogeneous + source (kappa test), in hit-starved regions, and in regions whose + accumulated flux is negative at the end of the inactive phase (a + one-shot demotion for the active batches), which removes the + negative-flux instabilities 'hybrid' can exhibit in optically + thin, in-scatter-fed regions. :source_shape: Assumed shape of the source distribution within each source region. Options are 'flat' (default), 'linear', or 'linear_xy'. @@ -1416,7 +1423,7 @@ def random_ray(self, random_ray: dict): elif key == 'volume_estimator': cv.check_value('volume estimator', value, ('naive', 'simulation_averaged', - 'hybrid')) + 'hybrid', 'adaptive')) elif key == 'source_shape': cv.check_value('source shape', value, ('flat', 'linear', 'linear_xy')) diff --git a/src/random_ray/flat_source_domain.cpp b/src/random_ray/flat_source_domain.cpp index 83128fdaa05..9d90a875e37 100644 --- a/src/random_ray/flat_source_domain.cpp +++ b/src/random_ray/flat_source_domain.cpp @@ -28,7 +28,7 @@ namespace openmc { // Static Variable Declarations RandomRayVolumeEstimator FlatSourceDomain::volume_estimator_ { - RandomRayVolumeEstimator::HYBRID}; + RandomRayVolumeEstimator::ADAPTIVE}; bool FlatSourceDomain::volume_normalized_flux_tallies_ {false}; bool FlatSourceDomain::adjoint_requested_ {false}; RandomRaySolve FlatSourceDomain::solve_ {RandomRaySolve::FORWARD}; @@ -103,6 +103,56 @@ void FlatSourceDomain::accumulate_iteration_flux() } } +// Demotion step for the adaptive volume estimator (no-op for the +// others). Rather than reacting to per-iteration negatives, this estimator +// runs the unmodified simulation-averaged update throughout the inactive phase +// and decides demotion once, from the actual sign of each region's converged +// estimate. During the inactive phase the (un-rescued) flux is accumulated; +// on the final inactive batch, any region whose accumulated flux is negative +// in any group is demoted to the naive (iteration) volume estimator for the +// active phase -- a positively weighted estimator that cannot go negative with +// a non-negative source -- while every other region keeps the unbiased +// simulation-averaged estimator. Because the decision is made on the +// accumulated mean rather than on individual fluctuations, the lower tail of +// the noise distribution is not clipped, so regions that are merely noisy (and +// average non-negative) are left unbiased. The demotion is recorded in +// n_negative_fluxes (>= 1 == demoted), consumed by the volume switch and miss +// treatment in add_source_to_scalar_flux. +void FlatSourceDomain::inactive_demotion_step() +{ + if (volume_estimator_ != RandomRayVolumeEstimator::ADAPTIVE) + return; + if (simulation::current_batch > settings::n_inactive) + return; + + // scalar_flux_final is untouched until active accumulation begins, so it + // serves as the temporary inactive accumulator. +#pragma omp parallel for + for (int64_t se = 0; se < n_source_elements(); se++) { + source_regions_.scalar_flux_final(se) += + source_regions_.scalar_flux_new(se); + } + + // On the last inactive batch, settle the demotion decision and clear the + // accumulator so the active phase tallies start from zero. + if (simulation::current_batch == settings::n_inactive) { +#pragma omp parallel for + for (int64_t sr = 0; sr < n_source_regions(); sr++) { + bool negative = false; + for (int g = 0; g < negroups_; g++) { + if (source_regions_.scalar_flux_final(sr, g) < 0.0) { + negative = true; + break; + } + } + source_regions_.n_negative_fluxes(sr) = negative ? 1 : 0; + for (int g = 0; g < negroups_; g++) { + source_regions_.scalar_flux_final(sr, g) = 0.0; + } + } + } +} + void FlatSourceDomain::update_single_neutron_source(SourceRegionHandle& srh) { // Reset all source regions to zero (important for void regions) @@ -226,14 +276,38 @@ void FlatSourceDomain::set_flux_to_source(int64_t sr, int g) source_regions_.scalar_flux_new(sr, g) = source_regions_.source(sr, g); } +bool FlatSourceDomain::region_has_strong_source( + const float* reduced_source, const double* flux_old) const +{ + for (int g = 0; g < negroups_; g++) { + double src = reduced_source[g]; + if (src < 0.0 || src > ADAPTIVE_VOLUME_KAPPA * std::max(flux_old[g], 0.0)) { + return true; + } + } + return false; +} + // Combine transport flux contributions and flat source contributions from the // previous iteration to generate this iteration's estimate of scalar flux. int64_t FlatSourceDomain::add_source_to_scalar_flux() { int64_t n_hits = 0; double inverse_batch = 1.0 / simulation::current_batch; - -#pragma omp parallel for reduction(+ : n_hits) + int64_t n_naive = 0; + int64_t n_strong = 0; + int64_t n_demoted = 0; + int64_t n_small = 0; + bool final_iteration = (simulation::current_batch == settings::n_batches); + // The adaptive estimator uses the proactive strong-source (kappa) test, the + // demote-to-naive volume switch, and the previous-flux miss treatment, with + // demotion decided once at the end of the inactive phase (recorded as a 0/1 + // flag in n_negative_fluxes by inactive_demotion_step). + const bool is_adaptive = + volume_estimator_ == RandomRayVolumeEstimator::ADAPTIVE; + +#pragma omp parallel for reduction( \ + + : n_hits, n_naive, n_strong, n_demoted, n_small) for (int64_t sr = 0; sr < n_source_regions(); sr++) { double volume_simulation_avg = source_regions_.volume(sr); @@ -252,50 +326,114 @@ int64_t FlatSourceDomain::add_source_to_scalar_flux() source_regions_.is_small(sr) = 0; } - // The volume treatment depends on the volume estimator type - // and whether or not an external source is present in the cell. - double volume; + // Determine if the source region has a "strong" inhomogeneous source, + // defined as any group whose reduced source greatly exceeds the previous + // iteration's scalar flux. In that condition the cell sits far below its + // own infinite-medium flux (q/Sigma_t), which arises when an optically + // thin cell holds a source that does not derive from its own local flux + // (an external source, or in-scatter from other groups). The + // flux update in such cells is a near-cancellation of the transport term + // against q/Sigma_t, which is only exact when the volumes used by the two + // terms are consistent -- so these cells require the naive (iteration) + // volume estimator and the previous-flux miss treatment to avoid error + // terms proportional to (q/Sigma_t) * (1 - V_iteration/V_average) that + // can greatly exceed the physical flux. + // + // A reduced source that is itself negative is also treated as strong. This + // arises under transport-corrected (e.g. TCP0) cross sections, whose + // within-group scattering term can be negative, driving q/Sigma_t below + // zero even for a non-negative flux; it can also arise transiently from a + // negative previous-iteration flux, which the estimator permits by design + // (there is no per-iteration positivity rescue). The diagonal (Gunow) + // stabilization keeps the TCP0 iteration convergent but acts on the flux, + // not on the source sign, so such regions still need the consistent + // (naive) volume and previous-flux miss treatment to keep a negative + // source from depositing negative flux through the miss path. In a normal + // slowing-down spectrum the positive in-scatter from faster groups + // dominates the negative within-group term, so in practice this condition + // rarely fires. + // + // Only the adaptive estimator consults the strong-source flag, so the + // other estimators skip the test (and its end-of-run report) entirely. + // Void (and effectively-void, sub-MINIMUM_MACRO_XS) regions are also + // excluded: they carry no q/Sigma_t term -- their flux is the streaming + // tally plus a bounded external contribution -- so the near-cancellation + // the test guards against cannot occur, and demoting them to the naive + // volume would only add ratio bias. This matches the linear domain, which + // already gates its strong-source gradient fallback on MATERIAL_VOID. + bool strong_source = + is_adaptive && source_regions_.material(sr) != MATERIAL_VOID && + region_has_strong_source(&source_regions_.source(sr, 0), + &source_regions_.scalar_flux_old(sr, 0)); + // Per-region demotion reasons. The hit-starved (small) and strong-source + // flags are re-evaluated every iteration; converged_neg is the one-shot + // flag set at the end of the inactive phase by inactive_demotion_step. The + // external-source flag drives only the hybrid policy (and the default miss + // treatment); the adaptive estimator catches a low-cross-section external + // region through the kappa strong-source test instead, since its external + // term is folded into q/Sigma_t. All are g-independent. + bool external = source_regions_.external_source_present(sr); + bool small = source_regions_.is_small(sr); + bool converged_neg = source_regions_.n_negative_fluxes(sr) > 0; + + // Every estimator reduces to two g-independent per-region decisions: + // 1. which volume to use on a hit -- the simulation-averaged volume, + // unless the region is demoted to the naive (iteration) volume; and + // 2. what to substitute on a miss -- the reduced source by default, or + // the previous iterate. + // The previous-flux miss treatment is needed wherever assigning the bare + // reduced source q/Sigma_t to a missed region would bias it: a low-cross- + // section region would otherwise deposit its full infinite-medium flux + // every time it is missed. Hybrid keys this on the external-source flag; + // the adaptive estimator instead extends the previous-flux treatment to + // every region it demotes, which (through the kappa test) already covers + // any region whose q/Sigma_t greatly exceeds its flux -- external or not. + // Both decisions are made once here so the per-group loop stays estimator- + // agnostic. + bool use_naive_volume = false; + bool use_old_flux_on_miss = external; switch (volume_estimator_) { case RandomRayVolumeEstimator::NAIVE: - volume = volume_iteration; + use_naive_volume = true; break; case RandomRayVolumeEstimator::SIMULATION_AVERAGED: - volume = volume_simulation_avg; break; case RandomRayVolumeEstimator::HYBRID: - if (source_regions_.external_source_present(sr) || - source_regions_.is_small(sr)) { - volume = volume_iteration; - } else { - volume = volume_simulation_avg; - } + use_naive_volume = external || small; + break; + case RandomRayVolumeEstimator::ADAPTIVE: + use_naive_volume = small || strong_source || converged_neg; + use_old_flux_on_miss = use_naive_volume; break; default: fatal_error("Invalid volume estimator type"); } + double volume = use_naive_volume ? volume_iteration : volume_simulation_avg; + + // On the final iteration, classify the demoted (naive-volume) regions by + // cause -- mutually exclusive, in priority order, so the causes sum to the + // total -- for the end-of-simulation report. + if (final_iteration && is_adaptive && use_naive_volume) { + n_naive++; + if (strong_source) { + n_strong++; + } else if (converged_neg) { + n_demoted++; + } else if (small) { + n_small++; + } + } for (int g = 0; g < negroups_; g++) { - // There are three scenarios we need to consider: if (volume_iteration > 0.0) { - // 1. If the FSR was hit this iteration, then the new flux is equal to - // the flat source from the previous iteration plus the contributions - // from rays passing through the source region (computed during the - // transport sweep) + // Hit this iteration: the flat source from the previous iteration plus + // this iteration's transport contribution, normalized by the chosen + // volume. set_flux_to_flux_plus_source(sr, volume, g); } else if (volume_simulation_avg > 0.0) { - // 2. If the FSR was not hit this iteration, but has been hit some - // previous iteration, then we need to make a choice about what - // to do. Naively we will usually want to set the flux to be equal - // to the reduced source. However, in fixed source problems where - // there is a strong external source present in the cell, and where - // the cell has a very low cross section, this approximation will - // cause a huge upward bias in the flux estimate of the cell (in these - // conditions, the flux estimate can be orders of magnitude too large). - // Thus, to avoid this bias, if any external source is present - // in the cell we will use the previous iteration's flux estimate. This - // injects a small degree of correlation into the simulation, but this - // is going to be trivial when the miss rate is a few percent or less. - if (source_regions_.external_source_present(sr)) { + // Missed this iteration but hit previously: substitute per the miss + // policy decided above (the previous iterate, or the reduced source). + if (use_old_flux_on_miss) { set_flux_to_old_flux(sr, g); } else { set_flux_to_source(sr, g); @@ -311,6 +449,17 @@ int64_t FlatSourceDomain::add_source_to_scalar_flux() } } + // Store the final-iteration treatment snapshot for reporting (adaptive only; + // the other estimators do not produce a by-cause naive-treatment breakdown) + if (final_iteration && + volume_estimator_ == RandomRayVolumeEstimator::ADAPTIVE) { + n_final_naive_ = n_naive; + n_final_strong_ = n_strong; + n_final_demoted_ = n_demoted; + n_final_small_ = n_small; + final_stats_valid_ = true; + } + // Return the number of source regions that were hit this iteration return n_hits; } @@ -1391,7 +1540,7 @@ void FlatSourceDomain::set_fw_adjoint_sources() source_regions_.external_source_present(sr) = 0; } } // End loop over source regions - } // End local FW-CADIS logic + } // End local FW-CADIS logic } void FlatSourceDomain::set_local_adjoint_sources() diff --git a/src/random_ray/linear_source_domain.cpp b/src/random_ray/linear_source_domain.cpp index b4701ed1fa9..c6c5c5ac44b 100644 --- a/src/random_ray/linear_source_domain.cpp +++ b/src/random_ray/linear_source_domain.cpp @@ -111,6 +111,23 @@ void LinearSourceDomain::update_single_neutron_source(SourceRegionHandle& srh) srh.source(g) += srh.external_source(g); } } + + // Under the adaptive volume estimator, regions receiving the protected + // (naive volume) treatment for a strong inhomogeneous source also fall back + // to a flat source representation. In such regions the reduced source greatly + // exceeds the scalar flux, so the flat-source cancellation must be exact; the + // gradient terms attenuate segments against the local rather than the flat + // source, introducing per-iteration noise at the gradient scale that the + // volume choice cannot cancel. Zeroing the gradients there extends the + // existing flat-source fallback already applied to hit-starved (small) + // regions. + if (volume_estimator_ == RandomRayVolumeEstimator::ADAPTIVE && + material != MATERIAL_VOID && + region_has_strong_source(&srh.source(0), &srh.scalar_flux_old(0))) { + for (int g = 0; g < negroups_; g++) { + srh.source_gradients(g) = {0.0, 0.0, 0.0}; + } + } } void LinearSourceDomain::normalize_scalar_flux_and_volumes( diff --git a/src/random_ray/random_ray_simulation.cpp b/src/random_ray/random_ray_simulation.cpp index 00fff99a7c9..34a4931bfa4 100644 --- a/src/random_ray/random_ray_simulation.cpp +++ b/src/random_ray/random_ray_simulation.cpp @@ -287,7 +287,7 @@ void validate_random_ray_inputs() void openmc_finalize_random_ray() { - FlatSourceDomain::volume_estimator_ = RandomRayVolumeEstimator::HYBRID; + FlatSourceDomain::volume_estimator_ = RandomRayVolumeEstimator::ADAPTIVE; FlatSourceDomain::volume_normalized_flux_tallies_ = false; FlatSourceDomain::adjoint_requested_ = false; FlatSourceDomain::solve_ = RandomRaySolve::FORWARD; @@ -346,7 +346,21 @@ void RandomRaySimulation::prepare_fw_fixed_sources_adjoint() // Prepare adjoint fixed sources using forward flux domain_->source_regions_.adjoint_reset(); if (settings::run_mode == RunMode::FIXED_SOURCE) { + // Consumes the accumulated forward flux (and zeroes it as it goes), so + // the adjoint solve starts from a clean accumulator. domain_->set_fw_adjoint_sources(); + } else { + // In eigenvalue mode there are no fixed adjoint sources to derive from + // the forward flux, but the accumulated forward flux must still be + // cleared so that the adjoint solve starts from a clean accumulator -- + // otherwise the adaptive estimator's inactive-demotion decision (which + // accumulates into the same array during the adjoint inactive batches) + // would be swamped by the forward solve's strictly positive sums, and + // any consumer of the final flux would mix forward and adjoint modes. +#pragma omp parallel for + for (int64_t se = 0; se < domain_->n_source_elements(); se++) { + domain_->source_regions_.scalar_flux_final(se) = 0.0; + } } } @@ -467,6 +481,11 @@ void RandomRaySimulation::simulate() domain_->random_ray_tally(); } + // For the adaptive estimator, accumulate the inactive-phase flux and, on + // the final inactive batch, settle which regions are demoted to the naive + // volume estimator (no-op for the other estimators). + domain_->inactive_demotion_step(); + // Set phi_old = phi_new domain_->flux_swap(); @@ -597,10 +616,33 @@ void RandomRaySimulation::print_results_random_ray( case RandomRayVolumeEstimator::HYBRID: estimator = "Hybrid"; break; + case RandomRayVolumeEstimator::ADAPTIVE: + estimator = "Adaptive"; + break; default: fatal_error("Invalid volume estimator type"); } fmt::print(" Volume Estimator Type = {}\n", estimator); + if (domain_->final_stats_valid_) { + double inv = 100.0 / domain_->n_source_regions(); + fmt::print(" Naive Volume Treatment (final iteration, by cause):\n"); + fmt::print(" Total = {} SRs ({:.4f}%)\n", + domain_->n_final_naive_, domain_->n_final_naive_ * inv); + fmt::print(" Strong Source = {} SRs ({:.4f}%)\n", + domain_->n_final_strong_, domain_->n_final_strong_ * inv); + fmt::print(" Converged Negative (demoted) = {} SRs ({:.4f}%)\n", + domain_->n_final_demoted_, domain_->n_final_demoted_ * inv); + fmt::print(" Hit-Starved (Small) = {} SRs ({:.4f}%)\n", + domain_->n_final_small_, domain_->n_final_small_ * inv); + // For linear-source runs, the strong-source regions additionally have + // their source gradients zeroed, reverting them to a flat source. This is + // the same set as "Strong Source" above (both apply the kappa test to the + // same data), reported here as the linear -> flat fallback frequency. + if (RandomRay::source_shape_ != RandomRaySourceShape::FLAT) { + fmt::print(" Strong Source -> Flat (linear) = {} SRs ({:.4f}%)\n", + domain_->n_final_strong_, domain_->n_final_strong_ * inv); + } + } std::string adjoint_true = (FlatSourceDomain::solve_ == RandomRaySolve::ADJOINT) ? "ON" : "OFF"; diff --git a/src/random_ray/source_region.cpp b/src/random_ray/source_region.cpp index 78543c5ab53..4a4ec685305 100644 --- a/src/random_ray/source_region.cpp +++ b/src/random_ray/source_region.cpp @@ -12,7 +12,8 @@ namespace openmc { SourceRegionHandle::SourceRegionHandle(SourceRegion& sr) : negroups_(sr.scalar_flux_old_.size()), material_(&sr.material_), temperature_idx_(&sr.temperature_idx_), density_mult_(&sr.density_mult_), - is_small_(&sr.is_small_), n_hits_(&sr.n_hits_), + is_small_(&sr.is_small_), n_negative_fluxes_(&sr.n_negative_fluxes_), + n_hits_(&sr.n_hits_), is_linear_(sr.source_gradients_.size() > 0), lock_(&sr.lock_), volume_(&sr.volume_), volume_t_(&sr.volume_t_), volume_sq_(&sr.volume_sq_), volume_sq_t_(&sr.volume_sq_t_), volume_naive_(&sr.volume_naive_), @@ -74,6 +75,7 @@ void SourceRegionContainer::push_back(const SourceRegion& sr) temperature_idx_.push_back(sr.temperature_idx_); density_mult_.push_back(sr.density_mult_); is_small_.push_back(sr.is_small_); + n_negative_fluxes_.push_back(sr.n_negative_fluxes_); n_hits_.push_back(sr.n_hits_); lock_.push_back(sr.lock_); volume_.push_back(sr.volume_); @@ -129,6 +131,7 @@ void SourceRegionContainer::assign( temperature_idx_.clear(); density_mult_.clear(); is_small_.clear(); + n_negative_fluxes_.clear(); n_hits_.clear(); lock_.clear(); volume_.clear(); @@ -188,6 +191,7 @@ SourceRegionHandle SourceRegionContainer::get_source_region_handle(int64_t sr) handle.temperature_idx_ = &temperature_idx(sr); handle.density_mult_ = &density_mult(sr); handle.is_small_ = &is_small(sr); + handle.n_negative_fluxes_ = &n_negative_fluxes(sr); handle.n_hits_ = &n_hits(sr); handle.is_linear_ = is_linear(); handle.lock_ = &lock(sr); @@ -231,6 +235,7 @@ SourceRegionHandle SourceRegionContainer::get_source_region_handle(int64_t sr) void SourceRegionContainer::adjoint_reset() { std::fill(n_hits_.begin(), n_hits_.end(), 0); + std::fill(n_negative_fluxes_.begin(), n_negative_fluxes_.end(), 0); std::fill(volume_.begin(), volume_.end(), 0.0); std::fill(volume_t_.begin(), volume_t_.end(), 0.0); std::fill(volume_sq_.begin(), volume_sq_.end(), 0.0); diff --git a/src/settings.cpp b/src/settings.cpp index 58bced94031..3b4be668e8a 100644 --- a/src/settings.cpp +++ b/src/settings.cpp @@ -300,6 +300,9 @@ void get_run_parameters(pugi::xml_node node_base) FlatSourceDomain::volume_estimator_ = RandomRayVolumeEstimator::NAIVE; } else if (temp_str == "hybrid") { FlatSourceDomain::volume_estimator_ = RandomRayVolumeEstimator::HYBRID; + } else if (temp_str == "adaptive") { + FlatSourceDomain::volume_estimator_ = + RandomRayVolumeEstimator::ADAPTIVE; } else { fatal_error("Unrecognized volume estimator: " + temp_str); } From f3f03b37dbea6a42d514328ab235b7bf4ac05dbc Mon Sep 17 00:00:00 2001 From: John Tramm Date: Thu, 9 Jul 2026 21:18:42 +0000 Subject: [PATCH 02/35] Document the adaptive random ray volume estimator Methods-guide derivation and rationale for the strong-source (kappa) test and the end-of-inactive converged-negative demotion, a pros/cons and recommendations table in the user's guide, and the volume_estimator entry in the settings specification, all reflecting adaptive as the default. Co-Authored-By: Claude Fable 5 --- docs/source/io_formats/settings.rst | 3 +- docs/source/methods/random_ray.rst | 48 ++++++++++++++++++++++++--- docs/source/usersguide/random_ray.rst | 46 ++++++++++++++++++++++++- 3 files changed, 90 insertions(+), 7 deletions(-) diff --git a/docs/source/io_formats/settings.rst b/docs/source/io_formats/settings.rst index fb02159169e..32d9e83f60e 100644 --- a/docs/source/io_formats/settings.rst +++ b/docs/source/io_formats/settings.rst @@ -626,7 +626,8 @@ found in the :ref:`random ray user guide `. :volume_estimator: Specifies choice of volume estimator for the random ray solver. Options - are 'naive', 'simulation_averaged', or 'hybrid'. The default is 'hybrid'. + are 'naive', 'simulation_averaged', 'hybrid', or 'adaptive'. The default is + 'adaptive'. *Default*: None diff --git a/docs/source/methods/random_ray.rst b/docs/source/methods/random_ray.rst index 8bc2a0a1bf5..df3c54cefcc 100644 --- a/docs/source/methods/random_ray.rst +++ b/docs/source/methods/random_ray.rst @@ -511,19 +511,57 @@ when using the naive estimator, though at the cost of a notable increase in variance. Empirical testing reveals that on most eigenvalue problems, the simulation averaged estimator does win out overall in numerical performance, as a much coarser quadrature can be used resulting in faster runtimes overall. -Thus, OpenMC uses the simulation averaged estimator as default in its random ray -mode for eigenvalue solves. +Thus, the simulation averaged estimator is generally preferred over the naive +estimator for eigenvalue solves. OpenMC also features a "hybrid" volume estimator that uses the naive estimator for all regions containing an external (fixed) source term. For all other source regions, the "simulation averaged" estimator is used. This typically achieves a best of both worlds result, with the benefits of the low bias simulation averaged estimator in most regions, while preventing instability and/or large biases in regions -with external source terms via use of the naive estimator. In general, it is -recommended to use the "hybrid" estimator, which is the default method used -in OpenMC. If instability is encountered despite high ray densities, then +with external source terms via use of the naive estimator. The "hybrid" +estimator was previously the default in OpenMC; it has been superseded by the +"adaptive" estimator (described below), which generalizes it and is now the +default. If instability is encountered despite high ray densities, then the naive estimator may be preferable. +OpenMC also features an "adaptive" volume estimator that generalizes the +hybrid estimator. Rather than selecting the estimator from the presence of an +external source alone, it uses the simulation averaged estimator by default and +falls back to the naive estimator (and the previous-iteration miss treatment) +on a per-region basis wherever the simulation averaged estimator is prone to +instability. The fallback is triggered by any of the following: a reduced +source that greatly exceeds the region's scalar flux (a source sustained by an +external or in-scatter contribution rather than by the local flux), a reduced +source that is itself negative (which can occur under transport-corrected cross +sections, whose negative within-group scattering term can drive the reduced +source below zero even for a non-negative flux), a hit-starved region, and a +region whose flux converges to a negative value. + +The first three conditions are evaluated each iteration from already-resident +data. The negative-flux condition is instead decided once, at the transition +from the inactive to the active batches: the unmodified simulation averaged +estimator is run throughout the inactive phase while each region's flux is +accumulated, and any region whose accumulated (and therefore noise-averaged) +flux is negative is demoted to the naive estimator for all of the active +batches. Deferring the decision to the sign of the converged estimate -- rather +than reacting to individual per-iteration negatives -- avoids the upward bias +that repairing or demoting on isolated fluctuations would introduce by clipping +only the lower tail of the estimator's noise distribution; regions that are +merely noisy but average non-negative retain the unbiased simulation averaged +estimator. The trade-off is that non-negative fluxes are no longer strictly +enforced in every active iteration, so a small number of near-zero regions may +register slightly negative in the active tally; in variance reduction workflows +these are discarded by the weight-window generator, which ignores non-positive +fluxes. + +Whereas the hybrid estimator guards only regions with explicit external +sources, the adaptive estimator also catches the optically thin regions of +fixed source problems where the simulation averaged and hybrid estimators can +otherwise develop persistent negative fluxes. It is the default estimator in +OpenMC, and is particularly beneficial for fixed source and shielding problems +that exhibit such instability. + A table that summarizes the pros and cons, as well as recommendations for different use cases, is given in the :ref:`volume estimators` section of the user guide. diff --git a/docs/source/usersguide/random_ray.rst b/docs/source/usersguide/random_ray.rst index 06d4e50ed7f..d0ff86cd8c4 100644 --- a/docs/source/usersguide/random_ray.rst +++ b/docs/source/usersguide/random_ray.rst @@ -1049,7 +1049,7 @@ following methods are currently available in OpenMC: unstable - * Biased estimator * Requires more rays or longer active ray length to mitigate bias - * - ``hybrid`` (default) + * - ``hybrid`` - Applies the naive estimator to all cells that contain an external (fixed) source contribution. Applies the simulation averaged estimator to all other cells. @@ -1058,6 +1058,32 @@ following methods are currently available in OpenMC: * Stability of the naive estimator in cells with fixed sources - * Can lead to slightly negative fluxes in cells where the simulation averaged estimator is used + * - ``adaptive`` (default) + - Generalizes the hybrid estimator. Uses the simulation averaged estimator + by default, but falls back to the naive estimator (and the + previous-iteration miss treatment) wherever it is needed for stability: + cells whose reduced source greatly exceeds their flux (a strong external + or in-scatter source), cells whose reduced source is itself negative + (possible under transport-corrected cross sections), hit-starved cells, + and cells whose accumulated flux is negative at the end of the inactive + phase. This last demotion is a one-shot decision: the unmodified + simulation averaged estimator runs throughout the inactive phase, and any + cell whose converged (accumulated) flux is negative -- genuinely + negative rather than merely noisy -- is demoted to the naive estimator + for all of the active batches. The decision is made automatically from + each cell's behavior during the run, with no per-iteration rescue or + positivity floor. + - * Retains the low bias of the simulation averaged estimator wherever it + is well behaved + * Eliminates the negative-flux instabilities that the simulation averaged + and hybrid estimators can exhibit in optically thin, in-scatter-fed + fixed source problems + * No parameters to tune + - * Does not strictly guarantee non-negative active-phase fluxes: a few + near-zero cells can still fluctuate slightly negative by statistical + chance (these are discarded downstream by the weight-window + generator, which ignores non-positive fluxes) + * Requires inactive batches in order to make the demotion decision These estimators can be selected by setting the ``volume_estimator`` field in the :attr:`openmc.Settings.random_ray` dictionary. For example, to use the naive @@ -1067,6 +1093,24 @@ estimator, the following code would be used: settings.random_ray['volume_estimator'] = 'naive' +The ``adaptive`` estimator is the default, as it gives reliable behavior out of +the box across problem types. It is especially valuable for fixed source and +shielding problems, where the ``hybrid`` and ``simulation_averaged`` estimators +can otherwise produce negative fluxes or numerical instability. This commonly occurs in optically thin, +scattering- or streaming-dominated regions (for example, the air- or +void-filled regions of a shielding model), where a small number of cells can +develop persistent negative fluxes that degrade tally results and, in +variance reduction workflows, the quality of generated weight windows. The +adaptive estimator detects and stabilizes those cells automatically while +leaving the rest of the problem on the low-bias simulation averaged estimator. +Because the negative-flux demotion is decided once, from each cell's accumulated +(converged) flux at the end of the inactive phase rather than from individual +per-iteration negatives, it avoids the small upward bias that per-iteration +demotion can introduce in cells that are noisy but not genuinely negative. The +trade-off is that it does not strictly guarantee non-negative fluxes in every +active-phase cell; the rare near-zero cells that fluctuate negative are filtered +out by the weight-window generator, which discards non-positive fluxes. + ----------------- Adjoint Flux Mode ----------------- From e19f7711e0b4452e72896e0faa110ef9850dc2ce Mon Sep 17 00:00:00 2001 From: John Tramm Date: Thu, 9 Jul 2026 21:19:02 +0000 Subject: [PATCH 03/35] Add regression coverage for the adaptive volume estimator New reference cases: adaptive on the three-region cube (flat and linear source shapes); a deliberately ray-starved adaptive case (~20% miss rate) that exercises every mechanism at once (strong-source demotion, hit-starved demotion, end-of-inactive converged-negative demotion, and the previous-flux miss treatment); the transport-corrected (P0) pin cell under adaptive, whose negative within-group scattering exercises the negative-reduced-source treatment; a ray-starved fixed source adjoint case whose second (adjoint) solve makes real demotion decisions; and a ray-starved subdivided eigenvalue case engaging the demotion machinery in eigenvalue mode. A unit test runs the same model twice through openmc.lib in one process and asserts the reported estimator both times, guarding the default restored by openmc_finalize_random_ray(). The existing random ray regression tests that relied on the implicit default are pinned to hybrid so their reference results are byte-for-byte unchanged (only test.py and the recorded inputs change), also avoiding churn in the planned follow-up that changes the adaptive fallback estimator. Co-Authored-By: Claude Fable 5 --- .../adaptive_starved/inputs_true.dat | 248 ++++++++++++++++++ .../adaptive_starved/results_true.dat | 9 + .../random_ray_adjoint_fixed_source/test.py | 20 ++ .../fs/inputs_true.dat | 1 + .../random_ray_cell_density/test.py | 1 + .../adaptive/inputs_true.dat | 68 +++++ .../adaptive/results_true.dat | 2 + .../inputs_true.dat | 1 + .../random_ray_diagonal_stabilization/test.py | 23 +- .../cell/inputs_true.dat | 1 + .../material/inputs_true.dat | 1 + .../random_ray_fixed_source_domain/test.py | 1 + .../universe/inputs_true.dat | 1 + .../linear/inputs_true.dat | 1 + .../linear_xy/inputs_true.dat | 1 + .../random_ray_fixed_source_linear/test.py | 1 + .../flat/inputs_true.dat | 1 + .../linear/inputs_true.dat | 1 + .../random_ray_fixed_source_mesh/test.py | 1 + .../False/inputs_true.dat | 1 + .../True/inputs_true.dat | 1 + .../test.py | 1 + .../flat/inputs_true.dat | 1 + .../linear_xy/inputs_true.dat | 1 + .../test.py | 1 + .../adaptive_starved/inputs_true.dat | 122 +++++++++ .../adaptive_starved/results_true.dat | 171 ++++++++++++ .../random_ray_k_eff_mesh/inputs_true.dat | 1 + .../random_ray_k_eff_mesh/test.py | 30 ++- .../random_ray_low_density/inputs_true.dat | 1 + .../random_ray_low_density/test.py | 1 + .../inputs_true.dat | 1 + .../random_ray_point_source_locator/test.py | 1 + .../random_ray_void/flat/inputs_true.dat | 1 + .../random_ray_void/linear/inputs_true.dat | 1 + .../regression_tests/random_ray_void/test.py | 1 + .../adaptive/inputs_true.dat | 247 +++++++++++++++++ .../adaptive/results_true.dat | 9 + .../adaptive_starved/inputs_true.dat | 247 +++++++++++++++++ .../adaptive_starved/results_true.dat | 9 + .../random_ray_volume_estimator/test.py | 22 +- .../adaptive/inputs_true.dat | 248 ++++++++++++++++++ .../adaptive/results_true.dat | 9 + .../test.py | 3 +- .../test_random_ray_default_persistence.py | 35 +++ 45 files changed, 1542 insertions(+), 7 deletions(-) create mode 100644 tests/regression_tests/random_ray_adjoint_fixed_source/adaptive_starved/inputs_true.dat create mode 100644 tests/regression_tests/random_ray_adjoint_fixed_source/adaptive_starved/results_true.dat create mode 100644 tests/regression_tests/random_ray_diagonal_stabilization/adaptive/inputs_true.dat create mode 100644 tests/regression_tests/random_ray_diagonal_stabilization/adaptive/results_true.dat create mode 100644 tests/regression_tests/random_ray_k_eff_mesh/adaptive_starved/inputs_true.dat create mode 100644 tests/regression_tests/random_ray_k_eff_mesh/adaptive_starved/results_true.dat create mode 100644 tests/regression_tests/random_ray_volume_estimator/adaptive/inputs_true.dat create mode 100644 tests/regression_tests/random_ray_volume_estimator/adaptive/results_true.dat create mode 100644 tests/regression_tests/random_ray_volume_estimator/adaptive_starved/inputs_true.dat create mode 100644 tests/regression_tests/random_ray_volume_estimator/adaptive_starved/results_true.dat create mode 100644 tests/regression_tests/random_ray_volume_estimator_linear/adaptive/inputs_true.dat create mode 100644 tests/regression_tests/random_ray_volume_estimator_linear/adaptive/results_true.dat create mode 100644 tests/unit_tests/test_random_ray_default_persistence.py diff --git a/tests/regression_tests/random_ray_adjoint_fixed_source/adaptive_starved/inputs_true.dat b/tests/regression_tests/random_ray_adjoint_fixed_source/adaptive_starved/inputs_true.dat new file mode 100644 index 00000000000..55f004b680b --- /dev/null +++ b/tests/regression_tests/random_ray_adjoint_fixed_source/adaptive_starved/inputs_true.dat @@ -0,0 +1,248 @@ + + + + mgxs.h5 + + + + + + + + + + + + + + + + + + + + + 2.5 2.5 2.5 + 12 12 12 + 0.0 0.0 0.0 + +3 3 3 3 3 3 3 3 3 3 3 3 +3 3 3 3 3 3 3 3 3 3 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +1 1 2 2 2 2 2 2 2 2 3 3 +1 1 2 2 2 2 2 2 2 2 3 3 + +3 3 3 3 3 3 3 3 3 3 3 3 +3 3 3 3 3 3 3 3 3 3 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +1 1 2 2 2 2 2 2 2 2 3 3 +1 1 2 2 2 2 2 2 2 2 3 3 + +3 3 3 3 3 3 3 3 3 3 3 3 +3 3 3 3 3 3 3 3 3 3 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 + +3 3 3 3 3 3 3 3 3 3 3 3 +3 3 3 3 3 3 3 3 3 3 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 + +3 3 3 3 3 3 3 3 3 3 3 3 +3 3 3 3 3 3 3 3 3 3 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 + +3 3 3 3 3 3 3 3 3 3 3 3 +3 3 3 3 3 3 3 3 3 3 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 + +3 3 3 3 3 3 3 3 3 3 3 3 +3 3 3 3 3 3 3 3 3 3 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 + +3 3 3 3 3 3 3 3 3 3 3 3 +3 3 3 3 3 3 3 3 3 3 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 + +3 3 3 3 3 3 3 3 3 3 3 3 +3 3 3 3 3 3 3 3 3 3 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 + +3 3 3 3 3 3 3 3 3 3 3 3 +3 3 3 3 3 3 3 3 3 3 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 + +3 3 3 3 3 3 3 3 3 3 3 3 +3 3 3 3 3 3 3 3 3 3 3 3 +3 3 3 3 3 3 3 3 3 3 3 3 +3 3 3 3 3 3 3 3 3 3 3 3 +3 3 3 3 3 3 3 3 3 3 3 3 +3 3 3 3 3 3 3 3 3 3 3 3 +3 3 3 3 3 3 3 3 3 3 3 3 +3 3 3 3 3 3 3 3 3 3 3 3 +3 3 3 3 3 3 3 3 3 3 3 3 +3 3 3 3 3 3 3 3 3 3 3 3 +3 3 3 3 3 3 3 3 3 3 3 3 +3 3 3 3 3 3 3 3 3 3 3 3 + +3 3 3 3 3 3 3 3 3 3 3 3 +3 3 3 3 3 3 3 3 3 3 3 3 +3 3 3 3 3 3 3 3 3 3 3 3 +3 3 3 3 3 3 3 3 3 3 3 3 +3 3 3 3 3 3 3 3 3 3 3 3 +3 3 3 3 3 3 3 3 3 3 3 3 +3 3 3 3 3 3 3 3 3 3 3 3 +3 3 3 3 3 3 3 3 3 3 3 3 +3 3 3 3 3 3 3 3 3 3 3 3 +3 3 3 3 3 3 3 3 3 3 3 3 +3 3 3 3 3 3 3 3 3 3 3 3 +3 3 3 3 3 3 3 3 3 3 3 3 + + + + + + + + + + fixed source + 10 + 40 + 20 + + + 100.0 1.0 + + + universe + 1 + + + multi-group + + 500.0 + 100.0 + + + + 0.0 0.0 0.0 30.0 30.0 30.0 + + + + true + true + adaptive + + + + + 1 + + + 2 + + + 3 + + + 3 + flux + tracklength + + + 2 + flux + tracklength + + + 1 + flux + tracklength + + + diff --git a/tests/regression_tests/random_ray_adjoint_fixed_source/adaptive_starved/results_true.dat b/tests/regression_tests/random_ray_adjoint_fixed_source/adaptive_starved/results_true.dat new file mode 100644 index 00000000000..c09f6029037 --- /dev/null +++ b/tests/regression_tests/random_ray_adjoint_fixed_source/adaptive_starved/results_true.dat @@ -0,0 +1,9 @@ +tally 1: +4.182567E+06 +7.270666E+12 +tally 2: +8.529692E+06 +3.890832E+12 +tally 3: +1.794581E+07 +1.614417E+13 diff --git a/tests/regression_tests/random_ray_adjoint_fixed_source/test.py b/tests/regression_tests/random_ray_adjoint_fixed_source/test.py index 6c2790fa093..8efb2e34faf 100644 --- a/tests/regression_tests/random_ray_adjoint_fixed_source/test.py +++ b/tests/regression_tests/random_ray_adjoint_fixed_source/test.py @@ -1,5 +1,7 @@ import os +import openmc +from openmc.utility_funcs import change_directory from openmc.examples import random_ray_three_region_cube from tests.testing_harness import TolerantPyAPITestHarness @@ -20,3 +22,21 @@ def test_random_ray_adjoint_fixed_source(): model.settings.particles = 500 harness = MGXSTestHarness('statepoint.10.h5', model) harness.main() + + +def test_random_ray_adjoint_fixed_source_adaptive_starved(): + # Ray-starved adaptive case (~20% forward / ~40% adjoint miss rate): the + # adjoint (second) solve makes real end-of-inactive demotion decisions on + # its own accumulated flux, guarding both the adaptive machinery in + # adjoint mode and the clearing of the forward solve's accumulated flux + # between the two solves (which would otherwise swamp the demotion test). + with change_directory('adaptive_starved'): + openmc.reset_auto_ids() + model = random_ray_three_region_cube() + model.settings.random_ray['adjoint'] = True + model.settings.random_ray['volume_estimator'] = 'adaptive' + model.settings.particles = 10 + model.settings.inactive = 20 + model.settings.batches = 40 + harness = MGXSTestHarness('statepoint.40.h5', model) + harness.main() diff --git a/tests/regression_tests/random_ray_cell_density/fs/inputs_true.dat b/tests/regression_tests/random_ray_cell_density/fs/inputs_true.dat index f369bae89f3..9a578087213 100644 --- a/tests/regression_tests/random_ray_cell_density/fs/inputs_true.dat +++ b/tests/regression_tests/random_ray_cell_density/fs/inputs_true.dat @@ -215,6 +215,7 @@ true + hybrid diff --git a/tests/regression_tests/random_ray_cell_density/test.py b/tests/regression_tests/random_ray_cell_density/test.py index 48ebe0baaa2..9e77b4c8d35 100644 --- a/tests/regression_tests/random_ray_cell_density/test.py +++ b/tests/regression_tests/random_ray_cell_density/test.py @@ -39,5 +39,6 @@ def test_random_ray_basic(run_mode): cell.density = 1e3 # Gold file was generated with manually scaled source cross sections. + model.settings.random_ray['volume_estimator'] = 'hybrid' harness = MGXSTestHarness('statepoint.10.h5', model) harness.main() diff --git a/tests/regression_tests/random_ray_diagonal_stabilization/adaptive/inputs_true.dat b/tests/regression_tests/random_ray_diagonal_stabilization/adaptive/inputs_true.dat new file mode 100644 index 00000000000..8012b6c76b5 --- /dev/null +++ b/tests/regression_tests/random_ray_diagonal_stabilization/adaptive/inputs_true.dat @@ -0,0 +1,68 @@ + + + + mgxs.h5 + + + + + + + + + + + + + + + + + + + + + + + + + + eigenvalue + 100 + 20 + 15 + + + -0.63 -0.63 -1 0.63 0.63 1 + + + true + + + multi-group + + + + + -0.63 -0.63 -1.0 0.63 0.63 1.0 + + + + 30.0 + 150.0 + + + + + + linear + 0.5 + adaptive + + + 2 2 + -0.63 -0.63 + 0.63 0.63 + + + diff --git a/tests/regression_tests/random_ray_diagonal_stabilization/adaptive/results_true.dat b/tests/regression_tests/random_ray_diagonal_stabilization/adaptive/results_true.dat new file mode 100644 index 00000000000..e344de949d4 --- /dev/null +++ b/tests/regression_tests/random_ray_diagonal_stabilization/adaptive/results_true.dat @@ -0,0 +1,2 @@ +k-combined: +7.135560E-01 1.421126E-02 diff --git a/tests/regression_tests/random_ray_diagonal_stabilization/inputs_true.dat b/tests/regression_tests/random_ray_diagonal_stabilization/inputs_true.dat index 0ea8c017760..4d48f151a51 100644 --- a/tests/regression_tests/random_ray_diagonal_stabilization/inputs_true.dat +++ b/tests/regression_tests/random_ray_diagonal_stabilization/inputs_true.dat @@ -57,6 +57,7 @@ linear 0.5 + hybrid 2 2 diff --git a/tests/regression_tests/random_ray_diagonal_stabilization/test.py b/tests/regression_tests/random_ray_diagonal_stabilization/test.py index 8d36e1d2581..e53effe14b9 100644 --- a/tests/regression_tests/random_ray_diagonal_stabilization/test.py +++ b/tests/regression_tests/random_ray_diagonal_stabilization/test.py @@ -1,6 +1,8 @@ import os +import openmc from openmc.examples import pwr_pin_cell +from openmc.utility_funcs import change_directory from openmc import RegularMesh from tests.testing_harness import TolerantPyAPITestHarness @@ -14,7 +16,7 @@ def _cleanup(self): os.remove(f) -def test_random_ray_diagonal_stabilization(): +def _build_model(): # Start with a normal continuous energy model model = pwr_pin_cell() @@ -57,5 +59,24 @@ def test_random_ray_diagonal_stabilization(): model.settings.inactive = 15 model.settings.batches = 20 + return model + + +def test_random_ray_diagonal_stabilization(): + model = _build_model() + model.settings.random_ray['volume_estimator'] = 'hybrid' harness = MGXSTestHarness('statepoint.20.h5', model) harness.main() + + +def test_random_ray_diagonal_stabilization_adaptive(): + # The transport-corrected (P0) library's negative within-group scattering + # drives some reduced sources negative, which the adaptive estimator must + # handle through its negative-source (strong) treatment and its + # end-of-inactive demotion; this case pins that interplay. + with change_directory('adaptive'): + openmc.reset_auto_ids() + model = _build_model() + model.settings.random_ray['volume_estimator'] = 'adaptive' + harness = MGXSTestHarness('statepoint.20.h5', model) + harness.main() diff --git a/tests/regression_tests/random_ray_fixed_source_domain/cell/inputs_true.dat b/tests/regression_tests/random_ray_fixed_source_domain/cell/inputs_true.dat index d650bbaf95c..d2fbd6d2247 100644 --- a/tests/regression_tests/random_ray_fixed_source_domain/cell/inputs_true.dat +++ b/tests/regression_tests/random_ray_fixed_source_domain/cell/inputs_true.dat @@ -215,6 +215,7 @@ true + hybrid diff --git a/tests/regression_tests/random_ray_fixed_source_domain/material/inputs_true.dat b/tests/regression_tests/random_ray_fixed_source_domain/material/inputs_true.dat index 98a51add1f3..bad8edb0ceb 100644 --- a/tests/regression_tests/random_ray_fixed_source_domain/material/inputs_true.dat +++ b/tests/regression_tests/random_ray_fixed_source_domain/material/inputs_true.dat @@ -215,6 +215,7 @@ true + hybrid diff --git a/tests/regression_tests/random_ray_fixed_source_domain/test.py b/tests/regression_tests/random_ray_fixed_source_domain/test.py index 5885a92009a..5b54871b780 100644 --- a/tests/regression_tests/random_ray_fixed_source_domain/test.py +++ b/tests/regression_tests/random_ray_fixed_source_domain/test.py @@ -47,5 +47,6 @@ def test_random_ray_fixed_source(domain_type): constraints['domain_type'] = 'universe' constraints['domain_ids'] = [universe.id] + model.settings.random_ray['volume_estimator'] = 'hybrid' harness = MGXSTestHarness('statepoint.10.h5', model) harness.main() diff --git a/tests/regression_tests/random_ray_fixed_source_domain/universe/inputs_true.dat b/tests/regression_tests/random_ray_fixed_source_domain/universe/inputs_true.dat index 20deba664bd..4d1af46b121 100644 --- a/tests/regression_tests/random_ray_fixed_source_domain/universe/inputs_true.dat +++ b/tests/regression_tests/random_ray_fixed_source_domain/universe/inputs_true.dat @@ -215,6 +215,7 @@ true + hybrid diff --git a/tests/regression_tests/random_ray_fixed_source_linear/linear/inputs_true.dat b/tests/regression_tests/random_ray_fixed_source_linear/linear/inputs_true.dat index 2268d82c391..dd11567f69d 100644 --- a/tests/regression_tests/random_ray_fixed_source_linear/linear/inputs_true.dat +++ b/tests/regression_tests/random_ray_fixed_source_linear/linear/inputs_true.dat @@ -216,6 +216,7 @@ true linear + hybrid diff --git a/tests/regression_tests/random_ray_fixed_source_linear/linear_xy/inputs_true.dat b/tests/regression_tests/random_ray_fixed_source_linear/linear_xy/inputs_true.dat index fe95baa7bb5..74a7a0b7f4d 100644 --- a/tests/regression_tests/random_ray_fixed_source_linear/linear_xy/inputs_true.dat +++ b/tests/regression_tests/random_ray_fixed_source_linear/linear_xy/inputs_true.dat @@ -216,6 +216,7 @@ true linear_xy + hybrid diff --git a/tests/regression_tests/random_ray_fixed_source_linear/test.py b/tests/regression_tests/random_ray_fixed_source_linear/test.py index 99211024e6e..b0532a458bb 100644 --- a/tests/regression_tests/random_ray_fixed_source_linear/test.py +++ b/tests/regression_tests/random_ray_fixed_source_linear/test.py @@ -25,5 +25,6 @@ def test_random_ray_fixed_source_linear(shape): model.settings.random_ray['source_shape'] = shape model.settings.inactive = 20 model.settings.batches = 40 + model.settings.random_ray['volume_estimator'] = 'hybrid' harness = MGXSTestHarness('statepoint.40.h5', model) harness.main() diff --git a/tests/regression_tests/random_ray_fixed_source_mesh/flat/inputs_true.dat b/tests/regression_tests/random_ray_fixed_source_mesh/flat/inputs_true.dat index a5632ece960..6b0eaaab6c7 100644 --- a/tests/regression_tests/random_ray_fixed_source_mesh/flat/inputs_true.dat +++ b/tests/regression_tests/random_ray_fixed_source_mesh/flat/inputs_true.dat @@ -227,6 +227,7 @@ flat + hybrid 24 24 24 diff --git a/tests/regression_tests/random_ray_fixed_source_mesh/linear/inputs_true.dat b/tests/regression_tests/random_ray_fixed_source_mesh/linear/inputs_true.dat index 9d22603c63c..d7a7d4f418b 100644 --- a/tests/regression_tests/random_ray_fixed_source_mesh/linear/inputs_true.dat +++ b/tests/regression_tests/random_ray_fixed_source_mesh/linear/inputs_true.dat @@ -227,6 +227,7 @@ linear + hybrid 24 24 24 diff --git a/tests/regression_tests/random_ray_fixed_source_mesh/test.py b/tests/regression_tests/random_ray_fixed_source_mesh/test.py index 0b93b2a7a6a..9e09959e904 100644 --- a/tests/regression_tests/random_ray_fixed_source_mesh/test.py +++ b/tests/regression_tests/random_ray_fixed_source_mesh/test.py @@ -49,5 +49,6 @@ def test_random_ray_fixed_source_mesh(shape): model.settings.inactive = 15 model.settings.batches = 30 + model.settings.random_ray['volume_estimator'] = 'hybrid' harness = MGXSTestHarness('statepoint.30.h5', model) harness.main() diff --git a/tests/regression_tests/random_ray_fixed_source_normalization/False/inputs_true.dat b/tests/regression_tests/random_ray_fixed_source_normalization/False/inputs_true.dat index de941f10fbb..ffe38772386 100644 --- a/tests/regression_tests/random_ray_fixed_source_normalization/False/inputs_true.dat +++ b/tests/regression_tests/random_ray_fixed_source_normalization/False/inputs_true.dat @@ -215,6 +215,7 @@ false + hybrid diff --git a/tests/regression_tests/random_ray_fixed_source_normalization/True/inputs_true.dat b/tests/regression_tests/random_ray_fixed_source_normalization/True/inputs_true.dat index 20deba664bd..4d1af46b121 100644 --- a/tests/regression_tests/random_ray_fixed_source_normalization/True/inputs_true.dat +++ b/tests/regression_tests/random_ray_fixed_source_normalization/True/inputs_true.dat @@ -215,6 +215,7 @@ true + hybrid diff --git a/tests/regression_tests/random_ray_fixed_source_normalization/test.py b/tests/regression_tests/random_ray_fixed_source_normalization/test.py index 3fa4ba2a63f..aa9823d0e27 100644 --- a/tests/regression_tests/random_ray_fixed_source_normalization/test.py +++ b/tests/regression_tests/random_ray_fixed_source_normalization/test.py @@ -23,5 +23,6 @@ def test_random_ray_fixed_source(normalize): model = random_ray_three_region_cube() model.settings.random_ray['volume_normalized_flux_tallies'] = normalize + model.settings.random_ray['volume_estimator'] = 'hybrid' harness = MGXSTestHarness('statepoint.10.h5', model) harness.main() diff --git a/tests/regression_tests/random_ray_fixed_source_subcritical/flat/inputs_true.dat b/tests/regression_tests/random_ray_fixed_source_subcritical/flat/inputs_true.dat index 943468a1095..8a992399806 100644 --- a/tests/regression_tests/random_ray_fixed_source_subcritical/flat/inputs_true.dat +++ b/tests/regression_tests/random_ray_fixed_source_subcritical/flat/inputs_true.dat @@ -119,6 +119,7 @@ false flat + hybrid diff --git a/tests/regression_tests/random_ray_fixed_source_subcritical/linear_xy/inputs_true.dat b/tests/regression_tests/random_ray_fixed_source_subcritical/linear_xy/inputs_true.dat index 650953c4b06..20e5095f8f2 100644 --- a/tests/regression_tests/random_ray_fixed_source_subcritical/linear_xy/inputs_true.dat +++ b/tests/regression_tests/random_ray_fixed_source_subcritical/linear_xy/inputs_true.dat @@ -119,6 +119,7 @@ false linear_xy + hybrid diff --git a/tests/regression_tests/random_ray_fixed_source_subcritical/test.py b/tests/regression_tests/random_ray_fixed_source_subcritical/test.py index e2f3cf17582..b8f08e63696 100644 --- a/tests/regression_tests/random_ray_fixed_source_subcritical/test.py +++ b/tests/regression_tests/random_ray_fixed_source_subcritical/test.py @@ -129,5 +129,6 @@ def test_random_ray_fixed_source_subcritical(shape): ######################################## # Run test + model.settings.random_ray['volume_estimator'] = 'hybrid' harness = MGXSTestHarness('statepoint.125.h5', model) harness.main() diff --git a/tests/regression_tests/random_ray_k_eff_mesh/adaptive_starved/inputs_true.dat b/tests/regression_tests/random_ray_k_eff_mesh/adaptive_starved/inputs_true.dat new file mode 100644 index 00000000000..0935ab4c9c0 --- /dev/null +++ b/tests/regression_tests/random_ray_k_eff_mesh/adaptive_starved/inputs_true.dat @@ -0,0 +1,122 @@ + + + + mgxs.h5 + + + + + + + + + + + + + + + + + + + + + + + + + + + + + 0.126 0.126 + 10 10 + -0.63 -0.63 + +3 3 3 3 3 3 3 3 3 3 +3 3 3 3 3 3 3 3 3 3 +3 3 3 3 3 3 3 3 3 3 +3 3 3 3 3 3 3 3 3 3 +3 3 3 3 3 3 3 3 3 3 +3 3 3 3 3 3 3 3 3 3 +3 3 3 3 3 3 3 3 3 3 +3 3 3 3 3 3 3 3 3 3 +3 3 3 3 3 3 3 3 3 3 +3 3 3 3 3 3 3 3 3 3 + + + 1.26 1.26 + 2 2 + -1.26 -1.26 + +2 2 +2 5 + + + + + + + + + + + + + + + + + + + + + eigenvalue + 25 + 40 + 20 + multi-group + + 100.0 + 20.0 + + + + -1.26 -1.26 -1 1.26 1.26 1 + + + + true + + + + + + adaptive + + + 40 40 + -1.26 -1.26 + 1.26 1.26 + + + + + 2 2 + -1.26 -1.26 + 1.26 1.26 + + + 1 + + + 1e-05 0.0635 10.0 100.0 1000.0 500000.0 1000000.0 20000000.0 + + + 1 2 + flux fission nu-fission + analog + + + diff --git a/tests/regression_tests/random_ray_k_eff_mesh/adaptive_starved/results_true.dat b/tests/regression_tests/random_ray_k_eff_mesh/adaptive_starved/results_true.dat new file mode 100644 index 00000000000..e019a69a3c9 --- /dev/null +++ b/tests/regression_tests/random_ray_k_eff_mesh/adaptive_starved/results_true.dat @@ -0,0 +1,171 @@ +k-combined: +1.107143E+00 1.651522E-02 +tally 1: +5.381525E+00 +1.457993E+00 +1.989739E+00 +1.995895E-01 +4.842627E+00 +1.182245E+00 +3.851210E+00 +7.436550E-01 +5.695198E-01 +1.627076E-02 +1.386097E+00 +9.637797E-02 +2.905390E+00 +4.222640E-01 +9.355466E-02 +4.378775E-04 +2.276933E-01 +2.593716E-03 +3.728182E+00 +6.951843E-01 +1.234728E-01 +7.626184E-04 +3.005082E-01 +4.517281E-03 +9.698270E+00 +4.703385E+00 +1.132059E-01 +6.408770E-04 +2.755238E-01 +3.796252E-03 +2.110508E+01 +2.227508E+01 +3.196621E-02 +5.110885E-05 +7.909814E-02 +3.129295E-04 +1.114270E+01 +6.213586E+00 +1.495587E-01 +1.120798E-03 +4.159900E-01 +8.671009E-03 +8.789785E+00 +3.891485E+00 +0.000000E+00 +0.000000E+00 +0.000000E+00 +0.000000E+00 +4.704887E+00 +1.109875E+00 +0.000000E+00 +0.000000E+00 +0.000000E+00 +0.000000E+00 +3.067148E+00 +4.705479E-01 +0.000000E+00 +0.000000E+00 +0.000000E+00 +0.000000E+00 +4.067592E+00 +8.274689E-01 +0.000000E+00 +0.000000E+00 +0.000000E+00 +0.000000E+00 +9.782491E+00 +4.785360E+00 +0.000000E+00 +0.000000E+00 +0.000000E+00 +0.000000E+00 +1.891642E+01 +1.789487E+01 +0.000000E+00 +0.000000E+00 +0.000000E+00 +0.000000E+00 +9.665523E+00 +4.680512E+00 +0.000000E+00 +0.000000E+00 +0.000000E+00 +0.000000E+00 +4.684837E+00 +1.105786E+00 +1.748910E+00 +1.543697E-01 +4.256497E+00 +9.143906E-01 +3.633611E+00 +6.621053E-01 +5.450791E-01 +1.490499E-02 +1.326614E+00 +8.828795E-02 +2.859882E+00 +4.091604E-01 +9.369162E-02 +4.391300E-04 +2.280267E-01 +2.601134E-03 +3.656451E+00 +6.687181E-01 +1.230419E-01 +7.573186E-04 +2.994593E-01 +4.485888E-03 +9.693266E+00 +4.698549E+00 +1.151230E-01 +6.627375E-04 +2.801899E-01 +3.925743E-03 +2.161501E+01 +2.336278E+01 +3.339306E-02 +5.577222E-05 +8.262879E-02 +3.414824E-04 +1.149136E+01 +6.606944E+00 +1.577251E-01 +1.246058E-03 +4.387044E-01 +9.640079E-03 +5.394007E+00 +1.465865E+00 +2.033657E+00 +2.084465E-01 +4.949515E+00 +1.234708E+00 +3.854200E+00 +7.449018E-01 +5.821747E-01 +1.699740E-02 +1.416897E+00 +1.006821E-01 +2.904577E+00 +4.220232E-01 +9.569122E-02 +4.580633E-04 +2.328933E-01 +2.713284E-03 +3.721192E+00 +6.925779E-01 +1.259829E-01 +7.939284E-04 +3.066171E-01 +4.702742E-03 +9.683481E+00 +4.688966E+00 +1.157040E-01 +6.694547E-04 +2.816039E-01 +3.965533E-03 +2.116973E+01 +2.241256E+01 +3.293792E-02 +5.426737E-05 +8.150257E-02 +3.322685E-04 +1.127462E+01 +6.361201E+00 +1.560968E-01 +1.221259E-03 +4.341752E-01 +9.448223E-03 diff --git a/tests/regression_tests/random_ray_k_eff_mesh/inputs_true.dat b/tests/regression_tests/random_ray_k_eff_mesh/inputs_true.dat index f6e9c8e3e71..2dc2577e393 100644 --- a/tests/regression_tests/random_ray_k_eff_mesh/inputs_true.dat +++ b/tests/regression_tests/random_ray_k_eff_mesh/inputs_true.dat @@ -93,6 +93,7 @@ + hybrid 40 40 diff --git a/tests/regression_tests/random_ray_k_eff_mesh/test.py b/tests/regression_tests/random_ray_k_eff_mesh/test.py index cffdaf8bb4c..6fa3b555b6d 100644 --- a/tests/regression_tests/random_ray_k_eff_mesh/test.py +++ b/tests/regression_tests/random_ray_k_eff_mesh/test.py @@ -1,6 +1,7 @@ import os import openmc +from openmc.utility_funcs import change_directory from openmc.examples import random_ray_lattice from tests.testing_harness import TolerantPyAPITestHarness @@ -27,10 +28,35 @@ def test_random_ray_k_eff_mesh(): mesh.dimension = (dim, dim) mesh.lower_left = (-pitch, -pitch) mesh.upper_right = (pitch, pitch) - + root = model.geometry.root_universe - + model.settings.random_ray['source_region_meshes'] = [(mesh, [root])] + model.settings.random_ray['volume_estimator'] = 'hybrid' harness = MGXSTestHarness('statepoint.10.h5', model) harness.main() + + +def test_random_ray_k_eff_mesh_adaptive_starved(): + # Ray-starved adaptive eigenvalue case (~1.2% miss rate): the subdivision + # mesh plus a low ray count engages the adaptive demotion machinery + # (strong-source and hit-starved regions, plus the end-of-inactive + # demotion decision and the previous-flux miss treatment) in eigenvalue + # mode, which the nominal 0%-miss eigenvalue tests never exercise. + with change_directory('adaptive_starved'): + openmc.reset_auto_ids() + model = random_ray_lattice() + pitch = 1.26 + mesh = openmc.RegularMesh() + mesh.dimension = (40, 40) + mesh.lower_left = (-pitch, -pitch) + mesh.upper_right = (pitch, pitch) + root = model.geometry.root_universe + model.settings.random_ray['source_region_meshes'] = [(mesh, [root])] + model.settings.random_ray['volume_estimator'] = 'adaptive' + model.settings.particles = 25 + model.settings.inactive = 20 + model.settings.batches = 40 + harness = MGXSTestHarness('statepoint.40.h5', model) + harness.main() diff --git a/tests/regression_tests/random_ray_low_density/inputs_true.dat b/tests/regression_tests/random_ray_low_density/inputs_true.dat index 20deba664bd..4d1af46b121 100644 --- a/tests/regression_tests/random_ray_low_density/inputs_true.dat +++ b/tests/regression_tests/random_ray_low_density/inputs_true.dat @@ -215,6 +215,7 @@ true + hybrid diff --git a/tests/regression_tests/random_ray_low_density/test.py b/tests/regression_tests/random_ray_low_density/test.py index 1b4ffb78183..22f68cca291 100644 --- a/tests/regression_tests/random_ray_low_density/test.py +++ b/tests/regression_tests/random_ray_low_density/test.py @@ -56,5 +56,6 @@ def test_random_ray_low_density(): [source_mat_data, void_mat_data, absorber_mat_data]) mg_cross_sections_file.export_to_hdf5() + model.settings.random_ray['volume_estimator'] = 'hybrid' harness = MGXSTestHarness('statepoint.10.h5', model) harness.main() diff --git a/tests/regression_tests/random_ray_point_source_locator/inputs_true.dat b/tests/regression_tests/random_ray_point_source_locator/inputs_true.dat index b4bd263f5ac..a66cd835c8b 100644 --- a/tests/regression_tests/random_ray_point_source_locator/inputs_true.dat +++ b/tests/regression_tests/random_ray_point_source_locator/inputs_true.dat @@ -219,6 +219,7 @@ + hybrid 30 30 30 diff --git a/tests/regression_tests/random_ray_point_source_locator/test.py b/tests/regression_tests/random_ray_point_source_locator/test.py index fd3d8a18fe9..c72e2b85699 100644 --- a/tests/regression_tests/random_ray_point_source_locator/test.py +++ b/tests/regression_tests/random_ray_point_source_locator/test.py @@ -40,5 +40,6 @@ def test_random_ray_point_source_locator(): model.settings.inactive = 15 model.settings.batches = 30 + model.settings.random_ray['volume_estimator'] = 'hybrid' harness = MGXSTestHarness('statepoint.30.h5', model) harness.main() diff --git a/tests/regression_tests/random_ray_void/flat/inputs_true.dat b/tests/regression_tests/random_ray_void/flat/inputs_true.dat index 66390c76661..39533ab0dd6 100644 --- a/tests/regression_tests/random_ray_void/flat/inputs_true.dat +++ b/tests/regression_tests/random_ray_void/flat/inputs_true.dat @@ -216,6 +216,7 @@ true flat + hybrid diff --git a/tests/regression_tests/random_ray_void/linear/inputs_true.dat b/tests/regression_tests/random_ray_void/linear/inputs_true.dat index 45228a03955..83d960a441c 100644 --- a/tests/regression_tests/random_ray_void/linear/inputs_true.dat +++ b/tests/regression_tests/random_ray_void/linear/inputs_true.dat @@ -216,6 +216,7 @@ true linear + hybrid diff --git a/tests/regression_tests/random_ray_void/test.py b/tests/regression_tests/random_ray_void/test.py index b48a7794d7e..cc45650175e 100644 --- a/tests/regression_tests/random_ray_void/test.py +++ b/tests/regression_tests/random_ray_void/test.py @@ -68,5 +68,6 @@ def test_random_ray_void(shape): tallies = openmc.Tallies([source_tally, void_tally, absorber_tally]) model.tallies = tallies + model.settings.random_ray['volume_estimator'] = 'hybrid' harness = MGXSTestHarness('statepoint.40.h5', model) harness.main() diff --git a/tests/regression_tests/random_ray_volume_estimator/adaptive/inputs_true.dat b/tests/regression_tests/random_ray_volume_estimator/adaptive/inputs_true.dat new file mode 100644 index 00000000000..8d4f8b02e2c --- /dev/null +++ b/tests/regression_tests/random_ray_volume_estimator/adaptive/inputs_true.dat @@ -0,0 +1,247 @@ + + + + mgxs.h5 + + + + + + + + + + + + + + + + + + + + + 2.5 2.5 2.5 + 12 12 12 + 0.0 0.0 0.0 + +3 3 3 3 3 3 3 3 3 3 3 3 +3 3 3 3 3 3 3 3 3 3 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +1 1 2 2 2 2 2 2 2 2 3 3 +1 1 2 2 2 2 2 2 2 2 3 3 + +3 3 3 3 3 3 3 3 3 3 3 3 +3 3 3 3 3 3 3 3 3 3 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +1 1 2 2 2 2 2 2 2 2 3 3 +1 1 2 2 2 2 2 2 2 2 3 3 + +3 3 3 3 3 3 3 3 3 3 3 3 +3 3 3 3 3 3 3 3 3 3 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 + +3 3 3 3 3 3 3 3 3 3 3 3 +3 3 3 3 3 3 3 3 3 3 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 + +3 3 3 3 3 3 3 3 3 3 3 3 +3 3 3 3 3 3 3 3 3 3 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 + +3 3 3 3 3 3 3 3 3 3 3 3 +3 3 3 3 3 3 3 3 3 3 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 + +3 3 3 3 3 3 3 3 3 3 3 3 +3 3 3 3 3 3 3 3 3 3 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 + +3 3 3 3 3 3 3 3 3 3 3 3 +3 3 3 3 3 3 3 3 3 3 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 + +3 3 3 3 3 3 3 3 3 3 3 3 +3 3 3 3 3 3 3 3 3 3 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 + +3 3 3 3 3 3 3 3 3 3 3 3 +3 3 3 3 3 3 3 3 3 3 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 + +3 3 3 3 3 3 3 3 3 3 3 3 +3 3 3 3 3 3 3 3 3 3 3 3 +3 3 3 3 3 3 3 3 3 3 3 3 +3 3 3 3 3 3 3 3 3 3 3 3 +3 3 3 3 3 3 3 3 3 3 3 3 +3 3 3 3 3 3 3 3 3 3 3 3 +3 3 3 3 3 3 3 3 3 3 3 3 +3 3 3 3 3 3 3 3 3 3 3 3 +3 3 3 3 3 3 3 3 3 3 3 3 +3 3 3 3 3 3 3 3 3 3 3 3 +3 3 3 3 3 3 3 3 3 3 3 3 +3 3 3 3 3 3 3 3 3 3 3 3 + +3 3 3 3 3 3 3 3 3 3 3 3 +3 3 3 3 3 3 3 3 3 3 3 3 +3 3 3 3 3 3 3 3 3 3 3 3 +3 3 3 3 3 3 3 3 3 3 3 3 +3 3 3 3 3 3 3 3 3 3 3 3 +3 3 3 3 3 3 3 3 3 3 3 3 +3 3 3 3 3 3 3 3 3 3 3 3 +3 3 3 3 3 3 3 3 3 3 3 3 +3 3 3 3 3 3 3 3 3 3 3 3 +3 3 3 3 3 3 3 3 3 3 3 3 +3 3 3 3 3 3 3 3 3 3 3 3 +3 3 3 3 3 3 3 3 3 3 3 3 + + + + + + + + + + fixed source + 90 + 10 + 5 + + + 100.0 1.0 + + + universe + 1 + + + multi-group + + 500.0 + 100.0 + + + + 0.0 0.0 0.0 30.0 30.0 30.0 + + + + true + adaptive + + + + + 1 + + + 2 + + + 3 + + + 3 + flux + tracklength + + + 2 + flux + tracklength + + + 1 + flux + tracklength + + + diff --git a/tests/regression_tests/random_ray_volume_estimator/adaptive/results_true.dat b/tests/regression_tests/random_ray_volume_estimator/adaptive/results_true.dat new file mode 100644 index 00000000000..274ca3db560 --- /dev/null +++ b/tests/regression_tests/random_ray_volume_estimator/adaptive/results_true.dat @@ -0,0 +1,9 @@ +tally 1: +5.934460E-01 +7.058893E-02 +tally 2: +3.209138E-02 +2.066116E-04 +tally 3: +2.096421E-03 +8.805020E-07 diff --git a/tests/regression_tests/random_ray_volume_estimator/adaptive_starved/inputs_true.dat b/tests/regression_tests/random_ray_volume_estimator/adaptive_starved/inputs_true.dat new file mode 100644 index 00000000000..33ab014d4c6 --- /dev/null +++ b/tests/regression_tests/random_ray_volume_estimator/adaptive_starved/inputs_true.dat @@ -0,0 +1,247 @@ + + + + mgxs.h5 + + + + + + + + + + + + + + + + + + + + + 2.5 2.5 2.5 + 12 12 12 + 0.0 0.0 0.0 + +3 3 3 3 3 3 3 3 3 3 3 3 +3 3 3 3 3 3 3 3 3 3 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +1 1 2 2 2 2 2 2 2 2 3 3 +1 1 2 2 2 2 2 2 2 2 3 3 + +3 3 3 3 3 3 3 3 3 3 3 3 +3 3 3 3 3 3 3 3 3 3 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +1 1 2 2 2 2 2 2 2 2 3 3 +1 1 2 2 2 2 2 2 2 2 3 3 + +3 3 3 3 3 3 3 3 3 3 3 3 +3 3 3 3 3 3 3 3 3 3 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 + +3 3 3 3 3 3 3 3 3 3 3 3 +3 3 3 3 3 3 3 3 3 3 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 + +3 3 3 3 3 3 3 3 3 3 3 3 +3 3 3 3 3 3 3 3 3 3 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 + +3 3 3 3 3 3 3 3 3 3 3 3 +3 3 3 3 3 3 3 3 3 3 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 + +3 3 3 3 3 3 3 3 3 3 3 3 +3 3 3 3 3 3 3 3 3 3 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 + +3 3 3 3 3 3 3 3 3 3 3 3 +3 3 3 3 3 3 3 3 3 3 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 + +3 3 3 3 3 3 3 3 3 3 3 3 +3 3 3 3 3 3 3 3 3 3 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 + +3 3 3 3 3 3 3 3 3 3 3 3 +3 3 3 3 3 3 3 3 3 3 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 + +3 3 3 3 3 3 3 3 3 3 3 3 +3 3 3 3 3 3 3 3 3 3 3 3 +3 3 3 3 3 3 3 3 3 3 3 3 +3 3 3 3 3 3 3 3 3 3 3 3 +3 3 3 3 3 3 3 3 3 3 3 3 +3 3 3 3 3 3 3 3 3 3 3 3 +3 3 3 3 3 3 3 3 3 3 3 3 +3 3 3 3 3 3 3 3 3 3 3 3 +3 3 3 3 3 3 3 3 3 3 3 3 +3 3 3 3 3 3 3 3 3 3 3 3 +3 3 3 3 3 3 3 3 3 3 3 3 +3 3 3 3 3 3 3 3 3 3 3 3 + +3 3 3 3 3 3 3 3 3 3 3 3 +3 3 3 3 3 3 3 3 3 3 3 3 +3 3 3 3 3 3 3 3 3 3 3 3 +3 3 3 3 3 3 3 3 3 3 3 3 +3 3 3 3 3 3 3 3 3 3 3 3 +3 3 3 3 3 3 3 3 3 3 3 3 +3 3 3 3 3 3 3 3 3 3 3 3 +3 3 3 3 3 3 3 3 3 3 3 3 +3 3 3 3 3 3 3 3 3 3 3 3 +3 3 3 3 3 3 3 3 3 3 3 3 +3 3 3 3 3 3 3 3 3 3 3 3 +3 3 3 3 3 3 3 3 3 3 3 3 + + + + + + + + + + fixed source + 10 + 40 + 20 + + + 100.0 1.0 + + + universe + 1 + + + multi-group + + 500.0 + 100.0 + + + + 0.0 0.0 0.0 30.0 30.0 30.0 + + + + true + adaptive + + + + + 1 + + + 2 + + + 3 + + + 3 + flux + tracklength + + + 2 + flux + tracklength + + + 1 + flux + tracklength + + + diff --git a/tests/regression_tests/random_ray_volume_estimator/adaptive_starved/results_true.dat b/tests/regression_tests/random_ray_volume_estimator/adaptive_starved/results_true.dat new file mode 100644 index 00000000000..b13c7172649 --- /dev/null +++ b/tests/regression_tests/random_ray_volume_estimator/adaptive_starved/results_true.dat @@ -0,0 +1,9 @@ +tally 1: +2.258230E+00 +2.672994E-01 +tally 2: +9.236733E-02 +5.894388E-04 +tally 3: +7.475196E-03 +3.603831E-06 diff --git a/tests/regression_tests/random_ray_volume_estimator/test.py b/tests/regression_tests/random_ray_volume_estimator/test.py index fba4bbbbe6d..23647eb3c39 100644 --- a/tests/regression_tests/random_ray_volume_estimator/test.py +++ b/tests/regression_tests/random_ray_volume_estimator/test.py @@ -18,13 +18,29 @@ def _cleanup(self): @pytest.mark.parametrize("estimator", ["hybrid", "simulation_averaged", - "naive" + "naive", + "adaptive", + "adaptive_starved" ]) def test_random_ray_volume_estimator(estimator): with change_directory(estimator): openmc.reset_auto_ids() model = random_ray_three_region_cube() - model.settings.random_ray['volume_estimator'] = estimator + if estimator == 'adaptive_starved': + # Deliberately ray-starved configuration (~20% miss rate): unlike + # the nominal cases, this exercises every adaptive mechanism at + # once -- the strong-source (kappa) demotion, the hit-starved + # demotion, the end-of-inactive converged-negative demotion, and + # the previous-flux miss treatment -- so changes to any of those + # code paths show up in this gold. + model.settings.random_ray['volume_estimator'] = 'adaptive' + model.settings.particles = 10 + model.settings.inactive = 20 + model.settings.batches = 40 + sp_name = 'statepoint.40.h5' + else: + model.settings.random_ray['volume_estimator'] = estimator + sp_name = 'statepoint.10.h5' - harness = MGXSTestHarness('statepoint.10.h5', model) + harness = MGXSTestHarness(sp_name, model) harness.main() diff --git a/tests/regression_tests/random_ray_volume_estimator_linear/adaptive/inputs_true.dat b/tests/regression_tests/random_ray_volume_estimator_linear/adaptive/inputs_true.dat new file mode 100644 index 00000000000..592a9fea69f --- /dev/null +++ b/tests/regression_tests/random_ray_volume_estimator_linear/adaptive/inputs_true.dat @@ -0,0 +1,248 @@ + + + + mgxs.h5 + + + + + + + + + + + + + + + + + + + + + 2.5 2.5 2.5 + 12 12 12 + 0.0 0.0 0.0 + +3 3 3 3 3 3 3 3 3 3 3 3 +3 3 3 3 3 3 3 3 3 3 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +1 1 2 2 2 2 2 2 2 2 3 3 +1 1 2 2 2 2 2 2 2 2 3 3 + +3 3 3 3 3 3 3 3 3 3 3 3 +3 3 3 3 3 3 3 3 3 3 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +1 1 2 2 2 2 2 2 2 2 3 3 +1 1 2 2 2 2 2 2 2 2 3 3 + +3 3 3 3 3 3 3 3 3 3 3 3 +3 3 3 3 3 3 3 3 3 3 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 + +3 3 3 3 3 3 3 3 3 3 3 3 +3 3 3 3 3 3 3 3 3 3 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 + +3 3 3 3 3 3 3 3 3 3 3 3 +3 3 3 3 3 3 3 3 3 3 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 + +3 3 3 3 3 3 3 3 3 3 3 3 +3 3 3 3 3 3 3 3 3 3 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 + +3 3 3 3 3 3 3 3 3 3 3 3 +3 3 3 3 3 3 3 3 3 3 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 + +3 3 3 3 3 3 3 3 3 3 3 3 +3 3 3 3 3 3 3 3 3 3 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 + +3 3 3 3 3 3 3 3 3 3 3 3 +3 3 3 3 3 3 3 3 3 3 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 + +3 3 3 3 3 3 3 3 3 3 3 3 +3 3 3 3 3 3 3 3 3 3 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 + +3 3 3 3 3 3 3 3 3 3 3 3 +3 3 3 3 3 3 3 3 3 3 3 3 +3 3 3 3 3 3 3 3 3 3 3 3 +3 3 3 3 3 3 3 3 3 3 3 3 +3 3 3 3 3 3 3 3 3 3 3 3 +3 3 3 3 3 3 3 3 3 3 3 3 +3 3 3 3 3 3 3 3 3 3 3 3 +3 3 3 3 3 3 3 3 3 3 3 3 +3 3 3 3 3 3 3 3 3 3 3 3 +3 3 3 3 3 3 3 3 3 3 3 3 +3 3 3 3 3 3 3 3 3 3 3 3 +3 3 3 3 3 3 3 3 3 3 3 3 + +3 3 3 3 3 3 3 3 3 3 3 3 +3 3 3 3 3 3 3 3 3 3 3 3 +3 3 3 3 3 3 3 3 3 3 3 3 +3 3 3 3 3 3 3 3 3 3 3 3 +3 3 3 3 3 3 3 3 3 3 3 3 +3 3 3 3 3 3 3 3 3 3 3 3 +3 3 3 3 3 3 3 3 3 3 3 3 +3 3 3 3 3 3 3 3 3 3 3 3 +3 3 3 3 3 3 3 3 3 3 3 3 +3 3 3 3 3 3 3 3 3 3 3 3 +3 3 3 3 3 3 3 3 3 3 3 3 +3 3 3 3 3 3 3 3 3 3 3 3 + + + + + + + + + + fixed source + 90 + 40 + 20 + + + 100.0 1.0 + + + universe + 1 + + + multi-group + + 500.0 + 100.0 + + + + 0.0 0.0 0.0 30.0 30.0 30.0 + + + + true + linear + adaptive + + + + + 1 + + + 2 + + + 3 + + + 3 + flux + tracklength + + + 2 + flux + tracklength + + + 1 + flux + tracklength + + + diff --git a/tests/regression_tests/random_ray_volume_estimator_linear/adaptive/results_true.dat b/tests/regression_tests/random_ray_volume_estimator_linear/adaptive/results_true.dat new file mode 100644 index 00000000000..917e237e4d8 --- /dev/null +++ b/tests/regression_tests/random_ray_volume_estimator_linear/adaptive/results_true.dat @@ -0,0 +1,9 @@ +tally 1: +2.347042E+00 +2.759340E-01 +tally 2: +1.091213E-01 +6.071243E-04 +tally 3: +7.311065E-03 +2.725278E-06 diff --git a/tests/regression_tests/random_ray_volume_estimator_linear/test.py b/tests/regression_tests/random_ray_volume_estimator_linear/test.py index 94a14f3ad3b..5b43224d566 100644 --- a/tests/regression_tests/random_ray_volume_estimator_linear/test.py +++ b/tests/regression_tests/random_ray_volume_estimator_linear/test.py @@ -18,7 +18,8 @@ def _cleanup(self): @pytest.mark.parametrize("estimator", ["hybrid", "simulation_averaged", - "naive" + "naive", + "adaptive" ]) def test_random_ray_volume_estimator_linear(estimator): with change_directory(estimator): diff --git a/tests/unit_tests/test_random_ray_default_persistence.py b/tests/unit_tests/test_random_ray_default_persistence.py new file mode 100644 index 00000000000..d59cfcb6a2d --- /dev/null +++ b/tests/unit_tests/test_random_ray_default_persistence.py @@ -0,0 +1,35 @@ +"""The random ray volume-estimator default must survive an in-process +finalize/re-initialize cycle (openmc.lib workflows such as iterative weight +window generation). openmc_finalize_random_ray() restores the built-in +defaults between runs; if it restores a different estimator than the static +default, the first and subsequent runs of a process silently use different +estimators. This test runs the same model twice through openmc.lib in one +process and checks the reported estimator both times.""" + +import openmc +import openmc.lib +from openmc.examples import random_ray_three_region_cube + + +def test_random_ray_default_estimator_persistence(run_in_tmpdir, capfd): + openmc.reset_auto_ids() + model = random_ray_three_region_cube() + model.settings.particles = 10 + model.settings.inactive = 2 + model.settings.batches = 4 + # No volume_estimator set: exercises the built-in default both runs + model.export_to_model_xml() + + reported = [] + for _ in range(2): + openmc.lib.init() + openmc.lib.run_random_ray() + openmc.lib.finalize() + out = capfd.readouterr().out + for line in out.splitlines(): + if 'Volume Estimator Type' in line: + reported.append(line.split('=')[-1].strip()) + + assert reported == ['Adaptive', 'Adaptive'], ( + f"default volume estimator changed across in-process reruns: " + f"{reported}") From 7c8a61b7383aa6f31c44f704423c83a7328f5d2a Mon Sep 17 00:00:00 2001 From: John Tramm Date: Thu, 9 Jul 2026 22:23:15 +0000 Subject: [PATCH 04/35] Apply the flat-source fallback to all adaptive demotions under linear sources Previously only strong-source (kappa) demotions zeroed their source gradients under linear source shapes; converged-negative demotions kept live tilts fitted from the statistics of regions whose accumulated flux is negative -- shape information with no meaning that only injects noise into exactly the cells already prone to negativity. Demotion is now uniform in effect: any demoted region uses the naive volume, the previous-flux miss treatment, and a flat source. On a coarse-ray streaming problem this removes a third of the negative flux tally bins (9,664 -> 6,429 of 42,875) with the total flux unchanged well within statistics. The end-of-run report line becomes 'Demoted -> Flat (linear)' covering the full flattened set. Co-Authored-By: Claude Fable 5 --- docs/source/methods/random_ray.rst | 13 ++++++++--- src/random_ray/linear_source_domain.cpp | 23 +++++++++++-------- src/random_ray/random_ray_simulation.cpp | 13 ++++++----- .../adaptive/results_true.dat | 2 +- .../adaptive/results_true.dat | 4 ++-- 5 files changed, 33 insertions(+), 22 deletions(-) diff --git a/docs/source/methods/random_ray.rst b/docs/source/methods/random_ray.rst index df3c54cefcc..63504421447 100644 --- a/docs/source/methods/random_ray.rst +++ b/docs/source/methods/random_ray.rst @@ -558,9 +558,16 @@ fluxes. Whereas the hybrid estimator guards only regions with explicit external sources, the adaptive estimator also catches the optically thin regions of fixed source problems where the simulation averaged and hybrid estimators can -otherwise develop persistent negative fluxes. It is the default estimator in -OpenMC, and is particularly beneficial for fixed source and shielding problems -that exhibit such instability. +otherwise develop persistent negative fluxes. When a linear source shape is in +use, demoted regions additionally revert to a flat source representation +(their source gradients are zeroed), extending the flat-source treatment +already applied to hit-starved regions: in a strong-source region the gradient +terms attenuate segments against the local rather than the flat source, +re-injecting per-iteration noise at the scale of the reduced source that the +volume choice cannot cancel, while in a converged-negative region the fitted +gradients carry no meaningful shape information. The adaptive estimator is the +default in OpenMC, and is particularly beneficial for fixed source and +shielding problems that exhibit such instability. A table that summarizes the pros and cons, as well as recommendations for different use cases, is given in the :ref:`volume diff --git a/src/random_ray/linear_source_domain.cpp b/src/random_ray/linear_source_domain.cpp index c6c5c5ac44b..b081ab0b3f7 100644 --- a/src/random_ray/linear_source_domain.cpp +++ b/src/random_ray/linear_source_domain.cpp @@ -112,18 +112,21 @@ void LinearSourceDomain::update_single_neutron_source(SourceRegionHandle& srh) } } - // Under the adaptive volume estimator, regions receiving the protected - // (naive volume) treatment for a strong inhomogeneous source also fall back - // to a flat source representation. In such regions the reduced source greatly - // exceeds the scalar flux, so the flat-source cancellation must be exact; the - // gradient terms attenuate segments against the local rather than the flat - // source, introducing per-iteration noise at the gradient scale that the - // volume choice cannot cancel. Zeroing the gradients there extends the - // existing flat-source fallback already applied to hit-starved (small) - // regions. + // Under the adaptive volume estimator, demoted regions also fall back to a + // flat source representation, extending the flat-source fallback already + // applied to hit-starved (small) regions so that demotion is uniform in + // effect. For strong-source regions the reduced source greatly exceeds the + // scalar flux, so the flat-source cancellation must be exact; the gradient + // terms attenuate segments against the local rather than the flat source, + // introducing per-iteration noise at the gradient scale that the volume + // choice cannot cancel. For converged-negative regions (demoted at the end + // of the inactive phase), the accumulated flux itself is negative, so the + // fitted gradients carry no meaningful shape information and only inject + // noise into cells already prone to negativity. if (volume_estimator_ == RandomRayVolumeEstimator::ADAPTIVE && material != MATERIAL_VOID && - region_has_strong_source(&srh.source(0), &srh.scalar_flux_old(0))) { + (srh.n_negative_fluxes() > 0 || + region_has_strong_source(&srh.source(0), &srh.scalar_flux_old(0)))) { for (int g = 0; g < negroups_; g++) { srh.source_gradients(g) = {0.0, 0.0, 0.0}; } diff --git a/src/random_ray/random_ray_simulation.cpp b/src/random_ray/random_ray_simulation.cpp index 34a4931bfa4..02a5cef59dc 100644 --- a/src/random_ray/random_ray_simulation.cpp +++ b/src/random_ray/random_ray_simulation.cpp @@ -634,13 +634,14 @@ void RandomRaySimulation::print_results_random_ray( domain_->n_final_demoted_, domain_->n_final_demoted_ * inv); fmt::print(" Hit-Starved (Small) = {} SRs ({:.4f}%)\n", domain_->n_final_small_, domain_->n_final_small_ * inv); - // For linear-source runs, the strong-source regions additionally have - // their source gradients zeroed, reverting them to a flat source. This is - // the same set as "Strong Source" above (both apply the kappa test to the - // same data), reported here as the linear -> flat fallback frequency. + // For linear-source runs, the strong-source and converged-negative + // regions additionally have their source gradients zeroed, reverting + // them to a flat source; this is the same data the volume switch uses, + // reported here as the linear -> flat fallback frequency. if (RandomRay::source_shape_ != RandomRaySourceShape::FLAT) { - fmt::print(" Strong Source -> Flat (linear) = {} SRs ({:.4f}%)\n", - domain_->n_final_strong_, domain_->n_final_strong_ * inv); + int64_t n_flat = domain_->n_final_strong_ + domain_->n_final_demoted_; + fmt::print(" Demoted -> Flat (linear) = {} SRs ({:.4f}%)\n", + n_flat, n_flat * inv); } } diff --git a/tests/regression_tests/random_ray_diagonal_stabilization/adaptive/results_true.dat b/tests/regression_tests/random_ray_diagonal_stabilization/adaptive/results_true.dat index e344de949d4..23606262414 100644 --- a/tests/regression_tests/random_ray_diagonal_stabilization/adaptive/results_true.dat +++ b/tests/regression_tests/random_ray_diagonal_stabilization/adaptive/results_true.dat @@ -1,2 +1,2 @@ k-combined: -7.135560E-01 1.421126E-02 +7.146436E-01 1.344871E-02 diff --git a/tests/regression_tests/random_ray_volume_estimator_linear/adaptive/results_true.dat b/tests/regression_tests/random_ray_volume_estimator_linear/adaptive/results_true.dat index 917e237e4d8..020742f66df 100644 --- a/tests/regression_tests/random_ray_volume_estimator_linear/adaptive/results_true.dat +++ b/tests/regression_tests/random_ray_volume_estimator_linear/adaptive/results_true.dat @@ -5,5 +5,5 @@ tally 2: 1.091213E-01 6.071243E-04 tally 3: -7.311065E-03 -2.725278E-06 +7.311013E-03 +2.725239E-06 From 70893a6fdee8310d50f80d6730140538980d6e3f Mon Sep 17 00:00:00 2001 From: John Tramm Date: Fri, 10 Jul 2026 01:54:24 +0000 Subject: [PATCH 05/35] Describe the estimator from the current code state in docs and comments Documentation and code comments now describe present behavior only, with change rationale left to commit messages: the methods guide states the default once (with the adaptive estimator) instead of narrating what it replaced, and wording that referenced mechanisms which do not exist in the code base ('per-iteration rescue', 'positivity floor', 'no longer enforced') is replaced with statements of what the solver actually does. The converged-negative demotion flag is renamed from n_negative_fluxes (a counter name on what is a 0/1 flag) to converged_negative, matching the report and documentation terminology. Co-Authored-By: Claude Fable 5 --- docs/source/methods/random_ray.rst | 10 ++++------ docs/source/usersguide/random_ray.rst | 4 ++-- include/openmc/random_ray/source_region.h | 16 ++++++++-------- src/random_ray/flat_source_domain.cpp | 12 ++++++------ src/random_ray/linear_source_domain.cpp | 2 +- src/random_ray/source_region.cpp | 10 +++++----- 6 files changed, 26 insertions(+), 28 deletions(-) diff --git a/docs/source/methods/random_ray.rst b/docs/source/methods/random_ray.rst index 63504421447..b6c47e1d774 100644 --- a/docs/source/methods/random_ray.rst +++ b/docs/source/methods/random_ray.rst @@ -519,11 +519,9 @@ for all regions containing an external (fixed) source term. For all other source regions, the "simulation averaged" estimator is used. This typically achieves a best of both worlds result, with the benefits of the low bias simulation averaged estimator in most regions, while preventing instability and/or large biases in regions -with external source terms via use of the naive estimator. The "hybrid" -estimator was previously the default in OpenMC; it has been superseded by the -"adaptive" estimator (described below), which generalizes it and is now the -default. If instability is encountered despite high ray densities, then -the naive estimator may be preferable. +with external source terms via use of the naive estimator. If instability is +encountered despite high ray densities, then the naive estimator may be +preferable. OpenMC also features an "adaptive" volume estimator that generalizes the hybrid estimator. Rather than selecting the estimator from the presence of an @@ -549,7 +547,7 @@ than reacting to individual per-iteration negatives -- avoids the upward bias that repairing or demoting on isolated fluctuations would introduce by clipping only the lower tail of the estimator's noise distribution; regions that are merely noisy but average non-negative retain the unbiased simulation averaged -estimator. The trade-off is that non-negative fluxes are no longer strictly +estimator. The trade-off is that non-negative fluxes are not strictly enforced in every active iteration, so a small number of near-zero regions may register slightly negative in the active tally; in variance reduction workflows these are discarded by the weight-window generator, which ignores non-positive diff --git a/docs/source/usersguide/random_ray.rst b/docs/source/usersguide/random_ray.rst index d0ff86cd8c4..cc09be05b7c 100644 --- a/docs/source/usersguide/random_ray.rst +++ b/docs/source/usersguide/random_ray.rst @@ -1071,8 +1071,8 @@ following methods are currently available in OpenMC: cell whose converged (accumulated) flux is negative -- genuinely negative rather than merely noisy -- is demoted to the naive estimator for all of the active batches. The decision is made automatically from - each cell's behavior during the run, with no per-iteration rescue or - positivity floor. + each cell's accumulated statistics; individual iterations are never + modified. - * Retains the low bias of the simulation averaged estimator wherever it is well behaved * Eliminates the negative-flux instabilities that the simulation averaged diff --git a/include/openmc/random_ray/source_region.h b/include/openmc/random_ray/source_region.h index dec5eaab330..3593712c8f5 100644 --- a/include/openmc/random_ray/source_region.h +++ b/include/openmc/random_ray/source_region.h @@ -149,7 +149,7 @@ class SourceRegionHandle { int* temperature_idx_; double* density_mult_; int* is_small_; - int* n_negative_fluxes_; + int* converged_negative_; int* n_hits_; int* birthday_; OpenMPMutex* lock_; @@ -206,8 +206,8 @@ class SourceRegionHandle { int& is_small() { return *is_small_; } const int is_small() const { return *is_small_; } - int& n_negative_fluxes() { return *n_negative_fluxes_; } - const int n_negative_fluxes() const { return *n_negative_fluxes_; } + int& converged_negative() { return *converged_negative_; } + const int converged_negative() const { return *converged_negative_; } int& n_hits() { return *n_hits_; } const int n_hits() const { return *n_hits_; } @@ -342,7 +342,7 @@ class SourceRegion { int external_source_present_ { 0}; //!< Is an external source present in this region? int is_small_ {0}; //!< Is it "small", receiving < 1.5 hits per iteration? - int n_negative_fluxes_ { + int converged_negative_ { 0}; //!< One-shot demotion flag (adaptive estimator only): set to 1 at the //!< end of the inactive phase when this region's accumulated flux was //!< negative, demoting it to the naive volume estimator for the active @@ -421,10 +421,10 @@ class SourceRegionContainer { int& is_small(int64_t sr) { return is_small_[sr]; } const int is_small(int64_t sr) const { return is_small_[sr]; } - int& n_negative_fluxes(int64_t sr) { return n_negative_fluxes_[sr]; } - const int n_negative_fluxes(int64_t sr) const + int& converged_negative(int64_t sr) { return converged_negative_[sr]; } + const int converged_negative(int64_t sr) const { - return n_negative_fluxes_[sr]; + return converged_negative_[sr]; } int& n_hits(int64_t sr) { return n_hits_[sr]; } @@ -658,7 +658,7 @@ class SourceRegionContainer { vector temperature_idx_; vector density_mult_; vector is_small_; - vector n_negative_fluxes_; + vector converged_negative_; vector n_hits_; vector mesh_; vector parent_sr_; diff --git a/src/random_ray/flat_source_domain.cpp b/src/random_ray/flat_source_domain.cpp index 9d90a875e37..f5e95f4fa38 100644 --- a/src/random_ray/flat_source_domain.cpp +++ b/src/random_ray/flat_source_domain.cpp @@ -107,7 +107,7 @@ void FlatSourceDomain::accumulate_iteration_flux() // others). Rather than reacting to per-iteration negatives, this estimator // runs the unmodified simulation-averaged update throughout the inactive phase // and decides demotion once, from the actual sign of each region's converged -// estimate. During the inactive phase the (un-rescued) flux is accumulated; +// estimate. During the inactive phase the flux is accumulated as computed; // on the final inactive batch, any region whose accumulated flux is negative // in any group is demoted to the naive (iteration) volume estimator for the // active phase -- a positively weighted estimator that cannot go negative with @@ -116,7 +116,7 @@ void FlatSourceDomain::accumulate_iteration_flux() // accumulated mean rather than on individual fluctuations, the lower tail of // the noise distribution is not clipped, so regions that are merely noisy (and // average non-negative) are left unbiased. The demotion is recorded in -// n_negative_fluxes (>= 1 == demoted), consumed by the volume switch and miss +// converged_negative (>= 1 == demoted), consumed by the volume switch and miss // treatment in add_source_to_scalar_flux. void FlatSourceDomain::inactive_demotion_step() { @@ -145,7 +145,7 @@ void FlatSourceDomain::inactive_demotion_step() break; } } - source_regions_.n_negative_fluxes(sr) = negative ? 1 : 0; + source_regions_.converged_negative(sr) = negative ? 1 : 0; for (int g = 0; g < negroups_; g++) { source_regions_.scalar_flux_final(sr, g) = 0.0; } @@ -302,7 +302,7 @@ int64_t FlatSourceDomain::add_source_to_scalar_flux() // The adaptive estimator uses the proactive strong-source (kappa) test, the // demote-to-naive volume switch, and the previous-flux miss treatment, with // demotion decided once at the end of the inactive phase (recorded as a 0/1 - // flag in n_negative_fluxes by inactive_demotion_step). + // flag in converged_negative by inactive_demotion_step). const bool is_adaptive = volume_estimator_ == RandomRayVolumeEstimator::ADAPTIVE; @@ -344,7 +344,7 @@ int64_t FlatSourceDomain::add_source_to_scalar_flux() // within-group scattering term can be negative, driving q/Sigma_t below // zero even for a non-negative flux; it can also arise transiently from a // negative previous-iteration flux, which the estimator permits by design - // (there is no per-iteration positivity rescue). The diagonal (Gunow) + // (individual iterations are never modified). The diagonal (Gunow) // stabilization keeps the TCP0 iteration convergent but acts on the flux, // not on the source sign, so such regions still need the consistent // (naive) volume and previous-flux miss treatment to keep a negative @@ -374,7 +374,7 @@ int64_t FlatSourceDomain::add_source_to_scalar_flux() // term is folded into q/Sigma_t. All are g-independent. bool external = source_regions_.external_source_present(sr); bool small = source_regions_.is_small(sr); - bool converged_neg = source_regions_.n_negative_fluxes(sr) > 0; + bool converged_neg = source_regions_.converged_negative(sr) > 0; // Every estimator reduces to two g-independent per-region decisions: // 1. which volume to use on a hit -- the simulation-averaged volume, diff --git a/src/random_ray/linear_source_domain.cpp b/src/random_ray/linear_source_domain.cpp index b081ab0b3f7..d9bcf7ffb37 100644 --- a/src/random_ray/linear_source_domain.cpp +++ b/src/random_ray/linear_source_domain.cpp @@ -125,7 +125,7 @@ void LinearSourceDomain::update_single_neutron_source(SourceRegionHandle& srh) // noise into cells already prone to negativity. if (volume_estimator_ == RandomRayVolumeEstimator::ADAPTIVE && material != MATERIAL_VOID && - (srh.n_negative_fluxes() > 0 || + (srh.converged_negative() > 0 || region_has_strong_source(&srh.source(0), &srh.scalar_flux_old(0)))) { for (int g = 0; g < negroups_; g++) { srh.source_gradients(g) = {0.0, 0.0, 0.0}; diff --git a/src/random_ray/source_region.cpp b/src/random_ray/source_region.cpp index 4a4ec685305..5e7f36a1def 100644 --- a/src/random_ray/source_region.cpp +++ b/src/random_ray/source_region.cpp @@ -12,7 +12,7 @@ namespace openmc { SourceRegionHandle::SourceRegionHandle(SourceRegion& sr) : negroups_(sr.scalar_flux_old_.size()), material_(&sr.material_), temperature_idx_(&sr.temperature_idx_), density_mult_(&sr.density_mult_), - is_small_(&sr.is_small_), n_negative_fluxes_(&sr.n_negative_fluxes_), + is_small_(&sr.is_small_), converged_negative_(&sr.converged_negative_), n_hits_(&sr.n_hits_), is_linear_(sr.source_gradients_.size() > 0), lock_(&sr.lock_), volume_(&sr.volume_), volume_t_(&sr.volume_t_), volume_sq_(&sr.volume_sq_), @@ -75,7 +75,7 @@ void SourceRegionContainer::push_back(const SourceRegion& sr) temperature_idx_.push_back(sr.temperature_idx_); density_mult_.push_back(sr.density_mult_); is_small_.push_back(sr.is_small_); - n_negative_fluxes_.push_back(sr.n_negative_fluxes_); + converged_negative_.push_back(sr.converged_negative_); n_hits_.push_back(sr.n_hits_); lock_.push_back(sr.lock_); volume_.push_back(sr.volume_); @@ -131,7 +131,7 @@ void SourceRegionContainer::assign( temperature_idx_.clear(); density_mult_.clear(); is_small_.clear(); - n_negative_fluxes_.clear(); + converged_negative_.clear(); n_hits_.clear(); lock_.clear(); volume_.clear(); @@ -191,7 +191,7 @@ SourceRegionHandle SourceRegionContainer::get_source_region_handle(int64_t sr) handle.temperature_idx_ = &temperature_idx(sr); handle.density_mult_ = &density_mult(sr); handle.is_small_ = &is_small(sr); - handle.n_negative_fluxes_ = &n_negative_fluxes(sr); + handle.converged_negative_ = &converged_negative(sr); handle.n_hits_ = &n_hits(sr); handle.is_linear_ = is_linear(); handle.lock_ = &lock(sr); @@ -235,7 +235,7 @@ SourceRegionHandle SourceRegionContainer::get_source_region_handle(int64_t sr) void SourceRegionContainer::adjoint_reset() { std::fill(n_hits_.begin(), n_hits_.end(), 0); - std::fill(n_negative_fluxes_.begin(), n_negative_fluxes_.end(), 0); + std::fill(converged_negative_.begin(), converged_negative_.end(), 0); std::fill(volume_.begin(), volume_.end(), 0.0); std::fill(volume_t_.begin(), volume_t_.end(), 0.0); std::fill(volume_sq_.begin(), volume_sq_.end(), 0.0); From ea74c9d5792cb57066af4c5700e3ed8c11cd7e76 Mon Sep 17 00:00:00 2001 From: John Tramm Date: Fri, 10 Jul 2026 02:09:37 +0000 Subject: [PATCH 06/35] Give the ray-starved adaptive case its own test function The volume-estimator parametrization axis enumerates estimators; 'adaptive_starved' is a scenario (the adaptive estimator at a deliberately starved ray density), so it moves to a dedicated test function. Reference results are unchanged (same configuration and working directory). Co-Authored-By: Claude Fable 5 --- .../random_ray_volume_estimator/test.py | 42 ++++++++++--------- 1 file changed, 23 insertions(+), 19 deletions(-) diff --git a/tests/regression_tests/random_ray_volume_estimator/test.py b/tests/regression_tests/random_ray_volume_estimator/test.py index 23647eb3c39..f78a8881429 100644 --- a/tests/regression_tests/random_ray_volume_estimator/test.py +++ b/tests/regression_tests/random_ray_volume_estimator/test.py @@ -19,28 +19,32 @@ def _cleanup(self): @pytest.mark.parametrize("estimator", ["hybrid", "simulation_averaged", "naive", - "adaptive", - "adaptive_starved" + "adaptive" ]) def test_random_ray_volume_estimator(estimator): with change_directory(estimator): openmc.reset_auto_ids() model = random_ray_three_region_cube() - if estimator == 'adaptive_starved': - # Deliberately ray-starved configuration (~20% miss rate): unlike - # the nominal cases, this exercises every adaptive mechanism at - # once -- the strong-source (kappa) demotion, the hit-starved - # demotion, the end-of-inactive converged-negative demotion, and - # the previous-flux miss treatment -- so changes to any of those - # code paths show up in this gold. - model.settings.random_ray['volume_estimator'] = 'adaptive' - model.settings.particles = 10 - model.settings.inactive = 20 - model.settings.batches = 40 - sp_name = 'statepoint.40.h5' - else: - model.settings.random_ray['volume_estimator'] = estimator - sp_name = 'statepoint.10.h5' - - harness = MGXSTestHarness(sp_name, model) + model.settings.random_ray['volume_estimator'] = estimator + + harness = MGXSTestHarness('statepoint.10.h5', model) + harness.main() + + +def test_random_ray_volume_estimator_starved(): + # Deliberately ray-starved adaptive configuration (~20% miss rate): unlike + # the nominal cases above, this exercises every adaptive mechanism at once + # -- the strong-source (kappa) demotion, the hit-starved demotion, the + # end-of-inactive converged-negative demotion, and the previous-flux miss + # treatment -- so changes to any of those code paths show up in its + # reference results. + with change_directory('adaptive_starved'): + openmc.reset_auto_ids() + model = random_ray_three_region_cube() + model.settings.random_ray['volume_estimator'] = 'adaptive' + model.settings.particles = 10 + model.settings.inactive = 20 + model.settings.batches = 40 + + harness = MGXSTestHarness('statepoint.40.h5', model) harness.main() From 41e51d8f38b23366c482df6d862988fd288b3d1d Mon Sep 17 00:00:00 2001 From: John Tramm Date: Fri, 10 Jul 2026 02:12:35 +0000 Subject: [PATCH 07/35] Test the volume estimators at a starved ray density At the previous ray density the source region miss rate was zero, so the miss-treatment code paths -- where the volume estimators meaningfully differ -- never executed in these tests. All four estimators now run deliberately ray-starved (~20% miss rate), which exercises the per-estimator volume choices, the miss treatments, and all of the adaptive demotion mechanisms (strong-source, hit-starved, and converged-negative) in every parametrization; the separate starved adaptive case is absorbed into the adaptive parametrization. Reference results are regenerated for the new configuration. Co-Authored-By: Claude Fable 5 --- .../adaptive/inputs_true.dat | 6 +- .../adaptive/results_true.dat | 12 +- .../adaptive_starved/inputs_true.dat | 247 ------------------ .../adaptive_starved/results_true.dat | 9 - .../hybrid/inputs_true.dat | 6 +- .../hybrid/results_true.dat | 12 +- .../naive/inputs_true.dat | 6 +- .../naive/results_true.dat | 12 +- .../simulation_averaged/inputs_true.dat | 6 +- .../simulation_averaged/results_true.dat | 12 +- .../random_ray_volume_estimator/test.py | 23 +- .../adaptive/inputs_true.dat | 4 +- .../adaptive/results_true.dat | 12 +- .../hybrid/inputs_true.dat | 4 +- .../hybrid/results_true.dat | 12 +- .../naive/inputs_true.dat | 4 +- .../naive/results_true.dat | 12 +- .../simulation_averaged/inputs_true.dat | 4 +- .../simulation_averaged/results_true.dat | 12 +- .../test.py | 11 +- 20 files changed, 85 insertions(+), 341 deletions(-) delete mode 100644 tests/regression_tests/random_ray_volume_estimator/adaptive_starved/inputs_true.dat delete mode 100644 tests/regression_tests/random_ray_volume_estimator/adaptive_starved/results_true.dat diff --git a/tests/regression_tests/random_ray_volume_estimator/adaptive/inputs_true.dat b/tests/regression_tests/random_ray_volume_estimator/adaptive/inputs_true.dat index 8d4f8b02e2c..33ab014d4c6 100644 --- a/tests/regression_tests/random_ray_volume_estimator/adaptive/inputs_true.dat +++ b/tests/regression_tests/random_ray_volume_estimator/adaptive/inputs_true.dat @@ -191,9 +191,9 @@ fixed source - 90 - 10 - 5 + 10 + 40 + 20 100.0 1.0 diff --git a/tests/regression_tests/random_ray_volume_estimator/adaptive/results_true.dat b/tests/regression_tests/random_ray_volume_estimator/adaptive/results_true.dat index 274ca3db560..b13c7172649 100644 --- a/tests/regression_tests/random_ray_volume_estimator/adaptive/results_true.dat +++ b/tests/regression_tests/random_ray_volume_estimator/adaptive/results_true.dat @@ -1,9 +1,9 @@ tally 1: -5.934460E-01 -7.058893E-02 +2.258230E+00 +2.672994E-01 tally 2: -3.209138E-02 -2.066116E-04 +9.236733E-02 +5.894388E-04 tally 3: -2.096421E-03 -8.805020E-07 +7.475196E-03 +3.603831E-06 diff --git a/tests/regression_tests/random_ray_volume_estimator/adaptive_starved/inputs_true.dat b/tests/regression_tests/random_ray_volume_estimator/adaptive_starved/inputs_true.dat deleted file mode 100644 index 33ab014d4c6..00000000000 --- a/tests/regression_tests/random_ray_volume_estimator/adaptive_starved/inputs_true.dat +++ /dev/null @@ -1,247 +0,0 @@ - - - - mgxs.h5 - - - - - - - - - - - - - - - - - - - - - 2.5 2.5 2.5 - 12 12 12 - 0.0 0.0 0.0 - -3 3 3 3 3 3 3 3 3 3 3 3 -3 3 3 3 3 3 3 3 3 3 3 3 -2 2 2 2 2 2 2 2 2 2 3 3 -2 2 2 2 2 2 2 2 2 2 3 3 -2 2 2 2 2 2 2 2 2 2 3 3 -2 2 2 2 2 2 2 2 2 2 3 3 -2 2 2 2 2 2 2 2 2 2 3 3 -2 2 2 2 2 2 2 2 2 2 3 3 -2 2 2 2 2 2 2 2 2 2 3 3 -2 2 2 2 2 2 2 2 2 2 3 3 -1 1 2 2 2 2 2 2 2 2 3 3 -1 1 2 2 2 2 2 2 2 2 3 3 - -3 3 3 3 3 3 3 3 3 3 3 3 -3 3 3 3 3 3 3 3 3 3 3 3 -2 2 2 2 2 2 2 2 2 2 3 3 -2 2 2 2 2 2 2 2 2 2 3 3 -2 2 2 2 2 2 2 2 2 2 3 3 -2 2 2 2 2 2 2 2 2 2 3 3 -2 2 2 2 2 2 2 2 2 2 3 3 -2 2 2 2 2 2 2 2 2 2 3 3 -2 2 2 2 2 2 2 2 2 2 3 3 -2 2 2 2 2 2 2 2 2 2 3 3 -1 1 2 2 2 2 2 2 2 2 3 3 -1 1 2 2 2 2 2 2 2 2 3 3 - -3 3 3 3 3 3 3 3 3 3 3 3 -3 3 3 3 3 3 3 3 3 3 3 3 -2 2 2 2 2 2 2 2 2 2 3 3 -2 2 2 2 2 2 2 2 2 2 3 3 -2 2 2 2 2 2 2 2 2 2 3 3 -2 2 2 2 2 2 2 2 2 2 3 3 -2 2 2 2 2 2 2 2 2 2 3 3 -2 2 2 2 2 2 2 2 2 2 3 3 -2 2 2 2 2 2 2 2 2 2 3 3 -2 2 2 2 2 2 2 2 2 2 3 3 -2 2 2 2 2 2 2 2 2 2 3 3 -2 2 2 2 2 2 2 2 2 2 3 3 - -3 3 3 3 3 3 3 3 3 3 3 3 -3 3 3 3 3 3 3 3 3 3 3 3 -2 2 2 2 2 2 2 2 2 2 3 3 -2 2 2 2 2 2 2 2 2 2 3 3 -2 2 2 2 2 2 2 2 2 2 3 3 -2 2 2 2 2 2 2 2 2 2 3 3 -2 2 2 2 2 2 2 2 2 2 3 3 -2 2 2 2 2 2 2 2 2 2 3 3 -2 2 2 2 2 2 2 2 2 2 3 3 -2 2 2 2 2 2 2 2 2 2 3 3 -2 2 2 2 2 2 2 2 2 2 3 3 -2 2 2 2 2 2 2 2 2 2 3 3 - -3 3 3 3 3 3 3 3 3 3 3 3 -3 3 3 3 3 3 3 3 3 3 3 3 -2 2 2 2 2 2 2 2 2 2 3 3 -2 2 2 2 2 2 2 2 2 2 3 3 -2 2 2 2 2 2 2 2 2 2 3 3 -2 2 2 2 2 2 2 2 2 2 3 3 -2 2 2 2 2 2 2 2 2 2 3 3 -2 2 2 2 2 2 2 2 2 2 3 3 -2 2 2 2 2 2 2 2 2 2 3 3 -2 2 2 2 2 2 2 2 2 2 3 3 -2 2 2 2 2 2 2 2 2 2 3 3 -2 2 2 2 2 2 2 2 2 2 3 3 - -3 3 3 3 3 3 3 3 3 3 3 3 -3 3 3 3 3 3 3 3 3 3 3 3 -2 2 2 2 2 2 2 2 2 2 3 3 -2 2 2 2 2 2 2 2 2 2 3 3 -2 2 2 2 2 2 2 2 2 2 3 3 -2 2 2 2 2 2 2 2 2 2 3 3 -2 2 2 2 2 2 2 2 2 2 3 3 -2 2 2 2 2 2 2 2 2 2 3 3 -2 2 2 2 2 2 2 2 2 2 3 3 -2 2 2 2 2 2 2 2 2 2 3 3 -2 2 2 2 2 2 2 2 2 2 3 3 -2 2 2 2 2 2 2 2 2 2 3 3 - -3 3 3 3 3 3 3 3 3 3 3 3 -3 3 3 3 3 3 3 3 3 3 3 3 -2 2 2 2 2 2 2 2 2 2 3 3 -2 2 2 2 2 2 2 2 2 2 3 3 -2 2 2 2 2 2 2 2 2 2 3 3 -2 2 2 2 2 2 2 2 2 2 3 3 -2 2 2 2 2 2 2 2 2 2 3 3 -2 2 2 2 2 2 2 2 2 2 3 3 -2 2 2 2 2 2 2 2 2 2 3 3 -2 2 2 2 2 2 2 2 2 2 3 3 -2 2 2 2 2 2 2 2 2 2 3 3 -2 2 2 2 2 2 2 2 2 2 3 3 - -3 3 3 3 3 3 3 3 3 3 3 3 -3 3 3 3 3 3 3 3 3 3 3 3 -2 2 2 2 2 2 2 2 2 2 3 3 -2 2 2 2 2 2 2 2 2 2 3 3 -2 2 2 2 2 2 2 2 2 2 3 3 -2 2 2 2 2 2 2 2 2 2 3 3 -2 2 2 2 2 2 2 2 2 2 3 3 -2 2 2 2 2 2 2 2 2 2 3 3 -2 2 2 2 2 2 2 2 2 2 3 3 -2 2 2 2 2 2 2 2 2 2 3 3 -2 2 2 2 2 2 2 2 2 2 3 3 -2 2 2 2 2 2 2 2 2 2 3 3 - -3 3 3 3 3 3 3 3 3 3 3 3 -3 3 3 3 3 3 3 3 3 3 3 3 -2 2 2 2 2 2 2 2 2 2 3 3 -2 2 2 2 2 2 2 2 2 2 3 3 -2 2 2 2 2 2 2 2 2 2 3 3 -2 2 2 2 2 2 2 2 2 2 3 3 -2 2 2 2 2 2 2 2 2 2 3 3 -2 2 2 2 2 2 2 2 2 2 3 3 -2 2 2 2 2 2 2 2 2 2 3 3 -2 2 2 2 2 2 2 2 2 2 3 3 -2 2 2 2 2 2 2 2 2 2 3 3 -2 2 2 2 2 2 2 2 2 2 3 3 - -3 3 3 3 3 3 3 3 3 3 3 3 -3 3 3 3 3 3 3 3 3 3 3 3 -2 2 2 2 2 2 2 2 2 2 3 3 -2 2 2 2 2 2 2 2 2 2 3 3 -2 2 2 2 2 2 2 2 2 2 3 3 -2 2 2 2 2 2 2 2 2 2 3 3 -2 2 2 2 2 2 2 2 2 2 3 3 -2 2 2 2 2 2 2 2 2 2 3 3 -2 2 2 2 2 2 2 2 2 2 3 3 -2 2 2 2 2 2 2 2 2 2 3 3 -2 2 2 2 2 2 2 2 2 2 3 3 -2 2 2 2 2 2 2 2 2 2 3 3 - -3 3 3 3 3 3 3 3 3 3 3 3 -3 3 3 3 3 3 3 3 3 3 3 3 -3 3 3 3 3 3 3 3 3 3 3 3 -3 3 3 3 3 3 3 3 3 3 3 3 -3 3 3 3 3 3 3 3 3 3 3 3 -3 3 3 3 3 3 3 3 3 3 3 3 -3 3 3 3 3 3 3 3 3 3 3 3 -3 3 3 3 3 3 3 3 3 3 3 3 -3 3 3 3 3 3 3 3 3 3 3 3 -3 3 3 3 3 3 3 3 3 3 3 3 -3 3 3 3 3 3 3 3 3 3 3 3 -3 3 3 3 3 3 3 3 3 3 3 3 - -3 3 3 3 3 3 3 3 3 3 3 3 -3 3 3 3 3 3 3 3 3 3 3 3 -3 3 3 3 3 3 3 3 3 3 3 3 -3 3 3 3 3 3 3 3 3 3 3 3 -3 3 3 3 3 3 3 3 3 3 3 3 -3 3 3 3 3 3 3 3 3 3 3 3 -3 3 3 3 3 3 3 3 3 3 3 3 -3 3 3 3 3 3 3 3 3 3 3 3 -3 3 3 3 3 3 3 3 3 3 3 3 -3 3 3 3 3 3 3 3 3 3 3 3 -3 3 3 3 3 3 3 3 3 3 3 3 -3 3 3 3 3 3 3 3 3 3 3 3 - - - - - - - - - - fixed source - 10 - 40 - 20 - - - 100.0 1.0 - - - universe - 1 - - - multi-group - - 500.0 - 100.0 - - - - 0.0 0.0 0.0 30.0 30.0 30.0 - - - - true - adaptive - - - - - 1 - - - 2 - - - 3 - - - 3 - flux - tracklength - - - 2 - flux - tracklength - - - 1 - flux - tracklength - - - diff --git a/tests/regression_tests/random_ray_volume_estimator/adaptive_starved/results_true.dat b/tests/regression_tests/random_ray_volume_estimator/adaptive_starved/results_true.dat deleted file mode 100644 index b13c7172649..00000000000 --- a/tests/regression_tests/random_ray_volume_estimator/adaptive_starved/results_true.dat +++ /dev/null @@ -1,9 +0,0 @@ -tally 1: -2.258230E+00 -2.672994E-01 -tally 2: -9.236733E-02 -5.894388E-04 -tally 3: -7.475196E-03 -3.603831E-06 diff --git a/tests/regression_tests/random_ray_volume_estimator/hybrid/inputs_true.dat b/tests/regression_tests/random_ray_volume_estimator/hybrid/inputs_true.dat index 4d1af46b121..505d4c7dfed 100644 --- a/tests/regression_tests/random_ray_volume_estimator/hybrid/inputs_true.dat +++ b/tests/regression_tests/random_ray_volume_estimator/hybrid/inputs_true.dat @@ -191,9 +191,9 @@ fixed source - 90 - 10 - 5 + 10 + 40 + 20 100.0 1.0 diff --git a/tests/regression_tests/random_ray_volume_estimator/hybrid/results_true.dat b/tests/regression_tests/random_ray_volume_estimator/hybrid/results_true.dat index 6da51a711bf..16adfa6d81a 100644 --- a/tests/regression_tests/random_ray_volume_estimator/hybrid/results_true.dat +++ b/tests/regression_tests/random_ray_volume_estimator/hybrid/results_true.dat @@ -1,9 +1,9 @@ tally 1: -5.934460E-01 -7.058894E-02 +2.261079E+00 +2.678641E-01 tally 2: -3.206214E-02 -2.063370E-04 +1.043325E-01 +7.552784E-04 tally 3: -2.096411E-03 -8.804924E-07 +6.475295E-03 +2.740906E-06 diff --git a/tests/regression_tests/random_ray_volume_estimator/naive/inputs_true.dat b/tests/regression_tests/random_ray_volume_estimator/naive/inputs_true.dat index a268d55d04a..b408131f815 100644 --- a/tests/regression_tests/random_ray_volume_estimator/naive/inputs_true.dat +++ b/tests/regression_tests/random_ray_volume_estimator/naive/inputs_true.dat @@ -191,9 +191,9 @@ fixed source - 90 - 10 - 5 + 10 + 40 + 20 100.0 1.0 diff --git a/tests/regression_tests/random_ray_volume_estimator/naive/results_true.dat b/tests/regression_tests/random_ray_volume_estimator/naive/results_true.dat index f8d6d10b001..6afcb204a05 100644 --- a/tests/regression_tests/random_ray_volume_estimator/naive/results_true.dat +++ b/tests/regression_tests/random_ray_volume_estimator/naive/results_true.dat @@ -1,9 +1,9 @@ tally 1: -5.935538E-01 -7.061433E-02 +2.258260E+00 +2.673097E-01 tally 2: -3.263210E-02 -2.134164E-04 +1.100975E-01 +7.432009E-04 tally 3: -2.107977E-03 -8.905227E-07 +6.385176E-03 +2.616155E-06 diff --git a/tests/regression_tests/random_ray_volume_estimator/simulation_averaged/inputs_true.dat b/tests/regression_tests/random_ray_volume_estimator/simulation_averaged/inputs_true.dat index 777ccaea510..789d280f166 100644 --- a/tests/regression_tests/random_ray_volume_estimator/simulation_averaged/inputs_true.dat +++ b/tests/regression_tests/random_ray_volume_estimator/simulation_averaged/inputs_true.dat @@ -191,9 +191,9 @@ fixed source - 90 - 10 - 5 + 10 + 40 + 20 100.0 1.0 diff --git a/tests/regression_tests/random_ray_volume_estimator/simulation_averaged/results_true.dat b/tests/regression_tests/random_ray_volume_estimator/simulation_averaged/results_true.dat index 5f297586075..3466720b9f5 100644 --- a/tests/regression_tests/random_ray_volume_estimator/simulation_averaged/results_true.dat +++ b/tests/regression_tests/random_ray_volume_estimator/simulation_averaged/results_true.dat @@ -1,9 +1,9 @@ tally 1: --5.745886E+02 -9.758367E+04 +-5.372158E+03 +9.133802E+06 tally 2: -2.971927E-02 -1.827222E-04 +8.960408E-02 +1.520820E-03 tally 3: -1.978393E-03 -7.951531E-07 +4.634379E-03 +2.696798E-06 diff --git a/tests/regression_tests/random_ray_volume_estimator/test.py b/tests/regression_tests/random_ray_volume_estimator/test.py index f78a8881429..ded37303ade 100644 --- a/tests/regression_tests/random_ray_volume_estimator/test.py +++ b/tests/regression_tests/random_ray_volume_estimator/test.py @@ -16,6 +16,13 @@ def _cleanup(self): os.remove(f) +# A deliberately ray-starved configuration (~20% source region miss rate): +# the volume estimators only differ meaningfully when regions are missed or +# sparsely hit, so a starved run exercises every estimator code path -- the +# per-estimator volume choices, the miss treatments, and for the adaptive +# estimator the strong-source (kappa) demotion, the hit-starved demotion, +# the end-of-inactive converged-negative demotion, and the previous-flux +# miss treatment all fire at this density. @pytest.mark.parametrize("estimator", ["hybrid", "simulation_averaged", "naive", @@ -26,22 +33,6 @@ def test_random_ray_volume_estimator(estimator): openmc.reset_auto_ids() model = random_ray_three_region_cube() model.settings.random_ray['volume_estimator'] = estimator - - harness = MGXSTestHarness('statepoint.10.h5', model) - harness.main() - - -def test_random_ray_volume_estimator_starved(): - # Deliberately ray-starved adaptive configuration (~20% miss rate): unlike - # the nominal cases above, this exercises every adaptive mechanism at once - # -- the strong-source (kappa) demotion, the hit-starved demotion, the - # end-of-inactive converged-negative demotion, and the previous-flux miss - # treatment -- so changes to any of those code paths show up in its - # reference results. - with change_directory('adaptive_starved'): - openmc.reset_auto_ids() - model = random_ray_three_region_cube() - model.settings.random_ray['volume_estimator'] = 'adaptive' model.settings.particles = 10 model.settings.inactive = 20 model.settings.batches = 40 diff --git a/tests/regression_tests/random_ray_volume_estimator_linear/adaptive/inputs_true.dat b/tests/regression_tests/random_ray_volume_estimator_linear/adaptive/inputs_true.dat index 592a9fea69f..82587334ecb 100644 --- a/tests/regression_tests/random_ray_volume_estimator_linear/adaptive/inputs_true.dat +++ b/tests/regression_tests/random_ray_volume_estimator_linear/adaptive/inputs_true.dat @@ -191,7 +191,7 @@ fixed source - 90 + 10 40 20 @@ -215,8 +215,8 @@ true - linear adaptive + linear diff --git a/tests/regression_tests/random_ray_volume_estimator_linear/adaptive/results_true.dat b/tests/regression_tests/random_ray_volume_estimator_linear/adaptive/results_true.dat index 020742f66df..127220de75a 100644 --- a/tests/regression_tests/random_ray_volume_estimator_linear/adaptive/results_true.dat +++ b/tests/regression_tests/random_ray_volume_estimator_linear/adaptive/results_true.dat @@ -1,9 +1,9 @@ tally 1: -2.347042E+00 -2.759340E-01 +2.286588E+00 +2.738030E-01 tally 2: -1.091213E-01 -6.071243E-04 +8.636233E-02 +5.009484E-04 tally 3: -7.311013E-03 -2.725239E-06 +7.688328E-03 +3.858911E-06 diff --git a/tests/regression_tests/random_ray_volume_estimator_linear/hybrid/inputs_true.dat b/tests/regression_tests/random_ray_volume_estimator_linear/hybrid/inputs_true.dat index dd11567f69d..53d7bbac171 100644 --- a/tests/regression_tests/random_ray_volume_estimator_linear/hybrid/inputs_true.dat +++ b/tests/regression_tests/random_ray_volume_estimator_linear/hybrid/inputs_true.dat @@ -191,7 +191,7 @@ fixed source - 90 + 10 40 20 @@ -215,8 +215,8 @@ true - linear hybrid + linear diff --git a/tests/regression_tests/random_ray_volume_estimator_linear/hybrid/results_true.dat b/tests/regression_tests/random_ray_volume_estimator_linear/hybrid/results_true.dat index e90d6bfdcb8..e1937ae6510 100644 --- a/tests/regression_tests/random_ray_volume_estimator_linear/hybrid/results_true.dat +++ b/tests/regression_tests/random_ray_volume_estimator_linear/hybrid/results_true.dat @@ -1,9 +1,9 @@ tally 1: -2.339086E+00 -2.747305E-01 +2.334964E+00 +3.212860E-01 tally 2: -1.089827E-01 -6.069324E-04 +8.558657E-02 +5.274716E-03 tally 3: -7.300831E-03 -2.715940E-06 +6.549250E-03 +2.816575E-06 diff --git a/tests/regression_tests/random_ray_volume_estimator_linear/naive/inputs_true.dat b/tests/regression_tests/random_ray_volume_estimator_linear/naive/inputs_true.dat index 6933fba435e..31620e93ee8 100644 --- a/tests/regression_tests/random_ray_volume_estimator_linear/naive/inputs_true.dat +++ b/tests/regression_tests/random_ray_volume_estimator_linear/naive/inputs_true.dat @@ -191,7 +191,7 @@ fixed source - 90 + 10 40 20 @@ -215,8 +215,8 @@ true - linear naive + linear diff --git a/tests/regression_tests/random_ray_volume_estimator_linear/naive/results_true.dat b/tests/regression_tests/random_ray_volume_estimator_linear/naive/results_true.dat index 5258ffd9c84..991fc3e9abe 100644 --- a/tests/regression_tests/random_ray_volume_estimator_linear/naive/results_true.dat +++ b/tests/regression_tests/random_ray_volume_estimator_linear/naive/results_true.dat @@ -1,9 +1,9 @@ tally 1: -2.339567E+00 -2.748423E-01 +2.313040E+00 +3.180880E-01 tally 2: -1.085878E-01 -6.024509E-04 +1.040963E-01 +6.664367E-04 tally 3: -7.299803E-03 -2.741867E-06 +6.583311E-03 +2.750085E-06 diff --git a/tests/regression_tests/random_ray_volume_estimator_linear/simulation_averaged/inputs_true.dat b/tests/regression_tests/random_ray_volume_estimator_linear/simulation_averaged/inputs_true.dat index 3ccab1d21b7..b681d2618a5 100644 --- a/tests/regression_tests/random_ray_volume_estimator_linear/simulation_averaged/inputs_true.dat +++ b/tests/regression_tests/random_ray_volume_estimator_linear/simulation_averaged/inputs_true.dat @@ -191,7 +191,7 @@ fixed source - 90 + 10 40 20 @@ -215,8 +215,8 @@ true - linear simulation_averaged + linear diff --git a/tests/regression_tests/random_ray_volume_estimator_linear/simulation_averaged/results_true.dat b/tests/regression_tests/random_ray_volume_estimator_linear/simulation_averaged/results_true.dat index 1e8aa9fb75f..0ea3a88bd83 100644 --- a/tests/regression_tests/random_ray_volume_estimator_linear/simulation_averaged/results_true.dat +++ b/tests/regression_tests/random_ray_volume_estimator_linear/simulation_averaged/results_true.dat @@ -1,9 +1,9 @@ tally 1: -2.670850E+02 -4.432939E+05 +-5.372146E+03 +9.133101E+06 tally 2: -1.116994E-01 -6.491358E-04 +7.428803E-02 +3.870266E-03 tally 3: -7.564527E-03 -2.947794E-06 +4.615173E-03 +2.683571E-06 diff --git a/tests/regression_tests/random_ray_volume_estimator_linear/test.py b/tests/regression_tests/random_ray_volume_estimator_linear/test.py index 5b43224d566..631029e4202 100644 --- a/tests/regression_tests/random_ray_volume_estimator_linear/test.py +++ b/tests/regression_tests/random_ray_volume_estimator_linear/test.py @@ -16,6 +16,13 @@ def _cleanup(self): os.remove(f) +# A deliberately ray-starved configuration (~20% source region miss rate): +# the volume estimators only differ meaningfully when regions are missed or +# sparsely hit, so a starved run exercises every estimator code path -- the +# per-estimator volume choices, the miss treatments, and for the adaptive +# estimator the strong-source (kappa) demotion, the hit-starved demotion, +# the end-of-inactive converged-negative demotion, and the previous-flux +# miss treatment all fire at this density. @pytest.mark.parametrize("estimator", ["hybrid", "simulation_averaged", "naive", @@ -25,9 +32,11 @@ def test_random_ray_volume_estimator_linear(estimator): with change_directory(estimator): openmc.reset_auto_ids() model = random_ray_three_region_cube() - model.settings.random_ray['source_shape'] = 'linear' model.settings.random_ray['volume_estimator'] = estimator + model.settings.random_ray['source_shape'] = 'linear' + model.settings.particles = 10 model.settings.inactive = 20 model.settings.batches = 40 + harness = MGXSTestHarness('statepoint.40.h5', model) harness.main() From 02bdfb52ec7e00580b850d4ec8c9e73d47a64142 Mon Sep 17 00:00:00 2001 From: John Tramm Date: Fri, 10 Jul 2026 02:47:45 +0000 Subject: [PATCH 08/35] Restrict the negative-source demotion to the transport-corrected signature The strong-source test demoted any region whose reduced source was negative in some group. In one-group problems the source sign is locked to the previous flux sign (q = c*phi + q_external), so that branch degenerated into a per-iteration negative-flux reactor: noisy streaming regions flip-flopped between the simulation-averaged and naive policies based on the sign of the previous iterate, conditioning the estimator choice on the noise -- exactly the fluctuation-reactive behavior the end-of-inactive converged-negative demotion is designed to avoid. On the cube stress problem of Cosgrove and Tramm (Negative fluxes and cell-miss errors in the random ray method, Prog. Nucl. Energy 192 (2026) 106153), built by openmc.examples.random_ray_three_region_cube, at a 20.5% miss rate this misclassified 29% of all regions as strong-source and biased the adaptive estimator's void-region integral flux by -19% relative to the Monte Carlo reference (hybrid: +0.1%). A negative reduced source now counts as strong only when the region's own previous flux is non-negative -- the genuine transport-corrected (TCP0) signature, where negative within-group scattering drives the source negative independently of the flux. With the fix the cube problem gives adaptive a void error of -0.04% (strong-source count 8, exactly the external source region), matching hybrid in all three regions, while sign-locked chronic negativity is handled by the converged-negative demotion (6 -> 24 regions). Adaptive reference results are regenerated; the other estimators are unaffected. Co-Authored-By: Claude Fable 5 --- src/random_ray/flat_source_domain.cpp | 15 +- .../adaptive_starved/results_true.dat | 12 +- .../adaptive/results_true.dat | 2 +- .../adaptive_starved/results_true.dat | 276 +++++++++--------- .../adaptive/results_true.dat | 12 +- .../adaptive/results_true.dat | 12 +- 6 files changed, 171 insertions(+), 158 deletions(-) diff --git a/src/random_ray/flat_source_domain.cpp b/src/random_ray/flat_source_domain.cpp index f5e95f4fa38..dce34e1f34d 100644 --- a/src/random_ray/flat_source_domain.cpp +++ b/src/random_ray/flat_source_domain.cpp @@ -281,7 +281,20 @@ bool FlatSourceDomain::region_has_strong_source( { for (int g = 0; g < negroups_; g++) { double src = reduced_source[g]; - if (src < 0.0 || src > ADAPTIVE_VOLUME_KAPPA * std::max(flux_old[g], 0.0)) { + // A negative reduced source counts as strong only when the region's own + // previous flux is non-negative -- the transport-corrected (TCP0) + // signature, where negative within-group scattering drives the source + // negative independently of the flux. When the previous flux is itself + // negative, a negative source is just the sign-locked image of that + // fluctuation (exactly so in one-group problems, where q = c*phi + + // q_external); reacting to it iteration-by-iteration would condition the + // estimator choice on the sign of the noise, which is the bias the + // converged-negative demotion exists to avoid. Chronically negative + // regions are handled by that demotion instead. + if (src < 0.0 && flux_old[g] >= 0.0) { + return true; + } + if (src > ADAPTIVE_VOLUME_KAPPA * std::max(flux_old[g], 0.0)) { return true; } } diff --git a/tests/regression_tests/random_ray_adjoint_fixed_source/adaptive_starved/results_true.dat b/tests/regression_tests/random_ray_adjoint_fixed_source/adaptive_starved/results_true.dat index c09f6029037..cdc188654ff 100644 --- a/tests/regression_tests/random_ray_adjoint_fixed_source/adaptive_starved/results_true.dat +++ b/tests/regression_tests/random_ray_adjoint_fixed_source/adaptive_starved/results_true.dat @@ -1,9 +1,9 @@ tally 1: -4.182567E+06 -7.270666E+12 +2.521404E+06 +1.138286E+12 tally 2: -8.529692E+06 -3.890832E+12 +4.652820E+06 +1.100178E+12 tally 3: -1.794581E+07 -1.614417E+13 +1.520574E+07 +1.160165E+13 diff --git a/tests/regression_tests/random_ray_diagonal_stabilization/adaptive/results_true.dat b/tests/regression_tests/random_ray_diagonal_stabilization/adaptive/results_true.dat index 23606262414..cbd2bcff9a6 100644 --- a/tests/regression_tests/random_ray_diagonal_stabilization/adaptive/results_true.dat +++ b/tests/regression_tests/random_ray_diagonal_stabilization/adaptive/results_true.dat @@ -1,2 +1,2 @@ k-combined: -7.146436E-01 1.344871E-02 +7.147211E-01 1.345902E-02 diff --git a/tests/regression_tests/random_ray_k_eff_mesh/adaptive_starved/results_true.dat b/tests/regression_tests/random_ray_k_eff_mesh/adaptive_starved/results_true.dat index e019a69a3c9..ba6db4b17ec 100644 --- a/tests/regression_tests/random_ray_k_eff_mesh/adaptive_starved/results_true.dat +++ b/tests/regression_tests/random_ray_k_eff_mesh/adaptive_starved/results_true.dat @@ -1,171 +1,171 @@ k-combined: -1.107143E+00 1.651522E-02 +1.107142E+00 1.651536E-02 tally 1: -5.381525E+00 -1.457993E+00 -1.989739E+00 -1.995895E-01 -4.842627E+00 -1.182245E+00 -3.851210E+00 -7.436550E-01 -5.695198E-01 -1.627076E-02 -1.386097E+00 -9.637797E-02 -2.905390E+00 -4.222640E-01 -9.355466E-02 -4.378775E-04 -2.276933E-01 -2.593716E-03 -3.728182E+00 -6.951843E-01 -1.234728E-01 -7.626184E-04 -3.005082E-01 -4.517281E-03 -9.698270E+00 -4.703385E+00 -1.132059E-01 -6.408770E-04 -2.755238E-01 -3.796252E-03 -2.110508E+01 -2.227508E+01 -3.196621E-02 -5.110885E-05 -7.909814E-02 -3.129295E-04 -1.114270E+01 -6.213586E+00 -1.495587E-01 -1.120798E-03 -4.159900E-01 -8.671009E-03 +5.381531E+00 +1.457996E+00 +1.989743E+00 +1.995903E-01 +4.842636E+00 +1.182249E+00 +3.851222E+00 +7.436599E-01 +5.695223E-01 +1.627091E-02 +1.386103E+00 +9.637883E-02 +2.905403E+00 +4.222679E-01 +9.355516E-02 +4.378823E-04 +2.276945E-01 +2.593744E-03 +3.728200E+00 +6.951912E-01 +1.234736E-01 +7.626279E-04 +3.005101E-01 +4.517337E-03 +9.698295E+00 +4.703409E+00 +1.132062E-01 +6.408809E-04 +2.755246E-01 +3.796276E-03 +2.110499E+01 +2.227489E+01 +3.196600E-02 +5.110820E-05 +7.909764E-02 +3.129255E-04 +1.114268E+01 +6.213561E+00 +1.495582E-01 +1.120790E-03 +4.159886E-01 +8.670950E-03 8.789785E+00 3.891485E+00 0.000000E+00 0.000000E+00 0.000000E+00 0.000000E+00 -4.704887E+00 -1.109875E+00 +4.704892E+00 +1.109878E+00 0.000000E+00 0.000000E+00 0.000000E+00 0.000000E+00 -3.067148E+00 -4.705479E-01 +3.067155E+00 +4.705500E-01 0.000000E+00 0.000000E+00 0.000000E+00 0.000000E+00 -4.067592E+00 -8.274689E-01 +4.067600E+00 +8.274723E-01 0.000000E+00 0.000000E+00 0.000000E+00 0.000000E+00 -9.782491E+00 -4.785360E+00 +9.782503E+00 +4.785373E+00 0.000000E+00 0.000000E+00 0.000000E+00 0.000000E+00 -1.891642E+01 -1.789487E+01 +1.891641E+01 +1.789486E+01 0.000000E+00 0.000000E+00 0.000000E+00 0.000000E+00 -9.665523E+00 -4.680512E+00 +9.665519E+00 +4.680509E+00 0.000000E+00 0.000000E+00 0.000000E+00 0.000000E+00 -4.684837E+00 -1.105786E+00 -1.748910E+00 -1.543697E-01 -4.256497E+00 -9.143906E-01 -3.633611E+00 -6.621053E-01 -5.450791E-01 -1.490499E-02 -1.326614E+00 -8.828795E-02 -2.859882E+00 -4.091604E-01 -9.369162E-02 -4.391300E-04 -2.280267E-01 -2.601134E-03 -3.656451E+00 -6.687181E-01 -1.230419E-01 -7.573186E-04 -2.994593E-01 -4.485888E-03 -9.693266E+00 -4.698549E+00 -1.151230E-01 -6.627375E-04 -2.801899E-01 -3.925743E-03 -2.161501E+01 -2.336278E+01 -3.339306E-02 -5.577222E-05 -8.262879E-02 -3.414824E-04 +4.684830E+00 +1.105783E+00 +1.748908E+00 +1.543693E-01 +4.256492E+00 +9.143881E-01 +3.633615E+00 +6.621069E-01 +5.450799E-01 +1.490503E-02 +1.326615E+00 +8.828821E-02 +2.859894E+00 +4.091636E-01 +9.369197E-02 +4.391332E-04 +2.280275E-01 +2.601154E-03 +3.656465E+00 +6.687233E-01 +1.230423E-01 +7.573239E-04 +2.994603E-01 +4.485919E-03 +9.693290E+00 +4.698572E+00 +1.151232E-01 +6.627400E-04 +2.801904E-01 +3.925758E-03 +2.161500E+01 +2.336275E+01 +3.339302E-02 +5.577207E-05 +8.262868E-02 +3.414815E-04 1.149136E+01 -6.606944E+00 -1.577251E-01 -1.246058E-03 -4.387044E-01 -9.640079E-03 -5.394007E+00 -1.465865E+00 -2.033657E+00 -2.084465E-01 -4.949515E+00 -1.234708E+00 -3.854200E+00 -7.449018E-01 -5.821747E-01 -1.699740E-02 -1.416897E+00 -1.006821E-01 -2.904577E+00 -4.220232E-01 -9.569122E-02 -4.580633E-04 -2.328933E-01 -2.713284E-03 -3.721192E+00 -6.925779E-01 -1.259829E-01 -7.939284E-04 -3.066171E-01 -4.702742E-03 -9.683481E+00 -4.688966E+00 -1.157040E-01 -6.694547E-04 -2.816039E-01 -3.965533E-03 -2.116973E+01 -2.241256E+01 -3.293792E-02 -5.426737E-05 -8.150257E-02 -3.322685E-04 -1.127462E+01 -6.361201E+00 -1.560968E-01 -1.221259E-03 -4.341752E-01 -9.448223E-03 +6.606940E+00 +1.577250E-01 +1.246057E-03 +4.387042E-01 +9.640071E-03 +5.393970E+00 +1.465845E+00 +2.033639E+00 +2.084426E-01 +4.949471E+00 +1.234685E+00 +3.854189E+00 +7.448976E-01 +5.821725E-01 +1.699727E-02 +1.416891E+00 +1.006813E-01 +2.904585E+00 +4.220256E-01 +9.569168E-02 +4.580679E-04 +2.328944E-01 +2.713311E-03 +3.721209E+00 +6.925841E-01 +1.259837E-01 +7.939399E-04 +3.066192E-01 +4.702809E-03 +9.683524E+00 +4.689009E+00 +1.157048E-01 +6.694632E-04 +2.816057E-01 +3.965583E-03 +2.116976E+01 +2.241261E+01 +3.293797E-02 +5.426754E-05 +8.150270E-02 +3.322695E-04 +1.127460E+01 +6.361186E+00 +1.560965E-01 +1.221255E-03 +4.341745E-01 +9.448188E-03 diff --git a/tests/regression_tests/random_ray_volume_estimator/adaptive/results_true.dat b/tests/regression_tests/random_ray_volume_estimator/adaptive/results_true.dat index b13c7172649..d8ea959a9dc 100644 --- a/tests/regression_tests/random_ray_volume_estimator/adaptive/results_true.dat +++ b/tests/regression_tests/random_ray_volume_estimator/adaptive/results_true.dat @@ -1,9 +1,9 @@ tally 1: -2.258230E+00 -2.672994E-01 +2.261142E+00 +2.678744E-01 tally 2: -9.236733E-02 -5.894388E-04 +1.029707E-01 +7.377908E-04 tally 3: -7.475196E-03 -3.603831E-06 +7.337695E-03 +3.519893E-06 diff --git a/tests/regression_tests/random_ray_volume_estimator_linear/adaptive/results_true.dat b/tests/regression_tests/random_ray_volume_estimator_linear/adaptive/results_true.dat index 127220de75a..d6949631dcb 100644 --- a/tests/regression_tests/random_ray_volume_estimator_linear/adaptive/results_true.dat +++ b/tests/regression_tests/random_ray_volume_estimator_linear/adaptive/results_true.dat @@ -1,9 +1,9 @@ tally 1: -2.286588E+00 -2.738030E-01 +2.308009E+00 +2.776368E-01 tally 2: -8.636233E-02 -5.009484E-04 +8.690165E-02 +5.512767E-03 tally 3: -7.688328E-03 -3.858911E-06 +7.510574E-03 +3.694630E-06 From 75d13f8a46bb01690847356a85e96dae07e21c29 Mon Sep 17 00:00:00 2001 From: John Tramm Date: Fri, 10 Jul 2026 22:45:52 +0000 Subject: [PATCH 09/35] Add a strong-feed latch to the adaptive estimator's transition demotion The per-iteration strong-source test, evaluated on noisy single-iteration values, has one blind state: an unlucky iteration can drag a strongly fed region's source and flux negative together, and in that state neither the ratio condition nor the negative-source condition can fire, so the region rides the excursion on unprotected simulation-averaged updates. Because the per-iteration noise scale in such regions is set by q/Sigma_t rather than by the flux, their flux averaged over a whole phase of batches can straddle zero: on the irradiation-vault validation problem (in-scatter-fed thin hall, 5.9% miss), 82 of 42,875 slow-group tally bins finish negative, each within 1.5 sigma of zero, on a different set of bins for every seed. No sign-based demotion can remove this population -- the transition sign test already catches its fair share (92 regions), and the survivors simply re-roll in the active window. The latch keys on the stable property the whole class shares: at the inactive->active transition, alongside the existing converged-negative decision and from the same accumulated flux, any region whose flux-independent feed (cross-group in-scatter, fission, and external source) exceeds ADAPTIVE_VOLUME_KAPPA times its own accumulated flux is demoted for the active phase. A region with no cross-group or external feed can never latch, so the estimator choice still never reacts to sign-locked noise (the Cosgrove-cube constraint), and the external source term is only read in fixed source mode, where the arrays exist. Validation: the vault's negative bins go from 82/61 (two seeds) to 0/0 with the formerly negative bin set now scored to -2.7%/-1.8% (was -203%) and the global answer unchanged; the Cosgrove cube is identical to the digit; the latch is inert on TLD-class door-2 problems (no feed) and on C5G7 (0 regions latched, k/pin powers identical to the digit). Also reports the transition decisions on their own lines, since the existing by-cause block is a final-iteration snapshot whose priority attribution files transition-demoted regions under Strong Source. Co-Authored-By: Claude Fable 5 --- docs/source/methods/random_ray.rst | 64 +++++++---- docs/source/usersguide/random_ray.rst | 45 ++++---- .../openmc/random_ray/flat_source_domain.h | 7 ++ src/random_ray/flat_source_domain.cpp | 108 +++++++++++++++--- src/random_ray/linear_source_domain.cpp | 10 +- src/random_ray/random_ray_simulation.cpp | 5 + 6 files changed, 180 insertions(+), 59 deletions(-) diff --git a/docs/source/methods/random_ray.rst b/docs/source/methods/random_ray.rst index b6c47e1d774..c065d1fa644 100644 --- a/docs/source/methods/random_ray.rst +++ b/docs/source/methods/random_ray.rst @@ -533,25 +533,44 @@ source that greatly exceeds the region's scalar flux (a source sustained by an external or in-scatter contribution rather than by the local flux), a reduced source that is itself negative (which can occur under transport-corrected cross sections, whose negative within-group scattering term can drive the reduced -source below zero even for a non-negative flux), a hit-starved region, and a -region whose flux converges to a negative value. +source below zero even for a non-negative flux), a hit-starved region, a +region whose flux converges to a negative value, and a region whose converged +flux-independent source ("feed") is strong relative to its own converged flux. The first three conditions are evaluated each iteration from already-resident -data. The negative-flux condition is instead decided once, at the transition -from the inactive to the active batches: the unmodified simulation averaged -estimator is run throughout the inactive phase while each region's flux is -accumulated, and any region whose accumulated (and therefore noise-averaged) -flux is negative is demoted to the naive estimator for all of the active -batches. Deferring the decision to the sign of the converged estimate -- rather -than reacting to individual per-iteration negatives -- avoids the upward bias -that repairing or demoting on isolated fluctuations would introduce by clipping -only the lower tail of the estimator's noise distribution; regions that are -merely noisy but average non-negative retain the unbiased simulation averaged -estimator. The trade-off is that non-negative fluxes are not strictly -enforced in every active iteration, so a small number of near-zero regions may -register slightly negative in the active tally; in variance reduction workflows -these are discarded by the weight-window generator, which ignores non-positive -fluxes. +data. The last two are instead decided once, at the transition from the +inactive to the active batches: the unmodified simulation averaged estimator +is run throughout the inactive phase while each region's flux is accumulated, +and at the transition a region is demoted to the naive estimator for all of +the active batches if its accumulated (and therefore noise-averaged) flux is +negative in any group, or if its flux-independent feed -- the part of its +source arising from cross-group in-scatter, fission, and any external source, +evaluated from the same accumulated flux -- exceeds the strong-source +threshold times its own accumulated flux in any group. Deferring these +decisions to the converged estimate -- rather than reacting to individual +per-iteration values -- avoids the upward bias that repairing or demoting on +isolated fluctuations would introduce by clipping only the lower tail of the +estimator's noise distribution; regions that are merely noisy but average +non-negative (and are not strongly fed) retain the unbiased simulation +averaged estimator. + +The feed-based latch exists because the per-iteration strong-source test, +evaluated on noisy single-iteration values, has exactly one blind state: an +unlucky iteration can drag a strongly fed region's source and flux negative +together, and in that state neither the ratio condition nor the +negative-source condition can fire. Such a region would ride out the +excursion on unprotected simulation averaged updates, and -- because for +these regions the per-iteration noise scale is set by the reduced source +rather than by the flux -- the average over a whole phase of active batches +can land slightly negative. The latch identifies the entire strongly fed +class once, from converged data that individual fluctuations cannot flip, and +removes it from the simulation averaged estimator before active tallies +begin. A region with no cross-group or external feed can never latch, so the +estimator choice never reacts to noise whose sign is locked to the region's +own flux (as in one-group media, where the source is proportional to the +local flux). Non-negativity is still not strictly enforced on individual +active iterations; in variance reduction workflows any residual non-positive +tally values are discarded by the weight-window generator. Whereas the hybrid estimator guards only regions with explicit external sources, the adaptive estimator also catches the optically thin regions of @@ -559,11 +578,12 @@ fixed source problems where the simulation averaged and hybrid estimators can otherwise develop persistent negative fluxes. When a linear source shape is in use, demoted regions additionally revert to a flat source representation (their source gradients are zeroed), extending the flat-source treatment -already applied to hit-starved regions: in a strong-source region the gradient -terms attenuate segments against the local rather than the flat source, -re-injecting per-iteration noise at the scale of the reduced source that the -volume choice cannot cancel, while in a converged-negative region the fitted -gradients carry no meaningful shape information. The adaptive estimator is the +already applied to hit-starved regions: in a strong-source or latched region +the gradient terms attenuate segments against the local rather than the flat +source, re-injecting per-iteration noise at the scale of the reduced source +that the volume choice cannot cancel, while in a converged-negative region +the fitted gradients carry no meaningful shape information. The adaptive +estimator is the default in OpenMC, and is particularly beneficial for fixed source and shielding problems that exhibit such instability. diff --git a/docs/source/usersguide/random_ray.rst b/docs/source/usersguide/random_ray.rst index cc09be05b7c..3e19298a03b 100644 --- a/docs/source/usersguide/random_ray.rst +++ b/docs/source/usersguide/random_ray.rst @@ -1065,25 +1065,28 @@ following methods are currently available in OpenMC: cells whose reduced source greatly exceeds their flux (a strong external or in-scatter source), cells whose reduced source is itself negative (possible under transport-corrected cross sections), hit-starved cells, - and cells whose accumulated flux is negative at the end of the inactive - phase. This last demotion is a one-shot decision: the unmodified - simulation averaged estimator runs throughout the inactive phase, and any - cell whose converged (accumulated) flux is negative -- genuinely - negative rather than merely noisy -- is demoted to the naive estimator - for all of the active batches. The decision is made automatically from - each cell's accumulated statistics; individual iterations are never - modified. + and -- decided once at the end of the inactive phase, from each cell's + accumulated (converged) flux -- cells whose accumulated flux is + negative as well as cells whose flux-independent feed (cross-group + in-scatter, fission, and external source) is strong relative to their + own accumulated flux. The transition decisions are one-shot: the + unmodified simulation averaged estimator runs throughout the inactive + phase, and the demoted cells use the naive estimator for all of the + active batches. The decisions are made automatically from each cell's + accumulated statistics; individual iterations are never modified. - * Retains the low bias of the simulation averaged estimator wherever it is well behaved * Eliminates the negative-flux instabilities that the simulation averaged and hybrid estimators can exhibit in optically thin, in-scatter-fed fixed source problems + * The converged-feed latch removes the strongly fed cell population + whose phase-averaged flux could otherwise straddle zero, so tallies + are free of negative bins in all validation problems * No parameters to tune - - * Does not strictly guarantee non-negative active-phase fluxes: a few - near-zero cells can still fluctuate slightly negative by statistical - chance (these are discarded downstream by the weight-window - generator, which ignores non-positive fluxes) - * Requires inactive batches in order to make the demotion decision + - * Does not strictly guarantee non-negative fluxes on individual + active iterations (any residual non-positive tally values are + discarded downstream by the weight-window generator) + * Requires inactive batches in order to make the transition decisions These estimators can be selected by setting the ``volume_estimator`` field in the :attr:`openmc.Settings.random_ray` dictionary. For example, to use the naive @@ -1103,13 +1106,15 @@ develop persistent negative fluxes that degrade tally results and, in variance reduction workflows, the quality of generated weight windows. The adaptive estimator detects and stabilizes those cells automatically while leaving the rest of the problem on the low-bias simulation averaged estimator. -Because the negative-flux demotion is decided once, from each cell's accumulated -(converged) flux at the end of the inactive phase rather than from individual -per-iteration negatives, it avoids the small upward bias that per-iteration -demotion can introduce in cells that are noisy but not genuinely negative. The -trade-off is that it does not strictly guarantee non-negative fluxes in every -active-phase cell; the rare near-zero cells that fluctuate negative are filtered -out by the weight-window generator, which discards non-positive fluxes. +Because the negative-flux and strong-feed demotions are decided once, from +each cell's accumulated (converged) flux at the end of the inactive phase +rather than from individual per-iteration values, they avoid the small upward +bias that per-iteration demotion can introduce in cells that are noisy but +not genuinely negative, while still removing -- via the strong-feed latch -- +the strongly fed cell class whose phase-averaged flux could otherwise +straddle zero. Non-negativity is still not strictly enforced on individual +active iterations; any residual non-positive tally values are filtered out by +the weight-window generator, which discards non-positive fluxes. ----------------- Adjoint Flux Mode diff --git a/include/openmc/random_ray/flat_source_domain.h b/include/openmc/random_ray/flat_source_domain.h index d60e6868eb0..206d79a1f3f 100644 --- a/include/openmc/random_ray/flat_source_domain.h +++ b/include/openmc/random_ray/flat_source_domain.h @@ -113,6 +113,13 @@ class FlatSourceDomain { int64_t n_final_small_ {0}; bool final_stats_valid_ {false}; + // One-shot demotion decisions made at the inactive->active transition, + // reported separately from the final-iteration snapshot above (whose + // priority attribution would otherwise hide transition demotions behind + // the per-iteration strong-source cause) + int64_t n_transition_sign_ {0}; // accumulated inactive flux negative + int64_t n_transition_latch_ {0}; // strong accumulated feed (kappa latch) + // 1D array representing source region starting offset for each OpenMC Cell // in model::cells vector source_region_offsets_; diff --git a/src/random_ray/flat_source_domain.cpp b/src/random_ray/flat_source_domain.cpp index dce34e1f34d..22fdebbc7e3 100644 --- a/src/random_ray/flat_source_domain.cpp +++ b/src/random_ray/flat_source_domain.cpp @@ -106,18 +106,35 @@ void FlatSourceDomain::accumulate_iteration_flux() // Demotion step for the adaptive volume estimator (no-op for the // others). Rather than reacting to per-iteration negatives, this estimator // runs the unmodified simulation-averaged update throughout the inactive phase -// and decides demotion once, from the actual sign of each region's converged -// estimate. During the inactive phase the flux is accumulated as computed; -// on the final inactive batch, any region whose accumulated flux is negative -// in any group is demoted to the naive (iteration) volume estimator for the -// active phase -- a positively weighted estimator that cannot go negative with -// a non-negative source -- while every other region keeps the unbiased -// simulation-averaged estimator. Because the decision is made on the -// accumulated mean rather than on individual fluctuations, the lower tail of -// the noise distribution is not clipped, so regions that are merely noisy (and -// average non-negative) are left unbiased. The demotion is recorded in -// converged_negative (>= 1 == demoted), consumed by the volume switch and miss -// treatment in add_source_to_scalar_flux. +// and makes two one-shot demotion decisions at the inactive->active +// transition, each from the accumulated (converged) inactive flux: +// +// 1. Converged-negative (sign): any region whose accumulated flux is +// negative in any group is demoted to the naive (iteration) volume +// estimator for the active phase -- a positively weighted estimator that +// cannot go negative with a non-negative source. Because the decision is +// made on the accumulated mean rather than on individual fluctuations, +// the lower tail of the noise distribution is not clipped, so regions +// that are merely noisy (and average non-negative) are left unbiased. +// +// 2. Strong-feed latch: any region whose flux-independent feed (cross-group +// in-scatter, fission, and external source), evaluated from the same +// accumulated flux, exceeds ADAPTIVE_VOLUME_KAPPA times its own +// accumulated flux in any group is likewise demoted for the active +// phase. This is the same physical condition the per-iteration +// strong-source test targets, decided from converged data: the +// per-iteration test, evaluated on noisy iterates, cannot fire in the +// joint excursion where a bad iteration drags a region's source and flux +// negative together, so a strong region would otherwise ride such +// excursions on unprotected simulation-averaged updates and can +// accumulate a negative window average. The latch removes the whole +// strong-feed class ahead of time. A region with no cross-group or +// external feed can never latch, so sign-locked (e.g. one-group) noise +// cannot cause demotion through this path. +// +// The decisions are recorded in converged_negative (1 = sign, 2 = latch; +// > 0 == demoted), consumed by the volume switch and miss treatment in +// add_source_to_scalar_flux and by the linear-source gradient fallback. void FlatSourceDomain::inactive_demotion_step() { if (volume_estimator_ != RandomRayVolumeEstimator::ADAPTIVE) @@ -145,11 +162,76 @@ void FlatSourceDomain::inactive_demotion_step() break; } } - source_regions_.converged_negative(sr) = negative ? 1 : 0; + // One-shot strong-feed latch, decided here at the moment of maximum + // information from the same inactive-accumulated flux: a region whose + // flux-independent feed (cross-group in-scatter, fission, and external + // source) exceeds kappa times its own accumulated flux in any group is + // demoted for the entire active phase. This covers the joint excursion + // (per-iteration source and flux dragged negative together) that the + // per-iteration strong-source test cannot fire on, with a label that + // active-phase noise can never flip. A region with no cross-group or + // external feed can never latch, so sign-locked (one-group) noise + // cannot cause demotion through this path. + bool latched = false; + int material = source_regions_.material(sr); + if (!negative && material != MATERIAL_VOID) { + int temp = source_regions_.temperature_idx(sr); + const int material_offset = + (material * ntemperature_ + temp) * negroups_; + const int scatter_offset = + (material * ntemperature_ + temp) * negroups_ * negroups_; + double inverse_k_eff = 1.0 / k_eff_; + for (int g = 0; g < negroups_ && !latched; g++) { + double feed = 0.0; + double chi = chi_[material_offset + g]; + for (int gp = 0; gp < negroups_; gp++) { + double phi = + std::max(source_regions_.scalar_flux_final(sr, gp), 0.0); + if (gp != g) { + feed += sigma_s_[scatter_offset + g * negroups_ + gp] * phi; + } + if (settings::create_fission_neutrons) { + feed += + chi * nu_sigma_f_[material_offset + gp] * phi * inverse_k_eff; + } + } + double sigma_t = sigma_t_[material_offset + g]; + double q_indep = feed / sigma_t; + // The external source arrays are only allocated in fixed source + // mode, so the external term must not be read in an eigenvalue + // solve (where no external sources exist). + if (settings::run_mode == RunMode::FIXED_SOURCE) { + q_indep += + settings::n_inactive * source_regions_.external_source(sr, g); + } + if (q_indep > + ADAPTIVE_VOLUME_KAPPA * + std::max(source_regions_.scalar_flux_final(sr, g), 0.0)) { + latched = true; + } + } + } + source_regions_.converged_negative(sr) = negative ? 1 : (latched ? 2 : 0); for (int g = 0; g < negroups_; g++) { source_regions_.scalar_flux_final(sr, g) = 0.0; } } + // Record the transition decisions for the end-of-run report (the + // final-iteration by-cause snapshot cannot show them: its priority + // attribution files most transition-demoted regions under the + // per-iteration strong-source cause). + int64_t n_sign = 0; + int64_t n_latch = 0; +#pragma omp parallel for reduction(+ : n_sign, n_latch) + for (int64_t sr = 0; sr < n_source_regions(); sr++) { + if (source_regions_.converged_negative(sr) == 1) { + n_sign++; + } else if (source_regions_.converged_negative(sr) == 2) { + n_latch++; + } + } + n_transition_sign_ = n_sign; + n_transition_latch_ = n_latch; } } diff --git a/src/random_ray/linear_source_domain.cpp b/src/random_ray/linear_source_domain.cpp index d9bcf7ffb37..d4ff04d847b 100644 --- a/src/random_ray/linear_source_domain.cpp +++ b/src/random_ray/linear_source_domain.cpp @@ -119,10 +119,12 @@ void LinearSourceDomain::update_single_neutron_source(SourceRegionHandle& srh) // scalar flux, so the flat-source cancellation must be exact; the gradient // terms attenuate segments against the local rather than the flat source, // introducing per-iteration noise at the gradient scale that the volume - // choice cannot cancel. For converged-negative regions (demoted at the end - // of the inactive phase), the accumulated flux itself is negative, so the - // fitted gradients carry no meaningful shape information and only inject - // noise into cells already prone to negativity. + // choice cannot cancel. For regions demoted at the end of the inactive + // phase (converged_negative > 0: negative accumulated flux, or the + // strong-feed latch), the same reasoning applies to their cause -- a + // negative accumulated flux means the fitted gradients carry no meaningful + // shape information, and a latched strong feed is the gradient-scale noise + // hazard the strong-source fallback above exists for. if (volume_estimator_ == RandomRayVolumeEstimator::ADAPTIVE && material != MATERIAL_VOID && (srh.converged_negative() > 0 || diff --git a/src/random_ray/random_ray_simulation.cpp b/src/random_ray/random_ray_simulation.cpp index 02a5cef59dc..1c299044be5 100644 --- a/src/random_ray/random_ray_simulation.cpp +++ b/src/random_ray/random_ray_simulation.cpp @@ -643,6 +643,11 @@ void RandomRaySimulation::print_results_random_ray( fmt::print(" Demoted -> Flat (linear) = {} SRs ({:.4f}%)\n", n_flat, n_flat * inv); } + fmt::print(" Transition Demotions (decided at end of inactive):\n"); + fmt::print(" Converged Negative (sign) = {} SRs ({:.4f}%)\n", + domain_->n_transition_sign_, domain_->n_transition_sign_ * inv); + fmt::print(" Strong-Feed Latch = {} SRs ({:.4f}%)\n", + domain_->n_transition_latch_, domain_->n_transition_latch_ * inv); } std::string adjoint_true = From 07fc29f306938c3e52b7663f96b9e7f1e1100692 Mon Sep 17 00:00:00 2001 From: John Tramm Date: Fri, 10 Jul 2026 22:51:11 +0000 Subject: [PATCH 10/35] Regenerate the two starved adaptive golds for the strong-feed latch The ray-starved adaptive tests exist as sensitive detectors for changes to the adaptive estimator's demotion policy, and the latch fires in both at their starved densities (a noisy 20-batch accumulated flux trips the feed test in the adjoint fixed-source problem via the external term and in the starved eigenvalue lattice via the fission feed). The other thirteen tests in the adaptive-covered set -- including every volume-estimator parametrization and the pinned-hybrid suite -- pass against their existing golds byte-for-byte. Co-Authored-By: Claude Fable 5 --- .../adaptive_starved/results_true.dat | 12 +- .../adaptive_starved/results_true.dat | 256 +++++++++--------- 2 files changed, 134 insertions(+), 134 deletions(-) diff --git a/tests/regression_tests/random_ray_adjoint_fixed_source/adaptive_starved/results_true.dat b/tests/regression_tests/random_ray_adjoint_fixed_source/adaptive_starved/results_true.dat index cdc188654ff..041cdf4446c 100644 --- a/tests/regression_tests/random_ray_adjoint_fixed_source/adaptive_starved/results_true.dat +++ b/tests/regression_tests/random_ray_adjoint_fixed_source/adaptive_starved/results_true.dat @@ -1,9 +1,9 @@ tally 1: -2.521404E+06 -1.138286E+12 +3.448604E+06 +8.591189E+11 tally 2: -4.652820E+06 -1.100178E+12 +4.618544E+06 +1.081124E+12 tally 3: -1.520574E+07 -1.160165E+13 +1.520575E+07 +1.160167E+13 diff --git a/tests/regression_tests/random_ray_k_eff_mesh/adaptive_starved/results_true.dat b/tests/regression_tests/random_ray_k_eff_mesh/adaptive_starved/results_true.dat index ba6db4b17ec..bd6fd607b9a 100644 --- a/tests/regression_tests/random_ray_k_eff_mesh/adaptive_starved/results_true.dat +++ b/tests/regression_tests/random_ray_k_eff_mesh/adaptive_starved/results_true.dat @@ -1,171 +1,171 @@ k-combined: -1.107142E+00 1.651536E-02 +1.107140E+00 1.651517E-02 tally 1: -5.381531E+00 -1.457996E+00 -1.989743E+00 -1.995903E-01 -4.842636E+00 -1.182249E+00 -3.851222E+00 -7.436599E-01 -5.695223E-01 -1.627091E-02 -1.386103E+00 -9.637883E-02 -2.905403E+00 -4.222679E-01 -9.355516E-02 -4.378823E-04 -2.276945E-01 -2.593744E-03 -3.728200E+00 -6.951912E-01 -1.234736E-01 -7.626279E-04 -3.005101E-01 -4.517337E-03 -9.698295E+00 -4.703409E+00 +5.381515E+00 +1.457987E+00 +1.989733E+00 +1.995883E-01 +4.842613E+00 +1.182238E+00 +3.851215E+00 +7.436570E-01 +5.695206E-01 +1.627081E-02 +1.386099E+00 +9.637823E-02 +2.905402E+00 +4.222675E-01 +9.355508E-02 +4.378815E-04 +2.276943E-01 +2.593739E-03 +3.728197E+00 +6.951902E-01 +1.234734E-01 +7.626255E-04 +3.005096E-01 +4.517322E-03 +9.698301E+00 +4.703415E+00 1.132062E-01 -6.408809E-04 -2.755246E-01 -3.796276E-03 -2.110499E+01 -2.227489E+01 -3.196600E-02 -5.110820E-05 -7.909764E-02 -3.129255E-04 -1.114268E+01 -6.213561E+00 -1.495582E-01 -1.120790E-03 -4.159886E-01 -8.670950E-03 -8.789785E+00 -3.891485E+00 +6.408815E-04 +2.755247E-01 +3.796279E-03 +2.110508E+01 +2.227507E+01 +3.196617E-02 +5.110874E-05 +7.909806E-02 +3.129288E-04 +1.114275E+01 +6.213641E+00 +1.495597E-01 +1.120813E-03 +4.159926E-01 +8.671123E-03 +8.789783E+00 +3.891484E+00 0.000000E+00 0.000000E+00 0.000000E+00 0.000000E+00 4.704892E+00 -1.109878E+00 +1.109877E+00 0.000000E+00 0.000000E+00 0.000000E+00 0.000000E+00 -3.067155E+00 -4.705500E-01 +3.067156E+00 +4.705503E-01 0.000000E+00 0.000000E+00 0.000000E+00 0.000000E+00 -4.067600E+00 -8.274723E-01 +4.067603E+00 +8.274733E-01 0.000000E+00 0.000000E+00 0.000000E+00 0.000000E+00 -9.782503E+00 -4.785373E+00 +9.782515E+00 +4.785384E+00 0.000000E+00 0.000000E+00 0.000000E+00 0.000000E+00 -1.891641E+01 -1.789486E+01 +1.891645E+01 +1.789493E+01 0.000000E+00 0.000000E+00 0.000000E+00 0.000000E+00 -9.665519E+00 -4.680509E+00 +9.665536E+00 +4.680525E+00 0.000000E+00 0.000000E+00 0.000000E+00 0.000000E+00 -4.684830E+00 -1.105783E+00 -1.748908E+00 -1.543693E-01 -4.256492E+00 -9.143881E-01 +4.684829E+00 +1.105782E+00 +1.748907E+00 +1.543692E-01 +4.256490E+00 +9.143875E-01 3.633615E+00 -6.621069E-01 -5.450799E-01 +6.621067E-01 +5.450798E-01 1.490503E-02 1.326615E+00 -8.828821E-02 +8.828818E-02 2.859894E+00 -4.091636E-01 -9.369197E-02 -4.391332E-04 +4.091638E-01 +9.369199E-02 +4.391334E-04 2.280275E-01 -2.601154E-03 -3.656465E+00 -6.687233E-01 +2.601155E-03 +3.656467E+00 +6.687240E-01 1.230423E-01 -7.573239E-04 -2.994603E-01 -4.485919E-03 -9.693290E+00 -4.698572E+00 -1.151232E-01 -6.627400E-04 -2.801904E-01 -3.925758E-03 -2.161500E+01 -2.336275E+01 -3.339302E-02 -5.577207E-05 -8.262868E-02 -3.414815E-04 -1.149136E+01 -6.606940E+00 -1.577250E-01 -1.246057E-03 -4.387042E-01 -9.640071E-03 -5.393970E+00 -1.465845E+00 +7.573246E-04 +2.994604E-01 +4.485923E-03 +9.693300E+00 +4.698581E+00 +1.151234E-01 +6.627413E-04 +2.801907E-01 +3.925766E-03 +2.161503E+01 +2.336283E+01 +3.339307E-02 +5.577227E-05 +8.262883E-02 +3.414827E-04 +1.149137E+01 +6.606960E+00 +1.577253E-01 +1.246060E-03 +4.387049E-01 +9.640098E-03 +5.393968E+00 +1.465844E+00 2.033639E+00 -2.084426E-01 -4.949471E+00 +2.084425E-01 +4.949469E+00 1.234685E+00 -3.854189E+00 -7.448976E-01 -5.821725E-01 -1.699727E-02 +3.854188E+00 +7.448973E-01 +5.821724E-01 +1.699726E-02 1.416891E+00 1.006813E-01 2.904585E+00 -4.220256E-01 -9.569168E-02 -4.580679E-04 -2.328944E-01 -2.713311E-03 -3.721209E+00 -6.925841E-01 -1.259837E-01 -7.939399E-04 -3.066192E-01 -4.702809E-03 -9.683524E+00 -4.689009E+00 -1.157048E-01 -6.694632E-04 -2.816057E-01 -3.965583E-03 -2.116976E+01 -2.241261E+01 -3.293797E-02 -5.426754E-05 -8.150270E-02 -3.322695E-04 -1.127460E+01 -6.361186E+00 -1.560965E-01 -1.221255E-03 -4.341745E-01 -9.448188E-03 +4.220258E-01 +9.569171E-02 +4.580681E-04 +2.328945E-01 +2.713312E-03 +3.721210E+00 +6.925849E-01 +1.259838E-01 +7.939406E-04 +3.066194E-01 +4.702814E-03 +9.683534E+00 +4.689019E+00 +1.157049E-01 +6.694646E-04 +2.816060E-01 +3.965591E-03 +2.116980E+01 +2.241270E+01 +3.293803E-02 +5.426774E-05 +8.150285E-02 +3.322708E-04 +1.127462E+01 +6.361206E+00 +1.560967E-01 +1.221259E-03 +4.341751E-01 +9.448219E-03 From 6a6c2eae3f93a7c266e6bcb3aff11a50f2484644 Mon Sep 17 00:00:00 2001 From: John Tramm Date: Sat, 11 Jul 2026 01:05:56 +0000 Subject: [PATCH 11/35] State the latch's effect without referencing validation problems The user guide should describe what the mechanism does, not cite the PR's validation suite. Co-Authored-By: Claude Fable 5 --- docs/source/usersguide/random_ray.rst | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/docs/source/usersguide/random_ray.rst b/docs/source/usersguide/random_ray.rst index 3e19298a03b..a338dd915f4 100644 --- a/docs/source/usersguide/random_ray.rst +++ b/docs/source/usersguide/random_ray.rst @@ -1080,8 +1080,8 @@ following methods are currently available in OpenMC: and hybrid estimators can exhibit in optically thin, in-scatter-fed fixed source problems * The converged-feed latch removes the strongly fed cell population - whose phase-averaged flux could otherwise straddle zero, so tallies - are free of negative bins in all validation problems + whose phase-averaged flux could otherwise straddle zero, eliminating + the negative tally bins that class otherwise produces * No parameters to tune - * Does not strictly guarantee non-negative fluxes on individual active iterations (any residual non-positive tally values are From e6f715f9962c3365c21127a86d96c3777cd03ffd Mon Sep 17 00:00:00 2001 From: John Tramm Date: Fri, 17 Jul 2026 01:48:08 +0000 Subject: [PATCH 12/35] Report a single demotion count by default, details at verbosity 8 The end-of-run block had grown to nine diagnostic lines whose labels (strong-feed latch, transition demotions) mean little without developer context. At the default verbosity the report now prints one line -- 'Number of Naive Demotions', the count of source regions receiving the naive volume treatment in the final batch for any reason (the one-shot transition demotions plus that batch's per-iteration demotions) -- which is the barometer a user actually needs. The per-cause breakdown and the transition-decision counts move behind verbosity 8, a previously unused level that sits above the default (7) and below the per-particle output (9), and the verbosity table in the settings documentation gains the level-8 entry. Co-Authored-By: Claude Fable 5 --- docs/source/io_formats/settings.rst | 1 + src/random_ray/random_ray_simulation.cpp | 49 ++++++++++++++---------- 2 files changed, 29 insertions(+), 21 deletions(-) diff --git a/docs/source/io_formats/settings.rst b/docs/source/io_formats/settings.rst index 32d9e83f60e..34415c94fa1 100644 --- a/docs/source/io_formats/settings.rst +++ b/docs/source/io_formats/settings.rst @@ -1575,6 +1575,7 @@ and 10. The verbosity levels are defined as follows: :5: all of the above + file I/O :6: all of the above + timing statistics and initialization messages :7: all of the above + :math:`k` by generation + :8: all of the above + random ray volume-estimator diagnostics :9: all of the above + indicate when each particle starts :10: all of the above + event information diff --git a/src/random_ray/random_ray_simulation.cpp b/src/random_ray/random_ray_simulation.cpp index 1c299044be5..42093e4988e 100644 --- a/src/random_ray/random_ray_simulation.cpp +++ b/src/random_ray/random_ray_simulation.cpp @@ -625,29 +625,36 @@ void RandomRaySimulation::print_results_random_ray( fmt::print(" Volume Estimator Type = {}\n", estimator); if (domain_->final_stats_valid_) { double inv = 100.0 / domain_->n_source_regions(); - fmt::print(" Naive Volume Treatment (final iteration, by cause):\n"); - fmt::print(" Total = {} SRs ({:.4f}%)\n", + // Single summary at default verbosity: every source region that + // received the naive volume treatment in the final batch, for any + // reason (the one-shot demotions decided at the inactive->active + // transition plus that batch's per-iteration demotions). + fmt::print(" Number of Naive Demotions = {} SRs ({:.4f}%)\n", domain_->n_final_naive_, domain_->n_final_naive_ * inv); - fmt::print(" Strong Source = {} SRs ({:.4f}%)\n", - domain_->n_final_strong_, domain_->n_final_strong_ * inv); - fmt::print(" Converged Negative (demoted) = {} SRs ({:.4f}%)\n", - domain_->n_final_demoted_, domain_->n_final_demoted_ * inv); - fmt::print(" Hit-Starved (Small) = {} SRs ({:.4f}%)\n", - domain_->n_final_small_, domain_->n_final_small_ * inv); - // For linear-source runs, the strong-source and converged-negative - // regions additionally have their source gradients zeroed, reverting - // them to a flat source; this is the same data the volume switch uses, - // reported here as the linear -> flat fallback frequency. - if (RandomRay::source_shape_ != RandomRaySourceShape::FLAT) { - int64_t n_flat = domain_->n_final_strong_ + domain_->n_final_demoted_; - fmt::print(" Demoted -> Flat (linear) = {} SRs ({:.4f}%)\n", - n_flat, n_flat * inv); + // The per-cause diagnostic breakdown is developer-facing; verbosity 8 + // sits above the default (7) but below the per-particle output (9). + if (settings::verbosity >= 8) { + fmt::print(" Strong Source = {} SRs ({:.4f}%)\n", + domain_->n_final_strong_, domain_->n_final_strong_ * inv); + fmt::print(" Converged Negative (demoted) = {} SRs ({:.4f}%)\n", + domain_->n_final_demoted_, domain_->n_final_demoted_ * inv); + fmt::print(" Hit-Starved (Small) = {} SRs ({:.4f}%)\n", + domain_->n_final_small_, domain_->n_final_small_ * inv); + // For linear-source runs, the strong-source and converged-negative + // regions additionally have their source gradients zeroed, reverting + // them to a flat source; this is the same data the volume switch + // uses, reported here as the linear -> flat fallback frequency. + if (RandomRay::source_shape_ != RandomRaySourceShape::FLAT) { + int64_t n_flat = domain_->n_final_strong_ + domain_->n_final_demoted_; + fmt::print(" Demoted -> Flat (linear) = {} SRs ({:.4f}%)\n", + n_flat, n_flat * inv); + } + fmt::print(" Transition Demotions (decided at end of inactive):\n"); + fmt::print(" Converged Negative (sign) = {} SRs ({:.4f}%)\n", + domain_->n_transition_sign_, domain_->n_transition_sign_ * inv); + fmt::print(" Strong-Feed Latch = {} SRs ({:.4f}%)\n", + domain_->n_transition_latch_, domain_->n_transition_latch_ * inv); } - fmt::print(" Transition Demotions (decided at end of inactive):\n"); - fmt::print(" Converged Negative (sign) = {} SRs ({:.4f}%)\n", - domain_->n_transition_sign_, domain_->n_transition_sign_ * inv); - fmt::print(" Strong-Feed Latch = {} SRs ({:.4f}%)\n", - domain_->n_transition_latch_, domain_->n_transition_latch_ * inv); } std::string adjoint_true = From 544f36832aa2eebac33d1a9606ea67081ffb22bb Mon Sep 17 00:00:00 2001 From: John Tramm Date: Fri, 17 Jul 2026 14:25:24 +0000 Subject: [PATCH 13/35] Partition the demotion diagnostics into one coherent cause breakdown The verbosity-8 view had two blocks describing the same regions with colliding vocabulary: 'Converged Negative (demoted)' was a residual attribution bucket while 'Converged Negative (sign)' was a decision count, and 'Strong-Feed Latch' is the same physical condition as 'Strong Source', just decided once from converged data instead of per batch. Replace both blocks with a single partition of the demotion total into four mutually exclusive causes named on a consistent cause x when-decided axis: Strong source (end of inactive) Strong source (per batch) Negative flux (end of inactive) Hit-starved (per batch) The transition decisions are counted with first priority in the final-batch snapshot (their flags are fixed for the whole active phase, so those counts equal the decisions made at the transition), which removes the need for the separate transition-count bookkeeping and its report block entirely, and makes visible at a glance how few regions the per-batch test catches beyond the latched population. Co-Authored-By: Claude Fable 5 --- .../openmc/random_ray/flat_source_domain.h | 24 +++++----- src/random_ray/flat_source_domain.cpp | 46 +++++++++---------- src/random_ray/random_ray_simulation.cpp | 31 +++++++------ 3 files changed, 49 insertions(+), 52 deletions(-) diff --git a/include/openmc/random_ray/flat_source_domain.h b/include/openmc/random_ray/flat_source_domain.h index 206d79a1f3f..0e96f29e6bd 100644 --- a/include/openmc/random_ray/flat_source_domain.h +++ b/include/openmc/random_ray/flat_source_domain.h @@ -104,22 +104,20 @@ class FlatSourceDomain { int64_t n_external_source_regions_ {0}; // Total number of source regions with // non-zero external source terms - // Final-iteration snapshot of the naive volume treatment, classified by - // cause with mutually exclusive attribution (the cause lines sum to the - // total), for end-of-simulation reporting + // Final-batch snapshot of the naive volume treatment, partitioned by + // mutually exclusive cause (the four counts sum to n_final_naive_), for + // end-of-simulation reporting. The two one-shot demotions decided at the + // inactive->active transition are counted with first priority -- a strong + // converged (accumulated) feed and a negative converged flux -- so their + // counts equal the decisions made at the transition; the per-batch + // strong-source test and hit-starved causes count the remainder. int64_t n_final_naive_ {0}; - int64_t n_final_strong_ {0}; - int64_t n_final_demoted_ {0}; - int64_t n_final_small_ {0}; + int64_t n_final_latch_ {0}; // strong source, from the converged feed + int64_t n_final_strong_ {0}; // strong source, from the per-batch test + int64_t n_final_sign_ {0}; // negative converged flux + int64_t n_final_small_ {0}; // hit-starved bool final_stats_valid_ {false}; - // One-shot demotion decisions made at the inactive->active transition, - // reported separately from the final-iteration snapshot above (whose - // priority attribution would otherwise hide transition demotions behind - // the per-iteration strong-source cause) - int64_t n_transition_sign_ {0}; // accumulated inactive flux negative - int64_t n_transition_latch_ {0}; // strong accumulated feed (kappa latch) - // 1D array representing source region starting offset for each OpenMC Cell // in model::cells vector source_region_offsets_; diff --git a/src/random_ray/flat_source_domain.cpp b/src/random_ray/flat_source_domain.cpp index 22fdebbc7e3..5ae1018fa89 100644 --- a/src/random_ray/flat_source_domain.cpp +++ b/src/random_ray/flat_source_domain.cpp @@ -216,22 +216,10 @@ void FlatSourceDomain::inactive_demotion_step() source_regions_.scalar_flux_final(sr, g) = 0.0; } } - // Record the transition decisions for the end-of-run report (the - // final-iteration by-cause snapshot cannot show them: its priority - // attribution files most transition-demoted regions under the - // per-iteration strong-source cause). - int64_t n_sign = 0; - int64_t n_latch = 0; -#pragma omp parallel for reduction(+ : n_sign, n_latch) - for (int64_t sr = 0; sr < n_source_regions(); sr++) { - if (source_regions_.converged_negative(sr) == 1) { - n_sign++; - } else if (source_regions_.converged_negative(sr) == 2) { - n_latch++; - } - } - n_transition_sign_ = n_sign; - n_transition_latch_ = n_latch; + // No separate decision-count bookkeeping is needed here: the flags are + // fixed for the whole active phase, so the final-batch by-cause snapshot + // in add_source_to_scalar_flux (which counts them with first priority) + // reports these decisions exactly. } } @@ -390,8 +378,9 @@ int64_t FlatSourceDomain::add_source_to_scalar_flux() int64_t n_hits = 0; double inverse_batch = 1.0 / simulation::current_batch; int64_t n_naive = 0; + int64_t n_latch = 0; int64_t n_strong = 0; - int64_t n_demoted = 0; + int64_t n_sign = 0; int64_t n_small = 0; bool final_iteration = (simulation::current_batch == settings::n_batches); // The adaptive estimator uses the proactive strong-source (kappa) test, the @@ -402,7 +391,7 @@ int64_t FlatSourceDomain::add_source_to_scalar_flux() volume_estimator_ == RandomRayVolumeEstimator::ADAPTIVE; #pragma omp parallel for reduction( \ - + : n_hits, n_naive, n_strong, n_demoted, n_small) + + : n_hits, n_naive, n_latch, n_strong, n_sign, n_small) for (int64_t sr = 0; sr < n_source_regions(); sr++) { double volume_simulation_avg = source_regions_.volume(sr); @@ -469,7 +458,8 @@ int64_t FlatSourceDomain::add_source_to_scalar_flux() // term is folded into q/Sigma_t. All are g-independent. bool external = source_regions_.external_source_present(sr); bool small = source_regions_.is_small(sr); - bool converged_neg = source_regions_.converged_negative(sr) > 0; + int conv_flag = source_regions_.converged_negative(sr); + bool converged_neg = conv_flag > 0; // Every estimator reduces to two g-independent per-region decisions: // 1. which volume to use on a hit -- the simulation-averaged volume, @@ -506,14 +496,19 @@ int64_t FlatSourceDomain::add_source_to_scalar_flux() double volume = use_naive_volume ? volume_iteration : volume_simulation_avg; // On the final iteration, classify the demoted (naive-volume) regions by - // cause -- mutually exclusive, in priority order, so the causes sum to the - // total -- for the end-of-simulation report. + // cause -- mutually exclusive, in priority order, so the causes sum to + // the total -- for the end-of-simulation report. The one-shot transition + // demotions are counted first (their flags are fixed for the whole + // active phase, so these counts equal the decisions made at the end of + // the inactive phase); the per-batch causes count only the remainder. if (final_iteration && is_adaptive && use_naive_volume) { n_naive++; - if (strong_source) { + if (conv_flag == 2) { + n_latch++; + } else if (conv_flag == 1) { + n_sign++; + } else if (strong_source) { n_strong++; - } else if (converged_neg) { - n_demoted++; } else if (small) { n_small++; } @@ -549,8 +544,9 @@ int64_t FlatSourceDomain::add_source_to_scalar_flux() if (final_iteration && volume_estimator_ == RandomRayVolumeEstimator::ADAPTIVE) { n_final_naive_ = n_naive; + n_final_latch_ = n_latch; n_final_strong_ = n_strong; - n_final_demoted_ = n_demoted; + n_final_sign_ = n_sign; n_final_small_ = n_small; final_stats_valid_ = true; } diff --git a/src/random_ray/random_ray_simulation.cpp b/src/random_ray/random_ray_simulation.cpp index 42093e4988e..2f10b9b285f 100644 --- a/src/random_ray/random_ray_simulation.cpp +++ b/src/random_ray/random_ray_simulation.cpp @@ -633,27 +633,30 @@ void RandomRaySimulation::print_results_random_ray( domain_->n_final_naive_, domain_->n_final_naive_ * inv); // The per-cause diagnostic breakdown is developer-facing; verbosity 8 // sits above the default (7) but below the per-particle output (9). + // The causes are mutually exclusive and sum to the total above: + // "end of inactive" causes are the one-shot demotions decided from the + // converged (accumulated) inactive flux, "per batch" causes are + // re-evaluated each batch and reported for the final batch. if (settings::verbosity >= 8) { - fmt::print(" Strong Source = {} SRs ({:.4f}%)\n", + fmt::print(" Strong source (end of inactive) = {} SRs ({:.4f}%)\n", + domain_->n_final_latch_, domain_->n_final_latch_ * inv); + fmt::print(" Strong source (per batch) = {} SRs ({:.4f}%)\n", domain_->n_final_strong_, domain_->n_final_strong_ * inv); - fmt::print(" Converged Negative (demoted) = {} SRs ({:.4f}%)\n", - domain_->n_final_demoted_, domain_->n_final_demoted_ * inv); - fmt::print(" Hit-Starved (Small) = {} SRs ({:.4f}%)\n", + fmt::print(" Negative flux (end of inactive) = {} SRs ({:.4f}%)\n", + domain_->n_final_sign_, domain_->n_final_sign_ * inv); + fmt::print(" Hit-starved (per batch) = {} SRs ({:.4f}%)\n", domain_->n_final_small_, domain_->n_final_small_ * inv); - // For linear-source runs, the strong-source and converged-negative - // regions additionally have their source gradients zeroed, reverting - // them to a flat source; this is the same data the volume switch - // uses, reported here as the linear -> flat fallback frequency. + // For linear-source runs, every demoted region except the purely + // hit-starved additionally has its source gradients zeroed, + // reverting it to a flat source; this is the same data the volume + // switch uses, reported here as the linear -> flat fallback + // frequency. if (RandomRay::source_shape_ != RandomRaySourceShape::FLAT) { - int64_t n_flat = domain_->n_final_strong_ + domain_->n_final_demoted_; + int64_t n_flat = domain_->n_final_latch_ + domain_->n_final_strong_ + + domain_->n_final_sign_; fmt::print(" Demoted -> Flat (linear) = {} SRs ({:.4f}%)\n", n_flat, n_flat * inv); } - fmt::print(" Transition Demotions (decided at end of inactive):\n"); - fmt::print(" Converged Negative (sign) = {} SRs ({:.4f}%)\n", - domain_->n_transition_sign_, domain_->n_transition_sign_ * inv); - fmt::print(" Strong-Feed Latch = {} SRs ({:.4f}%)\n", - domain_->n_transition_latch_, domain_->n_transition_latch_ * inv); } } From a50797fb9c77514df9619d625dca8e495bc07e00 Mon Sep 17 00:00:00 2001 From: John Tramm Date: Mon, 20 Jul 2026 16:48:55 +0000 Subject: [PATCH 14/35] Drop the redundant linear-to-flat count from the demotion report Under a linear source shape every demoted region already runs with a flat representation -- hit-starved regions through the mainline small-region moment zeroing, the other causes through the adaptive gradient fallback -- so the true linear-to-flat count is simply the demotion total, and the printed line (total minus hit-starved) was both derivable from the other lines and not that count. Co-Authored-By: Claude Fable 5 --- src/random_ray/random_ray_simulation.cpp | 16 +++++----------- 1 file changed, 5 insertions(+), 11 deletions(-) diff --git a/src/random_ray/random_ray_simulation.cpp b/src/random_ray/random_ray_simulation.cpp index 2f10b9b285f..f1c9e27861c 100644 --- a/src/random_ray/random_ray_simulation.cpp +++ b/src/random_ray/random_ray_simulation.cpp @@ -646,17 +646,11 @@ void RandomRaySimulation::print_results_random_ray( domain_->n_final_sign_, domain_->n_final_sign_ * inv); fmt::print(" Hit-starved (per batch) = {} SRs ({:.4f}%)\n", domain_->n_final_small_, domain_->n_final_small_ * inv); - // For linear-source runs, every demoted region except the purely - // hit-starved additionally has its source gradients zeroed, - // reverting it to a flat source; this is the same data the volume - // switch uses, reported here as the linear -> flat fallback - // frequency. - if (RandomRay::source_shape_ != RandomRaySourceShape::FLAT) { - int64_t n_flat = domain_->n_final_latch_ + domain_->n_final_strong_ + - domain_->n_final_sign_; - fmt::print(" Demoted -> Flat (linear) = {} SRs ({:.4f}%)\n", - n_flat, n_flat * inv); - } + // No separate linear -> flat count is reported: under a linear + // source shape every demoted region runs with a flat representation + // (hit-starved regions through the small-region moment zeroing, the + // other causes through the adaptive gradient fallback), so that + // count is simply the demotion total above. } } From 74b89ed155ab28dbf8c3bd937e6055199cab10db Mon Sep 17 00:00:00 2001 From: John Tramm Date: Mon, 20 Jul 2026 16:50:24 +0000 Subject: [PATCH 15/35] Remove a comment explaining a line that is not there Comments describe the code that exists. Co-Authored-By: Claude Fable 5 --- src/random_ray/random_ray_simulation.cpp | 5 ----- 1 file changed, 5 deletions(-) diff --git a/src/random_ray/random_ray_simulation.cpp b/src/random_ray/random_ray_simulation.cpp index f1c9e27861c..51f191e5697 100644 --- a/src/random_ray/random_ray_simulation.cpp +++ b/src/random_ray/random_ray_simulation.cpp @@ -646,11 +646,6 @@ void RandomRaySimulation::print_results_random_ray( domain_->n_final_sign_, domain_->n_final_sign_ * inv); fmt::print(" Hit-starved (per batch) = {} SRs ({:.4f}%)\n", domain_->n_final_small_, domain_->n_final_small_ * inv); - // No separate linear -> flat count is reported: under a linear - // source shape every demoted region runs with a flat representation - // (hit-starved regions through the small-region moment zeroing, the - // other causes through the adaptive gradient fallback), so that - // count is simply the demotion total above. } } From 96361b9ed934b49d5d34aa0e2c42bb0ddc6b20c3 Mon Sep 17 00:00:00 2001 From: John Tramm Date: Mon, 20 Jul 2026 17:36:16 +0000 Subject: [PATCH 16/35] Restrict the per-batch strong-source ratio test to the inactive batches On a healthy overlay-mesh C5G7 eigenvalue (102x102 regions, 200 rays, 0.16% miss), the per-batch ratio test was demoting a churning ~1.4-2% of regions every active batch -- none of them strong in equilibrium (the strong-feed latch fires on zero regions) -- and that noise-conditioned, one-sided treatment biased tallied batches: k ran +18/+19/+18 pcm above simulation_averaged across three paired seeds with a flat source, and +106/+114/+83 pcm above hybrid with linear_xy (where demotion also flattens gradients, holding the solution at the flat-source answer). Two alternatives measured worse or partial: the tilt limiter recovers only ~40 pcm of the linear bias, and removing just the gradient fallback while keeping the volume demotion is worse than baseline. The ratio condition now applies only while the source is converging (the inactive batches), where it is the stability guard that keeps door-1 problems finite. In the active batches the estimator choice is governed by the stable classifications alone -- the strong-feed latch, the converged-negative sign demotion, hit-starved regions, and the per-batch negative-source (TCP0) condition -- so tallied batches never see noise-conditioned demotion. With the change the C5G7 overlay-mesh case gives k within -0/-1/-0 pcm of simulation_averaged (flat, paired seeds) and +11/+18/+8 pcm of hybrid (linear_xy), with pin-power AAPE matching to 0.006%; the irradiation vault (0 negative bins both seeds, totals to the digit), the Cosgrove cube (to the digit), and the TLD room (to the digit) are unchanged. Co-Authored-By: Claude Fable 5 --- docs/source/methods/random_ray.rst | 26 +++++++++----- docs/source/usersguide/random_ray.rst | 15 ++++---- .../openmc/random_ray/flat_source_domain.h | 15 ++++---- src/random_ray/flat_source_domain.cpp | 35 +++++++++++++------ src/random_ray/linear_source_domain.cpp | 3 +- 5 files changed, 62 insertions(+), 32 deletions(-) diff --git a/docs/source/methods/random_ray.rst b/docs/source/methods/random_ray.rst index c065d1fa644..aa946b2f52f 100644 --- a/docs/source/methods/random_ray.rst +++ b/docs/source/methods/random_ray.rst @@ -537,8 +537,14 @@ source below zero even for a non-negative flux), a hit-starved region, a region whose flux converges to a negative value, and a region whose converged flux-independent source ("feed") is strong relative to its own converged flux. -The first three conditions are evaluated each iteration from already-resident -data. The last two are instead decided once, at the transition from the +The negative-source and hit-starved conditions are evaluated each iteration +from already-resident data. The strong-source ratio condition is evaluated +each iteration as well, but only while the source is still converging (the +inactive batches): applied to noisy single-iteration values in the active +batches it would demote a churning population of regions whose converged +ratios are below the threshold, and conditioning the estimator choice on +per-iteration noise in the tallied batches introduces a systematic bias. The +last two conditions are instead decided once, at the transition from the inactive to the active batches: the unmodified simulation averaged estimator is run throughout the inactive phase while each region's flux is accumulated, and at the transition a region is demoted to the naive estimator for all of @@ -546,13 +552,15 @@ the active batches if its accumulated (and therefore noise-averaged) flux is negative in any group, or if its flux-independent feed -- the part of its source arising from cross-group in-scatter, fission, and any external source, evaluated from the same accumulated flux -- exceeds the strong-source -threshold times its own accumulated flux in any group. Deferring these -decisions to the converged estimate -- rather than reacting to individual -per-iteration values -- avoids the upward bias that repairing or demoting on -isolated fluctuations would introduce by clipping only the lower tail of the -estimator's noise distribution; regions that are merely noisy but average -non-negative (and are not strongly fed) retain the unbiased simulation -averaged estimator. +threshold times its own accumulated flux in any group. In the active batches +these one-shot decisions, together with the per-iteration negative-source +and hit-starved conditions, are what govern the estimator choice. Deferring +the noise-sensitive decisions to the converged estimate -- rather than +reacting to individual per-iteration values -- avoids the bias that +demoting on isolated fluctuations would introduce by treating only one tail +of the estimator's noise distribution; regions that are merely noisy but +average non-negative (and are not strongly fed) retain the unbiased +simulation averaged estimator. The feed-based latch exists because the per-iteration strong-source test, evaluated on noisy single-iteration values, has exactly one blind state: an diff --git a/docs/source/usersguide/random_ray.rst b/docs/source/usersguide/random_ray.rst index a338dd915f4..167f579fbe6 100644 --- a/docs/source/usersguide/random_ray.rst +++ b/docs/source/usersguide/random_ray.rst @@ -1062,18 +1062,21 @@ following methods are currently available in OpenMC: - Generalizes the hybrid estimator. Uses the simulation averaged estimator by default, but falls back to the naive estimator (and the previous-iteration miss treatment) wherever it is needed for stability: - cells whose reduced source greatly exceeds their flux (a strong external - or in-scatter source), cells whose reduced source is itself negative - (possible under transport-corrected cross sections), hit-starved cells, - and -- decided once at the end of the inactive phase, from each cell's + during the inactive batches, cells whose reduced source greatly exceeds + their flux (a strong external or in-scatter source); in every batch, + cells whose reduced source is itself negative (possible under + transport-corrected cross sections) and hit-starved cells; and -- + decided once at the end of the inactive phase, from each cell's accumulated (converged) flux -- cells whose accumulated flux is negative as well as cells whose flux-independent feed (cross-group in-scatter, fission, and external source) is strong relative to their own accumulated flux. The transition decisions are one-shot: the unmodified simulation averaged estimator runs throughout the inactive phase, and the demoted cells use the naive estimator for all of the - active batches. The decisions are made automatically from each cell's - accumulated statistics; individual iterations are never modified. + active batches, so the estimator choice in the tallied batches never + depends on single-batch noise. The decisions are made automatically + from each cell's accumulated statistics; individual iterations are + never modified. - * Retains the low bias of the simulation averaged estimator wherever it is well behaved * Eliminates the negative-flux instabilities that the simulation averaged diff --git a/include/openmc/random_ray/flat_source_domain.h b/include/openmc/random_ray/flat_source_domain.h index 0e96f29e6bd..d2692cc7e7e 100644 --- a/include/openmc/random_ray/flat_source_domain.h +++ b/include/openmc/random_ray/flat_source_domain.h @@ -187,13 +187,16 @@ class FlatSourceDomain { virtual void set_flux_to_old_flux(int64_t sr, int g); //! Adaptive-estimator "strong source" test: true if, in any group, the - //! region's reduced source q/Sigma_t is negative or exceeds + //! region's reduced source q/Sigma_t is negative (with a non-negative + //! previous-iteration flux), or -- when include_ratio is set, which the + //! callers do only during the inactive batches -- exceeds //! ADAPTIVE_VOLUME_KAPPA times the (non-negative) previous-iteration scalar - //! flux. Shared by the flat volume switch (add_source_to_scalar_flux) and the - //! linear gradient fallback (update_single_neutron_source); the region's - //! per-group reduced-source and previous-flux arrays are passed directly. - bool region_has_strong_source( - const float* reduced_source, const double* flux_old) const; + //! flux. Shared by the flat volume switch (add_source_to_scalar_flux) and + //! the linear gradient fallback (update_single_neutron_source); the + //! region's per-group reduced-source and previous-flux arrays are passed + //! directly. + bool region_has_strong_source(const float* reduced_source, + const double* flux_old, bool include_ratio) const; //---------------------------------------------------------------------------- // Private data members diff --git a/src/random_ray/flat_source_domain.cpp b/src/random_ray/flat_source_domain.cpp index 5ae1018fa89..3040cb9c0f0 100644 --- a/src/random_ray/flat_source_domain.cpp +++ b/src/random_ray/flat_source_domain.cpp @@ -347,7 +347,7 @@ void FlatSourceDomain::set_flux_to_source(int64_t sr, int g) } bool FlatSourceDomain::region_has_strong_source( - const float* reduced_source, const double* flux_old) const + const float* reduced_source, const double* flux_old, bool include_ratio) const { for (int g = 0; g < negroups_; g++) { double src = reduced_source[g]; @@ -364,7 +364,16 @@ bool FlatSourceDomain::region_has_strong_source( if (src < 0.0 && flux_old[g] >= 0.0) { return true; } - if (src > ADAPTIVE_VOLUME_KAPPA * std::max(flux_old[g], 0.0)) { + // The ratio condition is consulted only while the source is converging + // (include_ratio is false in the active batches): evaluated on noisy + // single-batch iterates, it demotes a churning population of regions + // whose converged ratios are below kappa, and that noise-conditioned, + // one-sided treatment biases the accumulated tallies. Once the + // transition decisions are made from the converged flux, the stable + // classifications (the strong-feed latch, the converged-negative sign + // demotion, and the hit-starved treatment) govern the tallied batches. + if (include_ratio && + src > ADAPTIVE_VOLUME_KAPPA * std::max(flux_old[g], 0.0)) { return true; } } @@ -448,14 +457,20 @@ int64_t FlatSourceDomain::add_source_to_scalar_flux() bool strong_source = is_adaptive && source_regions_.material(sr) != MATERIAL_VOID && region_has_strong_source(&source_regions_.source(sr, 0), - &source_regions_.scalar_flux_old(sr, 0)); - // Per-region demotion reasons. The hit-starved (small) and strong-source - // flags are re-evaluated every iteration; converged_neg is the one-shot - // flag set at the end of the inactive phase by inactive_demotion_step. The - // external-source flag drives only the hybrid policy (and the default miss - // treatment); the adaptive estimator catches a low-cross-section external - // region through the kappa strong-source test instead, since its external - // term is folded into q/Sigma_t. All are g-independent. + &source_regions_.scalar_flux_old(sr, 0), + simulation::current_batch <= settings::n_inactive); + // Per-region demotion reasons. The hit-starved (small) flag is + // re-evaluated every iteration; the strong-source flag is re-evaluated + // every iteration during the inactive batches and reduces to the + // negative-source (TCP0) condition in the active batches, where the + // stable transition decisions govern instead; converged_neg is the + // one-shot flag set at the end of the inactive phase by + // inactive_demotion_step. The external-source flag drives only the + // hybrid policy (and the default miss treatment); the adaptive estimator + // catches a low-cross-section external region through the kappa + // strong-source test (during the inactive batches) and the strong-feed + // latch (thereafter), since its external term is folded into q/Sigma_t. + // All are g-independent. bool external = source_regions_.external_source_present(sr); bool small = source_regions_.is_small(sr); int conv_flag = source_regions_.converged_negative(sr); diff --git a/src/random_ray/linear_source_domain.cpp b/src/random_ray/linear_source_domain.cpp index d4ff04d847b..5184fac77b3 100644 --- a/src/random_ray/linear_source_domain.cpp +++ b/src/random_ray/linear_source_domain.cpp @@ -128,7 +128,8 @@ void LinearSourceDomain::update_single_neutron_source(SourceRegionHandle& srh) if (volume_estimator_ == RandomRayVolumeEstimator::ADAPTIVE && material != MATERIAL_VOID && (srh.converged_negative() > 0 || - region_has_strong_source(&srh.source(0), &srh.scalar_flux_old(0)))) { + region_has_strong_source(&srh.source(0), &srh.scalar_flux_old(0), + simulation::current_batch <= settings::n_inactive))) { for (int g = 0; g < negroups_; g++) { srh.source_gradients(g) = {0.0, 0.0, 0.0}; } From 3e94e0dc436cad2b9143ab80a60c0625917e5da9 Mon Sep 17 00:00:00 2001 From: John Tramm Date: Mon, 20 Jul 2026 17:36:16 +0000 Subject: [PATCH 17/35] Regenerate the two starved adaptive golds for the phase-gated ratio test The ray-starved detector tests have active-phase per-batch ratio demotions on the previous code, so their results shift; the other thirteen adaptive-covered tests pass against their existing golds byte-for-byte. Co-Authored-By: Claude Fable 5 --- .../adaptive_starved/results_true.dat | 12 +- .../adaptive_starved/results_true.dat | 338 +++++++++--------- 2 files changed, 175 insertions(+), 175 deletions(-) diff --git a/tests/regression_tests/random_ray_adjoint_fixed_source/adaptive_starved/results_true.dat b/tests/regression_tests/random_ray_adjoint_fixed_source/adaptive_starved/results_true.dat index 041cdf4446c..b47d764f897 100644 --- a/tests/regression_tests/random_ray_adjoint_fixed_source/adaptive_starved/results_true.dat +++ b/tests/regression_tests/random_ray_adjoint_fixed_source/adaptive_starved/results_true.dat @@ -1,9 +1,9 @@ tally 1: -3.448604E+06 -8.591189E+11 +3.668344E+06 +9.453869E+11 tally 2: -4.618544E+06 -1.081124E+12 +4.649756E+06 +1.094198E+12 tally 3: -1.520575E+07 -1.160167E+13 +1.584906E+07 +1.263152E+13 diff --git a/tests/regression_tests/random_ray_k_eff_mesh/adaptive_starved/results_true.dat b/tests/regression_tests/random_ray_k_eff_mesh/adaptive_starved/results_true.dat index bd6fd607b9a..ae005723c7d 100644 --- a/tests/regression_tests/random_ray_k_eff_mesh/adaptive_starved/results_true.dat +++ b/tests/regression_tests/random_ray_k_eff_mesh/adaptive_starved/results_true.dat @@ -1,171 +1,171 @@ k-combined: -1.107140E+00 1.651517E-02 +1.107189E+00 1.650637E-02 tally 1: -5.381515E+00 -1.457987E+00 -1.989733E+00 -1.995883E-01 -4.842613E+00 -1.182238E+00 -3.851215E+00 -7.436570E-01 -5.695206E-01 -1.627081E-02 -1.386099E+00 -9.637823E-02 -2.905402E+00 -4.222675E-01 -9.355508E-02 -4.378815E-04 -2.276943E-01 -2.593739E-03 -3.728197E+00 -6.951902E-01 -1.234734E-01 -7.626255E-04 -3.005096E-01 -4.517322E-03 -9.698301E+00 -4.703415E+00 -1.132062E-01 -6.408815E-04 -2.755247E-01 -3.796279E-03 -2.110508E+01 -2.227507E+01 -3.196617E-02 -5.110874E-05 -7.909806E-02 -3.129288E-04 -1.114275E+01 -6.213641E+00 -1.495597E-01 -1.120813E-03 -4.159926E-01 -8.671123E-03 -8.789783E+00 -3.891484E+00 -0.000000E+00 -0.000000E+00 -0.000000E+00 -0.000000E+00 -4.704892E+00 -1.109877E+00 -0.000000E+00 -0.000000E+00 -0.000000E+00 -0.000000E+00 -3.067156E+00 -4.705503E-01 -0.000000E+00 -0.000000E+00 -0.000000E+00 -0.000000E+00 -4.067603E+00 -8.274733E-01 -0.000000E+00 -0.000000E+00 -0.000000E+00 -0.000000E+00 -9.782515E+00 -4.785384E+00 -0.000000E+00 -0.000000E+00 -0.000000E+00 -0.000000E+00 -1.891645E+01 -1.789493E+01 -0.000000E+00 -0.000000E+00 -0.000000E+00 -0.000000E+00 -9.665536E+00 -4.680525E+00 -0.000000E+00 -0.000000E+00 -0.000000E+00 -0.000000E+00 -4.684829E+00 -1.105782E+00 -1.748907E+00 -1.543692E-01 -4.256490E+00 -9.143875E-01 -3.633615E+00 -6.621067E-01 -5.450798E-01 -1.490503E-02 -1.326615E+00 -8.828818E-02 -2.859894E+00 -4.091638E-01 -9.369199E-02 -4.391334E-04 -2.280275E-01 -2.601155E-03 -3.656467E+00 -6.687240E-01 -1.230423E-01 -7.573246E-04 -2.994604E-01 -4.485923E-03 -9.693300E+00 -4.698581E+00 -1.151234E-01 -6.627413E-04 -2.801907E-01 -3.925766E-03 -2.161503E+01 -2.336283E+01 -3.339307E-02 -5.577227E-05 -8.262883E-02 -3.414827E-04 -1.149137E+01 -6.606960E+00 -1.577253E-01 -1.246060E-03 -4.387049E-01 -9.640098E-03 -5.393968E+00 -1.465844E+00 -2.033639E+00 -2.084425E-01 -4.949469E+00 -1.234685E+00 -3.854188E+00 -7.448973E-01 -5.821724E-01 -1.699726E-02 -1.416891E+00 -1.006813E-01 -2.904585E+00 -4.220258E-01 -9.569171E-02 -4.580681E-04 -2.328945E-01 -2.713312E-03 -3.721210E+00 -6.925849E-01 -1.259838E-01 -7.939406E-04 -3.066194E-01 -4.702814E-03 -9.683534E+00 -4.689019E+00 -1.157049E-01 -6.694646E-04 -2.816060E-01 -3.965591E-03 -2.116980E+01 -2.241270E+01 -3.293803E-02 -5.426774E-05 -8.150285E-02 -3.322708E-04 -1.127462E+01 -6.361206E+00 -1.560967E-01 -1.221259E-03 -4.341751E-01 -9.448219E-03 +5.382306E+00 +1.458407E+00 +1.990183E+00 +1.996766E-01 +4.843706E+00 +1.182761E+00 +3.851437E+00 +7.437404E-01 +5.695722E-01 +1.627370E-02 +1.386225E+00 +9.639533E-02 +2.905353E+00 +4.222527E-01 +9.355432E-02 +4.378736E-04 +2.276925E-01 +2.593692E-03 +3.728130E+00 +6.951645E-01 +1.234730E-01 +7.626203E-04 +3.005086E-01 +4.517292E-03 +9.697796E+00 +4.702923E+00 +1.132009E-01 +6.408203E-04 +2.755117E-01 +3.795916E-03 +2.110312E+01 +2.227093E+01 +3.196291E-02 +5.109833E-05 +7.908998E-02 +3.128651E-04 +1.114018E+01 +6.210794E+00 +1.495090E-01 +1.120073E-03 +4.158517E-01 +8.665401E-03 +8.789910E+00 +3.891597E+00 +0.000000E+00 +0.000000E+00 +0.000000E+00 +0.000000E+00 +4.704870E+00 +1.109865E+00 +0.000000E+00 +0.000000E+00 +0.000000E+00 +0.000000E+00 +3.067041E+00 +4.705146E-01 +0.000000E+00 +0.000000E+00 +0.000000E+00 +0.000000E+00 +4.067401E+00 +8.273907E-01 +0.000000E+00 +0.000000E+00 +0.000000E+00 +0.000000E+00 +9.781901E+00 +4.784781E+00 +0.000000E+00 +0.000000E+00 +0.000000E+00 +0.000000E+00 +1.891496E+01 +1.789211E+01 +0.000000E+00 +0.000000E+00 +0.000000E+00 +0.000000E+00 +9.664805E+00 +4.679820E+00 +0.000000E+00 +0.000000E+00 +0.000000E+00 +0.000000E+00 +4.684699E+00 +1.105729E+00 +1.748804E+00 +1.543530E-01 +4.256238E+00 +9.142916E-01 +3.633543E+00 +6.620809E-01 +5.450625E-01 +1.490411E-02 +1.326573E+00 +8.828275E-02 +2.859799E+00 +4.091361E-01 +9.368861E-02 +4.391014E-04 +2.280193E-01 +2.600965E-03 +3.656299E+00 +6.686621E-01 +1.230361E-01 +7.572477E-04 +2.994452E-01 +4.485467E-03 +9.692742E+00 +4.698037E+00 +1.151165E-01 +6.626621E-04 +2.801740E-01 +3.925297E-03 +2.161347E+01 +2.335945E+01 +3.339068E-02 +5.576430E-05 +8.262289E-02 +3.414339E-04 +1.149110E+01 +6.606652E+00 +1.577265E-01 +1.246081E-03 +4.387082E-01 +9.640260E-03 +5.394236E+00 +1.465976E+00 +2.033782E+00 +2.084700E-01 +4.949818E+00 +1.234847E+00 +3.854245E+00 +7.449164E-01 +5.821876E-01 +1.699809E-02 +1.416928E+00 +1.006862E-01 +2.904499E+00 +4.220002E-01 +9.568914E-02 +4.580432E-04 +2.328882E-01 +2.713165E-03 +3.721096E+00 +6.925417E-01 +1.259813E-01 +7.939084E-04 +3.066132E-01 +4.702623E-03 +9.683019E+00 +4.688517E+00 +1.156992E-01 +6.693990E-04 +2.815922E-01 +3.965203E-03 +2.116800E+01 +2.240889E+01 +3.293511E-02 +5.425811E-05 +8.149563E-02 +3.322118E-04 +1.127273E+01 +6.359129E+00 +1.560615E-01 +1.220746E-03 +4.340772E-01 +9.444254E-03 From 9e46080eec7f65c965ca9ac1d47f0d9bdf3ca50e Mon Sep 17 00:00:00 2001 From: John Tramm Date: Wed, 22 Jul 2026 04:57:21 +0000 Subject: [PATCH 18/35] Re-evaluate the accumulated demotion decisions throughout the active phase At scale, an inactive phase long enough to converge deep regions may be unaffordable, and the transition-time decisions alone then miss chronically unstable regions whose escapees corrupt the active phase (observed on a 105M-region shielding model as mass negative fluxes). The two accumulated- flux demotions now re-evaluate every active batch, demote-only, from a new running flux accumulator (kept separate from the active-only tally accumulator, which the sign test also watches so a positive inactive sum cannot mask a negative reported mean). Transition-batch decisions are unchanged; the five adaptive golds shift by a few pcm from the added active-phase demotions. Co-Authored-By: Claude Fable 5 --- docs/source/methods/random_ray.rst | 51 ++-- docs/source/usersguide/random_ray.rst | 38 +-- .../openmc/random_ray/flat_source_domain.h | 14 +- include/openmc/random_ray/source_region.h | 29 +- src/random_ray/flat_source_domain.cpp | 210 ++++++++------ src/random_ray/linear_source_domain.cpp | 4 +- src/random_ray/random_ray_simulation.cpp | 29 +- src/random_ray/source_region.cpp | 6 + .../adaptive_starved/results_true.dat | 12 +- .../adaptive/results_true.dat | 2 +- .../adaptive_starved/results_true.dat | 270 +++++++++--------- .../adaptive/results_true.dat | 12 +- .../adaptive/results_true.dat | 12 +- 13 files changed, 374 insertions(+), 315 deletions(-) diff --git a/docs/source/methods/random_ray.rst b/docs/source/methods/random_ray.rst index aa946b2f52f..dbfaa7bdf06 100644 --- a/docs/source/methods/random_ray.rst +++ b/docs/source/methods/random_ray.rst @@ -544,23 +544,34 @@ inactive batches): applied to noisy single-iteration values in the active batches it would demote a churning population of regions whose converged ratios are below the threshold, and conditioning the estimator choice on per-iteration noise in the tallied batches introduces a systematic bias. The -last two conditions are instead decided once, at the transition from the -inactive to the active batches: the unmodified simulation averaged estimator -is run throughout the inactive phase while each region's flux is accumulated, -and at the transition a region is demoted to the naive estimator for all of -the active batches if its accumulated (and therefore noise-averaged) flux is -negative in any group, or if its flux-independent feed -- the part of its -source arising from cross-group in-scatter, fission, and any external source, -evaluated from the same accumulated flux -- exceeds the strong-source -threshold times its own accumulated flux in any group. In the active batches -these one-shot decisions, together with the per-iteration negative-source -and hit-starved conditions, are what govern the estimator choice. Deferring -the noise-sensitive decisions to the converged estimate -- rather than -reacting to individual per-iteration values -- avoids the bias that -demoting on isolated fluctuations would introduce by treating only one tail -of the estimator's noise distribution; regions that are merely noisy but -average non-negative (and are not strongly fed) retain the unbiased -simulation averaged estimator. +last two conditions are instead decided from each region's accumulated flux: +the unmodified simulation averaged estimator is run while every batch's flux +is accumulated into a running sum, and -- beginning at the transition from +the inactive to the active batches, and re-evaluated every active batch as +the accumulation keeps growing -- a region is demoted to the naive estimator +if its accumulated (and therefore noise-averaged) flux is negative in any +group, or if its flux-independent feed -- the part of its source arising +from cross-group in-scatter, fission, and any external source, evaluated +from the same accumulated flux -- exceeds the strong-source threshold times +its own accumulated flux in any group. These decisions are demote-only: once +a region is demoted it is never returned to the simulation averaged +estimator, so the estimator choice cannot churn with active-batch noise, and +a marginal region whose accumulated ratio converges below the threshold is +never eroded into demotion by the continued re-evaluation. The active-phase +re-evaluation matters for problems whose inactive phase is too short to +converge deep regions: there the transition-time decision alone can miss +chronically unstable regions whose accumulated flux only turns negative (or +whose feed ratio only crosses the threshold) after active batches begin, and +a single such region left on unprotected simulation averaged updates can +corrupt the solution well beyond its own boundary through scattering +feedback. In the active batches these accumulated-flux decisions, together +with the per-iteration negative-source and hit-starved conditions, are what +govern the estimator choice. Basing the noise-sensitive decisions on the +accumulated estimate -- rather than reacting to individual per-iteration +values -- avoids the bias that demoting on isolated fluctuations would +introduce by treating only one tail of the estimator's noise distribution; +regions that are merely noisy but average non-negative (and are not strongly +fed) retain the unbiased simulation averaged estimator. The feed-based latch exists because the per-iteration strong-source test, evaluated on noisy single-iteration values, has exactly one blind state: an @@ -571,9 +582,9 @@ excursion on unprotected simulation averaged updates, and -- because for these regions the per-iteration noise scale is set by the reduced source rather than by the flux -- the average over a whole phase of active batches can land slightly negative. The latch identifies the entire strongly fed -class once, from converged data that individual fluctuations cannot flip, and -removes it from the simulation averaged estimator before active tallies -begin. A region with no cross-group or external feed can never latch, so the +class from accumulated data that individual fluctuations cannot flip, and +removes it from the simulation averaged estimator. A region with no +cross-group or external feed can never latch, so the estimator choice never reacts to noise whose sign is locked to the region's own flux (as in one-group media, where the source is proportional to the local flux). Non-negativity is still not strictly enforced on individual diff --git a/docs/source/usersguide/random_ray.rst b/docs/source/usersguide/random_ray.rst index 167f579fbe6..c4f35c822a5 100644 --- a/docs/source/usersguide/random_ray.rst +++ b/docs/source/usersguide/random_ray.rst @@ -1066,15 +1066,14 @@ following methods are currently available in OpenMC: their flux (a strong external or in-scatter source); in every batch, cells whose reduced source is itself negative (possible under transport-corrected cross sections) and hit-starved cells; and -- - decided once at the end of the inactive phase, from each cell's - accumulated (converged) flux -- cells whose accumulated flux is - negative as well as cells whose flux-independent feed (cross-group - in-scatter, fission, and external source) is strong relative to their - own accumulated flux. The transition decisions are one-shot: the - unmodified simulation averaged estimator runs throughout the inactive - phase, and the demoted cells use the naive estimator for all of the - active batches, so the estimator choice in the tallied batches never - depends on single-batch noise. The decisions are made automatically + decided from each cell's running accumulated flux, first at the end of + the inactive phase and re-evaluated every active batch -- cells whose + accumulated flux is negative as well as cells whose flux-independent + feed (cross-group in-scatter, fission, and external source) is strong + relative to their own accumulated flux. The accumulated-flux decisions + are demote-only: a demoted cell stays on the naive estimator for the + rest of the solve, so the estimator choice in the tallied batches never + churns with single-batch noise. The decisions are made automatically from each cell's accumulated statistics; individual iterations are never modified. - * Retains the low bias of the simulation averaged estimator wherever it @@ -1082,14 +1081,15 @@ following methods are currently available in OpenMC: * Eliminates the negative-flux instabilities that the simulation averaged and hybrid estimators can exhibit in optically thin, in-scatter-fed fixed source problems - * The converged-feed latch removes the strongly fed cell population + * The accumulated-feed latch removes the strongly fed cell population whose phase-averaged flux could otherwise straddle zero, eliminating the negative tally bins that class otherwise produces * No parameters to tune - * Does not strictly guarantee non-negative fluxes on individual active iterations (any residual non-positive tally values are discarded downstream by the weight-window generator) - * Requires inactive batches in order to make the transition decisions + * Benefits from inactive batches to season the accumulated-flux + decisions before tallies begin These estimators can be selected by setting the ``volume_estimator`` field in the :attr:`openmc.Settings.random_ray` dictionary. For example, to use the naive @@ -1109,13 +1109,15 @@ develop persistent negative fluxes that degrade tally results and, in variance reduction workflows, the quality of generated weight windows. The adaptive estimator detects and stabilizes those cells automatically while leaving the rest of the problem on the low-bias simulation averaged estimator. -Because the negative-flux and strong-feed demotions are decided once, from -each cell's accumulated (converged) flux at the end of the inactive phase -rather than from individual per-iteration values, they avoid the small upward -bias that per-iteration demotion can introduce in cells that are noisy but -not genuinely negative, while still removing -- via the strong-feed latch -- -the strongly fed cell class whose phase-averaged flux could otherwise -straddle zero. Non-negativity is still not strictly enforced on individual +Because the negative-flux and strong-feed demotions are decided from each +cell's running accumulated flux -- first at the end of the inactive phase and +re-evaluated (demote-only) every active batch -- rather than from individual +per-iteration values, they avoid the small upward bias that per-iteration +demotion can introduce in cells that are noisy but not genuinely negative, +while still removing -- via the strong-feed latch -- the strongly fed cell +class whose phase-averaged flux could otherwise straddle zero, and still +catching cells whose instability only becomes visible after the inactive +phase ends (as on large problems run with short inactive phases). Non-negativity is still not strictly enforced on individual active iterations; any residual non-positive tally values are filtered out by the weight-window generator, which discards non-positive fluxes. diff --git a/include/openmc/random_ray/flat_source_domain.h b/include/openmc/random_ray/flat_source_domain.h index d2692cc7e7e..af6ecc673d4 100644 --- a/include/openmc/random_ray/flat_source_domain.h +++ b/include/openmc/random_ray/flat_source_domain.h @@ -39,7 +39,7 @@ class FlatSourceDomain { void reset_tally_volumes(); void random_ray_tally(); virtual void accumulate_iteration_flux(); - void inactive_demotion_step(); + void demotion_step(); void output_to_vtk() const; void convert_external_sources(bool use_adjoint_sources); void count_external_source_regions(); @@ -106,15 +106,15 @@ class FlatSourceDomain { // Final-batch snapshot of the naive volume treatment, partitioned by // mutually exclusive cause (the four counts sum to n_final_naive_), for - // end-of-simulation reporting. The two one-shot demotions decided at the - // inactive->active transition are counted with first priority -- a strong - // converged (accumulated) feed and a negative converged flux -- so their - // counts equal the decisions made at the transition; the per-batch + // end-of-simulation reporting. The two demote-only decisions made from the + // running accumulated flux -- a strong accumulated feed and a negative + // accumulated flux -- are counted with first priority, so their counts + // equal the decisions settled by the final batch; the per-batch // strong-source test and hit-starved causes count the remainder. int64_t n_final_naive_ {0}; - int64_t n_final_latch_ {0}; // strong source, from the converged feed + int64_t n_final_latch_ {0}; // strong source, from the accumulated feed int64_t n_final_strong_ {0}; // strong source, from the per-batch test - int64_t n_final_sign_ {0}; // negative converged flux + int64_t n_final_sign_ {0}; // negative accumulated flux int64_t n_final_small_ {0}; // hit-starved bool final_stats_valid_ {false}; diff --git a/include/openmc/random_ray/source_region.h b/include/openmc/random_ray/source_region.h index 3593712c8f5..88ae39183fa 100644 --- a/include/openmc/random_ray/source_region.h +++ b/include/openmc/random_ray/source_region.h @@ -343,10 +343,12 @@ class SourceRegion { 0}; //!< Is an external source present in this region? int is_small_ {0}; //!< Is it "small", receiving < 1.5 hits per iteration? int converged_negative_ { - 0}; //!< One-shot demotion flag (adaptive estimator only): set to 1 at the - //!< end of the inactive phase when this region's accumulated flux was - //!< negative, demoting it to the naive volume estimator for the active - //!< phase. + 0}; //!< Demote-only flag (adaptive estimator only), evaluated from the + //!< running accumulated flux at the inactive->active transition and + //!< re-evaluated every active batch: 1 = accumulated flux negative in + //!< some group, 2 = strong accumulated feed (latch). Any value > 0 + //!< demotes the region to the naive volume estimator; once set, the + //!< flag is never released. int n_hits_ {0}; //!< Number of total hits (ray crossings) // Mesh that subdivides this source region int mesh_ {C_NONE}; //!< Index in openmc::model::meshes array that subdivides @@ -403,8 +405,8 @@ class SourceRegionContainer { public: //---------------------------------------------------------------------------- // Constructors - SourceRegionContainer(int negroups, bool is_linear) - : negroups_(negroups), is_linear_(is_linear) + SourceRegionContainer(int negroups, bool is_linear, bool is_adaptive) + : negroups_(negroups), is_linear_(is_linear), is_adaptive_(is_adaptive) {} SourceRegionContainer() = default; @@ -585,6 +587,14 @@ class SourceRegionContainer { return scalar_flux_final_[se]; } + double& scalar_flux_t(int64_t sr, int g) { return scalar_flux_t_[index(sr, g)]; } + const double scalar_flux_t(int64_t sr, int g) const + { + return scalar_flux_t_[index(sr, g)]; + } + double& scalar_flux_t(int64_t se) { return scalar_flux_t_[se]; } + const double scalar_flux_t(int64_t se) const { return scalar_flux_t_[se]; } + float& source(int64_t sr, int g) { return source_[index(sr, g)]; } const float source(int64_t sr, int g) const { return source_[index(sr, g)]; } float& source(int64_t se) { return source_[se]; } @@ -652,6 +662,7 @@ class SourceRegionContainer { int64_t n_source_regions_ {0}; int negroups_ {0}; bool is_linear_ {false}; + bool is_adaptive_ {false}; // SoA storage for scalar fields (one item per source region) vector material_; @@ -685,6 +696,12 @@ class SourceRegionContainer { vector scalar_flux_old_; vector scalar_flux_new_; vector scalar_flux_final_; + // Running sum of the scalar flux over every batch of the current solve + // (inactive and active; never reset within a solve, unlike + // scalar_flux_final which holds only the active-batch accumulation used + // for tallies). Allocated only for the adaptive volume estimator, which + // makes its demotion decisions from it. + vector scalar_flux_t_; vector source_; vector external_source_; diff --git a/src/random_ray/flat_source_domain.cpp b/src/random_ray/flat_source_domain.cpp index 3040cb9c0f0..1ce1c83cee4 100644 --- a/src/random_ray/flat_source_domain.cpp +++ b/src/random_ray/flat_source_domain.cpp @@ -56,7 +56,8 @@ FlatSourceDomain::FlatSourceDomain() : negroups_(data::mg.num_energy_groups_) // Initialize source regions. bool is_linear = RandomRay::source_shape_ != RandomRaySourceShape::FLAT; - source_regions_ = SourceRegionContainer(negroups_, is_linear); + bool is_adaptive = volume_estimator_ == RandomRayVolumeEstimator::ADAPTIVE; + source_regions_ = SourceRegionContainer(negroups_, is_linear, is_adaptive); // Initialize tally volumes if (volume_normalized_flux_tallies_) { @@ -105,122 +106,145 @@ void FlatSourceDomain::accumulate_iteration_flux() // Demotion step for the adaptive volume estimator (no-op for the // others). Rather than reacting to per-iteration negatives, this estimator -// runs the unmodified simulation-averaged update throughout the inactive phase -// and makes two one-shot demotion decisions at the inactive->active -// transition, each from the accumulated (converged) inactive flux: +// runs the unmodified simulation-averaged update and accumulates every +// batch's flux into a running sum (scalar_flux_t, kept separate from the +// active-only tally accumulator), from which it makes two demotion +// decisions: // -// 1. Converged-negative (sign): any region whose accumulated flux is +// 1. Accumulated-negative (sign): any region whose accumulated flux is // negative in any group is demoted to the naive (iteration) volume -// estimator for the active phase -- a positively weighted estimator that -// cannot go negative with a non-negative source. Because the decision is -// made on the accumulated mean rather than on individual fluctuations, +// estimator -- a positively weighted estimator that cannot go negative +// with a non-negative source. During the active phase the test also +// watches the active-only tally accumulation (scalar_flux_final), the +// quantity tally means are actually computed from. Because the decision +// is made on accumulated means rather than on individual fluctuations, // the lower tail of the noise distribution is not clipped, so regions // that are merely noisy (and average non-negative) are left unbiased. // // 2. Strong-feed latch: any region whose flux-independent feed (cross-group // in-scatter, fission, and external source), evaluated from the same // accumulated flux, exceeds ADAPTIVE_VOLUME_KAPPA times its own -// accumulated flux in any group is likewise demoted for the active -// phase. This is the same physical condition the per-iteration -// strong-source test targets, decided from converged data: the -// per-iteration test, evaluated on noisy iterates, cannot fire in the -// joint excursion where a bad iteration drags a region's source and flux -// negative together, so a strong region would otherwise ride such -// excursions on unprotected simulation-averaged updates and can -// accumulate a negative window average. The latch removes the whole -// strong-feed class ahead of time. A region with no cross-group or -// external feed can never latch, so sign-locked (e.g. one-group) noise -// cannot cause demotion through this path. +// accumulated flux in any group is likewise demoted. This is the same +// physical condition the per-iteration strong-source test targets, +// decided from accumulated data: the per-iteration test, evaluated on +// noisy iterates, cannot fire in the joint excursion where a bad +// iteration drags a region's source and flux negative together, so a +// strong region would otherwise ride such excursions on unprotected +// simulation-averaged updates and can accumulate a negative window +// average. The latch removes the whole strong-feed class. A region with +// no cross-group or external feed can never latch, so sign-locked (e.g. +// one-group) noise cannot cause demotion through this path. +// +// Both conditions are first decided at the inactive->active transition and +// then re-evaluated every active batch as the accumulation keeps growing. +// Decisions are demote-only: a set flag is never released, so the estimator +// choice cannot churn with active-batch noise (a release/re-demote cycle +// conditioned on tallied iterations would bias the tallies), and a marginal +// region whose accumulated ratio converges below the threshold is never +// eroded into demotion by continued re-evaluation. The active-phase +// re-evaluation exists for solves whose inactive phase is too short to +// converge deep regions: there the transition-time decision alone misses +// chronically unstable regions whose accumulated flux only turns negative +// (or whose feed ratio only crosses the threshold) after active batches +// begin, and an escapee left on unprotected simulation-averaged updates can +// corrupt the solution far beyond its own boundary through scattering +// feedback. // // The decisions are recorded in converged_negative (1 = sign, 2 = latch; // > 0 == demoted), consumed by the volume switch and miss treatment in // add_source_to_scalar_flux and by the linear-source gradient fallback. -void FlatSourceDomain::inactive_demotion_step() +void FlatSourceDomain::demotion_step() { if (volume_estimator_ != RandomRayVolumeEstimator::ADAPTIVE) return; - if (simulation::current_batch > settings::n_inactive) - return; - // scalar_flux_final is untouched until active accumulation begins, so it - // serves as the temporary inactive accumulator. #pragma omp parallel for for (int64_t se = 0; se < n_source_elements(); se++) { - source_regions_.scalar_flux_final(se) += - source_regions_.scalar_flux_new(se); + source_regions_.scalar_flux_t(se) += source_regions_.scalar_flux_new(se); } - // On the last inactive batch, settle the demotion decision and clear the - // accumulator so the active phase tallies start from zero. - if (simulation::current_batch == settings::n_inactive) { + // Decisions start on the last inactive batch and continue every active + // batch thereafter. + if (simulation::current_batch < settings::n_inactive) + return; + #pragma omp parallel for - for (int64_t sr = 0; sr < n_source_regions(); sr++) { - bool negative = false; + for (int64_t sr = 0; sr < n_source_regions(); sr++) { + // Demote-only: settled regions are never re-evaluated or released. + if (source_regions_.converged_negative(sr) > 0) + continue; + bool negative = false; + for (int g = 0; g < negroups_; g++) { + if (source_regions_.scalar_flux_t(sr, g) < 0.0) { + negative = true; + break; + } + } + // During the active phase, a negative accumulated tally flux + // (scalar_flux_final, the active-only sum that tally means are computed + // from) also demotes: a region with a strong positive inactive + // accumulation can hold the running sum positive while the active-only + // sum -- the quantity actually reported -- goes negative. This branch + // fires only when the reported mean has already lost positivity, so it + // clips realized-negative outcomes rather than one tail of a healthy + // region's noise. + if (!negative && simulation::current_batch > settings::n_inactive) { for (int g = 0; g < negroups_; g++) { if (source_regions_.scalar_flux_final(sr, g) < 0.0) { negative = true; break; } } - // One-shot strong-feed latch, decided here at the moment of maximum - // information from the same inactive-accumulated flux: a region whose - // flux-independent feed (cross-group in-scatter, fission, and external - // source) exceeds kappa times its own accumulated flux in any group is - // demoted for the entire active phase. This covers the joint excursion - // (per-iteration source and flux dragged negative together) that the - // per-iteration strong-source test cannot fire on, with a label that - // active-phase noise can never flip. A region with no cross-group or - // external feed can never latch, so sign-locked (one-group) noise - // cannot cause demotion through this path. - bool latched = false; - int material = source_regions_.material(sr); - if (!negative && material != MATERIAL_VOID) { - int temp = source_regions_.temperature_idx(sr); - const int material_offset = - (material * ntemperature_ + temp) * negroups_; - const int scatter_offset = - (material * ntemperature_ + temp) * negroups_ * negroups_; - double inverse_k_eff = 1.0 / k_eff_; - for (int g = 0; g < negroups_ && !latched; g++) { - double feed = 0.0; - double chi = chi_[material_offset + g]; - for (int gp = 0; gp < negroups_; gp++) { - double phi = - std::max(source_regions_.scalar_flux_final(sr, gp), 0.0); - if (gp != g) { - feed += sigma_s_[scatter_offset + g * negroups_ + gp] * phi; - } - if (settings::create_fission_neutrons) { - feed += - chi * nu_sigma_f_[material_offset + gp] * phi * inverse_k_eff; - } - } - double sigma_t = sigma_t_[material_offset + g]; - double q_indep = feed / sigma_t; - // The external source arrays are only allocated in fixed source - // mode, so the external term must not be read in an eigenvalue - // solve (where no external sources exist). - if (settings::run_mode == RunMode::FIXED_SOURCE) { - q_indep += - settings::n_inactive * source_regions_.external_source(sr, g); + } + bool latched = false; + int material = source_regions_.material(sr); + if (!negative && material != MATERIAL_VOID) { + int temp = source_regions_.temperature_idx(sr); + const int material_offset = (material * ntemperature_ + temp) * negroups_; + const int scatter_offset = + (material * ntemperature_ + temp) * negroups_ * negroups_; + double inverse_k_eff = 1.0 / k_eff_; + for (int g = 0; g < negroups_ && !latched; g++) { + double feed = 0.0; + double chi = chi_[material_offset + g]; + for (int gp = 0; gp < negroups_; gp++) { + double phi = std::max(source_regions_.scalar_flux_t(sr, gp), 0.0); + if (gp != g) { + feed += sigma_s_[scatter_offset + g * negroups_ + gp] * phi; } - if (q_indep > - ADAPTIVE_VOLUME_KAPPA * - std::max(source_regions_.scalar_flux_final(sr, g), 0.0)) { - latched = true; + if (settings::create_fission_neutrons) { + feed += + chi * nu_sigma_f_[material_offset + gp] * phi * inverse_k_eff; } } + double sigma_t = sigma_t_[material_offset + g]; + double q_indep = feed / sigma_t; + // The external source arrays are only allocated in fixed source + // mode, so the external term must not be read in an eigenvalue + // solve (where no external sources exist). The external source is a + // per-batch quantity while the flux is an accumulated one, so the + // term is scaled by the number of accumulated batches. + if (settings::run_mode == RunMode::FIXED_SOURCE) { + q_indep += + simulation::current_batch * source_regions_.external_source(sr, g); + } + if (q_indep > + ADAPTIVE_VOLUME_KAPPA * + std::max(source_regions_.scalar_flux_t(sr, g), 0.0)) { + latched = true; + } } - source_regions_.converged_negative(sr) = negative ? 1 : (latched ? 2 : 0); - for (int g = 0; g < negroups_; g++) { - source_regions_.scalar_flux_final(sr, g) = 0.0; - } } - // No separate decision-count bookkeeping is needed here: the flags are - // fixed for the whole active phase, so the final-batch by-cause snapshot - // in add_source_to_scalar_flux (which counts them with first priority) - // reports these decisions exactly. + if (negative) { + source_regions_.converged_negative(sr) = 1; + } else if (latched) { + source_regions_.converged_negative(sr) = 2; + } } + // No separate decision-count bookkeeping is needed here: demote-only flags + // can only accumulate, so the final-batch by-cause snapshot in + // add_source_to_scalar_flux (which counts them with first priority) + // reports the settled decisions exactly. } void FlatSourceDomain::update_single_neutron_source(SourceRegionHandle& srh) @@ -394,8 +418,8 @@ int64_t FlatSourceDomain::add_source_to_scalar_flux() bool final_iteration = (simulation::current_batch == settings::n_batches); // The adaptive estimator uses the proactive strong-source (kappa) test, the // demote-to-naive volume switch, and the previous-flux miss treatment, with - // demotion decided once at the end of the inactive phase (recorded as a 0/1 - // flag in converged_negative by inactive_demotion_step). + // demote-only decisions made from the running accumulated flux (recorded + // as a flag in converged_negative by demotion_step). const bool is_adaptive = volume_estimator_ == RandomRayVolumeEstimator::ADAPTIVE; @@ -463,9 +487,9 @@ int64_t FlatSourceDomain::add_source_to_scalar_flux() // re-evaluated every iteration; the strong-source flag is re-evaluated // every iteration during the inactive batches and reduces to the // negative-source (TCP0) condition in the active batches, where the - // stable transition decisions govern instead; converged_neg is the - // one-shot flag set at the end of the inactive phase by - // inactive_demotion_step. The external-source flag drives only the + // stable accumulated-flux decisions govern instead; converged_neg is the + // demote-only flag set from the running accumulated flux by + // demotion_step. The external-source flag drives only the // hybrid policy (and the default miss treatment); the adaptive estimator // catches a low-cross-section external region through the kappa // strong-source test (during the inactive batches) and the strong-feed @@ -512,10 +536,10 @@ int64_t FlatSourceDomain::add_source_to_scalar_flux() // On the final iteration, classify the demoted (naive-volume) regions by // cause -- mutually exclusive, in priority order, so the causes sum to - // the total -- for the end-of-simulation report. The one-shot transition - // demotions are counted first (their flags are fixed for the whole - // active phase, so these counts equal the decisions made at the end of - // the inactive phase); the per-batch causes count only the remainder. + // the total -- for the end-of-simulation report. The accumulated-flux + // demotions are counted first (their demote-only flags can only + // accumulate, so these counts equal the decisions settled by the final + // batch); the per-batch causes count only the remainder. if (final_iteration && is_adaptive && use_naive_volume) { n_naive++; if (conv_flag == 2) { diff --git a/src/random_ray/linear_source_domain.cpp b/src/random_ray/linear_source_domain.cpp index 5184fac77b3..29e5564ac6a 100644 --- a/src/random_ray/linear_source_domain.cpp +++ b/src/random_ray/linear_source_domain.cpp @@ -119,8 +119,8 @@ void LinearSourceDomain::update_single_neutron_source(SourceRegionHandle& srh) // scalar flux, so the flat-source cancellation must be exact; the gradient // terms attenuate segments against the local rather than the flat source, // introducing per-iteration noise at the gradient scale that the volume - // choice cannot cancel. For regions demoted at the end of the inactive - // phase (converged_negative > 0: negative accumulated flux, or the + // choice cannot cancel. For regions demoted from the accumulated flux + // (converged_negative > 0: negative accumulated flux, or the // strong-feed latch), the same reasoning applies to their cause -- a // negative accumulated flux means the fitted gradients carry no meaningful // shape information, and a latched strong feed is the gradient-scale noise diff --git a/src/random_ray/random_ray_simulation.cpp b/src/random_ray/random_ray_simulation.cpp index 51f191e5697..eb8a8b0ba2b 100644 --- a/src/random_ray/random_ray_simulation.cpp +++ b/src/random_ray/random_ray_simulation.cpp @@ -352,11 +352,9 @@ void RandomRaySimulation::prepare_fw_fixed_sources_adjoint() } else { // In eigenvalue mode there are no fixed adjoint sources to derive from // the forward flux, but the accumulated forward flux must still be - // cleared so that the adjoint solve starts from a clean accumulator -- - // otherwise the adaptive estimator's inactive-demotion decision (which - // accumulates into the same array during the adjoint inactive batches) - // would be swamped by the forward solve's strictly positive sums, and - // any consumer of the final flux would mix forward and adjoint modes. + // cleared so that the adjoint solve's active accumulation starts from a + // clean array -- otherwise any consumer of the final flux would mix + // forward and adjoint modes. #pragma omp parallel for for (int64_t se = 0; se < domain_->n_source_elements(); se++) { domain_->source_regions_.scalar_flux_final(se) = 0.0; @@ -481,10 +479,10 @@ void RandomRaySimulation::simulate() domain_->random_ray_tally(); } - // For the adaptive estimator, accumulate the inactive-phase flux and, on - // the final inactive batch, settle which regions are demoted to the naive + // For the adaptive estimator, accumulate this batch's flux into the + // running sum and update (demote-only) which regions use the naive // volume estimator (no-op for the other estimators). - domain_->inactive_demotion_step(); + domain_->demotion_step(); // Set phi_old = phi_new domain_->flux_swap(); @@ -627,22 +625,23 @@ void RandomRaySimulation::print_results_random_ray( double inv = 100.0 / domain_->n_source_regions(); // Single summary at default verbosity: every source region that // received the naive volume treatment in the final batch, for any - // reason (the one-shot demotions decided at the inactive->active - // transition plus that batch's per-iteration demotions). + // reason (the demote-only decisions made from the accumulated flux + // plus that batch's per-iteration demotions). fmt::print(" Number of Naive Demotions = {} SRs ({:.4f}%)\n", domain_->n_final_naive_, domain_->n_final_naive_ * inv); // The per-cause diagnostic breakdown is developer-facing; verbosity 8 // sits above the default (7) but below the per-particle output (9). // The causes are mutually exclusive and sum to the total above: - // "end of inactive" causes are the one-shot demotions decided from the - // converged (accumulated) inactive flux, "per batch" causes are - // re-evaluated each batch and reported for the final batch. + // "accumulated" causes are the demote-only decisions made from the + // running accumulated flux (from the inactive->active transition + // onward), "per batch" causes are re-evaluated each batch and reported + // for the final batch. if (settings::verbosity >= 8) { - fmt::print(" Strong source (end of inactive) = {} SRs ({:.4f}%)\n", + fmt::print(" Strong source (accumulated) = {} SRs ({:.4f}%)\n", domain_->n_final_latch_, domain_->n_final_latch_ * inv); fmt::print(" Strong source (per batch) = {} SRs ({:.4f}%)\n", domain_->n_final_strong_, domain_->n_final_strong_ * inv); - fmt::print(" Negative flux (end of inactive) = {} SRs ({:.4f}%)\n", + fmt::print(" Negative flux (accumulated) = {} SRs ({:.4f}%)\n", domain_->n_final_sign_, domain_->n_final_sign_ * inv); fmt::print(" Hit-starved (per batch) = {} SRs ({:.4f}%)\n", domain_->n_final_small_, domain_->n_final_small_ * inv); diff --git a/src/random_ray/source_region.cpp b/src/random_ray/source_region.cpp index 5e7f36a1def..47319932484 100644 --- a/src/random_ray/source_region.cpp +++ b/src/random_ray/source_region.cpp @@ -104,6 +104,10 @@ void SourceRegionContainer::push_back(const SourceRegion& sr) scalar_flux_old_.push_back(sr.scalar_flux_old_[g]); scalar_flux_new_.push_back(sr.scalar_flux_new_[g]); scalar_flux_final_.push_back(sr.scalar_flux_final_[g]); + // A newly discovered region starts with nothing accumulated + if (is_adaptive_) { + scalar_flux_t_.push_back(0.0); + } source_.push_back(sr.source_[g]); if (settings::run_mode == RunMode::FIXED_SOURCE) { external_source_.push_back(sr.external_source_[g]); @@ -156,6 +160,7 @@ void SourceRegionContainer::assign( scalar_flux_old_.clear(); scalar_flux_new_.clear(); scalar_flux_final_.clear(); + scalar_flux_t_.clear(); source_.clear(); external_source_.clear(); @@ -258,6 +263,7 @@ void SourceRegionContainer::adjoint_reset() std::fill(scalar_flux_old_.begin(), scalar_flux_old_.end(), 1.0); } std::fill(scalar_flux_new_.begin(), scalar_flux_new_.end(), 0.0); + std::fill(scalar_flux_t_.begin(), scalar_flux_t_.end(), 0.0); std::fill(source_.begin(), source_.end(), 0.0f); std::fill(external_source_.begin(), external_source_.end(), 0.0f); std::fill(source_gradients_.begin(), source_gradients_.end(), diff --git a/tests/regression_tests/random_ray_adjoint_fixed_source/adaptive_starved/results_true.dat b/tests/regression_tests/random_ray_adjoint_fixed_source/adaptive_starved/results_true.dat index b47d764f897..a8b8824e6b9 100644 --- a/tests/regression_tests/random_ray_adjoint_fixed_source/adaptive_starved/results_true.dat +++ b/tests/regression_tests/random_ray_adjoint_fixed_source/adaptive_starved/results_true.dat @@ -1,9 +1,9 @@ tally 1: -3.668344E+06 -9.453869E+11 +3.923616E+06 +1.276954E+12 tally 2: -4.649756E+06 -1.094198E+12 +5.338607E+06 +1.444114E+12 tally 3: -1.584906E+07 -1.263152E+13 +1.722575E+07 +1.493106E+13 diff --git a/tests/regression_tests/random_ray_diagonal_stabilization/adaptive/results_true.dat b/tests/regression_tests/random_ray_diagonal_stabilization/adaptive/results_true.dat index cbd2bcff9a6..57f8c47bbc5 100644 --- a/tests/regression_tests/random_ray_diagonal_stabilization/adaptive/results_true.dat +++ b/tests/regression_tests/random_ray_diagonal_stabilization/adaptive/results_true.dat @@ -1,2 +1,2 @@ k-combined: -7.147211E-01 1.345902E-02 +7.165325E-01 1.371333E-02 diff --git a/tests/regression_tests/random_ray_k_eff_mesh/adaptive_starved/results_true.dat b/tests/regression_tests/random_ray_k_eff_mesh/adaptive_starved/results_true.dat index ae005723c7d..038d14115e4 100644 --- a/tests/regression_tests/random_ray_k_eff_mesh/adaptive_starved/results_true.dat +++ b/tests/regression_tests/random_ray_k_eff_mesh/adaptive_starved/results_true.dat @@ -1,48 +1,48 @@ k-combined: -1.107189E+00 1.650637E-02 +1.107191E+00 1.650659E-02 tally 1: -5.382306E+00 -1.458407E+00 -1.990183E+00 -1.996766E-01 -4.843706E+00 -1.182761E+00 -3.851437E+00 -7.437404E-01 -5.695722E-01 -1.627370E-02 -1.386225E+00 -9.639533E-02 -2.905353E+00 -4.222527E-01 -9.355432E-02 -4.378736E-04 -2.276925E-01 -2.593692E-03 -3.728130E+00 -6.951645E-01 -1.234730E-01 -7.626203E-04 -3.005086E-01 -4.517292E-03 -9.697796E+00 -4.702923E+00 -1.132009E-01 -6.408203E-04 -2.755117E-01 -3.795916E-03 -2.110312E+01 -2.227093E+01 -3.196291E-02 -5.109833E-05 -7.908998E-02 -3.128651E-04 -1.114018E+01 -6.210794E+00 -1.495090E-01 -1.120073E-03 -4.158517E-01 -8.665401E-03 +5.382339E+00 +1.458425E+00 +1.990201E+00 +1.996805E-01 +4.843752E+00 +1.182784E+00 +3.851449E+00 +7.437451E-01 +5.695747E-01 +1.627384E-02 +1.386231E+00 +9.639619E-02 +2.905354E+00 +4.222530E-01 +9.355436E-02 +4.378740E-04 +2.276926E-01 +2.593694E-03 +3.728133E+00 +6.951656E-01 +1.234731E-01 +7.626219E-04 +3.005089E-01 +4.517301E-03 +9.697786E+00 +4.702912E+00 +1.132007E-01 +6.408188E-04 +2.755113E-01 +3.795908E-03 +2.110307E+01 +2.227083E+01 +3.196285E-02 +5.109814E-05 +7.908983E-02 +3.128639E-04 +1.114013E+01 +6.210744E+00 +1.495081E-01 +1.120059E-03 +4.158491E-01 +8.665291E-03 8.789910E+00 3.891597E+00 0.000000E+00 @@ -55,117 +55,117 @@ tally 1: 0.000000E+00 0.000000E+00 0.000000E+00 -3.067041E+00 -4.705146E-01 +3.067040E+00 +4.705142E-01 0.000000E+00 0.000000E+00 0.000000E+00 0.000000E+00 -4.067401E+00 -8.273907E-01 +4.067399E+00 +8.273897E-01 0.000000E+00 0.000000E+00 0.000000E+00 0.000000E+00 -9.781901E+00 -4.784781E+00 +9.781891E+00 +4.784770E+00 0.000000E+00 0.000000E+00 0.000000E+00 0.000000E+00 -1.891496E+01 -1.789211E+01 +1.891493E+01 +1.789205E+01 0.000000E+00 0.000000E+00 0.000000E+00 0.000000E+00 -9.664805E+00 -4.679820E+00 +9.664789E+00 +4.679805E+00 0.000000E+00 0.000000E+00 0.000000E+00 0.000000E+00 -4.684699E+00 -1.105729E+00 +4.684700E+00 +1.105730E+00 1.748804E+00 -1.543530E-01 -4.256238E+00 -9.142916E-01 -3.633543E+00 -6.620809E-01 -5.450625E-01 -1.490411E-02 +1.543531E-01 +4.256239E+00 +9.142921E-01 +3.633542E+00 +6.620806E-01 +5.450623E-01 +1.490410E-02 1.326573E+00 -8.828275E-02 -2.859799E+00 -4.091361E-01 -9.368861E-02 -4.391014E-04 -2.280193E-01 -2.600965E-03 -3.656299E+00 -6.686621E-01 -1.230361E-01 -7.572477E-04 -2.994452E-01 -4.485467E-03 -9.692742E+00 -4.698037E+00 -1.151165E-01 -6.626621E-04 -2.801740E-01 -3.925297E-03 -2.161347E+01 -2.335945E+01 -3.339068E-02 -5.576430E-05 -8.262289E-02 -3.414339E-04 -1.149110E+01 -6.606652E+00 -1.577265E-01 -1.246081E-03 -4.387082E-01 -9.640260E-03 -5.394236E+00 -1.465976E+00 -2.033782E+00 -2.084700E-01 -4.949818E+00 -1.234847E+00 -3.854245E+00 -7.449164E-01 -5.821876E-01 -1.699809E-02 -1.416928E+00 -1.006862E-01 -2.904499E+00 -4.220002E-01 -9.568914E-02 -4.580432E-04 -2.328882E-01 -2.713165E-03 -3.721096E+00 -6.925417E-01 -1.259813E-01 -7.939084E-04 -3.066132E-01 -4.702623E-03 -9.683019E+00 -4.688517E+00 -1.156992E-01 -6.693990E-04 -2.815922E-01 -3.965203E-03 -2.116800E+01 -2.240889E+01 -3.293511E-02 -5.425811E-05 -8.149563E-02 -3.322118E-04 -1.127273E+01 -6.359129E+00 -1.560615E-01 -1.220746E-03 -4.340772E-01 -9.444254E-03 +8.828271E-02 +2.859797E+00 +4.091356E-01 +9.368855E-02 +4.391008E-04 +2.280192E-01 +2.600962E-03 +3.656296E+00 +6.686608E-01 +1.230360E-01 +7.572462E-04 +2.994450E-01 +4.485459E-03 +9.692731E+00 +4.698026E+00 +1.151164E-01 +6.626605E-04 +2.801737E-01 +3.925288E-03 +2.161344E+01 +2.335940E+01 +3.339064E-02 +5.576419E-05 +8.262280E-02 +3.414332E-04 +1.149109E+01 +6.606633E+00 +1.577263E-01 +1.246078E-03 +4.387076E-01 +9.640234E-03 +5.394231E+00 +1.465973E+00 +2.033778E+00 +2.084693E-01 +4.949809E+00 +1.234843E+00 +3.854239E+00 +7.449142E-01 +5.821861E-01 +1.699800E-02 +1.416925E+00 +1.006856E-01 +2.904495E+00 +4.219991E-01 +9.568897E-02 +4.580415E-04 +2.328878E-01 +2.713155E-03 +3.721089E+00 +6.925391E-01 +1.259809E-01 +7.939042E-04 +3.066124E-01 +4.702598E-03 +9.683004E+00 +4.688502E+00 +1.156990E-01 +6.693965E-04 +2.815917E-01 +3.965188E-03 +2.116801E+01 +2.240891E+01 +3.293516E-02 +5.425827E-05 +8.149575E-02 +3.322128E-04 +1.127270E+01 +6.359090E+00 +1.560609E-01 +1.220736E-03 +4.340755E-01 +9.444178E-03 diff --git a/tests/regression_tests/random_ray_volume_estimator/adaptive/results_true.dat b/tests/regression_tests/random_ray_volume_estimator/adaptive/results_true.dat index d8ea959a9dc..ac9fcd45390 100644 --- a/tests/regression_tests/random_ray_volume_estimator/adaptive/results_true.dat +++ b/tests/regression_tests/random_ray_volume_estimator/adaptive/results_true.dat @@ -1,9 +1,9 @@ tally 1: -2.261142E+00 -2.678744E-01 +2.259425E+00 +2.675738E-01 tally 2: -1.029707E-01 -7.377908E-04 +1.018431E-01 +6.713412E-04 tally 3: -7.337695E-03 -3.519893E-06 +7.288312E-03 +3.488945E-06 diff --git a/tests/regression_tests/random_ray_volume_estimator_linear/adaptive/results_true.dat b/tests/regression_tests/random_ray_volume_estimator_linear/adaptive/results_true.dat index d6949631dcb..c80049b6427 100644 --- a/tests/regression_tests/random_ray_volume_estimator_linear/adaptive/results_true.dat +++ b/tests/regression_tests/random_ray_volume_estimator_linear/adaptive/results_true.dat @@ -1,9 +1,9 @@ tally 1: -2.308009E+00 -2.776368E-01 +2.285865E+00 +2.736206E-01 tally 2: -8.690165E-02 -5.512767E-03 +1.030651E-01 +8.460405E-04 tally 3: -7.510574E-03 -3.694630E-06 +7.895001E-03 +4.249732E-06 From 147a5e7578384cbf15b35b4debb6eaf055cb132f Mon Sep 17 00:00:00 2001 From: John Tramm Date: Thu, 27 Aug 2026 22:07:10 +0000 Subject: [PATCH 19/35] Add the strict adaptive volume estimator and an auto selection policy Weight window generation and adjoint workflows require strictly non-negative fluxes: the adjoint source is built from the forward flux, so even a small population of noise-driven negative tally bins (which the adaptive estimator permits by design in near-zero-flux regions, and which grows through the adjoint solve) contaminates the generated windows. No demotion criterion can close this gap by itself -- a region can inherit negativity through in-scatter from neighbors that have not yet been demoted -- so a value-level treatment is required. The strict adaptive estimator runs the adaptive machinery unchanged and adds a per-batch non-negativity enforcement: a negative batch flux is first rescued (its transport term algebraically rescaled to the batch volume, reproducing the naive-volume update) and floored at the previous iterate if still negative, which an induction from the non-negative initial condition turns into a guarantee. Regions whose flux goes negative chronically are demoted outright: the floor masks the accumulated-flux sign signal the adaptive demotion relies on, and without the chronic channel such regions would be clipped every batch, a one-sided ratchet that biases them upward. On shielding benchmarks the strict estimator matches the adaptive estimator's accuracy with zero negative fluxes where adaptive leaves a small negative residue at short active batch counts, and it degrades far more gracefully than the naive estimator at coarse ray densities (region errors of a few percent where naive exceeds one hundred percent). The cost is a small conservative bias (several hundred pcm on eigenvalue problems). The volume estimator now defaults to "auto", which resolves by solve type at the start of the run: strict adaptive for solves whose results feed variance reduction (weight window generation, and any adjoint workflow, including the forward solve an adjoint source is derived from), and adaptive for all other solves. The resolution is reported in the simulation output, and explicit estimator selections override it. New regression tests pin the strict estimator in flat and linear source modes and the auto routing itself in both solve types. Co-Authored-By: Claude Fable 5 --- docs/source/io_formats/settings.rst | 7 +- docs/source/methods/random_ray.rst | 40 ++- docs/source/usersguide/random_ray.rst | 43 ++- include/openmc/constants.h | 16 +- .../openmc/random_ray/flat_source_domain.h | 23 +- include/openmc/random_ray/source_region.h | 8 + openmc/settings.py | 25 +- src/random_ray/flat_source_domain.cpp | 82 +++++- src/random_ray/linear_source_domain.cpp | 3 +- src/random_ray/random_ray_simulation.cpp | 39 ++- src/random_ray/source_region.cpp | 8 +- src/settings.cpp | 7 + .../strict_adaptive/inputs_true.dat | 247 +++++++++++++++++ .../strict_adaptive/results_true.dat | 9 + .../random_ray_volume_estimator/test.py | 3 +- .../__init__.py | 0 .../adjoint/inputs_true.dat | 247 +++++++++++++++++ .../adjoint/results_true.dat | 9 + .../forward/inputs_true.dat | 246 +++++++++++++++++ .../forward/results_true.dat | 9 + .../random_ray_volume_estimator_auto/test.py | 37 +++ .../strict_adaptive/inputs_true.dat | 248 ++++++++++++++++++ .../strict_adaptive/results_true.dat | 9 + .../test.py | 3 +- 24 files changed, 1332 insertions(+), 36 deletions(-) create mode 100644 tests/regression_tests/random_ray_volume_estimator/strict_adaptive/inputs_true.dat create mode 100644 tests/regression_tests/random_ray_volume_estimator/strict_adaptive/results_true.dat create mode 100644 tests/regression_tests/random_ray_volume_estimator_auto/__init__.py create mode 100644 tests/regression_tests/random_ray_volume_estimator_auto/adjoint/inputs_true.dat create mode 100644 tests/regression_tests/random_ray_volume_estimator_auto/adjoint/results_true.dat create mode 100644 tests/regression_tests/random_ray_volume_estimator_auto/forward/inputs_true.dat create mode 100644 tests/regression_tests/random_ray_volume_estimator_auto/forward/results_true.dat create mode 100644 tests/regression_tests/random_ray_volume_estimator_auto/test.py create mode 100644 tests/regression_tests/random_ray_volume_estimator_linear/strict_adaptive/inputs_true.dat create mode 100644 tests/regression_tests/random_ray_volume_estimator_linear/strict_adaptive/results_true.dat diff --git a/docs/source/io_formats/settings.rst b/docs/source/io_formats/settings.rst index 34415c94fa1..cdefbc985c5 100644 --- a/docs/source/io_formats/settings.rst +++ b/docs/source/io_formats/settings.rst @@ -626,8 +626,11 @@ found in the :ref:`random ray user guide `. :volume_estimator: Specifies choice of volume estimator for the random ray solver. Options - are 'naive', 'simulation_averaged', 'hybrid', or 'adaptive'. The default is - 'adaptive'. + are 'naive', 'simulation_averaged', 'hybrid', 'adaptive', + 'strict_adaptive', or 'auto'. The default is 'auto', which selects + 'adaptive' for standard solves and 'strict_adaptive' (which guarantees + non-negative fluxes) for solves whose results feed variance reduction: + weight window generation, and any adjoint workflow. *Default*: None diff --git a/docs/source/methods/random_ray.rst b/docs/source/methods/random_ray.rst index dbfaa7bdf06..a89b797f9f2 100644 --- a/docs/source/methods/random_ray.rst +++ b/docs/source/methods/random_ray.rst @@ -602,9 +602,43 @@ the gradient terms attenuate segments against the local rather than the flat source, re-injecting per-iteration noise at the scale of the reduced source that the volume choice cannot cancel, while in a converged-negative region the fitted gradients carry no meaningful shape information. The adaptive -estimator is the -default in OpenMC, and is particularly beneficial for fixed source and -shielding problems that exhibit such instability. +estimator is particularly beneficial for fixed source and shielding problems +that exhibit such instability. + +The adaptive estimator's demotion machinery selects estimators; it never +modifies a computed flux value, which is what preserves its unbiasedness -- +and also why it cannot guarantee non-negativity: in near-zero-flux regions +the simulation averaged noise is sign-indefinite at stationarity, and a +region can also inherit negativity through in-scatter from neighbors that +have not (yet) been demoted, so no demotion criterion alone closes the gap. +The "strict adaptive" estimator therefore runs the same machinery and adds a +per-batch enforcement on the flux iterates. A group whose batch flux comes +out negative is first *rescued*: its transport term is rescaled from the +volume used to the batch's own volume, algebraically reproducing the naive +(iteration) volume update, whose consistency removes the volume-mismatch +noise that produced most negative excursions. If the flux remains negative +(or the region already used the batch volume), it is *floored* at the +previous iterate, which is non-negative by induction from a non-negative +initial condition -- so strict adaptive fluxes are guaranteed non-negative. +Because the floor prevents a chronically noisy region's accumulated flux +from ever going negative -- masking the very signal the accumulated sign +demotion detects -- a region whose flux goes negative (before enforcement) +in more than a few batches is demoted outright to the naive volume and +previous-flux miss treatment, where clipping is no longer needed; without +this chronic-negativity channel, repeated one-sided clipping would bias the +affected regions upward. The residual cost of the enforcement is a small +conservative bias (several hundred pcm on typical eigenvalue problems), +which is why the strict estimator is reserved for solves that require +positivity rather than used as the standard default. + +By default OpenMC selects the volume estimator automatically ("auto"): +solves whose results feed variance reduction -- weight window generation, +and any adjoint workflow, including the forward solve an adjoint source is +derived from -- receive the strict adaptive estimator, since a small +population of negative fluxes would otherwise contaminate the adjoint +source and degrade the generated weight windows, while all other solves +receive the adaptive estimator, preserving unbiased results where accuracy +is the priority. A table that summarizes the pros and cons, as well as recommendations for different use cases, is given in the :ref:`volume diff --git a/docs/source/usersguide/random_ray.rst b/docs/source/usersguide/random_ray.rst index c4f35c822a5..4713fceec32 100644 --- a/docs/source/usersguide/random_ray.rst +++ b/docs/source/usersguide/random_ray.rst @@ -1058,7 +1058,7 @@ following methods are currently available in OpenMC: * Stability of the naive estimator in cells with fixed sources - * Can lead to slightly negative fluxes in cells where the simulation averaged estimator is used - * - ``adaptive`` (default) + * - ``adaptive`` - Generalizes the hybrid estimator. Uses the simulation averaged estimator by default, but falls back to the naive estimator (and the previous-iteration miss treatment) wherever it is needed for stability: @@ -1090,6 +1090,32 @@ following methods are currently available in OpenMC: discarded downstream by the weight-window generator) * Benefits from inactive batches to season the accumulated-flux decisions before tallies begin + * - ``strict_adaptive`` + - As ``adaptive``, but additionally enforces non-negativity on the flux + iterates every batch: a cell whose batch flux comes out negative is + first recomputed with the batch's own volume, floored at the previous + iterate if still negative, and demoted outright to the naive + treatment if its flux goes negative chronically. Because the previous + iterate is non-negative by induction from a non-negative start, the + resulting fluxes are guaranteed non-negative everywhere. + - * Guarantees non-negative fluxes -- the property required by weight + window generation and adjoint workflows, where a small population + of noise-driven negative fluxes would otherwise contaminate the + adjoint source and degrade weight window quality + * Matches ``adaptive``'s accuracy in stable fixed source problems and + degrades far more gracefully than ``naive`` at coarse ray densities + - * The one-sided enforcement introduces a small conservative bias + (several hundred pcm on eigenvalue problems), so it should not be + used where unbiased results are the priority + +By default, the ``volume_estimator`` field is set to ``auto``, which selects +the appropriate estimator for the type of simulation being performed: +``strict_adaptive`` for solves whose results feed variance reduction -- +weight window generation, and any adjoint workflow, including the forward +solve an adjoint source is derived from -- and ``adaptive`` for all other +solves. The end-of-simulation output reports which estimator ``auto`` +resolved to. Explicitly setting any other value overrides the automatic +selection. These estimators can be selected by setting the ``volume_estimator`` field in the :attr:`openmc.Settings.random_ray` dictionary. For example, to use the naive @@ -1099,7 +1125,7 @@ estimator, the following code would be used: settings.random_ray['volume_estimator'] = 'naive' -The ``adaptive`` estimator is the default, as it gives reliable behavior out of +The ``auto`` setting is the default, as it gives reliable behavior out of the box across problem types. It is especially valuable for fixed source and shielding problems, where the ``hybrid`` and ``simulation_averaged`` estimators can otherwise produce negative fluxes or numerical instability. This commonly occurs in optically thin, @@ -1117,9 +1143,16 @@ demotion can introduce in cells that are noisy but not genuinely negative, while still removing -- via the strong-feed latch -- the strongly fed cell class whose phase-averaged flux could otherwise straddle zero, and still catching cells whose instability only becomes visible after the inactive -phase ends (as on large problems run with short inactive phases). Non-negativity is still not strictly enforced on individual -active iterations; any residual non-positive tally values are filtered out by -the weight-window generator, which discards non-positive fluxes. +phase ends (as on large problems run with short inactive phases). The +adaptive estimator does not strictly enforce non-negativity, however: in +near-zero-flux regions its sampling noise is sign-indefinite, so over a +finite number of active batches a small population of tally bins can land +negative. That residue is harmless for standard tallies but contaminates +variance reduction workflows, where the adjoint source is built from the +forward flux and amplifies it -- which is why ``auto`` routes weight window +generation and adjoint solves to ``strict_adaptive`` instead, whose +per-batch enforcement guarantees non-negative fluxes at the cost of a small +conservative bias. ----------------- Adjoint Flux Mode diff --git a/include/openmc/constants.h b/include/openmc/constants.h index d9d510a873b..c4163a3d169 100644 --- a/include/openmc/constants.h +++ b/include/openmc/constants.h @@ -75,6 +75,18 @@ constexpr double MIN_HITS_PER_BATCH {1.5}; // caught. constexpr double ADAPTIVE_VOLUME_KAPPA {4.0}; +// Chronic-negativity demotion thresholds for the strict adaptive volume +// estimator: a region whose flux has gone negative (before enforcement) in +// at least max(MIN_COUNT, RATE * current_batch) batches is demoted to the +// naive volume and previous-flux miss treatment. Without this channel the +// non-negativity floor would mask the accumulated-flux sign signal the +// adaptive demotion relies on, leaving noisy regions to be clipped every +// batch -- a one-sided ratchet that biases their fluxes upward. The chronic +// channel converts "clip forever" into "clip a few times, then switch +// estimator". +constexpr int NEGATIVE_FLUX_DEMOTION_MIN_COUNT {3}; +constexpr double NEGATIVE_FLUX_DEMOTION_RATE {0.005}; + // The minimum flux value to be considered non-zero when computing adjoint // sources. Positive values below this cutoff will be treated as zero, so as to // prevent extremely large adjoint source terms from being generated. @@ -379,7 +391,9 @@ enum class RandomRayVolumeEstimator { NAIVE, SIMULATION_AVERAGED, HYBRID, - ADAPTIVE + ADAPTIVE, + STRICT_ADAPTIVE, + AUTO }; enum class RandomRaySourceShape { FLAT, LINEAR, LINEAR_XY }; enum class RandomRaySampleMethod { PRNG, HALTON, S2 }; diff --git a/include/openmc/random_ray/flat_source_domain.h b/include/openmc/random_ray/flat_source_domain.h index af6ecc673d4..c84a82098cb 100644 --- a/include/openmc/random_ray/flat_source_domain.h +++ b/include/openmc/random_ray/flat_source_domain.h @@ -12,6 +12,16 @@ namespace openmc { +// True for the members of the adaptive volume estimator family: the +// adaptive estimator, and the strict adaptive estimator, which runs the +// same machinery plus a per-batch non-negativity enforcement on the flux +// iterates. +inline bool is_adaptive_family(RandomRayVolumeEstimator e) +{ + return e == RandomRayVolumeEstimator::ADAPTIVE || + e == RandomRayVolumeEstimator::STRICT_ADAPTIVE; +} + /* * The FlatSourceDomain class encompasses data and methods for storing * scalar flux and source region for all flat source regions in a @@ -95,6 +105,10 @@ class FlatSourceDomain { //---------------------------------------------------------------------------- // Static data members static RandomRayVolumeEstimator volume_estimator_; + // True when the user selected (or defaulted to) the "auto" volume + // estimator, which is resolved to a concrete estimator at the start of the + // random ray solve based on the type of simulation being performed. + static bool volume_estimator_is_auto_; //---------------------------------------------------------------------------- // Public Data members @@ -105,7 +119,7 @@ class FlatSourceDomain { // non-zero external source terms // Final-batch snapshot of the naive volume treatment, partitioned by - // mutually exclusive cause (the four counts sum to n_final_naive_), for + // mutually exclusive cause (the cause counts sum to n_final_naive_), for // end-of-simulation reporting. The two demote-only decisions made from the // running accumulated flux -- a strong accumulated feed and a negative // accumulated flux -- are counted with first priority, so their counts @@ -116,6 +130,13 @@ class FlatSourceDomain { int64_t n_final_strong_ {0}; // strong source, from the per-batch test int64_t n_final_sign_ {0}; // negative accumulated flux int64_t n_final_small_ {0}; // hit-starved + int64_t n_final_chronic_ {0}; // chronic negativity (strict adaptive) + // Final-batch counts of the strict adaptive estimator's non-negativity + // enforcement: regions whose negative batch flux was recomputed with the + // batch volume (rescued), and regions floored at the previous iterate + // after the rescue was insufficient or unavailable. + int64_t n_final_rescued_ {0}; + int64_t n_final_floored_ {0}; bool final_stats_valid_ {false}; // 1D array representing source region starting offset for each OpenMC Cell diff --git a/include/openmc/random_ray/source_region.h b/include/openmc/random_ray/source_region.h index 88ae39183fa..5e012cdba3f 100644 --- a/include/openmc/random_ray/source_region.h +++ b/include/openmc/random_ray/source_region.h @@ -149,6 +149,7 @@ class SourceRegionHandle { int* temperature_idx_; double* density_mult_; int* is_small_; + int* n_negative_batches_; int* converged_negative_; int* n_hits_; int* birthday_; @@ -205,6 +206,7 @@ class SourceRegionHandle { const int temperature_idx() const { return *temperature_idx_; } int& is_small() { return *is_small_; } + int& n_negative_batches() { return *n_negative_batches_; } const int is_small() const { return *is_small_; } int& converged_negative() { return *converged_negative_; } const int converged_negative() const { return *converged_negative_; } @@ -342,6 +344,10 @@ class SourceRegion { int external_source_present_ { 0}; //!< Is an external source present in this region? int is_small_ {0}; //!< Is it "small", receiving < 1.5 hits per iteration? + int n_negative_batches_ { + 0}; //!< Number of batches in which this region's flux went negative + //!< before the strict adaptive estimator's non-negativity + //!< enforcement (drives the chronic-negativity demotion) int converged_negative_ { 0}; //!< Demote-only flag (adaptive estimator only), evaluated from the //!< running accumulated flux at the inactive->active transition and @@ -422,6 +428,7 @@ class SourceRegionContainer { const double density_mult(int64_t sr) const { return density_mult_[sr]; } int& is_small(int64_t sr) { return is_small_[sr]; } + int& n_negative_batches(int64_t sr) { return n_negative_batches_[sr]; } const int is_small(int64_t sr) const { return is_small_[sr]; } int& converged_negative(int64_t sr) { return converged_negative_[sr]; } const int converged_negative(int64_t sr) const @@ -669,6 +676,7 @@ class SourceRegionContainer { vector temperature_idx_; vector density_mult_; vector is_small_; + vector n_negative_batches_; vector converged_negative_; vector n_hits_; vector mesh_; diff --git a/openmc/settings.py b/openmc/settings.py index 984fe8a8755..59b6a572b5d 100644 --- a/openmc/settings.py +++ b/openmc/settings.py @@ -202,15 +202,19 @@ class Settings: specified by a :class:`openmc.SourceBase` object. :volume_estimator: Choice of volume estimator for the random ray solver. Options are - 'naive', 'simulation_averaged', 'hybrid', or 'adaptive'. The default - is 'adaptive'. The 'adaptive' estimator generalizes 'hybrid': it uses - the simulation-averaged volume by default but falls back to the - naive (iteration) volume in regions with a strong inhomogeneous - source (kappa test), in hit-starved regions, and in regions whose - accumulated flux is negative at the end of the inactive phase (a - one-shot demotion for the active batches), which removes the - negative-flux instabilities 'hybrid' can exhibit in optically - thin, in-scatter-fed regions. + 'naive', 'simulation_averaged', 'hybrid', 'adaptive', + 'strict_adaptive', or 'auto'. The default is 'auto', which selects + 'adaptive' for standard solves and 'strict_adaptive' for solves + whose results feed variance reduction (weight window generation + and any adjoint workflow). The 'adaptive' estimator generalizes + 'hybrid': it uses the simulation-averaged volume by default but + falls back to the naive (iteration) volume in individual regions + where that estimator is unsafe, removing the negative-flux + instabilities 'hybrid' can exhibit in optically thin, + in-scatter-fed regions. The 'strict_adaptive' estimator runs the + same machinery and additionally enforces non-negativity on the + flux iterates every batch, guaranteeing non-negative fluxes at + the cost of a small conservative bias. :source_shape: Assumed shape of the source distribution within each source region. Options are 'flat' (default), 'linear', or 'linear_xy'. @@ -1423,7 +1427,8 @@ def random_ray(self, random_ray: dict): elif key == 'volume_estimator': cv.check_value('volume estimator', value, ('naive', 'simulation_averaged', - 'hybrid', 'adaptive')) + 'hybrid', 'adaptive', 'strict_adaptive', + 'auto')) elif key == 'source_shape': cv.check_value('source shape', value, ('flat', 'linear', 'linear_xy')) diff --git a/src/random_ray/flat_source_domain.cpp b/src/random_ray/flat_source_domain.cpp index 1ce1c83cee4..d317a9b3cd3 100644 --- a/src/random_ray/flat_source_domain.cpp +++ b/src/random_ray/flat_source_domain.cpp @@ -28,7 +28,8 @@ namespace openmc { // Static Variable Declarations RandomRayVolumeEstimator FlatSourceDomain::volume_estimator_ { - RandomRayVolumeEstimator::ADAPTIVE}; + RandomRayVolumeEstimator::AUTO}; +bool FlatSourceDomain::volume_estimator_is_auto_ {true}; bool FlatSourceDomain::volume_normalized_flux_tallies_ {false}; bool FlatSourceDomain::adjoint_requested_ {false}; RandomRaySolve FlatSourceDomain::solve_ {RandomRaySolve::FORWARD}; @@ -56,7 +57,7 @@ FlatSourceDomain::FlatSourceDomain() : negroups_(data::mg.num_energy_groups_) // Initialize source regions. bool is_linear = RandomRay::source_shape_ != RandomRaySourceShape::FLAT; - bool is_adaptive = volume_estimator_ == RandomRayVolumeEstimator::ADAPTIVE; + bool is_adaptive = is_adaptive_family(volume_estimator_); source_regions_ = SourceRegionContainer(negroups_, is_linear, is_adaptive); // Initialize tally volumes @@ -155,7 +156,7 @@ void FlatSourceDomain::accumulate_iteration_flux() // add_source_to_scalar_flux and by the linear-source gradient fallback. void FlatSourceDomain::demotion_step() { - if (volume_estimator_ != RandomRayVolumeEstimator::ADAPTIVE) + if (!is_adaptive_family(volume_estimator_)) return; #pragma omp parallel for @@ -420,11 +421,18 @@ int64_t FlatSourceDomain::add_source_to_scalar_flux() // demote-to-naive volume switch, and the previous-flux miss treatment, with // demote-only decisions made from the running accumulated flux (recorded // as a flag in converged_negative by demotion_step). - const bool is_adaptive = - volume_estimator_ == RandomRayVolumeEstimator::ADAPTIVE; + const bool is_adaptive = is_adaptive_family(volume_estimator_); + // The strict adaptive estimator additionally enforces non-negativity on + // the flux iterates each batch (see the enforcement step below). + const bool is_strict = + volume_estimator_ == RandomRayVolumeEstimator::STRICT_ADAPTIVE; + int64_t n_rescued = 0; + int64_t n_floored = 0; + int64_t n_chronic = 0; #pragma omp parallel for reduction( \ - + : n_hits, n_naive, n_latch, n_strong, n_sign, n_small) + + : n_hits, n_naive, n_latch, n_strong, n_sign, n_small, n_rescued, \ + n_floored, n_chronic) for (int64_t sr = 0; sr < n_source_regions(); sr++) { double volume_simulation_avg = source_regions_.volume(sr); @@ -526,6 +534,7 @@ int64_t FlatSourceDomain::add_source_to_scalar_flux() use_naive_volume = external || small; break; case RandomRayVolumeEstimator::ADAPTIVE: + case RandomRayVolumeEstimator::STRICT_ADAPTIVE: use_naive_volume = small || strong_source || converged_neg; use_old_flux_on_miss = use_naive_volume; break; @@ -546,6 +555,8 @@ int64_t FlatSourceDomain::add_source_to_scalar_flux() n_latch++; } else if (conv_flag == 1) { n_sign++; + } else if (conv_flag == 3) { + n_chronic++; } else if (strong_source) { n_strong++; } else if (small) { @@ -553,12 +564,43 @@ int64_t FlatSourceDomain::add_source_to_scalar_flux() } } + bool region_rescued = false; + bool region_floored = false; for (int g = 0; g < negroups_; g++) { if (volume_iteration > 0.0) { // Hit this iteration: the flat source from the previous iteration plus // this iteration's transport contribution, normalized by the chosen // volume. set_flux_to_flux_plus_source(sr, volume, g); + // The strict adaptive estimator enforces non-negativity on the flux + // iterates: a group whose batch flux comes out negative is first + // rescued -- its transport term rescaled from the volume used to the + // batch's own volume, algebraically reproducing the naive-volume + // update, whose noise the near-cancellation regions require -- and, + // if still negative (or if the region already used the batch + // volume), floored at the previous iterate, which is non-negative by + // induction. This is what upgrades the family's demotion machinery + // into a guarantee: demotion alone cannot prevent a region from + // inheriting negativity through in-scatter from not-yet-demoted + // neighbors. The price is a small conservative (positivity-clip) + // bias, which is why the strict estimator is not the standard-solve + // default. Linear-source flux moments are left untouched; demoted + // and hit-starved regions already fall back to flat shapes. + if (is_strict && source_regions_.scalar_flux_new(sr, g) < 0.0) { + if (volume != volume_iteration) { + double src = source_regions_.source(sr, g); + source_regions_.scalar_flux_new(sr, g) = + (source_regions_.scalar_flux_new(sr, g) - src) * + (volume / volume_iteration) + + src; + region_rescued = true; + } + if (source_regions_.scalar_flux_new(sr, g) < 0.0) { + source_regions_.scalar_flux_new(sr, g) = + source_regions_.scalar_flux_old(sr, g); + region_floored = true; + } + } } else if (volume_simulation_avg > 0.0) { // Missed this iteration but hit previously: substitute per the miss // policy decided above (the previous iterate, or the reduced source). @@ -576,17 +618,41 @@ int64_t FlatSourceDomain::add_source_to_scalar_flux() "the source region mesh."); } } + // Chronic-negativity demotion (strict adaptive only): the + // non-negativity floor prevents a chronically noisy region's + // accumulated flux from ever going negative, masking the very signal + // the accumulated sign demotion detects -- so left alone, such a + // region would be clipped every batch, a one-sided ratchet that + // biases its flux upward. Counting pre-enforcement negative batches + // restores the escape: after a few events the region is demoted to + // the naive volume and previous-flux miss treatment, where clipping + // is no longer needed. + if (is_strict && (region_rescued || region_floored)) { + int n = ++source_regions_.n_negative_batches(sr); + double threshold = + std::max(static_cast(NEGATIVE_FLUX_DEMOTION_MIN_COUNT), + NEGATIVE_FLUX_DEMOTION_RATE * simulation::current_batch); + if (n >= threshold && source_regions_.converged_negative(sr) == 0) { + source_regions_.converged_negative(sr) = 3; + } + } + if (final_iteration) { + n_rescued += region_rescued; + n_floored += region_floored; + } } // Store the final-iteration treatment snapshot for reporting (adaptive only; // the other estimators do not produce a by-cause naive-treatment breakdown) - if (final_iteration && - volume_estimator_ == RandomRayVolumeEstimator::ADAPTIVE) { + if (final_iteration && is_adaptive) { n_final_naive_ = n_naive; n_final_latch_ = n_latch; n_final_strong_ = n_strong; n_final_sign_ = n_sign; n_final_small_ = n_small; + n_final_chronic_ = n_chronic; + n_final_rescued_ = n_rescued; + n_final_floored_ = n_floored; final_stats_valid_ = true; } diff --git a/src/random_ray/linear_source_domain.cpp b/src/random_ray/linear_source_domain.cpp index 29e5564ac6a..31610eef5c3 100644 --- a/src/random_ray/linear_source_domain.cpp +++ b/src/random_ray/linear_source_domain.cpp @@ -125,8 +125,7 @@ void LinearSourceDomain::update_single_neutron_source(SourceRegionHandle& srh) // negative accumulated flux means the fitted gradients carry no meaningful // shape information, and a latched strong feed is the gradient-scale noise // hazard the strong-source fallback above exists for. - if (volume_estimator_ == RandomRayVolumeEstimator::ADAPTIVE && - material != MATERIAL_VOID && + if (is_adaptive_family(volume_estimator_) && material != MATERIAL_VOID && (srh.converged_negative() > 0 || region_has_strong_source(&srh.source(0), &srh.scalar_flux_old(0), simulation::current_batch <= settings::n_inactive))) { diff --git a/src/random_ray/random_ray_simulation.cpp b/src/random_ray/random_ray_simulation.cpp index eb8a8b0ba2b..1108f8d4763 100644 --- a/src/random_ray/random_ray_simulation.cpp +++ b/src/random_ray/random_ray_simulation.cpp @@ -287,7 +287,8 @@ void validate_random_ray_inputs() void openmc_finalize_random_ray() { - FlatSourceDomain::volume_estimator_ = RandomRayVolumeEstimator::ADAPTIVE; + FlatSourceDomain::volume_estimator_ = RandomRayVolumeEstimator::AUTO; + FlatSourceDomain::volume_estimator_is_auto_ = true; FlatSourceDomain::volume_normalized_flux_tallies_ = false; FlatSourceDomain::adjoint_requested_ = false; FlatSourceDomain::solve_ = RandomRaySolve::FORWARD; @@ -617,9 +618,15 @@ void RandomRaySimulation::print_results_random_ray( case RandomRayVolumeEstimator::ADAPTIVE: estimator = "Adaptive"; break; + case RandomRayVolumeEstimator::STRICT_ADAPTIVE: + estimator = "Strict Adaptive"; + break; default: fatal_error("Invalid volume estimator type"); } + if (FlatSourceDomain::volume_estimator_is_auto_) { + estimator += " (auto)"; + } fmt::print(" Volume Estimator Type = {}\n", estimator); if (domain_->final_stats_valid_) { double inv = 100.0 / domain_->n_source_regions(); @@ -645,6 +652,19 @@ void RandomRaySimulation::print_results_random_ray( domain_->n_final_sign_, domain_->n_final_sign_ * inv); fmt::print(" Hit-starved (per batch) = {} SRs ({:.4f}%)\n", domain_->n_final_small_, domain_->n_final_small_ * inv); + // The strict adaptive estimator's per-batch non-negativity + // enforcement, reported for the final batch. These overlap the + // partition above rather than extending it: a rescued or floored + // region may or may not also carry the naive treatment. + if (FlatSourceDomain::volume_estimator_ == + RandomRayVolumeEstimator::STRICT_ADAPTIVE) { + fmt::print(" Chronic negative (per batch) = {} SRs ({:.4f}%)\n", + domain_->n_final_chronic_, domain_->n_final_chronic_ * inv); + fmt::print(" Rescued (batch volume) = {} SRs ({:.4f}%)\n", + domain_->n_final_rescued_, domain_->n_final_rescued_ * inv); + fmt::print(" Floored (previous flux) = {} SRs ({:.4f}%)\n", + domain_->n_final_floored_, domain_->n_final_floored_ * inv); + } } } @@ -723,6 +743,23 @@ void openmc_run_random_ray() { using namespace openmc; + // Resolve the "auto" volume estimator (the default) to a concrete + // estimator based on the type of simulation being performed. Solves whose + // results feed variance reduction -- weight window generation, and any + // adjoint workflow, including the forward solve an adjoint source is + // derived from -- receive the strict adaptive estimator, whose guaranteed + // non-negative fluxes those workflows require. All other solves receive + // the adaptive estimator, which preserves unbiasedness at the cost of + // allowing rare noise-driven negative tallies in near-zero-flux regions. + if (FlatSourceDomain::volume_estimator_ == RandomRayVolumeEstimator::AUTO) { + bool positivity_needed = + FlatSourceDomain::adjoint_requested_ || + !variance_reduction::weight_windows_generators.empty(); + FlatSourceDomain::volume_estimator_ = + positivity_needed ? RandomRayVolumeEstimator::STRICT_ADAPTIVE + : RandomRayVolumeEstimator::ADAPTIVE; + } + // Determine which solves to run. If adjoint results are requested and no // user-defined adjoint source is present, an initial forward solve is needed // to construct the adjoint source from the forward flux (FW-CADIS). If the diff --git a/src/random_ray/source_region.cpp b/src/random_ray/source_region.cpp index 47319932484..8c6025dfb24 100644 --- a/src/random_ray/source_region.cpp +++ b/src/random_ray/source_region.cpp @@ -12,7 +12,9 @@ namespace openmc { SourceRegionHandle::SourceRegionHandle(SourceRegion& sr) : negroups_(sr.scalar_flux_old_.size()), material_(&sr.material_), temperature_idx_(&sr.temperature_idx_), density_mult_(&sr.density_mult_), - is_small_(&sr.is_small_), converged_negative_(&sr.converged_negative_), + is_small_(&sr.is_small_), + n_negative_batches_(&sr.n_negative_batches_), + converged_negative_(&sr.converged_negative_), n_hits_(&sr.n_hits_), is_linear_(sr.source_gradients_.size() > 0), lock_(&sr.lock_), volume_(&sr.volume_), volume_t_(&sr.volume_t_), volume_sq_(&sr.volume_sq_), @@ -75,6 +77,7 @@ void SourceRegionContainer::push_back(const SourceRegion& sr) temperature_idx_.push_back(sr.temperature_idx_); density_mult_.push_back(sr.density_mult_); is_small_.push_back(sr.is_small_); + n_negative_batches_.push_back(sr.n_negative_batches_); converged_negative_.push_back(sr.converged_negative_); n_hits_.push_back(sr.n_hits_); lock_.push_back(sr.lock_); @@ -135,6 +138,7 @@ void SourceRegionContainer::assign( temperature_idx_.clear(); density_mult_.clear(); is_small_.clear(); + n_negative_batches_.clear(); converged_negative_.clear(); n_hits_.clear(); lock_.clear(); @@ -196,6 +200,7 @@ SourceRegionHandle SourceRegionContainer::get_source_region_handle(int64_t sr) handle.temperature_idx_ = &temperature_idx(sr); handle.density_mult_ = &density_mult(sr); handle.is_small_ = &is_small(sr); + handle.n_negative_batches_ = &n_negative_batches(sr); handle.converged_negative_ = &converged_negative(sr); handle.n_hits_ = &n_hits(sr); handle.is_linear_ = is_linear(); @@ -241,6 +246,7 @@ void SourceRegionContainer::adjoint_reset() { std::fill(n_hits_.begin(), n_hits_.end(), 0); std::fill(converged_negative_.begin(), converged_negative_.end(), 0); + std::fill(n_negative_batches_.begin(), n_negative_batches_.end(), 0); std::fill(volume_.begin(), volume_.end(), 0.0); std::fill(volume_t_.begin(), volume_t_.end(), 0.0); std::fill(volume_sq_.begin(), volume_sq_.end(), 0.0); diff --git a/src/settings.cpp b/src/settings.cpp index 3b4be668e8a..8bdfda694bf 100644 --- a/src/settings.cpp +++ b/src/settings.cpp @@ -303,9 +303,16 @@ void get_run_parameters(pugi::xml_node node_base) } else if (temp_str == "adaptive") { FlatSourceDomain::volume_estimator_ = RandomRayVolumeEstimator::ADAPTIVE; + } else if (temp_str == "strict_adaptive") { + FlatSourceDomain::volume_estimator_ = + RandomRayVolumeEstimator::STRICT_ADAPTIVE; + } else if (temp_str == "auto") { + FlatSourceDomain::volume_estimator_ = RandomRayVolumeEstimator::AUTO; } else { fatal_error("Unrecognized volume estimator: " + temp_str); } + FlatSourceDomain::volume_estimator_is_auto_ = + FlatSourceDomain::volume_estimator_ == RandomRayVolumeEstimator::AUTO; } if (check_for_node(random_ray_node, "source_shape")) { std::string temp_str = diff --git a/tests/regression_tests/random_ray_volume_estimator/strict_adaptive/inputs_true.dat b/tests/regression_tests/random_ray_volume_estimator/strict_adaptive/inputs_true.dat new file mode 100644 index 00000000000..f7aa1795cfe --- /dev/null +++ b/tests/regression_tests/random_ray_volume_estimator/strict_adaptive/inputs_true.dat @@ -0,0 +1,247 @@ + + + + mgxs.h5 + + + + + + + + + + + + + + + + + + + + + 2.5 2.5 2.5 + 12 12 12 + 0.0 0.0 0.0 + +3 3 3 3 3 3 3 3 3 3 3 3 +3 3 3 3 3 3 3 3 3 3 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +1 1 2 2 2 2 2 2 2 2 3 3 +1 1 2 2 2 2 2 2 2 2 3 3 + +3 3 3 3 3 3 3 3 3 3 3 3 +3 3 3 3 3 3 3 3 3 3 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +1 1 2 2 2 2 2 2 2 2 3 3 +1 1 2 2 2 2 2 2 2 2 3 3 + +3 3 3 3 3 3 3 3 3 3 3 3 +3 3 3 3 3 3 3 3 3 3 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 + +3 3 3 3 3 3 3 3 3 3 3 3 +3 3 3 3 3 3 3 3 3 3 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 + +3 3 3 3 3 3 3 3 3 3 3 3 +3 3 3 3 3 3 3 3 3 3 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 + +3 3 3 3 3 3 3 3 3 3 3 3 +3 3 3 3 3 3 3 3 3 3 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 + +3 3 3 3 3 3 3 3 3 3 3 3 +3 3 3 3 3 3 3 3 3 3 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 + +3 3 3 3 3 3 3 3 3 3 3 3 +3 3 3 3 3 3 3 3 3 3 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 + +3 3 3 3 3 3 3 3 3 3 3 3 +3 3 3 3 3 3 3 3 3 3 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 + +3 3 3 3 3 3 3 3 3 3 3 3 +3 3 3 3 3 3 3 3 3 3 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 + +3 3 3 3 3 3 3 3 3 3 3 3 +3 3 3 3 3 3 3 3 3 3 3 3 +3 3 3 3 3 3 3 3 3 3 3 3 +3 3 3 3 3 3 3 3 3 3 3 3 +3 3 3 3 3 3 3 3 3 3 3 3 +3 3 3 3 3 3 3 3 3 3 3 3 +3 3 3 3 3 3 3 3 3 3 3 3 +3 3 3 3 3 3 3 3 3 3 3 3 +3 3 3 3 3 3 3 3 3 3 3 3 +3 3 3 3 3 3 3 3 3 3 3 3 +3 3 3 3 3 3 3 3 3 3 3 3 +3 3 3 3 3 3 3 3 3 3 3 3 + +3 3 3 3 3 3 3 3 3 3 3 3 +3 3 3 3 3 3 3 3 3 3 3 3 +3 3 3 3 3 3 3 3 3 3 3 3 +3 3 3 3 3 3 3 3 3 3 3 3 +3 3 3 3 3 3 3 3 3 3 3 3 +3 3 3 3 3 3 3 3 3 3 3 3 +3 3 3 3 3 3 3 3 3 3 3 3 +3 3 3 3 3 3 3 3 3 3 3 3 +3 3 3 3 3 3 3 3 3 3 3 3 +3 3 3 3 3 3 3 3 3 3 3 3 +3 3 3 3 3 3 3 3 3 3 3 3 +3 3 3 3 3 3 3 3 3 3 3 3 + + + + + + + + + + fixed source + 10 + 40 + 20 + + + 100.0 1.0 + + + universe + 1 + + + multi-group + + 500.0 + 100.0 + + + + 0.0 0.0 0.0 30.0 30.0 30.0 + + + + true + strict_adaptive + + + + + 1 + + + 2 + + + 3 + + + 3 + flux + tracklength + + + 2 + flux + tracklength + + + 1 + flux + tracklength + + + diff --git a/tests/regression_tests/random_ray_volume_estimator/strict_adaptive/results_true.dat b/tests/regression_tests/random_ray_volume_estimator/strict_adaptive/results_true.dat new file mode 100644 index 00000000000..ea66b9cc72b --- /dev/null +++ b/tests/regression_tests/random_ray_volume_estimator/strict_adaptive/results_true.dat @@ -0,0 +1,9 @@ +tally 1: +2.274462E+00 +2.703615E-01 +tally 2: +1.222058E-01 +9.217159E-04 +tally 3: +7.435355E-03 +3.594692E-06 diff --git a/tests/regression_tests/random_ray_volume_estimator/test.py b/tests/regression_tests/random_ray_volume_estimator/test.py index ded37303ade..fd2ae30ffb9 100644 --- a/tests/regression_tests/random_ray_volume_estimator/test.py +++ b/tests/regression_tests/random_ray_volume_estimator/test.py @@ -26,7 +26,8 @@ def _cleanup(self): @pytest.mark.parametrize("estimator", ["hybrid", "simulation_averaged", "naive", - "adaptive" + "adaptive", + "strict_adaptive" ]) def test_random_ray_volume_estimator(estimator): with change_directory(estimator): diff --git a/tests/regression_tests/random_ray_volume_estimator_auto/__init__.py b/tests/regression_tests/random_ray_volume_estimator_auto/__init__.py new file mode 100644 index 00000000000..e69de29bb2d diff --git a/tests/regression_tests/random_ray_volume_estimator_auto/adjoint/inputs_true.dat b/tests/regression_tests/random_ray_volume_estimator_auto/adjoint/inputs_true.dat new file mode 100644 index 00000000000..66d297ada61 --- /dev/null +++ b/tests/regression_tests/random_ray_volume_estimator_auto/adjoint/inputs_true.dat @@ -0,0 +1,247 @@ + + + + mgxs.h5 + + + + + + + + + + + + + + + + + + + + + 2.5 2.5 2.5 + 12 12 12 + 0.0 0.0 0.0 + +3 3 3 3 3 3 3 3 3 3 3 3 +3 3 3 3 3 3 3 3 3 3 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +1 1 2 2 2 2 2 2 2 2 3 3 +1 1 2 2 2 2 2 2 2 2 3 3 + +3 3 3 3 3 3 3 3 3 3 3 3 +3 3 3 3 3 3 3 3 3 3 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +1 1 2 2 2 2 2 2 2 2 3 3 +1 1 2 2 2 2 2 2 2 2 3 3 + +3 3 3 3 3 3 3 3 3 3 3 3 +3 3 3 3 3 3 3 3 3 3 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 + +3 3 3 3 3 3 3 3 3 3 3 3 +3 3 3 3 3 3 3 3 3 3 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 + +3 3 3 3 3 3 3 3 3 3 3 3 +3 3 3 3 3 3 3 3 3 3 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 + +3 3 3 3 3 3 3 3 3 3 3 3 +3 3 3 3 3 3 3 3 3 3 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 + +3 3 3 3 3 3 3 3 3 3 3 3 +3 3 3 3 3 3 3 3 3 3 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 + +3 3 3 3 3 3 3 3 3 3 3 3 +3 3 3 3 3 3 3 3 3 3 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 + +3 3 3 3 3 3 3 3 3 3 3 3 +3 3 3 3 3 3 3 3 3 3 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 + +3 3 3 3 3 3 3 3 3 3 3 3 +3 3 3 3 3 3 3 3 3 3 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 + +3 3 3 3 3 3 3 3 3 3 3 3 +3 3 3 3 3 3 3 3 3 3 3 3 +3 3 3 3 3 3 3 3 3 3 3 3 +3 3 3 3 3 3 3 3 3 3 3 3 +3 3 3 3 3 3 3 3 3 3 3 3 +3 3 3 3 3 3 3 3 3 3 3 3 +3 3 3 3 3 3 3 3 3 3 3 3 +3 3 3 3 3 3 3 3 3 3 3 3 +3 3 3 3 3 3 3 3 3 3 3 3 +3 3 3 3 3 3 3 3 3 3 3 3 +3 3 3 3 3 3 3 3 3 3 3 3 +3 3 3 3 3 3 3 3 3 3 3 3 + +3 3 3 3 3 3 3 3 3 3 3 3 +3 3 3 3 3 3 3 3 3 3 3 3 +3 3 3 3 3 3 3 3 3 3 3 3 +3 3 3 3 3 3 3 3 3 3 3 3 +3 3 3 3 3 3 3 3 3 3 3 3 +3 3 3 3 3 3 3 3 3 3 3 3 +3 3 3 3 3 3 3 3 3 3 3 3 +3 3 3 3 3 3 3 3 3 3 3 3 +3 3 3 3 3 3 3 3 3 3 3 3 +3 3 3 3 3 3 3 3 3 3 3 3 +3 3 3 3 3 3 3 3 3 3 3 3 +3 3 3 3 3 3 3 3 3 3 3 3 + + + + + + + + + + fixed source + 10 + 40 + 20 + + + 100.0 1.0 + + + universe + 1 + + + multi-group + + 500.0 + 100.0 + + + + 0.0 0.0 0.0 30.0 30.0 30.0 + + + + true + true + + + + + 1 + + + 2 + + + 3 + + + 3 + flux + tracklength + + + 2 + flux + tracklength + + + 1 + flux + tracklength + + + diff --git a/tests/regression_tests/random_ray_volume_estimator_auto/adjoint/results_true.dat b/tests/regression_tests/random_ray_volume_estimator_auto/adjoint/results_true.dat new file mode 100644 index 00000000000..7b6c664d32f --- /dev/null +++ b/tests/regression_tests/random_ray_volume_estimator_auto/adjoint/results_true.dat @@ -0,0 +1,9 @@ +tally 1: +3.511731E+06 +7.366501E+11 +tally 2: +4.076663E+06 +8.358445E+11 +tally 3: +1.531116E+07 +1.175684E+13 diff --git a/tests/regression_tests/random_ray_volume_estimator_auto/forward/inputs_true.dat b/tests/regression_tests/random_ray_volume_estimator_auto/forward/inputs_true.dat new file mode 100644 index 00000000000..dca77cb4edb --- /dev/null +++ b/tests/regression_tests/random_ray_volume_estimator_auto/forward/inputs_true.dat @@ -0,0 +1,246 @@ + + + + mgxs.h5 + + + + + + + + + + + + + + + + + + + + + 2.5 2.5 2.5 + 12 12 12 + 0.0 0.0 0.0 + +3 3 3 3 3 3 3 3 3 3 3 3 +3 3 3 3 3 3 3 3 3 3 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +1 1 2 2 2 2 2 2 2 2 3 3 +1 1 2 2 2 2 2 2 2 2 3 3 + +3 3 3 3 3 3 3 3 3 3 3 3 +3 3 3 3 3 3 3 3 3 3 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +1 1 2 2 2 2 2 2 2 2 3 3 +1 1 2 2 2 2 2 2 2 2 3 3 + +3 3 3 3 3 3 3 3 3 3 3 3 +3 3 3 3 3 3 3 3 3 3 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 + +3 3 3 3 3 3 3 3 3 3 3 3 +3 3 3 3 3 3 3 3 3 3 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 + +3 3 3 3 3 3 3 3 3 3 3 3 +3 3 3 3 3 3 3 3 3 3 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 + +3 3 3 3 3 3 3 3 3 3 3 3 +3 3 3 3 3 3 3 3 3 3 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 + +3 3 3 3 3 3 3 3 3 3 3 3 +3 3 3 3 3 3 3 3 3 3 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 + +3 3 3 3 3 3 3 3 3 3 3 3 +3 3 3 3 3 3 3 3 3 3 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 + +3 3 3 3 3 3 3 3 3 3 3 3 +3 3 3 3 3 3 3 3 3 3 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 + +3 3 3 3 3 3 3 3 3 3 3 3 +3 3 3 3 3 3 3 3 3 3 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 + +3 3 3 3 3 3 3 3 3 3 3 3 +3 3 3 3 3 3 3 3 3 3 3 3 +3 3 3 3 3 3 3 3 3 3 3 3 +3 3 3 3 3 3 3 3 3 3 3 3 +3 3 3 3 3 3 3 3 3 3 3 3 +3 3 3 3 3 3 3 3 3 3 3 3 +3 3 3 3 3 3 3 3 3 3 3 3 +3 3 3 3 3 3 3 3 3 3 3 3 +3 3 3 3 3 3 3 3 3 3 3 3 +3 3 3 3 3 3 3 3 3 3 3 3 +3 3 3 3 3 3 3 3 3 3 3 3 +3 3 3 3 3 3 3 3 3 3 3 3 + +3 3 3 3 3 3 3 3 3 3 3 3 +3 3 3 3 3 3 3 3 3 3 3 3 +3 3 3 3 3 3 3 3 3 3 3 3 +3 3 3 3 3 3 3 3 3 3 3 3 +3 3 3 3 3 3 3 3 3 3 3 3 +3 3 3 3 3 3 3 3 3 3 3 3 +3 3 3 3 3 3 3 3 3 3 3 3 +3 3 3 3 3 3 3 3 3 3 3 3 +3 3 3 3 3 3 3 3 3 3 3 3 +3 3 3 3 3 3 3 3 3 3 3 3 +3 3 3 3 3 3 3 3 3 3 3 3 +3 3 3 3 3 3 3 3 3 3 3 3 + + + + + + + + + + fixed source + 10 + 40 + 20 + + + 100.0 1.0 + + + universe + 1 + + + multi-group + + 500.0 + 100.0 + + + + 0.0 0.0 0.0 30.0 30.0 30.0 + + + + true + + + + + 1 + + + 2 + + + 3 + + + 3 + flux + tracklength + + + 2 + flux + tracklength + + + 1 + flux + tracklength + + + diff --git a/tests/regression_tests/random_ray_volume_estimator_auto/forward/results_true.dat b/tests/regression_tests/random_ray_volume_estimator_auto/forward/results_true.dat new file mode 100644 index 00000000000..ac9fcd45390 --- /dev/null +++ b/tests/regression_tests/random_ray_volume_estimator_auto/forward/results_true.dat @@ -0,0 +1,9 @@ +tally 1: +2.259425E+00 +2.675738E-01 +tally 2: +1.018431E-01 +6.713412E-04 +tally 3: +7.288312E-03 +3.488945E-06 diff --git a/tests/regression_tests/random_ray_volume_estimator_auto/test.py b/tests/regression_tests/random_ray_volume_estimator_auto/test.py new file mode 100644 index 00000000000..f0fa0d6aaf7 --- /dev/null +++ b/tests/regression_tests/random_ray_volume_estimator_auto/test.py @@ -0,0 +1,37 @@ +import os + +import openmc +from openmc.utility_funcs import change_directory +from openmc.examples import random_ray_three_region_cube +import pytest + +from tests.testing_harness import TolerantPyAPITestHarness + + +class MGXSTestHarness(TolerantPyAPITestHarness): + def _cleanup(self): + super()._cleanup() + f = 'mgxs.h5' + if os.path.exists(f): + os.remove(f) + + +# The default "auto" volume estimator resolves by solve type: standard +# solves receive the adaptive estimator, while solves whose results feed +# variance reduction (any adjoint workflow, and weight window generation) +# receive the strict adaptive estimator. Neither case sets an estimator +# explicitly, so these golds pin the routing itself: if the resolution +# policy regresses, the affected case's results shift. +@pytest.mark.parametrize("solve", ["forward", "adjoint"]) +def test_random_ray_volume_estimator_auto(solve): + with change_directory(solve): + openmc.reset_auto_ids() + model = random_ray_three_region_cube() + if solve == "adjoint": + model.settings.random_ray['adjoint'] = True + model.settings.particles = 10 + model.settings.inactive = 20 + model.settings.batches = 40 + + harness = MGXSTestHarness('statepoint.40.h5', model) + harness.main() diff --git a/tests/regression_tests/random_ray_volume_estimator_linear/strict_adaptive/inputs_true.dat b/tests/regression_tests/random_ray_volume_estimator_linear/strict_adaptive/inputs_true.dat new file mode 100644 index 00000000000..4d0d5f1f7a6 --- /dev/null +++ b/tests/regression_tests/random_ray_volume_estimator_linear/strict_adaptive/inputs_true.dat @@ -0,0 +1,248 @@ + + + + mgxs.h5 + + + + + + + + + + + + + + + + + + + + + 2.5 2.5 2.5 + 12 12 12 + 0.0 0.0 0.0 + +3 3 3 3 3 3 3 3 3 3 3 3 +3 3 3 3 3 3 3 3 3 3 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +1 1 2 2 2 2 2 2 2 2 3 3 +1 1 2 2 2 2 2 2 2 2 3 3 + +3 3 3 3 3 3 3 3 3 3 3 3 +3 3 3 3 3 3 3 3 3 3 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +1 1 2 2 2 2 2 2 2 2 3 3 +1 1 2 2 2 2 2 2 2 2 3 3 + +3 3 3 3 3 3 3 3 3 3 3 3 +3 3 3 3 3 3 3 3 3 3 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 + +3 3 3 3 3 3 3 3 3 3 3 3 +3 3 3 3 3 3 3 3 3 3 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 + +3 3 3 3 3 3 3 3 3 3 3 3 +3 3 3 3 3 3 3 3 3 3 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 + +3 3 3 3 3 3 3 3 3 3 3 3 +3 3 3 3 3 3 3 3 3 3 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 + +3 3 3 3 3 3 3 3 3 3 3 3 +3 3 3 3 3 3 3 3 3 3 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 + +3 3 3 3 3 3 3 3 3 3 3 3 +3 3 3 3 3 3 3 3 3 3 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 + +3 3 3 3 3 3 3 3 3 3 3 3 +3 3 3 3 3 3 3 3 3 3 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 + +3 3 3 3 3 3 3 3 3 3 3 3 +3 3 3 3 3 3 3 3 3 3 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 + +3 3 3 3 3 3 3 3 3 3 3 3 +3 3 3 3 3 3 3 3 3 3 3 3 +3 3 3 3 3 3 3 3 3 3 3 3 +3 3 3 3 3 3 3 3 3 3 3 3 +3 3 3 3 3 3 3 3 3 3 3 3 +3 3 3 3 3 3 3 3 3 3 3 3 +3 3 3 3 3 3 3 3 3 3 3 3 +3 3 3 3 3 3 3 3 3 3 3 3 +3 3 3 3 3 3 3 3 3 3 3 3 +3 3 3 3 3 3 3 3 3 3 3 3 +3 3 3 3 3 3 3 3 3 3 3 3 +3 3 3 3 3 3 3 3 3 3 3 3 + +3 3 3 3 3 3 3 3 3 3 3 3 +3 3 3 3 3 3 3 3 3 3 3 3 +3 3 3 3 3 3 3 3 3 3 3 3 +3 3 3 3 3 3 3 3 3 3 3 3 +3 3 3 3 3 3 3 3 3 3 3 3 +3 3 3 3 3 3 3 3 3 3 3 3 +3 3 3 3 3 3 3 3 3 3 3 3 +3 3 3 3 3 3 3 3 3 3 3 3 +3 3 3 3 3 3 3 3 3 3 3 3 +3 3 3 3 3 3 3 3 3 3 3 3 +3 3 3 3 3 3 3 3 3 3 3 3 +3 3 3 3 3 3 3 3 3 3 3 3 + + + + + + + + + + fixed source + 10 + 40 + 20 + + + 100.0 1.0 + + + universe + 1 + + + multi-group + + 500.0 + 100.0 + + + + 0.0 0.0 0.0 30.0 30.0 30.0 + + + + true + strict_adaptive + linear + + + + + 1 + + + 2 + + + 3 + + + 3 + flux + tracklength + + + 2 + flux + tracklength + + + 1 + flux + tracklength + + + diff --git a/tests/regression_tests/random_ray_volume_estimator_linear/strict_adaptive/results_true.dat b/tests/regression_tests/random_ray_volume_estimator_linear/strict_adaptive/results_true.dat new file mode 100644 index 00000000000..4e0dbef63cd --- /dev/null +++ b/tests/regression_tests/random_ray_volume_estimator_linear/strict_adaptive/results_true.dat @@ -0,0 +1,9 @@ +tally 1: +2.282556E+00 +2.728257E-01 +tally 2: +1.337071E-01 +1.094397E-03 +tally 3: +8.725872E-03 +5.015646E-06 diff --git a/tests/regression_tests/random_ray_volume_estimator_linear/test.py b/tests/regression_tests/random_ray_volume_estimator_linear/test.py index 631029e4202..8adec4cd6ba 100644 --- a/tests/regression_tests/random_ray_volume_estimator_linear/test.py +++ b/tests/regression_tests/random_ray_volume_estimator_linear/test.py @@ -26,7 +26,8 @@ def _cleanup(self): @pytest.mark.parametrize("estimator", ["hybrid", "simulation_averaged", "naive", - "adaptive" + "adaptive", + "strict_adaptive" ]) def test_random_ray_volume_estimator_linear(estimator): with change_directory(estimator): From 4771627d11f6799256523ab4d2d043729d6437e0 Mon Sep 17 00:00:00 2001 From: John Tramm Date: Fri, 28 Aug 2026 02:37:52 +0000 Subject: [PATCH 20/35] Pin the weight-window trigger of the auto estimator routing The auto regression test's forward and adjoint cases pin the adjoint-flag trigger of the automatic volume estimator selection, but not its second trigger: a weight window generator present in settings routes even a purely forward run to the strict adaptive estimator. Add a weight_windows case that attaches an FW-CADIS generator without setting an estimator, so a regression in the generator-presence trigger shifts the gold. Co-Authored-By: Claude Fable 5 --- .../random_ray_volume_estimator_auto/test.py | 22 +- .../weight_windows/inputs_true.dat | 261 +++++++++++ .../weight_windows/results_true.dat | 442 ++++++++++++++++++ 3 files changed, 719 insertions(+), 6 deletions(-) create mode 100644 tests/regression_tests/random_ray_volume_estimator_auto/weight_windows/inputs_true.dat create mode 100644 tests/regression_tests/random_ray_volume_estimator_auto/weight_windows/results_true.dat diff --git a/tests/regression_tests/random_ray_volume_estimator_auto/test.py b/tests/regression_tests/random_ray_volume_estimator_auto/test.py index f0fa0d6aaf7..227665f42b3 100644 --- a/tests/regression_tests/random_ray_volume_estimator_auto/test.py +++ b/tests/regression_tests/random_ray_volume_estimator_auto/test.py @@ -11,24 +11,34 @@ class MGXSTestHarness(TolerantPyAPITestHarness): def _cleanup(self): super()._cleanup() - f = 'mgxs.h5' - if os.path.exists(f): - os.remove(f) + for f in ('mgxs.h5', 'weight_windows.h5'): + if os.path.exists(f): + os.remove(f) # The default "auto" volume estimator resolves by solve type: standard # solves receive the adaptive estimator, while solves whose results feed # variance reduction (any adjoint workflow, and weight window generation) -# receive the strict adaptive estimator. Neither case sets an estimator +# receive the strict adaptive estimator. No case sets an estimator # explicitly, so these golds pin the routing itself: if the resolution -# policy regresses, the affected case's results shift. -@pytest.mark.parametrize("solve", ["forward", "adjoint"]) +# policy regresses, the affected case's results shift. The forward and +# adjoint cases pin the adjoint-flag trigger; the weight_windows case pins +# the generator-presence trigger. +@pytest.mark.parametrize("solve", ["forward", "adjoint", "weight_windows"]) def test_random_ray_volume_estimator_auto(solve): with change_directory(solve): openmc.reset_auto_ids() model = random_ray_three_region_cube() if solve == "adjoint": model.settings.random_ray['adjoint'] = True + elif solve == "weight_windows": + ww_mesh = openmc.RegularMesh() + ww_mesh.dimension = (6, 6, 6) + ww_mesh.lower_left = (0.0, 0.0, 0.0) + ww_mesh.upper_right = (30.0, 30.0, 30.0) + model.settings.weight_window_generators = \ + openmc.WeightWindowGenerator( + method="fw_cadis", mesh=ww_mesh, max_realizations=40) model.settings.particles = 10 model.settings.inactive = 20 model.settings.batches = 40 diff --git a/tests/regression_tests/random_ray_volume_estimator_auto/weight_windows/inputs_true.dat b/tests/regression_tests/random_ray_volume_estimator_auto/weight_windows/inputs_true.dat new file mode 100644 index 00000000000..edb3f4834ff --- /dev/null +++ b/tests/regression_tests/random_ray_volume_estimator_auto/weight_windows/inputs_true.dat @@ -0,0 +1,261 @@ + + + + mgxs.h5 + + + + + + + + + + + + + + + + + + + + + 2.5 2.5 2.5 + 12 12 12 + 0.0 0.0 0.0 + +3 3 3 3 3 3 3 3 3 3 3 3 +3 3 3 3 3 3 3 3 3 3 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +1 1 2 2 2 2 2 2 2 2 3 3 +1 1 2 2 2 2 2 2 2 2 3 3 + +3 3 3 3 3 3 3 3 3 3 3 3 +3 3 3 3 3 3 3 3 3 3 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +1 1 2 2 2 2 2 2 2 2 3 3 +1 1 2 2 2 2 2 2 2 2 3 3 + +3 3 3 3 3 3 3 3 3 3 3 3 +3 3 3 3 3 3 3 3 3 3 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 + +3 3 3 3 3 3 3 3 3 3 3 3 +3 3 3 3 3 3 3 3 3 3 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 + +3 3 3 3 3 3 3 3 3 3 3 3 +3 3 3 3 3 3 3 3 3 3 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 + +3 3 3 3 3 3 3 3 3 3 3 3 +3 3 3 3 3 3 3 3 3 3 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 + +3 3 3 3 3 3 3 3 3 3 3 3 +3 3 3 3 3 3 3 3 3 3 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 + +3 3 3 3 3 3 3 3 3 3 3 3 +3 3 3 3 3 3 3 3 3 3 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 + +3 3 3 3 3 3 3 3 3 3 3 3 +3 3 3 3 3 3 3 3 3 3 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 + +3 3 3 3 3 3 3 3 3 3 3 3 +3 3 3 3 3 3 3 3 3 3 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 + +3 3 3 3 3 3 3 3 3 3 3 3 +3 3 3 3 3 3 3 3 3 3 3 3 +3 3 3 3 3 3 3 3 3 3 3 3 +3 3 3 3 3 3 3 3 3 3 3 3 +3 3 3 3 3 3 3 3 3 3 3 3 +3 3 3 3 3 3 3 3 3 3 3 3 +3 3 3 3 3 3 3 3 3 3 3 3 +3 3 3 3 3 3 3 3 3 3 3 3 +3 3 3 3 3 3 3 3 3 3 3 3 +3 3 3 3 3 3 3 3 3 3 3 3 +3 3 3 3 3 3 3 3 3 3 3 3 +3 3 3 3 3 3 3 3 3 3 3 3 + +3 3 3 3 3 3 3 3 3 3 3 3 +3 3 3 3 3 3 3 3 3 3 3 3 +3 3 3 3 3 3 3 3 3 3 3 3 +3 3 3 3 3 3 3 3 3 3 3 3 +3 3 3 3 3 3 3 3 3 3 3 3 +3 3 3 3 3 3 3 3 3 3 3 3 +3 3 3 3 3 3 3 3 3 3 3 3 +3 3 3 3 3 3 3 3 3 3 3 3 +3 3 3 3 3 3 3 3 3 3 3 3 +3 3 3 3 3 3 3 3 3 3 3 3 +3 3 3 3 3 3 3 3 3 3 3 3 +3 3 3 3 3 3 3 3 3 3 3 3 + + + + + + + + + + fixed source + 10 + 40 + 20 + + + 100.0 1.0 + + + universe + 1 + + + multi-group + + + 1 + neutron + 40 + 1 + true + fw_cadis + + + + 6 6 6 + 0.0 0.0 0.0 + 30.0 30.0 30.0 + + + 500.0 + 100.0 + + + + 0.0 0.0 0.0 30.0 30.0 30.0 + + + + true + + + + + 1 + + + 2 + + + 3 + + + 3 + flux + tracklength + + + 2 + flux + tracklength + + + 1 + flux + tracklength + + + diff --git a/tests/regression_tests/random_ray_volume_estimator_auto/weight_windows/results_true.dat b/tests/regression_tests/random_ray_volume_estimator_auto/weight_windows/results_true.dat new file mode 100644 index 00000000000..f9eb7e003ae --- /dev/null +++ b/tests/regression_tests/random_ray_volume_estimator_auto/weight_windows/results_true.dat @@ -0,0 +1,442 @@ +tally 1: +3.511731E+06 +7.366501E+11 +tally 2: +4.076663E+06 +8.358445E+11 +tally 3: +1.531116E+07 +1.175684E+13 +tally 4: +3.511731E+06 +7.366501E+11 +3.188749E+06 +5.374186E+11 +3.650230E+06 +7.551441E+11 +3.754951E+06 +7.778043E+11 +3.468497E+06 +7.956805E+11 +1.004161E+06 +5.103314E+10 +4.647621E+06 +1.334221E+12 +4.017974E+06 +8.770192E+11 +4.029574E+06 +8.966119E+11 +3.953177E+06 +8.652758E+11 +3.957070E+06 +8.892706E+11 +7.152463E+05 +2.836055E+10 +4.148038E+06 +9.672359E+11 +3.750889E+06 +7.648028E+11 +3.290958E+06 +6.160634E+11 +3.956685E+06 +8.356989E+11 +4.787057E+06 +1.189227E+12 +3.535766E+06 +6.427457E+11 +4.149550E+06 +1.006099E+12 +4.658888E+06 +1.149165E+12 +4.145351E+06 +9.728065E+11 +4.606747E+06 +1.205466E+12 +5.017471E+06 +1.401316E+12 +1.000641E+07 +5.175932E+12 +5.085297E+06 +1.542497E+12 +3.579261E+06 +7.951955E+11 +3.760606E+06 +8.413000E+11 +4.280654E+06 +9.893678E+11 +4.550993E+06 +1.153381E+12 +2.043152E+07 +2.134729E+13 +2.364753E+06 +2.880193E+11 +1.134082E+06 +6.713813E+10 +1.364444E+06 +9.760639E+10 +1.019639E+06 +5.340783E+10 +4.621795E+06 +1.181955E+12 +2.583832E+07 +3.849646E+13 +3.981869E+06 +1.086257E+12 +3.294522E+06 +6.029500E+11 +3.744218E+06 +7.967128E+11 +4.110475E+06 +8.967747E+11 +3.749926E+06 +8.162320E+11 +1.261849E+06 +8.335011E+10 +3.949725E+06 +9.014090E+11 +3.356913E+06 +5.874923E+11 +4.228265E+06 +1.026989E+12 +7.370925E+06 +3.037283E+12 +4.288574E+06 +1.000915E+12 +1.697752E+06 +1.488605E+11 +4.490552E+06 +1.208635E+12 +3.871653E+06 +8.072918E+11 +3.750247E+06 +7.870138E+11 +4.226575E+06 +9.572865E+11 +4.278271E+06 +9.839734E+11 +2.853690E+06 +4.295371E+11 +5.041098E+06 +1.477825E+12 +4.617993E+06 +1.225844E+12 +4.666636E+06 +1.204811E+12 +4.480502E+06 +1.062325E+12 +5.129911E+06 +1.423279E+12 +1.018553E+07 +5.448004E+12 +4.645548E+06 +1.225328E+12 +3.414316E+06 +7.183967E+11 +3.299665E+06 +6.152288E+11 +4.676476E+06 +1.226497E+12 +5.993866E+06 +1.916331E+12 +2.273457E+07 +2.634530E+13 +1.024619E+06 +5.418869E+10 +8.671716E+05 +4.105630E+10 +9.781613E+05 +4.922450E+10 +8.982715E+05 +4.632745E+10 +8.214506E+06 +3.502364E+12 +1.892745E+08 +1.817735E+15 +4.395286E+06 +1.140780E+12 +3.785742E+06 +8.767900E+11 +4.009253E+06 +9.466143E+11 +4.485226E+06 +1.135228E+12 +3.926414E+06 +8.341972E+11 +7.427310E+06 +2.962614E+12 +3.218770E+06 +5.613005E+11 +3.606096E+06 +6.835904E+11 +4.143995E+06 +8.943078E+11 +4.162951E+06 +9.119008E+11 +3.546854E+06 +6.595082E+11 +3.688159E+06 +7.104847E+11 +3.295761E+06 +6.129546E+11 +3.351777E+06 +6.027285E+11 +3.842937E+06 +8.073173E+11 +4.364532E+06 +1.012091E+12 +4.387931E+06 +1.025643E+12 +6.201362E+06 +1.994758E+12 +4.134093E+06 +9.194351E+11 +3.853368E+06 +9.429980E+11 +4.133933E+06 +1.051933E+12 +4.587503E+06 +1.204363E+12 +4.775310E+06 +1.220055E+12 +2.193940E+07 +2.484892E+13 +3.781856E+06 +8.523019E+11 +3.501634E+06 +7.260762E+11 +3.742681E+06 +7.892108E+11 +4.463231E+06 +1.270897E+12 +5.608885E+06 +1.688139E+12 +2.975687E+07 +4.750545E+13 +9.838028E+06 +5.129536E+12 +2.924712E+06 +4.804874E+11 +3.236877E+06 +5.427566E+11 +7.509008E+05 +3.294363E+10 +2.171245E+06 +2.478454E+11 +4.602123E+07 +1.190969E+14 +3.884936E+06 +8.360487E+11 +3.405745E+06 +6.871056E+11 +4.778259E+06 +1.368089E+12 +4.374198E+06 +1.117471E+12 +3.974206E+06 +8.501475E+11 +2.411812E+07 +2.928116E+13 +3.691588E+06 +7.339252E+11 +3.820362E+06 +8.680514E+11 +3.691492E+06 +7.170432E+11 +3.796136E+06 +7.551591E+11 +4.074632E+06 +8.975806E+11 +8.125921E+06 +3.353014E+12 +3.176230E+06 +5.742728E+11 +3.757790E+06 +7.535155E+11 +4.115031E+06 +8.998277E+11 +3.914108E+06 +7.994967E+11 +4.154937E+06 +9.090614E+11 +1.468863E+07 +1.108387E+13 +4.261661E+06 +1.221398E+12 +5.131497E+06 +1.479243E+12 +4.169697E+06 +9.408782E+11 +4.247139E+06 +9.734525E+11 +3.723081E+06 +8.021019E+11 +3.284082E+06 +6.716747E+11 +4.209247E+06 +1.033962E+12 +4.319313E+06 +1.026540E+12 +4.494468E+06 +1.068931E+12 +4.215369E+06 +9.851685E+11 +4.378681E+06 +1.054765E+12 +4.718820E+06 +1.455518E+12 +4.447193E+06 +1.021099E+12 +2.294114E+06 +2.729155E+11 +4.377327E+06 +9.788008E+11 +2.081229E+06 +2.307087E+11 +2.935037E+06 +5.750307E+11 +3.558339E+07 +6.512040E+13 +3.718008E+06 +7.878636E+11 +3.225813E+06 +6.619645E+11 +3.154046E+06 +5.675161E+11 +3.142328E+06 +5.655317E+11 +3.396229E+06 +6.808510E+11 +1.389886E+06 +1.101227E+11 +3.835349E+06 +7.871905E+11 +3.311217E+06 +5.752228E+11 +2.914361E+06 +4.489648E+11 +3.354950E+06 +6.075447E+11 +3.044035E+06 +5.047191E+11 +1.616458E+06 +1.372809E+11 +3.838041E+06 +7.978651E+11 +3.284366E+06 +6.000267E+11 +3.415545E+06 +6.273188E+11 +3.664195E+06 +7.879427E+11 +3.587293E+06 +9.920550E+11 +7.555655E+06 +2.958614E+12 +3.136367E+06 +5.698810E+11 +4.425221E+06 +1.090182E+12 +5.349680E+06 +1.601384E+12 +4.571635E+06 +1.116531E+12 +3.480027E+06 +6.946927E+11 +3.410988E+06 +6.135500E+11 +2.956921E+06 +5.456226E+11 +3.849969E+06 +8.590042E+11 +5.848881E+06 +1.842619E+12 +4.992325E+06 +1.491634E+12 +5.217210E+06 +1.450929E+12 +1.159970E+07 +7.155993E+12 +7.849803E+05 +3.676798E+10 +1.486403E+06 +1.171380E+11 +6.162755E+06 +1.936067E+12 +2.581138E+06 +3.504055E+11 +8.777419E+06 +5.124225E+12 +3.867422E+07 +8.940284E+13 +6.252498E+05 +2.797823E+10 +1.386801E+06 +9.832696E+10 +1.219518E+06 +7.524495E+10 +1.377031E+06 +9.892865E+10 +2.572278E+06 +3.346946E+11 +5.503366E+06 +1.544794E+12 +5.508395E+06 +1.568296E+12 +8.937474E+05 +4.416202E+10 +1.315355E+06 +1.010789E+11 +3.927717E+06 +8.017487E+11 +2.631941E+06 +3.604918E+11 +4.973776E+06 +1.320408E+12 +9.384108E+06 +4.487679E+12 +7.096218E+05 +2.913956E+10 +1.105527E+07 +6.197818E+12 +8.303532E+06 +3.557135E+12 +1.393782E+06 +1.059622E+11 +1.252992E+07 +8.452169E+12 +7.598952E+06 +2.951110E+12 +5.548974E+05 +1.868332E+10 +6.795833E+06 +2.499745E+12 +1.276300E+07 +8.359363E+12 +3.473792E+06 +6.366007E+11 +4.292349E+06 +1.051648E+12 +1.040833E+06 +6.564349E+10 +6.971116E+05 +3.020991E+10 +2.218284E+07 +2.522320E+13 +1.296505E+07 +8.891396E+12 +6.897286E+06 +2.768700E+12 +6.825276E+07 +2.402817E+14 +2.800386E+06 +4.295429E+11 +1.784838E+06 +1.678569E+11 +8.425260E+06 +3.647005E+12 +9.344687E+06 +4.541404E+12 +2.352474E+07 +5.039598E+13 +4.715877E+08 +1.133465E+16 From aa64d2810efe08795fa56c8422a3b158090faff6 Mon Sep 17 00:00:00 2001 From: John Tramm Date: Wed, 2 Sep 2026 18:00:56 +0000 Subject: [PATCH 21/35] Update the default-persistence canary for the auto estimator The unit test guarding the finalize-time restoration of the default volume estimator still asserted the pre-auto report string, so it fails on the current branch (the report now reads "Adaptive (auto)"). Beyond the string, the auto default changes what the test must do to detect a leak: auto is resolved by overwriting the stored setting at the start of each run, and a forward rerun re-resolves to the same estimator whether or not finalize restored the default, so running the same forward model twice can no longer distinguish a reset from stale state. Run an adjoint solve first (which resolves the setting to the strict adaptive estimator) and a forward solve second: if finalize fails to restore "auto", the forward run inherits the strict estimator and the reported type exposes it. This is also the sequence real openmc.lib workflows use (an adjoint weight-window generation run followed by forward runs in the same process). Co-Authored-By: Claude Fable 5 --- .../test_random_ray_default_persistence.py | 30 ++++++++++++------- 1 file changed, 19 insertions(+), 11 deletions(-) diff --git a/tests/unit_tests/test_random_ray_default_persistence.py b/tests/unit_tests/test_random_ray_default_persistence.py index d59cfcb6a2d..8e75fa943bf 100644 --- a/tests/unit_tests/test_random_ray_default_persistence.py +++ b/tests/unit_tests/test_random_ray_default_persistence.py @@ -1,10 +1,14 @@ """The random ray volume-estimator default must survive an in-process finalize/re-initialize cycle (openmc.lib workflows such as iterative weight -window generation). openmc_finalize_random_ray() restores the built-in -defaults between runs; if it restores a different estimator than the static -default, the first and subsequent runs of a process silently use different -estimators. This test runs the same model twice through openmc.lib in one -process and checks the reported estimator both times.""" +window generation). The default is "auto", which is resolved to a concrete +estimator at the start of each run by overwriting the stored setting, so +openmc_finalize_random_ray() must restore "auto" between runs; if it leaves +the resolved value behind, later runs of the process inherit the previous +run's estimator instead of re-resolving. An adjoint run resolves to the +strict adaptive estimator, so running adjoint first and forward second makes +any leak visible: the forward run would report the strict estimator instead +of re-resolving to the adaptive one. This test runs that sequence through +openmc.lib in one process and checks the reported estimator each solve.""" import openmc import openmc.lib @@ -17,11 +21,12 @@ def test_random_ray_default_estimator_persistence(run_in_tmpdir, capfd): model.settings.particles = 10 model.settings.inactive = 2 model.settings.batches = 4 - # No volume_estimator set: exercises the built-in default both runs - model.export_to_model_xml() + # No volume_estimator set: both runs exercise the built-in default reported = [] - for _ in range(2): + for adjoint in (True, False): + model.settings.random_ray['adjoint'] = adjoint + model.export_to_model_xml() openmc.lib.init() openmc.lib.run_random_ray() openmc.lib.finalize() @@ -30,6 +35,9 @@ def test_random_ray_default_estimator_persistence(run_in_tmpdir, capfd): if 'Volume Estimator Type' in line: reported.append(line.split('=')[-1].strip()) - assert reported == ['Adaptive', 'Adaptive'], ( - f"default volume estimator changed across in-process reruns: " - f"{reported}") + # The adjoint run reports once per solve (forward-for-adjoint, adjoint); + # the forward run reports once. + assert reported == ['Strict Adaptive (auto)', 'Strict Adaptive (auto)', + 'Adaptive (auto)'], ( + f"default volume estimator did not re-resolve across in-process " + f"reruns: {reported}") From 8341c6d2213ee974f1e58df2924532fd612b4d80 Mon Sep 17 00:00:00 2001 From: John Tramm Date: Wed, 2 Sep 2026 18:13:50 +0000 Subject: [PATCH 22/35] Resolve the auto volume estimator without modifying the setting The auto default was resolved by overwriting the configured volume_estimator_ static with the concrete estimator at the start of the run, which made the solver a second writer of a user setting: if openmc_finalize_random_ray() failed to restore the default, a later in-process run inherited the previous run's resolved estimator (an adjoint weight-window generation run would leak the strict adaptive estimator into subsequent forward runs of the same process). Resolve into a separate resolved_volume_estimator_ instead, assigned unconditionally at the start of every random ray solve, and point all solver code at it. The configured setting is now never modified by the solver, so the resolved value cannot go stale by construction, and the "(auto)" report suffix follows directly from the configured value, replacing the volume_estimator_is_auto_ flag. The finalize-time restore of the configured setting remains, as for any settings static (XML parsing only assigns it when the element is present), and the persistence unit test now pins that surviving hazard: an explicit estimator run followed by a default adjoint run in one process, which also exercises the auto routing under openmc.lib. No behavior changes; all reference results are unchanged. Co-Authored-By: Claude Fable 5 --- .../openmc/random_ray/flat_source_domain.h | 11 +++-- src/random_ray/flat_source_domain.cpp | 13 +++--- src/random_ray/linear_source_domain.cpp | 3 +- src/random_ray/random_ray_simulation.cpp | 30 +++++++------ src/settings.cpp | 2 - .../test_random_ray_default_persistence.py | 43 ++++++++++--------- 6 files changed, 56 insertions(+), 46 deletions(-) diff --git a/include/openmc/random_ray/flat_source_domain.h b/include/openmc/random_ray/flat_source_domain.h index c84a82098cb..3f7851201b3 100644 --- a/include/openmc/random_ray/flat_source_domain.h +++ b/include/openmc/random_ray/flat_source_domain.h @@ -104,11 +104,14 @@ class FlatSourceDomain { //---------------------------------------------------------------------------- // Static data members + // The volume estimator as configured ("auto" by default); set when the + // settings are read and never modified by the solver. static RandomRayVolumeEstimator volume_estimator_; - // True when the user selected (or defaulted to) the "auto" volume - // estimator, which is resolved to a concrete estimator at the start of the - // random ray solve based on the type of simulation being performed. - static bool volume_estimator_is_auto_; + // The concrete estimator the solver runs with, assigned unconditionally at + // the start of every random ray solve: the configured value, or for "auto" + // the estimator selected for the type of simulation being performed. All + // solver code reads this member, never volume_estimator_. + static RandomRayVolumeEstimator resolved_volume_estimator_; //---------------------------------------------------------------------------- // Public Data members diff --git a/src/random_ray/flat_source_domain.cpp b/src/random_ray/flat_source_domain.cpp index d317a9b3cd3..4759a0439cd 100644 --- a/src/random_ray/flat_source_domain.cpp +++ b/src/random_ray/flat_source_domain.cpp @@ -29,7 +29,8 @@ namespace openmc { // Static Variable Declarations RandomRayVolumeEstimator FlatSourceDomain::volume_estimator_ { RandomRayVolumeEstimator::AUTO}; -bool FlatSourceDomain::volume_estimator_is_auto_ {true}; +RandomRayVolumeEstimator FlatSourceDomain::resolved_volume_estimator_ { + RandomRayVolumeEstimator::AUTO}; bool FlatSourceDomain::volume_normalized_flux_tallies_ {false}; bool FlatSourceDomain::adjoint_requested_ {false}; RandomRaySolve FlatSourceDomain::solve_ {RandomRaySolve::FORWARD}; @@ -57,7 +58,7 @@ FlatSourceDomain::FlatSourceDomain() : negroups_(data::mg.num_energy_groups_) // Initialize source regions. bool is_linear = RandomRay::source_shape_ != RandomRaySourceShape::FLAT; - bool is_adaptive = is_adaptive_family(volume_estimator_); + bool is_adaptive = is_adaptive_family(resolved_volume_estimator_); source_regions_ = SourceRegionContainer(negroups_, is_linear, is_adaptive); // Initialize tally volumes @@ -156,7 +157,7 @@ void FlatSourceDomain::accumulate_iteration_flux() // add_source_to_scalar_flux and by the linear-source gradient fallback. void FlatSourceDomain::demotion_step() { - if (!is_adaptive_family(volume_estimator_)) + if (!is_adaptive_family(resolved_volume_estimator_)) return; #pragma omp parallel for @@ -421,11 +422,11 @@ int64_t FlatSourceDomain::add_source_to_scalar_flux() // demote-to-naive volume switch, and the previous-flux miss treatment, with // demote-only decisions made from the running accumulated flux (recorded // as a flag in converged_negative by demotion_step). - const bool is_adaptive = is_adaptive_family(volume_estimator_); + const bool is_adaptive = is_adaptive_family(resolved_volume_estimator_); // The strict adaptive estimator additionally enforces non-negativity on // the flux iterates each batch (see the enforcement step below). const bool is_strict = - volume_estimator_ == RandomRayVolumeEstimator::STRICT_ADAPTIVE; + resolved_volume_estimator_ == RandomRayVolumeEstimator::STRICT_ADAPTIVE; int64_t n_rescued = 0; int64_t n_floored = 0; int64_t n_chronic = 0; @@ -524,7 +525,7 @@ int64_t FlatSourceDomain::add_source_to_scalar_flux() // agnostic. bool use_naive_volume = false; bool use_old_flux_on_miss = external; - switch (volume_estimator_) { + switch (resolved_volume_estimator_) { case RandomRayVolumeEstimator::NAIVE: use_naive_volume = true; break; diff --git a/src/random_ray/linear_source_domain.cpp b/src/random_ray/linear_source_domain.cpp index 31610eef5c3..0343fdd1c21 100644 --- a/src/random_ray/linear_source_domain.cpp +++ b/src/random_ray/linear_source_domain.cpp @@ -125,7 +125,8 @@ void LinearSourceDomain::update_single_neutron_source(SourceRegionHandle& srh) // negative accumulated flux means the fitted gradients carry no meaningful // shape information, and a latched strong feed is the gradient-scale noise // hazard the strong-source fallback above exists for. - if (is_adaptive_family(volume_estimator_) && material != MATERIAL_VOID && + if (is_adaptive_family(resolved_volume_estimator_) && + material != MATERIAL_VOID && (srh.converged_negative() > 0 || region_has_strong_source(&srh.source(0), &srh.scalar_flux_old(0), simulation::current_batch <= settings::n_inactive))) { diff --git a/src/random_ray/random_ray_simulation.cpp b/src/random_ray/random_ray_simulation.cpp index 1108f8d4763..4f5157f202f 100644 --- a/src/random_ray/random_ray_simulation.cpp +++ b/src/random_ray/random_ray_simulation.cpp @@ -288,7 +288,7 @@ void validate_random_ray_inputs() void openmc_finalize_random_ray() { FlatSourceDomain::volume_estimator_ = RandomRayVolumeEstimator::AUTO; - FlatSourceDomain::volume_estimator_is_auto_ = true; + FlatSourceDomain::resolved_volume_estimator_ = RandomRayVolumeEstimator::AUTO; FlatSourceDomain::volume_normalized_flux_tallies_ = false; FlatSourceDomain::adjoint_requested_ = false; FlatSourceDomain::solve_ = RandomRaySolve::FORWARD; @@ -605,7 +605,7 @@ void RandomRaySimulation::print_results_random_ray( total_integrations / settings::n_batches); std::string estimator; - switch (domain_->volume_estimator_) { + switch (FlatSourceDomain::resolved_volume_estimator_) { case RandomRayVolumeEstimator::SIMULATION_AVERAGED: estimator = "Simulation Averaged"; break; @@ -624,7 +624,7 @@ void RandomRaySimulation::print_results_random_ray( default: fatal_error("Invalid volume estimator type"); } - if (FlatSourceDomain::volume_estimator_is_auto_) { + if (FlatSourceDomain::volume_estimator_ == RandomRayVolumeEstimator::AUTO) { estimator += " (auto)"; } fmt::print(" Volume Estimator Type = {}\n", estimator); @@ -656,7 +656,7 @@ void RandomRaySimulation::print_results_random_ray( // enforcement, reported for the final batch. These overlap the // partition above rather than extending it: a rescued or floored // region may or may not also carry the naive treatment. - if (FlatSourceDomain::volume_estimator_ == + if (FlatSourceDomain::resolved_volume_estimator_ == RandomRayVolumeEstimator::STRICT_ADAPTIVE) { fmt::print(" Chronic negative (per batch) = {} SRs ({:.4f}%)\n", domain_->n_final_chronic_, domain_->n_final_chronic_ * inv); @@ -743,21 +743,25 @@ void openmc_run_random_ray() { using namespace openmc; - // Resolve the "auto" volume estimator (the default) to a concrete - // estimator based on the type of simulation being performed. Solves whose - // results feed variance reduction -- weight window generation, and any - // adjoint workflow, including the forward solve an adjoint source is - // derived from -- receive the strict adaptive estimator, whose guaranteed - // non-negative fluxes those workflows require. All other solves receive - // the adaptive estimator, which preserves unbiasedness at the cost of - // allowing rare noise-driven negative tallies in near-zero-flux regions. + // Resolve the volume estimator for this solve, leaving the configured + // setting untouched. "Auto" (the default) maps to a concrete estimator + // based on the type of simulation being performed: solves whose results + // feed variance reduction -- weight window generation, and any adjoint + // workflow, including the forward solve an adjoint source is derived from + // -- receive the strict adaptive estimator, whose guaranteed non-negative + // fluxes those workflows require, while all other solves receive the + // adaptive estimator, which preserves unbiasedness at the cost of allowing + // rare noise-driven negative tallies in near-zero-flux regions. if (FlatSourceDomain::volume_estimator_ == RandomRayVolumeEstimator::AUTO) { bool positivity_needed = FlatSourceDomain::adjoint_requested_ || !variance_reduction::weight_windows_generators.empty(); - FlatSourceDomain::volume_estimator_ = + FlatSourceDomain::resolved_volume_estimator_ = positivity_needed ? RandomRayVolumeEstimator::STRICT_ADAPTIVE : RandomRayVolumeEstimator::ADAPTIVE; + } else { + FlatSourceDomain::resolved_volume_estimator_ = + FlatSourceDomain::volume_estimator_; } // Determine which solves to run. If adjoint results are requested and no diff --git a/src/settings.cpp b/src/settings.cpp index 8bdfda694bf..9b4b1bfd455 100644 --- a/src/settings.cpp +++ b/src/settings.cpp @@ -311,8 +311,6 @@ void get_run_parameters(pugi::xml_node node_base) } else { fatal_error("Unrecognized volume estimator: " + temp_str); } - FlatSourceDomain::volume_estimator_is_auto_ = - FlatSourceDomain::volume_estimator_ == RandomRayVolumeEstimator::AUTO; } if (check_for_node(random_ray_node, "source_shape")) { std::string temp_str = diff --git a/tests/unit_tests/test_random_ray_default_persistence.py b/tests/unit_tests/test_random_ray_default_persistence.py index 8e75fa943bf..d5109e6b2b0 100644 --- a/tests/unit_tests/test_random_ray_default_persistence.py +++ b/tests/unit_tests/test_random_ray_default_persistence.py @@ -1,14 +1,15 @@ -"""The random ray volume-estimator default must survive an in-process -finalize/re-initialize cycle (openmc.lib workflows such as iterative weight -window generation). The default is "auto", which is resolved to a concrete -estimator at the start of each run by overwriting the stored setting, so -openmc_finalize_random_ray() must restore "auto" between runs; if it leaves -the resolved value behind, later runs of the process inherit the previous -run's estimator instead of re-resolving. An adjoint run resolves to the -strict adaptive estimator, so running adjoint first and forward second makes -any leak visible: the forward run would report the strict estimator instead -of re-resolving to the adaptive one. This test runs that sequence through -openmc.lib in one process and checks the reported estimator each solve.""" +"""The random ray volume-estimator setting must not leak across an +in-process finalize/re-initialize cycle (openmc.lib workflows such as +iterative weight window generation). The solver never modifies the +configured setting -- "auto" is resolved into a separate run-scoped value -- +but the configured setting is a static that XML parsing only assigns when +the element is present, so openmc_finalize_random_ray() must restore the +"auto" default between runs. Running an explicit estimator first and a +default model second makes a missed restore visible: the second run would +report the first run's estimator instead of resolving "auto". The default +second run is an adjoint solve, which also pins the auto routing under +openmc.lib (it must resolve to the strict adaptive estimator, once per +solve).""" import openmc import openmc.lib @@ -21,11 +22,14 @@ def test_random_ray_default_estimator_persistence(run_in_tmpdir, capfd): model.settings.particles = 10 model.settings.inactive = 2 model.settings.batches = 4 - # No volume_estimator set: both runs exercise the built-in default reported = [] - for adjoint in (True, False): - model.settings.random_ray['adjoint'] = adjoint + for explicit in (True, False): + if explicit: + model.settings.random_ray['volume_estimator'] = 'naive' + else: + del model.settings.random_ray['volume_estimator'] + model.settings.random_ray['adjoint'] = True model.export_to_model_xml() openmc.lib.init() openmc.lib.run_random_ray() @@ -35,9 +39,8 @@ def test_random_ray_default_estimator_persistence(run_in_tmpdir, capfd): if 'Volume Estimator Type' in line: reported.append(line.split('=')[-1].strip()) - # The adjoint run reports once per solve (forward-for-adjoint, adjoint); - # the forward run reports once. - assert reported == ['Strict Adaptive (auto)', 'Strict Adaptive (auto)', - 'Adaptive (auto)'], ( - f"default volume estimator did not re-resolve across in-process " - f"reruns: {reported}") + # The forward run reports once; the adjoint run reports once per solve + # (forward-for-adjoint, adjoint). + assert reported == ['Naive', 'Strict Adaptive (auto)', + 'Strict Adaptive (auto)'], ( + f"volume estimator leaked across in-process reruns: {reported}") From 564770706d3867f575fcdef1890f0c0da74e63b9 Mon Sep 17 00:00:00 2001 From: John Tramm Date: Wed, 2 Sep 2026 19:31:30 +0000 Subject: [PATCH 23/35] Tighten the volume estimator documentation Compress the adaptive and strict adaptive documentation to match the register of the surrounding text: the user's guide estimator table rows are cut to glanceable descriptions in line with the existing rows, the default-selection paragraphs and the methods discussion are shortened to their load-bearing content, and the settings references state the options and the automatic selection without re-explaining the estimators. Also drop the claims that the strict adaptive estimator guarantees non-negative fluxes -- it enforces a per-batch fixup on the flat flux iterates, which is not a theoretical guarantee on all outputs (under a linear source shape the reconstructed in-region flux can still locally dip below zero) -- and confine the discussion of negative fluxes to the mechanism explanations, framing them as arising in pathological cases rather than as a routine occurrence. Co-Authored-By: Claude Fable 5 Claude-Session: https://claude.ai/code/session_01P9rxXRzNh13xQBGzePHEeK --- docs/source/io_formats/settings.rst | 6 +- docs/source/methods/random_ray.rst | 165 ++++++++--------------- docs/source/usersguide/random_ray.rst | 118 ++++++---------- openmc/settings.py | 16 +-- src/random_ray/flat_source_domain.cpp | 20 ++- src/random_ray/random_ray_simulation.cpp | 7 +- 6 files changed, 110 insertions(+), 222 deletions(-) diff --git a/docs/source/io_formats/settings.rst b/docs/source/io_formats/settings.rst index cdefbc985c5..ec86d81c6eb 100644 --- a/docs/source/io_formats/settings.rst +++ b/docs/source/io_formats/settings.rst @@ -628,9 +628,9 @@ found in the :ref:`random ray user guide `. Specifies choice of volume estimator for the random ray solver. Options are 'naive', 'simulation_averaged', 'hybrid', 'adaptive', 'strict_adaptive', or 'auto'. The default is 'auto', which selects - 'adaptive' for standard solves and 'strict_adaptive' (which guarantees - non-negative fluxes) for solves whose results feed variance reduction: - weight window generation, and any adjoint workflow. + 'adaptive' for standard solves and 'strict_adaptive' for solves whose + results feed variance reduction (weight window generation and adjoint + workflows). *Default*: None diff --git a/docs/source/methods/random_ray.rst b/docs/source/methods/random_ray.rst index a89b797f9f2..d912a8129b3 100644 --- a/docs/source/methods/random_ray.rst +++ b/docs/source/methods/random_ray.rst @@ -525,120 +525,61 @@ preferable. OpenMC also features an "adaptive" volume estimator that generalizes the hybrid estimator. Rather than selecting the estimator from the presence of an -external source alone, it uses the simulation averaged estimator by default and -falls back to the naive estimator (and the previous-iteration miss treatment) -on a per-region basis wherever the simulation averaged estimator is prone to -instability. The fallback is triggered by any of the following: a reduced -source that greatly exceeds the region's scalar flux (a source sustained by an -external or in-scatter contribution rather than by the local flux), a reduced -source that is itself negative (which can occur under transport-corrected cross -sections, whose negative within-group scattering term can drive the reduced -source below zero even for a non-negative flux), a hit-starved region, a -region whose flux converges to a negative value, and a region whose converged -flux-independent source ("feed") is strong relative to its own converged flux. - -The negative-source and hit-starved conditions are evaluated each iteration -from already-resident data. The strong-source ratio condition is evaluated -each iteration as well, but only while the source is still converging (the -inactive batches): applied to noisy single-iteration values in the active -batches it would demote a churning population of regions whose converged -ratios are below the threshold, and conditioning the estimator choice on -per-iteration noise in the tallied batches introduces a systematic bias. The -last two conditions are instead decided from each region's accumulated flux: -the unmodified simulation averaged estimator is run while every batch's flux -is accumulated into a running sum, and -- beginning at the transition from -the inactive to the active batches, and re-evaluated every active batch as -the accumulation keeps growing -- a region is demoted to the naive estimator -if its accumulated (and therefore noise-averaged) flux is negative in any -group, or if its flux-independent feed -- the part of its source arising -from cross-group in-scatter, fission, and any external source, evaluated -from the same accumulated flux -- exceeds the strong-source threshold times -its own accumulated flux in any group. These decisions are demote-only: once -a region is demoted it is never returned to the simulation averaged -estimator, so the estimator choice cannot churn with active-batch noise, and -a marginal region whose accumulated ratio converges below the threshold is -never eroded into demotion by the continued re-evaluation. The active-phase -re-evaluation matters for problems whose inactive phase is too short to -converge deep regions: there the transition-time decision alone can miss -chronically unstable regions whose accumulated flux only turns negative (or -whose feed ratio only crosses the threshold) after active batches begin, and -a single such region left on unprotected simulation averaged updates can -corrupt the solution well beyond its own boundary through scattering -feedback. In the active batches these accumulated-flux decisions, together -with the per-iteration negative-source and hit-starved conditions, are what -govern the estimator choice. Basing the noise-sensitive decisions on the -accumulated estimate -- rather than reacting to individual per-iteration -values -- avoids the bias that demoting on isolated fluctuations would -introduce by treating only one tail of the estimator's noise distribution; -regions that are merely noisy but average non-negative (and are not strongly -fed) retain the unbiased simulation averaged estimator. - -The feed-based latch exists because the per-iteration strong-source test, -evaluated on noisy single-iteration values, has exactly one blind state: an -unlucky iteration can drag a strongly fed region's source and flux negative -together, and in that state neither the ratio condition nor the -negative-source condition can fire. Such a region would ride out the -excursion on unprotected simulation averaged updates, and -- because for -these regions the per-iteration noise scale is set by the reduced source -rather than by the flux -- the average over a whole phase of active batches -can land slightly negative. The latch identifies the entire strongly fed -class from accumulated data that individual fluctuations cannot flip, and -removes it from the simulation averaged estimator. A region with no -cross-group or external feed can never latch, so the -estimator choice never reacts to noise whose sign is locked to the region's -own flux (as in one-group media, where the source is proportional to the -local flux). Non-negativity is still not strictly enforced on individual -active iterations; in variance reduction workflows any residual non-positive -tally values are discarded by the weight-window generator. - -Whereas the hybrid estimator guards only regions with explicit external -sources, the adaptive estimator also catches the optically thin regions of -fixed source problems where the simulation averaged and hybrid estimators can -otherwise develop persistent negative fluxes. When a linear source shape is in -use, demoted regions additionally revert to a flat source representation -(their source gradients are zeroed), extending the flat-source treatment -already applied to hit-starved regions: in a strong-source or latched region -the gradient terms attenuate segments against the local rather than the flat -source, re-injecting per-iteration noise at the scale of the reduced source -that the volume choice cannot cancel, while in a converged-negative region -the fitted gradients carry no meaningful shape information. The adaptive -estimator is particularly beneficial for fixed source and shielding problems -that exhibit such instability. +external source alone, it uses the simulation averaged estimator by default +and falls back to the naive estimator (and the previous-iteration miss +treatment) on a per-region basis wherever the simulation averaged estimator +is prone to instability: regions that are hit-starved, regions whose reduced +source is negative (possible under transport-corrected cross sections) or -- +while the source is still converging -- greatly exceeds their scalar flux (a +source sustained by external or in-scatter contributions rather than by the +local flux), and, decided from each region's running accumulated flux from +the end of the inactive batches onward, regions whose accumulated flux is +negative or whose flux-independent "feed" (cross-group in-scatter, fission, +and any external source) is strong relative to their own accumulated flux. + +The accumulated-flux decisions are demote-only -- once demoted, a region is +never returned to the simulation averaged estimator -- so the estimator +choice in the tallied batches cannot churn with single-batch noise, and +basing them on accumulated rather than per-iteration values avoids the bias +that reacting to one tail of the noise distribution would introduce. The +feed-based condition exists because strongly fed regions are the one class +whose per-iteration noise scale is set by the reduced source rather than by +the flux, which the per-iteration conditions cannot reliably identify; +regions with no cross-group or external feed can never trigger it. +Re-evaluating the decisions through the active phase catches regions whose +instability only becomes visible after tallies begin, as on large problems +run with short inactive phases. When a linear source shape is in use, +demoted regions additionally revert to a flat source representation (their +source gradients are zeroed), extending the flat-source treatment already +applied to hit-starved regions. Compared to the hybrid estimator, the +adaptive estimator notably also stabilizes the optically thin, +scatter-dominated regions of fixed source problems, making it particularly +beneficial for shielding analysis. The adaptive estimator's demotion machinery selects estimators; it never -modifies a computed flux value, which is what preserves its unbiasedness -- -and also why it cannot guarantee non-negativity: in near-zero-flux regions -the simulation averaged noise is sign-indefinite at stationarity, and a -region can also inherit negativity through in-scatter from neighbors that -have not (yet) been demoted, so no demotion criterion alone closes the gap. -The "strict adaptive" estimator therefore runs the same machinery and adds a -per-batch enforcement on the flux iterates. A group whose batch flux comes -out negative is first *rescued*: its transport term is rescaled from the -volume used to the batch's own volume, algebraically reproducing the naive -(iteration) volume update, whose consistency removes the volume-mismatch -noise that produced most negative excursions. If the flux remains negative -(or the region already used the batch volume), it is *floored* at the -previous iterate, which is non-negative by induction from a non-negative -initial condition -- so strict adaptive fluxes are guaranteed non-negative. -Because the floor prevents a chronically noisy region's accumulated flux -from ever going negative -- masking the very signal the accumulated sign -demotion detects -- a region whose flux goes negative (before enforcement) -in more than a few batches is demoted outright to the naive volume and -previous-flux miss treatment, where clipping is no longer needed; without -this chronic-negativity channel, repeated one-sided clipping would bias the -affected regions upward. The residual cost of the enforcement is a small -conservative bias (several hundred pcm on typical eigenvalue problems), -which is why the strict estimator is reserved for solves that require -positivity rather than used as the standard default. - -By default OpenMC selects the volume estimator automatically ("auto"): -solves whose results feed variance reduction -- weight window generation, -and any adjoint workflow, including the forward solve an adjoint source is -derived from -- receive the strict adaptive estimator, since a small -population of negative fluxes would otherwise contaminate the adjoint -source and degrade the generated weight windows, while all other solves -receive the adaptive estimator, preserving unbiased results where accuracy -is the priority. +modifies a computed flux value, which preserves its unbiasedness but means +that in pathological cases a small number of flux estimates in near-void +regions can still land slightly negative. This is generally harmless, but +solves whose results feed variance reduction benefit from suppressing it, +as the adjoint source is constructed from the forward flux. The "strict +adaptive" estimator therefore runs the same machinery and adds a per-batch +fixup: a group whose batch flux comes out negative is first recomputed with +the batch's own volume (algebraically, the naive volume update, whose +consistency removes the volume-mismatch noise responsible for most negative +excursions) and floored at the previous iterate if still negative. A region +requiring the fixup in more than a few batches is demoted outright to the +naive treatment: the floor masks the accumulated-flux sign signal the +adaptive demotion relies on, and without this chronic channel the repeated +one-sided fixup would bias the affected regions upward. The residual cost is +a small conservative bias (several hundred pcm on typical eigenvalue +problems), which is why the strict estimator is reserved for variance +reduction solves rather than used as the standard default. + +By default, OpenMC selects the volume estimator automatically ("auto"): +solves whose results feed variance reduction -- weight window generation and +adjoint workflows, including the forward solve an adjoint source is derived +from -- receive the strict adaptive estimator, while all other solves +receive the adaptive estimator. A table that summarizes the pros and cons, as well as recommendations for different use cases, is given in the :ref:`volume diff --git a/docs/source/usersguide/random_ray.rst b/docs/source/usersguide/random_ray.rst index 4713fceec32..7d016350a80 100644 --- a/docs/source/usersguide/random_ray.rst +++ b/docs/source/usersguide/random_ray.rst @@ -1059,63 +1059,34 @@ following methods are currently available in OpenMC: - * Can lead to slightly negative fluxes in cells where the simulation averaged estimator is used * - ``adaptive`` - - Generalizes the hybrid estimator. Uses the simulation averaged estimator - by default, but falls back to the naive estimator (and the - previous-iteration miss treatment) wherever it is needed for stability: - during the inactive batches, cells whose reduced source greatly exceeds - their flux (a strong external or in-scatter source); in every batch, - cells whose reduced source is itself negative (possible under - transport-corrected cross sections) and hit-starved cells; and -- - decided from each cell's running accumulated flux, first at the end of - the inactive phase and re-evaluated every active batch -- cells whose - accumulated flux is negative as well as cells whose flux-independent - feed (cross-group in-scatter, fission, and external source) is strong - relative to their own accumulated flux. The accumulated-flux decisions - are demote-only: a demoted cell stays on the naive estimator for the - rest of the solve, so the estimator choice in the tallied batches never - churns with single-batch noise. The decisions are made automatically - from each cell's accumulated statistics; individual iterations are - never modified. - - * Retains the low bias of the simulation averaged estimator wherever it - is well behaved - * Eliminates the negative-flux instabilities that the simulation averaged - and hybrid estimators can exhibit in optically thin, in-scatter-fed - fixed source problems - * The accumulated-feed latch removes the strongly fed cell population - whose phase-averaged flux could otherwise straddle zero, eliminating - the negative tally bins that class otherwise produces + - Generalizes the hybrid estimator: uses the simulation averaged + estimator by default, but automatically (and permanently) demotes + individual cells to the naive treatment when their accumulated + statistics indicate the simulation averaged estimator is unstable + there (e.g., cells dominated by external or in-scatter sources, and + hit-starved cells). + - * Accuracy of the simulation averaged estimator in most cells + * Stable in cases where the simulation averaged and hybrid estimators + are not * No parameters to tune - - * Does not strictly guarantee non-negative fluxes on individual - active iterations (any residual non-positive tally values are - discarded downstream by the weight-window generator) - * Benefits from inactive batches to season the accumulated-flux - decisions before tallies begin + - * Benefits from a longer inactive phase to inform the demotion + decisions * - ``strict_adaptive`` - - As ``adaptive``, but additionally enforces non-negativity on the flux - iterates every batch: a cell whose batch flux comes out negative is - first recomputed with the batch's own volume, floored at the previous - iterate if still negative, and demoted outright to the naive - treatment if its flux goes negative chronically. Because the previous - iterate is non-negative by induction from a non-negative start, the - resulting fluxes are guaranteed non-negative everywhere. - - * Guarantees non-negative fluxes -- the property required by weight - window generation and adjoint workflows, where a small population - of noise-driven negative fluxes would otherwise contaminate the - adjoint source and degrade weight window quality - * Matches ``adaptive``'s accuracy in stable fixed source problems and - degrades far more gracefully than ``naive`` at coarse ray densities - - * The one-sided enforcement introduces a small conservative bias - (several hundred pcm on eigenvalue problems), so it should not be - used where unbiased results are the priority + - As ``adaptive``, but additionally applies a per-batch fixup to any + negative flux estimate (recomputing it with the batch's own volume, + then falling back on the previous iterate) and demotes chronically + affected cells to the naive treatment. + - * Suppresses the negative flux estimates other estimators can produce + in pathological cases + * Improves the quality of generated weight windows + - * The one-sided fixup introduces a small conservative bias, so it is + not recommended where unbiased results are the priority By default, the ``volume_estimator`` field is set to ``auto``, which selects -the appropriate estimator for the type of simulation being performed: -``strict_adaptive`` for solves whose results feed variance reduction -- -weight window generation, and any adjoint workflow, including the forward -solve an adjoint source is derived from -- and ``adaptive`` for all other -solves. The end-of-simulation output reports which estimator ``auto`` -resolved to. Explicitly setting any other value overrides the automatic -selection. +``strict_adaptive`` for solves whose results feed variance reduction (weight +window generation and adjoint workflows) and ``adaptive`` for all other +solves. The end-of-simulation output reports which estimator was selected, +and explicitly setting any other value overrides the automatic selection. These estimators can be selected by setting the ``volume_estimator`` field in the :attr:`openmc.Settings.random_ray` dictionary. For example, to use the naive @@ -1126,33 +1097,20 @@ estimator, the following code would be used: settings.random_ray['volume_estimator'] = 'naive' The ``auto`` setting is the default, as it gives reliable behavior out of -the box across problem types. It is especially valuable for fixed source and -shielding problems, where the ``hybrid`` and ``simulation_averaged`` estimators -can otherwise produce negative fluxes or numerical instability. This commonly occurs in optically thin, -scattering- or streaming-dominated regions (for example, the air- or -void-filled regions of a shielding model), where a small number of cells can -develop persistent negative fluxes that degrade tally results and, in -variance reduction workflows, the quality of generated weight windows. The -adaptive estimator detects and stabilizes those cells automatically while -leaving the rest of the problem on the low-bias simulation averaged estimator. -Because the negative-flux and strong-feed demotions are decided from each -cell's running accumulated flux -- first at the end of the inactive phase and -re-evaluated (demote-only) every active batch -- rather than from individual -per-iteration values, they avoid the small upward bias that per-iteration -demotion can introduce in cells that are noisy but not genuinely negative, -while still removing -- via the strong-feed latch -- the strongly fed cell -class whose phase-averaged flux could otherwise straddle zero, and still -catching cells whose instability only becomes visible after the inactive -phase ends (as on large problems run with short inactive phases). The -adaptive estimator does not strictly enforce non-negativity, however: in -near-zero-flux regions its sampling noise is sign-indefinite, so over a -finite number of active batches a small population of tally bins can land -negative. That residue is harmless for standard tallies but contaminates -variance reduction workflows, where the adjoint source is built from the -forward flux and amplifies it -- which is why ``auto`` routes weight window -generation and adjoint solves to ``strict_adaptive`` instead, whose -per-batch enforcement guarantees non-negative fluxes at the cost of a small -conservative bias. +the box across problem types. The adaptive estimator is especially valuable +for fixed source and shielding problems, where optically thin, scattering- +or streaming-dominated regions (for example, the air- or void-filled regions +of a shielding model) can destabilize the ``hybrid`` and +``simulation_averaged`` estimators: it detects and stabilizes the affected +cells automatically while leaving the rest of the problem on the low-bias +simulation averaged estimator. Because demotions are decided from each +cell's accumulated statistics rather than from single-iteration values, the +estimator choice does not churn with iteration noise and avoids the bias +that per-iteration selection can introduce. Solves that feed variance +reduction are routed to ``strict_adaptive`` instead, as even a small number +of slightly negative flux estimates in the near-void regions of pathological +problems can otherwise degrade the adjoint solve and the quality of +generated weight windows. ----------------- Adjoint Flux Mode diff --git a/openmc/settings.py b/openmc/settings.py index 59b6a572b5d..6cbb23acfb7 100644 --- a/openmc/settings.py +++ b/openmc/settings.py @@ -203,18 +203,10 @@ class Settings: :volume_estimator: Choice of volume estimator for the random ray solver. Options are 'naive', 'simulation_averaged', 'hybrid', 'adaptive', - 'strict_adaptive', or 'auto'. The default is 'auto', which selects - 'adaptive' for standard solves and 'strict_adaptive' for solves - whose results feed variance reduction (weight window generation - and any adjoint workflow). The 'adaptive' estimator generalizes - 'hybrid': it uses the simulation-averaged volume by default but - falls back to the naive (iteration) volume in individual regions - where that estimator is unsafe, removing the negative-flux - instabilities 'hybrid' can exhibit in optically thin, - in-scatter-fed regions. The 'strict_adaptive' estimator runs the - same machinery and additionally enforces non-negativity on the - flux iterates every batch, guaranteeing non-negative fluxes at - the cost of a small conservative bias. + 'strict_adaptive', or 'auto'. The default is 'auto', which + selects 'adaptive' for standard solves and 'strict_adaptive' for + solves whose results feed variance reduction (weight window + generation and adjoint workflows). :source_shape: Assumed shape of the source distribution within each source region. Options are 'flat' (default), 'linear', or 'linear_xy'. diff --git a/src/random_ray/flat_source_domain.cpp b/src/random_ray/flat_source_domain.cpp index 4759a0439cd..2cd63136670 100644 --- a/src/random_ray/flat_source_domain.cpp +++ b/src/random_ray/flat_source_domain.cpp @@ -573,17 +573,15 @@ int64_t FlatSourceDomain::add_source_to_scalar_flux() // this iteration's transport contribution, normalized by the chosen // volume. set_flux_to_flux_plus_source(sr, volume, g); - // The strict adaptive estimator enforces non-negativity on the flux - // iterates: a group whose batch flux comes out negative is first - // rescued -- its transport term rescaled from the volume used to the - // batch's own volume, algebraically reproducing the naive-volume - // update, whose noise the near-cancellation regions require -- and, - // if still negative (or if the region already used the batch - // volume), floored at the previous iterate, which is non-negative by - // induction. This is what upgrades the family's demotion machinery - // into a guarantee: demotion alone cannot prevent a region from - // inheriting negativity through in-scatter from not-yet-demoted - // neighbors. The price is a small conservative (positivity-clip) + // The strict adaptive estimator applies a per-batch fixup to + // negative flux iterates: first a rescue -- the transport term + // rescaled from the volume used to the batch's own volume, + // algebraically reproducing the naive-volume update -- and, if still + // negative (or if the region already used the batch volume), a floor + // at the previous iterate. A value-level fixup is needed here + // because demotion alone cannot prevent a region from inheriting a + // negative excursion through in-scatter from not-yet-demoted + // neighbors. The price is a small conservative (one-sided clip) // bias, which is why the strict estimator is not the standard-solve // default. Linear-source flux moments are left untouched; demoted // and hit-starved regions already fall back to flat shapes. diff --git a/src/random_ray/random_ray_simulation.cpp b/src/random_ray/random_ray_simulation.cpp index 4f5157f202f..4c8ee930f7e 100644 --- a/src/random_ray/random_ray_simulation.cpp +++ b/src/random_ray/random_ray_simulation.cpp @@ -748,10 +748,9 @@ void openmc_run_random_ray() // based on the type of simulation being performed: solves whose results // feed variance reduction -- weight window generation, and any adjoint // workflow, including the forward solve an adjoint source is derived from - // -- receive the strict adaptive estimator, whose guaranteed non-negative - // fluxes those workflows require, while all other solves receive the - // adaptive estimator, which preserves unbiasedness at the cost of allowing - // rare noise-driven negative tallies in near-zero-flux regions. + // -- receive the strict adaptive estimator, whose per-batch fixup of + // negative flux iterates benefits those workflows, while all other solves + // receive the unbiased adaptive estimator. if (FlatSourceDomain::volume_estimator_ == RandomRayVolumeEstimator::AUTO) { bool positivity_needed = FlatSourceDomain::adjoint_requested_ || From 63400ae5ee63845fbc96bc459ca03477c3639463 Mon Sep 17 00:00:00 2001 From: John Tramm Date: Wed, 2 Sep 2026 19:47:15 +0000 Subject: [PATCH 24/35] Describe the simulation averaged estimator as unbiased The simulation averaged estimator is unbiased, not merely low-bias. Correct the description in the new default-selection paragraph, and the two pre-existing instances in the hybrid estimator's table row and methods discussion that made the same claim. Co-Authored-By: Claude Fable 5 Claude-Session: https://claude.ai/code/session_01P9rxXRzNh13xQBGzePHEeK --- docs/source/methods/random_ray.rst | 2 +- docs/source/usersguide/random_ray.rst | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/docs/source/methods/random_ray.rst b/docs/source/methods/random_ray.rst index d912a8129b3..67634b18271 100644 --- a/docs/source/methods/random_ray.rst +++ b/docs/source/methods/random_ray.rst @@ -517,7 +517,7 @@ estimator for eigenvalue solves. OpenMC also features a "hybrid" volume estimator that uses the naive estimator for all regions containing an external (fixed) source term. For all other source regions, the "simulation averaged" estimator is used. This typically achieves -a best of both worlds result, with the benefits of the low bias simulation averaged +a best of both worlds result, with the benefits of the unbiased simulation averaged estimator in most regions, while preventing instability and/or large biases in regions with external source terms via use of the naive estimator. If instability is encountered despite high ray densities, then the naive estimator may be diff --git a/docs/source/usersguide/random_ray.rst b/docs/source/usersguide/random_ray.rst index 7d016350a80..57f9a5656c7 100644 --- a/docs/source/usersguide/random_ray.rst +++ b/docs/source/usersguide/random_ray.rst @@ -1053,7 +1053,7 @@ following methods are currently available in OpenMC: - Applies the naive estimator to all cells that contain an external (fixed) source contribution. Applies the simulation averaged estimator to all other cells. - - * High accuracy/low bias of the simulation averaged estimator in most + - * Accuracy of the unbiased simulation averaged estimator in most cells * Stability of the naive estimator in cells with fixed sources - * Can lead to slightly negative fluxes in cells where the simulation @@ -1102,7 +1102,7 @@ for fixed source and shielding problems, where optically thin, scattering- or streaming-dominated regions (for example, the air- or void-filled regions of a shielding model) can destabilize the ``hybrid`` and ``simulation_averaged`` estimators: it detects and stabilizes the affected -cells automatically while leaving the rest of the problem on the low-bias +cells automatically while leaving the rest of the problem on the unbiased simulation averaged estimator. Because demotions are decided from each cell's accumulated statistics rather than from single-iteration values, the estimator choice does not churn with iteration noise and avoids the bias From 3c93633f6fa61286d8f4f09478aa2d082ddaa571 Mon Sep 17 00:00:00 2001 From: John Tramm Date: Wed, 2 Sep 2026 20:14:59 +0000 Subject: [PATCH 25/35] Match the surrounding prose style in comments and docs Restructure the added comments, docstrings, and documentation prose to read like the surrounding text, replacing dash asides, semicolon joins, and colon-led definitions with plain sentences. No wording changes to upstream text and no changes to code or reference results. Co-Authored-By: Claude Fable 5 Claude-Session: https://claude.ai/code/session_01P9rxXRzNh13xQBGzePHEeK --- docs/source/methods/random_ray.rst | 83 ++++++------ docs/source/usersguide/random_ray.rst | 4 +- include/openmc/constants.h | 27 ++-- .../openmc/random_ray/flat_source_domain.h | 38 +++--- include/openmc/random_ray/source_region.h | 18 +-- src/random_ray/flat_source_domain.cpp | 121 +++++++++--------- src/random_ray/linear_source_domain.cpp | 15 +-- src/random_ray/random_ray_simulation.cpp | 17 +-- .../random_ray_adjoint_fixed_source/test.py | 2 +- .../random_ray_diagonal_stabilization/test.py | 2 +- .../random_ray_k_eff_mesh/test.py | 2 +- .../random_ray_volume_estimator/test.py | 14 +- .../random_ray_volume_estimator_auto/test.py | 10 +- .../test.py | 14 +- .../test_random_ray_default_persistence.py | 22 ++-- 15 files changed, 195 insertions(+), 194 deletions(-) diff --git a/docs/source/methods/random_ray.rst b/docs/source/methods/random_ray.rst index 67634b18271..5af40cd6e1e 100644 --- a/docs/source/methods/random_ray.rst +++ b/docs/source/methods/random_ray.rst @@ -528,58 +528,59 @@ hybrid estimator. Rather than selecting the estimator from the presence of an external source alone, it uses the simulation averaged estimator by default and falls back to the naive estimator (and the previous-iteration miss treatment) on a per-region basis wherever the simulation averaged estimator -is prone to instability: regions that are hit-starved, regions whose reduced -source is negative (possible under transport-corrected cross sections) or -- -while the source is still converging -- greatly exceeds their scalar flux (a -source sustained by external or in-scatter contributions rather than by the -local flux), and, decided from each region's running accumulated flux from -the end of the inactive batches onward, regions whose accumulated flux is -negative or whose flux-independent "feed" (cross-group in-scatter, fission, -and any external source) is strong relative to their own accumulated flux. - -The accumulated-flux decisions are demote-only -- once demoted, a region is -never returned to the simulation averaged estimator -- so the estimator -choice in the tallied batches cannot churn with single-batch noise, and -basing them on accumulated rather than per-iteration values avoids the bias -that reacting to one tail of the noise distribution would introduce. The -feed-based condition exists because strongly fed regions are the one class -whose per-iteration noise scale is set by the reduced source rather than by -the flux, which the per-iteration conditions cannot reliably identify; -regions with no cross-group or external feed can never trigger it. -Re-evaluating the decisions through the active phase catches regions whose -instability only becomes visible after tallies begin, as on large problems -run with short inactive phases. When a linear source shape is in use, -demoted regions additionally revert to a flat source representation (their -source gradients are zeroed), extending the flat-source treatment already -applied to hit-starved regions. Compared to the hybrid estimator, the -adaptive estimator notably also stabilizes the optically thin, -scatter-dominated regions of fixed source problems, making it particularly -beneficial for shielding analysis. - -The adaptive estimator's demotion machinery selects estimators; it never +is prone to instability. The fallback covers regions that are hit-starved, +regions whose reduced source is negative (possible under transport-corrected +cross sections) or, while the source is still converging, greatly exceeds +their scalar flux (a source sustained by external or in-scatter +contributions rather than by the local flux), and regions whose accumulated +flux is negative or whose flux-independent "feed" (cross-group in-scatter, +fission, and any external source) is strong relative to their own +accumulated flux, with these last two conditions decided from each region's +running accumulated flux from the end of the inactive batches onward. + +The accumulated-flux decisions are demote-only, so once a region is demoted +it is never returned to the simulation averaged estimator. As a result, the +estimator choice in the tallied batches cannot churn with single-batch +noise, and basing the decisions on accumulated rather than per-iteration +values avoids the bias that reacting to one tail of the noise distribution +would introduce. The feed-based condition exists because strongly fed +regions are the one class whose per-iteration noise scale is set by the +reduced source rather than by the flux, which the per-iteration conditions +cannot reliably identify. Regions with no cross-group or external feed can +never trigger it. Re-evaluating the decisions through the active phase +catches regions whose instability only becomes visible after tallies begin, +as on large problems run with short inactive phases. When a linear source +shape is in use, demoted regions additionally revert to a flat source +representation (their source gradients are zeroed), extending the +flat-source treatment already applied to hit-starved regions. Compared to +the hybrid estimator, the adaptive estimator notably also stabilizes the +optically thin, scatter-dominated regions of fixed source problems, making +it particularly beneficial for shielding analysis. + +The adaptive estimator's demotion machinery selects estimators and never modifies a computed flux value, which preserves its unbiasedness but means that in pathological cases a small number of flux estimates in near-void regions can still land slightly negative. This is generally harmless, but solves whose results feed variance reduction benefit from suppressing it, as the adjoint source is constructed from the forward flux. The "strict adaptive" estimator therefore runs the same machinery and adds a per-batch -fixup: a group whose batch flux comes out negative is first recomputed with +fixup. A group whose batch flux comes out negative is first recomputed with the batch's own volume (algebraically, the naive volume update, whose consistency removes the volume-mismatch noise responsible for most negative excursions) and floored at the previous iterate if still negative. A region requiring the fixup in more than a few batches is demoted outright to the -naive treatment: the floor masks the accumulated-flux sign signal the -adaptive demotion relies on, and without this chronic channel the repeated -one-sided fixup would bias the affected regions upward. The residual cost is -a small conservative bias (several hundred pcm on typical eigenvalue -problems), which is why the strict estimator is reserved for variance -reduction solves rather than used as the standard default. - -By default, OpenMC selects the volume estimator automatically ("auto"): -solves whose results feed variance reduction -- weight window generation and +naive treatment, as the floor masks the accumulated-flux sign signal that +the adaptive demotion relies on, and without this chronic channel the +repeated one-sided fixup would bias the affected regions upward. The +residual cost is a small conservative bias (several hundred pcm on typical +eigenvalue problems), which is why the strict estimator is reserved for +variance reduction solves rather than used as the standard default. + +By default, OpenMC selects the volume estimator automatically ("auto"). +Solves whose results feed variance reduction (weight window generation and adjoint workflows, including the forward solve an adjoint source is derived -from -- receive the strict adaptive estimator, while all other solves -receive the adaptive estimator. +from) receive the strict adaptive estimator, while all other solves receive +the adaptive estimator. A table that summarizes the pros and cons, as well as recommendations for different use cases, is given in the :ref:`volume diff --git a/docs/source/usersguide/random_ray.rst b/docs/source/usersguide/random_ray.rst index 57f9a5656c7..5382257ee4c 100644 --- a/docs/source/usersguide/random_ray.rst +++ b/docs/source/usersguide/random_ray.rst @@ -1059,7 +1059,7 @@ following methods are currently available in OpenMC: - * Can lead to slightly negative fluxes in cells where the simulation averaged estimator is used * - ``adaptive`` - - Generalizes the hybrid estimator: uses the simulation averaged + - Generalizes the hybrid estimator. Uses the simulation averaged estimator by default, but automatically (and permanently) demotes individual cells to the naive treatment when their accumulated statistics indicate the simulation averaged estimator is unstable @@ -1101,7 +1101,7 @@ the box across problem types. The adaptive estimator is especially valuable for fixed source and shielding problems, where optically thin, scattering- or streaming-dominated regions (for example, the air- or void-filled regions of a shielding model) can destabilize the ``hybrid`` and -``simulation_averaged`` estimators: it detects and stabilizes the affected +``simulation_averaged`` estimators. It detects and stabilizes the affected cells automatically while leaving the rest of the problem on the unbiased simulation averaged estimator. Because demotions are decided from each cell's accumulated statistics rather than from single-iteration values, the diff --git a/include/openmc/constants.h b/include/openmc/constants.h index c4163a3d169..ab08353edbe 100644 --- a/include/openmc/constants.h +++ b/include/openmc/constants.h @@ -66,24 +66,23 @@ constexpr int MAX_SAMPLE {100000}; constexpr double MIN_HITS_PER_BATCH {1.5}; // Strong-source ratio threshold for the adaptive volume estimator. A source -// region is treated as having a "strong" inhomogeneous source -- and is given -// the naive volume and previous-flux miss treatment -- in any group where the -// reduced source q/Sigma_t exceeds this multiple of the region's scalar flux, -// indicating a source sustained by an external or in-scatter contribution -// rather than by the local flux. The value sits well inside the range over -// which benign problems remain untriggered while pathological cells are still -// caught. +// region is treated as having a "strong" inhomogeneous source in any group +// where the reduced source q/Sigma_t exceeds this multiple of the region's +// scalar flux, indicating a source sustained by an external or in-scatter +// contribution rather than by the local flux. Such regions are given the +// naive volume and previous-flux miss treatment. The value sits well inside +// the range over which benign problems remain untriggered while pathological +// cells are still caught. constexpr double ADAPTIVE_VOLUME_KAPPA {4.0}; // Chronic-negativity demotion thresholds for the strict adaptive volume -// estimator: a region whose flux has gone negative (before enforcement) in -// at least max(MIN_COUNT, RATE * current_batch) batches is demoted to the -// naive volume and previous-flux miss treatment. Without this channel the -// non-negativity floor would mask the accumulated-flux sign signal the +// estimator. A region whose flux has gone negative (before the fixup) in at +// least max(MIN_COUNT, RATE * current_batch) batches is demoted to the naive +// volume and previous-flux miss treatment. Without this channel the +// non-negativity floor would mask the accumulated-flux sign signal that the // adaptive demotion relies on, leaving noisy regions to be clipped every -// batch -- a one-sided ratchet that biases their fluxes upward. The chronic -// channel converts "clip forever" into "clip a few times, then switch -// estimator". +// batch and biasing their fluxes upward. Demotion instead moves such regions +// onto an estimator that does not need clipping. constexpr int NEGATIVE_FLUX_DEMOTION_MIN_COUNT {3}; constexpr double NEGATIVE_FLUX_DEMOTION_RATE {0.005}; diff --git a/include/openmc/random_ray/flat_source_domain.h b/include/openmc/random_ray/flat_source_domain.h index 3f7851201b3..d80b0d4fe1a 100644 --- a/include/openmc/random_ray/flat_source_domain.h +++ b/include/openmc/random_ray/flat_source_domain.h @@ -104,13 +104,13 @@ class FlatSourceDomain { //---------------------------------------------------------------------------- // Static data members - // The volume estimator as configured ("auto" by default); set when the - // settings are read and never modified by the solver. + // The volume estimator as configured ("auto" by default). This is set when + // the settings are read and is never modified by the solver. static RandomRayVolumeEstimator volume_estimator_; - // The concrete estimator the solver runs with, assigned unconditionally at - // the start of every random ray solve: the configured value, or for "auto" - // the estimator selected for the type of simulation being performed. All - // solver code reads this member, never volume_estimator_. + // The concrete estimator the solver runs with, assigned at the start of + // every random ray solve. It holds the configured value, or for "auto" the + // estimator selected for the type of simulation being performed. All + // solver code reads this member rather than volume_estimator_. static RandomRayVolumeEstimator resolved_volume_estimator_; //---------------------------------------------------------------------------- @@ -124,10 +124,10 @@ class FlatSourceDomain { // Final-batch snapshot of the naive volume treatment, partitioned by // mutually exclusive cause (the cause counts sum to n_final_naive_), for // end-of-simulation reporting. The two demote-only decisions made from the - // running accumulated flux -- a strong accumulated feed and a negative - // accumulated flux -- are counted with first priority, so their counts - // equal the decisions settled by the final batch; the per-batch - // strong-source test and hit-starved causes count the remainder. + // running accumulated flux (a strong accumulated feed and a negative + // accumulated flux) are counted with first priority, so their counts equal + // the decisions settled by the final batch. The per-batch strong-source + // test and hit-starved causes count the remainder. int64_t n_final_naive_ {0}; int64_t n_final_latch_ {0}; // strong source, from the accumulated feed int64_t n_final_strong_ {0}; // strong source, from the per-batch test @@ -210,15 +210,15 @@ class FlatSourceDomain { void set_flux_to_source(int64_t sr, int g); virtual void set_flux_to_old_flux(int64_t sr, int g); - //! Adaptive-estimator "strong source" test: true if, in any group, the - //! region's reduced source q/Sigma_t is negative (with a non-negative - //! previous-iteration flux), or -- when include_ratio is set, which the - //! callers do only during the inactive batches -- exceeds - //! ADAPTIVE_VOLUME_KAPPA times the (non-negative) previous-iteration scalar - //! flux. Shared by the flat volume switch (add_source_to_scalar_flux) and - //! the linear gradient fallback (update_single_neutron_source); the - //! region's per-group reduced-source and previous-flux arrays are passed - //! directly. + //! Adaptive-estimator "strong source" test. Returns true if, in any group, + //! the region's reduced source q/Sigma_t is negative (with a non-negative + //! previous-iteration flux) or exceeds ADAPTIVE_VOLUME_KAPPA times the + //! (non-negative) previous-iteration scalar flux. The ratio condition is + //! only checked when include_ratio is set, which callers do only during + //! the inactive batches. The test is shared by the flat volume switch + //! (add_source_to_scalar_flux) and the linear gradient fallback + //! (update_single_neutron_source), with the region's per-group + //! reduced-source and previous-flux arrays passed directly. bool region_has_strong_source(const float* reduced_source, const double* flux_old, bool include_ratio) const; diff --git a/include/openmc/random_ray/source_region.h b/include/openmc/random_ray/source_region.h index 5e012cdba3f..eb1823101a2 100644 --- a/include/openmc/random_ray/source_region.h +++ b/include/openmc/random_ray/source_region.h @@ -351,10 +351,10 @@ class SourceRegion { int converged_negative_ { 0}; //!< Demote-only flag (adaptive estimator only), evaluated from the //!< running accumulated flux at the inactive->active transition and - //!< re-evaluated every active batch: 1 = accumulated flux negative in - //!< some group, 2 = strong accumulated feed (latch). Any value > 0 - //!< demotes the region to the naive volume estimator; once set, the - //!< flag is never released. + //!< re-evaluated every active batch. 1 = accumulated flux negative + //!< in some group, 2 = strong accumulated feed (latch). Any value + //!< > 0 demotes the region to the naive volume estimator, and once + //!< set the flag is never released. int n_hits_ {0}; //!< Number of total hits (ray crossings) // Mesh that subdivides this source region int mesh_ {C_NONE}; //!< Index in openmc::model::meshes array that subdivides @@ -704,11 +704,11 @@ class SourceRegionContainer { vector scalar_flux_old_; vector scalar_flux_new_; vector scalar_flux_final_; - // Running sum of the scalar flux over every batch of the current solve - // (inactive and active; never reset within a solve, unlike - // scalar_flux_final which holds only the active-batch accumulation used - // for tallies). Allocated only for the adaptive volume estimator, which - // makes its demotion decisions from it. + // Running sum of the scalar flux over every batch of the current solve, + // inactive and active. Unlike scalar_flux_final, which holds only the + // active-batch accumulation used for tallies, it is never reset within a + // solve. Allocated only for the adaptive volume estimator, which makes its + // demotion decisions from it. vector scalar_flux_t_; vector source_; vector external_source_; diff --git a/src/random_ray/flat_source_domain.cpp b/src/random_ray/flat_source_domain.cpp index 2cd63136670..f00b86dd2ea 100644 --- a/src/random_ray/flat_source_domain.cpp +++ b/src/random_ray/flat_source_domain.cpp @@ -115,7 +115,7 @@ void FlatSourceDomain::accumulate_iteration_flux() // // 1. Accumulated-negative (sign): any region whose accumulated flux is // negative in any group is demoted to the naive (iteration) volume -// estimator -- a positively weighted estimator that cannot go negative +// estimator, a positively weighted estimator that cannot go negative // with a non-negative source. During the active phase the test also // watches the active-only tally accumulation (scalar_flux_final), the // quantity tally means are actually computed from. Because the decision @@ -184,12 +184,12 @@ void FlatSourceDomain::demotion_step() } // During the active phase, a negative accumulated tally flux // (scalar_flux_final, the active-only sum that tally means are computed - // from) also demotes: a region with a strong positive inactive + // from) also demotes, as a region with a strong positive inactive // accumulation can hold the running sum positive while the active-only - // sum -- the quantity actually reported -- goes negative. This branch - // fires only when the reported mean has already lost positivity, so it - // clips realized-negative outcomes rather than one tail of a healthy - // region's noise. + // sum that is actually reported goes negative. This branch fires only + // when the reported mean has already lost positivity, so it clips + // realized-negative outcomes rather than one tail of a healthy region's + // noise. if (!negative && simulation::current_batch > settings::n_inactive) { for (int g = 0; g < negroups_; g++) { if (source_regions_.scalar_flux_final(sr, g) < 0.0) { @@ -378,13 +378,13 @@ bool FlatSourceDomain::region_has_strong_source( for (int g = 0; g < negroups_; g++) { double src = reduced_source[g]; // A negative reduced source counts as strong only when the region's own - // previous flux is non-negative -- the transport-corrected (TCP0) + // previous flux is non-negative. That is the transport-corrected (TCP0) // signature, where negative within-group scattering drives the source // negative independently of the flux. When the previous flux is itself // negative, a negative source is just the sign-locked image of that // fluctuation (exactly so in one-group problems, where q = c*phi + - // q_external); reacting to it iteration-by-iteration would condition the - // estimator choice on the sign of the noise, which is the bias the + // q_external), and reacting to it iteration-by-iteration would condition + // the estimator choice on the sign of the noise, which is the bias the // converged-negative demotion exists to avoid. Chronically negative // regions are handled by that demotion instead. if (src < 0.0 && flux_old[g] >= 0.0) { @@ -460,15 +460,16 @@ int64_t FlatSourceDomain::add_source_to_scalar_flux() // (an external source, or in-scatter from other groups). The // flux update in such cells is a near-cancellation of the transport term // against q/Sigma_t, which is only exact when the volumes used by the two - // terms are consistent -- so these cells require the naive (iteration) - // volume estimator and the previous-flux miss treatment to avoid error + // terms are consistent. These cells therefore require the naive + // (iteration) volume estimator and the previous-flux miss treatment to + // avoid error // terms proportional to (q/Sigma_t) * (1 - V_iteration/V_average) that // can greatly exceed the physical flux. // // A reduced source that is itself negative is also treated as strong. This // arises under transport-corrected (e.g. TCP0) cross sections, whose // within-group scattering term can be negative, driving q/Sigma_t below - // zero even for a non-negative flux; it can also arise transiently from a + // zero even for a non-negative flux. It can also arise transiently from a // negative previous-iteration flux, which the estimator permits by design // (individual iterations are never modified). The diagonal (Gunow) // stabilization keeps the TCP0 iteration convergent but acts on the flux, @@ -482,9 +483,9 @@ int64_t FlatSourceDomain::add_source_to_scalar_flux() // Only the adaptive estimator consults the strong-source flag, so the // other estimators skip the test (and its end-of-run report) entirely. // Void (and effectively-void, sub-MINIMUM_MACRO_XS) regions are also - // excluded: they carry no q/Sigma_t term -- their flux is the streaming - // tally plus a bounded external contribution -- so the near-cancellation - // the test guards against cannot occur, and demoting them to the naive + // excluded. They carry no q/Sigma_t term, as their flux is the streaming + // tally plus a bounded external contribution, so the near-cancellation + // the test guards against cannot occur and demoting them to the naive // volume would only add ratio bias. This matches the linear domain, which // already gates its strong-source gradient fallback on MATERIAL_VOID. bool strong_source = @@ -492,37 +493,36 @@ int64_t FlatSourceDomain::add_source_to_scalar_flux() region_has_strong_source(&source_regions_.source(sr, 0), &source_regions_.scalar_flux_old(sr, 0), simulation::current_batch <= settings::n_inactive); - // Per-region demotion reasons. The hit-starved (small) flag is - // re-evaluated every iteration; the strong-source flag is re-evaluated - // every iteration during the inactive batches and reduces to the - // negative-source (TCP0) condition in the active batches, where the - // stable accumulated-flux decisions govern instead; converged_neg is the + // Per-region demotion reasons, all g-independent. The hit-starved + // (small) flag is re-evaluated every iteration. The strong-source flag + // is also re-evaluated every iteration, though in the active batches it + // reduces to the negative-source (TCP0) condition, as the stable + // accumulated-flux decisions govern there instead. converged_neg is the // demote-only flag set from the running accumulated flux by - // demotion_step. The external-source flag drives only the - // hybrid policy (and the default miss treatment); the adaptive estimator - // catches a low-cross-section external region through the kappa - // strong-source test (during the inactive batches) and the strong-feed - // latch (thereafter), since its external term is folded into q/Sigma_t. - // All are g-independent. + // demotion_step. The external-source flag drives only the hybrid policy + // and the default miss treatment, since the adaptive estimator catches a + // low-cross-section external region through the kappa strong-source test + // (during the inactive batches) and the strong-feed latch (thereafter), + // its external term being folded into q/Sigma_t. bool external = source_regions_.external_source_present(sr); bool small = source_regions_.is_small(sr); int conv_flag = source_regions_.converged_negative(sr); bool converged_neg = conv_flag > 0; // Every estimator reduces to two g-independent per-region decisions: - // 1. which volume to use on a hit -- the simulation-averaged volume, - // unless the region is demoted to the naive (iteration) volume; and - // 2. what to substitute on a miss -- the reduced source by default, or - // the previous iterate. + // 1. which volume to use on a hit (the simulation-averaged volume, + // unless the region is demoted to the naive (iteration) volume) + // 2. what to substitute on a miss (the reduced source by default, or + // the previous iterate) // The previous-flux miss treatment is needed wherever assigning the bare - // reduced source q/Sigma_t to a missed region would bias it: a low-cross- - // section region would otherwise deposit its full infinite-medium flux - // every time it is missed. Hybrid keys this on the external-source flag; - // the adaptive estimator instead extends the previous-flux treatment to - // every region it demotes, which (through the kappa test) already covers - // any region whose q/Sigma_t greatly exceeds its flux -- external or not. - // Both decisions are made once here so the per-group loop stays estimator- - // agnostic. + // reduced source q/Sigma_t to a missed region would bias it, as a low + // cross section region would otherwise deposit its full infinite-medium + // flux every time it is missed. Hybrid keys this on the external-source + // flag. The adaptive estimator instead extends the previous-flux + // treatment to every region it demotes, which (through the kappa test) + // already covers any region whose q/Sigma_t greatly exceeds its flux, + // external or not. Both decisions are made once here so the per-group + // loop stays estimator-agnostic. bool use_naive_volume = false; bool use_old_flux_on_miss = external; switch (resolved_volume_estimator_) { @@ -545,11 +545,11 @@ int64_t FlatSourceDomain::add_source_to_scalar_flux() double volume = use_naive_volume ? volume_iteration : volume_simulation_avg; // On the final iteration, classify the demoted (naive-volume) regions by - // cause -- mutually exclusive, in priority order, so the causes sum to - // the total -- for the end-of-simulation report. The accumulated-flux - // demotions are counted first (their demote-only flags can only - // accumulate, so these counts equal the decisions settled by the final - // batch); the per-batch causes count only the remainder. + // cause for the end-of-simulation report. The causes are mutually + // exclusive and assigned in priority order, so they sum to the total. + // The accumulated-flux demotions are counted first, as their demote-only + // flags can only accumulate and so equal the decisions settled by the + // final batch. The per-batch causes count only the remainder. if (final_iteration && is_adaptive && use_naive_volume) { n_naive++; if (conv_flag == 2) { @@ -574,17 +574,18 @@ int64_t FlatSourceDomain::add_source_to_scalar_flux() // volume. set_flux_to_flux_plus_source(sr, volume, g); // The strict adaptive estimator applies a per-batch fixup to - // negative flux iterates: first a rescue -- the transport term - // rescaled from the volume used to the batch's own volume, - // algebraically reproducing the naive-volume update -- and, if still - // negative (or if the region already used the batch volume), a floor - // at the previous iterate. A value-level fixup is needed here - // because demotion alone cannot prevent a region from inheriting a - // negative excursion through in-scatter from not-yet-demoted - // neighbors. The price is a small conservative (one-sided clip) - // bias, which is why the strict estimator is not the standard-solve - // default. Linear-source flux moments are left untouched; demoted - // and hit-starved regions already fall back to flat shapes. + // negative flux iterates. First the flux is rescued by rescaling the + // transport term from the volume used to the batch's own volume, + // algebraically reproducing the naive-volume update. If it is still + // negative (or the region already used the batch volume), it is + // floored at the previous iterate. A value-level fixup is needed + // here because demotion alone cannot prevent a region from + // inheriting a negative excursion through in-scatter from + // not-yet-demoted neighbors. The price is a small conservative + // (one-sided clip) bias, which is why the strict estimator is not + // the standard-solve default. Linear-source flux moments are left + // untouched, as demoted and hit-starved regions already fall back to + // flat shapes. if (is_strict && source_regions_.scalar_flux_new(sr, g) < 0.0) { if (volume != volume_iteration) { double src = source_regions_.source(sr, g); @@ -617,14 +618,14 @@ int64_t FlatSourceDomain::add_source_to_scalar_flux() "the source region mesh."); } } - // Chronic-negativity demotion (strict adaptive only): the + // Chronic-negativity demotion (strict adaptive only). The // non-negativity floor prevents a chronically noisy region's // accumulated flux from ever going negative, masking the very signal - // the accumulated sign demotion detects -- so left alone, such a - // region would be clipped every batch, a one-sided ratchet that - // biases its flux upward. Counting pre-enforcement negative batches - // restores the escape: after a few events the region is demoted to - // the naive volume and previous-flux miss treatment, where clipping + // the accumulated sign demotion detects. Left alone, such a region + // would be clipped every batch, biasing its flux upward. Counting the + // batches that needed the fixup restores the escape, as after a few + // events the region is demoted to the naive volume and previous-flux + // miss treatment, where clipping // is no longer needed. if (is_strict && (region_rescued || region_floored)) { int n = ++source_regions_.n_negative_batches(sr); diff --git a/src/random_ray/linear_source_domain.cpp b/src/random_ray/linear_source_domain.cpp index 0343fdd1c21..229752ca7f9 100644 --- a/src/random_ray/linear_source_domain.cpp +++ b/src/random_ray/linear_source_domain.cpp @@ -116,14 +116,13 @@ void LinearSourceDomain::update_single_neutron_source(SourceRegionHandle& srh) // flat source representation, extending the flat-source fallback already // applied to hit-starved (small) regions so that demotion is uniform in // effect. For strong-source regions the reduced source greatly exceeds the - // scalar flux, so the flat-source cancellation must be exact; the gradient - // terms attenuate segments against the local rather than the flat source, - // introducing per-iteration noise at the gradient scale that the volume - // choice cannot cancel. For regions demoted from the accumulated flux - // (converged_negative > 0: negative accumulated flux, or the - // strong-feed latch), the same reasoning applies to their cause -- a - // negative accumulated flux means the fitted gradients carry no meaningful - // shape information, and a latched strong feed is the gradient-scale noise + // scalar flux, so the flat-source cancellation must be exact, while the + // gradient terms attenuate segments against the local rather than the flat + // source, introducing per-iteration noise at the gradient scale that the + // volume choice cannot cancel. The same reasoning applies to regions + // demoted from the accumulated flux (converged_negative > 0). A negative + // accumulated flux means the fitted gradients carry no meaningful shape + // information, and a latched strong feed is the gradient-scale noise // hazard the strong-source fallback above exists for. if (is_adaptive_family(resolved_volume_estimator_) && material != MATERIAL_VOID && diff --git a/src/random_ray/random_ray_simulation.cpp b/src/random_ray/random_ray_simulation.cpp index 4c8ee930f7e..e710e73e8f9 100644 --- a/src/random_ray/random_ray_simulation.cpp +++ b/src/random_ray/random_ray_simulation.cpp @@ -354,7 +354,7 @@ void RandomRaySimulation::prepare_fw_fixed_sources_adjoint() // In eigenvalue mode there are no fixed adjoint sources to derive from // the forward flux, but the accumulated forward flux must still be // cleared so that the adjoint solve's active accumulation starts from a - // clean array -- otherwise any consumer of the final flux would mix + // clean array. Otherwise any consumer of the final flux would mix // forward and adjoint modes. #pragma omp parallel for for (int64_t se = 0; se < domain_->n_source_elements(); se++) { @@ -636,8 +636,9 @@ void RandomRaySimulation::print_results_random_ray( // plus that batch's per-iteration demotions). fmt::print(" Number of Naive Demotions = {} SRs ({:.4f}%)\n", domain_->n_final_naive_, domain_->n_final_naive_ * inv); - // The per-cause diagnostic breakdown is developer-facing; verbosity 8 - // sits above the default (7) but below the per-particle output (9). + // The per-cause diagnostic breakdown is developer-facing, so it is + // printed at verbosity 8, above the default (7) but below the + // per-particle output (9). // The causes are mutually exclusive and sum to the total above: // "accumulated" causes are the demote-only decisions made from the // running accumulated flux (from the inactive->active transition @@ -745,11 +746,11 @@ void openmc_run_random_ray() // Resolve the volume estimator for this solve, leaving the configured // setting untouched. "Auto" (the default) maps to a concrete estimator - // based on the type of simulation being performed: solves whose results - // feed variance reduction -- weight window generation, and any adjoint - // workflow, including the forward solve an adjoint source is derived from - // -- receive the strict adaptive estimator, whose per-batch fixup of - // negative flux iterates benefits those workflows, while all other solves + // based on the type of simulation being performed. Solves whose results + // feed variance reduction (weight window generation, and any adjoint + // workflow, including the forward solve an adjoint source is derived + // from) receive the strict adaptive estimator, whose per-batch fixup of + // negative flux iterates benefits those workflows. All other solves // receive the unbiased adaptive estimator. if (FlatSourceDomain::volume_estimator_ == RandomRayVolumeEstimator::AUTO) { bool positivity_needed = diff --git a/tests/regression_tests/random_ray_adjoint_fixed_source/test.py b/tests/regression_tests/random_ray_adjoint_fixed_source/test.py index 8efb2e34faf..489d3a70516 100644 --- a/tests/regression_tests/random_ray_adjoint_fixed_source/test.py +++ b/tests/regression_tests/random_ray_adjoint_fixed_source/test.py @@ -25,7 +25,7 @@ def test_random_ray_adjoint_fixed_source(): def test_random_ray_adjoint_fixed_source_adaptive_starved(): - # Ray-starved adaptive case (~20% forward / ~40% adjoint miss rate): the + # Ray-starved adaptive case (~20% forward / ~40% adjoint miss rate). The # adjoint (second) solve makes real end-of-inactive demotion decisions on # its own accumulated flux, guarding both the adaptive machinery in # adjoint mode and the clearing of the forward solve's accumulated flux diff --git a/tests/regression_tests/random_ray_diagonal_stabilization/test.py b/tests/regression_tests/random_ray_diagonal_stabilization/test.py index e53effe14b9..5ba09f5f76c 100644 --- a/tests/regression_tests/random_ray_diagonal_stabilization/test.py +++ b/tests/regression_tests/random_ray_diagonal_stabilization/test.py @@ -73,7 +73,7 @@ def test_random_ray_diagonal_stabilization_adaptive(): # The transport-corrected (P0) library's negative within-group scattering # drives some reduced sources negative, which the adaptive estimator must # handle through its negative-source (strong) treatment and its - # end-of-inactive demotion; this case pins that interplay. + # end-of-inactive demotion. This case pins that interplay. with change_directory('adaptive'): openmc.reset_auto_ids() model = _build_model() diff --git a/tests/regression_tests/random_ray_k_eff_mesh/test.py b/tests/regression_tests/random_ray_k_eff_mesh/test.py index 6fa3b555b6d..1f3b212d173 100644 --- a/tests/regression_tests/random_ray_k_eff_mesh/test.py +++ b/tests/regression_tests/random_ray_k_eff_mesh/test.py @@ -39,7 +39,7 @@ def test_random_ray_k_eff_mesh(): def test_random_ray_k_eff_mesh_adaptive_starved(): - # Ray-starved adaptive eigenvalue case (~1.2% miss rate): the subdivision + # Ray-starved adaptive eigenvalue case (~1.2% miss rate). The subdivision # mesh plus a low ray count engages the adaptive demotion machinery # (strong-source and hit-starved regions, plus the end-of-inactive # demotion decision and the previous-flux miss treatment) in eigenvalue diff --git a/tests/regression_tests/random_ray_volume_estimator/test.py b/tests/regression_tests/random_ray_volume_estimator/test.py index fd2ae30ffb9..db711adc90f 100644 --- a/tests/regression_tests/random_ray_volume_estimator/test.py +++ b/tests/regression_tests/random_ray_volume_estimator/test.py @@ -16,13 +16,13 @@ def _cleanup(self): os.remove(f) -# A deliberately ray-starved configuration (~20% source region miss rate): -# the volume estimators only differ meaningfully when regions are missed or -# sparsely hit, so a starved run exercises every estimator code path -- the -# per-estimator volume choices, the miss treatments, and for the adaptive -# estimator the strong-source (kappa) demotion, the hit-starved demotion, -# the end-of-inactive converged-negative demotion, and the previous-flux -# miss treatment all fire at this density. +# A deliberately ray-starved configuration (~20% source region miss rate). +# The volume estimators only differ meaningfully when regions are missed or +# sparsely hit, so a starved run exercises every estimator code path. At +# this density the per-estimator volume choices, the miss treatments, and +# for the adaptive estimator the strong-source (kappa) demotion, the +# hit-starved demotion, the end-of-inactive converged-negative demotion, +# and the previous-flux miss treatment all fire. @pytest.mark.parametrize("estimator", ["hybrid", "simulation_averaged", "naive", diff --git a/tests/regression_tests/random_ray_volume_estimator_auto/test.py b/tests/regression_tests/random_ray_volume_estimator_auto/test.py index 227665f42b3..3a144e3055f 100644 --- a/tests/regression_tests/random_ray_volume_estimator_auto/test.py +++ b/tests/regression_tests/random_ray_volume_estimator_auto/test.py @@ -16,14 +16,14 @@ def _cleanup(self): os.remove(f) -# The default "auto" volume estimator resolves by solve type: standard +# The default "auto" volume estimator resolves by solve type. Standard # solves receive the adaptive estimator, while solves whose results feed # variance reduction (any adjoint workflow, and weight window generation) # receive the strict adaptive estimator. No case sets an estimator -# explicitly, so these golds pin the routing itself: if the resolution -# policy regresses, the affected case's results shift. The forward and -# adjoint cases pin the adjoint-flag trigger; the weight_windows case pins -# the generator-presence trigger. +# explicitly, so these golds pin the routing itself, and a regression in +# the resolution policy shifts the affected case's results. The forward and +# adjoint cases pin the adjoint-flag trigger, while the weight_windows case +# pins the generator-presence trigger. @pytest.mark.parametrize("solve", ["forward", "adjoint", "weight_windows"]) def test_random_ray_volume_estimator_auto(solve): with change_directory(solve): diff --git a/tests/regression_tests/random_ray_volume_estimator_linear/test.py b/tests/regression_tests/random_ray_volume_estimator_linear/test.py index 8adec4cd6ba..5be5b79b04c 100644 --- a/tests/regression_tests/random_ray_volume_estimator_linear/test.py +++ b/tests/regression_tests/random_ray_volume_estimator_linear/test.py @@ -16,13 +16,13 @@ def _cleanup(self): os.remove(f) -# A deliberately ray-starved configuration (~20% source region miss rate): -# the volume estimators only differ meaningfully when regions are missed or -# sparsely hit, so a starved run exercises every estimator code path -- the -# per-estimator volume choices, the miss treatments, and for the adaptive -# estimator the strong-source (kappa) demotion, the hit-starved demotion, -# the end-of-inactive converged-negative demotion, and the previous-flux -# miss treatment all fire at this density. +# A deliberately ray-starved configuration (~20% source region miss rate). +# The volume estimators only differ meaningfully when regions are missed or +# sparsely hit, so a starved run exercises every estimator code path. At +# this density the per-estimator volume choices, the miss treatments, and +# for the adaptive estimator the strong-source (kappa) demotion, the +# hit-starved demotion, the end-of-inactive converged-negative demotion, +# and the previous-flux miss treatment all fire. @pytest.mark.parametrize("estimator", ["hybrid", "simulation_averaged", "naive", diff --git a/tests/unit_tests/test_random_ray_default_persistence.py b/tests/unit_tests/test_random_ray_default_persistence.py index d5109e6b2b0..d261917a6c9 100644 --- a/tests/unit_tests/test_random_ray_default_persistence.py +++ b/tests/unit_tests/test_random_ray_default_persistence.py @@ -1,15 +1,15 @@ """The random ray volume-estimator setting must not leak across an in-process finalize/re-initialize cycle (openmc.lib workflows such as iterative weight window generation). The solver never modifies the -configured setting -- "auto" is resolved into a separate run-scoped value -- -but the configured setting is a static that XML parsing only assigns when -the element is present, so openmc_finalize_random_ray() must restore the -"auto" default between runs. Running an explicit estimator first and a -default model second makes a missed restore visible: the second run would -report the first run's estimator instead of resolving "auto". The default -second run is an adjoint solve, which also pins the auto routing under -openmc.lib (it must resolve to the strict adaptive estimator, once per -solve).""" +configured setting, as "auto" is resolved into a separate run-scoped value. +However, the configured setting is a static that XML parsing only assigns +when the element is present, so openmc_finalize_random_ray() must restore +the "auto" default between runs. Running an explicit estimator first and a +default model second makes a missed restore visible, as the second run +would report the first run's estimator instead of resolving "auto". The +default second run is an adjoint solve, which also pins the auto routing +under openmc.lib (it must resolve to the strict adaptive estimator, once +per solve).""" import openmc import openmc.lib @@ -39,8 +39,8 @@ def test_random_ray_default_estimator_persistence(run_in_tmpdir, capfd): if 'Volume Estimator Type' in line: reported.append(line.split('=')[-1].strip()) - # The forward run reports once; the adjoint run reports once per solve - # (forward-for-adjoint, adjoint). + # The forward run reports once, while the adjoint run reports once per + # solve (forward-for-adjoint, adjoint). assert reported == ['Naive', 'Strict Adaptive (auto)', 'Strict Adaptive (auto)'], ( f"volume estimator leaked across in-process reruns: {reported}") From bbdf542b1a2c1732764d3a4aac701d1772a127a5 Mon Sep 17 00:00:00 2001 From: John Tramm Date: Wed, 2 Sep 2026 20:27:22 +0000 Subject: [PATCH 26/35] Simplify the theory section on the adaptive estimators Rewrite the methods discussion of the adaptive and strict adaptive volume estimators from scratch. The purpose of each estimator is now stated up front, the demotion conditions are given as short separate sentences instead of one long enumeration, and the mechanism detail that the section does not need is left to the code. Co-Authored-By: Claude Fable 5 Claude-Session: https://claude.ai/code/session_01P9rxXRzNh13xQBGzePHEeK --- docs/source/methods/random_ray.rst | 87 +++++++++++------------------- 1 file changed, 32 insertions(+), 55 deletions(-) diff --git a/docs/source/methods/random_ray.rst b/docs/source/methods/random_ray.rst index 5af40cd6e1e..537930096d8 100644 --- a/docs/source/methods/random_ray.rst +++ b/docs/source/methods/random_ray.rst @@ -524,63 +524,40 @@ encountered despite high ray densities, then the naive estimator may be preferable. OpenMC also features an "adaptive" volume estimator that generalizes the -hybrid estimator. Rather than selecting the estimator from the presence of an -external source alone, it uses the simulation averaged estimator by default -and falls back to the naive estimator (and the previous-iteration miss -treatment) on a per-region basis wherever the simulation averaged estimator -is prone to instability. The fallback covers regions that are hit-starved, -regions whose reduced source is negative (possible under transport-corrected -cross sections) or, while the source is still converging, greatly exceeds -their scalar flux (a source sustained by external or in-scatter -contributions rather than by the local flux), and regions whose accumulated -flux is negative or whose flux-independent "feed" (cross-group in-scatter, -fission, and any external source) is strong relative to their own -accumulated flux, with these last two conditions decided from each region's -running accumulated flux from the end of the inactive batches onward. - -The accumulated-flux decisions are demote-only, so once a region is demoted -it is never returned to the simulation averaged estimator. As a result, the -estimator choice in the tallied batches cannot churn with single-batch -noise, and basing the decisions on accumulated rather than per-iteration -values avoids the bias that reacting to one tail of the noise distribution -would introduce. The feed-based condition exists because strongly fed -regions are the one class whose per-iteration noise scale is set by the -reduced source rather than by the flux, which the per-iteration conditions -cannot reliably identify. Regions with no cross-group or external feed can -never trigger it. Re-evaluating the decisions through the active phase -catches regions whose instability only becomes visible after tallies begin, -as on large problems run with short inactive phases. When a linear source -shape is in use, demoted regions additionally revert to a flat source -representation (their source gradients are zeroed), extending the -flat-source treatment already applied to hit-starved regions. Compared to -the hybrid estimator, the adaptive estimator notably also stabilizes the -optically thin, scatter-dominated regions of fixed source problems, making -it particularly beneficial for shielding analysis. - -The adaptive estimator's demotion machinery selects estimators and never -modifies a computed flux value, which preserves its unbiasedness but means -that in pathological cases a small number of flux estimates in near-void -regions can still land slightly negative. This is generally harmless, but -solves whose results feed variance reduction benefit from suppressing it, -as the adjoint source is constructed from the forward flux. The "strict -adaptive" estimator therefore runs the same machinery and adds a per-batch -fixup. A group whose batch flux comes out negative is first recomputed with -the batch's own volume (algebraically, the naive volume update, whose -consistency removes the volume-mismatch noise responsible for most negative -excursions) and floored at the previous iterate if still negative. A region -requiring the fixup in more than a few batches is demoted outright to the -naive treatment, as the floor masks the accumulated-flux sign signal that -the adaptive demotion relies on, and without this chronic channel the -repeated one-sided fixup would bias the affected regions upward. The -residual cost is a small conservative bias (several hundred pcm on typical -eigenvalue problems), which is why the strict estimator is reserved for -variance reduction solves rather than used as the standard default. +hybrid estimator. It uses the simulation averaged estimator by default and +automatically demotes individual cells to the naive treatment (the naive +volume and the previous-flux miss treatment) when they show signs of +instability. The most important case this adds over the hybrid estimator is +a cell fed almost entirely by in-scatter from other groups, as in the +optically thin air regions common to shielding problems, where the reduced +source dwarfs the flux even though no external source is present. + +A cell is demoted when it is hit-starved or when its reduced source is +negative. During the inactive batches, a cell whose reduced source is much +larger than its scalar flux is also demoted. Finally, beginning at the end +of the inactive batches and re-evaluated throughout the active phase, a +cell is demoted permanently if its flux accumulated over the simulation is +negative or is dominated by sources that do not derive from its own flux. +Because these last decisions are made from accumulated statistics and are +never reversed, the estimator choice does not churn with iteration noise +during the tallied batches. When a linear source shape is in use, demoted +cells also revert to a flat source representation. + +A "strict adaptive" variant is provided for solves whose results feed +variance reduction, where even a small number of slightly negative flux +estimates can degrade the adjoint solve and the quality of generated weight +windows. It runs the same machinery and additionally repairs any negative +flux estimate each batch, first by recomputing it with the batch's own +volume and then, if it is still negative, by falling back on the previous +iterate. A cell that needs the repair repeatedly is demoted outright, which +keeps the one-sided repair from biasing its flux upward. The repair +introduces a small conservative bias overall (several hundred pcm on +typical eigenvalue problems), so the strict variant is not used for +standard solves. By default, OpenMC selects the volume estimator automatically ("auto"). -Solves whose results feed variance reduction (weight window generation and -adjoint workflows, including the forward solve an adjoint source is derived -from) receive the strict adaptive estimator, while all other solves receive -the adaptive estimator. +Weight window generation and adjoint solves receive the strict adaptive +estimator, and all other solves receive the adaptive estimator. A table that summarizes the pros and cons, as well as recommendations for different use cases, is given in the :ref:`volume From 911022b762d01ede292a7e1270c587abf17fa362 Mon Sep 17 00:00:00 2001 From: John Tramm Date: Wed, 2 Sep 2026 20:33:47 +0000 Subject: [PATCH 27/35] Add the default auto setting to the estimator table Place a short entry for the auto default first in the user's guide estimator comparison table, so readers see immediately that OpenMC selects an appropriate estimator on its own and that the rest of the table is only relevant when overriding it. The selection rule itself is described in the paragraphs that follow the table. Co-Authored-By: Claude Fable 5 Claude-Session: https://claude.ai/code/session_01P9rxXRzNh13xQBGzePHEeK --- docs/source/usersguide/random_ray.rst | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/docs/source/usersguide/random_ray.rst b/docs/source/usersguide/random_ray.rst index 5382257ee4c..7a7c4c49789 100644 --- a/docs/source/usersguide/random_ray.rst +++ b/docs/source/usersguide/random_ray.rst @@ -1030,6 +1030,12 @@ following methods are currently available in OpenMC: - Description - Pros - Cons + * - ``auto`` (default) + - Automatically selects an appropriate estimator for the type of + simulation being performed. Most users do not need to consider this + setting further. + - * Reliable behavior across problem types with no user input + - * The selection policy may evolve in future releases * - ``simulation_averaged`` - Accumulates total active ray lengths in each FSR over all iterations, improving the estimate of the volume in each cell each iteration. From a6841f940f82b2ce16231804c4bcaa0b37738bed Mon Sep 17 00:00:00 2001 From: John Tramm Date: Wed, 2 Sep 2026 20:35:27 +0000 Subject: [PATCH 28/35] Trim the auto table entry Co-Authored-By: Claude Fable 5 Claude-Session: https://claude.ai/code/session_01P9rxXRzNh13xQBGzePHEeK --- docs/source/usersguide/random_ray.rst | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/docs/source/usersguide/random_ray.rst b/docs/source/usersguide/random_ray.rst index 7a7c4c49789..b475b47100f 100644 --- a/docs/source/usersguide/random_ray.rst +++ b/docs/source/usersguide/random_ray.rst @@ -1034,8 +1034,8 @@ following methods are currently available in OpenMC: - Automatically selects an appropriate estimator for the type of simulation being performed. Most users do not need to consider this setting further. - - * Reliable behavior across problem types with no user input - - * The selection policy may evolve in future releases + - * No user input needed + - * N/A * - ``simulation_averaged`` - Accumulates total active ray lengths in each FSR over all iterations, improving the estimate of the volume in each cell each iteration. From 178b6649ffdaee4cde41148bf3b8a1c675f40619 Mon Sep 17 00:00:00 2001 From: John Tramm Date: Wed, 2 Sep 2026 21:11:32 +0000 Subject: [PATCH 29/35] ran git clang format --- include/openmc/random_ray/flat_source_domain.h | 8 ++++---- include/openmc/random_ray/source_region.h | 5 ++++- src/random_ray/flat_source_domain.cpp | 5 ++--- src/random_ray/source_region.cpp | 6 ++---- 4 files changed, 12 insertions(+), 12 deletions(-) diff --git a/include/openmc/random_ray/flat_source_domain.h b/include/openmc/random_ray/flat_source_domain.h index d80b0d4fe1a..f69956cec47 100644 --- a/include/openmc/random_ray/flat_source_domain.h +++ b/include/openmc/random_ray/flat_source_domain.h @@ -129,10 +129,10 @@ class FlatSourceDomain { // the decisions settled by the final batch. The per-batch strong-source // test and hit-starved causes count the remainder. int64_t n_final_naive_ {0}; - int64_t n_final_latch_ {0}; // strong source, from the accumulated feed - int64_t n_final_strong_ {0}; // strong source, from the per-batch test - int64_t n_final_sign_ {0}; // negative accumulated flux - int64_t n_final_small_ {0}; // hit-starved + int64_t n_final_latch_ {0}; // strong source, from the accumulated feed + int64_t n_final_strong_ {0}; // strong source, from the per-batch test + int64_t n_final_sign_ {0}; // negative accumulated flux + int64_t n_final_small_ {0}; // hit-starved int64_t n_final_chronic_ {0}; // chronic negativity (strict adaptive) // Final-batch counts of the strict adaptive estimator's non-negativity // enforcement: regions whose negative batch flux was recomputed with the diff --git a/include/openmc/random_ray/source_region.h b/include/openmc/random_ray/source_region.h index eb1823101a2..cfd10746a61 100644 --- a/include/openmc/random_ray/source_region.h +++ b/include/openmc/random_ray/source_region.h @@ -594,7 +594,10 @@ class SourceRegionContainer { return scalar_flux_final_[se]; } - double& scalar_flux_t(int64_t sr, int g) { return scalar_flux_t_[index(sr, g)]; } + double& scalar_flux_t(int64_t sr, int g) + { + return scalar_flux_t_[index(sr, g)]; + } const double scalar_flux_t(int64_t sr, int g) const { return scalar_flux_t_[index(sr, g)]; diff --git a/src/random_ray/flat_source_domain.cpp b/src/random_ray/flat_source_domain.cpp index f00b86dd2ea..228cd853621 100644 --- a/src/random_ray/flat_source_domain.cpp +++ b/src/random_ray/flat_source_domain.cpp @@ -230,9 +230,8 @@ void FlatSourceDomain::demotion_step() q_indep += simulation::current_batch * source_regions_.external_source(sr, g); } - if (q_indep > - ADAPTIVE_VOLUME_KAPPA * - std::max(source_regions_.scalar_flux_t(sr, g), 0.0)) { + if (q_indep > ADAPTIVE_VOLUME_KAPPA * + std::max(source_regions_.scalar_flux_t(sr, g), 0.0)) { latched = true; } } diff --git a/src/random_ray/source_region.cpp b/src/random_ray/source_region.cpp index 8c6025dfb24..ec84f515313 100644 --- a/src/random_ray/source_region.cpp +++ b/src/random_ray/source_region.cpp @@ -12,10 +12,8 @@ namespace openmc { SourceRegionHandle::SourceRegionHandle(SourceRegion& sr) : negroups_(sr.scalar_flux_old_.size()), material_(&sr.material_), temperature_idx_(&sr.temperature_idx_), density_mult_(&sr.density_mult_), - is_small_(&sr.is_small_), - n_negative_batches_(&sr.n_negative_batches_), - converged_negative_(&sr.converged_negative_), - n_hits_(&sr.n_hits_), + is_small_(&sr.is_small_), n_negative_batches_(&sr.n_negative_batches_), + converged_negative_(&sr.converged_negative_), n_hits_(&sr.n_hits_), is_linear_(sr.source_gradients_.size() > 0), lock_(&sr.lock_), volume_(&sr.volume_), volume_t_(&sr.volume_t_), volume_sq_(&sr.volume_sq_), volume_sq_t_(&sr.volume_sq_t_), volume_naive_(&sr.volume_naive_), From 8c95ffee053414dfca50c00c23b579d0a908a40e Mon Sep 17 00:00:00 2001 From: John Tramm Date: Thu, 3 Sep 2026 02:35:19 +0000 Subject: [PATCH 30/35] Ran git clang format Co-Authored-By: Claude Fable 5 Claude-Session: https://claude.ai/code/session_01P9rxXRzNh13xQBGzePHEeK --- src/random_ray/flat_source_domain.cpp | 7 +++---- 1 file changed, 3 insertions(+), 4 deletions(-) diff --git a/src/random_ray/flat_source_domain.cpp b/src/random_ray/flat_source_domain.cpp index 228cd853621..27388872a92 100644 --- a/src/random_ray/flat_source_domain.cpp +++ b/src/random_ray/flat_source_domain.cpp @@ -430,9 +430,8 @@ int64_t FlatSourceDomain::add_source_to_scalar_flux() int64_t n_floored = 0; int64_t n_chronic = 0; -#pragma omp parallel for reduction( \ - + : n_hits, n_naive, n_latch, n_strong, n_sign, n_small, n_rescued, \ - n_floored, n_chronic) +#pragma omp parallel for reduction(+ : n_hits, n_naive, n_latch, n_strong, \ + n_sign, n_small, n_rescued, n_floored, n_chronic) for (int64_t sr = 0; sr < n_source_regions(); sr++) { double volume_simulation_avg = source_regions_.volume(sr); @@ -1735,7 +1734,7 @@ void FlatSourceDomain::set_fw_adjoint_sources() source_regions_.external_source_present(sr) = 0; } } // End loop over source regions - } // End local FW-CADIS logic + } // End local FW-CADIS logic } void FlatSourceDomain::set_local_adjoint_sources() From a3bd0787ba22e880e932836c0c73c26f8b539922 Mon Sep 17 00:00:00 2001 From: John Tramm Date: Thu, 3 Sep 2026 13:10:22 +0000 Subject: [PATCH 31/35] Pin the FW-CADIS mesh test to the hybrid estimator The weightwindows_fw_cadis_mesh test was the one FW-CADIS test that set no volume estimator, so it silently rode the default. Under the new auto default it resolves to the strict adaptive estimator and its reference results no longer match, which failed CI. Pin it to hybrid, the previous default, restoring its reference results byte for byte (only the inputs gain the explicit element). The auto behavior of weight window generation is covered by the volume estimator auto test. Co-Authored-By: Claude Fable 5 Claude-Session: https://claude.ai/code/session_01P9rxXRzNh13xQBGzePHEeK --- .../weightwindows_fw_cadis_mesh/flat/inputs_true.dat | 1 + .../weightwindows_fw_cadis_mesh/linear/inputs_true.dat | 1 + tests/regression_tests/weightwindows_fw_cadis_mesh/test.py | 1 + 3 files changed, 3 insertions(+) diff --git a/tests/regression_tests/weightwindows_fw_cadis_mesh/flat/inputs_true.dat b/tests/regression_tests/weightwindows_fw_cadis_mesh/flat/inputs_true.dat index a0d84257a8d..ed14e0fbf6d 100644 --- a/tests/regression_tests/weightwindows_fw_cadis_mesh/flat/inputs_true.dat +++ b/tests/regression_tests/weightwindows_fw_cadis_mesh/flat/inputs_true.dat @@ -236,6 +236,7 @@ flat + hybrid diff --git a/tests/regression_tests/weightwindows_fw_cadis_mesh/linear/inputs_true.dat b/tests/regression_tests/weightwindows_fw_cadis_mesh/linear/inputs_true.dat index 62f8478586b..33c75bb271d 100644 --- a/tests/regression_tests/weightwindows_fw_cadis_mesh/linear/inputs_true.dat +++ b/tests/regression_tests/weightwindows_fw_cadis_mesh/linear/inputs_true.dat @@ -236,6 +236,7 @@ linear + hybrid diff --git a/tests/regression_tests/weightwindows_fw_cadis_mesh/test.py b/tests/regression_tests/weightwindows_fw_cadis_mesh/test.py index 680e9dc6df7..40caf1700f6 100644 --- a/tests/regression_tests/weightwindows_fw_cadis_mesh/test.py +++ b/tests/regression_tests/weightwindows_fw_cadis_mesh/test.py @@ -44,6 +44,7 @@ def test_weight_windows_fw_cadis_mesh(shape): model.settings.inactive = 20 model.settings.random_ray['source_shape'] = shape + model.settings.random_ray['volume_estimator'] = 'hybrid' harness = MGXSTestHarness('statepoint.30.h5', model) harness.main() From 2a8af7115db897db76f83704cd1983c849c7e92f Mon Sep 17 00:00:00 2001 From: Paul Romano Date: Wed, 9 Sep 2026 10:37:43 -0500 Subject: [PATCH 32/35] Move random ray constants to flat_source_domain.h --- include/openmc/constants.h | 35 ------------------- .../openmc/random_ray/flat_source_domain.h | 34 ++++++++++++++++++ 2 files changed, 34 insertions(+), 35 deletions(-) diff --git a/include/openmc/constants.h b/include/openmc/constants.h index ab08353edbe..6ba32ecd437 100644 --- a/include/openmc/constants.h +++ b/include/openmc/constants.h @@ -61,41 +61,6 @@ constexpr double RADIAL_MESH_TOL {1e-10}; // Maximum number of random samples per history constexpr int MAX_SAMPLE {100000}; -// Avg. number of hits per batch to be defined as a "small" -// source region in the random ray solver -constexpr double MIN_HITS_PER_BATCH {1.5}; - -// Strong-source ratio threshold for the adaptive volume estimator. A source -// region is treated as having a "strong" inhomogeneous source in any group -// where the reduced source q/Sigma_t exceeds this multiple of the region's -// scalar flux, indicating a source sustained by an external or in-scatter -// contribution rather than by the local flux. Such regions are given the -// naive volume and previous-flux miss treatment. The value sits well inside -// the range over which benign problems remain untriggered while pathological -// cells are still caught. -constexpr double ADAPTIVE_VOLUME_KAPPA {4.0}; - -// Chronic-negativity demotion thresholds for the strict adaptive volume -// estimator. A region whose flux has gone negative (before the fixup) in at -// least max(MIN_COUNT, RATE * current_batch) batches is demoted to the naive -// volume and previous-flux miss treatment. Without this channel the -// non-negativity floor would mask the accumulated-flux sign signal that the -// adaptive demotion relies on, leaving noisy regions to be clipped every -// batch and biasing their fluxes upward. Demotion instead moves such regions -// onto an estimator that does not need clipping. -constexpr int NEGATIVE_FLUX_DEMOTION_MIN_COUNT {3}; -constexpr double NEGATIVE_FLUX_DEMOTION_RATE {0.005}; - -// The minimum flux value to be considered non-zero when computing adjoint -// sources. Positive values below this cutoff will be treated as zero, so as to -// prevent extremely large adjoint source terms from being generated. -constexpr double ZERO_FLUX_CUTOFF {1e-22}; - -// The minimum macroscopic cross section value considered non-void for the -// random ray solver. Materials with any group with a cross section below this -// value will be converted to pure void. -constexpr double MINIMUM_MACRO_XS {1e-6}; - // ============================================================================ // MATH AND PHYSICAL CONSTANTS diff --git a/include/openmc/random_ray/flat_source_domain.h b/include/openmc/random_ray/flat_source_domain.h index f69956cec47..e5bd468ba9c 100644 --- a/include/openmc/random_ray/flat_source_domain.h +++ b/include/openmc/random_ray/flat_source_domain.h @@ -12,6 +12,40 @@ namespace openmc { +// Avg. number of hits per batch to be defined as a "small" source region. +constexpr double MIN_HITS_PER_BATCH {1.5}; + +// Strong-source ratio threshold for the adaptive volume estimator. A source +// region is treated as having a "strong" inhomogeneous source in any group +// where the reduced source q/Sigma_t exceeds this multiple of the region's +// scalar flux, indicating a source sustained by an external or in-scatter +// contribution rather than by the local flux. Such regions are given the +// naive volume and previous-flux miss treatment. The value sits well inside +// the range over which benign problems remain untriggered while pathological +// cells are still caught. +constexpr double ADAPTIVE_VOLUME_KAPPA {4.0}; + +// Chronic-negativity demotion thresholds for the strict adaptive volume +// estimator. A region whose flux has gone negative (before the fixup) in at +// least max(MIN_COUNT, RATE * current_batch) batches is demoted to the naive +// volume and previous-flux miss treatment. Without this channel the +// non-negativity floor would mask the accumulated-flux sign signal that the +// adaptive demotion relies on, leaving noisy regions to be clipped every +// batch and biasing their fluxes upward. Demotion instead moves such regions +// onto an estimator that does not need clipping. +constexpr int NEGATIVE_FLUX_DEMOTION_MIN_COUNT {3}; +constexpr double NEGATIVE_FLUX_DEMOTION_RATE {0.005}; + +// The minimum flux value to be considered non-zero when computing adjoint +// sources. Positive values below this cutoff will be treated as zero, so as to +// prevent extremely large adjoint source terms from being generated. +constexpr double ZERO_FLUX_CUTOFF {1e-22}; + +// The minimum macroscopic cross section value considered non-void for the +// random ray solver. Materials with any group with a cross section below this +// value will be converted to pure void. +constexpr double MINIMUM_MACRO_XS {1e-6}; + // True for the members of the adaptive volume estimator family: the // adaptive estimator, and the strict adaptive estimator, which runs the // same machinery plus a per-batch non-negativity enforcement on the flux From 30e9148ab00749b519c54ea2e8eceb30505abac2 Mon Sep 17 00:00:00 2001 From: John Tramm Date: Wed, 9 Sep 2026 16:16:36 +0000 Subject: [PATCH 33/35] Assess the strict positivity fixup on the stabilized flux iterate The strict adaptive estimator floored a negative raw flux iterate at the previous iterate before the diagonal stabilization ran. With transport corrected cross sections a raw iterate driven negative by the negative within-group scattering is expected, and the stabilization maps it to a positive value, but the previous iterate is a fixed point of the stabilization, so flooring first froze the iteration. On a homogeneous two-group problem with an analytic eigenvalue of 0.5, the strict estimator converged to a spurious 2.0. The stabilization is now applied inside the flux update, to every candidate value the update considers, so the fixup necessarily sees the stabilized value and the rescued candidate is stabilized before its own sign is assessed. The separate stabilization pass is removed, which makes the wrong ordering impossible rather than avoided. The arithmetic is unchanged, and the existing stabilization references for the hybrid and adaptive estimators pass unmodified. The rescue also reconstructed the transport contribution by subtracting the reduced source, which is not the additive term of a void region's update. One helper now supplies that term to both the update and the rescue, so the rescue reproduces the naive-volume update for void and material regions alike. A short strict variant of the transport-corrected regression test pins the corrected direction of the iteration toward the analytic answer. Co-Authored-By: Claude Fable 5.1 Claude-Session: https://claude.ai/code/session_01P9rxXRzNh13xQBGzePHEeK --- .../openmc/random_ray/flat_source_domain.h | 3 +- src/random_ray/flat_source_domain.cpp | 183 ++++++++++-------- src/random_ray/random_ray_simulation.cpp | 6 +- .../strict_adaptive/inputs_true.dat | 48 +++++ .../strict_adaptive/results_true.dat | 2 + .../random_ray_diagonal_stabilization/test.py | 74 +++++++ 6 files changed, 233 insertions(+), 83 deletions(-) create mode 100644 tests/regression_tests/random_ray_diagonal_stabilization/strict_adaptive/inputs_true.dat create mode 100644 tests/regression_tests/random_ray_diagonal_stabilization/strict_adaptive/results_true.dat diff --git a/include/openmc/random_ray/flat_source_domain.h b/include/openmc/random_ray/flat_source_domain.h index e5bd468ba9c..c87ff48f301 100644 --- a/include/openmc/random_ray/flat_source_domain.h +++ b/include/openmc/random_ray/flat_source_domain.h @@ -104,7 +104,6 @@ class FlatSourceDomain { SourceRegionHandle get_subdivided_source_region_handle( SourceRegionKey sr_key, Position r, Direction u); void finalize_discovered_source_regions(); - void apply_transport_stabilization(); int64_t n_source_regions() const { return source_regions_.n_source_regions(); @@ -243,6 +242,8 @@ class FlatSourceDomain { virtual void set_flux_to_flux_plus_source(int64_t sr, double volume, int g); void set_flux_to_source(int64_t sr, int g); virtual void set_flux_to_old_flux(int64_t sr, int g); + double flux_additive_term(int64_t sr, int g) const; + double stabilized_flux(int64_t sr, int g, double phi_new) const; //! Adaptive-estimator "strong source" test. Returns true if, in any group, //! the region's reduced source q/Sigma_t is negative (with a non-negative diff --git a/src/random_ray/flat_source_domain.cpp b/src/random_ray/flat_source_domain.cpp index 27388872a92..8a4d9ea3aff 100644 --- a/src/random_ray/flat_source_domain.cpp +++ b/src/random_ray/flat_source_domain.cpp @@ -339,6 +339,24 @@ void FlatSourceDomain::normalize_scalar_flux_and_volumes( } } +// The additive term of the flux update for a source region and group. A +// material region adds its reduced source q/Sigma_t. A void region has no +// such term and instead adds a bounded contribution from its external +// source, which is nonzero only in fixed source mode. The same term is used +// by the strict estimator's rescue, which rescales only the transport part +// of an update, so the two cannot drift apart. +double FlatSourceDomain::flux_additive_term(int64_t sr, int g) const +{ + if (source_regions_.material(sr) == MATERIAL_VOID) { + if (settings::run_mode == RunMode::FIXED_SOURCE) { + return 0.5f * source_regions_.external_source(sr, g) * + source_regions_.volume_sq(sr); + } + return 0.0; + } + return source_regions_.source(sr, g); +} + void FlatSourceDomain::set_flux_to_flux_plus_source( int64_t sr, double volume, int g) { @@ -346,18 +364,72 @@ void FlatSourceDomain::set_flux_to_flux_plus_source( int temp = source_regions_.temperature_idx(sr); if (material == MATERIAL_VOID) { source_regions_.scalar_flux_new(sr, g) /= volume; - if (settings::run_mode == RunMode::FIXED_SOURCE) { - source_regions_.scalar_flux_new(sr, g) += - 0.5f * source_regions_.external_source(sr, g) * - source_regions_.volume_sq(sr); - } } else { double sigma_t = sigma_t_[(material * ntemperature_ + temp) * negroups_ + g] * source_regions_.density_mult(sr); source_regions_.scalar_flux_new(sr, g) /= (sigma_t * volume); - source_regions_.scalar_flux_new(sr, g) += source_regions_.source(sr, g); } + source_regions_.scalar_flux_new(sr, g) += flux_additive_term(sr, g); +} + +// Applies the "diagonal stabilization" technique developed by Gunow et al. +// to one flux iterate: +// +// Geoffrey Gunow, Benoit Forget, Kord Smith, Stabilization of multi-group +// neutron transport with transport-corrected cross-sections, Annals of Nuclear +// Energy, Volume 126, 2019, Pages 211-219, ISSN 0306-4549, +// https://doi.org/10.1016/j.anucene.2018.10.036. +// +// Returns the given iterate unchanged unless the region's within-group +// scattering cross section for the group is negative, in which case the +// stabilized iterate is returned. The stabilization is part of the flux +// update rather than a separate pass, so that every candidate value the +// update considers, including the strict estimator's rescued and floored +// candidates, is assessed in stabilized form. With transport-corrected +// cross sections a raw iterate can legitimately be negative and stabilize +// to a positive value, and a positivity fixup applied to the raw value +// would instead freeze the iteration, since the previous iterate is a +// fixed point of the stabilization. +double FlatSourceDomain::stabilized_flux( + int64_t sr, int g, double phi_new) const +{ + // Nothing to do if all in-group scattering cross sections are positive + if (!is_transport_stabilization_needed_) { + return phi_new; + } + int material = source_regions_.material(sr); + if (material == MATERIAL_VOID) { + return phi_new; + } + int temp = source_regions_.temperature_idx(sr); + double density_mult = source_regions_.density_mult(sr); + + // Only apply stabilization if the diagonal (in-group) scattering XS is + // negative + double sigma_s = + sigma_s_[((material * ntemperature_ + temp) * negroups_ + g) * negroups_ + + g] * + density_mult; + if (sigma_s >= 0.0) { + return phi_new; + } + double sigma_t = + sigma_t_[(material * ntemperature_ + temp) * negroups_ + g] * density_mult; + double phi_old = source_regions_.scalar_flux_old(sr, g); + + // Equation 18 in the above Gunow et al. 2019 paper. For a default + // rho of 1.0, this ensures there are no negative diagonal elements + // in the iteration matrix. A lesser rho could be used (or exposed + // as a user input parameter) to reduce the negative impact on + // convergence rate though would need to be experimentally tested to see + // if it doesn't become unstable. rho = 1.0 is good as it gives the + // highest assurance of stability, and the impacts on convergence rate + // are pretty mild. + double D = diagonal_stabilization_rho_ * sigma_s / sigma_t; + + // Equation 16 in the above Gunow et al. 2019 paper + return (phi_new - D * phi_old) / (1.0 - D); } void FlatSourceDomain::set_flux_to_old_flux(int64_t sr, int g) @@ -569,14 +641,18 @@ int64_t FlatSourceDomain::add_source_to_scalar_flux() if (volume_iteration > 0.0) { // Hit this iteration: the flat source from the previous iteration plus // this iteration's transport contribution, normalized by the chosen - // volume. + // volume, then stabilized. set_flux_to_flux_plus_source(sr, volume, g); + double raw = source_regions_.scalar_flux_new(sr, g); + double phi = stabilized_flux(sr, g, raw); // The strict adaptive estimator applies a per-batch fixup to - // negative flux iterates. First the flux is rescued by rescaling the - // transport term from the volume used to the batch's own volume, - // algebraically reproducing the naive-volume update. If it is still - // negative (or the region already used the batch volume), it is - // floored at the previous iterate. A value-level fixup is needed + // negative flux iterates, assessed on the stabilized value. First + // the flux is rescued by rescaling the transport term from the + // volume used to the batch's own volume, algebraically reproducing + // the naive-volume update, with the rescued candidate stabilized in + // turn. If it is still negative (or the region already used the + // batch volume), it is floored at the previous iterate, which the + // stabilization leaves unchanged. A value-level fixup is needed // here because demotion alone cannot prevent a region from // inheriting a negative excursion through in-scatter from // not-yet-demoted neighbors. The price is a small conservative @@ -584,29 +660,36 @@ int64_t FlatSourceDomain::add_source_to_scalar_flux() // the standard-solve default. Linear-source flux moments are left // untouched, as demoted and hit-starved regions already fall back to // flat shapes. - if (is_strict && source_regions_.scalar_flux_new(sr, g) < 0.0) { + if (is_strict && phi < 0.0) { if (volume != volume_iteration) { - double src = source_regions_.source(sr, g); - source_regions_.scalar_flux_new(sr, g) = - (source_regions_.scalar_flux_new(sr, g) - src) * - (volume / volume_iteration) + - src; + double additive = flux_additive_term(sr, g); + double rescued = + (raw - additive) * (volume / volume_iteration) + additive; + phi = stabilized_flux(sr, g, rescued); region_rescued = true; } - if (source_regions_.scalar_flux_new(sr, g) < 0.0) { - source_regions_.scalar_flux_new(sr, g) = - source_regions_.scalar_flux_old(sr, g); + if (phi < 0.0) { + phi = source_regions_.scalar_flux_old(sr, g); region_floored = true; } } + source_regions_.scalar_flux_new(sr, g) = phi; } else if (volume_simulation_avg > 0.0) { // Missed this iteration but hit previously: substitute per the miss - // policy decided above (the previous iterate, or the reduced source). + // policy decided above (the previous iterate, or the reduced source), + // then stabilize. if (use_old_flux_on_miss) { set_flux_to_old_flux(sr, g); } else { set_flux_to_source(sr, g); } + source_regions_.scalar_flux_new(sr, g) = + stabilized_flux(sr, g, source_regions_.scalar_flux_new(sr, g)); + } else { + // Never hit: the iterate stays at its reset value, stabilized like + // every other element. + source_regions_.scalar_flux_new(sr, g) = + stabilized_flux(sr, g, source_regions_.scalar_flux_new(sr, g)); } // Halt if NaN implosion is detected if (!std::isfinite(source_regions_.scalar_flux_new(sr, g))) { @@ -2088,62 +2171,6 @@ void FlatSourceDomain::finalize_discovered_source_regions() discovered_source_regions_.clear(); } -// This is the "diagonal stabilization" technique developed by Gunow et al. in: -// -// Geoffrey Gunow, Benoit Forget, Kord Smith, Stabilization of multi-group -// neutron transport with transport-corrected cross-sections, Annals of Nuclear -// Energy, Volume 126, 2019, Pages 211-219, ISSN 0306-4549, -// https://doi.org/10.1016/j.anucene.2018.10.036. -void FlatSourceDomain::apply_transport_stabilization() -{ - // Don't do anything if all in-group scattering - // cross sections are positive - if (!is_transport_stabilization_needed_) { - return; - } - - // Apply the stabilization factor to all source elements -#pragma omp parallel for - for (int64_t sr = 0; sr < n_source_regions(); sr++) { - int material = source_regions_.material(sr); - int temp = source_regions_.temperature_idx(sr); - double density_mult = source_regions_.density_mult(sr); - if (material == MATERIAL_VOID) { - continue; - } - for (int g = 0; g < negroups_; g++) { - // Only apply stabilization if the diagonal (in-group) scattering XS is - // negative - double sigma_s = - sigma_s_[((material * ntemperature_ + temp) * negroups_ + g) * - negroups_ + - g] * - density_mult; - if (sigma_s < 0.0) { - double sigma_t = - sigma_t_[(material * ntemperature_ + temp) * negroups_ + g] * - density_mult; - double phi_new = source_regions_.scalar_flux_new(sr, g); - double phi_old = source_regions_.scalar_flux_old(sr, g); - - // Equation 18 in the above Gunow et al. 2019 paper. For a default - // rho of 1.0, this ensures there are no negative diagonal elements - // in the iteration matrix. A lesser rho could be used (or exposed - // as a user input parameter) to reduce the negative impact on - // convergence rate though would need to be experimentally tested to see - // if it doesn't become unstable. rho = 1.0 is good as it gives the - // highest assurance of stability, and the impacts on convergence rate - // are pretty mild. - double D = diagonal_stabilization_rho_ * sigma_s / sigma_t; - - // Equation 16 in the above Gunow et al. 2019 paper - source_regions_.scalar_flux_new(sr, g) = - (phi_new - D * phi_old) / (1.0 - D); - } - } - } -} - // Determines the base source region index (i.e., a material filled cell // instance) that corresponds to a particular location in the geometry. Requires // that the "gs" object passed in has already been initialized and has called diff --git a/src/random_ray/random_ray_simulation.cpp b/src/random_ray/random_ray_simulation.cpp index e710e73e8f9..9054df2c69a 100644 --- a/src/random_ray/random_ray_simulation.cpp +++ b/src/random_ray/random_ray_simulation.cpp @@ -454,12 +454,10 @@ void RandomRaySimulation::simulate() domain_->normalize_scalar_flux_and_volumes( settings::n_particles * RandomRay::distance_active_); - // Add source to scalar flux, compute number of FSR hits + // Add source to scalar flux (applying any transport stabilization + // factors), compute number of FSR hits int64_t n_hits = domain_->add_source_to_scalar_flux(); - // Apply transport stabilization factors - domain_->apply_transport_stabilization(); - if (settings::run_mode == RunMode::EIGENVALUE) { // Compute random ray k-eff domain_->compute_k_eff(); diff --git a/tests/regression_tests/random_ray_diagonal_stabilization/strict_adaptive/inputs_true.dat b/tests/regression_tests/random_ray_diagonal_stabilization/strict_adaptive/inputs_true.dat new file mode 100644 index 00000000000..59d650d0526 --- /dev/null +++ b/tests/regression_tests/random_ray_diagonal_stabilization/strict_adaptive/inputs_true.dat @@ -0,0 +1,48 @@ + + + + mgxs.h5 + + + + + + + + + + + + + + + + eigenvalue + 100 + 8 + 3 + multi-group + + 30.0 + 200.0 + + + + 0.0 0.0 0.0 10.0 10.0 10.0 + + + + + + + + + strict_adaptive + + + 4 4 4 + 0.0 0.0 0.0 + 10.0 10.0 10.0 + + + diff --git a/tests/regression_tests/random_ray_diagonal_stabilization/strict_adaptive/results_true.dat b/tests/regression_tests/random_ray_diagonal_stabilization/strict_adaptive/results_true.dat new file mode 100644 index 00000000000..530d8b49018 --- /dev/null +++ b/tests/regression_tests/random_ray_diagonal_stabilization/strict_adaptive/results_true.dat @@ -0,0 +1,2 @@ +k-combined: +5.484375E-01 2.130966E-02 diff --git a/tests/regression_tests/random_ray_diagonal_stabilization/test.py b/tests/regression_tests/random_ray_diagonal_stabilization/test.py index 5ba09f5f76c..ff9543869ab 100644 --- a/tests/regression_tests/random_ray_diagonal_stabilization/test.py +++ b/tests/regression_tests/random_ray_diagonal_stabilization/test.py @@ -1,6 +1,8 @@ import os +import numpy as np import openmc +import openmc.mgxs from openmc.examples import pwr_pin_cell from openmc.utility_funcs import change_directory from openmc import RegularMesh @@ -80,3 +82,75 @@ def test_random_ray_diagonal_stabilization_adaptive(): model.settings.random_ray['volume_estimator'] = 'adaptive' harness = MGXSTestHarness('statepoint.20.h5', model) harness.main() + + +def _build_homogeneous_model(): + # A homogeneous two-group eigenvalue problem with a negative within-group + # scattering cross section in the fast group, as a transport correction + # produces. With chi = (1, 0) and no upscatter the infinite-medium + # eigenvalue is + # k = nu_sigma_f2 * sigma_s12 / ((sigma_t2 - sigma_s22) * + # (sigma_t1 - sigma_s11)) + # = 0.375 * 1.0 / (0.5 * 1.5) = 0.5 + # exactly, and the iteration approaches it geometrically from above + # (1.5, 1.0, 0.75, 0.625, ...). Only a few batches are run, enough for + # the stored reference to pin that direction. + groups = openmc.mgxs.EnergyGroups(group_edges=[1e-5, 1.0e3, 20.0e6]) + xs = openmc.XSdata('mat', groups) + xs.order = 0 + xs.set_total([1.0, 1.0]) + xs.set_absorption([0.5, 0.5]) + xs.set_scatter_matrix(np.array([[[-0.5], [1.0]], + [[0.0], [0.5]]])) + xs.set_fission([0.0, 0.15]) + xs.set_nu_fission([0.0, 0.375]) + xs.set_chi([1.0, 0.0]) + lib = openmc.MGXSLibrary(groups) + lib.add_xsdatas([xs]) + lib.export_to_hdf5('mgxs.h5') + + mat = openmc.Material(name='mat') + mat.set_density('macro', 1.0) + mat.add_macroscopic(openmc.Macroscopic('mat')) + model = openmc.Model() + model.materials = openmc.Materials([mat]) + model.materials.cross_sections = 'mgxs.h5' + box = openmc.model.RectangularParallelepiped( + 0.0, 10.0, 0.0, 10.0, 0.0, 10.0, boundary_type='reflective') + cell = openmc.Cell(fill=mat, region=-box) + model.geometry = openmc.Geometry([cell]) + + mesh = RegularMesh() + mesh.lower_left = (0.0, 0.0, 0.0) + mesh.upper_right = (10.0, 10.0, 10.0) + mesh.dimension = (4, 4, 4) + + settings = model.settings + settings.energy_mode = 'multi-group' + settings.run_mode = 'eigenvalue' + settings.particles = 100 + settings.inactive = 3 + settings.batches = 8 + settings.random_ray = { + 'distance_inactive': 30.0, + 'distance_active': 200.0, + 'ray_source': openmc.IndependentSource( + space=openmc.stats.Box((0.0, 0.0, 0.0), (10.0, 10.0, 10.0))), + 'source_region_meshes': [(mesh, [model.geometry.root_universe])], + } + return model + + +def test_random_ray_diagonal_stabilization_strict_adaptive(): + # The strict estimator's non-negativity fixup must assess the stabilized + # flux iterate. The negative within-group scattering drives the raw + # fast-group iterate negative early on, which the stabilization maps to + # a positive value. Flooring the raw value first would freeze the + # iteration at the previous iterate, and this problem then climbs toward + # a spurious k of 2.0 instead of descending toward the analytic 0.5. + with change_directory('strict_adaptive'): + openmc.reset_auto_ids() + model = _build_homogeneous_model() + model.settings.random_ray['volume_estimator'] = 'strict_adaptive' + harness = MGXSTestHarness('statepoint.8.h5', model) + harness.main() From af28a90bb1f274317cb29e83fbea92981d87e8a2 Mon Sep 17 00:00:00 2001 From: John Tramm Date: Wed, 9 Sep 2026 16:40:08 +0000 Subject: [PATCH 34/35] Parametrize the diagonal stabilization tests over the estimator The file had grown one test function per estimator, against the convention used by the other random ray regression tests, which parametrize over the estimator with one stored reference per case. The pin cell case now runs over the hybrid and adaptive estimators, with the hybrid reference moved unchanged into its own directory, and the homogeneous transport-corrected case runs over all three estimators. Co-Authored-By: Claude Fable 5.1 Claude-Session: https://claude.ai/code/session_01P9rxXRzNh13xQBGzePHEeK --- .../homogeneous_adaptive/inputs_true.dat | 48 ++++++++++++++++ .../results_true.dat | 0 .../homogeneous_hybrid/inputs_true.dat | 48 ++++++++++++++++ .../homogeneous_hybrid/results_true.dat | 2 + .../inputs_true.dat | 0 .../results_true.dat | 2 + .../{ => hybrid}/inputs_true.dat | 0 .../{ => hybrid}/results_true.dat | 0 .../random_ray_diagonal_stabilization/test.py | 55 +++++++++---------- 9 files changed, 126 insertions(+), 29 deletions(-) create mode 100644 tests/regression_tests/random_ray_diagonal_stabilization/homogeneous_adaptive/inputs_true.dat rename tests/regression_tests/random_ray_diagonal_stabilization/{strict_adaptive => homogeneous_adaptive}/results_true.dat (100%) create mode 100644 tests/regression_tests/random_ray_diagonal_stabilization/homogeneous_hybrid/inputs_true.dat create mode 100644 tests/regression_tests/random_ray_diagonal_stabilization/homogeneous_hybrid/results_true.dat rename tests/regression_tests/random_ray_diagonal_stabilization/{strict_adaptive => homogeneous_strict_adaptive}/inputs_true.dat (100%) create mode 100644 tests/regression_tests/random_ray_diagonal_stabilization/homogeneous_strict_adaptive/results_true.dat rename tests/regression_tests/random_ray_diagonal_stabilization/{ => hybrid}/inputs_true.dat (100%) rename tests/regression_tests/random_ray_diagonal_stabilization/{ => hybrid}/results_true.dat (100%) diff --git a/tests/regression_tests/random_ray_diagonal_stabilization/homogeneous_adaptive/inputs_true.dat b/tests/regression_tests/random_ray_diagonal_stabilization/homogeneous_adaptive/inputs_true.dat new file mode 100644 index 00000000000..30c71486a91 --- /dev/null +++ b/tests/regression_tests/random_ray_diagonal_stabilization/homogeneous_adaptive/inputs_true.dat @@ -0,0 +1,48 @@ + + + + mgxs.h5 + + + + + + + + + + + + + + + + eigenvalue + 100 + 8 + 3 + multi-group + + 30.0 + 200.0 + + + + 0.0 0.0 0.0 10.0 10.0 10.0 + + + + + + + + + adaptive + + + 4 4 4 + 0.0 0.0 0.0 + 10.0 10.0 10.0 + + + diff --git a/tests/regression_tests/random_ray_diagonal_stabilization/strict_adaptive/results_true.dat b/tests/regression_tests/random_ray_diagonal_stabilization/homogeneous_adaptive/results_true.dat similarity index 100% rename from tests/regression_tests/random_ray_diagonal_stabilization/strict_adaptive/results_true.dat rename to tests/regression_tests/random_ray_diagonal_stabilization/homogeneous_adaptive/results_true.dat diff --git a/tests/regression_tests/random_ray_diagonal_stabilization/homogeneous_hybrid/inputs_true.dat b/tests/regression_tests/random_ray_diagonal_stabilization/homogeneous_hybrid/inputs_true.dat new file mode 100644 index 00000000000..1cf9b55110a --- /dev/null +++ b/tests/regression_tests/random_ray_diagonal_stabilization/homogeneous_hybrid/inputs_true.dat @@ -0,0 +1,48 @@ + + + + mgxs.h5 + + + + + + + + + + + + + + + + eigenvalue + 100 + 8 + 3 + multi-group + + 30.0 + 200.0 + + + + 0.0 0.0 0.0 10.0 10.0 10.0 + + + + + + + + + hybrid + + + 4 4 4 + 0.0 0.0 0.0 + 10.0 10.0 10.0 + + + diff --git a/tests/regression_tests/random_ray_diagonal_stabilization/homogeneous_hybrid/results_true.dat b/tests/regression_tests/random_ray_diagonal_stabilization/homogeneous_hybrid/results_true.dat new file mode 100644 index 00000000000..530d8b49018 --- /dev/null +++ b/tests/regression_tests/random_ray_diagonal_stabilization/homogeneous_hybrid/results_true.dat @@ -0,0 +1,2 @@ +k-combined: +5.484375E-01 2.130966E-02 diff --git a/tests/regression_tests/random_ray_diagonal_stabilization/strict_adaptive/inputs_true.dat b/tests/regression_tests/random_ray_diagonal_stabilization/homogeneous_strict_adaptive/inputs_true.dat similarity index 100% rename from tests/regression_tests/random_ray_diagonal_stabilization/strict_adaptive/inputs_true.dat rename to tests/regression_tests/random_ray_diagonal_stabilization/homogeneous_strict_adaptive/inputs_true.dat diff --git a/tests/regression_tests/random_ray_diagonal_stabilization/homogeneous_strict_adaptive/results_true.dat b/tests/regression_tests/random_ray_diagonal_stabilization/homogeneous_strict_adaptive/results_true.dat new file mode 100644 index 00000000000..530d8b49018 --- /dev/null +++ b/tests/regression_tests/random_ray_diagonal_stabilization/homogeneous_strict_adaptive/results_true.dat @@ -0,0 +1,2 @@ +k-combined: +5.484375E-01 2.130966E-02 diff --git a/tests/regression_tests/random_ray_diagonal_stabilization/inputs_true.dat b/tests/regression_tests/random_ray_diagonal_stabilization/hybrid/inputs_true.dat similarity index 100% rename from tests/regression_tests/random_ray_diagonal_stabilization/inputs_true.dat rename to tests/regression_tests/random_ray_diagonal_stabilization/hybrid/inputs_true.dat diff --git a/tests/regression_tests/random_ray_diagonal_stabilization/results_true.dat b/tests/regression_tests/random_ray_diagonal_stabilization/hybrid/results_true.dat similarity index 100% rename from tests/regression_tests/random_ray_diagonal_stabilization/results_true.dat rename to tests/regression_tests/random_ray_diagonal_stabilization/hybrid/results_true.dat diff --git a/tests/regression_tests/random_ray_diagonal_stabilization/test.py b/tests/regression_tests/random_ray_diagonal_stabilization/test.py index ff9543869ab..50c499d9d93 100644 --- a/tests/regression_tests/random_ray_diagonal_stabilization/test.py +++ b/tests/regression_tests/random_ray_diagonal_stabilization/test.py @@ -6,6 +6,7 @@ from openmc.examples import pwr_pin_cell from openmc.utility_funcs import change_directory from openmc import RegularMesh +import pytest from tests.testing_harness import TolerantPyAPITestHarness @@ -64,26 +65,6 @@ def _build_model(): return model -def test_random_ray_diagonal_stabilization(): - model = _build_model() - model.settings.random_ray['volume_estimator'] = 'hybrid' - harness = MGXSTestHarness('statepoint.20.h5', model) - harness.main() - - -def test_random_ray_diagonal_stabilization_adaptive(): - # The transport-corrected (P0) library's negative within-group scattering - # drives some reduced sources negative, which the adaptive estimator must - # handle through its negative-source (strong) treatment and its - # end-of-inactive demotion. This case pins that interplay. - with change_directory('adaptive'): - openmc.reset_auto_ids() - model = _build_model() - model.settings.random_ray['volume_estimator'] = 'adaptive' - harness = MGXSTestHarness('statepoint.20.h5', model) - harness.main() - - def _build_homogeneous_model(): # A homogeneous two-group eigenvalue problem with a negative within-group # scattering cross section in the fast group, as a transport correction @@ -141,16 +122,32 @@ def _build_homogeneous_model(): return model -def test_random_ray_diagonal_stabilization_strict_adaptive(): - # The strict estimator's non-negativity fixup must assess the stabilized - # flux iterate. The negative within-group scattering drives the raw - # fast-group iterate negative early on, which the stabilization maps to - # a positive value. Flooring the raw value first would freeze the - # iteration at the previous iterate, and this problem then climbs toward - # a spurious k of 2.0 instead of descending toward the analytic 0.5. - with change_directory('strict_adaptive'): +# The transport-corrected (P0) library's negative within-group scattering +# drives some reduced sources negative, which the adaptive estimator must +# handle through its negative-source (strong) treatment and its +# end-of-inactive demotion. The adaptive case pins that interplay. +@pytest.mark.parametrize("estimator", ["hybrid", "adaptive"]) +def test_random_ray_diagonal_stabilization(estimator): + with change_directory(estimator): + openmc.reset_auto_ids() + model = _build_model() + model.settings.random_ray['volume_estimator'] = estimator + harness = MGXSTestHarness('statepoint.20.h5', model) + harness.main() + + +# Every estimator must descend toward the analytic eigenvalue of the +# homogeneous problem. The strict estimator's non-negativity fixup must +# assess the stabilized flux iterate: the negative within-group scattering +# drives the raw fast-group iterate negative early on, which the +# stabilization maps to a positive value. Flooring the raw value first would +# freeze the iteration at the previous iterate, and this problem then climbs +# toward a spurious k of 2.0 instead. +@pytest.mark.parametrize("estimator", ["hybrid", "adaptive", "strict_adaptive"]) +def test_random_ray_diagonal_stabilization_homogeneous(estimator): + with change_directory(f'homogeneous_{estimator}'): openmc.reset_auto_ids() model = _build_homogeneous_model() - model.settings.random_ray['volume_estimator'] = 'strict_adaptive' + model.settings.random_ray['volume_estimator'] = estimator harness = MGXSTestHarness('statepoint.8.h5', model) harness.main() From 47f4ad6a3b45a5255740dc15fa4432eef2fdf353 Mon Sep 17 00:00:00 2001 From: Paul Romano Date: Wed, 9 Sep 2026 17:42:02 -0500 Subject: [PATCH 35/35] Reduce adaptive source region bookkeeping memory --- include/openmc/random_ray/source_region.h | 7 +++++-- src/random_ray/flat_source_domain.cpp | 7 +++++-- src/random_ray/source_region.cpp | 13 +++++++++---- 3 files changed, 19 insertions(+), 8 deletions(-) diff --git a/include/openmc/random_ray/source_region.h b/include/openmc/random_ray/source_region.h index cfd10746a61..f488aa293aa 100644 --- a/include/openmc/random_ray/source_region.h +++ b/include/openmc/random_ray/source_region.h @@ -411,8 +411,10 @@ class SourceRegionContainer { public: //---------------------------------------------------------------------------- // Constructors - SourceRegionContainer(int negroups, bool is_linear, bool is_adaptive) - : negroups_(negroups), is_linear_(is_linear), is_adaptive_(is_adaptive) + SourceRegionContainer( + int negroups, bool is_linear, bool is_adaptive, bool is_strict_adaptive) + : negroups_(negroups), is_linear_(is_linear), is_adaptive_(is_adaptive), + is_strict_adaptive_(is_strict_adaptive) {} SourceRegionContainer() = default; @@ -673,6 +675,7 @@ class SourceRegionContainer { int negroups_ {0}; bool is_linear_ {false}; bool is_adaptive_ {false}; + bool is_strict_adaptive_ {false}; // SoA storage for scalar fields (one item per source region) vector material_; diff --git a/src/random_ray/flat_source_domain.cpp b/src/random_ray/flat_source_domain.cpp index 2233212ede9..f3567e6f9db 100644 --- a/src/random_ray/flat_source_domain.cpp +++ b/src/random_ray/flat_source_domain.cpp @@ -59,7 +59,10 @@ FlatSourceDomain::FlatSourceDomain() : negroups_(data::mg.num_energy_groups_) // Initialize source regions. bool is_linear = RandomRay::source_shape_ != RandomRaySourceShape::FLAT; bool is_adaptive = is_adaptive_family(resolved_volume_estimator_); - source_regions_ = SourceRegionContainer(negroups_, is_linear, is_adaptive); + bool is_strict_adaptive = + resolved_volume_estimator_ == RandomRayVolumeEstimator::STRICT_ADAPTIVE; + source_regions_ = SourceRegionContainer( + negroups_, is_linear, is_adaptive, is_strict_adaptive); // Initialize tally volumes if (volume_normalized_flux_tallies_) { @@ -576,7 +579,7 @@ int64_t FlatSourceDomain::add_source_to_scalar_flux() // its external term being folded into q/Sigma_t. bool external = source_regions_.external_source_present(sr); bool small = source_regions_.is_small(sr); - int conv_flag = source_regions_.converged_negative(sr); + int conv_flag = is_adaptive ? source_regions_.converged_negative(sr) : 0; bool converged_neg = conv_flag > 0; // Every estimator reduces to two g-independent per-region decisions: diff --git a/src/random_ray/source_region.cpp b/src/random_ray/source_region.cpp index ec84f515313..ab8126f1804 100644 --- a/src/random_ray/source_region.cpp +++ b/src/random_ray/source_region.cpp @@ -75,8 +75,12 @@ void SourceRegionContainer::push_back(const SourceRegion& sr) temperature_idx_.push_back(sr.temperature_idx_); density_mult_.push_back(sr.density_mult_); is_small_.push_back(sr.is_small_); - n_negative_batches_.push_back(sr.n_negative_batches_); - converged_negative_.push_back(sr.converged_negative_); + if (is_strict_adaptive_) { + n_negative_batches_.push_back(sr.n_negative_batches_); + } + if (is_adaptive_) { + converged_negative_.push_back(sr.converged_negative_); + } n_hits_.push_back(sr.n_hits_); lock_.push_back(sr.lock_); volume_.push_back(sr.volume_); @@ -198,8 +202,9 @@ SourceRegionHandle SourceRegionContainer::get_source_region_handle(int64_t sr) handle.temperature_idx_ = &temperature_idx(sr); handle.density_mult_ = &density_mult(sr); handle.is_small_ = &is_small(sr); - handle.n_negative_batches_ = &n_negative_batches(sr); - handle.converged_negative_ = &converged_negative(sr); + handle.n_negative_batches_ = + is_strict_adaptive_ ? &n_negative_batches(sr) : nullptr; + handle.converged_negative_ = is_adaptive_ ? &converged_negative(sr) : nullptr; handle.n_hits_ = &n_hits(sr); handle.is_linear_ = is_linear(); handle.lock_ = &lock(sr);