LCOV - code coverage report
Current view: top level - src/neml2/userobjects - NEML2ModelExecutor.C (source / functions) Hit Total Coverage
Test: idaholab/moose framework: 419b9d Lines: 164 216 75.9 %
Date: 2025-08-08 20:01:16 Functions: 19 20 95.0 %
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             : #include "NEML2ModelExecutor.h"
      11             : #include "MOOSEToNEML2.h"
      12             : #include "NEML2Utils.h"
      13             : #include <string>
      14             : 
      15             : #ifdef NEML2_ENABLED
      16             : #include "libmesh/id_types.h"
      17             : #include "neml2/tensors/functions/jacrev.h"
      18             : #include "neml2/dispatchers/ValueMapLoader.h"
      19             : #endif
      20             : 
      21             : registerMooseObject("MooseApp", NEML2ModelExecutor);
      22             : 
      23             : InputParameters
      24       14739 : NEML2ModelExecutor::actionParams()
      25             : {
      26       14739 :   auto params = emptyInputParameters();
      27             :   // allow user to explicit skip required input variables
      28       14739 :   params.addParam<std::vector<std::string>>(
      29             :       "skip_inputs",
      30             :       {},
      31       29478 :       NEML2Utils::docstring(
      32             :           "List of NEML2 variables to skip error checking when setting up the model input. If an "
      33             :           "input variable is skipped, its value will stay zero. If a required input variable is "
      34             :           "not skipped, an error will be raised."));
      35       14739 :   return params;
      36           0 : }
      37             : 
      38             : InputParameters
      39       14299 : NEML2ModelExecutor::validParams()
      40             : {
      41       14299 :   auto params = NEML2ModelInterface<GeneralUserObject>::validParams();
      42       14299 :   params += NEML2ModelExecutor::actionParams();
      43       14299 :   params.addClassDescription(NEML2Utils::docstring("Execute the specified NEML2 model"));
      44             : 
      45       14299 :   params.addRequiredParam<UserObjectName>(
      46             :       "batch_index_generator",
      47             :       "The NEML2BatchIndexGenerator used to generate the element-to-batch-index map.");
      48       14299 :   params.addParam<std::vector<UserObjectName>>(
      49             :       "gatherers",
      50             :       {},
      51       28598 :       NEML2Utils::docstring(
      52             :           "List of MOOSE*ToNEML2 user objects gathering MOOSE data as NEML2 input variables"));
      53       14299 :   params.addParam<std::vector<UserObjectName>>(
      54             :       "param_gatherers",
      55             :       {},
      56       28598 :       NEML2Utils::docstring(
      57             :           "List of MOOSE*ToNEML2 user objects gathering MOOSE data as NEML2 model parameters"));
      58             : 
      59             :   // Since we use the NEML2 model to evaluate the residual AND the Jacobian at the same time, we
      60             :   // want to execute this user object only at execute_on = LINEAR (i.e. during residual evaluation).
      61             :   // The NONLINEAR exec flag below is for computing Jacobian during automatic scaling.
      62       14299 :   ExecFlagEnum execute_options = MooseUtils::getDefaultExecFlagEnum();
      63       57196 :   execute_options = {EXEC_INITIAL, EXEC_LINEAR, EXEC_NONLINEAR};
      64       14299 :   params.set<ExecFlagEnum>("execute_on") = execute_options;
      65             : 
      66       28598 :   return params;
      67       28598 : }
      68             : 
      69          17 : NEML2ModelExecutor::NEML2ModelExecutor(const InputParameters & params)
      70           0 :   : NEML2ModelInterface<GeneralUserObject>(params)
      71             : #ifdef NEML2_ENABLED
      72             :     ,
      73          17 :     _batch_index_generator(getUserObject<NEML2BatchIndexGenerator>("batch_index_generator")),
      74          17 :     _output_ready(false),
      75          34 :     _error_message("")
      76             : #endif
      77             : {
      78             : #ifdef NEML2_ENABLED
      79          17 :   validateModel();
      80             : 
      81             :   // add user object dependencies by name (the UOs do not need to exist yet for this)
      82          47 :   for (const auto & gatherer_name : getParam<std::vector<UserObjectName>>("gatherers"))
      83          30 :     _depend_uo.insert(gatherer_name);
      84          19 :   for (const auto & gatherer_name : getParam<std::vector<UserObjectName>>("param_gatherers"))
      85           2 :     _depend_uo.insert(gatherer_name);
      86             : 
      87             :   // variables to skip error checking (converting vector to set to prevent duplicate checks)
      88          17 :   for (const auto & var_name : getParam<std::vector<std::string>>("skip_inputs"))
      89           0 :     _skip_vars.insert(NEML2Utils::parseVariableName(var_name));
      90             : #endif
      91          17 : }
      92             : 
      93             : #ifdef NEML2_ENABLED
      94             : void
      95          17 : NEML2ModelExecutor::initialSetup()
      96             : {
      97             :   // deal with user object provided inputs
      98          47 :   for (const auto & gatherer_name : getParam<std::vector<UserObjectName>>("gatherers"))
      99             :   {
     100             :     // gather coupled user objects late to ensure they are constructed. Do not add them as
     101             :     // dependencies (that's already done in the constructor).
     102          30 :     const auto & uo = getUserObjectByName<MOOSEToNEML2>(gatherer_name, /*is_dependency=*/false);
     103             : 
     104             :     // the target neml2 variable must exist on the input axis
     105          30 :     if (!model().input_axis().has_variable(NEML2Utils::parseVariableName(uo.NEML2Name())))
     106           0 :       mooseError("The MOOSEToNEML2 gatherer named '",
     107             :                  gatherer_name,
     108             :                  "' is gathering MOOSE data for a non-existent NEML2 input variable named '",
     109           0 :                  uo.NEML2Name(),
     110             :                  "'.");
     111             : 
     112             :     // tell the gatherer to gather for a model input variable
     113          30 :     const auto varname = NEML2Utils::parseVariableName(uo.NEML2Name());
     114          30 :     if (varname.is_old_force() || varname.is_old_state())
     115           0 :       uo.setMode(MOOSEToNEML2::Mode::OLD_VARIABLE);
     116             :     else
     117          30 :       uo.setMode(MOOSEToNEML2::Mode::VARIABLE);
     118             : 
     119          30 :     addGatheredVariable(gatherer_name, uo.NEML2VariableName());
     120          30 :     _gatherers.push_back(&uo);
     121          30 :   }
     122             : 
     123             :   // deal with user object provided model parameters
     124          19 :   for (const auto & gatherer_name : getParam<std::vector<UserObjectName>>("param_gatherers"))
     125             :   {
     126             :     // gather coupled user objects late to ensure they are constructed. Do not add them as
     127             :     // dependencies (that's already done in the constructor).
     128           2 :     const auto & uo = getUserObjectByName<MOOSEToNEML2>(gatherer_name, /*is_dependency=*/false);
     129             : 
     130             :     // introspect the NEML2 model to figure out if the gatherer UO is gathering for a NEML2 input
     131             :     // variable or for a NEML2 model parameter
     132           2 :     if (model().named_parameters().count(uo.NEML2Name()) != 1)
     133           0 :       mooseError("The MOOSEToNEML2 gatherer named '",
     134             :                  gatherer_name,
     135             :                  "' is gathering MOOSE data for a non-existent NEML2 model parameter named '",
     136           0 :                  uo.NEML2Name(),
     137             :                  "'.");
     138             : 
     139             :     // tell the gatherer to gather for a model parameter
     140           2 :     uo.setMode(MOOSEToNEML2::Mode::PARAMETER);
     141             : 
     142           2 :     addGatheredParameter(gatherer_name, uo.NEML2ParameterName());
     143           2 :     _gatherers.push_back(&uo);
     144             :   }
     145             : 
     146             :   // iterate over set of required inputs and error out if we find one that is not provided
     147          17 :   std::vector<neml2::VariableName> required_inputs = model().input_axis().variable_names();
     148          47 :   for (const auto & input : required_inputs)
     149             :   {
     150          30 :     if (_skip_vars.count(input))
     151           0 :       continue;
     152             :     // skip input state variables because they are "initial guesses" to the nonlinear system
     153          30 :     if (input.is_state())
     154           0 :       continue;
     155          30 :     if (!_gathered_variable_names.count(input))
     156           0 :       paramError("gatherers", "The required model input `", input, "` is not gathered");
     157             :   }
     158             : 
     159             :   // If a variable is stateful, then it'd better been retrieved by someone! In theory that's not
     160             :   // sufficient for stateful data management, but that's the best we can do here without being
     161             :   // overly restrictive.
     162          47 :   for (const auto & input : required_inputs)
     163          30 :     if (input.is_old_state() && !_retrieved_outputs.count(input.current()))
     164           0 :       mooseError(
     165             :           "The NEML2 model requires a stateful input variable `",
     166             :           input,
     167             :           "`, but its state counterpart on the output axis has not been retrieved by any object. "
     168             :           "Therefore, there is no way to properly propagate the corresponding stateful data in "
     169             :           "time. The common solution to this problem is to add a NEML2ToMOOSE retriever such as "
     170             :           "those called `NEML2To*MOOSEMaterialProperty`.");
     171          17 : }
     172             : 
     173             : std::size_t
     174       43580 : NEML2ModelExecutor::getBatchIndex(dof_id_type elem_id) const
     175             : {
     176       43580 :   return _batch_index_generator.getBatchIndex(elem_id);
     177             : }
     178             : 
     179             : void
     180          30 : NEML2ModelExecutor::addGatheredVariable(const UserObjectName & gatherer_name,
     181             :                                         const neml2::VariableName & var)
     182             : {
     183          30 :   if (_gathered_variable_names.count(var))
     184           0 :     paramError("gatherers",
     185             :                "The NEML2 input variable `",
     186             :                var,
     187             :                "` gathered by UO '",
     188             :                gatherer_name,
     189             :                "' is already gathered by another gatherer.");
     190          30 :   _gathered_variable_names.insert(var);
     191          30 : }
     192             : 
     193             : void
     194           2 : NEML2ModelExecutor::addGatheredParameter(const UserObjectName & gatherer_name,
     195             :                                          const std::string & param)
     196             : {
     197           2 :   if (_gathered_parameter_names.count(param))
     198           0 :     paramError("gatherers",
     199             :                "The NEML2 model parameter `",
     200             :                param,
     201             :                "` gathered by UO '",
     202             :                gatherer_name,
     203             :                "' is already gathered by another gatherer.");
     204           2 :   _gathered_parameter_names.insert(param);
     205           2 : }
     206             : 
     207             : void
     208        1121 : NEML2ModelExecutor::initialize()
     209             : {
     210        1121 :   if (!NEML2Utils::shouldCompute(_fe_problem))
     211         460 :     return;
     212             : 
     213         661 :   _output_ready = false;
     214         661 :   _error = false;
     215         661 :   _error_message.clear();
     216             : }
     217             : 
     218             : void
     219           8 : NEML2ModelExecutor::meshChanged()
     220             : {
     221           8 :   if (!NEML2Utils::shouldCompute(_fe_problem))
     222           0 :     return;
     223             : 
     224           8 :   _output_ready = false;
     225             : }
     226             : 
     227             : void
     228        1121 : NEML2ModelExecutor::execute()
     229             : {
     230        1121 :   if (!NEML2Utils::shouldCompute(_fe_problem))
     231         460 :     return;
     232             : 
     233         661 :   fillInputs();
     234             : 
     235         661 :   if (_t_step > 0)
     236             :   {
     237         645 :     applyPredictor();
     238         645 :     auto success = solve();
     239         645 :     if (success)
     240         644 :       extractOutputs();
     241             :   }
     242             : }
     243             : 
     244             : void
     245         661 : NEML2ModelExecutor::fillInputs()
     246             : {
     247             :   try
     248             :   {
     249        1382 :     for (const auto & uo : _gatherers)
     250         721 :       uo->insertInto(_in, _model_params);
     251             : 
     252             :     // Send input variables and parameters to device
     253        1376 :     for (auto & [var, val] : _in)
     254         715 :       val = val.to(device());
     255         667 :     for (auto & [param, pval] : _model_params)
     256           6 :       pval = pval.to(device());
     257             : 
     258             :     // Update model parameters
     259         661 :     model().set_parameters(_model_params);
     260         661 :     _model_params.clear();
     261             : 
     262             :     // Request gradient for the model parameters that we request AD for
     263         661 :     for (const auto & [y, dy] : _retrieved_parameter_derivatives)
     264           0 :       for (const auto & [p, tensor] : dy)
     265           0 :         model().get_parameter(p).requires_grad_(true);
     266             :   }
     267           0 :   catch (std::exception & e)
     268             :   {
     269           0 :     mooseError("An error occurred while filling inputs for the NEML2 model. Error message:\n",
     270           0 :                e.what(),
     271             :                NEML2Utils::NEML2_help_message);
     272           0 :   }
     273         661 : }
     274             : 
     275             : void
     276         645 : NEML2ModelExecutor::applyPredictor()
     277             : {
     278             :   try
     279             :   {
     280         645 :     if (!model().input_axis().has_state())
     281         645 :       return;
     282           0 :     if (!model().input_axis().has_old_state())
     283           0 :       return;
     284             : 
     285             :     // Set trial state variables (i.e., initial guesses).
     286             :     // Right now we hard-code to use the old state as the trial state.
     287             :     // TODO: implement other predictors
     288           0 :     const auto & input_state = model().input_axis().subaxis(neml2::STATE);
     289           0 :     const auto & input_old_state = model().input_axis().subaxis(neml2::OLD_STATE);
     290           0 :     for (const auto & var : input_state.variable_names())
     291           0 :       if (input_old_state.has_variable(var))
     292           0 :         _in[var.prepend(neml2::STATE)] = _in[var.prepend(neml2::OLD_STATE)];
     293             :   }
     294           0 :   catch (std::exception & e)
     295             :   {
     296           0 :     mooseError("An error occurred while applying predictor for the NEML2 model. Error message:\n",
     297           0 :                e.what(),
     298             :                NEML2Utils::NEML2_help_message);
     299           0 :   }
     300             : }
     301             : 
     302             : void
     303           4 : NEML2ModelExecutor::expandInputs()
     304             : {
     305             :   // Figure out what our batch size is
     306           4 :   std::vector<neml2::Tensor> defined;
     307          12 :   for (const auto & [key, value] : _in)
     308           8 :     defined.push_back(value);
     309           4 :   const auto batch_shape = neml2::utils::broadcast_batch_sizes(defined);
     310             : 
     311             :   // Make all inputs conformal
     312          12 :   for (auto & [key, value] : _in)
     313           8 :     if (value.batch_sizes() != batch_shape)
     314           0 :       _in[key] = value.batch_unsqueeze(0).batch_expand(batch_shape);
     315           4 : }
     316             : 
     317             : bool
     318         645 : NEML2ModelExecutor::solve()
     319             : {
     320             :   try
     321             :   {
     322             :     // Evaluate the NEML2 material model
     323         645 :     TIME_SECTION("NEML2 solve", 3, "Solving NEML2 material model");
     324             : 
     325             :     // NEML2 requires double precision
     326         645 :     auto prev_dtype = neml2::get_default_dtype();
     327         645 :     neml2::set_default_dtype(neml2::kFloat64);
     328             : 
     329         645 :     if (scheduler())
     330             :     {
     331             :       // We only need consistent batch sizes if we are using the dispatcher
     332           4 :       expandInputs();
     333           4 :       neml2::ValueMapLoader loader(_in, 0);
     334           4 :       std::tie(_out, _dout_din) = dispatcher()->run(loader);
     335           4 :     }
     336             :     else
     337         641 :       std::tie(_out, _dout_din) = model().value_and_dvalue(_in);
     338         644 :     _in.clear();
     339             : 
     340             :     // Restore the default dtype
     341         644 :     neml2::set_default_dtype(prev_dtype);
     342         645 :   }
     343           1 :   catch (std::exception & e)
     344             :   {
     345           1 :     _error_message = e.what();
     346           1 :     _error = true;
     347           1 :   }
     348             : 
     349         645 :   return !_error;
     350             : }
     351             : 
     352             : void
     353         644 : NEML2ModelExecutor::extractOutputs()
     354             : {
     355             :   try
     356             :   {
     357         644 :     const auto N = _batch_index_generator.getBatchIndex();
     358             : 
     359             :     // retrieve outputs
     360        1328 :     for (auto & [y, target] : _retrieved_outputs)
     361         684 :       target = _out[y].to(torch::kCPU);
     362             : 
     363             :     // retrieve parameter derivatives
     364         644 :     for (auto & [y, dy] : _retrieved_parameter_derivatives)
     365           0 :       for (auto & [p, target] : dy)
     366           0 :         target = neml2::jacrev(_out[y],
     367           0 :                                model().get_parameter(p),
     368             :                                /*retain_graph=*/true,
     369             :                                /*create_graph=*/false,
     370             :                                /*allow_unused=*/false)
     371           0 :                      .to(torch::kCPU);
     372             : 
     373             :     // clear output
     374         644 :     _out.clear();
     375             : 
     376             :     // retrieve derivatives
     377        1288 :     for (auto & [y, dy] : _retrieved_derivatives)
     378        1288 :       for (auto & [x, target] : dy)
     379             :       {
     380         644 :         const auto & source = _dout_din[y][x];
     381         644 :         if (source.defined())
     382        1932 :           target = source.to(torch::kCPU).batch_expand({neml2::Size(N)});
     383             :       }
     384             : 
     385             :     // clear derivatives
     386         644 :     _dout_din.clear();
     387             :   }
     388           0 :   catch (std::exception & e)
     389             :   {
     390           0 :     mooseError("An error occurred while retrieving outputs from the NEML2 model. Error message:\n",
     391           0 :                e.what(),
     392             :                NEML2Utils::NEML2_help_message);
     393           0 :   }
     394        1288 : }
     395             : 
     396             : void
     397        1121 : NEML2ModelExecutor::finalize()
     398             : {
     399        1121 :   if (!NEML2Utils::shouldCompute(_fe_problem))
     400         460 :     return;
     401             : 
     402             :   // See if any rank failed
     403             :   processor_id_type pid;
     404         661 :   _communicator.maxloc(_error, pid);
     405             : 
     406             :   // Fail the next nonlinear convergence check if any rank failed
     407         661 :   if (_error)
     408             :   {
     409           1 :     _communicator.broadcast(_error_message, pid);
     410           1 :     if (_communicator.rank() == 0)
     411             :     {
     412           2 :       std::string msg = "NEML2 model execution failed on at least one processor with ID " +
     413           3 :                         std::to_string(pid) + ". Error message:\n";
     414           1 :       msg += _error_message;
     415           1 :       if (_fe_problem.isTransient())
     416             :         msg += "\nTo recover, the solution will fail and then be re-attempted with a reduced time "
     417           1 :                "step.";
     418           1 :       _console << COLOR_YELLOW << msg << COLOR_DEFAULT << std::endl;
     419           1 :     }
     420           1 :     _fe_problem.setFailNextNonlinearConvergenceCheck();
     421             :   }
     422         660 :   else if (_t_step > 0)
     423         644 :     _output_ready = true;
     424             : }
     425             : 
     426             : void
     427         141 : NEML2ModelExecutor::checkExecutionStage() const
     428             : {
     429         141 :   if (_fe_problem.startedInitialSetup())
     430           0 :     mooseError("NEML2 output variables and derivatives must be retrieved during object "
     431             :                "construction. This is a code problem.");
     432         141 : }
     433             : 
     434             : const neml2::Tensor &
     435          90 : NEML2ModelExecutor::getOutput(const neml2::VariableName & output_name) const
     436             : {
     437          90 :   checkExecutionStage();
     438             : 
     439          90 :   if (!model().output_axis().has_variable(output_name))
     440           0 :     mooseError("Trying to retrieve a non-existent NEML2 output variable '", output_name, "'.");
     441             : 
     442          90 :   return _retrieved_outputs[output_name];
     443             : }
     444             : 
     445             : const neml2::Tensor &
     446          51 : NEML2ModelExecutor::getOutputDerivative(const neml2::VariableName & output_name,
     447             :                                         const neml2::VariableName & input_name) const
     448             : {
     449          51 :   checkExecutionStage();
     450             : 
     451          51 :   if (!model().output_axis().has_variable(output_name))
     452           0 :     mooseError("Trying to retrieve the derivative of NEML2 output variable '",
     453             :                output_name,
     454             :                "' with respect to NEML2 input variable '",
     455             :                input_name,
     456             :                "', but the NEML2 output variable does not exist.");
     457             : 
     458          51 :   if (!model().input_axis().has_variable(input_name))
     459           0 :     mooseError("Trying to retrieve the derivative of NEML2 output variable '",
     460             :                output_name,
     461             :                "' with respect to NEML2 input variable '",
     462             :                input_name,
     463             :                "', but the NEML2 input variable does not exist.");
     464             : 
     465          51 :   return _retrieved_derivatives[output_name][input_name];
     466             : }
     467             : 
     468             : const neml2::Tensor &
     469           0 : NEML2ModelExecutor::getOutputParameterDerivative(const neml2::VariableName & output_name,
     470             :                                                  const std::string & parameter_name) const
     471             : {
     472           0 :   checkExecutionStage();
     473             : 
     474           0 :   if (!model().output_axis().has_variable(output_name))
     475           0 :     mooseError("Trying to retrieve the derivative of NEML2 output variable '",
     476             :                output_name,
     477             :                "' with respect to NEML2 model parameter '",
     478             :                parameter_name,
     479             :                "', but the NEML2 output variable does not exist.");
     480             : 
     481           0 :   if (model().named_parameters().count(parameter_name) != 1)
     482           0 :     mooseError("Trying to retrieve the derivative of NEML2 output variable '",
     483             :                output_name,
     484             :                "' with respect to NEML2 model parameter '",
     485             :                parameter_name,
     486             :                "', but the NEML2 model parameter does not exist.");
     487             : 
     488           0 :   return _retrieved_parameter_derivatives[output_name][parameter_name];
     489             : }
     490             : 
     491             : #endif

Generated by: LCOV version 1.14