Coverage for local_installation_linux/mumott/optimization/optimizers/lbfgs.py: 100%
42 statements
« prev ^ index » next coverage.py v7.3.2, created at 2025-05-05 21:21 +0000
« prev ^ index » next coverage.py v7.3.2, created at 2025-05-05 21:21 +0000
1import logging
3from typing import Dict, Any
5from numpy import float64
7from scipy.optimize import minimize
9from mumott.core.hashing import list_to_hash
10from mumott.optimization.loss_functions.base_loss_function import LossFunction
11from .base_optimizer import Optimizer
14logger = logging.getLogger(__name__)
17class LBFGS(Optimizer):
18 """This Optimizer makes the :term:`LBFGS` algorithm from `scipy.optimize
19 <https://docs.scipy.org/doc/scipy/reference/optimize.html>`_
20 available for usage with a :class:`LossFunction <module-mumott.optimization.loss_functions>`.
22 Parameters
23 ----------
24 loss_function : LossFunction
25 The :ref:`loss function <loss_functions>` to be minimized using this algorithm.
26 kwargs : Dict[str, Any]
27 Miscellaneous options. See notes for valid entries.
29 Notes
30 -----
31 Valid entries in :attr:`kwargs` are
33 x0
34 Initial guess for solution vector. Must be the same size as
35 :attr:`residual_calculator.coefficients`. Defaults to :attr:`loss_function.initial_values`.
36 bounds
37 Used to set the ``bounds`` of the optimization method, see `scipy.optimize.minimize
38 <https://docs.scipy.org/doc/scipy/reference/generated/scipy.optimize.minimize.html>`_
39 documentation for details. Defaults to ``None``.
40 maxiter
41 Maximum number of iterations. Defaults to ``10``.
42 disp
43 Whether to display output from the optimizer. Defaults to ``False``
44 maxcor
45 Maximum number of Hessian corrections to the Jacobian. Defaults to ``10``.
46 iprint
47 If ``disp`` is true, controls output with no output if ``iprint < 0``,
48 convergence output only if ``iprint == 0``, iteration-wise output if
49 ``0 < iprint <= 99``, and sub-iteration output if ``iprint > 99``.
50 maxfun
51 Maximum number of function evaluations, including line search evaluations.
52 Defaults to ``20``.
53 ftol
54 Relative change tolerance for objective function. Changes to absolute change tolerance
55 if objective function is ``< 1``, see `scipy.optimize.minimize
56 <https://docs.scipy.org/doc/scipy/reference/generated/scipy.optimize.minimize.html>`_
57 documentation, which may lead to excessively fast convergence.
58 Defaults to ``1e-3``.
59 gtol
60 Convergence tolerance for gradient. Defaults to ``1e-5``.
61 """
63 def __init__(self,
64 loss_function: LossFunction,
65 **kwargs: Dict[str, Any]):
66 super().__init__(loss_function, **kwargs)
67 # This will later be used to reshape the flattened output.
68 self._output_shape = None
70 def optimize(self) -> Dict:
71 """ Executes the optimization using the options stored in this class
72 instance. The optimization will continue until convergence,
73 or until the maximum number of iterations (:attr:`maxiter`) is exceeded.
75 Returns
76 -------
77 A ``dict`` of optimization results. See `scipy.optimize.OptimizeResult
78 <https://docs.scipy.org/doc/scipy/reference/generated/scipy.optimize.OptimizeResult.html>`_
79 for details. The entry ``'x'``, which contains the result, will be reshaped using
80 the shape of the gradient from :attr:`loss_function`.
81 """
82 lbfgs_kwargs = dict(x0=self._loss_function.initial_values,
83 bounds=None)
84 misc_options = dict(maxiter=10,
85 disp=False,
86 maxcor=10,
87 iprint=1,
88 maxfun=20,
89 ftol=1e-3,
90 gtol=1e-5)
92 for k in lbfgs_kwargs:
93 if k in dict(self):
94 lbfgs_kwargs[k] = self[k]
96 for k in misc_options:
97 if k in dict(self):
98 misc_options[k] = self[k]
100 for k in dict(self):
101 if k not in lbfgs_kwargs and k not in misc_options:
102 logger.warning(f'Unknown option {k}, with value {self[k]}, has been ignored.')
104 if lbfgs_kwargs['x0'] is None:
105 lbfgs_kwargs['x0'] = self._loss_function.initial_values
106 lbfgs_kwargs['x0'] = lbfgs_kwargs['x0'].ravel()
108 with self._tqdm(misc_options['maxiter']) as progress:
110 def progress_callback(*args, **kwargs):
111 progress.update(1)
113 def loss_function_wrapper(coefficients):
114 d = self._loss_function.get_loss(coefficients, get_gradient=True)
115 # Store gradient shape to reshape flattened output.
116 if self._output_shape is None:
117 self._output_shape = d['gradient'].shape
118 # LBFGS needs float64
119 return d['loss'], d['gradient'].ravel().astype(float64)
121 result = minimize(fun=loss_function_wrapper, callback=progress_callback,
122 **lbfgs_kwargs, jac=True, method='L-BFGS-B', options=misc_options)
123 result = dict(result)
124 result['x'] = result['x'].reshape(self._output_shape)
125 return dict(result)
127 def __hash__(self) -> int:
128 to_hash = [self._options, hash(self._loss_function)]
129 return int(list_to_hash(to_hash), 16)