Skip to content

Commit a982e83

Browse files
authored
Merge pull request #126 from BuildingEnergySimulationTools/optimize-minimize
✨ Minimize added to SciOptimizer
2 parents c20f15a + b47b199 commit a982e83

4 files changed

Lines changed: 278 additions & 33 deletions

File tree

corrai/base/model.py

Lines changed: 107 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,11 +1,11 @@
11
import datetime as dt
2+
from abc import ABC, abstractmethod
3+
from pathlib import Path
24
from typing import Union
35

4-
import pandas as pd
56
import numpy as np
6-
7-
from abc import ABC, abstractmethod
8-
from pathlib import Path
7+
import pandas as pd
8+
from scipy.optimize import rosen
99

1010
from corrai.base.parameter import Parameter
1111

@@ -257,6 +257,109 @@ def simulate(
257257
)
258258

259259

260+
class RosenFiveParamDynamic(PyModel):
261+
"""
262+
Example implementation of the Ishigami function.
263+
264+
The Ishigami function is a standard benchmark for sensitivity analysis:
265+
f(x) = sin(x1) + 7 sin^2(x2) + 0.1 x3^4 sin(x1)
266+
267+
Attributes
268+
----------
269+
x1, x2, x3 : float
270+
Model parameters controlling the output.
271+
272+
Methods
273+
-------
274+
get_property_values(property_list)
275+
Retrieve current values of x1, x2, x3.
276+
set_property_values(property_dict)
277+
Set properties from a dictionary.
278+
simulate(property_dict, simulation_options, simulation_kwargs)
279+
Evaluate the Ishigami function and return as a time series DataFrame.
280+
"""
281+
282+
def __init__(self):
283+
super().__init__(is_dynamic=True)
284+
self.x1 = 1
285+
self.x2 = 2
286+
self.x3 = 3
287+
self.x4 = 4
288+
self.x5 = 5
289+
290+
def simulate(
291+
self,
292+
property_dict: dict[str, str | int | float] = None,
293+
simulation_options: dict = None,
294+
simulation_kwargs: dict = None,
295+
) -> pd.DataFrame:
296+
if property_dict is not None:
297+
self.set_property_values(property_dict)
298+
299+
res = rosen([self.x1, self.x2, self.x3, self.x4, self.x5])
300+
301+
return pd.DataFrame(
302+
{"res": [res]},
303+
index=pd.date_range(
304+
simulation_options["start"],
305+
simulation_options["end"],
306+
freq=simulation_options["timestep"],
307+
),
308+
)
309+
310+
311+
class RosenFiveParam(PyModel):
312+
"""
313+
Five-dimensional Rosenbrock benchmark model.
314+
315+
This class implements the Rosenbrock function, a standard benchmark
316+
in numerical optimization, defined as:
317+
318+
f(x) = sum_{i=1}^{4} [100 (x_{i+1} - x_i^2)^2 + (1 - x_i)^2]
319+
320+
for x = (x1, x2, x3, x4, x5).
321+
322+
The global minimum is located at:
323+
324+
x* = (1, 1, 1, 1, 1), f(x*) = 0
325+
326+
Attributes
327+
----------
328+
x1, x2, x3, x4, x5 : float
329+
Model parameters defining the input vector.
330+
331+
Methods
332+
-------
333+
get_property_values(property_list)
334+
Retrieve current values of model parameters.
335+
set_property_values(property_dict)
336+
Set model parameters from a dictionary.
337+
simulate(property_dict, simulation_options, simulation_kwargs)
338+
Evaluate the Rosenbrock function and return the result.
339+
"""
340+
341+
def __init__(self):
342+
super().__init__(is_dynamic=False)
343+
self.x1 = 1
344+
self.x2 = 2
345+
self.x3 = 3
346+
self.x4 = 4
347+
self.x5 = 5
348+
349+
def simulate(
350+
self,
351+
property_dict: dict[str, str | int | float] = None,
352+
simulation_options: dict = None,
353+
simulation_kwargs: dict = None,
354+
) -> pd.Series:
355+
if property_dict is not None:
356+
self.set_property_values(property_dict)
357+
358+
res = rosen([self.x1, self.x2, self.x3, self.x4, self.x5])
359+
360+
return pd.Series({"res": res})
361+
362+
260363
class Ishigami(PyModel):
261364
"""
262365
Example implementation of the Ishigami function.

corrai/optimize.py

Lines changed: 114 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -3,17 +3,15 @@
33

44
import numpy as np
55
import pandas as pd
6-
7-
from scipy.optimize import differential_evolution, minimize_scalar
8-
96
from pymoo.core.problem import ElementwiseProblem
10-
from pymoo.core.variable import Integer, Real, Choice, Binary
7+
from pymoo.core.variable import Binary, Choice, Integer, Real
8+
from scipy.optimize import differential_evolution, minimize_scalar, minimize
119

1210
from corrai.base.math import METHODS
1311
from corrai.base.model import Model
12+
from corrai.base.parameter import Parameter
1413
from corrai.base.utils import check_indicators_configs
1514
from corrai.sampling import Sample, SampleMethodsMixin
16-
from corrai.base.parameter import Parameter
1715

1816

1917
def check_duplicate_params(params: list["Parameter"]) -> None:
@@ -724,6 +722,117 @@ def scalar_minimize(
724722
options=options,
725723
)
726724

725+
def minimize(
726+
self,
727+
indicator_config: str
728+
| tuple[str, str | Callable]
729+
| tuple[str, str | Callable, pd.Series],
730+
simulation_options: dict = None,
731+
simulation_kwargs=None,
732+
x0: list[float] = None,
733+
method=None,
734+
jac=None,
735+
hess=None,
736+
hessp=None,
737+
bounds=None,
738+
constraints=(),
739+
tol=None,
740+
callback=None,
741+
options=None,
742+
):
743+
"""
744+
This method wraps `scipy.optimize.minimize`
745+
746+
Parameters
747+
----------
748+
indicator_config : str or tuple
749+
Indicator configuration passed to `ModelEvaluator.scipy_obj_function`:
750+
- If the model is **static**: a string representing the indicator name.
751+
- If the model is **dynamic**: a tuple of the form
752+
(indicator, func) or (indicator, func, reference) where:
753+
* indicator : str
754+
Indicator name in the simulation results.
755+
* func : str or Callable
756+
Aggregation function (method name registered in `METHODS`
757+
or a Python callable).
758+
* reference : optional
759+
Reference time series if the aggregation function is an
760+
error metric such as nmbe, cv_rmse, or mean_squared_error.
761+
762+
simulation_options : dict, optional
763+
Options for the simulation (e.g., stop time, solver settings).
764+
765+
simulation_kwargs : dict, optional
766+
Additional keyword arguments for simulation.
767+
768+
x0 : list of float, optional
769+
Initial guess for the optimization variables.
770+
If None, the initial values are set to the mean of each
771+
parameter interval.
772+
773+
method : str or callable, optional
774+
Optimization method to use (e.g., 'BFGS', 'L-BFGS-B', 'SLSQP').
775+
Passed directly to `scipy.optimize.minimize`.
776+
777+
jac : callable or bool, optional
778+
Function computing the gradient of the objective, or a boolean
779+
indicating whether the objective returns the gradient.
780+
781+
hess : callable, optional
782+
Function computing the Hessian matrix of the objective.
783+
784+
hessp : callable, optional
785+
Function computing the Hessian-vector product.
786+
787+
bounds : sequence, optional
788+
Bounds on variables for bounded optimization methods.
789+
790+
constraints : sequence, optional
791+
Constraints definition for constrained optimization.
792+
793+
tol : float, optional
794+
Tolerance for convergence.
795+
796+
callback : callable, optional
797+
Function called after each iteration.
798+
799+
options : dict, optional
800+
Additional solver-specific options.
801+
802+
Returns
803+
-------
804+
scipy.optimize.OptimizeResult
805+
Result of the optimization. Accessible also via the `result`
806+
attribute.
807+
808+
Notes
809+
-----
810+
This method relies on `scipy.optimize.minimize`, which implements
811+
gradient-based and derivative-free local optimization algorithms.
812+
It is best suited for smooth problems and may converge to a local
813+
minimum depending on the initial guess.
814+
815+
For global optimization, consider using `diff_evo_minimize`.
816+
"""
817+
818+
if x0 is None:
819+
x0 = [float(np.mean(par.interval)) for par in self.parameters]
820+
821+
return minimize(
822+
self.model_evaluator.scipy_obj_function,
823+
x0,
824+
args=(indicator_config, simulation_options, simulation_kwargs),
825+
method=method,
826+
jac=jac,
827+
hess=hess,
828+
hessp=hessp,
829+
bounds=bounds,
830+
constraints=constraints,
831+
tol=tol,
832+
callback=callback,
833+
options=options,
834+
)
835+
727836
def diff_evo_minimize(
728837
self,
729838
indicator_config: str

pyproject.toml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -34,7 +34,7 @@ dependencies = [
3434
"fmpy>=0.3.6",
3535
"matplotlib>=3.5.1",
3636
"plotly>=5.3.1",
37-
"fastprogress>=1.0.3",
37+
"fastprogress<1.0.4",
3838
]
3939

4040
[build-system]

tests/test_optimize.py

Lines changed: 56 additions & 23 deletions
Original file line numberDiff line numberDiff line change
@@ -1,26 +1,31 @@
1-
import pandas as pd
2-
import numpy as np
31
from pathlib import Path
4-
import pytest
5-
6-
from corrai.optimize import (
7-
MixedProblem,
8-
RealContinuousProblem,
9-
PymooModelEvaluator,
10-
check_duplicate_params,
11-
ModelEvaluator,
12-
SciOptimizer,
13-
)
14-
from corrai.base.parameter import Parameter
15-
from corrai.base.model import Ishigami, IshigamiDynamic, PyModel
162

3+
import numpy as np
4+
import pandas as pd
5+
import pytest
176
from pymoo.algorithms.moo.nsga2 import NSGA2
187
from pymoo.algorithms.soo.nonconvex.de import DE
8+
from pymoo.core.mixed import MixedVariableGA
199
from pymoo.operators.sampling.lhs import LHS
2010
from pymoo.optimize import minimize
21-
from pymoo.core.mixed import MixedVariableGA
2211
from pymoo.termination import get_termination
2312

13+
from corrai.base.model import (
14+
Ishigami,
15+
IshigamiDynamic,
16+
PyModel,
17+
RosenFiveParam,
18+
RosenFiveParamDynamic,
19+
)
20+
from corrai.base.parameter import Parameter
21+
from corrai.optimize import (
22+
MixedProblem,
23+
ModelEvaluator,
24+
PymooModelEvaluator,
25+
RealContinuousProblem,
26+
SciOptimizer,
27+
check_duplicate_params,
28+
)
2429

2530
PACKAGE_DIR = Path(__file__).parent / "TestLib"
2631

@@ -291,6 +296,9 @@ def test_model_evaluator(self):
291296

292297
class TestSciOptimizer:
293298
def test_sci_optimizer(self):
299+
########################
300+
# Differential Evolution
301+
########################
294302
param_list = [
295303
Parameter(
296304
"par_x1",
@@ -305,9 +313,8 @@ def test_sci_optimizer(self):
305313
),
306314
]
307315
# Dynamic optimization
308-
sci_opt = SciOptimizer(parameters=param_list, model=IshigamiDynamic())
309-
310-
opt_res = sci_opt.diff_evo_minimize(
316+
sci_opt_dyn = SciOptimizer(parameters=param_list, model=IshigamiDynamic())
317+
opt_res = sci_opt_dyn.diff_evo_minimize(
311318
indicator_config=("res", "mean"),
312319
simulation_options={
313320
"start": "2009-01-01 00:00:00",
@@ -316,20 +323,46 @@ def test_sci_optimizer(self):
316323
},
317324
rng=42,
318325
)
319-
320326
assert round(opt_res.fun, 4) == -10.7409
321327

322328
# Static optimization
323-
sci_opt = SciOptimizer(parameters=param_list, model=Ishigami())
324-
325-
opt_res = sci_opt.diff_evo_minimize(
329+
sci_opt_stat = SciOptimizer(parameters=param_list, model=Ishigami())
330+
opt_res = sci_opt_stat.diff_evo_minimize(
326331
indicator_config="res",
327332
rng=42,
328333
)
329-
330334
assert round(opt_res.fun, 4) == -10.7409
331335

336+
##########
337+
# minimize
338+
##########
339+
param_list = [
340+
Parameter(f"x{i}", (-100, 100), model_property=f"x{i}") for i in range(1, 6)
341+
]
342+
x_expected = np.array([1, 1, 1, 1, 1])
343+
344+
# Dynamic optimization
345+
sci_opt_dyn = SciOptimizer(parameters=param_list, model=RosenFiveParamDynamic())
346+
opt_res = sci_opt_dyn.minimize(
347+
indicator_config=("res", "mean"),
348+
simulation_options={
349+
"start": "2009-01-01 00:00:00",
350+
"end": "2009-01-01 00:00:00",
351+
"timestep": "h",
352+
},
353+
)
354+
assert np.allclose(opt_res.x, x_expected, atol=1e-5)
355+
356+
# Static optimization
357+
sci_opt_stat = SciOptimizer(parameters=param_list, model=RosenFiveParam())
358+
opt_res = sci_opt_stat.minimize(
359+
indicator_config="res",
360+
)
361+
assert np.allclose(opt_res.x, x_expected, atol=1e-5)
362+
363+
#####################
332364
# Scalar optimization
365+
#####################
333366
parameter = Parameter("x_param", interval=(-10, 10), model_property="x")
334367

335368
sci_opt = SciOptimizer(parameters=[parameter], model=X2())

0 commit comments

Comments
 (0)