LCOV - code coverage report
Current view: top level - src/transfers - NekBoundaryFlux.C (source / functions) Hit Total Coverage
Test: neams-th-coe/cardinal: 920dc5 Lines: 167 172 97.1 %
Date: 2025-08-10 20:41:39 Functions: 7 7 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 "NekBoundaryFlux.h"
      22             : 
      23             : registerMooseObject("CardinalApp", NekBoundaryFlux);
      24             : 
      25             : extern nekrs::usrwrkIndices indices;
      26             : 
      27             : InputParameters
      28         525 : NekBoundaryFlux::validParams()
      29             : {
      30         525 :   auto params = ConservativeFieldTransfer::validParams();
      31        1050 :   params.addParam<Real>(
      32             :       "initial_flux_integral",
      33        1050 :       0,
      34             :       "Initial value to use for the 'postprocessor_to_conserve'; this initial value will be "
      35             :       "overridden once the coupled app executes its transfer of the boundary flux term integral "
      36             :       "into the 'postprocessor_to_conserve'. You may want to use this parameter if NekRS runs "
      37             :       "first, or if you are running NekRS in isolation but still want to apply a boundary flux "
      38             :       "term via Cardinal. Remember that this parameter is only used to normalize the flux, so you "
      39             :       "will need to populate an initial shape if you want to see this parameter take effect.");
      40             : 
      41        1050 :   params.addParam<bool>(
      42             :       "conserve_flux_by_sideset",
      43        1050 :       false,
      44             :       "Whether to conserve the flux by individual sideset (as opposed to lumping all sidesets "
      45             :       "together). Setting this option to true requires syntax changes in the input file to use "
      46             :       "vector postprocessors, and places restrictions on how the sidesets are set up.");
      47             : 
      48         525 :   params.addClassDescription("Reads/writes boundary flux data between NekRS and MOOSE.");
      49         525 :   return params;
      50           0 : }
      51             : 
      52         265 : NekBoundaryFlux::NekBoundaryFlux(const InputParameters & parameters)
      53             :   : ConservativeFieldTransfer(parameters),
      54         263 :     _conserve_flux_by_sideset(getParam<bool>("conserve_flux_by_sideset")),
      55         526 :     _initial_flux_integral(getParam<Real>("initial_flux_integral")),
      56         263 :     _boundary(_nek_mesh->boundary()),
      57         528 :     _reference_flux_integral(nekrs::referenceArea() * nekrs::nondimensionalDivisor(field::flux))
      58             : {
      59         263 :   if (!_boundary)
      60           1 :     mooseError("NekBoundaryFlux can only be used when there is boundary coupling of NekRS with "
      61             :                "MOOSE, i.e. when 'boundary' is provided in NekRSMesh.");
      62             : 
      63         262 :   if (!nekrs::hasTemperatureVariable())
      64           1 :     mooseError("In order to read or write NekRS's boundary heat flux, your case files must have a "
      65           1 :                "[TEMPERATURE] block. Note that you can set 'solver = none' in '" +
      66           1 :                _nek_problem.casename() + ".par' if you don't want to solve for temperature.");
      67             : 
      68             :   // add the variables for the coupling and perform checks on problem setup
      69         261 :   if (_direction == "from_nek")
      70             :   {
      71          17 :     if (_conserve_flux_by_sideset)
      72           0 :       paramError("conserve_flux_by_sideset",
      73             :                  "When 'direction = from_nek', the 'conserve_flux_by_sideset' option is not yet "
      74             :                  "supported. Contact the Cardinal developer team if you need this feature.");
      75             : 
      76          34 :     checkUnusedParam(parameters, "initial_flux_integral", "'direction = from_nek'");
      77          17 :     addExternalVariable(_variable);
      78             : 
      79             :     // right now, all of our systems used for transferring data assume that if we have a volume
      80             :     // mesh mirror, that we will be writing data to that entire mesh mirror. But this is not the
      81             :     // case if we want to write a heat flux - since the notion of a unit outward normal is s
      82             :     // surface quantity. For now, just prevent users from doing this.
      83          17 :     if (_nek_mesh->volume())
      84           1 :       mooseError("The NekBoundaryFlux does not currently support writing heat flux on a boundary "
      85             :                  "when the Mesh has 'volume = true.' Please contact the Cardinal developer team if "
      86             :                  "you require this feature.");
      87             :   }
      88             :   else
      89             :   {
      90         244 :     if (_usrwrk_slot.size() > 1)
      91           2 :       paramError("usrwrk_slot",
      92             :                  "'usrwrk_slot' must be of length 1 for boundary flux transfers; you have entered "
      93           1 :                  "a vector of length " +
      94           0 :                      Moose::stringify(_usrwrk_slot.size()));
      95             : 
      96         485 :     addExternalVariable(_usrwrk_slot[0], _variable);
      97         242 :     indices.flux = _usrwrk_slot[0] * nekrs::fieldOffset();
      98             : 
      99             :     // Check that the correct flux boundary condition is set on all of nekRS's
     100             :     // boundaries. To avoid throwing this error for test cases where we have a
     101             :     // [TEMPERATURE] block but set its solve to 'none', we screen by whether the
     102             :     // temperature solve is enabled
     103         242 :     if (_boundary && nekrs::hasTemperatureSolve())
     104         437 :       for (const auto & b : *_boundary)
     105         219 :         if (!nekrs::isHeatFluxBoundary(b))
     106           1 :           mooseError("In order to send a boundary heat flux to NekRS, you must have a heat flux "
     107           1 :                      "condition for each 'boundary' set in 'NekRSMesh'! Boundary " +
     108           2 :                      std::to_string(b) + " is of type '" + nekrs::temperatureBoundaryType(b) +
     109             :                      "' instead of 'fixedGradient'.");
     110             : 
     111         241 :     if (!nekrs::hasTemperatureSolve())
     112          68 :       mooseWarning("By setting 'solver = none' for temperature in '" + _nek_problem.casename() +
     113             :                    ".par', NekRS will not solve for temperature. The heat flux sent by this object "
     114             :                    "will be unused.");
     115             :   }
     116             : 
     117             :   // add postprocessors to hold integrated heat flux
     118         256 :   if (_direction == "to_nek")
     119             :   {
     120             :     // add the postprocessor that receives the flux integral for normalization
     121         240 :     if (_conserve_flux_by_sideset)
     122             :     {
     123          40 :       if (isParamSetByUser("initial_flux_integral"))
     124           0 :         mooseWarning("The 'initial_flux_integral' capability is not yet supported when "
     125             :                      "'conserve_flux_by_sideset' is enabled. Please contact a Cardinal developer "
     126             :                      "if this is hindering your use case.");
     127             : 
     128          20 :       auto vpp_params = _factory.getValidParams("ConstantVectorPostprocessor");
     129             : 
     130             :       // create zero initial values
     131          20 :       std::vector<std::vector<Real>> dummy_vals(1, std::vector<Real>(_boundary->size()));
     132          20 :       vpp_params.set<std::vector<std::vector<Real>>>("value") = dummy_vals;
     133          20 :       _nek_problem.addVectorPostprocessor(
     134          20 :           "ConstantVectorPostprocessor", _postprocessor_name, vpp_params);
     135          20 :     }
     136             :     else
     137         440 :       addExternalPostprocessor(_postprocessor_name, _initial_flux_integral);
     138             : 
     139         240 :     if (_conserve_flux_by_sideset)
     140          20 :       _flux_integral_vpp =
     141          40 :           &_nek_problem.getVectorPostprocessorValueByName(_postprocessor_name, "value");
     142             :     else
     143         220 :       _flux_integral = &getPostprocessorValueByName(_postprocessor_name);
     144             :   }
     145             :   else
     146             :   {
     147             :     // create a NekHeatFluxIntegral postprocessor to compute the heat flux
     148          16 :     auto pp_params = _factory.getValidParams("NekHeatFluxIntegral");
     149          16 :     pp_params.set<std::vector<int>>("boundary") = *_boundary;
     150             : 
     151             :     // we do not need to check for duplicate names, because MOOSE already handles
     152             :     // this error checking
     153          16 :     _nek_problem.addPostprocessor("NekHeatFluxIntegral", _postprocessor_name, pp_params);
     154          16 :     _flux_integral = &getPostprocessorValueByName(_postprocessor_name);
     155          16 :   }
     156         256 : }
     157             : 
     158             : void
     159         244 : NekBoundaryFlux::readDataFromNek()
     160             : {
     161         244 :   if (!_nek_mesh->volume())
     162         244 :     _nek_problem.boundarySolution(field::flux, _external_data);
     163             :   else
     164           0 :     _nek_problem.volumeSolution(field::flux, _external_data);
     165             : 
     166         244 :   fillAuxVariable(_variable_number[_variable], _external_data);
     167             : 
     168             :   // the postprocessor is automatically populated because its execution controls its writing
     169         244 : }
     170             : 
     171             : void
     172       13314 : NekBoundaryFlux::sendDataToNek()
     173             : {
     174       26628 :   _console << "Sending flux to NekRS boundary " << Moose::stringify(*_boundary) << "..."
     175       13314 :            << std::endl;
     176       13314 :   auto d = nekrs::nondimensionalDivisor(field::flux);
     177       13314 :   auto a = nekrs::nondimensionalAdditive(field::flux);
     178             : 
     179       13314 :   if (!_nek_mesh->volume())
     180             :   {
     181     1863100 :     for (unsigned int e = 0; e < _nek_mesh->numSurfaceElems(); e++)
     182             :     {
     183             :       // We can only write into the nekRS scratch space if that face is "owned" by the current
     184             :       // process
     185     1850608 :       if (nekrs::commRank() != _nek_mesh->boundaryCoupling().processor_id(e))
     186     1561840 :         continue;
     187             : 
     188      288768 :       _nek_problem.mapFaceDataToNekFace(e, _variable_number[_variable], d, a, &_v_face);
     189      288768 :       _nek_problem.writeBoundarySolution(e, field::flux, _v_face);
     190             :     }
     191             :   }
     192             :   else
     193             :   {
     194     2975814 :     for (unsigned int e = 0; e < _nek_mesh->numVolumeElems(); ++e)
     195             :     {
     196             :       // We can only write into the nekRS scratch space if that face is "owned" by the current
     197             :       // process
     198     2974992 :       if (nekrs::commRank() != _nek_mesh->volumeCoupling().processor_id(e))
     199     2338656 :         continue;
     200             : 
     201      636336 :       _nek_problem.mapFaceDataToNekVolume(e, _variable_number[_variable], d, a, &_v_elem);
     202      636336 :       _nek_problem.writeVolumeSolution(e, field::flux, _v_elem);
     203             :     }
     204             :   }
     205             : 
     206             :   // Because the NekRSMesh may be quite different from that used in the app solving for
     207             :   // the heat flux, we will need to normalize the flux on the nekRS side by the
     208             :   // flux computed by the coupled MOOSE app. For this and the next check of the
     209             :   // flux integral, we need to scale the integral back up again to the dimensional form
     210             :   // for the sake of comparison.
     211       13314 :   const Real scale_squared = _nek_mesh->scaling() * _nek_mesh->scaling();
     212       13314 :   const double nek_flux_print_mult = scale_squared * nekrs::nondimensionalDivisor(field::flux);
     213             : 
     214             :   // integrate the flux over each individual boundary
     215             :   std::vector<double> nek_flux_sidesets =
     216       13314 :       nekrs::usrwrkSideIntegral(indices.flux, *_boundary, nek_mesh::all);
     217             : 
     218             :   bool successful_normalization;
     219       13314 :   double normalized_nek_flux = 0.0;
     220             : 
     221             :   double total_moose_flux;
     222             : 
     223       13314 :   if (_conserve_flux_by_sideset)
     224             :   {
     225         254 :     auto moose_flux = *_flux_integral_vpp;
     226         254 :     if (moose_flux.size() != _boundary->size())
     227           1 :       mooseError("The sideset flux reporter transferred to NekRS must have a length equal to the "
     228             :                  "number of entries in 'boundary'! Please check the values written to the "
     229             :                  "'flux_integral' vector postprocessor.\n\n"
     230             :                  "Length of reporter: ",
     231           1 :                  moose_flux.size(),
     232             :                  "\n",
     233             :                  "Length of 'boundary': ",
     234           1 :                  _boundary->size());
     235             : 
     236         549 :     for (std::size_t b = 0; b < _boundary->size(); ++b)
     237             :     {
     238         592 :       _console << "[boundary " << Moose::stringify((*_boundary)[b])
     239         296 :                << "]: Normalizing NekRS flux of "
     240         592 :                << Moose::stringify(nek_flux_sidesets[b] * nek_flux_print_mult)
     241         592 :                << " to the conserved MOOSE value of " << Moose::stringify(moose_flux[b])
     242         296 :                << std::endl;
     243             : 
     244         296 :       checkInitialFluxValues(nek_flux_sidesets[b], moose_flux[b]);
     245             :     }
     246             : 
     247         253 :     total_moose_flux = std::accumulate(moose_flux.begin(), moose_flux.end(), 0.0);
     248             : 
     249             :     // For the sake of printing diagnostics to the screen regarding the flux normalization,
     250             :     // we first scale the nek flux by any unit changes and then by the reference flux.
     251             :     successful_normalization =
     252         253 :         normalizeFluxBySideset(moose_flux, nek_flux_sidesets, normalized_nek_flux);
     253             :   }
     254             :   else
     255             :   {
     256       13060 :     auto moose_flux = *_flux_integral;
     257             :     const double nek_flux =
     258       13060 :         std::accumulate(nek_flux_sidesets.begin(), nek_flux_sidesets.end(), 0.0);
     259             : 
     260       26120 :     _console << "[boundary " << Moose::stringify(*_boundary)
     261       13060 :              << "]: Normalizing total NekRS flux of "
     262       26120 :              << Moose::stringify(nek_flux * nek_flux_print_mult)
     263       13060 :              << " to the conserved MOOSE value of " << Moose::stringify(moose_flux) << std::endl;
     264       13060 :     _console << Moose::stringify(nek_flux) << " " << nek_flux_print_mult << std::endl;
     265             : 
     266       13060 :     checkInitialFluxValues(nek_flux, moose_flux);
     267             : 
     268       13060 :     total_moose_flux = moose_flux;
     269             : 
     270             :     // For the sake of printing diagnostics to the screen regarding the flux normalization,
     271       13060 :     successful_normalization = normalizeFlux(moose_flux, nek_flux, normalized_nek_flux);
     272             :   }
     273             : 
     274       13313 :   if (!successful_normalization)
     275           1 :     mooseError(
     276             :         "Flux normalization process failed! NekRS integrated flux: ",
     277             :         normalized_nek_flux,
     278             :         " MOOSE integrated flux: ",
     279             :         total_moose_flux,
     280           1 :         normalizationHint(),
     281             :         "\n\n- If you set 'conserve_flux_by_sideset = true' and nodes are SHARED by boundaries "
     282             :         "(like on corners between sidesets), you will end up renormalizing those shared nodes "
     283             :         "once per sideset that they lie on. There is no guarantee that the total imposed flux "
     284             :         "would be preserved.");
     285       13312 : }
     286             : 
     287             : void
     288       13356 : NekBoundaryFlux::checkInitialFluxValues(const Real & nek_flux, const Real & moose_flux) const
     289             : {
     290       13356 :   const Real scale_squared = _nek_mesh->scaling() * _nek_mesh->scaling();
     291       13356 :   const double nek_flux_print_mult = scale_squared * nekrs::nondimensionalDivisor(field::flux);
     292             : 
     293             :   // If before normalization, there is a large difference between the nekRS imposed flux
     294             :   // and the MOOSE flux, this could mean that there is a poor match between the domains,
     295             :   // even if neither value is zero. For instance, if you forgot that the nekRS mesh is in
     296             :   // units of centimeters, but you're coupling to an app based in meters, the fluxes will
     297             :   // be very different from one another.
     298       13356 :   if (moose_flux && (std::abs(nek_flux * nek_flux_print_mult - moose_flux) / moose_flux) > 0.25)
     299          72 :     mooseDoOnce(mooseWarning(
     300             :         "NekRS flux differs from MOOSE flux by more than 25\%! This is NOT necessarily a problem - "
     301             :         "but it could indicate that your geometries don't line up properly or something is amiss "
     302             :         "with your transfer. We recommend opening the output files to visually inspect the flux in "
     303             :         "both the main and sub applications to check that the fields look correct."));
     304       13356 : }
     305             : 
     306             : bool
     307         253 : NekBoundaryFlux::normalizeFluxBySideset(const std::vector<double> & moose_integral,
     308             :                                         std::vector<double> & nek_integral,
     309             :                                         double & normalized_nek_integral)
     310             : {
     311             :   // scale the nek flux to dimensional form for the sake of normalizing against
     312             :   // a dimensional MOOSE flux
     313         549 :   for (auto & i : nek_integral)
     314         296 :     i *= _reference_flux_integral;
     315             : 
     316         253 :   nrs_t * nrs = (nrs_t *)nekrs::nrsPtr();
     317         253 :   mesh_t * mesh = nekrs::temperatureMesh();
     318         253 :   auto nek_boundary_coupling = _nek_mesh->boundaryCoupling();
     319             : 
     320      146405 :   for (int k = 0; k < nek_boundary_coupling.total_n_faces; ++k)
     321             :   {
     322      146152 :     if (nek_boundary_coupling.process[k] == nekrs::commRank())
     323             :     {
     324       25264 :       int i = nek_boundary_coupling.element[k];
     325       25264 :       int j = nek_boundary_coupling.face[k];
     326             : 
     327       25264 :       int face_id = mesh->EToB[i * mesh->Nfaces + j];
     328       25264 :       auto it = std::find(_boundary->begin(), _boundary->end(), face_id);
     329             :       auto b_index = it - _boundary->begin();
     330             : 
     331             :       // avoid divide-by-zero
     332             :       double ratio = 1.0;
     333       25264 :       if (std::abs(nek_integral[b_index]) > _abs_tol)
     334       24512 :         ratio = moose_integral[b_index] / nek_integral[b_index];
     335             : 
     336       25264 :       int offset = i * mesh->Nfaces * mesh->Nfp + j * mesh->Nfp;
     337             : 
     338      670768 :       for (int v = 0; v < mesh->Nfp; ++v)
     339             :       {
     340      645504 :         int id = mesh->vmapM[offset + v];
     341      645504 :         nrs->usrwrk[indices.flux + id] *= ratio;
     342             :       }
     343             :     }
     344             :   }
     345             : 
     346             :   // check that the normalization worked properly - confirm against dimensional form
     347         253 :   auto integrals = nekrs::usrwrkSideIntegral(indices.flux, *_boundary, nek_mesh::all);
     348         253 :   normalized_nek_integral =
     349         253 :       std::accumulate(integrals.begin(), integrals.end(), 0.0) * _reference_flux_integral;
     350             :   double total_moose_integral = std::accumulate(moose_integral.begin(), moose_integral.end(), 0.0);
     351             :   bool low_rel_err =
     352         253 :       std::abs(total_moose_integral) > _abs_tol
     353         253 :           ? std::abs(normalized_nek_integral - total_moose_integral) / total_moose_integral <
     354         249 :                 _rel_tol
     355             :           : true;
     356         253 :   bool low_abs_err = std::abs(normalized_nek_integral - total_moose_integral) < _abs_tol;
     357             : 
     358         506 :   return low_rel_err || low_abs_err;
     359         253 : }
     360             : 
     361             : bool
     362       13060 : NekBoundaryFlux::normalizeFlux(const double moose_integral,
     363             :                                double nek_integral,
     364             :                                double & normalized_nek_integral)
     365             : {
     366             :   // scale the nek flux to dimensional form for the sake of normalizing against
     367             :   // a dimensional MOOSE flux
     368       13060 :   nek_integral *= _reference_flux_integral;
     369             : 
     370       13060 :   auto nek_boundary_coupling = _nek_mesh->boundaryCoupling();
     371             : 
     372             :   // avoid divide-by-zero
     373       13060 :   if (std::abs(nek_integral) < _abs_tol)
     374             :     return true;
     375             : 
     376       12876 :   nrs_t * nrs = (nrs_t *)nekrs::nrsPtr();
     377       12876 :   mesh_t * mesh = nekrs::temperatureMesh();
     378             : 
     379       12876 :   const double ratio = moose_integral / nek_integral;
     380             : 
     381     2049276 :   for (int k = 0; k < nek_boundary_coupling.total_n_faces; ++k)
     382             :   {
     383     2036400 :     if (nek_boundary_coupling.process[k] == nekrs::commRank())
     384             :     {
     385      390624 :       int i = nek_boundary_coupling.element[k];
     386      390624 :       int j = nek_boundary_coupling.face[k];
     387      390624 :       int offset = i * mesh->Nfaces * mesh->Nfp + j * mesh->Nfp;
     388             : 
     389     7235064 :       for (int v = 0; v < mesh->Nfp; ++v)
     390             :       {
     391     6844440 :         int id = mesh->vmapM[offset + v];
     392     6844440 :         nrs->usrwrk[indices.flux + id] *= ratio;
     393             :       }
     394             :     }
     395             :   }
     396             : 
     397             :   // check that the normalization worked properly - confirm against dimensional form
     398       12876 :   auto integrals = nekrs::usrwrkSideIntegral(indices.flux, *_boundary, nek_mesh::all);
     399       12876 :   normalized_nek_integral =
     400       12876 :       std::accumulate(integrals.begin(), integrals.end(), 0.0) * _reference_flux_integral;
     401       12876 :   bool low_rel_err = std::abs(normalized_nek_integral - moose_integral) / moose_integral < _rel_tol;
     402       12876 :   bool low_abs_err = std::abs(normalized_nek_integral - moose_integral) < _abs_tol;
     403             : 
     404       12876 :   return low_rel_err || low_abs_err;
     405       13060 : }
     406             : 
     407             : #endif

Generated by: LCOV version 1.14