LCOV - code coverage report
Current view: top level - src/transfers - NekVolumetricSource.C (source / functions) Hit Total Coverage
Test: neams-th-coe/cardinal: 920dc5 Lines: 61 66 92.4 %
Date: 2025-08-10 20:41:39 Functions: 4 4 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_NEK_COUPLING
      20             : 
      21             : #include "NekVolumetricSource.h"
      22             : 
      23             : registerMooseObject("CardinalApp", NekVolumetricSource);
      24             : 
      25             : InputParameters
      26         198 : NekVolumetricSource::validParams()
      27             : {
      28         198 :   auto params = ConservativeFieldTransfer::validParams();
      29         396 :   params.addParam<Real>(
      30             :       "initial_source_integral",
      31         396 :       0,
      32             :       "Initial value to use for the 'postprocessor_to_conserve'; this initial value will be "
      33             :       "overridden once the coupled app executes its transfer of the volumetric source term "
      34             :       "integral into the 'postprocessor_to_conserve'. You may want to use this parameter if NekRS "
      35             :       "runs first, or if you are running NekRS in isolation but still want to apply a source term "
      36             :       "via Cardinal. Remember that this parameter is only used to normalize the source term "
      37             :       "'source_variable', so you will need to populate an initial shape.");
      38         198 :   params.addClassDescription("Reads/writes volumetric source data between NekRS and MOOSE.");
      39         198 :   return params;
      40           0 : }
      41             : 
      42         102 : NekVolumetricSource::NekVolumetricSource(const InputParameters & parameters)
      43             :   : ConservativeFieldTransfer(parameters),
      44         204 :     _initial_source_integral(getParam<Real>("initial_source_integral"))
      45             : {
      46         102 :   if (_direction == "to_nek")
      47             :   {
      48         203 :     addExternalVariable(_usrwrk_slot[0], _variable);
      49         101 :     indices.heat_source = _usrwrk_slot[0] * nekrs::fieldOffset();
      50             : 
      51         101 :     if (_usrwrk_slot.size() > 1)
      52           2 :       paramError("usrwrk_slot",
      53             :                  "'usrwrk_slot' must be of length 1 for volumetric source transfers; you have "
      54           1 :                  "entered a vector of length " +
      55           0 :                      Moose::stringify(_usrwrk_slot.size()));
      56             :   }
      57             : 
      58         100 :   if (!_nek_mesh->volume())
      59           1 :     mooseError("The NekVolumetricSource object can only be used when there is volumetric coupling "
      60             :                "of NekRS with MOOSE, i.e. when 'volume = true' in NekRSMesh.");
      61             : 
      62          99 :   if (_direction == "from_nek")
      63           0 :     paramError("direction",
      64             :                "The NekVolumetricSource currently only supports transfers 'to_nek'; contact the "
      65             :                "Cardinal developer team if you require reading of NekRS volumetric source terms.");
      66             : 
      67             :   // Check that there is a udf function providing the source for the passive scalar
      68             :   // equations. NOTE: This check is imperfect, because even if there is a source kernel,
      69             :   // we cannot tell _which_ passive scalar equation that it is applied to (we have
      70             :   // source kernels for the RANS passive scalar equations, for instance).
      71          99 :   if (nekrs::hasTemperatureSolve() && !nekrs::hasHeatSourceKernel())
      72           1 :     mooseError("In order to send a volumetric heat source to NekRS, you must have an OCCA source "
      73             :                "kernel in the passive scalar equations!");
      74             : 
      75          98 :   if (!nekrs::hasTemperatureVariable())
      76           1 :     mooseError("In order to send a volumetric heat source to NekRS, your case files must have a "
      77           1 :                "[TEMPERATURE] block. Note that you can set 'solver = none' in '" +
      78           1 :                _nek_problem.casename() + ".par' if you don't want to solve for temperature.");
      79             : 
      80          97 :   if (!nekrs::hasTemperatureSolve())
      81          14 :     mooseWarning("By setting 'solver = none' for temperature in '" + _nek_problem.casename() +
      82             :                  ".par', NekRS will not solve for temperature. The volumetric heat source sent by "
      83             :                  "this object will be unused.");
      84             : 
      85          96 :   addExternalPostprocessor(_postprocessor_name, _initial_source_integral);
      86          96 :   _source_integral = &getPostprocessorValueByName(_postprocessor_name);
      87          96 : }
      88             : 
      89             : bool
      90        1132 : NekVolumetricSource::normalizeVolumetricSource(const double moose,
      91             :                                                double nek,
      92             :                                                double & normalized_nek)
      93             : {
      94             :   auto dimension_multiplier =
      95        1132 :       nekrs::referenceVolume() * nekrs::nondimensionalDivisor(field::heat_source);
      96             : 
      97             :   // scale the nek source to dimensional form for the sake of normalizing against
      98             :   // a dimensional MOOSE source
      99        1132 :   nek *= dimension_multiplier;
     100             : 
     101             :   // avoid divide-by-zero
     102        1132 :   if (std::abs(nek) < _abs_tol)
     103             :     return true;
     104             : 
     105        1108 :   nekrs::scaleUsrwrk(indices.heat_source, moose / nek);
     106             : 
     107             :   // check that the normalization worked properly
     108        1108 :   normalized_nek =
     109        1108 :       nekrs::usrwrkVolumeIntegral(indices.heat_source, nek_mesh::all) * dimension_multiplier;
     110        1108 :   bool low_rel_err = std::abs(normalized_nek - moose) / moose < _rel_tol;
     111        1108 :   bool low_abs_err = std::abs(normalized_nek - moose) < _abs_tol;
     112             : 
     113        1108 :   return low_rel_err && low_abs_err;
     114             : }
     115             : 
     116             : void
     117        1132 : NekVolumetricSource::sendDataToNek()
     118             : {
     119        1132 :   _console << "Sending volumetric source to NekRS..." << std::endl;
     120             : 
     121        1132 :   auto d = nekrs::nondimensionalDivisor(field::heat_source);
     122        1132 :   auto a = nekrs::nondimensionalAdditive(field::heat_source);
     123     3157788 :   for (unsigned int e = 0; e < _nek_mesh->numVolumeElems(); e++)
     124             :   {
     125             :     // We can only write into the nekRS scratch space if that face is "owned" by the current process
     126     3156656 :     if (nekrs::commRank() != _nek_mesh->volumeCoupling().processor_id(e))
     127     2759908 :       continue;
     128             : 
     129      396748 :     _nek_problem.mapVolumeDataToNekVolume(e, _variable_number[_variable], d, a, &_v_elem);
     130      396748 :     _nek_problem.writeVolumeSolution(e, field::heat_source, _v_elem);
     131             :   }
     132             : 
     133             :   // Because the NekRSMesh may be quite different from that used in the app solving for
     134             :   // the heat source, we will need to normalize the total source on the nekRS side by the
     135             :   // total source computed by the coupled MOOSE app.
     136        1132 :   const Real scale_cubed = _nek_mesh->scaling() * _nek_mesh->scaling() * _nek_mesh->scaling();
     137        1132 :   const double nek_source = nekrs::usrwrkVolumeIntegral(indices.heat_source, nek_mesh::all);
     138        1132 :   const double moose_source = *_source_integral;
     139             : 
     140             :   // For the sake of printing diagnostics to the screen regarding source normalization,
     141             :   // we first scale the nek source by any unit changes and then by the reference source
     142             :   const double nek_source_print_mult =
     143        1132 :       scale_cubed * nekrs::nondimensionalDivisor(field::heat_source);
     144        1132 :   double normalized_nek_source = 0.0;
     145             :   bool successful_normalization;
     146             : 
     147        1132 :   _console << "[volume]: Normalizing total NekRS heat source of "
     148        1132 :            << Moose::stringify(nek_source * nek_source_print_mult)
     149        2264 :            << " to the conserved MOOSE value of " + Moose::stringify(moose_source) << std::endl;
     150             : 
     151             :   // Any unit changes (for DIMENSIONAL nekRS runs) are automatically accounted for
     152             :   // here because moose_source is an integral on the MOOSE mesh, while nek_source is
     153             :   // an integral on the nek mesh
     154             :   successful_normalization =
     155        1132 :       normalizeVolumetricSource(moose_source, nek_source, normalized_nek_source);
     156             : 
     157             :   // If before normalization, there is a large difference between the nekRS imposed source
     158             :   // and the MOOSE source, this could mean that there is a poor match between the domains,
     159             :   // even if neither value is zero. For instance, if you forgot that the nekRS mesh is in
     160             :   // units of centimeters, but you're coupling to an app based in meters, the sources will
     161             :   // be very different from one another.
     162        1132 :   if (moose_source &&
     163        1132 :       (std::abs(nek_source * nek_source_print_mult - moose_source) / moose_source) > 0.25)
     164          24 :     mooseDoOnce(
     165             :         mooseWarning("NekRS source differs from MOOSE source by more than 25\%! This is NOT "
     166             :                      "necessarily a problem - but it could indicate that your geometries don't "
     167             :                      "line up properly or something is amiss with your transfer. We recommend "
     168             :                      "opening the output files to visually inspect the volumetric source in both "
     169             :                      "the main and sub applications to check that the fields look correct."));
     170             : 
     171        1132 :   if (!successful_normalization)
     172           0 :     mooseError("Volumetric source normalization process failed! NekRS integrated source: ",
     173             :                normalized_nek_source,
     174             :                " MOOSE integrated source: ",
     175             :                moose_source,
     176             :                ".\n\n",
     177           0 :                normalizationHint());
     178        1132 : }
     179             : 
     180             : #endif

Generated by: LCOV version 1.14