| 1234567891011121314151617181920212223242526272829303132333435363738394041424344454647484950515253545556575859606162636465666768697071727374757677787980818283848586878889909192939495969798991001011021031041051061071081091101111121131141151161171181191201211221231241251261271281291301311321331341351361371381391401411421431441451461471481491501511521531541551561571581591601611621631641651661671681691701711721731741751761771781791801811821831841851861871881891901911921931941951961971981992002012022032042052062072082092102112122132142152162172182192202212222232242252262272282292302312322332342352362372382392402412422432442452462472482492502512522532542552562572582592602612622632642652662672682692702712722732742752762772782792802812822832842852862872882892902912922932942952962972982993003013023033043053063073083093103113123133143153163173183193203213223233243253263273283293303313323333343353363373383393403413423433443453463473483493503513523533543553563573583593603613623633643653663673683693703713723733743753763773783793803813823833843853863873883893903913923933943953963973983994004014024034044054064074084094104114124134144154164174184194204214224234244254264274284294304314324334344354364374384394404414424434444454464474484494504514524534544554564574584594604614624634644654664674684694704714724734744754764774784794804814824834844854864874884894904914924934944954964974984995005015025035045055065075085095105115125135145155165175185195205215225235245255265275285295305315325335345355365375385395405415425435445455465475485495505515525535545555565575585595605615625635645655665675685695705715725735745755765775785795805815825835845855865875885895905915925935945955965975985996006016026036046056066076086096106116126136146156166176186196206216226236246256266276286296306316326336346356366376386396406416426436446456466476486496506516526536546556566576586596606616626636646656666676686696706716726736746756766776786796806816826836846856866876886896906916926936946956966976986997007017027037047057067077087097107117127137147157167177187197207217227237247257267277287297307317327337347357367377387397407417427437447457467477487497507517527537547557567577587597607617627637647657667677687697707717727737747757767777787797807817827837847857867877887897907917927937947957967977987998008018028038048058068078088098108118128138148158168178188198208218228238248258268278288298308318328338348358368378388398408418428438448458468478488498508518528538548558568578588598608618628638648658668678688698708718728738748758768778788798808818828838848858868878888898908918928938948958968978988999009019029039049059069079089099109119129139149159169179189199209219229239249259269279289299309319329339349359369379389399409419429439449459469479489499509519529539549559569579589599609619629639649659669679689699709719729739749759769779789799809819829839849859869879889899909919929939949959969979989991000100110021003100410051006100710081009101010111012101310141015101610171018101910201021102210231024102510261027102810291030103110321033103410351036103710381039104010411042104310441045104610471048104910501051105210531054105510561057105810591060106110621063106410651066106710681069107010711072107310741075107610771078107910801081108210831084108510861087108810891090109110921093109410951096109710981099110011011102110311041105110611071108110911101111111211131114111511161117111811191120112111221123112411251126112711281129113011311132113311341135113611371138113911401141114211431144114511461147114811491150115111521153115411551156115711581159116011611162116311641165116611671168116911701171117211731174117511761177117811791180118111821183118411851186118711881189119011911192119311941195119611971198119912001201120212031204120512061207120812091210121112121213121412151216121712181219122012211222122312241225122612271228122912301231123212331234123512361237123812391240124112421243124412451246124712481249125012511252125312541255125612571258125912601261126212631264126512661267126812691270127112721273127412751276127712781279128012811282128312841285128612871288128912901291129212931294129512961297129812991300130113021303130413051306130713081309131013111312131313141315131613171318131913201321132213231324132513261327132813291330133113321333133413351336133713381339134013411342134313441345134613471348134913501351135213531354135513561357135813591360136113621363136413651366136713681369137013711372137313741375137613771378137913801381138213831384138513861387138813891390139113921393139413951396139713981399140014011402140314041405140614071408140914101411141214131414141514161417141814191420142114221423142414251426142714281429143014311432143314341435143614371438143914401441144214431444144514461447144814491450145114521453145414551456145714581459146014611462146314641465146614671468146914701471147214731474147514761477147814791480148114821483148414851486148714881489149014911492149314941495149614971498149915001501150215031504150515061507150815091510151115121513151415151516151715181519152015211522152315241525152615271528152915301531153215331534153515361537153815391540154115421543154415451546154715481549155015511552155315541555155615571558155915601561156215631564156515661567156815691570157115721573157415751576157715781579158015811582158315841585158615871588158915901591159215931594159515961597159815991600160116021603160416051606160716081609161016111612161316141615161616171618161916201621162216231624162516261627162816291630163116321633163416351636163716381639164016411642164316441645164616471648 |
- # Copyright (C) 2009, Pauli Virtanen <pav@iki.fi>
- # Distributed under the same license as SciPy.
- import inspect
- import sys
- import warnings
- import numpy as np
- from numpy import asarray, dot, vdot
- from scipy.linalg import norm, solve, inv, qr, svd, LinAlgError
- import scipy.sparse.linalg
- import scipy.sparse
- from scipy.linalg import get_blas_funcs
- from scipy._lib._util import copy_if_needed, _dedent_for_py313
- from scipy._lib._util import getfullargspec_no_self as _getfullargspec
- from ._linesearch import scalar_search_wolfe1, scalar_search_armijo
- from inspect import signature
- from difflib import get_close_matches
- from types import GenericAlias
- __all__ = [
- 'broyden1', 'broyden2', 'anderson', 'linearmixing',
- 'diagbroyden', 'excitingmixing', 'newton_krylov',
- 'BroydenFirst', 'KrylovJacobian', 'InverseJacobian', 'NoConvergence']
- #------------------------------------------------------------------------------
- # Utility functions
- #------------------------------------------------------------------------------
- class NoConvergence(Exception):
- """Exception raised when nonlinear solver fails to converge within the specified
- `maxiter`."""
- pass
- def maxnorm(x):
- return np.absolute(x).max()
- def _as_inexact(x):
- """Return `x` as an array, of either floats or complex floats"""
- x = asarray(x)
- if not np.issubdtype(x.dtype, np.inexact):
- return asarray(x, dtype=np.float64)
- return x
- def _array_like(x, x0):
- """Return ndarray `x` as same array subclass and shape as `x0`"""
- x = np.reshape(x, np.shape(x0))
- wrap = getattr(x0, '__array_wrap__', x.__array_wrap__)
- return wrap(x)
- def _safe_norm(v):
- if not np.isfinite(v).all():
- return np.array(np.inf)
- return norm(v)
- #------------------------------------------------------------------------------
- # Generic nonlinear solver machinery
- #------------------------------------------------------------------------------
- _doc_parts = dict(
- params_basic=_dedent_for_py313("""
- F : function(x) -> f
- Function whose root to find; should take and return an array-like
- object.
- xin : array_like
- Initial guess for the solution
- """).strip(),
- params_extra=_dedent_for_py313("""
- iter : int, optional
- Number of iterations to make. If omitted (default), make as many
- as required to meet tolerances.
- verbose : bool, optional
- Print status to stdout on every iteration.
- maxiter : int, optional
- Maximum number of iterations to make. If more are needed to
- meet convergence, `NoConvergence` is raised.
- f_tol : float, optional
- Absolute tolerance (in max-norm) for the residual.
- If omitted, default is 6e-6.
- f_rtol : float, optional
- Relative tolerance for the residual. If omitted, not used.
- x_tol : float, optional
- Absolute minimum step size, as determined from the Jacobian
- approximation. If the step size is smaller than this, optimization
- is terminated as successful. If omitted, not used.
- x_rtol : float, optional
- Relative minimum step size. If omitted, not used.
- tol_norm : function(vector) -> scalar, optional
- Norm to use in convergence check. Default is the maximum norm.
- line_search : {None, 'armijo' (default), 'wolfe'}, optional
- Which type of a line search to use to determine the step size in the
- direction given by the Jacobian approximation. Defaults to 'armijo'.
- callback : function, optional
- Optional callback function. It is called on every iteration as
- ``callback(x, f)`` where `x` is the current solution and `f`
- the corresponding residual.
- Returns
- -------
- sol : ndarray
- An array (of similar array type as `x0`) containing the final solution.
- Raises
- ------
- NoConvergence
- When a solution was not found.
- """).strip()
- )
- def _set_doc(obj):
- if obj.__doc__:
- obj.__doc__ = obj.__doc__ % _doc_parts
- def nonlin_solve(F, x0, jacobian='krylov', iter=None, verbose=False,
- maxiter=None, f_tol=None, f_rtol=None, x_tol=None, x_rtol=None,
- tol_norm=None, line_search='armijo', callback=None,
- full_output=False, raise_exception=True):
- """
- Find a root of a function, in a way suitable for large-scale problems.
- Parameters
- ----------
- %(params_basic)s
- jacobian : Jacobian
- A Jacobian approximation: `Jacobian` object or something that
- `asjacobian` can transform to one. Alternatively, a string specifying
- which of the builtin Jacobian approximations to use:
- krylov, broyden1, broyden2, anderson
- diagbroyden, linearmixing, excitingmixing
- %(params_extra)s
- full_output : bool
- If true, returns a dictionary `info` containing convergence
- information.
- raise_exception : bool
- If True, a `NoConvergence` exception is raise if no solution is found.
- See Also
- --------
- asjacobian, Jacobian
- Notes
- -----
- This algorithm implements the inexact Newton method, with
- backtracking or full line searches. Several Jacobian
- approximations are available, including Krylov and Quasi-Newton
- methods.
- References
- ----------
- .. [KIM] C. T. Kelley, \"Iterative Methods for Linear and Nonlinear
- Equations\". Society for Industrial and Applied Mathematics. (1995)
- https://archive.siam.org/books/kelley/fr16/
- """
- # Can't use default parameters because it's being explicitly passed as None
- # from the calling function, so we need to set it here.
- tol_norm = maxnorm if tol_norm is None else tol_norm
- condition = TerminationCondition(f_tol=f_tol, f_rtol=f_rtol,
- x_tol=x_tol, x_rtol=x_rtol,
- iter=iter, norm=tol_norm)
- x0 = _as_inexact(x0)
- def func(z):
- return _as_inexact(F(_array_like(z, x0))).flatten()
- x = x0.flatten()
- dx = np.full_like(x, np.inf)
- Fx = func(x)
- Fx_norm = norm(Fx)
- jacobian = asjacobian(jacobian)
- jacobian.setup(x.copy(), Fx, func)
- if maxiter is None:
- if iter is not None:
- maxiter = iter + 1
- else:
- maxiter = 100*(x.size+1)
- if line_search is True:
- line_search = 'armijo'
- elif line_search is False:
- line_search = None
- if line_search not in (None, 'armijo', 'wolfe'):
- raise ValueError("Invalid line search")
- # Solver tolerance selection
- gamma = 0.9
- eta_max = 0.9999
- eta_treshold = 0.1
- eta = 1e-3
- for n in range(maxiter):
- status = condition.check(Fx, x, dx)
- if status:
- break
- # The tolerance, as computed for scipy.sparse.linalg.* routines
- tol = min(eta, eta*Fx_norm)
- dx = -jacobian.solve(Fx, tol=tol)
- if norm(dx) == 0:
- raise ValueError("Jacobian inversion yielded zero vector. "
- "This indicates a bug in the Jacobian "
- "approximation.")
- # Line search, or Newton step
- if line_search:
- s, x, Fx, Fx_norm_new = _nonlin_line_search(func, x, Fx, dx,
- line_search)
- else:
- s = 1.0
- x = x + dx
- Fx = func(x)
- Fx_norm_new = norm(Fx)
- jacobian.update(x.copy(), Fx)
- if callback:
- callback(x, Fx)
- # Adjust forcing parameters for inexact methods
- eta_A = gamma * Fx_norm_new**2 / Fx_norm**2
- if gamma * eta**2 < eta_treshold:
- eta = min(eta_max, eta_A)
- else:
- eta = min(eta_max, max(eta_A, gamma*eta**2))
- Fx_norm = Fx_norm_new
- # Print status
- if verbose:
- sys.stdout.write(f"{n}: |F(x)| = {tol_norm(Fx):g}; step {s:g}\n")
- sys.stdout.flush()
- else:
- if raise_exception:
- raise NoConvergence(_array_like(x, x0))
- else:
- status = 2
- if full_output:
- info = {'nit': condition.iteration,
- 'fun': Fx,
- 'status': status,
- 'success': status == 1,
- 'message': {1: 'A solution was found at the specified '
- 'tolerance.',
- 2: 'The maximum number of iterations allowed '
- 'has been reached.'
- }[status]
- }
- return _array_like(x, x0), info
- else:
- return _array_like(x, x0)
- _set_doc(nonlin_solve)
- def _nonlin_line_search(func, x, Fx, dx, search_type='armijo', rdiff=1e-8,
- smin=1e-2):
- tmp_s = [0]
- tmp_Fx = [Fx]
- tmp_phi = [norm(Fx)**2]
- s_norm = norm(x) / norm(dx)
- def phi(s, store=True):
- if s == tmp_s[0]:
- return tmp_phi[0]
- xt = x + s*dx
- v = func(xt)
- p = _safe_norm(v)**2
- if store:
- tmp_s[0] = s
- tmp_phi[0] = p
- tmp_Fx[0] = v
- return p
- def derphi(s):
- ds = (abs(s) + s_norm + 1) * rdiff
- return (phi(s+ds, store=False) - phi(s)) / ds
- if search_type == 'wolfe':
- s, phi1, phi0 = scalar_search_wolfe1(phi, derphi, tmp_phi[0],
- xtol=1e-2, amin=smin)
- elif search_type == 'armijo':
- s, phi1 = scalar_search_armijo(phi, tmp_phi[0], -tmp_phi[0],
- amin=smin)
- if s is None:
- # XXX: No suitable step length found. Take the full Newton step,
- # and hope for the best.
- s = 1.0
- x = x + s*dx
- if s == tmp_s[0]:
- Fx = tmp_Fx[0]
- else:
- Fx = func(x)
- Fx_norm = norm(Fx)
- return s, x, Fx, Fx_norm
- class TerminationCondition:
- """
- Termination condition for an iteration. It is terminated if
- - |F| < f_rtol*|F_0|, AND
- - |F| < f_tol
- AND
- - |dx| < x_rtol*|x|, AND
- - |dx| < x_tol
- """
- def __init__(self, f_tol=None, f_rtol=None, x_tol=None, x_rtol=None,
- iter=None, norm=maxnorm):
- if f_tol is None:
- f_tol = np.finfo(np.float64).eps ** (1./3)
- if f_rtol is None:
- f_rtol = np.inf
- if x_tol is None:
- x_tol = np.inf
- if x_rtol is None:
- x_rtol = np.inf
- self.x_tol = x_tol
- self.x_rtol = x_rtol
- self.f_tol = f_tol
- self.f_rtol = f_rtol
- self.norm = norm
- self.iter = iter
- self.f0_norm = None
- self.iteration = 0
- def check(self, f, x, dx):
- self.iteration += 1
- f_norm = self.norm(f)
- x_norm = self.norm(x)
- dx_norm = self.norm(dx)
- if self.f0_norm is None:
- self.f0_norm = f_norm
- if f_norm == 0:
- return 1
- if self.iter is not None:
- # backwards compatibility with SciPy 0.6.0
- return 2 * (self.iteration > self.iter)
- # NB: condition must succeed for rtol=inf even if norm == 0
- return int((f_norm <= self.f_tol
- and f_norm/self.f_rtol <= self.f0_norm)
- and (dx_norm <= self.x_tol
- and dx_norm/self.x_rtol <= x_norm))
- #------------------------------------------------------------------------------
- # Generic Jacobian approximation
- #------------------------------------------------------------------------------
- class Jacobian:
- """
- Common interface for Jacobians or Jacobian approximations.
- The optional methods come useful when implementing trust region
- etc., algorithms that often require evaluating transposes of the
- Jacobian.
- Methods
- -------
- solve
- Returns J^-1 * v
- update
- Updates Jacobian to point `x` (where the function has residual `Fx`)
- matvec : optional
- Returns J * v
- rmatvec : optional
- Returns A^H * v
- rsolve : optional
- Returns A^-H * v
- matmat : optional
- Returns A * V, where V is a dense matrix with dimensions (N,K).
- todense : optional
- Form the dense Jacobian matrix. Necessary for dense trust region
- algorithms, and useful for testing.
- Attributes
- ----------
- shape
- Matrix dimensions (M, N)
- dtype
- Data type of the matrix.
- func : callable, optional
- Function the Jacobian corresponds to
- """
- # generic type compatibility with scipy-stubs
- __class_getitem__ = classmethod(GenericAlias)
- def __init__(self, **kw):
- names = ["solve", "update", "matvec", "rmatvec", "rsolve",
- "matmat", "todense", "shape", "dtype"]
- for name, value in kw.items():
- if name not in names:
- raise ValueError(f"Unknown keyword argument {name}")
- if value is not None:
- setattr(self, name, kw[name])
- if hasattr(self, "todense"):
- def __array__(self, dtype=None, copy=None):
- if dtype is not None:
- raise ValueError(f"`dtype` must be None, was {dtype}")
- return self.todense()
- def aspreconditioner(self):
- return InverseJacobian(self)
- def solve(self, v, tol=0):
- raise NotImplementedError
- def update(self, x, F):
- pass
- def setup(self, x, F, func):
- self.func = func
- self.shape = (F.size, x.size)
- self.dtype = F.dtype
- if self.__class__.setup is Jacobian.setup:
- # Call on the first point unless overridden
- self.update(x, F)
- class InverseJacobian:
- """
- A simple wrapper that inverts the Jacobian using the `solve` method.
- .. legacy:: class
- See the newer, more consistent interfaces in :mod:`scipy.optimize`.
- Parameters
- ----------
- jacobian : Jacobian
- The Jacobian to invert.
- Attributes
- ----------
- shape
- Matrix dimensions (M, N)
- dtype
- Data type of the matrix.
- """
- # generic type compatibility with scipy-stubs
- __class_getitem__ = classmethod(GenericAlias)
- def __init__(self, jacobian):
- self.jacobian = jacobian
- self.matvec = jacobian.solve
- self.update = jacobian.update
- if hasattr(jacobian, 'setup'):
- self.setup = jacobian.setup
- if hasattr(jacobian, 'rsolve'):
- self.rmatvec = jacobian.rsolve
- @property
- def shape(self):
- return self.jacobian.shape
- @property
- def dtype(self):
- return self.jacobian.dtype
- def asjacobian(J):
- """
- Convert given object to one suitable for use as a Jacobian.
- """
- spsolve = scipy.sparse.linalg.spsolve
- if isinstance(J, Jacobian):
- return J
- elif inspect.isclass(J) and issubclass(J, Jacobian):
- return J()
- elif isinstance(J, np.ndarray):
- if J.ndim > 2:
- raise ValueError('array must have rank <= 2')
- J = np.atleast_2d(np.asarray(J))
- if J.shape[0] != J.shape[1]:
- raise ValueError('array must be square')
- return Jacobian(matvec=lambda v: dot(J, v),
- rmatvec=lambda v: dot(J.conj().T, v),
- solve=lambda v, tol=0: solve(J, v),
- rsolve=lambda v, tol=0: solve(J.conj().T, v),
- dtype=J.dtype, shape=J.shape)
- elif scipy.sparse.issparse(J):
- if J.shape[0] != J.shape[1]:
- raise ValueError('matrix must be square')
- return Jacobian(matvec=lambda v: J @ v,
- rmatvec=lambda v: J.conj().T @ v,
- solve=lambda v, tol=0: spsolve(J, v),
- rsolve=lambda v, tol=0: spsolve(J.conj().T, v),
- dtype=J.dtype, shape=J.shape)
- elif hasattr(J, 'shape') and hasattr(J, 'dtype') and hasattr(J, 'solve'):
- return Jacobian(matvec=getattr(J, 'matvec'),
- rmatvec=getattr(J, 'rmatvec'),
- solve=J.solve,
- rsolve=getattr(J, 'rsolve'),
- update=getattr(J, 'update'),
- setup=getattr(J, 'setup'),
- dtype=J.dtype,
- shape=J.shape)
- elif callable(J):
- # Assume it's a function J(x) that returns the Jacobian
- class Jac(Jacobian):
- def update(self, x, F):
- self.x = x
- def solve(self, v, tol=0):
- m = J(self.x)
- if isinstance(m, np.ndarray):
- return solve(m, v)
- elif scipy.sparse.issparse(m):
- return spsolve(m, v)
- else:
- raise ValueError("Unknown matrix type")
- def matvec(self, v):
- m = J(self.x)
- if isinstance(m, np.ndarray):
- return dot(m, v)
- elif scipy.sparse.issparse(m):
- return m @ v
- else:
- raise ValueError("Unknown matrix type")
- def rsolve(self, v, tol=0):
- m = J(self.x)
- if isinstance(m, np.ndarray):
- return solve(m.conj().T, v)
- elif scipy.sparse.issparse(m):
- return spsolve(m.conj().T, v)
- else:
- raise ValueError("Unknown matrix type")
- def rmatvec(self, v):
- m = J(self.x)
- if isinstance(m, np.ndarray):
- return dot(m.conj().T, v)
- elif scipy.sparse.issparse(m):
- return m.conj().T @ v
- else:
- raise ValueError("Unknown matrix type")
- return Jac()
- elif isinstance(J, str):
- return dict(broyden1=BroydenFirst,
- broyden2=BroydenSecond,
- anderson=Anderson,
- diagbroyden=DiagBroyden,
- linearmixing=LinearMixing,
- excitingmixing=ExcitingMixing,
- krylov=KrylovJacobian)[J]()
- else:
- raise TypeError('Cannot convert object to a Jacobian')
- #------------------------------------------------------------------------------
- # Broyden
- #------------------------------------------------------------------------------
- class GenericBroyden(Jacobian):
- # generic type compatibility with scipy-stubs
- __class_getitem__ = classmethod(GenericAlias)
- def setup(self, x0, f0, func):
- Jacobian.setup(self, x0, f0, func)
- self.last_f = f0
- self.last_x = x0
- if hasattr(self, 'alpha') and self.alpha is None:
- # Autoscale the initial Jacobian parameter
- # unless we have already guessed the solution.
- normf0 = norm(f0)
- if normf0:
- self.alpha = 0.5*max(norm(x0), 1) / normf0
- else:
- self.alpha = 1.0
- def _update(self, x, f, dx, df, dx_norm, df_norm):
- raise NotImplementedError
- def update(self, x, f):
- df = f - self.last_f
- dx = x - self.last_x
- self._update(x, f, dx, df, norm(dx), norm(df))
- self.last_f = f
- self.last_x = x
- class LowRankMatrix:
- r"""
- A matrix represented as
- .. math:: \alpha I + \sum_{n=0}^{n=M} c_n d_n^\dagger
- However, if the rank of the matrix reaches the dimension of the vectors,
- full matrix representation will be used thereon.
- """
- # generic type compatibility with scipy-stubs
- __class_getitem__ = classmethod(GenericAlias)
- def __init__(self, alpha, n, dtype):
- self.alpha = alpha
- self.cs = []
- self.ds = []
- self.n = n
- self.dtype = dtype
- self.collapsed = None
- @staticmethod
- def _matvec(v, alpha, cs, ds):
- axpy, scal, dotc = get_blas_funcs(['axpy', 'scal', 'dotc'],
- cs[:1] + [v])
- w = alpha * v
- for c, d in zip(cs, ds):
- a = dotc(d, v)
- w = axpy(c, w, w.size, a)
- return w
- @staticmethod
- def _solve(v, alpha, cs, ds):
- """Evaluate w = M^-1 v"""
- if len(cs) == 0:
- return v/alpha
- # (B + C D^H)^-1 = B^-1 - B^-1 C (I + D^H B^-1 C)^-1 D^H B^-1
- axpy, dotc = get_blas_funcs(['axpy', 'dotc'], cs[:1] + [v])
- c0 = cs[0]
- A = alpha * np.identity(len(cs), dtype=c0.dtype)
- for i, d in enumerate(ds):
- for j, c in enumerate(cs):
- A[i,j] += dotc(d, c)
- q = np.zeros(len(cs), dtype=c0.dtype)
- for j, d in enumerate(ds):
- q[j] = dotc(d, v)
- q /= alpha
- q = solve(A, q)
- w = v/alpha
- for c, qc in zip(cs, q):
- w = axpy(c, w, w.size, -qc)
- return w
- def matvec(self, v):
- """Evaluate w = M v"""
- if self.collapsed is not None:
- return np.dot(self.collapsed, v)
- return LowRankMatrix._matvec(v, self.alpha, self.cs, self.ds)
- def rmatvec(self, v):
- """Evaluate w = M^H v"""
- if self.collapsed is not None:
- return np.dot(self.collapsed.T.conj(), v)
- return LowRankMatrix._matvec(v, np.conj(self.alpha), self.ds, self.cs)
- def solve(self, v, tol=0):
- """Evaluate w = M^-1 v"""
- if self.collapsed is not None:
- return solve(self.collapsed, v)
- return LowRankMatrix._solve(v, self.alpha, self.cs, self.ds)
- def rsolve(self, v, tol=0):
- """Evaluate w = M^-H v"""
- if self.collapsed is not None:
- return solve(self.collapsed.T.conj(), v)
- return LowRankMatrix._solve(v, np.conj(self.alpha), self.ds, self.cs)
- def append(self, c, d):
- if self.collapsed is not None:
- self.collapsed += c[:,None] * d[None,:].conj()
- return
- self.cs.append(c)
- self.ds.append(d)
- if len(self.cs) > c.size:
- self.collapse()
- def __array__(self, dtype=None, copy=None):
- if dtype is not None:
- warnings.warn("LowRankMatrix is scipy-internal code, `dtype` "
- f"should only be None but was {dtype} (not handled)",
- stacklevel=3)
- if copy is not None:
- warnings.warn("LowRankMatrix is scipy-internal code, `copy` "
- f"should only be None but was {copy} (not handled)",
- stacklevel=3)
- if self.collapsed is not None:
- return self.collapsed
- Gm = self.alpha*np.identity(self.n, dtype=self.dtype)
- for c, d in zip(self.cs, self.ds):
- Gm += c[:,None]*d[None,:].conj()
- return Gm
- def collapse(self):
- """Collapse the low-rank matrix to a full-rank one."""
- self.collapsed = np.array(self, copy=copy_if_needed)
- self.cs = None
- self.ds = None
- self.alpha = None
- def restart_reduce(self, rank):
- """
- Reduce the rank of the matrix by dropping all vectors.
- """
- if self.collapsed is not None:
- return
- assert rank > 0
- if len(self.cs) > rank:
- del self.cs[:]
- del self.ds[:]
- def simple_reduce(self, rank):
- """
- Reduce the rank of the matrix by dropping oldest vectors.
- """
- if self.collapsed is not None:
- return
- assert rank > 0
- while len(self.cs) > rank:
- del self.cs[0]
- del self.ds[0]
- def svd_reduce(self, max_rank, to_retain=None):
- """
- Reduce the rank of the matrix by retaining some SVD components.
- This corresponds to the \"Broyden Rank Reduction Inverse\"
- algorithm described in [1]_.
- Note that the SVD decomposition can be done by solving only a
- problem whose size is the effective rank of this matrix, which
- is viable even for large problems.
- Parameters
- ----------
- max_rank : int
- Maximum rank of this matrix after reduction.
- to_retain : int, optional
- Number of SVD components to retain when reduction is done
- (ie. rank > max_rank). Default is ``max_rank - 2``.
- References
- ----------
- .. [1] B.A. van der Rotten, PhD thesis,
- \"A limited memory Broyden method to solve high-dimensional
- systems of nonlinear equations\". Mathematisch Instituut,
- Universiteit Leiden, The Netherlands (2003).
- https://web.archive.org/web/20161022015821/http://www.math.leidenuniv.nl/scripties/Rotten.pdf
- """
- if self.collapsed is not None:
- return
- p = max_rank
- if to_retain is not None:
- q = to_retain
- else:
- q = p - 2
- if self.cs:
- p = min(p, len(self.cs[0]))
- q = max(0, min(q, p-1))
- m = len(self.cs)
- if m < p:
- # nothing to do
- return
- C = np.array(self.cs).T
- D = np.array(self.ds).T
- D, R = qr(D, mode='economic')
- C = dot(C, R.T.conj())
- U, S, WH = svd(C, full_matrices=False)
- C = dot(C, inv(WH))
- D = dot(D, WH.T.conj())
- for k in range(q):
- self.cs[k] = C[:,k].copy()
- self.ds[k] = D[:,k].copy()
- del self.cs[q:]
- del self.ds[q:]
- _doc_parts['broyden_params'] = _dedent_for_py313("""
- alpha : float, optional
- Initial guess for the Jacobian is ``(-1/alpha)``.
- reduction_method : str or tuple, optional
- Method used in ensuring that the rank of the Broyden matrix
- stays low. Can either be a string giving the name of the method,
- or a tuple of the form ``(method, param1, param2, ...)``
- that gives the name of the method and values for additional parameters.
- Methods available:
- - ``restart``: drop all matrix columns. Has no extra parameters.
- - ``simple``: drop oldest matrix column. Has no extra parameters.
- - ``svd``: keep only the most significant SVD components.
- Takes an extra parameter, ``to_retain``, which determines the
- number of SVD components to retain when rank reduction is done.
- Default is ``max_rank - 2``.
- max_rank : int, optional
- Maximum rank for the Broyden matrix.
- Default is infinity (i.e., no rank reduction).
- """).strip()
- class BroydenFirst(GenericBroyden):
- """
- Find a root of a function, using Broyden's first Jacobian approximation.
- This method is also known as "Broyden's good method".
- Parameters
- ----------
- %(params_basic)s
- %(broyden_params)s
- %(params_extra)s
- See Also
- --------
- root : Interface to root finding algorithms for multivariate
- functions. See ``method='broyden1'`` in particular.
- Notes
- -----
- This algorithm implements the inverse Jacobian Quasi-Newton update
- .. math:: H_+ = H + (dx - H df) dx^\\dagger H / ( dx^\\dagger H df)
- which corresponds to Broyden's first Jacobian update
- .. math:: J_+ = J + (df - J dx) dx^\\dagger / dx^\\dagger dx
- References
- ----------
- .. [1] B.A. van der Rotten, PhD thesis,
- "A limited memory Broyden method to solve high-dimensional
- systems of nonlinear equations". Mathematisch Instituut,
- Universiteit Leiden, The Netherlands (2003).
- https://math.leidenuniv.nl/scripties/Rotten.pdf
- Examples
- --------
- The following functions define a system of nonlinear equations
- >>> def fun(x):
- ... return [x[0] + 0.5 * (x[0] - x[1])**3 - 1.0,
- ... 0.5 * (x[1] - x[0])**3 + x[1]]
- A solution can be obtained as follows.
- >>> from scipy import optimize
- >>> sol = optimize.broyden1(fun, [0, 0])
- >>> sol
- array([0.84116396, 0.15883641])
- """
- def __init__(self, alpha=None, reduction_method='restart', max_rank=None):
- GenericBroyden.__init__(self)
- self.alpha = alpha
- self.Gm = None
- if max_rank is None:
- max_rank = np.inf
- self.max_rank = max_rank
- if isinstance(reduction_method, str):
- reduce_params = ()
- else:
- reduce_params = reduction_method[1:]
- reduction_method = reduction_method[0]
- reduce_params = (max_rank - 1,) + reduce_params
- if reduction_method == 'svd':
- self._reduce = lambda: self.Gm.svd_reduce(*reduce_params)
- elif reduction_method == 'simple':
- self._reduce = lambda: self.Gm.simple_reduce(*reduce_params)
- elif reduction_method == 'restart':
- self._reduce = lambda: self.Gm.restart_reduce(*reduce_params)
- else:
- raise ValueError(f"Unknown rank reduction method '{reduction_method}'")
- def setup(self, x, F, func):
- GenericBroyden.setup(self, x, F, func)
- self.Gm = LowRankMatrix(-self.alpha, self.shape[0], self.dtype)
- def todense(self):
- return inv(self.Gm)
- def solve(self, f, tol=0):
- r = self.Gm.matvec(f)
- if not np.isfinite(r).all():
- # singular; reset the Jacobian approximation
- self.setup(self.last_x, self.last_f, self.func)
- return self.Gm.matvec(f)
- return r
- def matvec(self, f):
- return self.Gm.solve(f)
- def rsolve(self, f, tol=0):
- return self.Gm.rmatvec(f)
- def rmatvec(self, f):
- return self.Gm.rsolve(f)
- def _update(self, x, f, dx, df, dx_norm, df_norm):
- self._reduce() # reduce first to preserve secant condition
- v = self.Gm.rmatvec(dx)
- c = dx - self.Gm.matvec(df)
- d = v / vdot(df, v)
- self.Gm.append(c, d)
- class BroydenSecond(BroydenFirst):
- """
- Find a root of a function, using Broyden\'s second Jacobian approximation.
- This method is also known as \"Broyden's bad method\".
- Parameters
- ----------
- %(params_basic)s
- %(broyden_params)s
- %(params_extra)s
- See Also
- --------
- root : Interface to root finding algorithms for multivariate
- functions. See ``method='broyden2'`` in particular.
- Notes
- -----
- This algorithm implements the inverse Jacobian Quasi-Newton update
- .. math:: H_+ = H + (dx - H df) df^\\dagger / ( df^\\dagger df)
- corresponding to Broyden's second method.
- References
- ----------
- .. [1] B.A. van der Rotten, PhD thesis,
- \"A limited memory Broyden method to solve high-dimensional
- systems of nonlinear equations\". Mathematisch Instituut,
- Universiteit Leiden, The Netherlands (2003).
- https://web.archive.org/web/20161022015821/http://www.math.leidenuniv.nl/scripties/Rotten.pdf
- Examples
- --------
- The following functions define a system of nonlinear equations
- >>> def fun(x):
- ... return [x[0] + 0.5 * (x[0] - x[1])**3 - 1.0,
- ... 0.5 * (x[1] - x[0])**3 + x[1]]
- A solution can be obtained as follows.
- >>> from scipy import optimize
- >>> sol = optimize.broyden2(fun, [0, 0])
- >>> sol
- array([0.84116365, 0.15883529])
- """
- def _update(self, x, f, dx, df, dx_norm, df_norm):
- self._reduce() # reduce first to preserve secant condition
- v = df
- c = dx - self.Gm.matvec(df)
- d = v / df_norm**2
- self.Gm.append(c, d)
- #------------------------------------------------------------------------------
- # Broyden-like (restricted memory)
- #------------------------------------------------------------------------------
- class Anderson(GenericBroyden):
- """
- Find a root of a function, using (extended) Anderson mixing.
- The Jacobian is formed by for a 'best' solution in the space
- spanned by last `M` vectors. As a result, only a MxM matrix
- inversions and MxN multiplications are required. [Ey]_
- Parameters
- ----------
- %(params_basic)s
- alpha : float, optional
- Initial guess for the Jacobian is (-1/alpha).
- M : float, optional
- Number of previous vectors to retain. Defaults to 5.
- w0 : float, optional
- Regularization parameter for numerical stability.
- Compared to unity, good values of the order of 0.01.
- %(params_extra)s
- See Also
- --------
- root : Interface to root finding algorithms for multivariate
- functions. See ``method='anderson'`` in particular.
- References
- ----------
- .. [Ey] V. Eyert, J. Comp. Phys., 124, 271 (1996).
- Examples
- --------
- The following functions define a system of nonlinear equations
- >>> def fun(x):
- ... return [x[0] + 0.5 * (x[0] - x[1])**3 - 1.0,
- ... 0.5 * (x[1] - x[0])**3 + x[1]]
- A solution can be obtained as follows.
- >>> from scipy import optimize
- >>> sol = optimize.anderson(fun, [0, 0])
- >>> sol
- array([0.84116588, 0.15883789])
- """
- # Note:
- #
- # Anderson method maintains a rank M approximation of the inverse Jacobian,
- #
- # J^-1 v ~ -v*alpha + (dX + alpha dF) A^-1 dF^H v
- # A = W + dF^H dF
- # W = w0^2 diag(dF^H dF)
- #
- # so that for w0 = 0 the secant condition applies for last M iterates, i.e.,
- #
- # J^-1 df_j = dx_j
- #
- # for all j = 0 ... M-1.
- #
- # Moreover, (from Sherman-Morrison-Woodbury formula)
- #
- # J v ~ [ b I - b^2 C (I + b dF^H A^-1 C)^-1 dF^H ] v
- # C = (dX + alpha dF) A^-1
- # b = -1/alpha
- #
- # and after simplification
- #
- # J v ~ -v/alpha + (dX/alpha + dF) (dF^H dX - alpha W)^-1 dF^H v
- #
- def __init__(self, alpha=None, w0=0.01, M=5):
- GenericBroyden.__init__(self)
- self.alpha = alpha
- self.M = M
- self.dx = []
- self.df = []
- self.gamma = None
- self.w0 = w0
- def solve(self, f, tol=0):
- dx = -self.alpha*f
- n = len(self.dx)
- if n == 0:
- return dx
- df_f = np.empty(n, dtype=f.dtype)
- for k in range(n):
- df_f[k] = vdot(self.df[k], f)
- try:
- gamma = solve(self.a, df_f)
- except LinAlgError:
- # singular; reset the Jacobian approximation
- del self.dx[:]
- del self.df[:]
- return dx
- for m in range(n):
- dx += gamma[m]*(self.dx[m] + self.alpha*self.df[m])
- return dx
- def matvec(self, f):
- dx = -f/self.alpha
- n = len(self.dx)
- if n == 0:
- return dx
- df_f = np.empty(n, dtype=f.dtype)
- for k in range(n):
- df_f[k] = vdot(self.df[k], f)
- b = np.empty((n, n), dtype=f.dtype)
- for i in range(n):
- for j in range(n):
- b[i,j] = vdot(self.df[i], self.dx[j])
- if i == j and self.w0 != 0:
- b[i,j] -= vdot(self.df[i], self.df[i])*self.w0**2*self.alpha
- gamma = solve(b, df_f)
- for m in range(n):
- dx += gamma[m]*(self.df[m] + self.dx[m]/self.alpha)
- return dx
- def _update(self, x, f, dx, df, dx_norm, df_norm):
- if self.M == 0:
- return
- self.dx.append(dx)
- self.df.append(df)
- while len(self.dx) > self.M:
- self.dx.pop(0)
- self.df.pop(0)
- n = len(self.dx)
- a = np.zeros((n, n), dtype=f.dtype)
- for i in range(n):
- for j in range(i, n):
- if i == j:
- wd = self.w0**2
- else:
- wd = 0
- a[i,j] = (1+wd)*vdot(self.df[i], self.df[j])
- a += np.triu(a, 1).T.conj()
- self.a = a
- #------------------------------------------------------------------------------
- # Simple iterations
- #------------------------------------------------------------------------------
- class DiagBroyden(GenericBroyden):
- """
- Find a root of a function, using diagonal Broyden Jacobian approximation.
- The Jacobian approximation is derived from previous iterations, by
- retaining only the diagonal of Broyden matrices.
- .. warning::
- This algorithm may be useful for specific problems, but whether
- it will work may depend strongly on the problem.
- Parameters
- ----------
- %(params_basic)s
- alpha : float, optional
- Initial guess for the Jacobian is (-1/alpha).
- %(params_extra)s
- See Also
- --------
- root : Interface to root finding algorithms for multivariate
- functions. See ``method='diagbroyden'`` in particular.
- Examples
- --------
- The following functions define a system of nonlinear equations
- >>> def fun(x):
- ... return [x[0] + 0.5 * (x[0] - x[1])**3 - 1.0,
- ... 0.5 * (x[1] - x[0])**3 + x[1]]
- A solution can be obtained as follows.
- >>> from scipy import optimize
- >>> sol = optimize.diagbroyden(fun, [0, 0])
- >>> sol
- array([0.84116403, 0.15883384])
- """
- def __init__(self, alpha=None):
- GenericBroyden.__init__(self)
- self.alpha = alpha
- def setup(self, x, F, func):
- GenericBroyden.setup(self, x, F, func)
- self.d = np.full((self.shape[0],), 1 / self.alpha, dtype=self.dtype)
- def solve(self, f, tol=0):
- return -f / self.d
- def matvec(self, f):
- return -f * self.d
- def rsolve(self, f, tol=0):
- return -f / self.d.conj()
- def rmatvec(self, f):
- return -f * self.d.conj()
- def todense(self):
- return np.diag(-self.d)
- def _update(self, x, f, dx, df, dx_norm, df_norm):
- self.d -= (df + self.d*dx)*dx/dx_norm**2
- class LinearMixing(GenericBroyden):
- """
- Find a root of a function, using a scalar Jacobian approximation.
- .. warning::
- This algorithm may be useful for specific problems, but whether
- it will work may depend strongly on the problem.
- Parameters
- ----------
- %(params_basic)s
- alpha : float, optional
- The Jacobian approximation is (-1/alpha).
- %(params_extra)s
- See Also
- --------
- root : Interface to root finding algorithms for multivariate
- functions. See ``method='linearmixing'`` in particular.
- """
- def __init__(self, alpha=None):
- GenericBroyden.__init__(self)
- self.alpha = alpha
- def solve(self, f, tol=0):
- return -f*self.alpha
- def matvec(self, f):
- return -f/self.alpha
- def rsolve(self, f, tol=0):
- return -f*np.conj(self.alpha)
- def rmatvec(self, f):
- return -f/np.conj(self.alpha)
- def todense(self):
- return np.diag(np.full(self.shape[0], -1/self.alpha))
- def _update(self, x, f, dx, df, dx_norm, df_norm):
- pass
- class ExcitingMixing(GenericBroyden):
- """
- Find a root of a function, using a tuned diagonal Jacobian approximation.
- The Jacobian matrix is diagonal and is tuned on each iteration.
- .. warning::
- This algorithm may be useful for specific problems, but whether
- it will work may depend strongly on the problem.
- See Also
- --------
- root : Interface to root finding algorithms for multivariate
- functions. See ``method='excitingmixing'`` in particular.
- Parameters
- ----------
- %(params_basic)s
- alpha : float, optional
- Initial Jacobian approximation is (-1/alpha).
- alphamax : float, optional
- The entries of the diagonal Jacobian are kept in the range
- ``[alpha, alphamax]``.
- %(params_extra)s
- """
- def __init__(self, alpha=None, alphamax=1.0):
- GenericBroyden.__init__(self)
- self.alpha = alpha
- self.alphamax = alphamax
- self.beta = None
- def setup(self, x, F, func):
- GenericBroyden.setup(self, x, F, func)
- self.beta = np.full((self.shape[0],), self.alpha, dtype=self.dtype)
- def solve(self, f, tol=0):
- return -f*self.beta
- def matvec(self, f):
- return -f/self.beta
- def rsolve(self, f, tol=0):
- return -f*self.beta.conj()
- def rmatvec(self, f):
- return -f/self.beta.conj()
- def todense(self):
- return np.diag(-1/self.beta)
- def _update(self, x, f, dx, df, dx_norm, df_norm):
- incr = f*self.last_f > 0
- self.beta[incr] += self.alpha
- self.beta[~incr] = self.alpha
- np.clip(self.beta, 0, self.alphamax, out=self.beta)
- #------------------------------------------------------------------------------
- # Iterative/Krylov approximated Jacobians
- #------------------------------------------------------------------------------
- class KrylovJacobian(Jacobian):
- """
- Find a root of a function, using Krylov approximation for inverse Jacobian.
- This method is suitable for solving large-scale problems.
- Parameters
- ----------
- %(params_basic)s
- rdiff : float, optional
- Relative step size to use in numerical differentiation.
- method : str or callable, optional
- Krylov method to use to approximate the Jacobian. Can be a string,
- or a function implementing the same interface as the iterative
- solvers in `scipy.sparse.linalg`. If a string, needs to be one of:
- ``'lgmres'``, ``'gmres'``, ``'bicgstab'``, ``'cgs'``, ``'minres'``,
- ``'tfqmr'``.
- The default is `scipy.sparse.linalg.lgmres`.
- inner_maxiter : int, optional
- Parameter to pass to the "inner" Krylov solver: maximum number of
- iterations. Iteration will stop after maxiter steps even if the
- specified tolerance has not been achieved.
- inner_M : LinearOperator or InverseJacobian
- Preconditioner for the inner Krylov iteration.
- Note that you can use also inverse Jacobians as (adaptive)
- preconditioners. For example,
- >>> from scipy.optimize import BroydenFirst, KrylovJacobian
- >>> from scipy.optimize import InverseJacobian
- >>> jac = BroydenFirst()
- >>> kjac = KrylovJacobian(inner_M=InverseJacobian(jac))
- If the preconditioner has a method named 'update', it will be called
- as ``update(x, f)`` after each nonlinear step, with ``x`` giving
- the current point, and ``f`` the current function value.
- outer_k : int, optional
- Size of the subspace kept across LGMRES nonlinear iterations.
- See `scipy.sparse.linalg.lgmres` for details.
- inner_kwargs : kwargs
- Keyword parameters for the "inner" Krylov solver
- (defined with `method`). Parameter names must start with
- the `inner_` prefix which will be stripped before passing on
- the inner method. See, e.g., `scipy.sparse.linalg.gmres` for details.
- %(params_extra)s
- See Also
- --------
- root : Interface to root finding algorithms for multivariate
- functions. See ``method='krylov'`` in particular.
- scipy.sparse.linalg.gmres
- scipy.sparse.linalg.lgmres
- Notes
- -----
- This function implements a Newton-Krylov solver. The basic idea is
- to compute the inverse of the Jacobian with an iterative Krylov
- method. These methods require only evaluating the Jacobian-vector
- products, which are conveniently approximated by a finite difference:
- .. math:: J v \\approx (f(x + \\omega*v/|v|) - f(x)) / \\omega
- Due to the use of iterative matrix inverses, these methods can
- deal with large nonlinear problems.
- SciPy's `scipy.sparse.linalg` module offers a selection of Krylov
- solvers to choose from. The default here is `lgmres`, which is a
- variant of restarted GMRES iteration that reuses some of the
- information obtained in the previous Newton steps to invert
- Jacobians in subsequent steps.
- For a review on Newton-Krylov methods, see for example [1]_,
- and for the LGMRES sparse inverse method, see [2]_.
- References
- ----------
- .. [1] C. T. Kelley, Solving Nonlinear Equations with Newton's Method,
- SIAM, pp.57-83, 2003.
- :doi:`10.1137/1.9780898718898.ch3`
- .. [2] D.A. Knoll and D.E. Keyes, J. Comp. Phys. 193, 357 (2004).
- :doi:`10.1016/j.jcp.2003.08.010`
- .. [3] A.H. Baker and E.R. Jessup and T. Manteuffel,
- SIAM J. Matrix Anal. Appl. 26, 962 (2005).
- :doi:`10.1137/S0895479803422014`
- Examples
- --------
- The following functions define a system of nonlinear equations
- >>> def fun(x):
- ... return [x[0] + 0.5 * x[1] - 1.0,
- ... 0.5 * (x[1] - x[0]) ** 2]
- A solution can be obtained as follows.
- >>> from scipy import optimize
- >>> sol = optimize.newton_krylov(fun, [0, 0])
- >>> sol
- array([0.66731771, 0.66536458])
- """
- def __init__(self, rdiff=None, method='lgmres', inner_maxiter=20,
- inner_M=None, outer_k=10, **kw):
- self.preconditioner = inner_M
- self.rdiff = rdiff
- # Note that this retrieves one of the named functions, or otherwise
- # uses `method` as is (i.e., for a user-provided callable).
- self.method = dict(
- bicgstab=scipy.sparse.linalg.bicgstab,
- gmres=scipy.sparse.linalg.gmres,
- lgmres=scipy.sparse.linalg.lgmres,
- cgs=scipy.sparse.linalg.cgs,
- minres=scipy.sparse.linalg.minres,
- tfqmr=scipy.sparse.linalg.tfqmr,
- ).get(method, method)
- self.method_kw = dict(maxiter=inner_maxiter, M=self.preconditioner)
- if self.method is scipy.sparse.linalg.gmres:
- # Replace GMRES's outer iteration with Newton steps
- self.method_kw['restart'] = inner_maxiter
- self.method_kw['maxiter'] = 1
- self.method_kw.setdefault('atol', 0)
- elif self.method in (scipy.sparse.linalg.gcrotmk,
- scipy.sparse.linalg.bicgstab,
- scipy.sparse.linalg.cgs):
- self.method_kw.setdefault('atol', 0)
- elif self.method is scipy.sparse.linalg.lgmres:
- self.method_kw['outer_k'] = outer_k
- # Replace LGMRES's outer iteration with Newton steps
- self.method_kw['maxiter'] = 1
- # Carry LGMRES's `outer_v` vectors across nonlinear iterations
- self.method_kw.setdefault('outer_v', [])
- self.method_kw.setdefault('prepend_outer_v', True)
- # But don't carry the corresponding Jacobian*v products, in case
- # the Jacobian changes a lot in the nonlinear step
- #
- # XXX: some trust-region inspired ideas might be more efficient...
- # See e.g., Brown & Saad. But needs to be implemented separately
- # since it's not an inexact Newton method.
- self.method_kw.setdefault('store_outer_Av', False)
- self.method_kw.setdefault('atol', 0)
- # Retrieve the signature of the method to find the valid parameters
- valid_inner_params = [
- k for k in signature(self.method).parameters
- if k not in ('self', 'args', 'kwargs')
- ]
- for key, value in kw.items():
- if not key.startswith("inner_"):
- raise ValueError(f"Unknown parameter {key}")
- if key[6:] not in valid_inner_params:
- # Use difflib to find close matches to the invalid key
- inner_param_suggestions = get_close_matches(key[6:],
- valid_inner_params,
- n=1)
- if inner_param_suggestions:
- suggestion_msg = (f" Did you mean '"
- f"{inner_param_suggestions[0]}'?")
- else:
- suggestion_msg = ""
- # warn user that the parameter is not valid for the inner method
- warnings.warn(
- f"Option '{key}' is invalid for the inner method: {method}."
- " It will be ignored."
- "Please check inner method documentation for valid options."
- + suggestion_msg,
- stacklevel=3,
- category=UserWarning,
- # using `skip_file_prefixes` would be a good idea
- # and should be added once we drop support for Python 3.11
- )
- # ignore this parameter and continue
- continue
- self.method_kw[key[6:]] = value
- def _update_diff_step(self):
- mx = abs(self.x0).max()
- mf = abs(self.f0).max()
- self.omega = self.rdiff * max(1, mx) / max(1, mf)
- def matvec(self, v):
- nv = norm(v)
- if nv == 0:
- return 0*v
- sc = self.omega / nv
- r = (self.func(self.x0 + sc*v) - self.f0) / sc
- if not np.all(np.isfinite(r)) and np.all(np.isfinite(v)):
- raise ValueError('Function returned non-finite results')
- return r
- def solve(self, rhs, tol=0):
- if 'rtol' in self.method_kw:
- sol, info = self.method(self.op, rhs, **self.method_kw)
- else:
- sol, info = self.method(self.op, rhs, rtol=tol, **self.method_kw)
- return sol
- def update(self, x, f):
- self.x0 = x
- self.f0 = f
- self._update_diff_step()
- # Update also the preconditioner, if possible
- if self.preconditioner is not None:
- if hasattr(self.preconditioner, 'update'):
- self.preconditioner.update(x, f)
- def setup(self, x, f, func):
- Jacobian.setup(self, x, f, func)
- self.x0 = x
- self.f0 = f
- self.op = scipy.sparse.linalg.aslinearoperator(self)
- if self.rdiff is None:
- self.rdiff = np.finfo(x.dtype).eps ** (1./2)
- self._update_diff_step()
- # Setup also the preconditioner, if possible
- if self.preconditioner is not None:
- if hasattr(self.preconditioner, 'setup'):
- self.preconditioner.setup(x, f, func)
- #------------------------------------------------------------------------------
- # Wrapper functions
- #------------------------------------------------------------------------------
- def _nonlin_wrapper(name, jac):
- """
- Construct a solver wrapper with given name and Jacobian approx.
- It inspects the keyword arguments of ``jac.__init__``, and allows to
- use the same arguments in the wrapper function, in addition to the
- keyword arguments of `nonlin_solve`
- """
- signature = _getfullargspec(jac.__init__)
- args, varargs, varkw, defaults, kwonlyargs, kwdefaults, _ = signature
- kwargs = list(zip(args[-len(defaults):], defaults))
- kw_str = ", ".join([f"{k}={v!r}" for k, v in kwargs])
- if kw_str:
- kw_str = ", " + kw_str
- kwkw_str = ", ".join([f"{k}={k}" for k, v in kwargs])
- if kwkw_str:
- kwkw_str = kwkw_str + ", "
- if kwonlyargs:
- raise ValueError(f'Unexpected signature {signature}')
- # Construct the wrapper function so that its keyword arguments
- # are visible in pydoc.help etc.
- wrapper = """
- def %(name)s(F, xin, iter=None %(kw)s, verbose=False, maxiter=None,
- f_tol=None, f_rtol=None, x_tol=None, x_rtol=None,
- tol_norm=None, line_search='armijo', callback=None, **kw):
- jac = %(jac)s(%(kwkw)s **kw)
- return nonlin_solve(F, xin, jac, iter, verbose, maxiter,
- f_tol, f_rtol, x_tol, x_rtol, tol_norm, line_search,
- callback)
- """
- wrapper = wrapper % dict(name=name, kw=kw_str, jac=jac.__name__,
- kwkw=kwkw_str)
- ns = {}
- ns.update(globals())
- exec(wrapper, ns)
- func = ns[name]
- func.__doc__ = jac.__doc__
- _set_doc(func)
- return func
- broyden1 = _nonlin_wrapper('broyden1', BroydenFirst)
- broyden2 = _nonlin_wrapper('broyden2', BroydenSecond)
- anderson = _nonlin_wrapper('anderson', Anderson)
- linearmixing = _nonlin_wrapper('linearmixing', LinearMixing)
- diagbroyden = _nonlin_wrapper('diagbroyden', DiagBroyden)
- excitingmixing = _nonlin_wrapper('excitingmixing', ExcitingMixing)
- newton_krylov = _nonlin_wrapper('newton_krylov', KrylovJacobian)
|