LCOV - code coverage report
Current view: top level - src/base - OpenMCCellAverageProblem.C (source / functions) Hit Total Coverage
Test: neams-th-coe/cardinal: ddd5f2 Lines: 1305 1389 94.0 %
Date: 2026-06-07 19:35:24 Functions: 73 73 100.0 %
Legend: Lines: hit not hit

          Line data    Source code
       1             : /********************************************************************/
       2             : /*                  SOFTWARE COPYRIGHT NOTIFICATION                 */
       3             : /*                             Cardinal                             */
       4             : /*                                                                  */
       5             : /*                  (c) 2021 UChicago Argonne, LLC                  */
       6             : /*                        ALL RIGHTS RESERVED                       */
       7             : /*                                                                  */
       8             : /*                 Prepared by UChicago Argonne, LLC                */
       9             : /*               Under Contract No. DE-AC02-06CH11357               */
      10             : /*                With the U. S. Department of Energy               */
      11             : /*                                                                  */
      12             : /*             Prepared by Battelle Energy Alliance, LLC            */
      13             : /*               Under Contract No. DE-AC07-05ID14517               */
      14             : /*                With the U. S. Department of Energy               */
      15             : /*                                                                  */
      16             : /*                 See LICENSE for full restrictions                */
      17             : /********************************************************************/
      18             : 
      19             : #ifdef ENABLE_OPENMC_COUPLING
      20             : 
      21             : #include "OpenMCCellAverageProblem.h"
      22             : 
      23             : #include "DelimitedFileReader.h"
      24             : #include "DisplacedProblem.h"
      25             : #include "TallyBase.h"
      26             : #include "CellTally.h"
      27             : #include "AddTallyAction.h"
      28             : #include "SetupMGXSAction.h"
      29             : #include "OpenMCVolumeCalculation.h"
      30             : #include "CreateDisplacedProblemAction.h"
      31             : #include "CriticalitySearchBase.h"
      32             : 
      33             : #include "openmc/constants.h"
      34             : #include "openmc/cross_sections.h"
      35             : #include "openmc/dagmc.h"
      36             : #include "openmc/error.h"
      37             : #include "openmc/lattice.h"
      38             : #include "openmc/particle.h"
      39             : #include "openmc/photon.h"
      40             : #include "openmc/message_passing.h"
      41             : #include "openmc/mgxs_interface.h"
      42             : #include "openmc/nuclide.h"
      43             : #include "openmc/random_lcg.h"
      44             : #include "openmc/settings.h"
      45             : #include "openmc/summary.h"
      46             : #include "openmc/tallies/trigger.h"
      47             : #include "openmc/volume_calc.h"
      48             : #include "openmc/universe.h"
      49             : 
      50             : registerMooseObject("CardinalApp", OpenMCCellAverageProblem);
      51             : 
      52             : bool OpenMCCellAverageProblem::_first_transfer = true;
      53             : bool OpenMCCellAverageProblem::_printed_initial = false;
      54             : bool OpenMCCellAverageProblem::_printed_triso_warning = false;
      55             : 
      56             : InputParameters
      57        4317 : OpenMCCellAverageProblem::validParams()
      58             : {
      59        4317 :   InputParameters params = OpenMCProblemBase::validParams();
      60        8634 :   params.addParam<bool>("output_cell_mapping",
      61        8634 :                         true,
      62             :                         "Whether to automatically output the mapping from OpenMC cells to the "
      63             :                         "[Mesh], usually for diagnostic purposes");
      64             : 
      65        8634 :   params.addParam<MooseEnum>(
      66             :       "initial_properties",
      67        8634 :       getInitialPropertiesEnum(),
      68             :       "Where to read the temperature and density initial conditions for OpenMC");
      69             : 
      70        8634 :   params.addParam<bool>("export_properties",
      71        8634 :                         false,
      72             :                         "Whether to export OpenMC's temperature and density properties to an HDF5 "
      73             :                         "file after updating them from MOOSE.");
      74        8634 :   params.addParam<bool>(
      75             :       "normalize_by_global_tally",
      76        8634 :       true,
      77             :       "Whether to normalize local tallies by a global tally (true) or else by the sum "
      78             :       "of the local tally (false)");
      79        8634 :   params.addParam<bool>("assume_separate_tallies",
      80        8634 :                         false,
      81             :                         "Whether to assume that all tallies added in the XML files or by Cardinal "
      82             :                         "are spatially separate. This is a performance optimization");
      83             : 
      84             :   MooseEnum scores_heat(
      85        8634 :       "heating heating_local kappa_fission fission_q_prompt fission_q_recoverable");
      86        8634 :   params.addParam<MooseEnum>(
      87             :       "source_rate_normalization",
      88             :       scores_heat,
      89             :       "Score to use for computing the "
      90             :       "particle source rate (source/sec) for a certain tallies in "
      91             :       "eigenvalue mode. In other words, the "
      92             :       "source/sec is computed as (power divided by the global value of this tally)");
      93        8634 :   params.addParam<std::string>(
      94             :       "normalization_tally",
      95             :       "The name of a tally added in [Talliies] to be used when normalizing results in "
      96             :       "eigenvalue calculations. This tally object must contain the score specified in "
      97             :       "'source_rate_normalization'.");
      98             : 
      99        8634 :   params.addParam<MooseEnum>(
     100             :       "k_trigger",
     101        8634 :       getTallyTriggerEnum(),
     102             :       "Trigger criterion to determine when OpenMC simulation is complete based on k");
     103        8634 :   params.addRangeCheckedParam<Real>(
     104             :       "k_trigger_threshold", "k_trigger_threshold > 0", "Threshold for the k trigger");
     105        8634 :   params.addRangeCheckedParam<unsigned int>(
     106             :       "max_batches", "max_batches > 0", "Maximum number of batches, when using triggers");
     107       12951 :   params.addRangeCheckedParam<unsigned int>(
     108        8634 :       "batch_interval", 1, "batch_interval > 0", "Trigger batch interval");
     109             : 
     110        8634 :   params.addParam<std::vector<std::vector<std::string>>>(
     111             :       "temperature_variables",
     112             :       "Vector of variable names corresponding to the temperatures sent into OpenMC. Each entry "
     113             :       "maps to "
     114             :       "the corresponding entry in 'temperature_blocks.' If not specified, each entry defaults to "
     115             :       "'temp'");
     116        8634 :   params.addParam<std::vector<std::vector<SubdomainName>>>(
     117             :       "temperature_blocks",
     118             :       "Blocks corresponding to each of the 'temperature_variables'. If not specified, "
     119             :       "there will be no temperature feedback to OpenMC.");
     120             : 
     121        8634 :   params.addParam<std::vector<std::vector<std::string>>>(
     122             :       "density_variables",
     123             :       "Vector of variable names corresponding to the densities sent into OpenMC. Each entry maps "
     124             :       "to the corresponding entry in 'density_blocks.' If not specified, each entry defaults to "
     125             :       "'density'");
     126        8634 :   params.addParam<std::vector<std::vector<SubdomainName>>>(
     127             :       "density_blocks",
     128             :       "Blocks corresponding to each of the 'density_variables'. If not specified, "
     129             :       "there will be no density feedback to OpenMC.");
     130             : 
     131        8634 :   params.addParam<unsigned int>("cell_level",
     132             :                                 "Coordinate level in OpenMC (across the entire geometry) to use "
     133             :                                 "for identifying cells");
     134        8634 :   params.addParam<unsigned int>(
     135             :       "lowest_cell_level",
     136             :       "Lowest coordinate level in OpenMC to use for identifying cells. The cell level for coupling "
     137             :       "will use the value set with this parameter unless the geometry does not have that many "
     138             :       "layers of geometry nesting, in which case the locally lowest depth is used");
     139             : 
     140        8634 :   params.addParam<std::vector<SubdomainName>>(
     141             :       "identical_cell_fills",
     142             :       "Blocks on which the OpenMC cells have identical fill universes; this is an optimization to "
     143             :       "speed up initialization for TRISO problems while also reducing memory usage. It is assumed "
     144             :       "that any cell which maps to one of these subdomains has exactly the same universe filling "
     145             :       "it as all other cells which map to these subdomains. We HIGHLY recommend that the first "
     146             :       "time you try using this, that you also set 'check_identical_cell_fills = true' to catch "
     147             :       "any possible user errors which would exclude you from using this option safely.");
     148        8634 :   params.addParam<bool>(
     149             :       "check_identical_cell_fills",
     150        8634 :       false,
     151             :       "Whether to check that your model does indeed have identical cell fills, allowing "
     152             :       "you to set 'identical_cell_fills' to speed up initialization");
     153             : 
     154        8634 :   params.addParam<MooseEnum>(
     155        8634 :       "relaxation", getRelaxationEnum(), "Type of relaxation to apply to the OpenMC solution");
     156       12951 :   params.addRangeCheckedParam<Real>("relaxation_factor",
     157        8634 :                                     0.5,
     158             :                                     "relaxation_factor > 0.0 & relaxation_factor < 2.0",
     159             :                                     "Relaxation factor for use with constant relaxation");
     160        8634 :   params.addParam<int>("first_iteration_particles",
     161             :                        "Number of particles to use for first iteration "
     162             :                        "when using Dufek-Gudowski relaxation");
     163             : 
     164        8634 :   params.addParam<UserObjectName>(
     165             :       "symmetry_mapper",
     166             :       "User object (of type SymmetryPointGenerator) "
     167             :       "to map from a symmetric OpenMC model to a full-domain [Mesh]. For example, you can use this "
     168             :       "to map from a quarter-symmetric OpenMC model to a whole-domain [Mesh].");
     169             : 
     170        8634 :   params.addParam<UserObjectName>(
     171             :       "volume_calculation",
     172             :       "User object that will perform a stochastic volume calculation to get the OpenMC "
     173             :       "cell volumes. This can be used to check that the MOOSE regions to which the cells map are "
     174             :       "of approximately the same volume as the true cells.");
     175        8634 :   params.addParam<UserObjectName>("skinner",
     176             :                                   "When using DAGMC geometries, an optional skinner that will "
     177             :                                   "regenerate the OpenMC geometry on-the-fly according to "
     178             :                                   "iso-contours of temperature and density");
     179        4317 :   params.addClassDescription(
     180             :       "Couple OpenMC to MOOSE through cell-averaged temperature, density, and tallies.");
     181             : 
     182        4317 :   return params;
     183        4317 : }
     184             : 
     185        2175 : OpenMCCellAverageProblem::OpenMCCellAverageProblem(const InputParameters & params)
     186             :   : OpenMCProblemBase(params),
     187        2163 :     _serialized_solution(_aux->serializedSolution()),
     188        4326 :     _output_cell_mapping(getParam<bool>("output_cell_mapping")),
     189        2163 :     _initial_condition(
     190        2163 :         getParam<MooseEnum>("initial_properties").getEnum<coupling::OpenMCInitialCondition>()),
     191        4326 :     _relaxation(getParam<MooseEnum>("relaxation").getEnum<relaxation::RelaxationEnum>()),
     192        4326 :     _k_trigger(getParam<MooseEnum>("k_trigger").getEnum<trigger::TallyTriggerTypeEnum>()),
     193        4326 :     _export_properties(getParam<bool>("export_properties")),
     194        4326 :     _using_skinner(isParamValid("skinner")),
     195             :     // 'used_displaced' is added to '_need_to_reinit_coupling' later in the ctor.
     196        2163 :     _need_to_reinit_coupling(_has_adaptivity || _using_skinner),
     197        2163 :     _has_identical_cell_fills(params.isParamSetByUser("identical_cell_fills")),
     198        4326 :     _check_identical_cell_fills(getParam<bool>("check_identical_cell_fills")),
     199        4326 :     _assume_separate_tallies(getParam<bool>("assume_separate_tallies")),
     200        2163 :     _specified_density_feedback(params.isParamSetByUser("density_blocks")),
     201        2163 :     _specified_temperature_feedback(params.isParamSetByUser("temperature_blocks")),
     202        2163 :     _needs_to_map_cells(_specified_density_feedback || _specified_temperature_feedback),
     203        2163 :     _volume_calc(nullptr),
     204        2163 :     _symmetry(nullptr),
     205        5984 :     _initial_num_openmc_surfaces(openmc::model::surfaces.size())
     206             : {
     207        2163 :   const auto & subdomains = mesh().meshSubdomains();
     208        5578 :   for (const auto & s : subdomains)
     209        3417 :     if (mesh().getCoordSystem(s) == Moose::COORD_RZ)
     210           2 :       mooseError(
     211             :           "OpenMC coupling to axisymmetric meshes is not yet supported! Please convert your mesh "
     212             :           "block to a 3-D mesh (you may still use axisymmetric meshes for your other physics "
     213             :           "coupled to OpenMC and transfer data between those apps and a 3-D OpenMC model. You just "
     214             :           "cannot use an axisymmetric mesh from which OpenMC reads/writes data).");
     215             : 
     216        2161 :   if (_specified_temperature_feedback && openmc::settings::temperature_range[1] == 0.0)
     217           2 :     mooseWarning("For multiphysics simulations, we recommend setting the 'temperature_range' in "
     218             :                  "OpenMC's settings.xml file. This will pre-load nuclear data over a range of "
     219             :                  "temperatures, instead of only the temperatures defined in the XML file.\n\nFor "
     220             :                  "efficiency purposes, OpenMC only checks that cell temperatures are within the "
     221             :                  "global min/max of loaded data, which can be different from data loaded for each "
     222             :                  "nuclide. Run may abort suddenly if requested nuclear data is not available.");
     223             : 
     224             :   // Check to see if a displaced problem is being initialized
     225             :   const auto & dis_actions =
     226        2159 :       getMooseApp().actionWarehouse().getActions<CreateDisplacedProblemAction>();
     227        4318 :   for (const auto & act : dis_actions)
     228             :   {
     229        2159 :     auto displacements = act->isParamValid("displacements");
     230        4318 :     auto use = act->getParam<bool>("use_displaced_mesh");
     231        2159 :     _use_displaced = displacements && use;
     232             : 
     233             :     // print a warning if the user added displacements, but are not using them
     234        2159 :     if (!use && displacements)
     235           0 :       mooseWarning("When 'use_displaced_mesh' is false, the 'displacements' are unused!");
     236             : 
     237        6477 :     if (act->isParamSetByUser("use_displaced_mesh") && use && !displacements)
     238           0 :       mooseWarning("When 'use_displaced_mesh' is true, but no 'displacements' are provided, then "
     239             :                    "the displaced mesh will not be used.");
     240             : 
     241        2159 :     _need_to_reinit_coupling |= _use_displaced;
     242             :   }
     243             : 
     244             :   // Look through the list of AddTallyActions to see if we have a CellTally. If so, we need to map
     245             :   // cells.
     246        2159 :   const auto & tally_actions = getMooseApp().actionWarehouse().getActions<AddTallyAction>();
     247        4407 :   for (const auto & act : tally_actions)
     248        2248 :     _has_cell_tallies |= act->getMooseObjectType() == "CellTally";
     249             : 
     250             :   // Repeat the same check for SetUpMGXSActions.
     251        2159 :   const auto & mgxs_actions = getMooseApp().actionWarehouse().getActions<SetupMGXSAction>();
     252        2217 :   for (const auto & act : mgxs_actions)
     253          58 :     _has_cell_tallies |= act->addingCellTallies();
     254        2159 :   _needs_to_map_cells |= _has_cell_tallies;
     255             : 
     256        2159 :   if (!_needs_to_map_cells)
     257         436 :     checkUnusedParam(params,
     258             :                      "output_cell_mapping",
     259             :                      "'temperature_blocks', 'density_blocks', and 'tally_blocks' are empty");
     260             : 
     261        2159 :   if (!_specified_temperature_feedback && !_specified_density_feedback)
     262        1242 :     checkUnusedParam(
     263             :         params, "initial_properties", "'temperature_blocks' and 'density_blocks' are unused");
     264             : 
     265             :   // We need to clear and re-initialize OpenMC problem in the cases of:
     266             :   //   - the [Mesh] is being adaptively refined
     267             :   //   - the [Mesh] is deforming in space
     268             :   //
     269             :   // If the [Mesh] is changing, then we certainly know that the mesh tallies
     270             :   // need to be re-initialized because (a) for file-based mesh tallies, we need
     271             :   // to enforce that the mesh is identical to the [Mesh] and (b) for directly
     272             :   // tallying on the [Mesh], we need to pass that mesh info into OpenMC. For good
     273             :   // measure, we also need to re-initialize cell tallies because it's possible
     274             :   // that as the [Mesh] changes, the mapping from OpenMC cells to the [Mesh]
     275             :   // also changes, which could open the door to new cell IDs/instances being added
     276             :   // to the cell instance filter. If we need to re-init tallies, then we can't
     277             :   // guarantee that the tallies from iteration to iteration correspond to exactly
     278             :   // the same number of bins or to exactly the same regions of space, so we must
     279             :   // disable relaxation.
     280        2159 :   if ((_use_displaced || _has_adaptivity) && _relaxation != relaxation::none)
     281           4 :     paramError(
     282             :         "relaxation",
     283             :         "When adaptivity is requested or a displaced problem is used, the mapping from the "
     284             :         "OpenMC model to the [Mesh] may vary in time. This means that we have no guarantee that "
     285             :         "the "
     286             :         "number of tally bins (or even the regions of space corresponding to each bin) are fixed. "
     287             :         "Therefore, it is not possible to apply relaxation to the OpenMC tallies because you might "
     288             :         "end up trying to add vectors of different length (and possibly spatial mapping).");
     289             : 
     290        2155 :   if (_run_mode == openmc::RunMode::FIXED_SOURCE)
     291         226 :     checkUnusedParam(params, "normalize_by_global_tally", "running OpenMC in fixed source mode");
     292             : 
     293        2153 :   if (_run_mode != openmc::RunMode::EIGENVALUE && _k_trigger != trigger::none)
     294           2 :     paramError("k_trigger",
     295             :                "Cannot specify a 'k_trigger' for OpenMC runs that are not eigenvalue mode!");
     296             : 
     297             :   // determine the number of particles set either through XML or the wrapping
     298        2151 :   if (_relaxation == relaxation::dufek_gudowski)
     299             :   {
     300          32 :     checkUnusedParam(params, "particles", "using Dufek-Gudowski relaxation");
     301          32 :     checkRequiredParam(params, "first_iteration_particles", "using Dufek-Gudowski relaxation");
     302          32 :     openmc::settings::n_particles = getParam<int>("first_iteration_particles");
     303          32 :     _n_particles_1 = getParam<int>("first_iteration_particles");
     304             :   }
     305             :   else
     306        4270 :     checkUnusedParam(params, "first_iteration_particles", "not using Dufek-Gudowski relaxation");
     307             : 
     308             :     // OpenMC will throw an error if the geometry contains DAG universes but OpenMC wasn't compiled
     309             :     // with DAGMC. So we can assume that if we have a DAGMC geometry, that we will also by this
     310             :     // point have DAGMC enabled.
     311             : #ifdef ENABLE_DAGMC
     312             :   bool has_csg;
     313             :   bool has_dag;
     314        1100 :   geometryType(has_csg, has_dag);
     315             : 
     316        1100 :   if (!has_dag)
     317        2105 :     checkUnusedParam(
     318             :         params, "skinner", "the OpenMC model does not contain any DagMC universes", true);
     319          47 :   else if (_using_skinner)
     320             :   {
     321             :     // Loop over all universes to find the DAGMC universe and to check and make sure we only have
     322             :     // the one.
     323             :     unsigned int num_dag_universes = 0;
     324          75 :     for (const auto & universe : openmc::model::universes)
     325             :     {
     326          43 :       if (universe->geom_type() == openmc::GeometryType::DAG)
     327             :       {
     328          33 :         _dagmc_universe_id = universe->id_;
     329          33 :         num_dag_universes++;
     330             :       }
     331             :     }
     332             : 
     333          32 :     if (num_dag_universes != 1)
     334           2 :       mooseError("The 'skinner' can only be used when the OpenMC geometry contains a single DAGMC "
     335             :                  "universe.\n"
     336           1 :                  "Your geometry contains " +
     337           0 :                  Moose::stringify(num_dag_universes) + " DAGMC universes.");
     338             : 
     339             :     // Loop over each element of each lattice to make sure that it doesn't contain the DAGMC
     340             :     // universe.
     341          31 :     for (const auto & lattice : openmc::model::lattices)
     342             :     {
     343           3 :       for (openmc::LatticeIter it = lattice->begin(); it != lattice->end(); ++it)
     344           2 :         if (openmc::model::universes[*it]->id_ == _dagmc_universe_id)
     345           1 :           mooseError("The 'skinner' cannot be used when the DAGMC universe is contained in lattice "
     346             :                      "geometry.");
     347             : 
     348           1 :       if (lattice->outer_ != openmc::NO_OUTER_UNIVERSE &&
     349           1 :           openmc::model::universes[lattice->outer_]->id_ == _dagmc_universe_id)
     350           1 :         mooseError("The 'skinner' cannot be used when the DAGMC universe is used as the outer "
     351             :                    "universe of a lattice.");
     352             :     }
     353             : 
     354             :     // Need to make sure that there is only a single cell which uses the DAGMC universe as it's
     355             :     // fill. The root universe must contain that cell, otherwise the DAGMC universe may be
     356             :     // replicated across the problem.
     357             :     unsigned int num_dag_instances = 0;
     358         122 :     for (const auto & cell : openmc::model::cells)
     359             :     {
     360          93 :       if (cell->type_ == openmc::Fill::UNIVERSE &&
     361           7 :           cell->fill_ == openmc::model::universe_map.at(_dagmc_universe_id))
     362             :       {
     363           6 :         _dagmc_root_universe = false;
     364           6 :         num_dag_instances++;
     365           6 :         _cell_using_dagmc_universe_id = cell->id_;
     366             :       }
     367             :     }
     368             : 
     369          29 :     if (num_dag_instances > 1)
     370           2 :       mooseError("The 'skinner' can only be used when the DAGMC universe in the OpenMC geometry is "
     371             :                  "used as a cell "
     372           1 :                  "fill at most once.\n Your geometry contains " +
     373           0 :                  Moose::stringify(num_dag_instances) +
     374             :                  " cells which "
     375             :                  "use the DAGMC universe as their fill.");
     376             : 
     377          28 :     if (!_dagmc_root_universe &&
     378           4 :         openmc::model::cells[openmc::model::cell_map.at(_cell_using_dagmc_universe_id)]
     379           4 :                 ->universe_ != openmc::model::root_universe)
     380           1 :       mooseError("The 'skinner' can only be used when the cell using the DAGMC universe as a fill "
     381             :                  "is contained in the "
     382             :                  "root universe.");
     383             :   }
     384             : #else
     385        2102 :   checkUnusedParam(
     386             :       params, "skinner", "DAGMC geometries in OpenMC are not enabled in this build of Cardinal");
     387             : #endif
     388             : 
     389        2145 :   if (_relaxation != relaxation::constant)
     390        3990 :     checkUnusedParam(params, "relaxation_factor", "not using constant relaxation");
     391             : 
     392        2145 :   readBlockParameters("identical_cell_fills", _identical_cell_fill_blocks);
     393             : 
     394        2145 :   if (!_has_identical_cell_fills)
     395        4238 :     checkUnusedParam(
     396             :         params, "check_identical_cell_fills", "'identical_cell_fills' is not specified");
     397             : 
     398        4280 :   readBlockVariables("temperature", "temp", _temp_vars_to_blocks, _temp_blocks);
     399        4262 :   readBlockVariables("density", "density", _density_vars_to_blocks, _density_blocks);
     400             : 
     401        2161 :   for (const auto & i : _identical_cell_fill_blocks)
     402          36 :     if (std::find(_density_blocks.begin(), _density_blocks.end(), i) != _density_blocks.end())
     403           2 :       paramError(
     404             :           "identical_cell_fills",
     405             :           "Entries in 'identical_cell_fills' cannot be contained in 'density_blocks'; the\n"
     406             :           "identical fill universe optimization is not yet implemented for density feedback.");
     407             : 
     408        2125 :   if (_needs_to_map_cells)
     409             :   {
     410        5727 :     if (isParamValid("cell_level") == isParamValid("lowest_cell_level"))
     411           1 :       mooseError("Either 'cell_level' or 'lowest_cell_level' must be specified. You have given "
     412             :                  "either both or none.");
     413             : 
     414             :     std::string selected_param;
     415        3816 :     if (isParamValid("cell_level"))
     416             :     {
     417        3728 :       _cell_level = getParam<unsigned int>("cell_level");
     418             :       selected_param = "cell_level";
     419             : 
     420        1864 :       if (_cell_level >= openmc::model::n_coord_levels)
     421           4 :         paramError(selected_param,
     422             :                    "Coordinate level for finding cells cannot be greater than total number "
     423           2 :                    "of coordinate levels: " +
     424           0 :                        Moose::stringify(openmc::model::n_coord_levels) + "!");
     425             :     }
     426             :     else
     427             :     {
     428          88 :       _cell_level = getParam<unsigned int>("lowest_cell_level");
     429             :       selected_param = "lowest_cell_level";
     430             :     }
     431             :   }
     432             :   else
     433             :   {
     434         432 :     checkUnusedParam(params,
     435             :                      "cell_level",
     436             :                      "'temperature_blocks', 'density_blocks', and 'tally_blocks' are empty");
     437         432 :     checkUnusedParam(params,
     438             :                      "lowest_cell_level",
     439             :                      "'temperature_blocks', 'density_blocks', and 'tally_blocks' are empty");
     440             :   }
     441        2122 : }
     442             : 
     443             : const MooseMesh &
     444      943408 : OpenMCCellAverageProblem::getMooseMesh() const
     445             : {
     446      943408 :   return mesh(_use_displaced);
     447             : }
     448             : 
     449             : MooseMesh &
     450    30098215 : OpenMCCellAverageProblem::getMooseMesh()
     451             : {
     452             :   // TODO: this could go into MOOSE framework directly
     453    30098215 :   if (_use_displaced && !_displaced_problem)
     454           0 :     mooseWarning("Displaced mesh was requested but the displaced problem does not exist. "
     455             :                  "Regular mesh will be returned");
     456             : 
     457    30098215 :   MooseMesh & m = ((_use_displaced && _displaced_problem) ? _displaced_problem->mesh() : mesh());
     458    30098215 :   return m;
     459             : }
     460             : 
     461             : void
     462        4280 : OpenMCCellAverageProblem::readBlockVariables(
     463             :     const std::string & param,
     464             :     const std::string & default_name,
     465             :     std::map<std::string, std::vector<SubdomainName>> & vars_to_specified_blocks,
     466             :     std::vector<SubdomainID> & specified_blocks)
     467             : {
     468        4280 :   std::string b = param + "_blocks";
     469        4280 :   std::string v = param + "_variables";
     470             : 
     471        4280 :   if (!isParamValid(b))
     472             :   {
     473        6771 :     checkUnusedParam(parameters(), v, "not setting '" + b + "'");
     474             :     return;
     475             :   }
     476             : 
     477             :   std::vector<std::vector<SubdomainName>> blocks;
     478        4036 :   read2DBlockParameters(b, blocks, specified_blocks);
     479             : 
     480             :   // now, get the names of those temperature variables
     481             :   std::vector<std::vector<std::string>> vars;
     482        2013 :   if (isParamValid(v))
     483             :   {
     484          64 :     vars = getParam<std::vector<std::vector<std::string>>>(v);
     485             : 
     486         192 :     checkEmptyVector(vars, "'" + v + "");
     487         224 :     for (const auto & t : vars)
     488         480 :       checkEmptyVector(t, "Entries in '" + v + "'");
     489             : 
     490          64 :     if (vars.size() != blocks.size())
     491          24 :       mooseError("'" + v + "' and '" + b + "' must be the same length!\n'" + v + "' is of length " +
     492          12 :                  std::to_string(vars.size()) + " and '" + b + "' is of length " +
     493           4 :                  std::to_string(blocks.size()));
     494             : 
     495             :     // TODO: for now, we restrict each set of blocks to map to a single temperature variable
     496         204 :     for (std::size_t i = 0; i < vars.size(); ++i)
     497         148 :       if (vars[i].size() > 1)
     498          12 :         mooseError("Each entry in '" + v + "' must be of length 1. Entry " + std::to_string(i) +
     499           8 :                    " is of length " + std::to_string(vars[i].size()));
     500             :   }
     501             :   else
     502             :   {
     503             :     // set a reasonable default, if not specified
     504        1949 :     vars.resize(blocks.size(), std::vector<std::string>(1));
     505        3898 :     for (std::size_t i = 0; i < blocks.size(); ++i)
     506             :       vars[i][0] = default_name;
     507             :   }
     508             : 
     509        4098 :   for (std::size_t i = 0; i < vars.size(); ++i)
     510        5045 :     for (std::size_t j = 0; j < blocks[i].size(); ++j)
     511        2952 :       vars_to_specified_blocks[vars[i][0]].push_back(blocks[i][j]);
     512        2005 : }
     513             : 
     514             : void
     515        1952 : OpenMCCellAverageProblem::initialSetup()
     516             : {
     517        1952 :   OpenMCProblemBase::initialSetup();
     518             : 
     519        1952 :   getOpenMCUserObjects();
     520             : 
     521        1948 :   if (_use_displaced && !_using_skinner && !hasCellTransform())
     522          14 :     mooseWarning("Your problem has a moving mesh, but you have not provided a 'skinner' or an "
     523             :                  "OpenMCCellTransform user object (both of which move the OpenMC geometry). The "
     524             :                  "[Mesh] will move, but the underlying OpenMC geometry will remain unchanged. "
     525             :                  "Unexpected behavior may occur.");
     526             : 
     527             :   // Coupling re-initialization should be triggered if we have cell transforms which can happen
     528             :   // even if the mesh isn't moving or adaptive.
     529        1947 :   _need_to_reinit_coupling |= hasCellTransform();
     530             :   // The criticality search may modify the geometry.
     531        1947 :   if (_criticality_search)
     532          76 :     _need_to_reinit_coupling |= _criticality_search->changingGeometry();
     533             : 
     534        1947 :   if (!_needs_to_map_cells)
     535         386 :     checkUnusedParam(parameters(),
     536             :                      "volume_calculation",
     537             :                      "'temperature_blocks', 'density_blocks', and 'tally_blocks' are empty");
     538        3508 :   else if (isParamValid("volume_calculation"))
     539             :   {
     540         103 :     const auto & name = getParam<UserObjectName>("volume_calculation");
     541         103 :     auto * base = &getUserObject<UserObject>(name);
     542             : 
     543         103 :     _volume_calc = dynamic_cast<OpenMCVolumeCalculation *>(base);
     544             : 
     545         103 :     if (!_volume_calc)
     546           0 :       paramError("volume_calculation",
     547             :                  "The 'volume_calculation' user object must be of type "
     548             :                  "OpenMCVolumeCalculation!");
     549             :   }
     550             : 
     551        3894 :   if (isParamValid("symmetry_mapper"))
     552             :   {
     553          43 :     const auto & name = getParam<UserObjectName>("symmetry_mapper");
     554          43 :     auto base = &getUserObject<UserObject>(name);
     555             : 
     556          43 :     _symmetry = dynamic_cast<SymmetryPointGenerator *>(base);
     557             : 
     558          43 :     if (!_symmetry)
     559           2 :       paramError("symmetry_mapper",
     560             :                  "The 'symmetry_mapper' user object has to be of type SymmetryPointGenerator!");
     561             :   }
     562             : 
     563             :   // Get triggers.
     564        1945 :   getTallyTriggerParameters(_pars);
     565             : 
     566        1943 :   setupProblem();
     567             : 
     568             : #ifdef ENABLE_DAGMC
     569         968 :   if (_using_skinner)
     570             :   {
     571          26 :     std::set<SubdomainID> t(_temp_blocks.begin(), _temp_blocks.end());
     572          26 :     std::set<SubdomainID> d(_density_blocks.begin(), _density_blocks.end());
     573             : 
     574          26 :     if (t != getMooseMesh().meshSubdomains())
     575           0 :       paramError("temperature_blocks",
     576             :                  "The 'skinner' requires temperature feedback to be applied over the entire mesh. "
     577             :                  "Please update `temperature_blocks` to include all blocks.");
     578             : 
     579          26 :     if (d != getMooseMesh().meshSubdomains() && _specified_density_feedback)
     580           0 :       paramError("density_blocks",
     581             :                  "The 'skinner' requires density feedback to be applied over the entire mesh. "
     582             :                  "Please update `density_blocks` to include all blocks.");
     583             : 
     584          26 :     if (t != d && _specified_density_feedback)
     585           0 :       mooseError("The 'skinner' will apply skinning over the entire domain, and requires that the "
     586             :                  "entire problem uses identical settings for feedback. Please update "
     587             :                  "'temperature_blocks' and 'density_blocks' to include all blocks.");
     588             : 
     589          26 :     if (_symmetry)
     590           1 :       mooseError("Cannot combine the 'skinner' with 'symmetry_mapper'!\n\nWhen using a skinner, "
     591             :                  "the [Mesh] must exactly match the underlying OpenMC model, so there is\n"
     592             :                  "no need to transform spatial coordinates to map between OpenMC and the [Mesh].");
     593             : 
     594             :     // Rudimentary error checking to make sure all non-void DAGMC cells are mapped. This helps catch
     595             :     // errors where the skinned MOOSE mesh deletes DAGMC geometry. Also error if the user is
     596             :     // attempting to use a skinner when mapping both CSG cells and DAGMC geometry to the MOOSE mesh.
     597             :     // The skinner is currently not set up to ignore elements that map to cells and will generate
     598             :     // DAGMC geometry that overlaps with pre-existing CSG cells.
     599             :     // TODO: This would be nice to fix, but would require a rework of the skinner.
     600             :     std::set<int32_t> mapped_dag_cells;
     601          98 :     for (const auto & c : openmc::model::cells)
     602             :     {
     603         211 :       for (const auto & [c_info, elem] : _cell_to_elem)
     604             :       {
     605         138 :         if (c->geom_type() == openmc::GeometryType::DAG &&
     606         133 :             c_info.first == openmc::model::cell_map.at(c->id_))
     607          44 :           mapped_dag_cells.insert(c->id_);
     608          94 :         else if (c->geom_type() == openmc::GeometryType::CSG &&
     609           5 :                  c_info.first == openmc::model::cell_map.at(c->id_))
     610           1 :           mooseError("At present, the 'skinner' can only be used when the only OpenMC geometry "
     611             :                      "which maps to the MOOSE mesh is DAGMC geometry. Your geometry contains CSG "
     612             :                      "cells which map to the MOOSE mesh.");
     613             :       }
     614             :     }
     615             : 
     616             :     unsigned int num_unmapped = 0;
     617             :     unsigned int num_dag_cells = 0;
     618          97 :     for (const auto & c : openmc::model::cells)
     619             :     {
     620             :       auto no_void =
     621          73 :           std::find(c->material_.begin(), c->material_.end(), MATERIAL_VOID) == c->material_.end();
     622          73 :       if (mapped_dag_cells.count(c->id_) == 0 && c->geom_type() == openmc::GeometryType::DAG &&
     623             :           no_void)
     624           1 :         num_unmapped++;
     625          73 :       if (c->geom_type() == openmc::GeometryType::DAG)
     626          71 :         num_dag_cells++;
     627             :     }
     628             : 
     629          24 :     if (num_unmapped > 0)
     630           2 :       mooseWarning("Your DAGMC geometry contains unmapped cells! The skinner assumes that "
     631             :                    "the DAG geometry used in the OpenMC model maps one to one to the mesh "
     632             :                    "mirror; if that is not the case the skinner may delete some parts of "
     633           1 :                    "your OpenMC model when the underlying geometry is regenerated. You have " +
     634           1 :                    Moose::stringify(num_unmapped) + " unmapped DAGMC cells out of " +
     635           0 :                    Moose::stringify(num_dag_cells) + " DAGMC cells.");
     636             : 
     637          23 :     const auto & name = getParam<UserObjectName>("skinner");
     638          23 :     auto base = &getUserObject<UserObject>(name);
     639             : 
     640          23 :     _skinner = dynamic_cast<MoabSkinner *>(base);
     641             : 
     642          23 :     if (!_skinner)
     643           1 :       paramError("skinner", "The 'skinner' user object must be of type MoabSkinner!");
     644             : 
     645          22 :     if (_skinner->hasDensitySkinning() != _specified_density_feedback)
     646           1 :       mooseError(
     647             :           "Detected inconsistent settings for density skinning and 'density_blocks'. If applying "
     648             :           "density feedback with 'density_blocks', then you must apply density skinning in the '",
     649             :           name,
     650             :           "' user object (and vice versa)");
     651             : 
     652          21 :     if (_initial_condition == coupling::hdf5)
     653           1 :       paramError("initial_properties",
     654             :                  "Cannot load initial temperature and density properties from "
     655             :                  "HDF5 files because there is no guarantee that the geometry (which is adaptively "
     656             :                  "changing) matches "
     657             :                  "that used to write the HDF5 file.");
     658             : 
     659             :     // If the DAGMC universe is the root universe the geometry contains no CSG cells. We need
     660             :     // to force the skinner to add a graveyard as the problem will contain no boundary contitions
     661             :     // after skinning is performed. If there are CSG cells in the geometry, this is not the case
     662             :     // as the DAGMC universe is embedded in a cell (which applies boundary conditions).
     663          20 :     if (_dagmc_root_universe)
     664          18 :       _skinner->setGraveyard(true);
     665             : 
     666          19 :     _skinner->setScaling(_scaling);
     667          19 :     _skinner->setVerbosity(_verbose);
     668          19 :     _skinner->makeDependentOnExternalAction();
     669          19 :     _skinner->setUseDisplacedMesh(_use_displaced);
     670             : 
     671             :     // the skinner expects that there is one OpenMC material per subdomain (otherwise this
     672             :     // indicates that our [Mesh] doesn't match the .h5m model, because DAGMC itself imposes
     673             :     // the one-material-per-cell case. In the future, if we generate DAGMC models directly
     674             :     // from the [Mesh] (bypassing the .h5m), we would not need this error check.
     675          19 :     _skinner->setMaterialNames(getMaterialInEachSubdomain());
     676          18 :     _skinner->initialize();
     677             :   }
     678             : #endif
     679        1889 : }
     680             : 
     681             : std::vector<std::string>
     682          19 : OpenMCCellAverageProblem::getMaterialInEachSubdomain() const
     683             : {
     684             :   std::vector<std::string> mats;
     685          55 :   for (const auto & s : _subdomain_to_material)
     686             :   {
     687          37 :     if (s.second.size() > 1)
     688             :     {
     689           1 :       std::stringstream msg;
     690             :       msg << "The 'skinner' expects to find one OpenMC material mapped to each [Mesh] subdomain, "
     691             :              "but "
     692           3 :           << Moose::stringify(s.second.size()) << " materials\nmapped to subdomain " << s.first
     693             :           << ". This indicates your [Mesh] is not "
     694           1 :           << "consistent with the .h5m model.\n\nThe materials which mapped to subdomain "
     695           1 :           << s.first << " are:\n";
     696             : 
     697           3 :       for (const auto & m : s.second)
     698           4 :         msg << "\n" << materialName(m);
     699             : 
     700           1 :       mooseError(msg.str());
     701           0 :     }
     702             : 
     703          72 :     mats.push_back(materialName(*(s.second.begin())));
     704             :   }
     705             : 
     706          18 :   return mats;
     707           0 : }
     708             : 
     709             : void
     710        2847 : OpenMCCellAverageProblem::setupProblem()
     711             : {
     712             :   // establish the local -> global element mapping for convenience
     713        2847 :   _local_to_global_elem.clear();
     714     4025063 :   for (unsigned int e = 0; e < getMooseMesh().nElem(); ++e)
     715             :   {
     716     4022216 :     const auto * elem = getMooseMesh().queryElemPtr(e);
     717     4022216 :     if (!isLocalElem(elem) || !elem->active())
     718     1928840 :       continue;
     719             : 
     720     2093376 :     _local_to_global_elem.push_back(e);
     721             :   }
     722             : 
     723        2847 :   _n_openmc_cells = numCells();
     724             : 
     725        2847 :   initializeElementToCellMapping();
     726             : 
     727        2821 :   getMaterialFills();
     728             : 
     729             :   // we do this last so that we can at least hit any other errors first before
     730             :   // spending time on the costly filled cell caching
     731        2819 :   cacheContainedCells();
     732             : 
     733             :   // save the number of contained cells for printing in every transfer if verbose
     734             :   _cell_to_n_contained.clear();
     735       16246 :   for (const auto & c : _cell_to_elem)
     736             :   {
     737       13433 :     const auto & cell_info = c.first;
     738             :     int32_t n_contained = 0;
     739             : 
     740      200784 :     for (const auto & cc : containedMaterialCells(cell_info))
     741      187351 :       n_contained += cc.second.size();
     742             : 
     743       13433 :     _cell_to_n_contained[cell_info] = n_contained;
     744             :   }
     745             : 
     746        2813 :   subdomainsToMaterials();
     747             : 
     748        2813 :   initializeTallies();
     749        2801 : }
     750             : 
     751             : void
     752        1945 : OpenMCCellAverageProblem::getTallyTriggerParameters(const InputParameters & parameters)
     753             : {
     754             :   // parameters needed for k triggers
     755             :   bool has_tally_trigger = false;
     756        1945 :   if (_k_trigger != trigger::none)
     757             :   {
     758          68 :     checkRequiredParam(parameters, "k_trigger_threshold", "using a k trigger");
     759          68 :     openmc::settings::keff_trigger.threshold = getParam<Real>("k_trigger_threshold");
     760             :     has_tally_trigger = true;
     761             :   }
     762             :   else
     763        3822 :     checkUnusedParam(parameters, "k_trigger_threshold", "not using a k trigger");
     764             : 
     765             :   // Check to see if any of the local tallies have triggers.
     766        4517 :   for (const auto & local_tally : _local_tallies)
     767        2572 :     has_tally_trigger = has_tally_trigger || local_tally->hasTrigger();
     768             : 
     769        1945 :   if (has_tally_trigger) // at least one trigger
     770             :   {
     771         106 :     openmc::settings::trigger_on = true;
     772         212 :     checkRequiredParam(parameters, "max_batches", "using triggers");
     773             : 
     774         106 :     if (_skip_statepoint)
     775           0 :       checkUnusedParam(parameters, "skip_statepoint", "using a trigger");
     776             : 
     777         212 :     int err = openmc_set_n_batches(getParam<unsigned int>("max_batches"),
     778             :                                    true /* set the max batches */,
     779         106 :                                    true /* add the last batch for statepoint writing */);
     780         106 :     catchOpenMCError(err, "set the maximum number of batches");
     781             : 
     782         208 :     openmc::settings::trigger_batch_interval = getParam<unsigned int>("batch_interval");
     783             :   }
     784             :   else
     785             :   {
     786        3678 :     checkUnusedParam(parameters, "max_batches", "not using triggers");
     787        3678 :     checkUnusedParam(parameters, "batch_interval", "not using triggers");
     788             : 
     789        1839 :     if (_skip_statepoint)
     790             :       openmc::settings::statepoint_batch.clear();
     791             :   }
     792        1943 : }
     793             : 
     794             : const TallyBase *
     795          84 : OpenMCCellAverageProblem::getTally(const std::string & name)
     796             : {
     797         192 :   for (const auto & t : _local_tallies)
     798         190 :     if (t->name() == name)
     799             :       return t.get();
     800             :   return nullptr;
     801             : }
     802             : 
     803             : std::vector<const MooseVariableFE<Real> *>
     804          70 : OpenMCCellAverageProblem::getTallyScoreVariables(const std::string & score,
     805             :                                                  const std::string & tally_name,
     806             :                                                  THREAD_ID tid,
     807             :                                                  const std::string & output,
     808             :                                                  bool skip_func_exp)
     809             : {
     810             :   std::vector<const MooseVariableFE<Real> *> score_vars;
     811         210 :   for (const auto & t : _local_tallies)
     812             :   {
     813         140 :     if (t->hasScore(score) && t->name() == tally_name)
     814             :     {
     815          70 :       auto vars = t->getScoreVars(score);
     816         140 :       for (unsigned int ext_bin = 0; ext_bin < vars.size(); ++ext_bin)
     817             :       {
     818          70 :         if (skip_func_exp && t->extBinSkipped(ext_bin))
     819           0 :           continue;
     820          70 :         score_vars.emplace_back(
     821         140 :             dynamic_cast<const MooseVariableFE<Real> *>(&getVariable(tid, vars[ext_bin] + output)));
     822             :       }
     823          70 :     }
     824             :   }
     825             : 
     826          70 :   if (score_vars.size() == 0)
     827           0 :     mooseError("No tallies contain the requested score " + score + "!");
     828             : 
     829          70 :   return score_vars;
     830           0 : }
     831             : 
     832             : std::vector<const VariableValue *>
     833          70 : OpenMCCellAverageProblem::getTallyScoreVariableValues(const std::string & score,
     834             :                                                       const std::string & tally_name,
     835             :                                                       THREAD_ID tid,
     836             :                                                       const std::string & output,
     837             :                                                       bool skip_func_exp)
     838             : {
     839             :   std::vector<const VariableValue *> score_vars;
     840         210 :   for (const auto & t : _local_tallies)
     841             :   {
     842         140 :     if (t->hasScore(score) && t->name() == tally_name)
     843             :     {
     844          70 :       auto vars = t->getScoreVars(score);
     845         140 :       for (unsigned int ext_bin = 0; ext_bin < vars.size(); ++ext_bin)
     846             :       {
     847          70 :         if (skip_func_exp && t->extBinSkipped(ext_bin))
     848           0 :           continue;
     849          70 :         score_vars.emplace_back(
     850         210 :             &(dynamic_cast<MooseVariableFE<Real> *>(&getVariable(tid, vars[ext_bin] + output))
     851          70 :                   ->sln()));
     852             :       }
     853          70 :     }
     854             :   }
     855             : 
     856          70 :   if (score_vars.size() == 0)
     857           0 :     mooseError("No tallies contain the requested score " + score + "!");
     858             : 
     859          70 :   return score_vars;
     860           0 : }
     861             : 
     862             : std::vector<const VariableValue *>
     863          12 : OpenMCCellAverageProblem::getTallyScoreNeighborVariableValues(const std::string & score,
     864             :                                                               const std::string & tally_name,
     865             :                                                               THREAD_ID tid,
     866             :                                                               const std::string & output,
     867             :                                                               bool skip_func_exp)
     868             : {
     869             :   std::vector<const VariableValue *> score_vars;
     870          40 :   for (const auto & t : _local_tallies)
     871             :   {
     872          28 :     if (t->hasScore(score) && t->name() == tally_name)
     873             :     {
     874          12 :       auto vars = t->getScoreVars(score);
     875          24 :       for (unsigned int ext_bin = 0; ext_bin < vars.size(); ++ext_bin)
     876             :       {
     877          12 :         if (skip_func_exp && t->extBinSkipped(ext_bin))
     878           0 :           continue;
     879          12 :         score_vars.emplace_back(
     880          36 :             &(dynamic_cast<MooseVariableFE<Real> *>(&getVariable(tid, vars[ext_bin] + output))
     881          12 :                   ->slnNeighbor()));
     882             :       }
     883          12 :     }
     884             :   }
     885             : 
     886          12 :   if (score_vars.size() == 0)
     887           0 :     mooseError("No tallies contain the requested score " + score + "!");
     888             : 
     889          12 :   return score_vars;
     890           0 : }
     891             : 
     892             : bool
     893          16 : OpenMCCellAverageProblem::hasOutput(const std::string & score, const std::string & output) const
     894             : {
     895          18 :   for (const auto & t : _local_tallies)
     896          30 :     if (std::find(t->getOutputs().begin(), t->getOutputs().end(), output) !=
     897          16 :             t->getOutputs().end() &&
     898          14 :         t->hasScore(score))
     899             :       return true;
     900             :   return false;
     901             : }
     902             : 
     903             : void
     904        2145 : OpenMCCellAverageProblem::readBlockParameters(const std::string name,
     905             :                                               std::unordered_set<SubdomainID> & blocks)
     906             : {
     907        2145 :   if (isParamValid(name))
     908             :   {
     909          26 :     auto names = getParam<std::vector<SubdomainName>>(name);
     910          52 :     checkEmptyVector(names, "'" + name + "'");
     911             : 
     912             :     // here, we do not use the displaced mesh because we need to call this during initial
     913             :     // setup when the displaced problem does not yet exist. However, displacing the mesh
     914             :     // should not influence the subdomain IDs anyways
     915          26 :     auto b_ids = mesh().getSubdomainIDs(names);
     916          26 :     std::copy(b_ids.begin(), b_ids.end(), std::inserter(blocks, blocks.end()));
     917          26 :     checkBlocksInMesh(name, b_ids, names);
     918          26 :   }
     919        2145 : }
     920             : 
     921             : void
     922        2043 : OpenMCCellAverageProblem::checkBlocksInMesh(const std::string name,
     923             :                                             const std::vector<SubdomainID> & ids,
     924             :                                             const std::vector<SubdomainName> & names) const
     925             : {
     926             :   // here, we do not use the displaced mesh because we need to call this during initial
     927             :   // setup when the displaced problem does not yet exist. However, displacing the mesh
     928             :   // should not influence the subdomain IDs anyways
     929        2043 :   const auto & subdomains = mesh().meshSubdomains();
     930        5063 :   for (std::size_t b = 0; b < names.size(); ++b)
     931        3020 :     if (subdomains.find(ids[b]) == subdomains.end())
     932           0 :       mooseError("Block '" + names[b] + "' specified in '" + name + "' " + "not found in mesh!");
     933        2043 : }
     934             : 
     935             : void
     936        2023 : OpenMCCellAverageProblem::read2DBlockParameters(const std::string name,
     937             :                                                 std::vector<std::vector<SubdomainName>> & names,
     938             :                                                 std::vector<SubdomainID> & flattened_ids)
     939             : {
     940        2023 :   if (isParamValid(name))
     941             :   {
     942        2023 :     names = getParam<std::vector<std::vector<SubdomainName>>>(name);
     943             : 
     944             :     // check that entire vector is not empty
     945        6067 :     checkEmptyVector(names, "'" + name + "'");
     946             : 
     947             :     // check that each entry in vector is not empty
     948        4142 :     for (const auto & n : names)
     949        6371 :       checkEmptyVector(n, "Entries in '" + name + "'");
     950             : 
     951             :     // flatten the 2-d set of names into a 1-d vector
     952             :     std::vector<SubdomainName> flattened_names;
     953        4138 :     for (const auto & slice : names)
     954        5105 :       for (const auto & i : slice)
     955        2984 :         flattened_names.push_back(i);
     956             : 
     957             :     // here, we do not use the displaced mesh because we need to call this during initial
     958             :     // setup when the displaced problem does not yet exist. However, displacing the mesh
     959             :     // should not influence the subdomain IDs anyways
     960        4034 :     flattened_ids = mesh().getSubdomainIDs(flattened_names);
     961        4034 :     checkBlocksInMesh(name, flattened_ids, flattened_names);
     962             : 
     963             :     // should not be any duplicate blocks
     964             :     std::set<SubdomainName> n;
     965        4993 :     for (const auto & b : flattened_names)
     966             :     {
     967             :       if (n.count(b))
     968           4 :         mooseError(
     969           4 :             "Subdomains cannot be repeated in '" + name + "'! Subdomain '", b, "' is duplicated.");
     970        2976 :       n.insert(b);
     971             :     }
     972        2013 :   }
     973        2013 : }
     974             : 
     975             : coupling::CouplingFields
     976     4121180 : OpenMCCellAverageProblem::elemFeedback(const Elem * elem) const
     977             : {
     978     4121180 :   const auto & id = elem->subdomain_id();
     979             :   bool has_density =
     980     4121180 :       std::find(_density_blocks.begin(), _density_blocks.end(), id) != _density_blocks.end();
     981     4121180 :   bool has_temp = std::find(_temp_blocks.begin(), _temp_blocks.end(), id) != _temp_blocks.end();
     982             : 
     983     4121180 :   if (has_density && has_temp)
     984             :     return coupling::density_and_temperature;
     985     2870842 :   else if (!has_density && has_temp)
     986             :     return coupling::temperature;
     987     1052744 :   else if (has_density && !has_temp)
     988             :     return coupling::density;
     989             :   else
     990     1050384 :     return coupling::none;
     991             : }
     992             : 
     993             : void
     994        2847 : OpenMCCellAverageProblem::storeElementPhase()
     995             : {
     996             :   std::set<SubdomainID> excl_temp_blocks;
     997             :   std::set<SubdomainID> excl_density_blocks;
     998             :   std::set<SubdomainID> intersect;
     999             : 
    1000        2847 :   std::set<SubdomainID> t(_temp_blocks.begin(), _temp_blocks.end());
    1001        2847 :   std::set<SubdomainID> d(_density_blocks.begin(), _density_blocks.end());
    1002             : 
    1003        2847 :   std::set_difference(t.begin(),
    1004             :                       t.end(),
    1005             :                       d.begin(),
    1006             :                       d.end(),
    1007             :                       std::inserter(excl_temp_blocks, excl_temp_blocks.end()));
    1008             : 
    1009        2847 :   std::set_difference(d.begin(),
    1010             :                       d.end(),
    1011             :                       t.begin(),
    1012             :                       t.end(),
    1013             :                       std::inserter(excl_density_blocks, excl_density_blocks.end()));
    1014             : 
    1015        2847 :   std::set_intersection(
    1016             :       t.begin(), t.end(), d.begin(), d.end(), std::inserter(intersect, intersect.begin()));
    1017             : 
    1018        2847 :   _n_moose_temp_density_elems = 0;
    1019        3411 :   for (const auto & s : intersect)
    1020         564 :     _n_moose_temp_density_elems += numElemsInSubdomain(s);
    1021             : 
    1022        2847 :   _n_moose_temp_elems = 0;
    1023        4646 :   for (const auto & s : excl_temp_blocks)
    1024        1799 :     _n_moose_temp_elems += numElemsInSubdomain(s);
    1025             : 
    1026        2847 :   _n_moose_density_elems = 0;
    1027        2901 :   for (const auto & s : excl_density_blocks)
    1028          54 :     _n_moose_density_elems += numElemsInSubdomain(s);
    1029             : 
    1030        2847 :   _n_moose_none_elems = getMooseMesh().getMesh().n_active_elem() - _n_moose_temp_density_elems -
    1031        2847 :                         _n_moose_temp_elems - _n_moose_density_elems;
    1032        2847 : }
    1033             : 
    1034             : void
    1035        2843 : OpenMCCellAverageProblem::computeCellMappedVolumes()
    1036             : {
    1037             :   std::vector<Real> volumes;
    1038             : 
    1039       13742 :   for (const auto & c : _local_cell_to_elem)
    1040             :   {
    1041       10899 :     Real vol = 0.0;
    1042     1973903 :     for (const auto & e : c.second)
    1043             :     {
    1044             :       // we are looping over local elements, so no need to check for nullptr
    1045     1963004 :       const auto * elem = getMooseMesh().queryElemPtr(globalElemID(e));
    1046     1963004 :       vol += elem->volume();
    1047             :     }
    1048             : 
    1049       10899 :     volumes.push_back(vol);
    1050             :   }
    1051             : 
    1052        2843 :   gatherCellSum(volumes, _cell_to_elem_volume);
    1053        2843 : }
    1054             : 
    1055             : template <typename T>
    1056             : void
    1057       15583 : OpenMCCellAverageProblem::gatherCellSum(std::vector<T> & local,
    1058             :                                         std::map<cellInfo, T> & global) const
    1059             : {
    1060             :   global.clear();
    1061       15583 :   _communicator.allgather(local);
    1062             : 
    1063      169680 :   for (unsigned int i = 0; i < _flattened_ids.size(); ++i)
    1064             :   {
    1065             :     cellInfo cell_info = {_flattened_ids[i], _flattened_instances[i]};
    1066             : 
    1067             :     if (global.count(cell_info))
    1068       73302 :       global[cell_info] += local[i];
    1069             :     else
    1070       80795 :       global[cell_info] = local[i];
    1071             :   }
    1072       15583 : }
    1073             : 
    1074             : template <typename T>
    1075             : void
    1076        6097 : OpenMCCellAverageProblem::gatherCellVector(std::vector<T> & local,
    1077             :                                            std::vector<unsigned int> & n_local,
    1078             :                                            std::map<cellInfo, std::vector<T>> & global)
    1079             : {
    1080             :   global.clear();
    1081        6097 :   _communicator.allgather(n_local);
    1082        6097 :   _communicator.allgather(local);
    1083             : 
    1084             :   int e = 0;
    1085       59833 :   for (unsigned int i = 0; i < _flattened_ids.size(); ++i)
    1086             :   {
    1087             :     cellInfo cell_info = {_flattened_ids[i], _flattened_instances[i]};
    1088             : 
    1089     7823552 :     for (unsigned int j = e; j < e + n_local[i]; ++j)
    1090     7769816 :       global[cell_info].push_back(local[j]);
    1091             : 
    1092       53736 :     e += n_local[i];
    1093             :   }
    1094        6097 : }
    1095             : 
    1096             : coupling::CouplingFields
    1097     9354105 : OpenMCCellAverageProblem::cellFeedback(const cellInfo & cell_info) const
    1098             : {
    1099             :   // _cell_to_elem only holds cells that are coupled by feedback to the [Mesh] (for sake of
    1100             :   // efficiency in cell-based loops for updating temperatures, densities and
    1101             :   // extracting the tally). But in some auxiliary kernels, we figure out
    1102             :   // an element's phase in terms of the cell that it maps to. For these cells that
    1103             :   // do *map* spatially, but just don't participate in coupling, _cell_to_elem doesn't
    1104             :   // have any notion of those elements
    1105             :   if (!_cell_phase.count(cell_info))
    1106           0 :     return coupling::none;
    1107             :   else
    1108     9354105 :     return _cell_phase.at(cell_info);
    1109             : }
    1110             : 
    1111             : void
    1112        2843 : OpenMCCellAverageProblem::getCellMappedPhase()
    1113             : {
    1114             :   std::vector<int> cells_n_temp;
    1115             :   std::vector<int> cells_n_temp_rho;
    1116             :   std::vector<int> cells_n_rho;
    1117             :   std::vector<int> cells_n_none;
    1118             : 
    1119             :   // whether each cell maps to a single phase
    1120       13742 :   for (const auto & c : _local_cell_to_elem)
    1121             :   {
    1122       10899 :     std::vector<int> f(4 /* number of coupling options */, 0);
    1123             : 
    1124             :     // we are looping over local elements, so no need to check for nullptr
    1125     1973903 :     for (const auto & e : c.second)
    1126     1963004 :       f[elemFeedback(getMooseMesh().queryElemPtr(globalElemID(e)))]++;
    1127             : 
    1128       10899 :     cells_n_temp.push_back(f[coupling::temperature]);
    1129             :     cells_n_temp_rho.push_back(f[coupling::density_and_temperature]);
    1130       10899 :     cells_n_rho.push_back(f[coupling::density]);
    1131       10899 :     cells_n_none.push_back(f[coupling::none]);
    1132       10899 :   }
    1133             : 
    1134        2843 :   gatherCellSum(cells_n_temp, _n_temp);
    1135        2843 :   gatherCellSum(cells_n_temp_rho, _n_temp_rho);
    1136        2843 :   gatherCellSum(cells_n_rho, _n_rho);
    1137        2843 :   gatherCellSum(cells_n_none, _n_none);
    1138        2843 : }
    1139             : 
    1140             : Real
    1141      609312 : OpenMCCellAverageProblem::cellVolume(const cellInfo & cell_info) const
    1142             : {
    1143             :   if (_cell_volume.count(cell_info))
    1144      609312 :     return _cell_volume.at(cell_info);
    1145             :   else
    1146           0 :     return 0.0;
    1147             : }
    1148             : 
    1149             : void
    1150        2829 : OpenMCCellAverageProblem::checkCellMappedPhase()
    1151             : {
    1152        2829 :   if (_volume_calc)
    1153             :   {
    1154         105 :     _volume_calc->initializeVolumeCalculation();
    1155         103 :     _volume_calc->computeVolumes();
    1156             :   }
    1157             : 
    1158             :   VariadicTable<std::string, int, int, int, int, std::string, std::string> vt(
    1159        2827 :       {"Cell", "  T  ", " rho ", "T+rho", "Other", "Mapped Vol", "Actual Vol"});
    1160             : 
    1161             :   bool has_mapping = false;
    1162             : 
    1163             :   std::vector<Real> cv;
    1164             :   _cell_phase.clear();
    1165       16472 :   for (const auto & c : _cell_to_elem)
    1166             :   {
    1167       13649 :     auto cell_info = c.first;
    1168       13649 :     int n_temp = _n_temp[cell_info];
    1169       13649 :     int n_rho = _n_rho[cell_info];
    1170       13649 :     int n_temp_rho = _n_temp_rho[cell_info];
    1171       13649 :     int n_none = _n_none[cell_info];
    1172             : 
    1173       13649 :     std::ostringstream vol;
    1174       13649 :     vol << std::setprecision(3) << std::scientific << "";
    1175       13649 :     if (_volume_calc)
    1176             :     {
    1177             :       Real v, std_dev;
    1178         758 :       _volume_calc->cellVolume(c.first.first, v, std_dev);
    1179         756 :       cv.push_back(v);
    1180         756 :       vol << v << " +/- " << std_dev;
    1181             :     }
    1182             : 
    1183       13647 :     std::ostringstream map;
    1184       13647 :     map << std::setprecision(3) << std::scientific << _cell_to_elem_volume[cell_info];
    1185             : 
    1186             :     // okay to print vol.str() here because only rank 0 is printing (which is the only one
    1187             :     // with meaningful volume data from OpenMC)
    1188       27294 :     vt.addRow(printCell(cell_info, true), n_temp, n_rho, n_temp_rho, n_none, map.str(), vol.str());
    1189             : 
    1190             :     // cells can only map to a single type of feedback
    1191       13647 :     std::vector<bool> conditions = {n_temp_rho > 0, n_temp > 0, n_rho > 0, n_none > 0};
    1192       13647 :     if (std::count(conditions.begin(), conditions.end(), true) > 1)
    1193             :     {
    1194           2 :       std::stringstream msg;
    1195           2 :       std::vector<int> conds = {n_temp, n_rho, n_temp_rho, n_none};
    1196           2 :       int size = std::to_string(*std::max_element(conds.begin(), conds.end())).length();
    1197           4 :       msg << "Cell " << printCell(cell_info) << " mapped to:\n\n  " << std::setw(size) << n_temp
    1198           2 :           << "  elements with temperature feedback\n  " << std::setw(size) << n_rho
    1199           2 :           << "  elements with density feedback\n  " << std::setw(size) << n_temp_rho
    1200           2 :           << "  elements with both temperature and density feedback\n  " << std::setw(size)
    1201             :           << n_none
    1202             :           << "  uncoupled elements\n\n"
    1203             :              "Each OpenMC cell (ID, instance) pair must map to elements of the same coupling "
    1204           2 :              "settings.";
    1205           2 :       mooseError(msg.str());
    1206           0 :     }
    1207             : 
    1208       13645 :     if (n_temp)
    1209             :     {
    1210             :       has_mapping = true;
    1211        9659 :       _cell_phase[cell_info] = coupling::temperature;
    1212             :     }
    1213        3986 :     else if (n_rho)
    1214             :     {
    1215             :       has_mapping = true;
    1216          52 :       _cell_phase[cell_info] = coupling::density;
    1217             :     }
    1218        3934 :     else if (n_temp_rho)
    1219             :     {
    1220             :       has_mapping = true;
    1221        2002 :       _cell_phase[cell_info] = coupling::density_and_temperature;
    1222             :     }
    1223             :     else
    1224        1932 :       _cell_phase[cell_info] = coupling::none;
    1225       13645 :   }
    1226             : 
    1227             :   // collect values from rank 0 onto all other ranks, then populate cell_volume
    1228             :   // (this is necessary because in OpenMC, the stochastic volume calculation only
    1229             :   // gets meaningful results on rank 0
    1230        2823 :   if (_volume_calc)
    1231             :   {
    1232             :     _cell_volume.clear();
    1233         101 :     MPI_Bcast(cv.data(), cv.size(), MPI_DOUBLE, 0, _communicator.get());
    1234             :     int i = 0;
    1235         857 :     for (const auto & c : _cell_to_elem)
    1236         756 :       _cell_volume[c.first] = cv[i++];
    1237             :   }
    1238             : 
    1239        2823 :   if (_specified_density_feedback || _specified_temperature_feedback)
    1240        1488 :     if (!has_mapping)
    1241           2 :       mooseError("Feedback was specified using 'temperature_blocks' and/or 'density_blocks', but "
    1242             :                  "no MOOSE elements mapped to OpenMC cells!");
    1243             : 
    1244        2821 :   if (_verbose && _cell_to_elem.size())
    1245             :   {
    1246             :     _console
    1247        1641 :         << "\n ===================>     MAPPING FROM OPENMC TO MOOSE     <===================\n"
    1248        1641 :         << std::endl;
    1249        1641 :     _console << "          T:      # elems providing temperature-only feedback" << std::endl;
    1250        1641 :     _console << "          rho:    # elems providing density-only feedback" << std::endl;
    1251        1641 :     _console << "          T+rho:  # elems providing temperature and density feedback" << std::endl;
    1252        1641 :     _console << "          Other:  # elems which do not provide feedback to OpenMC" << std::endl;
    1253        1641 :     _console << "                    (but receives a cell tally from OpenMC)" << std::endl;
    1254        1641 :     _console << "     Mapped Vol:  volume of MOOSE elems each cell maps to" << std::endl;
    1255        1641 :     _console << "     Actual Vol:  OpenMC cell volume (computed with 'volume_calculation')\n"
    1256        1641 :              << std::endl;
    1257        1641 :     vt.print(_console);
    1258             :   }
    1259             : 
    1260        2821 :   printAuxVariableIO();
    1261        2821 :   _printed_initial = true;
    1262        2821 : }
    1263             : 
    1264             : void
    1265        2821 : OpenMCCellAverageProblem::printAuxVariableIO()
    1266             : {
    1267        2821 :   if (_printed_initial)
    1268             :     return;
    1269             : 
    1270        1917 :   if (!(_specified_density_feedback || _specified_temperature_feedback ||
    1271             :         _local_tallies.size() > 0))
    1272             :     return;
    1273             : 
    1274        1743 :   _console << "\n ===================>     AUXVARIABLES FOR OPENMC I/O     <===================\n"
    1275        1743 :            << std::endl;
    1276             : 
    1277        1743 :   if (_specified_density_feedback || _specified_temperature_feedback)
    1278             :   {
    1279        1380 :     _console << "      Subdomain:  subdomain name/ID" << std::endl;
    1280        1380 :     _console << "    Temperature:  variable OpenMC reads temperature from (empty if no feedback)"
    1281        1380 :              << std::endl;
    1282        1380 :     _console << "        Density:  variable OpenMC reads density from (empty if no feedback)\n"
    1283        1380 :              << std::endl;
    1284             : 
    1285             :     VariadicTable<std::string, std::string, std::string> aux(
    1286        1380 :         {"Subdomain", "Temperature", "Density"});
    1287             : 
    1288        3831 :     for (const auto & s : getMooseMesh().meshSubdomains())
    1289             :     {
    1290        2451 :       std::string temp = _subdomain_to_temp_vars.count(s) ? _subdomain_to_temp_vars[s].second : "";
    1291             :       std::string rho =
    1292        2451 :           _subdomain_to_density_vars.count(s) ? _subdomain_to_density_vars[s].second : "";
    1293             : 
    1294        2451 :       if (temp == "" && rho == "")
    1295             :         continue;
    1296             : 
    1297        4422 :       aux.addRow(subdomainName(s), temp, rho);
    1298             :     }
    1299             : 
    1300        1380 :     aux.print(_console);
    1301        1380 :     _console << std::endl;
    1302        1380 :   }
    1303             : 
    1304        1743 :   if (_local_tallies.size() > 0)
    1305             :   {
    1306        1588 :     _console << "    Tally Name:   Cardinal tally object name" << std::endl;
    1307        1588 :     _console << "    Tally Score:  OpenMC tally score" << std::endl;
    1308        1588 :     _console << "    AuxVariable:  variable where this score is written\n" << std::endl;
    1309             : 
    1310             :     VariadicTable<std::string, std::string, std::string> tallies(
    1311        1588 :         {"Tally Name", "Tally Score", "AuxVariable(s)"});
    1312        4144 :     for (unsigned int i = 0; i < _local_tallies.size(); ++i)
    1313             :     {
    1314             :       const auto & scores = _local_tallies[i]->getScores();
    1315             :       const auto & names = _local_tallies[i]->getAuxVarNames();
    1316             :       const auto bins = _local_tallies[i]->numExtFilterBins();
    1317        5508 :       for (unsigned int j = 0; j < scores.size(); ++j)
    1318             :       {
    1319        2952 :         if (names.size() == 0)
    1320         388 :           continue;
    1321             : 
    1322        6294 :         for (unsigned int k = bins * j; k < (j + 1) * bins; ++k)
    1323             :         {
    1324        3730 :           const auto l = j == 0 && k == bins * j ? _local_tallies[i]->name() : "";
    1325        3730 :           const auto c = k == bins * j ? scores[j] : "";
    1326        3730 :           const auto r = names[k];
    1327        7460 :           tallies.addRow(l, c, r);
    1328             :         }
    1329             :       }
    1330             :     }
    1331             : 
    1332        1588 :     tallies.print(_console);
    1333        1588 :   }
    1334             : }
    1335             : 
    1336             : void
    1337        2843 : OpenMCCellAverageProblem::getCellMappedSubdomains()
    1338             : {
    1339             :   std::vector<unsigned int> n_elems;
    1340             :   std::vector<unsigned int> elem_ids;
    1341             : 
    1342       13742 :   for (const auto & c : _local_cell_to_elem)
    1343             :   {
    1344       10899 :     n_elems.push_back(c.second.size());
    1345     1973903 :     for (const auto & e : c.second)
    1346             :     {
    1347             :       // we are looping over local elements, so no need to check for nullptr
    1348     1963004 :       const auto * elem = getMooseMesh().queryElemPtr(globalElemID(e));
    1349     1963004 :       elem_ids.push_back(elem->subdomain_id());
    1350             :     }
    1351             :   }
    1352             : 
    1353             :   std::map<cellInfo, std::vector<unsigned int>> cell_to_subdomain_vec;
    1354        2843 :   gatherCellVector(elem_ids, n_elems, cell_to_subdomain_vec);
    1355             : 
    1356             :   // convert to a set
    1357             :   _cell_to_elem_subdomain.clear();
    1358       16586 :   for (const auto & c : cell_to_subdomain_vec)
    1359     3809875 :     for (const auto & s : c.second)
    1360     3796132 :       _cell_to_elem_subdomain[c.first].insert(s);
    1361             : 
    1362             :   // each cell must map to a consistent setting for identical_cell_fills
    1363             :   // (all of the blocks it maps to must either _all_ be in the identical blocks,
    1364             :   // or all excluded)
    1365        2843 :   if (_has_identical_cell_fills)
    1366             :   {
    1367        1140 :     for (const auto & c : _cell_to_elem)
    1368             :     {
    1369        1118 :       auto cell_info = c.first;
    1370             :       bool at_least_one_in = false;
    1371             :       bool at_least_one_out = false;
    1372             :       SubdomainID in;
    1373             :       SubdomainID out;
    1374        1118 :       auto subdomains = _cell_to_elem_subdomain[cell_info];
    1375        2238 :       for (const auto & s : subdomains)
    1376             :       {
    1377        1120 :         if (_identical_cell_fill_blocks.find(s) == _identical_cell_fill_blocks.end())
    1378             :         {
    1379             :           at_least_one_out = true;
    1380         606 :           out = s;
    1381             :         }
    1382             :         else
    1383             :         {
    1384             :           at_least_one_in = true;
    1385         514 :           in = s;
    1386             :         }
    1387             :       }
    1388             : 
    1389        1118 :       if (at_least_one_in && at_least_one_out)
    1390             :       {
    1391           2 :         std::stringstream msg;
    1392           2 :         msg << "Cell " << printCell(cell_info)
    1393             :             << " mapped to inconsistent 'identical_cell_fills' settings.\n"
    1394           6 :             << "Subdomain " << in << " is in 'identical_cell_fills', but " << out << " is not.\n\n"
    1395             :             << "All subdomains to which this cell maps must either ALL be in "
    1396           2 :                "'identical_cell_fills' or ALL excluded.";
    1397           2 :         mooseError(msg.str());
    1398           0 :       }
    1399             :     }
    1400             :   }
    1401        2841 : }
    1402             : 
    1403             : std::set<SubdomainID>
    1404        2813 : OpenMCCellAverageProblem::coupledSubdomains() const
    1405             : {
    1406             :   std::set<SubdomainID> subdomains;
    1407       16246 :   for (const auto & c : _cell_to_elem)
    1408             :   {
    1409       13433 :     const auto & subdomains_spanning_cell = _cell_to_elem_subdomain.at(c.first);
    1410       27057 :     for (const auto & s : subdomains_spanning_cell)
    1411       13624 :       subdomains.insert(s);
    1412             :   }
    1413             : 
    1414        2813 :   return subdomains;
    1415             : }
    1416             : 
    1417             : void
    1418        2813 : OpenMCCellAverageProblem::subdomainsToMaterials()
    1419             : {
    1420        2813 :   const auto time_start = std::chrono::high_resolution_clock::now();
    1421             : 
    1422        5626 :   TIME_SECTION("subdomainsToMaterials", 3, "Mapping OpenMC Materials to Mesh", true);
    1423             : 
    1424             :   _subdomain_to_material.clear();
    1425             : 
    1426       16246 :   for (const auto & c : _cell_to_elem)
    1427             :   {
    1428       13433 :     printTrisoHelp(time_start);
    1429             : 
    1430       13433 :     const auto mats = cellHasIdenticalFill(c.first)
    1431       13433 :                           ? _first_identical_cell_materials
    1432       13433 :                           : materialsInCells(containedMaterialCells(c.first));
    1433             : 
    1434       27057 :     for (const auto & s : _cell_to_elem_subdomain.at(c.first))
    1435    11413404 :       for (const auto & m : mats)
    1436    11399780 :         _subdomain_to_material[s].insert(m);
    1437       13433 :   }
    1438             : 
    1439        2813 :   VariadicTable<std::string, std::string> vt({"Subdomain", "Material"});
    1440        2813 :   auto subdomains = coupledSubdomains();
    1441        6194 :   for (const auto & i : subdomains)
    1442             :   {
    1443             :     std::map<std::string, int> mat_to_num;
    1444             : 
    1445       10797 :     for (const auto & m : _subdomain_to_material[i])
    1446             :     {
    1447        7416 :       auto name = materialName(m);
    1448             :       if (mat_to_num.count(name))
    1449         296 :         mat_to_num[name] += 1;
    1450             :       else
    1451        7120 :         mat_to_num[name] = 1;
    1452             :     }
    1453             : 
    1454        3381 :     std::string mats = "";
    1455       10501 :     for (const auto & m : mat_to_num)
    1456             :     {
    1457        7136 :       std::string extra = m.second > 1 ? " (" + std::to_string(m.second) + ")" : "";
    1458       14240 :       mats += " " + m.first + extra + ",";
    1459             :     }
    1460             : 
    1461        3381 :     mats.pop_back();
    1462        6762 :     vt.addRow(subdomainName(i), mats);
    1463             :   }
    1464             : 
    1465        2813 :   if (_cell_to_elem.size())
    1466             :   {
    1467             :     _console
    1468        2280 :         << "\n ===================>  OPENMC SUBDOMAIN MATERIAL MAPPING  <====================\n"
    1469        2280 :         << std::endl;
    1470        2280 :     _console << "      Subdomain:  Subdomain name; if unnamed, we show the ID" << std::endl;
    1471        2280 :     _console << "       Material:  OpenMC material name(s) in this subdomain; if unnamed, we\n"
    1472        2280 :              << "                  show the ID. If N duplicate material names, we show the\n"
    1473        2280 :              << "                  number in ( ).\n"
    1474        2280 :              << std::endl;
    1475        2280 :     vt.print(_console);
    1476        2280 :     _console << std::endl;
    1477             :   }
    1478        2813 : }
    1479             : 
    1480             : void
    1481        2821 : OpenMCCellAverageProblem::getMaterialFills()
    1482             : {
    1483        2821 :   VariadicTable<std::string, int> vt({"Cell", "Material"});
    1484             : 
    1485             :   _cell_to_material.clear();
    1486       16458 :   for (const auto & c : _cell_to_elem)
    1487             :   {
    1488       13639 :     auto cell_info = c.first;
    1489             : 
    1490       13639 :     if (!hasDensityFeedback(cell_info))
    1491       11585 :       continue;
    1492             : 
    1493             :     int32_t material_index;
    1494        2054 :     auto is_material_cell = materialFill(cell_info, material_index);
    1495             : 
    1496        2054 :     if (!is_material_cell)
    1497           2 :       mooseError(
    1498             :           "Density transfer does not currently support cells filled with universes or lattices!");
    1499             : 
    1500        2052 :     _cell_to_material[cell_info] = material_index;
    1501        4104 :     vt.addRow(printCell(cell_info), materialID(material_index));
    1502             :   }
    1503             : 
    1504        2819 :   if (_verbose && _specified_density_feedback)
    1505             :   {
    1506             :     _console
    1507         451 :         << "\n ===================>       OPENMC MATERIAL MAPPING       <====================\n"
    1508         451 :         << std::endl;
    1509         451 :     _console << "           Cell:  OpenMC cell receiving density feedback" << std::endl;
    1510         451 :     _console << "       Material:  OpenMC material ID in this cell (-1 for void)\n" << std::endl;
    1511         451 :     vt.print(_console);
    1512             :   }
    1513        2819 : }
    1514             : 
    1515             : void
    1516        2847 : OpenMCCellAverageProblem::initializeElementToCellMapping()
    1517             : {
    1518             :   /* We consider five different cases here based on how the MOOSE and OpenMC
    1519             :    * domains might overlap in space:
    1520             :    *
    1521             :    * 1: Perfect overlap, every MOOSE element maps to an OpenMC cell and every
    1522             :    *    OpenMC cell maps to MOOSE element(s)
    1523             :    *
    1524             :    * 2: MOOSE domain fully encloses the OpenMC domain, so that not every MOOSE
    1525             :    *    element maps to an OpenMC cell, but every OpenMC cell maps to a MOOSE element
    1526             :    *
    1527             :    * 3: OpenMC domain fully encloses the MOOSE domain, so that not every OpenMC
    1528             :    *    cell maps to MOOSE element(s), but every MOOSE element maps to an OpenMC cell
    1529             :    *
    1530             :    * 4: MOOSE and OpenMC domains only partially overlap, so that not every MOOSE
    1531             :    *    element maps to an OpenMC and not every OpenMC cell maps to MOOSE element(s)
    1532             :    *
    1533             :    * 5: The MOOSE and OpenMC domains do not overlap at all, so no MOOSE elements
    1534             :    *    map to OpenMC cells and no OpenMC cells map to MOOSE elements.
    1535             :    *
    1536             :    * We consider situation #5 to be an error, while the others are technically allowed.
    1537             :    * We need to error here before getting to OpenMC where we don't map to any cells but
    1538             :    * would still try to set a cell filter based on no cells.
    1539             :    */
    1540             : 
    1541             :   // First, figure out the phase of each element according to the blocks defined by the user
    1542        2847 :   storeElementPhase();
    1543             : 
    1544             :   // perform element to cell mapping
    1545        2847 :   mapElemsToCells();
    1546             : 
    1547        2843 :   if (!_material_cells_only)
    1548             :   {
    1549             :     // gather all cell indices from the initial mapping
    1550             :     std::vector<int32_t> mapped_cells;
    1551      204915 :     for (const auto & item : _elem_to_cell)
    1552      204504 :       mapped_cells.push_back(item.first);
    1553             : 
    1554         411 :     std::sort(mapped_cells.begin(), mapped_cells.end());
    1555         411 :     auto new_end = std::unique(mapped_cells.begin(), mapped_cells.end());
    1556             :     mapped_cells.erase(new_end, mapped_cells.end());
    1557         411 :     openmc::prepare_distribcell(&mapped_cells);
    1558             : 
    1559             :     // perform element to cell mapping again to get correct instances
    1560         411 :     mapElemsToCells();
    1561         411 :   }
    1562             : 
    1563             :   // For each cell, get one point inside it to speed up the particle search
    1564        2843 :   getPointInCell();
    1565             : 
    1566             :   // Compute the volume that each OpenMC cell maps to in the MOOSE mesh
    1567        2843 :   computeCellMappedVolumes();
    1568             : 
    1569             :   // Get the number of elements of each phase within the cells
    1570        2843 :   getCellMappedPhase();
    1571             : 
    1572             :   // Get the element subdomains within each cell
    1573        2843 :   getCellMappedSubdomains();
    1574             : 
    1575        2841 :   if (_cell_to_elem.size() == 0 && _has_cell_tallies)
    1576           2 :     mooseError("Did not find any overlap between MOOSE elements and OpenMC cells for "
    1577             :                "the specified blocks!");
    1578             : 
    1579        8517 :   _console << "\nMapping between " + Moose::stringify(getMooseMesh().getMesh().n_active_elem()) +
    1580        8517 :                   " MOOSE elements and " + Moose::stringify(_n_openmc_cells) +
    1581        2839 :                   " OpenMC cells (on " + Moose::stringify(openmc::model::n_coord_levels) +
    1582        5678 :                   " coordinate levels):"
    1583        2839 :            << std::endl;
    1584             : 
    1585             :   VariadicTable<std::string, int, int, int, int> vt(
    1586        2839 :       {"", "# T Elems", "# rho Elems", "# T+rho Elems", "# Uncoupled Elems"});
    1587        2839 :   vt.addRow("MOOSE mesh",
    1588             :             _n_moose_temp_elems,
    1589             :             _n_moose_density_elems,
    1590             :             _n_moose_temp_density_elems,
    1591             :             _n_moose_none_elems);
    1592        2839 :   vt.addRow("OpenMC cells",
    1593             :             _n_mapped_temp_elems,
    1594             :             _n_mapped_density_elems,
    1595             :             _n_mapped_temp_density_elems,
    1596             :             _n_mapped_none_elems);
    1597        2839 :   vt.print(_console);
    1598        2839 :   _console << std::endl;
    1599             : 
    1600        2839 :   if (_needs_to_map_cells)
    1601             :   {
    1602        2306 :     if (_n_moose_temp_elems && (_n_mapped_temp_elems != _n_moose_temp_elems))
    1603          90 :       mooseWarning("The [Mesh] has " + Moose::stringify(_n_moose_temp_elems) +
    1604             :                    " elements providing temperature feedback (the elements in "
    1605          32 :                    "'temperature_blocks'), but only " +
    1606          26 :                    Moose::stringify(_n_mapped_temp_elems) + " got mapped to OpenMC cells.");
    1607             : 
    1608        2300 :     if (_n_moose_temp_elems && (_n_mapped_density_elems != _n_moose_density_elems))
    1609           4 :       mooseWarning("The [Mesh] has " + Moose::stringify(_n_moose_density_elems) +
    1610             :                    " elements providing density feedback (the elements in "
    1611           2 :                    "'density_blocks'), but only " +
    1612           0 :                    Moose::stringify(_n_mapped_density_elems) + " got mapped to OpenMC cells.");
    1613             : 
    1614        2298 :     if (_n_moose_temp_density_elems &&
    1615         489 :         (_n_mapped_temp_density_elems != _n_moose_temp_density_elems))
    1616          28 :       mooseWarning("The [Mesh] has " + Moose::stringify(_n_moose_temp_density_elems) +
    1617             :                    " elements providing temperature and density feedback (the elements in the "
    1618          10 :                    "intersection of 'temperature_blocks' and 'density_blocks'), but only " +
    1619           8 :                    Moose::stringify(_n_mapped_temp_density_elems) + " got mapped to OpenMC cells.");
    1620             : 
    1621        2296 :     if (_n_mapped_none_elems && (_specified_temperature_feedback || _specified_density_feedback))
    1622         528 :       mooseWarning("Skipping OpenMC multiphysics feedback from " +
    1623         528 :                    Moose::stringify(_n_mapped_none_elems) +
    1624         264 :                    " [Mesh] elements, which occupy a volume of: " +
    1625         528 :                    Moose::stringify(_uncoupled_volume * _scaling * _scaling * _scaling) + " cm3");
    1626             : 
    1627        2296 :     if (_n_openmc_cells < _cell_to_elem.size())
    1628           0 :       mooseError("Internal error: _cell_to_elem has length ",
    1629           0 :                  _cell_to_elem.size(),
    1630             :                  " which should\n"
    1631             :                  "not exceed the number of OpenMC cells, ",
    1632           0 :                  _n_openmc_cells);
    1633             :   }
    1634             : 
    1635             :   // Check that each cell maps to a single phase
    1636        2829 :   checkCellMappedPhase();
    1637        2821 : }
    1638             : 
    1639             : void
    1640       14425 : OpenMCCellAverageProblem::setContainedCells(const cellInfo & cell_info,
    1641             :                                             const Point & hint,
    1642             :                                             std::map<cellInfo, containedCells> & map)
    1643             : {
    1644             :   containedCells contained_cells;
    1645             : 
    1646       14425 :   openmc::Position p{hint(0), hint(1), hint(2)};
    1647             : 
    1648       14425 :   const auto & cell = openmc::model::cells[cell_info.first];
    1649       14425 :   if (cell->type_ == openmc::Fill::MATERIAL)
    1650             :   {
    1651       14004 :     std::vector<int32_t> instances = {cell_info.second};
    1652       14004 :     contained_cells[cell_info.first] = instances;
    1653       14004 :   }
    1654             :   else
    1655         842 :     contained_cells = cell->get_contained_cells(cell_info.second, &p);
    1656             : 
    1657       14425 :   map[cell_info] = contained_cells;
    1658       14425 : }
    1659             : 
    1660             : void
    1661       27070 : OpenMCCellAverageProblem::printTrisoHelp(
    1662             :     const std::chrono::time_point<std::chrono::high_resolution_clock> & start) const
    1663             : {
    1664       27070 :   if (!_printed_triso_warning)
    1665             :   {
    1666       27070 :     auto stop = std::chrono::high_resolution_clock::now();
    1667       27070 :     auto elapsed = std::chrono::duration<double, std::milli>(stop - start).count() / 1e3;
    1668       27070 :     if (elapsed > 120.0)
    1669             :     {
    1670           0 :       _printed_triso_warning = true;
    1671           0 :       _console << "\nThis is taking a long time. Does your problem have TRISOs/other "
    1672             :                << "highly heterogeneous geometry?\nIf you are repeating the same TRISO/etc. "
    1673           0 :                   "universe many times "
    1674             :                << "through your OpenMC model, setting\n'identical_cell_fills' will give you a big "
    1675           0 :                   "speedup.\n\n"
    1676             :                << "For more information, consult the Cardinal documentation: "
    1677           0 :                   "https://tinyurl.com/54kz9aw8"
    1678           0 :                << std::endl;
    1679             :     }
    1680             :   }
    1681       27070 : }
    1682             : 
    1683             : void
    1684        2819 : OpenMCCellAverageProblem::cacheContainedCells()
    1685             : {
    1686        5638 :   TIME_SECTION("cacheContainedCells", 3, "Caching Contained Cells", true);
    1687             : 
    1688             :   bool first_cell = true;
    1689             :   bool second_cell = false;
    1690             :   containedCells first_cell_cc;
    1691             :   containedCells second_cell_cc;
    1692             :   bool used_cache_shortcut = false;
    1693             : 
    1694             :   _cell_to_contained_material_cells.clear();
    1695        2819 :   _first_identical_cell_materials.clear();
    1696             :   _instance_offsets.clear();
    1697             :   _n_offset.clear();
    1698             : 
    1699             :   int n = -1;
    1700        2819 :   const auto time_start = std::chrono::high_resolution_clock::now();
    1701       16456 :   for (const auto & c : _cell_to_elem)
    1702             :   {
    1703       13637 :     auto cell_info = c.first;
    1704       13637 :     Point hint = transformPointToOpenMC(_cell_to_point[cell_info]);
    1705             : 
    1706       13637 :     printTrisoHelp(time_start);
    1707             : 
    1708             :     // default to the normal behavior
    1709       13637 :     if (!cellHasIdenticalFill(cell_info))
    1710       13325 :       setContainedCells(cell_info, hint, _cell_to_contained_material_cells);
    1711             :     else
    1712             :     {
    1713             :       used_cache_shortcut = true;
    1714         312 :       _n_offset[cell_info] = ++n;
    1715             : 
    1716         312 :       if (first_cell)
    1717             :       {
    1718          12 :         setContainedCells(cell_info, hint, _cell_to_contained_material_cells);
    1719          12 :         first_cell_cc = _cell_to_contained_material_cells[cell_info];
    1720             :         _first_identical_cell = cell_info;
    1721          24 :         _first_identical_cell_materials = materialsInCells(first_cell_cc);
    1722             :         first_cell = false;
    1723             :         second_cell = true;
    1724             :       }
    1725         300 :       else if (second_cell)
    1726             :       {
    1727          12 :         setContainedCells(cell_info, hint, _cell_to_contained_material_cells);
    1728          12 :         second_cell_cc = _cell_to_contained_material_cells[cell_info];
    1729             :         second_cell = false;
    1730             : 
    1731             :         // we will check for equivalence in the end mapping later; but here we still need
    1732             :         // some checks to make sure the structure is compatible
    1733          12 :         checkContainedCellsStructure(cell_info, first_cell_cc, second_cell_cc);
    1734             : 
    1735             :         // get the offset for each instance for each contained cell
    1736        6882 :         for (const auto & f : first_cell_cc)
    1737             :         {
    1738        6870 :           const auto id = f.first;
    1739             :           const auto & instances = f.second;
    1740             :           const auto & new_instances = second_cell_cc[id];
    1741             : 
    1742             :           std::vector<int32_t> offsets;
    1743      432036 :           for (unsigned int i = 0; i < instances.size(); ++i)
    1744      425166 :             offsets.push_back(new_instances[i] - instances[i]);
    1745             : 
    1746        6870 :           _instance_offsets[id] = offsets;
    1747        6870 :         }
    1748             :       }
    1749             :     }
    1750             :   }
    1751             : 
    1752             :   // only need to check if we were attempting the shortcut
    1753        2819 :   if (_check_identical_cell_fills)
    1754             :   {
    1755          40 :     TIME_SECTION("verifyCacheContainedCells", 4, "Verifying Cached Contained Cells", true);
    1756             : 
    1757             :     std::map<cellInfo, containedCells> checking_cell_fills;
    1758        1096 :     for (const auto & c : _cell_to_elem)
    1759        1076 :       setContainedCells(
    1760        2152 :           c.first, transformPointToOpenMC(_cell_to_point[c.first]), checking_cell_fills);
    1761             : 
    1762             :     std::map<cellInfo, containedCells> current_cell_fills;
    1763        1096 :     for (const auto & c : _cell_to_elem)
    1764        2152 :       current_cell_fills[c.first] = containedMaterialCells(c.first);
    1765             : 
    1766             :     std::map<cellInfo, containedCells> ordered_reference(checking_cell_fills.begin(),
    1767          20 :                                                          checking_cell_fills.end());
    1768             :     std::map<cellInfo, containedCells> ordered(current_cell_fills.begin(),
    1769          20 :                                                current_cell_fills.end());
    1770          20 :     compareContainedCells(ordered_reference, ordered);
    1771          16 :   }
    1772             : 
    1773        2815 :   if (_has_identical_cell_fills && !used_cache_shortcut)
    1774          10 :     mooseWarning("You specified 'identical_cell_fills', but all cells which mapped to these "
    1775             :                  "subdomains were filled \n"
    1776             :                  "by a material (as opposed to a universe/lattice), so the 'identical_cell_fills' "
    1777             :                  "parameter is unused.");
    1778        2813 : }
    1779             : 
    1780             : void
    1781        1002 : OpenMCCellAverageProblem::checkContainedCellsStructure(const cellInfo & cell_info,
    1782             :                                                        containedCells & reference,
    1783             :                                                        containedCells & compare) const
    1784             : {
    1785             :   // make sure the number of keys is the same
    1786        1002 :   if (reference.size() != compare.size())
    1787           0 :     mooseError("The cell caching failed to identify identical number of cell IDs filling cell " +
    1788           0 :                printCell(cell_info) + "\nYou must unset 'identical_cell_fills'");
    1789             : 
    1790      193190 :   for (const auto & entry : reference)
    1791             :   {
    1792      192190 :     const auto & key = entry.first;
    1793             : 
    1794             :     // check that each key exists
    1795             :     if (!compare.count(key))
    1796           6 :       mooseError("Not all cells contain cell ID " + Moose::stringify(cellID(key)) +
    1797           4 :                  ". The offender is: cell " + printCell(cell_info) +
    1798             :                  ".\nYou must unset 'identical_cell_fills'!");
    1799             : 
    1800             :     // for each int32_t key, compare the std::vector<int32_t> map
    1801             :     const auto & reference_instances = entry.second;
    1802             :     const auto & compare_instances = compare[key];
    1803             : 
    1804             :     // they should have the same number of instances
    1805      192188 :     if (reference_instances.size() != compare_instances.size())
    1806           0 :       mooseError("The cell caching should have identified " +
    1807           0 :                  Moose::stringify(reference_instances.size()) + "cell instances in cell ID " +
    1808           0 :                  Moose::stringify(cellID(key)) + ", but instead found " +
    1809             :                  Moose::stringify(compare_instances.size()) +
    1810             :                  "\nYou must unset 'identical_cell_fills'");
    1811             :   }
    1812        1000 : }
    1813             : 
    1814             : void
    1815          20 : OpenMCCellAverageProblem::compareContainedCells(std::map<cellInfo, containedCells> & reference,
    1816             :                                                 std::map<cellInfo, containedCells> & compare) const
    1817             : {
    1818             :   // check that the number of keys matches
    1819          20 :   if (reference.size() != compare.size())
    1820           0 :     mooseError("The cell caching should have identified " + Moose::stringify(reference.size()) +
    1821             :                " cells, but instead "
    1822           0 :                "found " +
    1823             :                Moose::stringify(compare.size()));
    1824             : 
    1825             :   // loop over each cellInfo
    1826        1006 :   for (const auto & entry : reference)
    1827             :   {
    1828         990 :     auto cell_info = entry.first;
    1829             : 
    1830             :     // make sure the key exists
    1831             :     if (!compare.count(cell_info))
    1832           0 :       mooseError("The cell caching failed to map cell " + printCell(cell_info));
    1833             : 
    1834             :     // for each cellInfo key, compare the contained cells map
    1835         990 :     auto reference_map = reference[cell_info];
    1836         990 :     auto compare_map = compare[cell_info];
    1837             : 
    1838         990 :     checkContainedCellsStructure(cell_info, reference_map, compare_map);
    1839             : 
    1840             :     // loop over each contained cell
    1841      185886 :     for (const auto & nested_entry : reference_map)
    1842             :     {
    1843             :       // for each int32_t key, compare the std::vector<int32_t> map
    1844      184900 :       auto reference_instances = nested_entry.second;
    1845      184900 :       auto compare_instances = compare_map[nested_entry.first];
    1846             : 
    1847      184900 :       std::sort(reference_instances.begin(), reference_instances.end());
    1848      184900 :       std::sort(compare_instances.begin(), compare_instances.end());
    1849             : 
    1850             :       // and the instances should exactly match
    1851      184900 :       if (reference_instances != compare_instances)
    1852           2 :         mooseError(
    1853           2 :             "The cell caching failed to get correct instances for material cell ID " +
    1854           4 :             Moose::stringify(cellID(nested_entry.first)) + " within cell " + printCell(cell_info) +
    1855           0 :             ". You must unset 'identical_cell_fills'!" + "\n\nThis error might appear if:\n" +
    1856             :             " - There is a mismatch between your OpenMC model and the [Mesh]\n"
    1857             :             " - There are additional OpenMC cells filled with this repeatable universe/lattice, "
    1858             :             "but which are not mapping to the blocks in 'identical_cell_fills'");
    1859      184898 :     }
    1860             :   }
    1861          16 : }
    1862             : 
    1863             : std::vector<int32_t>
    1864        2118 : OpenMCCellAverageProblem::getMappedTallyIDs() const
    1865             : {
    1866             :   std::vector<int32_t> tally_ids;
    1867             : 
    1868             :   // local mapped tallies
    1869        5266 :   for (const auto & t : _local_tallies)
    1870        3148 :     tally_ids.push_back(t->getTallyID());
    1871             :   // global normalization tallies
    1872        5266 :   for (const auto & t : _local_tallies)
    1873        3148 :     if (t->addingGlobalTally())
    1874        1448 :       tally_ids.push_back(t->getGlobalTallyID());
    1875             : 
    1876        2118 :   return tally_ids;
    1877           0 : }
    1878             : 
    1879             : unsigned int
    1880     2066272 : OpenMCCellAverageProblem::getCellLevel(const Point & c) const
    1881             : {
    1882     2066272 :   unsigned int level = _cell_level;
    1883     2066272 :   if (_cell_level > _particle.n_coord() - 1)
    1884             :   {
    1885        8388 :     if (isParamValid("lowest_cell_level"))
    1886        4192 :       level = _particle.n_coord() - 1;
    1887             :     else
    1888             :     {
    1889           2 :       std::string l = Moose::stringify(_cell_level);
    1890           4 :       mooseError("Requested coordinate level of " + l +
    1891           6 :                  " exceeds number of nested coordinate levels at " + printPoint(c) + ": " +
    1892           2 :                  Moose::stringify(_particle.n_coord()) +
    1893             :                  ".\n\nYou can either change how the OpenMC model is built by nesting universes "
    1894           2 :                  "into deeper levels, or you can try setting 'lowest_cell_level = " +
    1895           0 :                  l +
    1896             :                  "', which will couple on the lowest level found in the geometry at any given x, "
    1897           2 :                  "y, z point, up to and including level " +
    1898           0 :                  l + ".");
    1899             :     }
    1900             :   }
    1901             : 
    1902     2066270 :   return level;
    1903             : }
    1904             : 
    1905             : void
    1906        3258 : OpenMCCellAverageProblem::mapElemsToCells()
    1907             : {
    1908             :   // reset counters, flags
    1909        3258 :   _n_mapped_temp_elems = 0;
    1910        3258 :   _n_mapped_density_elems = 0;
    1911        3258 :   _n_mapped_temp_density_elems = 0;
    1912        3258 :   _n_mapped_none_elems = 0;
    1913        3258 :   _uncoupled_volume = 0.0;
    1914        3258 :   _material_cells_only = true;
    1915             : 
    1916             :   // reset data structures
    1917        3258 :   _elem_to_cell.clear();
    1918             :   _cell_to_elem.clear();
    1919        3258 :   _flattened_ids.clear();
    1920        3258 :   _flattened_instances.clear();
    1921             : 
    1922             :   int local_elem = -1;
    1923     4207006 :   for (unsigned int e = 0; e < getMooseMesh().nElem(); ++e)
    1924             :   {
    1925     4203752 :     const auto * elem = getMooseMesh().queryElemPtr(e);
    1926             : 
    1927     4203752 :     if (!isLocalElem(elem) || !elem->active())
    1928     2045576 :       continue;
    1929             : 
    1930     2184688 :     local_elem++;
    1931             : 
    1932     2184688 :     auto id = elem->subdomain_id();
    1933     2184688 :     const Point & c = elem->vertex_average();
    1934     2184688 :     Real element_volume = elem->volume();
    1935             : 
    1936             :     // find the OpenMC cell at the location 'c' (if any)
    1937     2184688 :     bool error = findCell(c);
    1938             : 
    1939             :     // if we didn't find an OpenMC cell here, then we certainly have an uncoupled region
    1940     2184688 :     if (error)
    1941             :     {
    1942       26512 :       _uncoupled_volume += element_volume;
    1943       26512 :       _n_mapped_none_elems++;
    1944       26512 :       continue;
    1945             :     }
    1946             : 
    1947             :     // next, see what type of data is to be sent into OpenMC (to further classify
    1948             :     // the type of couling)
    1949     2158176 :     auto phase = elemFeedback(elem);
    1950             : 
    1951             :     // Loop over the tallies to check if any CellTally objects map to this element.
    1952             :     bool elem_mapped_to_cell_tally = false;
    1953     4794610 :     for (const auto & tally : _local_tallies)
    1954             :     {
    1955     2636434 :       auto cell_tally = dynamic_cast<const CellTally *>(tally.get());
    1956     2636434 :       if (cell_tally)
    1957     2336346 :         elem_mapped_to_cell_tally |=
    1958             :             cell_tally->getBlocks().find(id) != cell_tally->getBlocks().end();
    1959             :     }
    1960             : 
    1961     2158176 :     bool requires_mapping = phase != coupling::none || elem_mapped_to_cell_tally;
    1962             : 
    1963             :     // get the level in the OpenMC model to fetch mapped cell information. For
    1964             :     // uncoupled regions, we know we will be successful in finding a cell (because
    1965             :     // we already screened out uncoupled cells), and the id and instance are unused
    1966             :     // (so we can just set zero).
    1967     2158176 :     auto level = requires_mapping ? getCellLevel(c) : 0;
    1968             : 
    1969             :     // ensure the mapped cell isn't in a unvierse being used as the "outer"
    1970             :     // universe of a lattice in the OpenMC model
    1971             :     if (requires_mapping)
    1972     2066270 :       latticeOuterCheck(c, level);
    1973             : 
    1974     2066268 :     switch (phase)
    1975             :     {
    1976      634264 :       case coupling::density_and_temperature:
    1977             :       {
    1978      634264 :         _n_mapped_temp_density_elems++;
    1979      634264 :         break;
    1980             :       }
    1981      951584 :       case coupling::temperature:
    1982             :       {
    1983      951584 :         _n_mapped_temp_elems++;
    1984      951584 :         break;
    1985             :       }
    1986        1180 :       case coupling::density:
    1987             :       {
    1988        1180 :         _n_mapped_density_elems++;
    1989        1180 :         break;
    1990             :       }
    1991      571144 :       case coupling::none:
    1992             :       {
    1993      571144 :         _uncoupled_volume += element_volume;
    1994      571144 :         _n_mapped_none_elems++;
    1995      571144 :         break;
    1996             :       }
    1997           0 :       default:
    1998           0 :         mooseError("Unhandled CouplingFields enum!");
    1999             :     }
    2000             : 
    2001     2158172 :     auto cell_index = _particle.coord(level).cell();
    2002     2158172 :     auto cell_instance = cell_instance_at_level(_particle, level);
    2003             : 
    2004             :     cellInfo cell_info = {cell_index, cell_instance};
    2005             : 
    2006     2158172 :     if (openmc::model::cells[cell_index]->type_ != openmc::Fill::MATERIAL)
    2007      165624 :       _material_cells_only = false;
    2008             : 
    2009             :     // store the map of cells to elements that will be coupled via feedback or a tally
    2010     2158172 :     if (requires_mapping)
    2011     2066268 :       _cell_to_elem[cell_info].push_back(local_elem);
    2012             :   }
    2013             : 
    2014        3254 :   _communicator.sum(_n_mapped_temp_elems);
    2015        3254 :   _communicator.sum(_n_mapped_temp_density_elems);
    2016        3254 :   _communicator.sum(_n_mapped_density_elems);
    2017        3254 :   _communicator.sum(_n_mapped_none_elems);
    2018        3254 :   _communicator.sum(_uncoupled_volume);
    2019             : 
    2020             :   // if ANY rank finds a non-material cell, they will hold 0 (false)
    2021        3254 :   _communicator.min(_material_cells_only);
    2022             : 
    2023             :   // store the local mapping of cells to elements for convenience
    2024             :   _local_cell_to_elem = _cell_to_elem;
    2025             : 
    2026             :   // flatten the cell IDs and instances
    2027       14703 :   for (const auto & c : _cell_to_elem)
    2028             :   {
    2029       11449 :     auto cell_info = c.first;
    2030       11449 :     _flattened_ids.push_back(cell_info.first);
    2031       11449 :     _flattened_instances.push_back(cell_info.second);
    2032             :   }
    2033             : 
    2034        3254 :   _communicator.allgather(_flattened_ids);
    2035        3254 :   _communicator.allgather(_flattened_instances);
    2036             : 
    2037             :   // collect the _cell_to_elem onto all ranks
    2038        3254 :   std::vector<unsigned int> n_elems;
    2039        3254 :   std::vector<unsigned int> elems;
    2040       14703 :   for (const auto & c : _cell_to_elem)
    2041             :   {
    2042       11449 :     n_elems.push_back(c.second.size());
    2043     2076181 :     for (const auto & e : c.second)
    2044     2064732 :       elems.push_back(_local_to_global_elem[e]);
    2045             :   }
    2046             : 
    2047        3254 :   gatherCellVector(elems, n_elems, _cell_to_elem);
    2048             : 
    2049             :   // fill out the elem_to_cell structure
    2050             :   // TODO: figure out how to shrink this so we only store the mapping for active
    2051             :   // elements as opposed to the entire element hierarchy.
    2052        3254 :   _elem_to_cell.resize(getMooseMesh().nElem(), {UNMAPPED, UNMAPPED});
    2053       17695 :   for (const auto & c : _cell_to_elem)
    2054     3988125 :     for (const auto & e : c.second)
    2055     3973684 :       _elem_to_cell[e] = c.first;
    2056        3254 : }
    2057             : 
    2058             : void
    2059        2843 : OpenMCCellAverageProblem::getPointInCell()
    2060             : {
    2061             :   std::vector<Real> x;
    2062             :   std::vector<Real> y;
    2063             :   std::vector<Real> z;
    2064       13742 :   for (const auto & c : _local_cell_to_elem)
    2065             :   {
    2066             :     // we are only dealing with local elements here, no need to check for nullptr
    2067       10899 :     const Elem * elem = getMooseMesh().queryElemPtr(globalElemID(c.second[0]));
    2068       10899 :     const Point & p = elem->vertex_average();
    2069             : 
    2070       10899 :     x.push_back(p(0));
    2071       10899 :     y.push_back(p(1));
    2072       10899 :     z.push_back(p(2));
    2073             :   }
    2074             : 
    2075        2843 :   _communicator.allgather(x);
    2076        2843 :   _communicator.allgather(y);
    2077        2843 :   _communicator.allgather(z);
    2078             : 
    2079             :   // this will get a point from the lowest rank in each cell
    2080             :   _cell_to_point.clear();
    2081       29128 :   for (unsigned int i = 0; i < _flattened_ids.size(); ++i)
    2082             :   {
    2083             :     cellInfo cell_info = {_flattened_ids[i], _flattened_instances[i]};
    2084             :     if (!_cell_to_point.count(cell_info))
    2085       13743 :       _cell_to_point[cell_info] = Point(x[i], y[i], z[i]);
    2086             :   }
    2087        2843 : }
    2088             : 
    2089             : void
    2090         904 : OpenMCCellAverageProblem::resetTallies()
    2091             : {
    2092         904 :   if (_local_tallies.size() == 0)
    2093             :     return;
    2094             : 
    2095             :   // We initialize [Problem/Tallies] by forward iterating this vector. We need to delete them in
    2096             :   // reverse.
    2097        1162 :   for (int i = _local_tallies.size() - 1; i >= 0; --i)
    2098         614 :     _local_tallies[i]->resetTally();
    2099             : }
    2100             : 
    2101             : void
    2102        2813 : OpenMCCellAverageProblem::initializeTallies()
    2103             : {
    2104             :   // add trigger information for k, if present
    2105        2813 :   openmc::settings::keff_trigger.metric = triggerMetric(_k_trigger);
    2106             : 
    2107        2813 :   if (_local_tallies.size() == 0)
    2108             :     return;
    2109             : 
    2110             :   // Initialize all of the [Problem/Tallies].
    2111        5278 :   for (auto & local_tally : _local_tallies)
    2112        3160 :     local_tally->initializeTally();
    2113             : 
    2114             :   // Ensure that any tally editors don't apply to mapped tallies
    2115        2118 :   checkTallyEditorIDs();
    2116             : }
    2117             : 
    2118             : void
    2119           2 : OpenMCCellAverageProblem::latticeOuterError(const Point & c, int level) const
    2120             : {
    2121           2 :   const auto & cell = openmc::model::cells[_particle.coord(level).cell()];
    2122           2 :   std::stringstream msg;
    2123           2 :   msg << "The point " << c << " mapped to cell " << cell->id_
    2124             :       << " in the OpenMC model is inside a universe "
    2125             :          "used as the 'outer' universe of a lattice. "
    2126             :          "All cells used for mapping in lattices must be explicitly set "
    2127             :          "on the 'universes' attribute of lattice objects. "
    2128             :       << "If you want to obtain feedback or cell tallies here, you "
    2129             :          "will need to widen your lattice to have universes covering all of the space you "
    2130             :          "want feedback or cell tallies.\n\nIn other words, re-build your OpenMC model but replace "
    2131             :          "lattice.outer by simply creating extra rings/rows in your lattice to cover all the space "
    2132             :          "needed. For more information, see: "
    2133           2 :          "https://github.com/openmc-dev/openmc/issues/551.";
    2134           2 :   mooseError(msg.str());
    2135           0 : }
    2136             : 
    2137             : void
    2138     2066270 : OpenMCCellAverageProblem::latticeOuterCheck(const Point & c, int level) const
    2139             : {
    2140     4682560 :   for (int i = 0; i <= level; ++i)
    2141             :   {
    2142             :     const auto & coord = _particle.coord(i);
    2143             : 
    2144             :     // if there is no lattice at this level, move on
    2145     2616292 :     if (coord.lattice() == openmc::C_NONE)
    2146     2151358 :       continue;
    2147             : 
    2148      464934 :     const auto & lat = openmc::model::lattices[coord.lattice()];
    2149             : 
    2150             :     // if the lattice's outer universe isn't set, move on
    2151      464934 :     if (lat->outer_ == openmc::NO_OUTER_UNIVERSE)
    2152           0 :       continue;
    2153             : 
    2154      464934 :     if (coord.universe() != lat->outer_)
    2155      464932 :       continue;
    2156             : 
    2157             :     // move on if the lattice indices are valid (position is in the set of explicitly defined
    2158             :     // universes)
    2159           2 :     if (lat->are_valid_indices(coord.lattice_index()))
    2160           0 :       continue;
    2161             : 
    2162             :     // if we get here, the mapping is occurring in a universe that is not explicitly defined in the
    2163             :     // lattice
    2164           2 :     latticeOuterError(c, level);
    2165             :   }
    2166     2066268 : }
    2167             : 
    2168             : bool
    2169     2184688 : OpenMCCellAverageProblem::findCell(const Point & point)
    2170             : {
    2171     2184688 :   _particle.clear();
    2172             :   // Use a random direction to minimize "lost" virtual particles.
    2173     2184688 :   _particle.u() = {0.6339976, -0.538536, 0.555026};
    2174     2184688 :   _particle.u() /= _particle.u().norm();
    2175             : 
    2176     2184688 :   Point pt = transformPointToOpenMC(point);
    2177             : 
    2178     2184688 :   _particle.r() = {pt(0), pt(1), pt(2)};
    2179     2184688 :   return !openmc::exhaustive_find_cell(_particle);
    2180             : }
    2181             : 
    2182             : void
    2183        2048 : OpenMCCellAverageProblem::addExternalVariables()
    2184             : {
    2185             :   // We need to validate tallies here to we can add scores that may be missing.
    2186        2048 :   validateLocalTallies();
    2187             : 
    2188             :   // Add all of the auxvariables in which the [Tallies] block will store results.
    2189             :   unsigned int previous_valid_name_index = 0;
    2190        4697 :   for (unsigned int i = 0; i < _local_tallies.size(); ++i)
    2191             :   {
    2192        2665 :     _tally_var_ids.emplace_back();
    2193             : 
    2194             :     // Convert the subdomain ID map into a std::vector for addExternalVariable(...).
    2195             :     std::vector<SubdomainName> block_name_vec;
    2196        6371 :     for (const auto b : _local_tallies[i]->getBlocks())
    2197        7412 :       block_name_vec.emplace_back(mesh().getSubdomainName(b) != "" ? mesh().getSubdomainName(b)
    2198             :                                                                    : std::to_string(b));
    2199             : 
    2200             :     // We use this to check if a sequence of added tallies corresponds to a single translated mesh.
    2201             :     // If the number of names reported in getAuxVarNames is zero, the tally must store it's results
    2202             :     // in the variables added by the first mesh tally in the sequence.
    2203             :     bool is_instanced = _local_tallies[i]->getAuxVarNames().size() == 0;
    2204        2665 :     previous_valid_name_index = !is_instanced ? i : previous_valid_name_index;
    2205             : 
    2206        2665 :     const auto & names = _local_tallies[previous_valid_name_index]->getAuxVarNames();
    2207             : 
    2208        2665 :     _tally_ext_var_ids.emplace_back();
    2209        2665 :     if (_local_tallies[i]->hasOutputs())
    2210         167 :       _tally_ext_var_ids[i].resize(_local_tallies[i]->getOutputs().size());
    2211             : 
    2212        7042 :     for (unsigned int j = 0; j < names.size(); ++j)
    2213             :     {
    2214        4381 :       if (is_instanced)
    2215         544 :         _tally_var_ids[i].push_back(
    2216             :             _tally_var_ids[previous_valid_name_index][j]); // Use variables from first in sequence.
    2217             :       else
    2218        7670 :         _tally_var_ids[i].push_back(addExternalVariable(names[j], "Tally", &block_name_vec));
    2219             : 
    2220        4377 :       if (_local_tallies[i]->hasOutputs())
    2221             :       {
    2222             :         const auto & outs = _local_tallies[i]->getOutputs();
    2223         374 :         for (std::size_t k = 0; k < outs.size(); ++k)
    2224             :         {
    2225         191 :           std::string n = names[j] + "_" + outs[k];
    2226         191 :           if (is_instanced)
    2227          16 :             _tally_ext_var_ids[i][k].push_back(
    2228             :                 _tally_ext_var_ids[previous_valid_name_index][k]
    2229             :                                   [j]); // Use variables from first in sequence.
    2230             :           else
    2231         350 :             _tally_ext_var_ids[i][k].push_back(addExternalVariable(n, "Tally", &block_name_vec));
    2232             :         }
    2233             :       }
    2234             :     }
    2235        2661 :   }
    2236             : 
    2237             :   // create the variable(s) that will be used to receive density
    2238             :   _subdomain_to_density_vars.clear();
    2239        2561 :   for (const auto & v : _density_vars_to_blocks)
    2240             :   {
    2241         529 :     auto number = addExternalVariable(v.first, "density feedback", &v.second);
    2242             : 
    2243         529 :     auto ids = getMooseMesh().getSubdomainIDs(v.second);
    2244        1111 :     for (const auto & s : ids)
    2245        1164 :       _subdomain_to_density_vars[s] = {number, v.first};
    2246         529 :   }
    2247             : 
    2248             :   // create the variable(s) that will be used to receive temperature
    2249             :   _subdomain_to_temp_vars.clear();
    2250        3500 :   for (const auto & v : _temp_vars_to_blocks)
    2251             :   {
    2252        1468 :     auto number = addExternalVariable(v.first, "temperature feedback", &v.second);
    2253             : 
    2254        1468 :     auto ids = getMooseMesh().getSubdomainIDs(v.second);
    2255        3723 :     for (const auto & s : ids)
    2256        4510 :       _subdomain_to_temp_vars[s] = {number, v.first};
    2257        1468 :   }
    2258             : 
    2259        2032 :   if (_output_cell_mapping && _needs_to_map_cells)
    2260             :   {
    2261        1696 :     std::string auxk_type = "CellIDAux";
    2262        1696 :     InputParameters params = _factory.getValidParams(auxk_type);
    2263        3392 :     addExternalVariable("cell_id", "cell mapping");
    2264        3392 :     params.set<AuxVariableName>("variable") = "cell_id";
    2265        3392 :     addAuxKernel(auxk_type, "cell_id", params);
    2266             : 
    2267             :     auxk_type = "CellInstanceAux";
    2268        1696 :     params = _factory.getValidParams(auxk_type);
    2269        3392 :     addExternalVariable("cell_instance", "cell mapping");
    2270        3392 :     params.set<AuxVariableName>("variable") = "cell_instance";
    2271        1696 :     addAuxKernel(auxk_type, "cell_instance", params);
    2272        3392 :   }
    2273             :   else
    2274             :     _console << "Skipping output of 'cell_id' and 'cell_instance' because 'temperature_blocks', "
    2275         336 :                 "'density_blocks', and 'tally_blocks' are all empty"
    2276         336 :              << std::endl;
    2277        2032 : }
    2278             : 
    2279             : void
    2280        2526 : OpenMCCellAverageProblem::externalSolve()
    2281             : {
    2282             :   // if using Dufek-Gudowski acceleration and this is not the first iteration, update
    2283             :   // the number of particles; we put this here so that changing the number of particles
    2284             :   // doesn't intrude with any other postprocessing routines that happen outside this class's purview
    2285        2526 :   if (_relaxation == relaxation::dufek_gudowski && !firstSolve())
    2286          32 :     dufekGudowskiParticleUpdate();
    2287             :   else
    2288             :   {
    2289        4988 :     if (isParamValid("particles"))
    2290             :     {
    2291         108 :       if (*_particles <= 0.0)
    2292           2 :         mooseError(
    2293             :             "'particles' must be a positive integer. Try `execute_on = 'timestep_begin'` in "
    2294             :             "your postprocessor and check that the postprocessor value itself is not less than "
    2295             :             "or equal to zero.");
    2296         106 :       int64_t n = std::llround(*_particles);
    2297         106 :       openmc::settings::n_particles = n;
    2298             :     }
    2299             :   }
    2300             : 
    2301        2524 :   OpenMCProblemBase::externalSolve();
    2302        2514 : }
    2303             : 
    2304             : std::map<OpenMCCellAverageProblem::cellInfo, Real>
    2305        1368 : OpenMCCellAverageProblem::computeVolumeWeightedCellInput(
    2306             :     const std::map<SubdomainID, std::pair<unsigned int, std::string>> & var_num,
    2307             :     const std::vector<coupling::CouplingFields> * phase = nullptr) const
    2308             : {
    2309        1368 :   const auto & sys_number = _aux->number();
    2310             : 
    2311             :   // collect the volume-weighted product across local ranks
    2312             :   std::vector<Real> volume_product;
    2313       11056 :   for (const auto & c : _local_cell_to_elem)
    2314             :   {
    2315             :     // if a specific phase is passed in, only evaluate for those elements in the phase;
    2316             :     // in order to have the correct array sizes for gatherCellSum, we set zero values
    2317             :     // for any cells that aren't in the correct phase, and leave it up to the send...ToOpenMC()
    2318             :     // routines to properly shield against incorrect phases
    2319        9688 :     if (phase)
    2320             :     {
    2321        9688 :       if (std::find(phase->begin(), phase->end(), cellFeedback(c.first)) == phase->end())
    2322             :       {
    2323        1692 :         volume_product.push_back(0.0 /* dummy value */);
    2324        1692 :         continue;
    2325             :       }
    2326             :     }
    2327             : 
    2328        7996 :     Real product = 0.0;
    2329      951404 :     for (const auto & e : c.second)
    2330             :     {
    2331             :       // we are only accessing local elements here, so no need to check for nullptr
    2332      943408 :       const auto * elem = getMooseMesh().queryElemPtr(globalElemID(e));
    2333      943408 :       auto v = var_num.at(elem->subdomain_id()).first;
    2334      943408 :       auto dof_idx = elem->dof_number(sys_number, v, 0);
    2335      943408 :       product += _serialized_solution(dof_idx) * elem->volume();
    2336             :     }
    2337             : 
    2338        7996 :     volume_product.push_back(product);
    2339             :   }
    2340             : 
    2341             :   std::map<cellInfo, Real> global_volume_product;
    2342        1368 :   gatherCellSum(volume_product, global_volume_product);
    2343             : 
    2344        1368 :   return global_volume_product;
    2345        1368 : }
    2346             : 
    2347             : void
    2348        2488 : OpenMCCellAverageProblem::sendTemperatureToOpenMC() const
    2349             : {
    2350        2488 :   if (!_specified_temperature_feedback)
    2351        1458 :     return;
    2352             : 
    2353        1030 :   _console << "Sending temperature to OpenMC cells... " << std::endl;
    2354             : 
    2355        1030 :   double maximum = std::numeric_limits<double>::min();
    2356        1030 :   double minimum = std::numeric_limits<double>::max();
    2357             : 
    2358             :   // collect the volume-temperature product across local ranks
    2359             :   std::vector<coupling::CouplingFields> phase = {coupling::temperature,
    2360        1030 :                                                  coupling::density_and_temperature};
    2361             :   std::map<cellInfo, Real> cell_vol_temp =
    2362        1030 :       computeVolumeWeightedCellInput(_subdomain_to_temp_vars, &phase);
    2363             : 
    2364             :   std::unordered_set<cellInfo> cells_already_set;
    2365             : 
    2366        9368 :   for (const auto & c : _cell_to_elem)
    2367             :   {
    2368        8344 :     auto cell_info = c.first;
    2369        8344 :     if (!hasTemperatureFeedback(cell_info))
    2370          32 :       continue;
    2371             : 
    2372        8312 :     Real average_temp = cell_vol_temp.at(cell_info) / _cell_to_elem_volume.at(cell_info);
    2373             : 
    2374        8312 :     minimum = std::min(minimum, average_temp);
    2375        8312 :     maximum = std::max(maximum, average_temp);
    2376             : 
    2377        8312 :     if (_verbose)
    2378       12496 :       _console << "Setting cell " << printCell(cell_info) << " ["
    2379        6248 :                << _cell_to_n_contained.at(cell_info)
    2380        6248 :                << " contained cells] to temperature (K): " << std::setw(4) << average_temp
    2381        6248 :                << std::endl;
    2382             : 
    2383        8312 :     containedCells contained_cells = containedMaterialCells(cell_info);
    2384             : 
    2385      190532 :     for (const auto & contained : contained_cells)
    2386             :     {
    2387    11575284 :       for (const auto & instance : contained.second)
    2388             :       {
    2389             :         cellInfo ci = {contained.first, instance};
    2390             :         if (cells_already_set.count(ci))
    2391             :         {
    2392             :           double T;
    2393           2 :           openmc_cell_get_temperature(ci.first, &ci.second, &T);
    2394             : 
    2395           6 :           mooseError("Cell " + std::to_string(cellID(contained.first)) + ", instance " +
    2396           2 :                      std::to_string(instance) +
    2397           4 :                      " has already had its temperature set by Cardinal to " + std::to_string(T) +
    2398             :                      "! This indicates a problem with how you have built your geometry, because "
    2399             :                      "this cell is trying to receive a distribution of temperatures in space, but "
    2400             :                      "each successive set-temperature operation is only overwriting the previous "
    2401             :                      "value.\n\nThis error most often appears when you are filling a LATTICE into "
    2402             :                      "multiple cells. One fix is to first place that lattice into a universe, and "
    2403             :                      "then fill that UNIVERSE into multiple cells.\n\nFor more information, please "
    2404             :                      "consult https://github.com/neams-th-coe/cardinal/pull/918.");
    2405             :         }
    2406             : 
    2407             :         cells_already_set.insert(ci);
    2408    11393062 :         setCellTemperature(contained.first, instance, average_temp, cell_info);
    2409             :       }
    2410             :     }
    2411             :   }
    2412             : 
    2413        1024 :   if (!_verbose)
    2414          82 :     _console << " Sent cell-averaged min/max (K): " << minimum << ", " << maximum << std::endl;
    2415        1024 : }
    2416             : 
    2417             : OpenMCCellAverageProblem::cellInfo
    2418     4075392 : OpenMCCellAverageProblem::firstContainedMaterialCell(const cellInfo & cell_info) const
    2419             : {
    2420     4075392 :   const auto & contained_cells = containedMaterialCells(cell_info);
    2421             :   const auto & instances = contained_cells.begin()->second;
    2422             :   cellInfo first_cell = {contained_cells.begin()->first, instances[0]};
    2423     4075392 :   return first_cell;
    2424             : }
    2425             : 
    2426             : void
    2427        2482 : OpenMCCellAverageProblem::sendDensityToOpenMC() const
    2428             : {
    2429        2482 :   if (!_specified_density_feedback)
    2430        2144 :     return;
    2431             : 
    2432         338 :   _console << "Sending density to OpenMC cells... " << std::endl;
    2433             : 
    2434         338 :   double maximum = std::numeric_limits<double>::min();
    2435         338 :   double minimum = std::numeric_limits<double>::max();
    2436             : 
    2437             :   // collect the volume-density product across local ranks
    2438             :   std::vector<coupling::CouplingFields> phase = {coupling::density,
    2439         338 :                                                  coupling::density_and_temperature};
    2440             :   std::map<cellInfo, Real> cell_vol_density =
    2441         338 :       computeVolumeWeightedCellInput(_subdomain_to_density_vars, &phase);
    2442             : 
    2443        4048 :   for (const auto & c : _cell_to_elem)
    2444             :   {
    2445        3714 :     auto cell_info = c.first;
    2446             : 
    2447        3714 :     if (!hasDensityFeedback(cell_info))
    2448        2096 :       continue;
    2449             : 
    2450        1618 :     Real average_density = cell_vol_density.at(cell_info) / _cell_to_elem_volume.at(cell_info);
    2451             : 
    2452        1618 :     minimum = std::min(minimum, average_density);
    2453        1618 :     maximum = std::max(maximum, average_density);
    2454             : 
    2455        1618 :     if (_verbose)
    2456        3236 :       _console << "Setting cell " << printCell(cell_info) << " to density (kg/m3): " << std::setw(4)
    2457        1618 :                << average_density << std::endl;
    2458             : 
    2459        1618 :     setCellDensity(average_density, cell_info);
    2460             :   }
    2461             : 
    2462         334 :   if (!_verbose)
    2463           0 :     _console << " Sent cell-averaged min/max (kg/m3): " << minimum << ", " << maximum << std::endl;
    2464         334 : }
    2465             : 
    2466             : Real
    2467      471842 : OpenMCCellAverageProblem::tallyMultiplier(const std::string & score_name,
    2468             :                                           const Real & local_mean_tally) const
    2469             : {
    2470      471842 :   if (!isHeatingScore(score_name))
    2471             :   {
    2472             :     // we need to get an effective source rate (particles / second) in order to
    2473             :     // normalize the tally
    2474      189028 :     Real source = local_mean_tally;
    2475      189028 :     if (_run_mode == openmc::RunMode::EIGENVALUE)
    2476      178152 :       source *= *_power / EV_TO_JOULE / _source_rate_norm_tally->getMean(_source_rate_score);
    2477             :     else
    2478       10876 :       source *= *_source_strength;
    2479             : 
    2480             :     // - Reaction rate scores have units of reactions/src (OpenMC) or reactions/s (Cardinal).
    2481             :     // - 'inverse-velocity' has units of particles*s/src (OpenMC) or particles (Cardinal).
    2482             :     //   This score is flux-weighted, and must be divided by the flux to recover the true
    2483             :     //   inverse velocity, which has units of s/cm.
    2484             :     // - 'decay-rate' has units of reactions/src/s (OpenMC) or reactions/s^2 (Cardinal).
    2485             :     //   This score is weighted by the delayed fission rate, and must be divided by
    2486             :     //   `delayed-nu-fission` to obtain the true decay rate, which has units of 1/s.
    2487             :     // - 'damage-energy' has units of eV/src (OpenMC) or eV/s (Cardinal). While the units of
    2488             :     //   damage-energy are the same as a heating tally, we don't normalize it like one as it's
    2489             :     //   used as an intermediate to compute DPA.
    2490      362720 :     if (isReactionRateScore(score_name) || score_name == "inverse-velocity" ||
    2491      361608 :         score_name == "decay-rate" || score_name == "damage-energy")
    2492             :       return source;
    2493             : 
    2494      172044 :     if (score_name == "flux")
    2495      172044 :       return source / _scaling;
    2496             :     else
    2497           0 :       mooseError("Unhandled tally score enum!");
    2498             :   }
    2499             :   else
    2500             :   {
    2501             :     // Heating tallies have units of eV / source particle
    2502      282814 :     if (_run_mode == openmc::RunMode::EIGENVALUE)
    2503      282734 :       return *_power;
    2504             :     else
    2505          80 :       return *_source_strength * EV_TO_JOULE * local_mean_tally;
    2506             :   }
    2507             : }
    2508             : 
    2509             : void
    2510          32 : OpenMCCellAverageProblem::dufekGudowskiParticleUpdate()
    2511             : {
    2512          32 :   int64_t n = (_n_particles_1 + std::sqrt(_n_particles_1 * _n_particles_1 +
    2513          32 :                                           4.0 * _n_particles_1 * _total_n_particles)) /
    2514          32 :               2.0;
    2515          32 :   openmc::settings::n_particles = n;
    2516          32 : }
    2517             : 
    2518             : void
    2519        5056 : OpenMCCellAverageProblem::syncSolutions(ExternalProblem::Direction direction)
    2520             : {
    2521        5056 :   OpenMCProblemBase::syncSolutions(direction);
    2522             : 
    2523             :   // We can skip syncronizing the solution when running with adaptivity
    2524             :   // and the mesh hasn't changed. This only applies to steady-state calculations
    2525             :   // as the mesh is adapted once per timestep in a transient calculation.
    2526        5056 :   if (_has_adaptivity && !_run_on_adaptivity_cycle)
    2527             :     return;
    2528             : 
    2529        5020 :   _aux->serializeSolution();
    2530             : 
    2531        5020 :   switch (direction)
    2532             :   {
    2533        2524 :     case ExternalProblem::Direction::TO_EXTERNAL_APP:
    2534             :     {
    2535             :       // update the [Mesh] internally, so that if we have the skinner we then propagate those
    2536             :       // changes to the OpenMC geometry
    2537        2524 :       if (_use_displaced)
    2538             :       {
    2539          94 :         _console << "Updating the displaced mesh..." << std::endl;
    2540          94 :         _displaced_problem->updateMesh();
    2541             :       }
    2542             : 
    2543             :       // Reinitialize the MOOSE -> OpenMC coupling.
    2544        2524 :       reinitCouplingAndApplyFeedback();
    2545             : 
    2546        2508 :       break;
    2547             :     }
    2548        2496 :     case ExternalProblem::Direction::FROM_EXTERNAL_APP:
    2549             :     {
    2550        2496 :       _console << "Extracting OpenMC tallies..." << std::endl;
    2551             : 
    2552        2496 :       if (_local_tallies.size() == 0)
    2553             :         break;
    2554             : 
    2555             :       // Loop over all of the tallies and calculate their sums and averages.
    2556        5516 :       for (auto & local_tally : _local_tallies)
    2557        3368 :         local_tally->computeSumAndMean();
    2558             : 
    2559             :       // Recompute sums and means for tallies that are linked to other tallies.
    2560             :       // This is used to perform local normalization for translated copies of mesh tallies.
    2561             :       // These loops must be separate due to data dependencies.
    2562        5516 :       for (auto & local_tally : _local_tallies)
    2563        3368 :         local_tally->gatherLinkedSum();
    2564        5516 :       for (auto & local_tally : _local_tallies)
    2565        3368 :         local_tally->renormalizeLinkedTallies();
    2566             : 
    2567             :       // Loop over the tallies to relax and normalize their results score by score. Then, store the
    2568             :       // results.
    2569        5514 :       for (unsigned int i = 0; i < _local_tallies.size(); ++i)
    2570             :       {
    2571        3368 :         _local_tallies[i]->relaxAndNormalizeTally();
    2572             : 
    2573        7176 :         for (unsigned int score = 0; score < _local_tallies[i]->getScores().size(); ++score)
    2574             :         {
    2575             :           // Store the tally results.
    2576        7620 :           _local_tallies[i]->storeResults(_tally_var_ids[i], score, "relaxed");
    2577             : 
    2578             :           // Store additional tally outputs.
    2579        3810 :           if (_local_tallies[i]->hasOutputs())
    2580             :           {
    2581             :             const auto & outs = _local_tallies[i]->getOutputs();
    2582         584 :             for (unsigned int j = 0; j < outs.size(); ++j)
    2583         296 :               _local_tallies[i]->storeResults(_tally_ext_var_ids[i][j], score, outs[j]);
    2584             :           }
    2585             :         }
    2586             :       }
    2587             : 
    2588             :       break;
    2589             :     }
    2590           0 :     default:
    2591           0 :       mooseError("Unhandled Direction enum in OpenMCCellAverageProblem!");
    2592             :   }
    2593             : 
    2594        5002 :   _first_transfer = false;
    2595        5002 :   _aux->solution().close();
    2596        5002 :   _aux->system().update();
    2597             : }
    2598             : 
    2599             : void
    2600        3336 : OpenMCCellAverageProblem::reinitCouplingAndApplyFeedback()
    2601             : {
    2602             : #ifdef ENABLE_DAGMC
    2603        1697 :   if (_skinner)
    2604             :   {
    2605             :     // Update the OpenMC geometry to take into account skinning. This also calls
    2606             :     // _skinner->update().
    2607          32 :     updateOpenMCGeometry();
    2608             : 
    2609             :     // regenerate the DAGMC geometry
    2610          32 :     reloadDAGMC();
    2611             :   }
    2612             : #endif
    2613             : 
    2614        3336 :   if (_need_to_reinit_coupling)
    2615             :   {
    2616         904 :     if (_volume_calc)
    2617           2 :       _volume_calc->resetVolumeCalculation();
    2618             : 
    2619         904 :     resetTallies();
    2620         904 :     setupProblem();
    2621             :   }
    2622             : 
    2623             :   // Change nuclide composition of material; we put this here so that we can still then change
    2624             :   // the _overall_ density (like due to thermal expansion, which does not change the relative
    2625             :   // amounts of the different nuclides)
    2626        3336 :   sendNuclideDensitiesToOpenMC();
    2627             : 
    2628        3332 :   if (_first_transfer && (_specified_temperature_feedback || _specified_density_feedback))
    2629             :   {
    2630             :     std::string incoming_transfer =
    2631        2231 :         _specified_density_feedback ? "temperature and density" : "temperature";
    2632             : 
    2633        1350 :     switch (_initial_condition)
    2634             :     {
    2635           2 :       case coupling::hdf5:
    2636             :       {
    2637             :         // if we're reading temperature and density from an existing HDF5 file,
    2638             :         // we don't need to send anything in to OpenMC, so we can leave.
    2639           2 :         importProperties();
    2640           0 :         _console << "Skipping " << incoming_transfer
    2641           0 :                  << " transfer into OpenMC because 'initial_properties = hdf5'" << std::endl;
    2642           0 :         return;
    2643             :       }
    2644             :       case coupling::moose:
    2645             :       {
    2646             :         // transfer will happen from MOOSE - proceed normally
    2647             :         break;
    2648             :       }
    2649         842 :       case coupling::xml:
    2650             :       {
    2651             :         // if we're just using whatever temperature and density are already in the XML
    2652             :         // files, we don't need to send anything in to OpenMC, so we can leave.
    2653         842 :         _console << "Skipping " << incoming_transfer
    2654         842 :                  << " transfer into OpenMC because 'initial_properties = xml'" << std::endl;
    2655         842 :         return;
    2656             :       }
    2657           0 :       default:
    2658           0 :         mooseError("Unhandled OpenMCInitialConditionEnum!");
    2659             :     }
    2660             :   }
    2661             : 
    2662             :   // Because we require at least one of fluid_blocks and solid_blocks, we are guaranteed
    2663             :   // to be setting the temperature of all of the cells in cell_to_elem - only for the density
    2664             :   // transfer do we need to filter for the fluid cells
    2665        2488 :   sendTemperatureToOpenMC();
    2666             : 
    2667        2482 :   sendDensityToOpenMC();
    2668             : 
    2669        2478 :   if (_export_properties)
    2670           0 :     openmc_properties_export("properties.h5");
    2671             : 
    2672             :   // After setting cell temperatures, we need to re-initialize MGXS data as temperature
    2673             :   // interpolation is performed on initialization. Verbosity is temporarily modified here
    2674             :   // as the user has seen the MGXS initialization info previously.
    2675        2478 :   if (!openmc::settings::run_CE)
    2676             :   {
    2677          40 :     auto initial_verbosity = openmc::settings::verbosity;
    2678          40 :     openmc::settings::verbosity = 1;
    2679             :     // Clear the MGXS manager.
    2680          40 :     openmc::data::mg = {};
    2681             :     // Reload the MGXS data.
    2682          40 :     openmc::data::mg.read_header(openmc::settings::path_cross_sections);
    2683          40 :     openmc::put_mgxs_header_data_to_globals();
    2684          40 :     openmc::finalize_cross_sections();
    2685          40 :     openmc::settings::verbosity = initial_verbosity;
    2686             :   }
    2687             : }
    2688             : 
    2689             : void
    2690         812 : OpenMCCellAverageProblem::critSearchStep()
    2691             : {
    2692         812 :   _aux->serializeSolution();
    2693             : 
    2694             :   // Reinitialize the OpenMC coupling prior to the execution of
    2695             :   // a criticality search step.
    2696         812 :   reinitCouplingAndApplyFeedback();
    2697             : 
    2698         812 :   _aux->solution().close();
    2699         812 :   _aux->system().update();
    2700         812 : }
    2701             : 
    2702             : void
    2703        2032 : OpenMCCellAverageProblem::createQRules(QuadratureType type,
    2704             :                                        Order order,
    2705             :                                        Order volume_order,
    2706             :                                        Order face_order,
    2707             :                                        SubdomainID block,
    2708             :                                        const bool allow_negative_qweights)
    2709             : {
    2710             :   // start copy: Copied from base class's createQRules in order to retain the same default behavior
    2711        2032 :   if (order == INVALID_ORDER)
    2712             :   {
    2713        2032 :     order = getNonlinearSystemBase(0).getMinQuadratureOrder();
    2714        2032 :     if (order < getAuxiliarySystem().getMinQuadratureOrder())
    2715        1864 :       order = getAuxiliarySystem().getMinQuadratureOrder();
    2716             :   }
    2717             : 
    2718        2032 :   if (volume_order == INVALID_ORDER)
    2719        2032 :     volume_order = order;
    2720             : 
    2721        2032 :   if (face_order == INVALID_ORDER)
    2722             :     face_order = order;
    2723             :   // end copy
    2724             : 
    2725             :   // The approximations made in elem->volume() are only valid for Gauss and Monomial quadratures
    2726             :   // if they are second order or above
    2727        4064 :   if (type == Moose::stringToEnum<QuadratureType>("GAUSS"))
    2728        4064 :     setMinimumVolumeQRules(volume_order, "GAUSS");
    2729        4064 :   if (type == Moose::stringToEnum<QuadratureType>("MONOMIAL"))
    2730           0 :     setMinimumVolumeQRules(volume_order, "MONOMIAL");
    2731        4064 :   if (type == Moose::stringToEnum<QuadratureType>("GAUSS_LOBATTO"))
    2732           0 :     setMinimumVolumeQRules(volume_order, "GAUSS_LOBATTO");
    2733             : 
    2734             :   // Some quadrature rules don't ever seem to give a matching elem->volume() with the MOOSE
    2735             :   // volume integrations
    2736        6096 :   if (type == Moose::stringToEnum<QuadratureType>("GRID") ||
    2737        6096 :       type == Moose::stringToEnum<QuadratureType>("TRAP"))
    2738           0 :     mooseError(
    2739             :         "The ",
    2740           0 :         std::to_string(type),
    2741             :         " quadrature set will never match the '_current_elem_volume' used to compute\n"
    2742             :         "integrals in MOOSE. This means that the tally computed by OpenMC is normalized by\n"
    2743             :         "a different volume than used for MOOSE volume integrations, such that the specified "
    2744             :         "'power' or 'source_strength'\n"
    2745             :         "would not be respected. Please switch to a different quadrature set.");
    2746             : 
    2747        2032 :   FEProblemBase::createQRules(
    2748             :       type, order, volume_order, face_order, block, allow_negative_qweights);
    2749        2032 : }
    2750             : 
    2751             : void
    2752        2032 : OpenMCCellAverageProblem::setMinimumVolumeQRules(Order & volume_order,
    2753             :                                                  const std::string & /* type */)
    2754             : {
    2755        4064 :   if (volume_order < Moose::stringToEnum<Order>("SECOND"))
    2756        2030 :     volume_order = SECOND;
    2757        2032 : }
    2758             : 
    2759             : double
    2760      184147 : OpenMCCellAverageProblem::cellMappedVolume(const cellInfo & cell_info) const
    2761             : {
    2762      184147 :   return _cell_to_elem_volume.at(cell_info);
    2763             : }
    2764             : 
    2765             : double
    2766     4075392 : OpenMCCellAverageProblem::cellTemperature(const cellInfo & cell_info) const
    2767             : {
    2768     4075392 :   auto material_cell = firstContainedMaterialCell(cell_info);
    2769             : 
    2770             :   double T;
    2771     4075392 :   int err = openmc_cell_get_temperature(material_cell.first, &material_cell.second, &T);
    2772     4075392 :   catchOpenMCError(err, "get temperature of cell " + printCell(cell_info));
    2773     4075392 :   return T;
    2774             : }
    2775             : 
    2776             : void
    2777          32 : OpenMCCellAverageProblem::reloadDAGMC()
    2778             : {
    2779             : #ifdef ENABLE_DAGMC
    2780          64 :   _dagmc.reset(new moab::DagMC(_skinner->moabPtr(),
    2781             :                                0.0 /* overlap tolerance, default */,
    2782             :                                0.001 /* numerical precision, default */,
    2783          96 :                                0 /* verbosity */));
    2784             : 
    2785             :   // Set up geometry in DagMC from already-loaded mesh
    2786          32 :   _dagmc->load_existing_contents();
    2787             : 
    2788             :   // Initialize acceleration data structures
    2789          32 :   _dagmc->init_OBBTree();
    2790             : 
    2791             :   // Get an iterator to the DAGMC universe unique ptr
    2792             :   auto univ_it =
    2793          32 :       openmc::model::universes.begin() + openmc::model::universe_map.at(_dagmc_universe_id);
    2794             : 
    2795             :   // Remove the old universe
    2796             :   openmc::model::universes.erase(univ_it);
    2797             : 
    2798             :   // Create new DAGMC universe
    2799          32 :   openmc::model::universes.emplace_back(std::make_unique<openmc::DAGUniverse>(_dagmc, "", true));
    2800          32 :   _dagmc_universe_id = openmc::model::universes.back()->id_;
    2801             : 
    2802             :   openmc::model::universe_map.clear();
    2803          67 :   for (int32_t i = 0; i < openmc::model::universes.size(); ++i)
    2804          35 :     openmc::model::universe_map[openmc::model::universes[i]->id_] = i;
    2805             : 
    2806          32 :   if (!_dagmc_root_universe)
    2807           3 :     openmc::model::cells[openmc::model::cell_map.at(_cell_using_dagmc_universe_id)]->fill_ =
    2808           3 :         _dagmc_universe_id;
    2809             : 
    2810          32 :   _console << "Re-generating OpenMC model with " << openmc::model::cells.size() << " cells... "
    2811          32 :            << std::endl;
    2812             : 
    2813             :   // Add cells to universes
    2814          32 :   openmc::populate_universes();
    2815             : 
    2816             :   // Set the root universe
    2817          32 :   openmc::model::root_universe = openmc::find_root_universe();
    2818          32 :   openmc::check_dagmc_root_univ();
    2819             : 
    2820             :   // Final geometry setup
    2821          32 :   openmc::finalize_geometry();
    2822             : 
    2823             :   // Finalize cross sections; we manually change the verbosity here because if skinning is
    2824             :   // enabled, we don't want to overwhelm the user with excess console output showing info
    2825             :   // which ultimately is no different from that shown on initialization
    2826          32 :   auto initial_verbosity = openmc::settings::verbosity;
    2827          32 :   openmc::settings::verbosity = 1;
    2828          32 :   openmc::finalize_cross_sections();
    2829             : 
    2830             :   // Finalize DAGMC cell densities after setting up the new geometry. CSG cells (and
    2831             :   // eventually non-skinned DAGMC cells) already have their densities finalized.
    2832         336 :   for (auto & c : openmc::model::cells)
    2833             :   {
    2834         304 :     if (c->geom_type() == openmc::GeometryType::CSG)
    2835           3 :       continue;
    2836             : 
    2837         301 :     c->density_mult_ = {1.0};
    2838             :   }
    2839             : 
    2840             :   // Needed to obtain correct cell instances
    2841          32 :   openmc::prepare_distribcell();
    2842          32 :   openmc::settings::verbosity = initial_verbosity;
    2843             : #endif
    2844          32 : }
    2845             : 
    2846             : void
    2847         582 : OpenMCCellAverageProblem::addFilter(const std::string & type,
    2848             :                                     const std::string & name,
    2849             :                                     InputParameters & moose_object_pars)
    2850             : {
    2851        1134 :   auto filter = addObject<FilterBase>(type, name, moose_object_pars, false)[0];
    2852         552 :   _filters[name] = filter;
    2853         552 : }
    2854             : 
    2855             : std::shared_ptr<TallyBase>
    2856        2735 : OpenMCCellAverageProblem::addTally(const std::string & type,
    2857             :                                    const std::string & name,
    2858             :                                    InputParameters & moose_object_pars)
    2859             : {
    2860        5427 :   auto tally = addObject<TallyBase>(type, name, moose_object_pars, false)[0];
    2861        2692 :   _local_tallies.push_back(tally);
    2862             : 
    2863             :   // Set the relaxation scheme.
    2864        8075 :   tally->setRelaxation(_relaxation, getParam<Real>("relaxation_factor"));
    2865             : 
    2866             :   const auto & tally_scores = tally->getScores();
    2867        5782 :   for (unsigned int i = 0; i < tally_scores.size(); ++i)
    2868             :   {
    2869             :     // Populate a map which counts the number of times a score is referenced by local tallies.
    2870             :     // Used for error checking.
    2871             :     if (_score_count.count(tally_scores[i]) == 0)
    2872        2623 :       _score_count[tally_scores[i]] = 1;
    2873             :     else
    2874         468 :       _score_count[tally_scores[i]]++;
    2875             : 
    2876             :     // Add the local tally's score to the list of scores if we don't have it yet.
    2877        3091 :     if (std::find(_all_tally_scores.begin(), _all_tally_scores.end(), tally_scores[i]) ==
    2878             :         _all_tally_scores.end())
    2879        2623 :       _all_tally_scores.push_back(tally_scores[i]);
    2880             :   }
    2881             : 
    2882        2691 :   return tally;
    2883             : }
    2884             : 
    2885             : void
    2886        2048 : OpenMCCellAverageProblem::validateLocalTallies()
    2887             : {
    2888             :   // We can skip this check if we don't have tallies.
    2889        2048 :   if (_local_tallies.size() == 0)
    2890             :     return;
    2891             : 
    2892             :   // Make sure we can assume that tallies can be separate.
    2893        1683 :   if (_assume_separate_tallies)
    2894             :   {
    2895          24 :     for (const auto & tally : _local_tallies)
    2896          14 :       if (tally->addingGlobalTally())
    2897           2 :         paramError("assume_separate_tallies",
    2898             :                    "Cannot assume separate tallies when either of 'check_tally_sum' or"
    2899             :                    "'normalize_by_global_tally' is true!");
    2900             : 
    2901          10 :     if (_local_tallies.size() > 1)
    2902           2 :       paramError("assume_separate_tallies",
    2903             :                  "Cannot assume separate tallies when there are multiple tallies added in the "
    2904             :                  "[Tallies] block!");
    2905             :   }
    2906             : 
    2907             :   // need some special treatment for non-heating scores, in eigenvalue mode
    2908             :   bool has_non_heating_score = false;
    2909        4288 :   for (const auto & t : _all_tally_scores)
    2910        2609 :     if (!isHeatingScore(t))
    2911             :       has_non_heating_score = true;
    2912             : 
    2913        1679 :   if (has_non_heating_score && _run_mode == openmc::RunMode::EIGENVALUE)
    2914             :   {
    2915             :     std::string non_heating_scores;
    2916        1788 :     for (const auto & e : _all_tally_scores)
    2917             :     {
    2918        1284 :       if (!isHeatingScore(e))
    2919             :       {
    2920         790 :         std::string l = e;
    2921             :         std::replace(l.begin(), l.end(), '-', '_');
    2922        1580 :         non_heating_scores += "" + l + ", ";
    2923             :       }
    2924             :     }
    2925             : 
    2926         504 :     if (non_heating_scores.length() > 0)
    2927         504 :       non_heating_scores.erase(non_heating_scores.length() - 2);
    2928             : 
    2929        1006 :     checkRequiredParam(_pars,
    2930             :                        "source_rate_normalization",
    2931         504 :                        "using a non-heating tally (" + non_heating_scores + ") in eigenvalue mode");
    2932         502 :     const auto & norm = getParam<MooseEnum>("source_rate_normalization");
    2933        1004 :     std::string n = enumToTallyScore(norm);
    2934             : 
    2935         502 :     if (_local_tallies.size() > 1)
    2936             :     {
    2937             :       if (_score_count.count(n) == 0)
    2938           2 :         mooseError("The local tallies added in the [Tallies] block do not contain the requested "
    2939           2 :                    "heating score " +
    2940           0 :                    n +
    2941             :                    ". You must either add this score in one of the tallies or choose a different "
    2942             :                    "heating score.");
    2943             : 
    2944         416 :       if (_score_count.at(n) > 1)
    2945             :       {
    2946             :         // Edge case: multiple scores from linked MeshTally objects.
    2947          36 :         unsigned int linked = 0;
    2948             :         unsigned int num_with_score = 0;
    2949         156 :         for (auto tally : _local_tallies)
    2950             :         {
    2951         120 :           if (tally->hasScore(n))
    2952             :           {
    2953          80 :             linked = std::max(linked, static_cast<unsigned int>(tally->linkedTallies().size()) + 1);
    2954          80 :             num_with_score++;
    2955             :           }
    2956             :         }
    2957             : 
    2958             :         // Can only allow auto-detection of the normalization tally if there is a single linkage
    2959             :         // of every mesh tally with the normalization score.
    2960          36 :         if (_score_count.at(n) != linked || _score_count.at(n) != num_with_score)
    2961             :         {
    2962             :           // If there are more then one value of 'source_rate_normalization', the user needs
    2963             :           // to tell us which tally to use.
    2964          54 :           checkRequiredParam(
    2965             :               _pars,
    2966             :               "normalization_tally",
    2967          28 :               "using a non-heating tally (" + non_heating_scores +
    2968             :                   ") in eigenvalue mode and adding more then one tally in the [Tallies] block");
    2969          78 :           const auto norm_tally_name = getParam<std::string>("normalization_tally");
    2970             : 
    2971             :           // Check to make sure the user provided a tally name for eigenvalue normalization
    2972             :           // that's been added.
    2973          94 :           for (auto tally : _local_tallies)
    2974          68 :             if (norm_tally_name == tally->name())
    2975             :               _source_rate_norm_tally = tally;
    2976             : 
    2977          26 :           if (!_source_rate_norm_tally)
    2978           0 :             paramError("normalization_tally",
    2979           0 :                        "The tally " + norm_tally_name +
    2980             :                            " does not exist in the problem! Please specify a tally added in the "
    2981             :                            "[Tallies] block!");
    2982             :         }
    2983             :         else
    2984             :         {
    2985          56 :           for (auto tally : _local_tallies)
    2986          48 :             if (tally->hasScore(n))
    2987             :               _source_rate_norm_tally = tally;
    2988             :         }
    2989             :       }
    2990             :       else
    2991             :       {
    2992             :         // Otherwise, we can check the tallies added and find the one scoring the requested
    2993             :         // value of 'source_rate_normalization'.
    2994        1404 :         for (auto tally : _local_tallies)
    2995        1024 :           if (tally->hasScore(n))
    2996             :             _source_rate_norm_tally = tally;
    2997             :       }
    2998             :     }
    2999             :     else
    3000             :       _source_rate_norm_tally = _local_tallies[0];
    3001             : 
    3002             :     // If it's not in the specified source rate tally, we can add it for the user.
    3003         498 :     if (!_source_rate_norm_tally->hasScore(n))
    3004             :     {
    3005          18 :       if (_source_rate_norm_tally->renamesTallyVars())
    3006           2 :         mooseError("When specifying 'name', the score indicated in "
    3007             :                    "'source_rate_normalization' must be\n"
    3008             :                    "listed in 'score' so that we know what you want to name that score (",
    3009             :                    norm,
    3010             :                    ")");
    3011             : 
    3012             :       // We can add the requested normalization score if and only if a single tally was added by
    3013             :       // [Tallies].
    3014          16 :       _all_tally_scores.push_back(n);
    3015          16 :       _source_rate_norm_tally->addScore(n);
    3016          16 :       _source_rate_score = _source_rate_norm_tally->scoreIndex(n);
    3017             :     }
    3018             :     else
    3019         480 :       _source_rate_score = _source_rate_norm_tally->scoreIndex(n);
    3020         496 :   }
    3021        2350 :   else if (isParamValid("source_rate_normalization"))
    3022          24 :     mooseWarning(
    3023             :         "When either running in fixed-source mode, or all tallies have units of eV/src, the "
    3024             :         "'source_rate_normalization' parameter is unused!");
    3025             : }
    3026             : 
    3027             : void
    3028          32 : OpenMCCellAverageProblem::updateOpenMCGeometry()
    3029             : {
    3030             : #ifdef ENABLE_DAGMC
    3031             :   // Need to swap array indices back to ids as OpenMC swapped these when preparing geometry.
    3032         208 :   for (const auto & cell : openmc::model::cells)
    3033             :   {
    3034         176 :     if (cell->type_ == openmc::Fill::MATERIAL)
    3035             :     {
    3036             :       std::vector<int32_t> mat_ids;
    3037         346 :       for (const auto & mat_index : cell->material_)
    3038         173 :         mat_ids.push_back(mat_index == openmc::MATERIAL_VOID
    3039             :                               ? openmc::MATERIAL_VOID
    3040         126 :                               : openmc::model::materials[mat_index]->id_);
    3041         173 :       cell->material_ = mat_ids;
    3042         173 :     }
    3043         176 :     if (cell->type_ == openmc::Fill::UNIVERSE && cell->fill_ != openmc::C_NONE)
    3044           3 :       cell->fill_ = openmc::model::universes[cell->fill_]->id_;
    3045         176 :     if (cell->type_ == openmc::Fill::LATTICE && cell->fill_ != openmc::C_NONE)
    3046           0 :       cell->fill_ = openmc::model::lattices[cell->fill_]->id_;
    3047             : 
    3048         176 :     cell->universe_ = openmc::model::universes[cell->universe_]->id_;
    3049             :   }
    3050             : 
    3051          32 :   for (const auto & lattice : openmc::model::lattices)
    3052             :   {
    3053           0 :     for (openmc::LatticeIter it = lattice->begin(); it != lattice->end(); ++it)
    3054             :     {
    3055           0 :       int u_index = *it;
    3056           0 :       *it = openmc::model::universes[u_index]->id_;
    3057             :     }
    3058             : 
    3059           0 :     if (lattice->outer_ != openmc::NO_OUTER_UNIVERSE)
    3060           0 :       lattice->outer_ = openmc::model::universes[lattice->outer_]->id_;
    3061             :   }
    3062             : 
    3063             :   // skin the mesh geometry according to contours in temperature, density, and subdomain
    3064          32 :   _skinner->update();
    3065             : 
    3066             :   openmc::model::universe_level_counts.clear();
    3067             : 
    3068             :   // Clear nuclides and elements, these will get reset in read_ce_cross_sections
    3069             :   // Horrible circular logic means that clearing nuclides clears nuclide_map, but
    3070             :   // which is needed before nuclides gets reset (similar for elements)
    3071             :   std::unordered_map<std::string, int> nuclide_map_copy = openmc::data::nuclide_map;
    3072          32 :   openmc::data::nuclides.clear();
    3073             :   openmc::data::nuclide_map = nuclide_map_copy;
    3074             : 
    3075             :   std::unordered_map<std::string, int> element_map_copy = openmc::data::element_map;
    3076          32 :   openmc::data::elements.clear();
    3077             :   openmc::data::element_map = element_map_copy;
    3078             : 
    3079             :   // Clear existing DAGMC cell data. Cells cannot be deleted in-place as that invalidates
    3080             :   // all pointers and iterators, so we loop over the cell map to store a list of DAGMC cells.
    3081             :   // Afterwards, the cells contained in the list can be deleted.
    3082             :   std::vector<int32_t> cells_to_delete;
    3083         208 :   for (auto [id, index] : openmc::model::cell_map)
    3084         176 :     if (openmc::model::cells[index]->geom_type() == openmc::GeometryType::DAG)
    3085         173 :       cells_to_delete.push_back(openmc::model::cells[index]->id_);
    3086             : 
    3087         205 :   for (auto cell : cells_to_delete)
    3088             :   {
    3089         638 :     for (int32_t i = 0; i < openmc::model::cells.size(); ++i)
    3090             :     {
    3091         638 :       if (openmc::model::cells[i]->id_ == cell)
    3092             :       {
    3093             :         openmc::model::cells.erase(openmc::model::cells.begin() + i);
    3094         173 :         break;
    3095             :       }
    3096             :     }
    3097             :   }
    3098          32 :   cells_to_delete.clear();
    3099             : 
    3100             :   // Clear existing surface data. Similar to cells, deletion of the DAGMC surfaces must be
    3101             :   // deferred.
    3102             :   std::vector<int> surfaces_to_delete;
    3103         495 :   for (auto [id, index] : openmc::model::surface_map)
    3104         463 :     if (openmc::model::surfaces[index]->geom_type() == openmc::GeometryType::DAG)
    3105         445 :       surfaces_to_delete.push_back(openmc::model::surfaces[index]->id_);
    3106             : 
    3107         477 :   for (auto surface : surfaces_to_delete)
    3108             :   {
    3109        3086 :     for (int i = 0; i < openmc::model::surfaces.size(); ++i)
    3110             :     {
    3111        3086 :       if (openmc::model::surfaces[i]->id_ == surface)
    3112             :       {
    3113             :         openmc::model::surface_map.erase(surface);
    3114             :         openmc::model::surfaces.erase(openmc::model::surfaces.begin() + i);
    3115         445 :         break;
    3116             :       }
    3117             :     }
    3118             :   }
    3119          32 :   surfaces_to_delete.clear();
    3120             : 
    3121             :   // Need to rebuild the cell_map and surface_map since the indices have changed.
    3122             :   openmc::model::cell_map.clear();
    3123          35 :   for (int32_t i = 0; i < openmc::model::cells.size(); ++i)
    3124           3 :     openmc::model::cell_map[openmc::model::cells[i]->id_] = i;
    3125             : 
    3126             :   // Horrible hack since we can't undo the surface id -> index swap that happens in
    3127             :   // CSGCell.region_.expression_, and so the 'surface_map' cannot be rebuilt. Intead, 'surfaces' is
    3128             :   // resized to the original length and the positions of each surface are shuffled such that they
    3129             :   // correspond to their indices in the original 'surface_map'. This results in the addition of N
    3130             :   // extra null 'DAGSurface' objects in 'surfaces', where N is the number of DAGMC surfaces in the
    3131             :   // geometry. These null surfaces aren't linked to a DAGMC universe and so they do not participate
    3132             :   // in particle transport, they just take up memory. CSGCell::region_ and Region::expression_ need
    3133             :   // to be made public in OpenMC to avoid this, or an appropriate series of C-API functions / member
    3134             :   // functions need to be added to OpenMC.
    3135          32 :   if (openmc::model::surfaces.size() > 0)
    3136             :   {
    3137          25 :     for (int i = openmc::model::surfaces.size(); i < _initial_num_openmc_surfaces; ++i)
    3138          22 :       openmc::model::surfaces.push_back(
    3139          44 :           std::move(std::make_unique<openmc::DAGSurface>(nullptr, 0)));
    3140          21 :     for (const auto & [id, index] : openmc::model::surface_map)
    3141             :     {
    3142             :       // If the surface at the index exists and the id is the same, do nothing.
    3143          18 :       if (openmc::model::surfaces[index]->id_ == id)
    3144          18 :         continue;
    3145             :       else
    3146             :       {
    3147             :         // Otherwise we need to find the filter and swap it with the filter at the current location.
    3148           0 :         for (int i = 0; i < openmc::model::surfaces.size(); ++i)
    3149             :         {
    3150           0 :           if (openmc::model::surfaces[i]->id_ == id)
    3151             :           {
    3152             :             auto temp = std::move(openmc::model::surfaces[index]);
    3153             :             openmc::model::surfaces[index] = std::move(openmc::model::surfaces[i]);
    3154             :             openmc::model::surfaces[i] = std::move(temp);
    3155             :             break;
    3156           0 :           }
    3157             :         }
    3158             :       }
    3159             :     }
    3160             : 
    3161             :     // Sanity check by looping over the surface_map to make sure the indices correspond to the
    3162             :     // surface ids.
    3163          21 :     for (const auto & [id, index] : openmc::model::surface_map)
    3164          18 :       if (openmc::model::surfaces[index]->id_ != id)
    3165           0 :         mooseError("Internal error: mismatch between surfaces[surface_map[id]]->id_ and id.");
    3166             :   }
    3167             : #endif
    3168          64 : }
    3169             : 
    3170             : bool
    3171       32400 : OpenMCCellAverageProblem::cellMapsToSubdomain(const cellInfo & cell_info,
    3172             :                                               const std::unordered_set<SubdomainID> & id) const
    3173             : {
    3174       32400 :   auto s = _cell_to_elem_subdomain.at(cell_info);
    3175       32424 :   for (const auto & i : id)
    3176       32400 :     if (s.find(i) != s.end())
    3177             :       return true;
    3178             : 
    3179             :   return false;
    3180             : }
    3181             : 
    3182             : bool
    3183     4138500 : OpenMCCellAverageProblem::cellHasIdenticalFill(const cellInfo & cell_info) const
    3184             : {
    3185             :   // material cells are discounted as identical fill
    3186     4138500 :   const auto & cell = openmc::model::cells[cell_info.first];
    3187     4138500 :   if (!_has_identical_cell_fills || cell->type_ == openmc::Fill::MATERIAL)
    3188             :     return false;
    3189             : 
    3190       32400 :   return cellMapsToSubdomain(cell_info, _identical_cell_fill_blocks);
    3191             : }
    3192             : 
    3193             : OpenMCCellAverageProblem::containedCells
    3194       31848 : OpenMCCellAverageProblem::shiftCellInstances(const cellInfo & cell_info) const
    3195             : {
    3196       31848 :   if (!_has_identical_cell_fills)
    3197           0 :     mooseError("Internal error: should not call shiftCellInstances!");
    3198             : 
    3199       31848 :   auto offset = _n_offset.at(cell_info);
    3200             : 
    3201             :   containedCells contained_cells;
    3202       31848 :   const auto & first_cell_cc = _cell_to_contained_material_cells.at(_first_identical_cell);
    3203    25636596 :   for (const auto & cc : first_cell_cc)
    3204             :   {
    3205    25604748 :     const auto & index = cc.first;
    3206             :     const auto & instances = cc.second;
    3207             :     auto n_instances = instances.size();
    3208             :     const auto & shifts = _instance_offsets.at(index);
    3209             : 
    3210             :     std::vector<int32_t> shifted_instances;
    3211  1699231176 :     for (unsigned int inst = 0; inst < n_instances; ++inst)
    3212  1673626428 :       shifted_instances.push_back(instances[inst] + offset * shifts[inst]);
    3213             : 
    3214    25604748 :     contained_cells[index] = shifted_instances;
    3215    25604748 :   }
    3216             : 
    3217       31848 :   return contained_cells;
    3218             : }
    3219             : 
    3220             : OpenMCCellAverageProblem::containedCells
    3221     4111430 : OpenMCCellAverageProblem::containedMaterialCells(const cellInfo & cell_info) const
    3222             : {
    3223     4111430 :   if (!cellHasIdenticalFill(cell_info))
    3224     4079582 :     return _cell_to_contained_material_cells.at(cell_info);
    3225             :   else
    3226       31848 :     return shiftCellInstances(cell_info);
    3227             : }
    3228             : 
    3229             : std::vector<int32_t>
    3230       13229 : OpenMCCellAverageProblem::materialsInCells(const containedCells & contained_cells) const
    3231             : {
    3232             :   std::vector<int32_t> mats;
    3233       33354 :   for (const auto & contained : contained_cells)
    3234             :   {
    3235      458990 :     for (const auto & instance : contained.second)
    3236             :     {
    3237             :       // we know this is a material cell, so we don't need to check that the fill is material
    3238             :       int32_t material_index;
    3239             :       cellInfo cell_info = {contained.first, instance};
    3240      438865 :       materialFill(cell_info, material_index);
    3241      438865 :       mats.push_back(material_index);
    3242             :     }
    3243             :   }
    3244             : 
    3245       13229 :   return mats;
    3246           0 : }
    3247             : 
    3248             : Point
    3249     2199401 : OpenMCCellAverageProblem::transformPointToOpenMC(const Point & pt) const
    3250             : {
    3251     2199401 :   Point pnt_out = transformPoint(pt);
    3252             : 
    3253             :   // scale point to OpenMC domain
    3254     2199401 :   pnt_out *= _scaling;
    3255             : 
    3256     2199401 :   return pnt_out;
    3257             : }
    3258             : #endif

Generated by: LCOV version 1.14