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

Generated by: LCOV version 1.14