Source code for VeraGridEngine.Simulations.PowerFlow.NumericalMethods.levenberg_marquadt_fx

# This Source Code Form is subject to the terms of the Mozilla Public
# License, v. 2.0. If a copy of the MPL was not distributed with this
# file, You can obtain one at https://mozilla.org/MPL/2.0/.  
# SPDX-License-Identifier: MPL-2.0
import time
import numpy as np
import scipy.sparse as sp
from VeraGridEngine.Utils.NumericalMethods.sparse_solve import get_linear_solver
from VeraGridEngine.Simulations.PowerFlow.power_flow_results import NumericPowerFlowResults
from VeraGridEngine.Simulations.PowerFlow.Formulations.pf_formulation_template import PfFormulationTemplate
from VeraGridEngine.Utils.Sparse.csc2 import mat_to_scipy
from VeraGridEngine.basic_structures import Logger

linear_solver = get_linear_solver()


[docs] def levenberg_marquardt_fx(problem: PfFormulationTemplate, tol: float = 1e-6, max_iter: int = 10, verbose: int = 0, logger: Logger = Logger()) -> NumericPowerFlowResults: """ Levenberg-Marquardt to solve: min: error(f(x)) s.t. f(x) = 0 From METHODS FOR NON-LINEAR LEAST SQUARES PROBLEMS by K. Madsen, H.B. Nielsen, O. Tingleff :param problem: PfFormulationTemplate :param tol: Error tolerance :param max_iter: Maximum number of iterations :param verbose: Display console information :param logger: Logger instance :return: ConvexMethodResult """ start = time.time() # get the initial point x = problem.var2x() if len(x) == 0: # if the length of x is zero, means that there's nothing to solve # for instance there might be a single node that is a slack node return problem.get_solution(elapsed=time.time() - start, iterations=0) iter_ = 0 # initialize the problem state error, converged, x, f = problem.update(x, update_controls=False) # save the error evolution error_evolution = np.zeros(max_iter + 1) error_evolution[iter_] = problem.error if verbose > 0: print(f'It {iter_}, error {problem.error}, converged {problem.converged}, x {x}, dx not computed yet') if converged: return problem.get_solution(elapsed=time.time() - start, iterations=iter_) else: nu = 2.0 obj_val_prev = 1e9 # very large number J = mat_to_scipy(problem.Jacobian()) if J.shape[0] != J.shape[1]: logger.add_error("Jacobian not square, check the controls!", "Levenberg-Marquadt") return problem.get_solution(elapsed=time.time() - start, iterations=iter_) elif J.shape[0] != len(f): logger.add_error("Jacobian and residuals have different sizes!", "Levenberg-Marquadt", value=len(f), expected_value=J.shape[0]) return problem.get_solution(elapsed=time.time() - start, iterations=iter_) # system matrix # H1 = H^t Jt = J.T # H2 = H1Β·H A = Jt @ J # set first value of mu mu = 1e-3 * A.diagonal().max() # compute system matrix A = H^TΒ·H - lambdaΒ·I Idn = sp.diags(np.full(J.shape[0], mu)) sys_mat = (A + Idn).tocsc() # right-hand side # H^tΒ·dz g = Jt @ f # objective function to minimize obj_val = 0.5 * f @ f while not converged and iter_ < max_iter: # update iteration counter iter_ += 1 if verbose > 0: print('-' * 200) print(f'Iter: {iter_}') print('-' * 200) try: # Solve the increment dx = linear_solver(sys_mat, g) except RuntimeError: logger.add_error(f"Levenberg-Marquardt's system matrix is singular @iter {iter_}:") return problem.get_solution(elapsed=time.time() - start, iterations=iter_) if verbose > 1: print("H:\n", problem.get_jacobian_df(J)) print("F:\n", problem.get_f_df(g)) print("dx:\n", problem.get_x_df(dx)) # decision function dF = obj_val_prev - obj_val dL = 0.5 * dx @ (mu * dx + g) if (dF != 0.0) and (dL > 0.0): rho = dF / dL mu *= max([1.0 / 3.0, 1.0 - (2 * rho - 1) ** 3.0]) nu = 2.0 # update update_controls = error < problem.options.controls_start_tolerance error, converged, x, f = problem.update(x - dx, update_controls=update_controls) # record the previous objective function value obj_val_prev = obj_val # objective function to minimize obj_val = 0.5 * f @ f # update Jacobian and system matrix J = mat_to_scipy(problem.Jacobian()) if J.shape[0] != J.shape[1]: logger.add_error("Jacobian not square, check the controls!", "Levenberg-Marquadt") return problem.get_solution(elapsed=time.time() - start, iterations=iter_) elif J.shape[0] != len(f): logger.add_error("Jacobian and residuals have different sizes!", "Levenberg-Marquadt", value=len(f), expected_value=J.shape[0]) return problem.get_solution(elapsed=time.time() - start, iterations=iter_) # system matrix # H1 = H^t Jt = J.T # H2 = H1Β·H A = Jt @ J # compute system matrix sys_mat = A + muΒ·I Idn = sp.diags(np.full(J.shape[0], mu)) sys_mat = (A + Idn).tocsc() # update right-hand side g = Jt @ f else: mu *= nu nu *= 2.0 # save the error evolution error_evolution[iter_] = error if verbose > 0: if verbose == 1: print(f'It {iter_}, error {error}, converged {converged}, x {x}, dx {dx}') else: print(f'error {error}, converged {converged}, x {x}, dx {dx}') return problem.get_solution(elapsed=time.time() - start, iterations=iter_)