Source code for pyrtid.inverse.executors.stochopy
"""
Implement the interface for the stochopy inversion executor.
Stochopy is a collection of stochastic solvers.
This inversion executor gives access to the Stochopy implementation to solve the inverse
problem. The original code is provided and maintained by Keurfon Luu
and can be found at: https://github.com/keurfonluu/stochopy
"""
from __future__ import annotations
import copy
import logging
from dataclasses import dataclass
from typing import Any, Dict, Optional
from stochopy.optimize import OptimizeResult as StochpyOptimizeResult
from stochopy.optimize import minimize as stochopy_minimize
from typing_extensions import Literal
from pyrtid.inverse.executors.base import (
BaseInversionExecutor,
BaseSolverConfig,
base_solver_config_params_ds,
register_params_ds,
)
from pyrtid.inverse.params import get_parameters_bounds
from pyrtid.utils import NDArrayFloat
stochopy_solver_config_params_ds = """
solver_name: Literal["cmaes", "cpso", "de", "na", "pso", "vdcma"]
The default is "cmaes".
solver_options: Optional[Dict[str, Any]]
The default is None.
max_optimization_round_nb: int
The default is 1.
max_fun_per_round: int
The default is 5.
"""
[docs]@register_params_ds(stochopy_solver_config_params_ds)
@register_params_ds(base_solver_config_params_ds)
@dataclass
class StochopySolverConfig(BaseSolverConfig):
"""_summary_
Parameters
----------
"""
# TODO: add other parameters names
solver_name: Literal["cmaes", "cpso", "de", "na", "pso", "vdcma"] = "cmaes"
solver_options: Optional[Dict[str, Any]] = None
max_optimization_round_nb: int = 1
max_fun_per_round: int = 5
[docs]class StochopyInversionExecutor(BaseInversionExecutor[StochopySolverConfig]):
"""Represent a inversion executor instance using stochopy's solvers."""
[docs] def _init_solver(self, s_init: NDArrayFloat) -> None:
"""Careful, s_init is supposed to be preconditioned."""
super()._init_solver(s_init)
[docs] def _get_solver_name(self) -> str:
"""Return the solver name."""
return self.solver_config.solver_name
[docs] def run(self) -> StochpyOptimizeResult:
"""
Run the history matching.
First is creates raw folders to store the different runs
required by the HM algorithms.
"""
super().run()
res = StochpyOptimizeResult()
x0 = self.data_model.s_init
# Empty dict for the results
# res: Dict[str, Any] = {}
# 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(
"Entering optimization loop: %s", 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
)
res = stochopy_minimize(
self.eval_loss,
get_parameters_bounds(
self.inv_model.parameters_to_adjust, is_preconditioned=True
),
x0=x0,
method=self.solver_config.solver_name,
options=_options,
)
# The output parameter vector becomes the input
return res
[docs] def _get_options_dict(
self,
solver_config: StochopySolverConfig,
nfev: 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 = {}
max_fun = min(
solver_config.max_fun_per_round, options.get("maxfun", 15000) - nfev
)
if (
self.inv_model.optimization_round_nb
!= solver_config.max_optimization_round_nb
):
options["maxfun"] = max_fun
else:
options["maxfun"] = 0
return options