Source code for pyrtid.inverse.executors.scipy
"""
Implement the interface for the scipy inversion executor.
See https://docs.scipy.org/doc/scipy/reference/generated/scipy.optimize.minimize.html
"""
from __future__ import annotations
import copy
import logging
from dataclasses import dataclass
from typing import Any, Dict, Optional
from scipy.optimize import OptimizeResult as ScipyOptimizeResult
from scipy.optimize import minimize as scipy_minimize
from pyrtid.inverse.executors.base import (
AdjointInversionExecutor,
AdjointSolverConfig,
adjoint_solver_config_params_ds,
base_solver_config_params_ds,
register_params_ds,
)
from pyrtid.inverse.params import get_parameters_bounds
from pyrtid.utils import NDArrayFloat
scipy_solver_config_params_ds = r"""solver_name: str = "L-BFGS-B"
Name of the solver to use. TODO: point to scipy.
solver_options: Optional[Dict[str, Any]]
Kwargs for scipy.optimize.minimize. The default is None.
max_optimization_round_nb: int
Maximum number of optimization rounds. The default is 1.
max_fun_first_round: int
The maximum number of forward computation in the first optimization round.
THe default is 5.
max_fun_per_round: int
The number of function evaluation before a new round starts.
The default is 5.
is_regularization_at_first_round: bool = True
Whether to apply regularization at first round. The default is True.
"""
[docs]@register_params_ds(scipy_solver_config_params_ds)
@register_params_ds(adjoint_solver_config_params_ds)
@register_params_ds(base_solver_config_params_ds)
@dataclass
class ScipySolverConfig(AdjointSolverConfig):
"""
Configuration for Scipy solvers.
Parameters
----------
"""
solver_name: str = "L-BFGS-B"
solver_options: Optional[Dict[str, Any]] = None
max_optimization_round_nb: int = 1
max_fun_first_round: int = 5
max_fun_per_round: int = 5
is_regularization_at_first_round: bool = True
[docs]class ScipyInversionExecutor(AdjointInversionExecutor[ScipySolverConfig]):
"""Represent a inversion executor instance using scipy's solvers."""
[docs] def _init_solver(self, s_init: NDArrayFloat) -> None:
"""Careful, s_init is supposed to be preconditioned."""
super()._init_solver(s_init)
# Create an adjoint model only if needed
self.adj_model = None
if self.solver_config.is_use_adjoint:
self._init_adjoint_model(
self.solver_config.afpi_eps,
self.solver_config.is_adj_numerical_acceleration,
self.solver_config.is_use_continuous_adj,
)
[docs] def _get_solver_name(self) -> str:
"""Return the solver name."""
return self.solver_config.solver_name
def get_display_dict(self) -> Dict[str, Any]:
# return {"Number of realizations": self.solver.s_dim}
# "Stop criteria on cost function value"
# "Minimum change in cost function"
# "Maximum number of forward HYTEC calls"
# "Maximum number of iterations"
# "Minimum change in parameter vector"
# "Maximum number of HYTEC gradient calls"
# "Minimum norm of the gradient vector"
# "Number of gradient kept in memory"
# "Adjoint-state status"
# "Check gradient by finite difference"
return {}
[docs] def run(self) -> ScipyOptimizeResult:
"""
Run the history matching.
First is creates raw folders to store the different runs
required by the HM algorithms.
"""
super().run()
res: ScipyOptimizeResult = ScipyOptimizeResult()
x0 = self.data_model.s_init
self.inv_model.is_regularization_at_first_round = (
self.solver_config.is_regularization_at_first_round
)
# The optimization loop might be launched several time successively to
# re-compute the regularization weights if automatically determined.
while self.inv_model.is_new_optimization_round_needed(
self.solver_config.max_optimization_round_nb
):
# Reset the booleans for the new loop
self.inv_model.is_first_loss_function_call_in_round = True
self.inv_model.optimization_round_nb += 1
logging.info(
f"Entering optimization loop: {self.inv_model.optimization_round_nb}"
)
# Update options and stop criteria from the previous loops
_options: Dict[str, Any] = self._get_options_dict(
self.solver_config,
self.inv_model.nb_f_calls,
self.inv_model.optimization_round_nb,
)
res = scipy_minimize(
self.eval_loss,
x0,
bounds=get_parameters_bounds(
self.inv_model.parameters_to_adjust, is_preconditioned=True
),
method=self.solver_config.solver_name,
jac=self.eval_loss_gradient,
options=_options,
)
# The output parameter vector becomes the input
x0 = res.x
return res
[docs] def _get_options_dict(
self, solver_config: ScipySolverConfig, nfev: int, round: int
) -> Dict[str, Any]:
"""Update optimization stop criteria."""
if solver_config.solver_options is not None:
options = copy.deepcopy(solver_config.solver_options)
else:
options = {}
if solver_config.max_optimization_round_nb == 1:
max_fun: int = options.get("maxfun", 15000)
elif round == 1:
max_fun: int = min(
solver_config.max_fun_first_round,
solver_config.max_fun_per_round,
options.get("maxfun", 15000) - nfev,
)
else:
max_fun: int = min(
solver_config.max_fun_per_round, options.get("maxfun", 15000) - nfev
)
options["maxfun"] = max_fun
return options