LCOV - code coverage report
Current view: top level - include/utils - NewtonInversion.h (source / functions) Hit Total Coverage
Test: idaholab/moose fluid_properties: #32971 (54bef8) with base c6cf66 Lines: 71 79 89.9 %
Date: 2026-05-29 20:36:28 Functions: 59 68 86.8 %
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             : // * This file is part of the MOOSE framework
      11             : // * https://mooseframework.inl.gov
      12             : // *
      13             : // * All rights reserved, see COPYRIGHT for full restrictions
      14             : // * https://github.com/idaholab/moose/blob/master/COPYRIGHT
      15             : // *
      16             : // * Licensed under LGPL 2.1, please see LICENSE for details
      17             : // * https://www.gnu.org/licenses/lgpl-2.1.html
      18             : 
      19             : #pragma once
      20             : 
      21             : #include "Moose.h"
      22             : #include "MooseUtils.h"
      23             : #include "libmesh/dense_matrix.h"
      24             : 
      25             : namespace FluidPropertiesUtils
      26             : {
      27             : /**
      28             :  * NewtonSolve does a 1D Newton Solve to solve the equation y = f(x, z) for variable z.
      29             :  * @param[in] x constant first argument of the f(x, z) term
      30             :  * @param[in] y constant which should be equal to f(x, z) with a converged z
      31             :  * @param[in] z_initial_guess initial guess for return variables
      32             :  * @param[in] tolerance criterion for relative or absolute (if y is sufficiently close to zero)
      33             :  * convergence checking
      34             :  * @param[in] y_from_x_z two-variable function returning both values and derivatives as references
      35             :  * @param[in] caller_name name of the fluid properties appended to name of the routine calling the
      36             :  * method
      37             :  * @param[in] max_its the maximum number of iterations for Newton's method
      38             :  * @param[in] verbose whether to output Newton iteration data
      39             :  * @return a pair in which the first member is the value z such that f(x, z) = y and the second
      40             :  * member is dy/dz
      41             :  */
      42             : template <typename T, typename Functor>
      43             : std::pair<T, T>
      44         379 : NewtonSolve(const T & x,
      45             :             const T & y,
      46             :             const Real z_initial_guess,
      47             :             const Real tolerance,
      48             :             const Functor & y_from_x_z,
      49             :             const std::string & caller_name,
      50             :             const unsigned int max_its = 100,
      51             :             const bool verbose = false)
      52             : {
      53             :   // R represents residual
      54             : 
      55             :   std::function<bool(const T &, const T &)> abs_tol_check =
      56             :       [tolerance](const T & R, const T & /*y*/)
      57           9 :   { return std::abs(MetaPhysicL::raw_value(R)) < tolerance; };
      58         235 :   std::function<bool(const T &, const T &)> rel_tol_check = [tolerance](const T & R, const T & y)
      59         905 :   { return std::abs(MetaPhysicL::raw_value(R / y)) < tolerance; };
      60         757 :   auto convergence_check = MooseUtils::absoluteFuzzyEqual(MetaPhysicL::raw_value(y), 0, tolerance)
      61             :                                ? abs_tol_check
      62             :                                : rel_tol_check;
      63             : 
      64          78 :   T z = z_initial_guess, R, new_y, dy_dx, dy_dz;
      65             :   unsigned int iteration = 0;
      66             : 
      67             :   using std::isnan;
      68         379 :   if (verbose)
      69             :     Moose::out << "Target value for 1D Newton inversion:\n" << y << std::endl;
      70             : 
      71             :   do
      72             :   {
      73         582 :     y_from_x_z(x, z, new_y, dy_dx, dy_dz);
      74         914 :     R = new_y - y;
      75             : 
      76             :     // We always want to perform at least one update in order to get derivatives on z correct (z
      77             :     // corresponding to the initial guess will have no derivative information), so we don't
      78             :     // immediately return if we are converged
      79         914 :     const bool converged = convergence_check(R, y);
      80             : 
      81             : #ifndef NDEBUG
      82             :     static constexpr Real perturbation_factor = 1 + 1e-8;
      83             :     T perturbed_y, dummy, dummy2;
      84             :     y_from_x_z(x, perturbation_factor * z, perturbed_y, dummy, dummy2);
      85             :     // Check the accuracy of the Jacobian
      86             :     auto J_differenced = (perturbed_y - new_y) / (1e-8 * z);
      87             :     if (!MooseUtils::relativeFuzzyEqual(J_differenced, dy_dz, 1e-2))
      88             :       mooseDoOnce(mooseWarning(caller_name + ": Bad Jacobian in NewtonSolve"));
      89             : #endif
      90             : 
      91         914 :     z += -(R / dy_dz);
      92             : 
      93         914 :     if (verbose)
      94             :     {
      95             :       Moose::out << "Iteration " << iteration << std::endl;
      96             :       Moose::out << "Current solution vector: " << z << std::endl;
      97           0 :       Moose::out << "Current (minus) residual: " << -R << std::endl;
      98             :       Moose::out << "Current Jacobian: " << dy_dz << std::endl;
      99             :     }
     100             : 
     101             :     // Check for NaNs
     102         914 :     if (isnan(z))
     103           0 :       mooseException(caller_name + ": NaN detected in Newton solve");
     104             : 
     105         914 :     if (converged)
     106             :       break;
     107         535 :   } while (++iteration < max_its);
     108             : 
     109             :   // Check for divergence or slow convergence of Newton's method
     110         379 :   if (iteration >= max_its)
     111           0 :     mooseException(caller_name +
     112             :                        ": Newton solve convergence failed: maximum number of iterations, ",
     113             :                    max_its,
     114             :                    ", exceeded");
     115             : 
     116         675 :   return {z, dy_dz};
     117             : }
     118             : 
     119             : /**
     120             :  * NewtonSolve2D does a 2D Newton Solve to solve for the x and y such that:
     121             :  * f = f_from_x_y(x, y) and g = g_from_x_y(x, y). This is done for example in the constant of (v, e)
     122             :  * to (p, T) variable set conversion.
     123             :  * @param[in] f target value for f_from_x_y
     124             :  * @param[in] g target value for g_from_x_y
     125             :  * @param[in] x0 initial guess for first output variable
     126             :  * @param[in] y0 initial guess for second output variable
     127             :  * @param[out] x_final output for first variable
     128             :  * @param[out] y_final output for second variable
     129             :  * @param[in] f_tol criterion for relative or absolute (if f is sufficently close to zero)
     130             :  * convergence checking
     131             :  * @param[in] g_tol criterion for relative or absolute (if g is sufficently close to zero)
     132             :  * convergence checking
     133             :  * @param[in] f_from_x_y two-variable function returning both values and derivatives as references
     134             :  * @param[in] g_from_x_y two-variable function returning both values and derivatives as references
     135             :  * @param[in] caller_name routine calling this solve
     136             :  * @param[in] max_its the maximum number of iterations for Newton's method
     137             :  * @param[in] debug whether to output the solution, residual and Jacobian on every iteration
     138             :  */
     139             : template <typename T, typename Functor1, typename Functor2>
     140             : void
     141      520314 : NewtonSolve2D(const T & f,
     142             :               const T & g,
     143             :               const Real x0,
     144             :               const Real y0,
     145             :               T & x_final,
     146             :               T & y_final,
     147             :               const Real f_tol,
     148             :               const Real g_tol,
     149             :               const Functor1 & f_from_x_y,
     150             :               const Functor2 & g_from_x_y,
     151             :               const std::string & caller_name = "",
     152             :               const unsigned int max_its = 100,
     153             :               bool debug = false)
     154             : {
     155             : 
     156             :   constexpr unsigned int system_size = 2;
     157      520314 :   DenseVector<T> targets = {{f, g}};
     158      639989 :   DenseVector<Real> tolerances = {{f_tol, g_tol}};
     159             :   // R represents a residual equal to y - y_in
     160    14137628 :   auto convergence_check = [&targets, &tolerances](const auto & minus_R)
     161             :   {
     162             :     using std::abs;
     163             : 
     164    14665250 :     for (const auto i : index_range(minus_R))
     165             :     {
     166    14264642 :       const auto error = abs(MooseUtils::absoluteFuzzyEqual(targets(i), 0, tolerances(i))
     167          32 :                                  ? minus_R(i)
     168    14264578 :                                  : minus_R(i) / targets(i));
     169    14264610 :       if (error >= tolerances(i))
     170             :         return false;
     171             :     }
     172             :     return true;
     173             :   };
     174             : 
     175      639989 :   DenseVector<T> u = {{x0, y0}};
     176      520314 :   DenseVector<T> minus_R(system_size), func_evals(system_size), u_update(system_size);
     177      520314 :   DenseMatrix<T> J(system_size, system_size);
     178             :   unsigned int iteration = 0;
     179             : #ifndef NDEBUG
     180             :   DenseVector<Real> svs(system_size), evs_real(system_size), evs_imag(system_size);
     181             :   DenseMatrix<Real> raw_J(system_size, system_size), raw_J2(system_size, system_size);
     182             : #endif
     183             : 
     184             :   typedef std::function<void(const T &, const T &, T &, T &, T &)> FuncType;
     185             :   std::array<FuncType, 2> func = {{f_from_x_y, g_from_x_y}};
     186             : 
     187          18 :   auto assign_solution = [&u, &x_final, &y_final]()
     188             :   {
     189      520314 :     x_final = u(0);
     190      520314 :     y_final = u(1);
     191             :   };
     192      759630 :   auto status_string = [&u, &func_evals, &targets, &minus_R](unsigned int comp) -> std::stringstream
     193             :   {
     194      239316 :     std::stringstream ss;
     195      478632 :     ss << "Current solution for component " << comp << ": " << u(comp)
     196      239316 :        << " (current ordinate: " << func_evals(comp) << " -> target: " << targets(comp)
     197      239316 :        << ", scaled residual: " << minus_R(comp) << ")";
     198      239316 :     return ss;
     199           0 :   };
     200      520314 :   if (debug)
     201             :     Moose::out << "Target values for 2D Newton inversion:\n" << targets << std::endl;
     202             : 
     203             :   using std::isnan, std::max, std::abs;
     204             : 
     205             :   do
     206             :   {
     207    40851942 :     for (const auto i : make_range(system_size))
     208    27234628 :       func[i](u(0), u(1), func_evals(i), J(i, 0), J(i, 1));
     209             : 
     210    40851942 :     for (const auto i : make_range(system_size))
     211    27234628 :       minus_R(i) = targets(i) - func_evals(i);
     212             : 
     213             :     // We always want to perform at least one update in order to get derivatives on z correct (z
     214             :     // corresponding to the initial guess will have no derivative information), so we don't
     215             :     // immediately return if we are converged
     216    13617314 :     const bool converged = convergence_check(minus_R);
     217             : 
     218             :     // Check for NaNs before proceeding to system solve. We may simultaneously not have NaNs in z
     219             :     // but have NaNs in the function evaluation
     220    40851909 :     for (const auto i : make_range(system_size))
     221    27234612 :       if (isnan(minus_R(i)))
     222             :       {
     223           0 :         assign_solution();
     224          34 :         mooseException(caller_name + ": NaN detected in Newton solve");
     225             :       }
     226             : 
     227    13617297 :     if (debug)
     228             :     {
     229             :       Moose::out << "Iteration " << iteration << std::endl;
     230             :       Moose::out << "Current solution vector:\n" << u << std::endl;
     231             :       Moose::out << "Current (minus) residual:\n" << minus_R << std::endl;
     232             :       Moose::out << "Current Jacobian:\n" << J << std::endl;
     233             :     }
     234             : 
     235             :     // Do some Jacobi (rowmax) preconditioning and check for an empty row
     236             :     int degenerate_row = -1;
     237    40851891 :     for (const auto i : make_range(system_size))
     238             :     {
     239    27234594 :       const auto rowmax = max(abs(J(i, 0)), abs(J(i, 1)));
     240    27234594 :       if (rowmax > 0)
     241             :       {
     242    81687576 :         for (const auto j : make_range(system_size))
     243    54458384 :           J(i, j) /= rowmax;
     244    27229192 :         minus_R(i) /= rowmax;
     245             :       }
     246             :       else
     247             :       {
     248        5402 :         if (degenerate_row != -1)
     249           0 :           mooseException(caller_name + ": Jacobian is all zeros in NewtonSolve2D");
     250        5402 :         degenerate_row = i;
     251             :       }
     252             :     }
     253             : 
     254             : #ifndef NDEBUG
     255             :     //
     256             :     // Check nature of linearized system
     257             :     //
     258             :     for (const auto i : make_range(system_size))
     259             :       for (const auto j : make_range(system_size))
     260             :       {
     261             :         raw_J(i, j) = MetaPhysicL::raw_value(J(i, j));
     262             :         raw_J2(i, j) = MetaPhysicL::raw_value(J(i, j));
     263             :       }
     264             :     raw_J.svd(svs);
     265             :     raw_J2.evd(evs_real, evs_imag);
     266             :     if (debug)
     267             :       Moose::out << "Jacobian singular values:\n" << svs << std::endl;
     268             : #endif
     269             : 
     270    13617297 :     if (degenerate_row == -1)
     271    13611895 :       J.lu_solve(minus_R, u_update);
     272             :     else
     273             :     {
     274             :       // use a 1D newton when the Jacobian has an empty row
     275        5402 :       const auto other_row = system_size - 1 - degenerate_row;
     276        5402 :       u_update(other_row) = minus_R(other_row) / J(other_row, other_row);
     277        5402 :       u_update(degenerate_row) = 0;
     278             :     }
     279             :     // reset the decomposition
     280          23 :     J.zero();
     281    13617297 :     u += u_update;
     282             : 
     283             :     // Check for NaNs
     284    40851891 :     for (const auto i : make_range(system_size))
     285    27234594 :       if (isnan(u(i)))
     286             :       {
     287           0 :         assign_solution();
     288           0 :         mooseException(caller_name + ": NaN detected in NewtonSolve2D\n" + status_string(0).str() +
     289             :                        "\n" + status_string(1).str());
     290             :       }
     291             : 
     292    13617297 :     if (converged)
     293             :       break;
     294    13216658 :   } while (++iteration < max_its);
     295             : 
     296           6 :   assign_solution();
     297             : 
     298             :   // Check for divergence or slow convergence of Newton's method
     299      520297 :   if (iteration >= max_its)
     300      717948 :     mooseException(caller_name +
     301             :                        ": Newton solve convergence failed: maximum number of iterations, ",
     302             :                    max_its,
     303             :                    ", exceeded.\n" + status_string(0).str() + "\n" + status_string(1).str());
     304      920959 : }
     305             : } // namespace FluidPropertiesUtils

Generated by: LCOV version 1.14