Skip to content
Merged
Show file tree
Hide file tree
Changes from 6 commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions .tools/envs/testenv-linux.yml
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,7 @@ dependencies:
- pyyaml # dev, tests
- jinja2 # dev, tests
- annotated-types # dev, tests
- iminuit # dev, tests
- pip: # dev, tests, docs
- DFO-LS>=1.5.3 # dev, tests
- Py-BOBYQA # dev, tests
Expand Down
1 change: 1 addition & 0 deletions .tools/envs/testenv-numpy.yml
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,7 @@ dependencies:
- pyyaml # dev, tests
- jinja2 # dev, tests
- annotated-types # dev, tests
- iminuit # dev, tests
- pip: # dev, tests, docs
- DFO-LS>=1.5.3 # dev, tests
- Py-BOBYQA # dev, tests
Expand Down
1 change: 1 addition & 0 deletions .tools/envs/testenv-others.yml
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,7 @@ dependencies:
- pyyaml # dev, tests
- jinja2 # dev, tests
- annotated-types # dev, tests
- iminuit # dev, tests
- pip: # dev, tests, docs
- DFO-LS>=1.5.3 # dev, tests
- Py-BOBYQA # dev, tests
Expand Down
1 change: 1 addition & 0 deletions .tools/envs/testenv-pandas.yml
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,7 @@ dependencies:
- pyyaml # dev, tests
- jinja2 # dev, tests
- annotated-types # dev, tests
- iminuit # dev, tests
- pip: # dev, tests, docs
- DFO-LS>=1.5.3 # dev, tests
- Py-BOBYQA # dev, tests
Expand Down
1 change: 1 addition & 0 deletions environment.yml
Original file line number Diff line number Diff line change
Expand Up @@ -36,6 +36,7 @@ dependencies:
- jinja2 # dev, tests
- furo # dev, docs
- annotated-types # dev, tests
- iminuit # dev, tests
- pip: # dev, tests, docs
- DFO-LS>=1.5.3 # dev, tests
- Py-BOBYQA # dev, tests
Expand Down
2 changes: 2 additions & 0 deletions pyproject.toml
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@ dependencies = [
"sqlalchemy>=1.3",
"annotated-types",
"typing-extensions",
"iminuit",
]
dynamic = ["version"]
keywords = [
Expand Down Expand Up @@ -378,5 +379,6 @@ module = [
"optimagic._version",
"annotated_types",
"pdbp",
"iminuit",
]
ignore_missing_imports = true
17 changes: 17 additions & 0 deletions src/optimagic/algorithms.py
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@
from optimagic.optimization.algorithm import Algorithm
from optimagic.optimizers.bhhh import BHHH
from optimagic.optimizers.fides import Fides
from optimagic.optimizers.iminuit_migrad import IminuitMigrad
from optimagic.optimizers.ipopt import Ipopt
from optimagic.optimizers.nag_optimizers import NagDFOLS, NagPyBOBYQA
from optimagic.optimizers.neldermead import NelderMeadParallel
Expand Down Expand Up @@ -286,6 +287,7 @@ def Scalar(self) -> BoundedGradientBasedLocalNonlinearConstrainedScalarAlgorithm
@dataclass(frozen=True)
class BoundedGradientBasedLocalScalarAlgorithms(AlgoSelection):
fides: Type[Fides] = Fides
iminuit_migrad: Type[IminuitMigrad] = IminuitMigrad
ipopt: Type[Ipopt] = Ipopt
nlopt_ccsaq: Type[NloptCCSAQ] = NloptCCSAQ
nlopt_lbfgsb: Type[NloptLBFGSB] = NloptLBFGSB
Expand Down Expand Up @@ -840,6 +842,7 @@ def NonlinearConstrained(
@dataclass(frozen=True)
class BoundedGradientBasedLocalAlgorithms(AlgoSelection):
fides: Type[Fides] = Fides
iminuit_migrad: Type[IminuitMigrad] = IminuitMigrad
ipopt: Type[Ipopt] = Ipopt
nlopt_ccsaq: Type[NloptCCSAQ] = NloptCCSAQ
nlopt_lbfgsb: Type[NloptLBFGSB] = NloptLBFGSB
Expand Down Expand Up @@ -889,6 +892,7 @@ def Scalar(self) -> GradientBasedLocalNonlinearConstrainedScalarAlgorithms:
@dataclass(frozen=True)
class GradientBasedLocalScalarAlgorithms(AlgoSelection):
fides: Type[Fides] = Fides
iminuit_migrad: Type[IminuitMigrad] = IminuitMigrad
ipopt: Type[Ipopt] = Ipopt
nlopt_ccsaq: Type[NloptCCSAQ] = NloptCCSAQ
nlopt_lbfgsb: Type[NloptLBFGSB] = NloptLBFGSB
Expand Down Expand Up @@ -956,6 +960,7 @@ def Scalar(self) -> BoundedGradientBasedNonlinearConstrainedScalarAlgorithms:
@dataclass(frozen=True)
class BoundedGradientBasedScalarAlgorithms(AlgoSelection):
fides: Type[Fides] = Fides
iminuit_migrad: Type[IminuitMigrad] = IminuitMigrad
ipopt: Type[Ipopt] = Ipopt
nlopt_ccsaq: Type[NloptCCSAQ] = NloptCCSAQ
nlopt_lbfgsb: Type[NloptLBFGSB] = NloptLBFGSB
Expand Down Expand Up @@ -1674,6 +1679,7 @@ def Scalar(self) -> BoundedLocalNonlinearConstrainedScalarAlgorithms:
@dataclass(frozen=True)
class BoundedLocalScalarAlgorithms(AlgoSelection):
fides: Type[Fides] = Fides
iminuit_migrad: Type[IminuitMigrad] = IminuitMigrad
ipopt: Type[Ipopt] = Ipopt
nag_pybobyqa: Type[NagPyBOBYQA] = NagPyBOBYQA
nlopt_bobyqa: Type[NloptBOBYQA] = NloptBOBYQA
Expand Down Expand Up @@ -1943,6 +1949,7 @@ def Scalar(self) -> GlobalGradientBasedScalarAlgorithms:
class GradientBasedLocalAlgorithms(AlgoSelection):
bhhh: Type[BHHH] = BHHH
fides: Type[Fides] = Fides
iminuit_migrad: Type[IminuitMigrad] = IminuitMigrad
ipopt: Type[Ipopt] = Ipopt
nlopt_ccsaq: Type[NloptCCSAQ] = NloptCCSAQ
nlopt_lbfgsb: Type[NloptLBFGSB] = NloptLBFGSB
Expand Down Expand Up @@ -1985,6 +1992,7 @@ def Scalar(self) -> GradientBasedLocalScalarAlgorithms:
@dataclass(frozen=True)
class BoundedGradientBasedAlgorithms(AlgoSelection):
fides: Type[Fides] = Fides
iminuit_migrad: Type[IminuitMigrad] = IminuitMigrad
ipopt: Type[Ipopt] = Ipopt
nlopt_ccsaq: Type[NloptCCSAQ] = NloptCCSAQ
nlopt_lbfgsb: Type[NloptLBFGSB] = NloptLBFGSB
Expand Down Expand Up @@ -2054,6 +2062,7 @@ def Scalar(self) -> GradientBasedNonlinearConstrainedScalarAlgorithms:
@dataclass(frozen=True)
class GradientBasedScalarAlgorithms(AlgoSelection):
fides: Type[Fides] = Fides
iminuit_migrad: Type[IminuitMigrad] = IminuitMigrad
ipopt: Type[Ipopt] = Ipopt
nlopt_ccsaq: Type[NloptCCSAQ] = NloptCCSAQ
nlopt_lbfgsb: Type[NloptLBFGSB] = NloptLBFGSB
Expand Down Expand Up @@ -2577,6 +2586,7 @@ def Scalar(self) -> GlobalParallelScalarAlgorithms:
@dataclass(frozen=True)
class BoundedLocalAlgorithms(AlgoSelection):
fides: Type[Fides] = Fides
iminuit_migrad: Type[IminuitMigrad] = IminuitMigrad
ipopt: Type[Ipopt] = Ipopt
nag_dfols: Type[NagDFOLS] = NagDFOLS
nag_pybobyqa: Type[NagPyBOBYQA] = NagPyBOBYQA
Expand Down Expand Up @@ -2659,6 +2669,7 @@ def Scalar(self) -> LocalNonlinearConstrainedScalarAlgorithms:
@dataclass(frozen=True)
class LocalScalarAlgorithms(AlgoSelection):
fides: Type[Fides] = Fides
iminuit_migrad: Type[IminuitMigrad] = IminuitMigrad
ipopt: Type[Ipopt] = Ipopt
nag_pybobyqa: Type[NagPyBOBYQA] = NagPyBOBYQA
neldermead_parallel: Type[NelderMeadParallel] = NelderMeadParallel
Expand Down Expand Up @@ -2809,6 +2820,7 @@ def Scalar(self) -> BoundedNonlinearConstrainedScalarAlgorithms:
@dataclass(frozen=True)
class BoundedScalarAlgorithms(AlgoSelection):
fides: Type[Fides] = Fides
iminuit_migrad: Type[IminuitMigrad] = IminuitMigrad
ipopt: Type[Ipopt] = Ipopt
nag_pybobyqa: Type[NagPyBOBYQA] = NagPyBOBYQA
nlopt_bobyqa: Type[NloptBOBYQA] = NloptBOBYQA
Expand Down Expand Up @@ -3063,6 +3075,7 @@ def Local(self) -> LeastSquaresLocalParallelAlgorithms:
class GradientBasedAlgorithms(AlgoSelection):
bhhh: Type[BHHH] = BHHH
fides: Type[Fides] = Fides
iminuit_migrad: Type[IminuitMigrad] = IminuitMigrad
ipopt: Type[Ipopt] = Ipopt
nlopt_ccsaq: Type[NloptCCSAQ] = NloptCCSAQ
nlopt_lbfgsb: Type[NloptLBFGSB] = NloptLBFGSB
Expand Down Expand Up @@ -3246,6 +3259,7 @@ def Scalar(self) -> GlobalScalarAlgorithms:
class LocalAlgorithms(AlgoSelection):
bhhh: Type[BHHH] = BHHH
fides: Type[Fides] = Fides
iminuit_migrad: Type[IminuitMigrad] = IminuitMigrad
ipopt: Type[Ipopt] = Ipopt
nag_dfols: Type[NagDFOLS] = NagDFOLS
nag_pybobyqa: Type[NagPyBOBYQA] = NagPyBOBYQA
Expand Down Expand Up @@ -3316,6 +3330,7 @@ def Scalar(self) -> LocalScalarAlgorithms:
@dataclass(frozen=True)
class BoundedAlgorithms(AlgoSelection):
fides: Type[Fides] = Fides
iminuit_migrad: Type[IminuitMigrad] = IminuitMigrad
ipopt: Type[Ipopt] = Ipopt
nag_dfols: Type[NagDFOLS] = NagDFOLS
nag_pybobyqa: Type[NagPyBOBYQA] = NagPyBOBYQA
Expand Down Expand Up @@ -3451,6 +3466,7 @@ def Scalar(self) -> NonlinearConstrainedScalarAlgorithms:
@dataclass(frozen=True)
class ScalarAlgorithms(AlgoSelection):
fides: Type[Fides] = Fides
iminuit_migrad: Type[IminuitMigrad] = IminuitMigrad
ipopt: Type[Ipopt] = Ipopt
nag_pybobyqa: Type[NagPyBOBYQA] = NagPyBOBYQA
neldermead_parallel: Type[NelderMeadParallel] = NelderMeadParallel
Expand Down Expand Up @@ -3625,6 +3641,7 @@ def Scalar(self) -> ParallelScalarAlgorithms:
class Algorithms(AlgoSelection):
bhhh: Type[BHHH] = BHHH
fides: Type[Fides] = Fides
iminuit_migrad: Type[IminuitMigrad] = IminuitMigrad
ipopt: Type[Ipopt] = Ipopt
nag_dfols: Type[NagDFOLS] = NagDFOLS
nag_pybobyqa: Type[NagPyBOBYQA] = NagPyBOBYQA
Expand Down
8 changes: 8 additions & 0 deletions src/optimagic/config.py
Original file line number Diff line number Diff line change
Expand Up @@ -92,6 +92,14 @@
IS_NUMBA_INSTALLED = True


try:
import iminuit # noqa: F401
except ImportError:
IS_IMINUIT_INSTALLED = False
else:
IS_IMINUIT_INSTALLED = True


# ======================================================================================
# Check if pandas version is newer or equal to version 2.1.0
# ======================================================================================
Expand Down
154 changes: 154 additions & 0 deletions src/optimagic/optimizers/iminuit_migrad.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,154 @@
from dataclasses import dataclass
from typing import Optional

import numpy as np
from numpy.typing import NDArray

from optimagic import mark
from optimagic.config import IS_IMINUIT_INSTALLED
from optimagic.optimization.algo_options import (
STOPPING_MAXFUN,
STOPPING_MAXITER,
)
from optimagic.optimization.algorithm import Algorithm, InternalOptimizeResult
from optimagic.optimization.internal_optimization_problem import (
InternalOptimizationProblem,
)
from optimagic.typing import AggregationLevel

if IS_IMINUIT_INSTALLED:
from iminuit import Minuit


@mark.minimizer(
name="iminuit_migrad",
solver_type=AggregationLevel.SCALAR,
is_available=IS_IMINUIT_INSTALLED,
is_global=False,
needs_jac=True,
needs_hess=False,
supports_parallelism=False,
supports_bounds=True,
supports_linear_constraints=False,
supports_nonlinear_constraints=False,
disable_history=False,
)
@dataclass(frozen=True)
class IminuitMigrad(Algorithm):
stopping_maxfun: int = STOPPING_MAXFUN
stopping_maxiter: int = STOPPING_MAXITER

def _solve_internal_problem(
self, problem: InternalOptimizationProblem, params: NDArray[np.float64]
) -> InternalOptimizeResult:
def wrapped_objective(x: NDArray[np.float64]) -> float:
return float(problem.fun(x))

m = Minuit(wrapped_objective, params, grad=problem.jac)

bounds = _convert_bounds_to_minuit_limits(
problem.bounds.lower, problem.bounds.upper
)
_set_minuit_limits(m, bounds)

m.migrad(
ncall=self.stopping_maxfun,
iterate=self.stopping_maxiter, # review
)

res = _process_minuit_result(m)
return res


def _process_minuit_result(minuit_result: Minuit) -> InternalOptimizeResult:
"""Convert iminuit result to Optimagic's internal result format."""

res = InternalOptimizeResult(
x=np.array(minuit_result.values),
fun=minuit_result.fval,
success=minuit_result.valid,
message=repr(minuit_result.fmin),
n_fun_evals=minuit_result.nfcn,
n_jac_evals=minuit_result.ngrad,
n_hess_evals=None,
n_iterations=minuit_result.nfcn,
status=None,
jac=None,
hess=None,
hess_inv=np.array(minuit_result.covariance),
max_constraint_violation=None,
info=None,
history=None,
)
return res


def _convert_bounds_to_minuit_limits(
Comment thread
spline2hg marked this conversation as resolved.
lower_bounds: Optional[NDArray[np.float64]],
upper_bounds: Optional[NDArray[np.float64]],
) -> list[tuple[Optional[float], Optional[float]]]:
"""Convert optimization bounds to Minuit-compatible limit format.

Transforms numpy arrays of bounds into List of tuples as expected by iminuit.
Handles special values like np.inf, -np.inf, and np.nan by converting
them to None where appropriate, as required by Minuit's limits API.

Parameters
----------
lower_bounds : Optional[NDArray[np.float64]]
Array of lower bounds for parameters.
upper_bounds : Optional[NDArray[np.float64]]
Array of upper bounds for parameters.

Returns
-------
list[tuple[Optional[float], Optional[float]]]
List of (lower, upper) limit tuples in Minuit format, where:
- None indicates unbounded (equivalent to infinity)
- Float values represent actual bounds

Notes
-----
Minuit expects bounds as tuples of (lower, upper) where:
- `None` indicates no bound (equivalent to -inf or +inf)
- A finite float value indicates a specific bound
- Bounds can be asymmetric (e.g., one side bounded, one side not)

"""
if lower_bounds is None or upper_bounds is None:
return []

return [
(
None if np.isneginf(lower) or np.isnan(lower) else float(lower),
None if np.isposinf(upper) or np.isnan(upper) else float(upper),
)
for lower, upper in zip(lower_bounds, upper_bounds, strict=True)
]


def _set_minuit_limits(
m: Minuit, bounds: list[tuple[Optional[float], Optional[float]]]
) -> None:
"""Set parameter limits on a Minuit minimizer instance.

Applies the converted bounds to an iminuit.Minuit object. Minuit expects
parameter limits as tuples of (lower, upper) for each parameter, where
None indicates an unbounded direction.

Parameters
----------
m : Minuit
The iminuit minimizer instance to configure.
bounds : list[tuple[Optional[float], Optional[float]]]
List of parameter bounds as (lower, upper) tuples in Minuit format.
For each tuple:
- (None, None): Fully unbounded parameter
- (value, None): Lower bound only
- (None, value): Upper bound only
- (min, max): Two-sided constraint

"""
for i, (lower, upper) in enumerate(bounds):
if lower is not None or upper is not None:
m.limits[i] = (lower, upper)
Comment thread
spline2hg marked this conversation as resolved.
Outdated