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

Generated by: LCOV version 1.14