LCOV - code coverage report
Current view: top level - src/outputs - SampledOutput.C (source / functions) Hit Total Coverage
Test: idaholab/moose framework: 2bf808 Lines: 209 219 95.4 %
Date: 2025-07-17 01:28:37 Functions: 11 12 91.7 %
Legend: Lines: hit not hit

          Line data    Source code
       1             : //* This file is part of the MOOSE framework
       2             : //* https://mooseframework.inl.gov
       3             : //*
       4             : //* All rights reserved, see COPYRIGHT for full restrictions
       5             : //* https://github.com/idaholab/moose/blob/master/COPYRIGHT
       6             : //*
       7             : //* Licensed under LGPL 2.1, please see LICENSE for details
       8             : //* https://www.gnu.org/licenses/lgpl-2.1.html
       9             : 
      10             : // MOOSE includes
      11             : #include "SampledOutput.h"
      12             : #include "FEProblem.h"
      13             : #include "DisplacedProblem.h"
      14             : #include "MooseApp.h"
      15             : #include "MoosePartitioner.h"
      16             : 
      17             : #include "libmesh/distributed_mesh.h"
      18             : #include "libmesh/equation_systems.h"
      19             : #include "libmesh/mesh_function.h"
      20             : #include "libmesh/explicit_system.h"
      21             : 
      22             : using namespace libMesh;
      23             : 
      24             : InputParameters
      25      158081 : SampledOutput::validParams()
      26             : {
      27             : 
      28             :   // Get the parameters from the parent object
      29      158081 :   InputParameters params = AdvancedOutput::validParams();
      30      474243 :   params.addParam<unsigned int>("refinements",
      31      316162 :                                 0,
      32             :                                 "Number of uniform refinements for oversampling "
      33             :                                 "(refinement levels beyond any level of "
      34             :                                 "refinements already applied on the regular mesh)");
      35      158081 :   params.addParam<Point>("position",
      36             :                          "Set a positional offset, this vector will get added to the "
      37             :                          "nodal coordinates to move the domain.");
      38      158081 :   params.addParam<MeshFileName>("file", "The name of the mesh file to read, for oversampling");
      39      158081 :   params.addParam<std::vector<SubdomainName>>(
      40             :       "sampling_blocks", "The list of blocks to restrict the mesh sampling to");
      41      474243 :   params.addParam<bool>(
      42             :       "serialize_sampling",
      43      316162 :       true,
      44             :       "If set to true, all sampled output (see sampling parameters) will be done "
      45             :       "on rank 0. This option is useful to debug suspected parallel output issues");
      46             : 
      47             :   // **** DEPRECATED PARAMETERS ****
      48      474243 :   params.addDeprecatedParam<bool>("append_oversample",
      49      316162 :                                   false,
      50             :                                   "Append '_oversample' to the output file base",
      51             :                                   "This parameter is deprecated. To append '_oversample' utilize "
      52             :                                   "the output block name or the 'file_base'");
      53             : 
      54             :   // 'Oversampling' Group
      55      158081 :   params.addParamNamesToGroup("refinements position file sampling_blocks serialize_sampling",
      56             :                               "Modified Mesh Sampling");
      57             : 
      58      158081 :   return params;
      59           0 : }
      60             : 
      61       35081 : SampledOutput::SampledOutput(const InputParameters & parameters)
      62             :   : AdvancedOutput(parameters),
      63       35069 :     _refinements(getParam<unsigned int>("refinements")),
      64       35069 :     _using_external_sampling_file(isParamValid("file")),
      65       35069 :     _change_position(isParamValid("position")),
      66       34949 :     _use_sampled_output(_refinements > 0 || _using_external_sampling_file ||
      67       70018 :                         isParamValid("sampling_blocks") || _change_position),
      68       35069 :     _position(_change_position ? getParam<Point>("position") : Point()),
      69       35069 :     _sampling_mesh_changed(true),
      70       35069 :     _mesh_subdomains_match(true),
      71      105219 :     _serialize(getParam<bool>("serialize_sampling"))
      72             : {
      73       35069 : }
      74             : 
      75             : void
      76       34606 : SampledOutput::initialSetup()
      77             : {
      78       34606 :   AdvancedOutput::initialSetup();
      79             : 
      80             :   // Creates and initializes the sampling mesh
      81       34602 :   initSample();
      82       34602 : }
      83             : 
      84             : void
      85     2392140 : SampledOutput::outputStep(const ExecFlagType & type)
      86             : {
      87             :   // Output is not allowed
      88     2392140 :   if (!_allow_output && type != EXEC_FORCED)
      89       46607 :     return;
      90             : 
      91             :   // If recovering disable output of initial condition, it was already output
      92     2345533 :   if (type == EXEC_INITIAL && _app.isRecovering())
      93           0 :     return;
      94             : 
      95             :   // Return if the current output is not on the desired interval
      96     2345533 :   if (type != EXEC_FINAL && !onInterval())
      97       43603 :     return;
      98             : 
      99             :   // store current simulation time
     100     2301930 :   _last_output_simulation_time = _time;
     101             : 
     102             :   // set current type
     103     2301930 :   _current_execute_flag = type;
     104             : 
     105             :   // Call the output method
     106     2301930 :   if (shouldOutput())
     107             :   {
     108      158234 :     TIME_SECTION("outputStep", 2, "Outputting Step");
     109      158234 :     updateSample();
     110      158234 :     output();
     111      158234 :   }
     112             : 
     113     2301930 :   _current_execute_flag = EXEC_NONE;
     114             : }
     115             : 
     116       33652 : SampledOutput::~SampledOutput()
     117             : {
     118             :   // TODO: Remove once libmesh Issue #1184 is fixed
     119       33652 :   _sampling_es.reset();
     120       33652 :   _sampling_mesh_ptr.reset();
     121       33652 : }
     122             : 
     123             : void
     124        4775 : SampledOutput::meshChanged()
     125             : {
     126        4775 :   _sampling_mesh_changed = true;
     127        4775 : }
     128             : 
     129             : void
     130       34602 : SampledOutput::initSample()
     131             : {
     132             :   // Perform the mesh cloning, if needed
     133       34602 :   if (!_use_sampled_output)
     134       34438 :     return;
     135             : 
     136         164 :   cloneMesh();
     137             : 
     138             :   // Re-position the sampling mesh
     139         164 :   if (_change_position)
     140        2264 :     for (auto & node : _mesh_ptr->getMesh().node_ptr_range())
     141        2264 :       *node += _position;
     142             : 
     143             :   // Perform the mesh refinement
     144         164 :   if (_refinements > 0)
     145             :   {
     146         120 :     MeshRefinement mesh_refinement(_mesh_ptr->getMesh());
     147             : 
     148             :     // We want original and refined partitioning to match so we can
     149             :     // query from one to the other safely on distributed meshes.
     150         120 :     _mesh_ptr->getMesh().skip_partitioning(true);
     151         120 :     mesh_refinement.uniformly_refine(_refinements);
     152             : 
     153             :     // Note that nodesets are not propagated with mesh refinement, unless you built the nodesets
     154             :     // from the sidesets again, which is what happens for the regular mesh with initial refinement
     155         120 :   }
     156             : 
     157             :   // We can't allow renumbering if we want to output multiple time
     158             :   // steps to the same Exodus file
     159         164 :   _mesh_ptr->getMesh().allow_renumbering(false);
     160             : 
     161             :   // This should be called after changing the mesh (block restriction for example)
     162         164 :   if (_change_position || (_refinements > 0) || isParamValid("sampling_blocks"))
     163         154 :     _sampling_mesh_ptr->meshChanged();
     164             : 
     165             :   // Create the new EquationSystems
     166         164 :   _sampling_es = std::make_unique<EquationSystems>(_mesh_ptr->getMesh());
     167         164 :   _es_ptr = _sampling_es.get();
     168             : 
     169             :   // Reference the system from which we are copying
     170         164 :   EquationSystems & source_es = _problem_ptr->es();
     171             : 
     172             :   // If we're going to be copying from that system later, we need to keep its
     173             :   // original elements as ghost elements even if it gets grossly
     174             :   // repartitioned, since we can't repartition the sample mesh to
     175             :   // match.
     176             :   // FIXME: this is not enough. It assumes our initial partition of the sampling mesh
     177             :   // and the source mesh match. But that's usually only true in the 'refinement' case,
     178             :   // not with an arbitrary sampling mesh file
     179         164 :   DistributedMesh * dist_mesh = dynamic_cast<DistributedMesh *>(&source_es.get_mesh());
     180         164 :   if (dist_mesh)
     181             :   {
     182         632 :     for (auto & elem : dist_mesh->active_local_element_ptr_range())
     183         632 :       dist_mesh->add_extra_ghost_elem(elem);
     184             :   }
     185             : 
     186             :   // Initialize the _mesh_functions vector
     187         164 :   const auto num_systems = source_es.n_systems();
     188         164 :   _mesh_functions.resize(num_systems);
     189             : 
     190             :   // Keep track of the variable numbering in both regular and sampled system
     191         164 :   _variable_numbers_in_system.resize(num_systems);
     192             : 
     193             :   // Get the list of nodal and elemental output data
     194         164 :   const auto & nodal_data = getNodalVariableOutput();
     195         164 :   const auto & elemental_data = getElementalVariableOutput();
     196             : 
     197             :   // Loop over the number of systems
     198         492 :   for (const auto sys_num : make_range(num_systems))
     199             :   {
     200             :     // Reference to the current system
     201         328 :     const auto & source_sys = source_es.get_system(sys_num);
     202             : 
     203             :     // Add the system to the new EquationsSystems
     204         328 :     ExplicitSystem & dest_sys = _sampling_es->add_system<ExplicitSystem>(source_sys.name());
     205             : 
     206             :     // Loop through the variables in the System
     207         328 :     const auto num_vars = source_sys.n_vars();
     208         328 :     unsigned int num_actual_vars = 0;
     209         328 :     if (num_vars > 0)
     210             :     {
     211         218 :       if (_serialize)
     212         194 :         _serialized_solution = NumericVector<Number>::build(_communicator);
     213             : 
     214             :       // Add the variables to the system... simultaneously creating MeshFunctions for them.
     215         504 :       for (const auto var_num : make_range(num_vars))
     216             :       {
     217             :         // Is the variable supposed to be output?
     218         286 :         const auto & var_name = source_sys.variable_name(var_num);
     219         286 :         if (!nodal_data.count(var_name) && !elemental_data.count(var_name))
     220          98 :           continue;
     221         188 :         _variable_numbers_in_system[sys_num].push_back(var_num);
     222         188 :         num_actual_vars++;
     223             : 
     224             :         // We do what we can to preserve the block restriction
     225          10 :         const auto * const subdomains = (_using_external_sampling_file && !_mesh_subdomains_match)
     226         188 :                                             ? nullptr
     227         188 :                                             : &source_sys.variable(var_num).active_subdomains();
     228             : 
     229             :         // Add the variable. We essentially support nodal variables and constant monomials
     230         188 :         const FEType & fe_type = source_sys.variable_type(var_num);
     231         188 :         if (isSampledAtNodes(fe_type))
     232             :         {
     233         152 :           dest_sys.add_variable(source_sys.variable_name(var_num), fe_type, subdomains);
     234         152 :           if (dist_mesh && !_serialize)
     235           0 :             paramError("serialize_sampling",
     236             :                        "Variables sampled as nodal currently require serialization with a "
     237             :                        "distributed mesh.");
     238             :         }
     239             :         else
     240             :         {
     241          36 :           const auto & var_name = source_sys.variable_name(var_num);
     242          36 :           if (fe_type != FEType(CONSTANT, MONOMIAL))
     243             :           {
     244           0 :             mooseInfoRepeated("Sampled output projects variable '" + var_name +
     245             :                               "' onto a constant monomial");
     246           0 :             if (!_serialize)
     247           0 :               paramWarning("serialize_sampling",
     248             :                            "Projection without serialization may fail with insufficient ghosting. "
     249             :                            "Consider setting 'serialize_sampling' to true.");
     250             :           }
     251          36 :           dest_sys.add_variable(var_name, FEType(CONSTANT, MONOMIAL), subdomains);
     252             :         }
     253             :         // Note: we could do more, using the generic projector. But exodus output of higher order
     254             :         // or more exotic variables is limited anyway
     255             :       }
     256             : 
     257             :       // Size for the actual number of variables output
     258         218 :       _mesh_functions[sys_num].resize(num_actual_vars);
     259             :     }
     260             :   }
     261             : 
     262             :   // Initialize the newly created EquationSystem
     263         164 :   _sampling_es->init();
     264             : }
     265             : 
     266             : void
     267      158234 : SampledOutput::updateSample()
     268             : {
     269             :   // Do nothing if oversampling and changing position are not enabled
     270      158234 :   if (!_use_sampled_output)
     271      157740 :     return;
     272             : 
     273             :   // We need the mesh functions to extend the whole domain so we serialize both the mesh and the
     274             :   // solution. We need this because the partitioning of the sampling mesh may not match the
     275             :   // partitioning of the source mesh
     276         494 :   if (_serialize)
     277             :   {
     278         472 :     _problem_ptr->mesh().getMesh().gather_to_zero();
     279         472 :     _mesh_ptr->getMesh().gather_to_zero();
     280             :   }
     281             : 
     282             :   // Get a reference to actual equation system
     283         494 :   EquationSystems & source_es = _problem_ptr->es();
     284         494 :   const auto num_systems = source_es.n_systems();
     285             : 
     286             :   // Loop through each system
     287        1482 :   for (const auto sys_num : make_range(num_systems))
     288             :   {
     289         988 :     if (!_mesh_functions[sys_num].empty())
     290             :     {
     291             :       // Get references to the source and destination systems
     292         516 :       System & source_sys = source_es.get_system(sys_num);
     293         516 :       System & dest_sys = _sampling_es->get_system(sys_num);
     294             : 
     295             :       // Update the solution for the sampling mesh
     296         516 :       if (_serialize)
     297             :       {
     298         494 :         _serialized_solution->clear();
     299         494 :         _serialized_solution->init(source_sys.n_dofs(), false, SERIAL);
     300             :         // Pull down a full copy of this vector on every processor so we can get values in parallel
     301         494 :         source_sys.solution->localize(*_serialized_solution);
     302             :       }
     303             : 
     304             :       // Update the mesh functions
     305        1054 :       for (const auto var_num : index_range(_mesh_functions[sys_num]))
     306             :       {
     307         538 :         const auto original_var_num = _variable_numbers_in_system[sys_num][var_num];
     308             : 
     309             :         // If the mesh has changed, the MeshFunctions need to be re-built, otherwise simply clear it
     310             :         // for re-initialization
     311             :         // TODO: inherit from MeshChangedInterface and rebuild mesh functions on meshChanged()
     312         538 :         if (!_mesh_functions[sys_num][var_num] || _sampling_mesh_changed)
     313         732 :           _mesh_functions[sys_num][var_num] = std::make_unique<MeshFunction>(
     314             :               source_es,
     315         244 :               _serialize ? *_serialized_solution : *source_sys.solution,
     316             :               source_sys.get_dof_map(),
     317         244 :               original_var_num);
     318             :         else
     319         294 :           _mesh_functions[sys_num][var_num]->clear();
     320             : 
     321             :         // Initialize the MeshFunctions for application to the sampled solution
     322         538 :         _mesh_functions[sys_num][var_num]->init();
     323             : 
     324             :         // Mesh functions are still defined on the original mesh, which might not fully overlap with
     325             :         // the sampling mesh. We don't want to error with a libMesh assert on the out of mesh mode
     326         538 :         _mesh_functions[sys_num][var_num]->enable_out_of_mesh_mode(-1e6);
     327             :       }
     328             : 
     329             :       // Fill solution vectors by evaluating mesh functions on sampling mesh
     330         901 :       for (const auto var_num : index_range(_mesh_functions[sys_num]))
     331             :       {
     332             :         // we serialized the mesh and the solution vector, we might as well just do this only on
     333             :         // processor 0.
     334         532 :         if (_serialize && processor_id() > 0)
     335         147 :           break;
     336             : 
     337         385 :         const auto original_var_num = _variable_numbers_in_system[sys_num][var_num];
     338         385 :         const FEType & fe_type = source_sys.variable_type(original_var_num);
     339             :         // Loop over the mesh, nodes for nodal data, elements for element data
     340         385 :         if (isSampledAtNodes(fe_type))
     341             :         {
     342         662 :           for (const auto & node : (_serialize ? _mesh_ptr->getMesh().node_ptr_range()
     343      989268 :                                                : _mesh_ptr->getMesh().local_node_ptr_range()))
     344             :           {
     345             :             // Avoid working on ghosted dofs
     346      988606 :             if (node->n_dofs(sys_num, var_num) &&
     347      494303 :                 (_serialize || processor_id() == node->processor_id()))
     348             :             {
     349             :               // the node has to be within the domain of the mesh function
     350      494303 :               const auto value = (*_mesh_functions[sys_num][var_num])(*node - _position);
     351      494303 :               if (value != -1e6)
     352      494303 :                 dest_sys.solution->set(node->dof_number(sys_num, var_num, /*comp=*/0), value);
     353             :               else
     354           0 :                 mooseDoOnce(mooseWarning(
     355             :                     "Sampling at location ",
     356             :                     *node - _position,
     357             :                     " by process ",
     358             :                     std::to_string(processor_id()),
     359             :                     " was outside the problem mesh.\nThis message will not be repeated"));
     360             :             }
     361         331 :           }
     362             :         }
     363             :         else
     364             :         {
     365          54 :           const auto elem_range = _serialize
     366          54 :                                       ? _mesh_ptr->getMesh().active_element_ptr_range()
     367          54 :                                       : _mesh_ptr->getMesh().active_local_element_ptr_range();
     368         918 :           for (const auto & elem : elem_range)
     369             :           {
     370         864 :             if (elem->n_dofs(sys_num, var_num) &&
     371         432 :                 (_serialize || processor_id() == elem->processor_id()))
     372             :             {
     373             :               const auto value =
     374         432 :                   (*_mesh_functions[sys_num][var_num])(elem->true_centroid() - _position);
     375         432 :               if (value != -1e6)
     376         432 :                 dest_sys.solution->set(elem->dof_number(sys_num, var_num, /*comp=*/0), value);
     377             :               else
     378           0 :                 mooseDoOnce(mooseWarning(
     379             :                     "Sampling at location ",
     380             :                     elem->true_centroid() - _position,
     381             :                     " was outside the problem mesh.\nThis message will not be repeated."));
     382             :             }
     383          54 :           }
     384          54 :         }
     385             :       }
     386             : 
     387             :       // We modified the solution vector directly, we have to close it
     388         516 :       dest_sys.solution->close();
     389             :     }
     390             :   }
     391             : 
     392             :   // Set this to false so that new output files are not created, since the sampling mesh
     393             :   // doesn't actually change
     394         494 :   _sampling_mesh_changed = false;
     395             : }
     396             : 
     397             : void
     398         164 : SampledOutput::cloneMesh()
     399             : {
     400             :   // Create the new mesh from a file
     401         164 :   if (isParamValid("file"))
     402             :   {
     403          10 :     InputParameters mesh_params = _app.getFactory().getValidParams("FileMesh");
     404          10 :     mesh_params.applyParameters(parameters(), {}, true);
     405          10 :     mesh_params.set<bool>("nemesis") = false;
     406             :     _sampling_mesh_ptr =
     407          10 :         _app.getFactory().createUnique<MooseMesh>("FileMesh", "output_problem_mesh", mesh_params);
     408          10 :     _sampling_mesh_ptr->allowRecovery(false); // We actually want to reread the initial mesh
     409          10 :     _sampling_mesh_ptr->init();
     410          10 :   }
     411             :   // Clone the existing mesh
     412             :   else
     413             :   {
     414         154 :     if (_app.isRecovering())
     415           2 :       mooseWarning("Recovering or Restarting with oversampling may not work (especially with "
     416             :                    "adapted meshes)!!  Refs #2295");
     417         154 :     _sampling_mesh_ptr = _mesh_ptr->safeClone();
     418             :   }
     419             : 
     420             :   // Remove unspecified blocks
     421         164 :   if (isParamValid("sampling_blocks"))
     422             :   {
     423             :     // Remove all elements not in the blocks
     424          24 :     const auto & blocks_to_keep_names = getParam<std::vector<SubdomainName>>("sampling_blocks");
     425          24 :     const auto & blocks_to_keep = _sampling_mesh_ptr->getSubdomainIDs(blocks_to_keep_names);
     426         760 :     for (const auto & elem_ptr : _sampling_mesh_ptr->getMesh().element_ptr_range())
     427         368 :       if (std::find(blocks_to_keep.begin(), blocks_to_keep.end(), elem_ptr->subdomain_id()) ==
     428         736 :           blocks_to_keep.end())
     429         184 :         _sampling_mesh_ptr->getMesh().delete_elem(elem_ptr);
     430             : 
     431             :     // Deleting elements and isolated nodes would cause renumbering. Not renumbering might help
     432             :     // user examining the sampling mesh and the regular mesh. Also if we end up partitioning the
     433             :     // elements, the node partitioning is unlikely to match if the element numbering is different.
     434             :     // Still not enough of a guarantee, because of deleted elements the node partitioning could be
     435             :     // different. We will rely on ghosting to make it work
     436          24 :     _sampling_mesh_ptr->getMesh().allow_renumbering(false);
     437          24 :   }
     438             : 
     439             :   // Set a partitioner
     440         164 :   if (!_serialize)
     441             :   {
     442          12 :     _sampling_mesh_ptr->setIsCustomPartitionerRequested(true);
     443          12 :     InputParameters partition_params = _app.getFactory().getValidParams("CopyMeshPartitioner");
     444          12 :     partition_params.set<MooseMesh *>("mesh") = _sampling_mesh_ptr.get();
     445          12 :     partition_params.set<MooseMesh *>("source_mesh") = _mesh_ptr;
     446          12 :     std::shared_ptr<MoosePartitioner> mp = _factory.create<MoosePartitioner>(
     447          12 :         "CopyMeshPartitioner", "sampled_output_part", partition_params);
     448          12 :     _sampling_mesh_ptr->setCustomPartitioner(mp.get());
     449             : 
     450          12 :     _sampling_mesh_ptr->getMesh().prepare_for_use();
     451             :     // this should be called by prepare_for_use, but is not.
     452             :     // it also requires a prior call to prepare_for_use()
     453          12 :     mp->partition(_sampling_mesh_ptr->getMesh(), comm().size());
     454          12 :   }
     455             : 
     456             :   // Prepare mesh, needed for the mesh functions
     457         164 :   if (_using_external_sampling_file)
     458          10 :     _sampling_mesh_ptr->prepare(/*mesh to clone*/ nullptr);
     459         154 :   else if (_serialize && isParamValid("sampling_blocks"))
     460             :     // TODO: constraints have not been initialized?
     461          12 :     _sampling_mesh_ptr->getMesh().prepare_for_use();
     462             : 
     463         164 :   if (_serialize)
     464             :     // we want to avoid re-partitioning, as we will serialize anyway
     465         152 :     _sampling_mesh_ptr->getMesh().skip_partitioning(true);
     466             : 
     467             :   // Make sure that the mesh pointer points to the newly cloned mesh
     468         164 :   _mesh_ptr = _sampling_mesh_ptr.get();
     469             : 
     470             :   // Check the source and target mesh in case their subdomains match
     471         164 :   const std::vector<SubdomainID> mesh_subdomain_ids_vec(_mesh_ptr->meshSubdomains().begin(),
     472         328 :                                                         _mesh_ptr->meshSubdomains().end());
     473             :   const std::vector<SubdomainID> initial_mesh_subdomain_ids_vec(
     474         164 :       _problem_ptr->mesh().meshSubdomains().begin(), _problem_ptr->mesh().meshSubdomains().end());
     475         164 :   _mesh_subdomains_match =
     476         174 :       (_mesh_ptr->meshSubdomains() == _problem_ptr->mesh().meshSubdomains() &&
     477         174 :        _mesh_ptr->getSubdomainNames(mesh_subdomain_ids_vec) ==
     478         174 :            _problem_ptr->mesh().getSubdomainNames(initial_mesh_subdomain_ids_vec));
     479         164 :   if (_using_external_sampling_file && !_mesh_subdomains_match)
     480           0 :     mooseInfoRepeated("Variable block restriction disabled in sampled output due to non-matching "
     481             :                       "subdomain names and ids");
     482         164 : }
     483             : 
     484             : void
     485       26285 : SampledOutput::setFileBaseInternal(const std::string & file_base)
     486             : {
     487       26285 :   AdvancedOutput::setFileBaseInternal(file_base);
     488             :   // ** DEPRECATED SUPPORT **
     489       26285 :   if (getParam<bool>("append_oversample"))
     490           0 :     _file_base += "_oversample";
     491       26285 : }
     492             : 
     493             : bool
     494         573 : SampledOutput::isSampledAtNodes(const FEType & fe_type) const
     495             : {
     496             :   // This is the same criterion as in MooseVariableData
     497         573 :   const auto continuity = FEInterface::get_continuity(fe_type);
     498         573 :   return (continuity == C_ZERO || continuity == C_ONE);
     499             : }

Generated by: LCOV version 1.14