From 45389cc1124e3cbd037b1eadc744e6f27431c149 Mon Sep 17 00:00:00 2001 From: "Joshua E. Hansel" Date: Tue, 21 Jan 2025 15:10:02 -0600 Subject: [PATCH] Added ParsedConvergence Closes #28844 --- .../source/convergence/ParsedConvergence.md | 49 ++++ .../include/convergence/ParsedConvergence.h | 98 ++++++++ framework/src/convergence/ParsedConvergence.C | 227 ++++++++++++++++++ .../doc/content/newsletter/2025/2025_01.md | 7 + .../convergence/parsed_convergence/base.i | 74 ++++++ .../parsed_convergence/error_invalid_value.i | 7 + .../gold/test_converge_out.csv | 2 + .../parsed_convergence/test_converge.i | 9 + .../parsed_convergence/test_diverge_custom.i | 7 + .../parsed_convergence/test_diverge_default.i | 9 + .../convergence/parsed_convergence/tests | 29 +++ 11 files changed, 518 insertions(+) create mode 100644 framework/doc/content/source/convergence/ParsedConvergence.md create mode 100644 framework/include/convergence/ParsedConvergence.h create mode 100644 framework/src/convergence/ParsedConvergence.C create mode 100644 test/tests/convergence/parsed_convergence/base.i create mode 100644 test/tests/convergence/parsed_convergence/error_invalid_value.i create mode 100644 test/tests/convergence/parsed_convergence/gold/test_converge_out.csv create mode 100644 test/tests/convergence/parsed_convergence/test_converge.i create mode 100644 test/tests/convergence/parsed_convergence/test_diverge_custom.i create mode 100644 test/tests/convergence/parsed_convergence/test_diverge_default.i create mode 100644 test/tests/convergence/parsed_convergence/tests diff --git a/framework/doc/content/source/convergence/ParsedConvergence.md b/framework/doc/content/source/convergence/ParsedConvergence.md new file mode 100644 index 000000000000..dbce69fa5bc8 --- /dev/null +++ b/framework/doc/content/source/convergence/ParsedConvergence.md @@ -0,0 +1,49 @@ +# ParsedConvergence + +This [Convergence](Convergence/index.md) allows the user to specify arbitrary expressions +for convergence and divergence criteria. These expressions +([!param](/Convergence/ParsedConvergence/convergence_expression) and [!param](/Convergence/ParsedConvergence/divergence_expression)) +may contain any of the following: + +- `Convergence` objects +- [Functions](Functions/index.md) +- [Post-processors](Postprocessors/index.md) +- Constant values + +The expressions are parsed using the [Function Parser syntax](http://warp.povusers.org/FunctionParser/fparser.html#functionsyntax). +The full library of mathematical operators is valid in the parsed +expression, but for convenience, we list some of the logical and comparison operators here: + +| Syntax | Description | +| :- | :- | +| `()` | Parentheses for order of operations | +| `!A` | *NOT* `A` | +| `A & B` | `A` *AND* `B` | +| `A` I `B` | `A` *OR* `B` | +| `A = B` | `A` *EQUALS* `B` | +| `A != B` | `A` *DOES NOT EQUAL* `B` | +| `A >= B` | `A` *GREATER THAN OR EQUAL TO* `B` | + +The expressions must evaluate to either 1 or 0, which correspond to `true` or `false`, +respectively; if the expression returns another value, an error results. Note +the following rules for the `Convergence` object values: + +- For the convergence expression, `Convergence` objects evaluate to `true` if they + are `CONVERGED` and `false` otherwise (`ITERATING` or `DIVERGED`). +- For the divergence expression, `Convergence` objects evaluate to `true` if they + are `DIVERGED` and `false` otherwise (`ITERATING` or `CONVERGED`). + +The divergence expression is optional. If omitted, divergence occurs if any of +the supplied `Convergence` objects return `DIVERGED`, e.g., + +``` +divergence_expression = 'conv1 | conv2 | conv3' +``` + +if [!param](/Convergence/ParsedConvergence/symbol_values) contains `conv1`, `conv2`, and `conv3`. + +!syntax parameters /Convergence/ParsedConvergence + +!syntax inputs /Convergence/ParsedConvergence + +!syntax children /Convergence/ParsedConvergence diff --git a/framework/include/convergence/ParsedConvergence.h b/framework/include/convergence/ParsedConvergence.h new file mode 100644 index 000000000000..2ca275fa1d17 --- /dev/null +++ b/framework/include/convergence/ParsedConvergence.h @@ -0,0 +1,98 @@ +//* This file is part of the MOOSE framework +//* https://www.mooseframework.org +//* +//* All rights reserved, see COPYRIGHT for full restrictions +//* https://github.com/idaholab/moose/blob/master/COPYRIGHT +//* +//* Licensed under LGPL 2.1, please see LICENSE for details +//* https://www.gnu.org/licenses/lgpl-2.1.html + +#pragma once + +#include "Convergence.h" +#include "FunctionParserUtils.h" + +/** + * Evaluates convergence from a parsed expression. + */ +class ParsedConvergence : public Convergence, public FunctionParserUtils +{ +public: + static InputParameters validParams(); + + ParsedConvergence(const InputParameters & parameters); + + virtual MooseConvergenceStatus checkConvergence(unsigned int iter) override; + + virtual void initialSetup() override; + +protected: + usingFunctionParserUtilsMembers(false); + + /** + * Initializes symbols used in the parsed expression + */ + void initializeSymbols(); + void initializePostprocessorSymbol(unsigned int i); + void initializeFunctionSymbol(unsigned int i); + void initializeConvergenceSymbol(unsigned int i); + void initializeConstantSymbol(unsigned int i); + + /** + * Makes a parsed function + * + * @param[in] expression expression to parse + */ + SymFunctionPtr makeParsedFunction(const std::string & expression); + + /** + * Updates non-constant symbol values + * + * @param[in] iter Iteration index + */ + void updateSymbolValues(unsigned int iter); + void updatePostprocessorSymbolValues(); + void updateFunctionSymbolValues(); + void updateConvergenceSymbolValues(unsigned int iter); + + /** + * Converts a Real value to a bool. Error results if value is not 0 or 1. + * + * @param[in] value Real value to convert + * @param[in] param Name of the corresponding input parameter + */ + bool convertRealToBool(Real value, const std::string & param) const; + + FEProblemBase & _fe_problem; + + std::vector _symbol_names; + std::vector _symbol_values; + + /// Expression to parse for convergence + const std::string _convergence_expression; + /// Expression to parse for divergence + const std::string _divergence_expression; + + /// Parsed function for convergence + SymFunctionPtr _convergence_function; + /// Parsed function for divergence + SymFunctionPtr _divergence_function; + + /// Convergence function parameters + std::vector _convergence_function_params; + /// Divergence function parameters + std::vector _divergence_function_params; + + /// Post-processor values in the provided symbols + std::vector _pp_values; + std::vector _pp_indices; + + /// Functions in the provided symbols + std::vector _functions; + std::vector _function_indices; + + /// Convergences in the provided symbols + std::vector _convergences; + std::vector _convergence_symbol_names; + std::vector _convergence_indices; +}; diff --git a/framework/src/convergence/ParsedConvergence.C b/framework/src/convergence/ParsedConvergence.C new file mode 100644 index 000000000000..67e399576b2f --- /dev/null +++ b/framework/src/convergence/ParsedConvergence.C @@ -0,0 +1,227 @@ +//* This file is part of the MOOSE framework +//* https://www.mooseframework.org +//* +//* All rights reserved, see COPYRIGHT for full restrictions +//* https://github.com/idaholab/moose/blob/master/COPYRIGHT +//* +//* Licensed under LGPL 2.1, please see LICENSE for details +//* https://www.gnu.org/licenses/lgpl-2.1.html + +#include "ParsedConvergence.h" +#include "MooseUtils.h" +#include "Function.h" + +registerMooseObject("MooseApp", ParsedConvergence); + +InputParameters +ParsedConvergence::validParams() +{ + InputParameters params = Convergence::validParams(); + params += FunctionParserUtils::validParams(); + + params.addClassDescription("Evaluates convergence from a parsed expression."); + + params.addRequiredCustomTypeParam( + "convergence_expression", "FunctionExpression", "Expression to parse for convergence"); + params.addCustomTypeParam( + "divergence_expression", "FunctionExpression", "Expression to parse for divergence"); + params.addParam>( + "symbol_names", {}, "Symbol names to use in the parsed expressions"); + params.addParam>( + "symbol_values", + {}, + "Values (Convergence names, Postprocessor names, Function names, and constants) " + "corresponding to each entry in 'symbol_names'"); + + return params; +} + +ParsedConvergence::ParsedConvergence(const InputParameters & parameters) + : Convergence(parameters), + FunctionParserUtils(parameters), + _fe_problem(*getCheckedPointerParam("_fe_problem_base")), + _symbol_names(getParam>("symbol_names")), + _symbol_values(getParam>("symbol_values")), + _convergence_function_params(_symbol_names.size(), 0.0), + _divergence_function_params(_symbol_names.size(), 0.0) +{ + if (_symbol_names.size() != _symbol_values.size()) + mooseError("The parameters 'symbol_names' and 'symbol_values' must have the same size."); +} + +void +ParsedConvergence::initialSetup() +{ + Convergence::initialSetup(); + + initializeSymbols(); + + const auto convergence_expression = getParam("convergence_expression"); + _convergence_function = makeParsedFunction(convergence_expression); + + const auto divergence_expression = isParamValid("divergence_expression") + ? getParam("divergence_expression") + : MooseUtils::join(_convergence_symbol_names, "|"); + _divergence_function = makeParsedFunction(divergence_expression); +} + +void +ParsedConvergence::initializeSymbols() +{ + for (const auto i : index_range(_symbol_values)) + { + ReporterName reporter_name(_symbol_values[i], "value"); + if (_fe_problem.getReporterData().hasReporterValue(reporter_name)) + initializePostprocessorSymbol(i); + else if (_fe_problem.hasFunction(_symbol_values[i])) + initializeFunctionSymbol(i); + else if (_fe_problem.hasConvergence(_symbol_values[i])) + initializeConvergenceSymbol(i); + else + initializeConstantSymbol(i); + } +} + +void +ParsedConvergence::initializePostprocessorSymbol(unsigned int i) +{ + const PostprocessorValue & pp_value = _fe_problem.getPostprocessorValueByName(_symbol_values[i]); + _pp_values.push_back(&pp_value); + _pp_indices.push_back(i); +} + +void +ParsedConvergence::initializeFunctionSymbol(unsigned int i) +{ + Function & function = _fe_problem.getFunction(_symbol_values[i], _tid); + _functions.push_back(&function); + _function_indices.push_back(i); +} + +void +ParsedConvergence::initializeConvergenceSymbol(unsigned int i) +{ + Convergence & convergence = _fe_problem.getConvergence(_symbol_values[i], _tid); + _convergences.push_back(&convergence); + _convergence_symbol_names.push_back(_symbol_names[i]); + _convergence_indices.push_back(i); +} + +void +ParsedConvergence::initializeConstantSymbol(unsigned int i) +{ + try + { + const Real value = MooseUtils::convert(_symbol_values[i], true); + _convergence_function_params[i] = value; + _divergence_function_params[i] = value; + } + catch (const std::invalid_argument & e) + { + mooseError( + "The 'symbol_values' entry '", + _symbol_values[i], + "' is not a constant value or the name of a Convergence, Postprocessor, or Function.", + e.what()); + } +} + +ParsedConvergence::SymFunctionPtr +ParsedConvergence::makeParsedFunction(const std::string & expression) +{ + auto sym_function = std::make_shared(); + + setParserFeatureFlags(sym_function); + + // Add constants + sym_function->AddConstant("pi", std::acos(Real(-1))); + sym_function->AddConstant("e", std::exp(Real(1))); + + // Parse the expression + const auto symbols_str = Moose::stringify(_symbol_names); + if (sym_function->Parse(expression, symbols_str) >= 0) + mooseError("The expression\n '", + expression, + "'\nwith symbols\n '", + symbols_str, + "'\ncould not be parsed:\n", + sym_function->ErrorMsg()); + + // Optimize the parsed function + functionsOptimize(sym_function); + + return sym_function; +} + +Convergence::MooseConvergenceStatus +ParsedConvergence::checkConvergence(unsigned int iter) +{ + updateSymbolValues(iter); + + const Real converged_real = evaluate(_convergence_function, _convergence_function_params, name()); + const Real diverged_real = evaluate(_divergence_function, _divergence_function_params, name()); + + if (convertRealToBool(diverged_real, "divergence_expression")) + return MooseConvergenceStatus::DIVERGED; + else if (convertRealToBool(converged_real, "convergence_expression")) + return MooseConvergenceStatus::CONVERGED; + else + return MooseConvergenceStatus::ITERATING; +} + +void +ParsedConvergence::updateSymbolValues(unsigned int iter) +{ + updatePostprocessorSymbolValues(); + updateFunctionSymbolValues(); + updateConvergenceSymbolValues(iter); +} + +void +ParsedConvergence::updatePostprocessorSymbolValues() +{ + for (const auto i : index_range(_pp_indices)) + { + _convergence_function_params[_pp_indices[i]] = (*_pp_values[i]); + _divergence_function_params[_pp_indices[i]] = (*_pp_values[i]); + } +} + +void +ParsedConvergence::updateFunctionSymbolValues() +{ + for (const auto i : index_range(_function_indices)) + { + const Real function_value = _functions[i]->value(_t, Point(0, 0, 0)); + _convergence_function_params[_function_indices[i]] = function_value; + _divergence_function_params[_function_indices[i]] = function_value; + } +} + +void +ParsedConvergence::updateConvergenceSymbolValues(unsigned int iter) +{ + for (const auto i : index_range(_convergence_indices)) + { + const auto status = _convergences[i]->checkConvergence(iter); + _convergence_function_params[_convergence_indices[i]] = + status == MooseConvergenceStatus::CONVERGED; + _divergence_function_params[_convergence_indices[i]] = + status == MooseConvergenceStatus::DIVERGED; + } +} + +bool +ParsedConvergence::convertRealToBool(Real value, const std::string & param) const +{ + if (MooseUtils::absoluteFuzzyEqual(value, 1.0)) + return true; + else if (MooseUtils::absoluteFuzzyEqual(value, 0.0)) + return false; + else + mooseError("The expression parameter '", + param, + "' evaluated to the value ", + value, + ", but it must only evaluate to either 0 or 1."); +} diff --git a/modules/doc/content/newsletter/2025/2025_01.md b/modules/doc/content/newsletter/2025/2025_01.md index 64708830b14f..693b5a2b1a44 100644 --- a/modules/doc/content/newsletter/2025/2025_01.md +++ b/modules/doc/content/newsletter/2025/2025_01.md @@ -7,6 +7,13 @@ for a complete description of all MOOSE changes. ## MOOSE Improvements +### Added ParsedConvergence + +[/ParsedConvergence.md] was added, which allows the user to specify arbitrary +convergence and divergence criteria. The parsed expression may include other +[Convergence](Convergence/index.md) objects, [Functions](Functions/index.md), +[Post-processors](Postprocessors/index.md), and constant values. + ## MOOSE Modules Changes ## libMesh-level Changes diff --git a/test/tests/convergence/parsed_convergence/base.i b/test/tests/convergence/parsed_convergence/base.i new file mode 100644 index 000000000000..75a0ad1c2419 --- /dev/null +++ b/test/tests/convergence/parsed_convergence/base.i @@ -0,0 +1,74 @@ +[Mesh] + type = GeneratedMesh + dim = 2 + nx = 10 + ny = 10 +[] + +[Variables] + [u] + [] +[] + +[Kernels] + [diff] + type = Diffusion + variable = u + [] +[] + +[BCs] + [left] + type = DirichletBC + variable = u + boundary = left + value = 0 + [] + [right] + type = DirichletBC + variable = u + boundary = right + value = 1 + [] +[] + +[Functions] + [test_fn] + type = ConstantFunction + value = 0 + [] +[] + +[Postprocessors] + [test_pp] + type = ConstantPostprocessor + value = -1e-6 + [] + [num_nl_iterations] + type = NumNonlinearIterations + execute_on = 'TIMESTEP_END' + [] +[] + +[Convergence] + [parsed_conv] + type = ParsedConvergence + convergence_expression = 'fn | (supplied & abs(pp) < tol)' + symbol_names = 'supplied pp tol fn' + symbol_values = 'supplied_conv test_pp 1e-5 test_fn' + [] +[] + +[Executioner] + type = Steady + solve_type = 'PJFNK' + petsc_options_iname = '-pc_type' + petsc_options_value = 'hypre' + nonlinear_convergence = parsed_conv +[] + +[Outputs] + csv = true + show = 'num_nl_iterations' + execute_on = 'FINAL' +[] diff --git a/test/tests/convergence/parsed_convergence/error_invalid_value.i b/test/tests/convergence/parsed_convergence/error_invalid_value.i new file mode 100644 index 000000000000..19e5f349499d --- /dev/null +++ b/test/tests/convergence/parsed_convergence/error_invalid_value.i @@ -0,0 +1,7 @@ +!include test_converge.i + +[Convergence] + [parsed_conv] + divergence_expression = 'pp' + [] +[] diff --git a/test/tests/convergence/parsed_convergence/gold/test_converge_out.csv b/test/tests/convergence/parsed_convergence/gold/test_converge_out.csv new file mode 100644 index 000000000000..ceb8a6bc92ba --- /dev/null +++ b/test/tests/convergence/parsed_convergence/gold/test_converge_out.csv @@ -0,0 +1,2 @@ +time,num_nl_iterations +2,2 diff --git a/test/tests/convergence/parsed_convergence/test_converge.i b/test/tests/convergence/parsed_convergence/test_converge.i new file mode 100644 index 000000000000..b3438bcac6b6 --- /dev/null +++ b/test/tests/convergence/parsed_convergence/test_converge.i @@ -0,0 +1,9 @@ +!include base.i + +[Convergence] + [supplied_conv] + type = SuppliedStatusConvergence + convergence_statuses = '0 0 1 1 1 1 1' + max_iterations = 5 + [] +[] diff --git a/test/tests/convergence/parsed_convergence/test_diverge_custom.i b/test/tests/convergence/parsed_convergence/test_diverge_custom.i new file mode 100644 index 000000000000..cb0997d48e1e --- /dev/null +++ b/test/tests/convergence/parsed_convergence/test_diverge_custom.i @@ -0,0 +1,7 @@ +!include test_converge.i + +[Convergence] + [parsed_conv] + divergence_expression = 'pp < 0' + [] +[] diff --git a/test/tests/convergence/parsed_convergence/test_diverge_default.i b/test/tests/convergence/parsed_convergence/test_diverge_default.i new file mode 100644 index 000000000000..03cd4649c58f --- /dev/null +++ b/test/tests/convergence/parsed_convergence/test_diverge_default.i @@ -0,0 +1,9 @@ +!include base.i + +[Convergence] + [supplied_conv] + type = SuppliedStatusConvergence + convergence_statuses = '0 0 0 0 0 0 0' + max_iterations = 5 + [] +[] diff --git a/test/tests/convergence/parsed_convergence/tests b/test/tests/convergence/parsed_convergence/tests new file mode 100644 index 000000000000..a559964a51d1 --- /dev/null +++ b/test/tests/convergence/parsed_convergence/tests @@ -0,0 +1,29 @@ +[Tests] + design = 'ParsedConvergence.md' + issues = '#28844' + + [test_converge] + type = CSVDiff + input = 'test_converge.i' + csvdiff = 'test_converge_out.csv' + requirement = 'The system shall be allow arbitrary convergence criteria based on Convergence objects, functions, post-processors, and constants.' + [] + [test_diverge_default] + type = RunApp + input = 'test_diverge_default.i' + expect_out = "did not converge" + requirement = 'ParsedConvergence objects shall have a default divergence criteria based on the provided Convergence objects.' + [] + [test_diverge_custom] + type = RunApp + input = 'test_diverge_custom.i' + expect_out = "did not converge" + requirement = "ParsedConvergence objects shall allow custom divergence criteria to be specified." + [] + [error_invalid_value] + type = RunException + input = 'error_invalid_value.i' + expect_err = "it must only evaluate to either 0 or 1" + requirement = "ParsedConvergence objects shall report an error if the parsed expression." + [] +[]