Skip to content

Commit 2d7f732

Browse files
authored
Merge pull request #229 from simopt-admin/feature/example2
Feature/example2
2 parents f0ff911 + 767c5de commit 2d7f732

7 files changed

Lines changed: 185 additions & 40 deletions

File tree

pyproject.toml

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -34,11 +34,12 @@ classifiers = [
3434
]
3535
dependencies = [
3636
"boltons>=25.0.0",
37+
"colorama>=0.4.6",
3738
"cvxpy>=1.7.3",
3839
"joblib>=1.5.2",
3940
"matplotlib>=3.10.7",
4041
"mrg32k3a[rust]>=2.0.0",
41-
"numpy>=2.3.4",
42+
"numpy>=2.3.4,<2.4",
4243
"pandas>=2.3.3",
4344
"pillow>=12.0.0",
4445
"pydantic>=2.12.3",

scripts/generate_experiment_results.py

Lines changed: 12 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -5,13 +5,16 @@
55
from pathlib import Path
66

77
import zstandard as zstd
8+
from colorama import Fore, init
89

910
# Append the parent directory (simopt package) to the system path
1011
sys.path.append(str(Path(__file__).resolve().parent.parent))
1112

1213
import simopt.directory as directory
1314
from simopt.experiment_base import ProblemSolver, post_normalize
1415

16+
init(autoreset=True)
17+
1518
# Workaround for AutoAPI
1619
problem_directory = directory.problem_directory
1720
solver_directory = directory.solver_directory
@@ -24,6 +27,10 @@
2427
EXPECTED_RESULTS_DIR = HOME_DIR / "test" / "expected_results"
2528

2629

30+
def _color_text(text: str, color: str | int) -> str:
31+
return color + text # type: ignore
32+
33+
2734
# Based off the similar function in simopt/experiment_base.py
2835
def is_compatible(problem_name: str, solver_name: str) -> bool:
2936
"""Check if a solver is compatible with a problem.
@@ -100,6 +107,7 @@ def create_test(problem_name: str, solver_name: str) -> None:
100107

101108
def main() -> None:
102109
"""Create test cases for all compatible problem-solver pairs."""
110+
skip_problems = {"ERM-EXAMPLE-1"}
103111
# Create a list of compatible problem-solver pairs
104112
compatible_pairs = [
105113
(problem_name, solver_name)
@@ -123,7 +131,10 @@ def main() -> None:
123131
results_filename = f"{file_problem_name}_{file_solver_name}.pickle.zst"
124132
# If file exists, skip it
125133
if results_filename in existing_results:
126-
print(f"Test for {pair} already exists")
134+
print(_color_text(f"Test for {pair} already exists", Fore.GREEN))
135+
continue
136+
if problem_name in skip_problems:
137+
print(_color_text(f"Skipping test for {pair}", Fore.YELLOW))
127138
continue
128139
# If file doesn't exist, create it
129140
print(f"Creating test for {pair}")

simopt/bootstrap.py

Lines changed: 38 additions & 34 deletions
Original file line numberDiff line numberDiff line change
@@ -321,41 +321,45 @@ def functional_of_curves(
321321
for curves in solver_1_curves
322322
]
323323
),
324-
PlotType.DIFFERENCE_OF_CDF_SOLVABILITY: lambda: curve_utils.difference_of_curves( # noqa: E501
325-
curve_utils.mean_of_curves(
326-
[
327-
curve_utils.cdf_of_curves_crossing_times(
328-
curves, threshold=solve_tol
329-
)
330-
for curves in solver_1_curves
331-
]
332-
),
333-
curve_utils.mean_of_curves(
334-
[
335-
curve_utils.cdf_of_curves_crossing_times(
336-
curves, threshold=solve_tol
337-
)
338-
for curves in solver_2_curves # type: ignore
339-
]
340-
),
324+
PlotType.DIFFERENCE_OF_CDF_SOLVABILITY: lambda: (
325+
curve_utils.difference_of_curves(
326+
curve_utils.mean_of_curves(
327+
[
328+
curve_utils.cdf_of_curves_crossing_times(
329+
curves, threshold=solve_tol
330+
)
331+
for curves in solver_1_curves
332+
]
333+
),
334+
curve_utils.mean_of_curves(
335+
[
336+
curve_utils.cdf_of_curves_crossing_times(
337+
curves, threshold=solve_tol
338+
)
339+
for curves in solver_2_curves # type: ignore
340+
]
341+
),
342+
)
341343
),
342-
PlotType.DIFFERENCE_OF_QUANTILE_SOLVABILITY: lambda: curve_utils.difference_of_curves( # noqa: E501
343-
curve_utils.mean_of_curves(
344-
[
345-
curve_utils.quantile_cross_jump(
346-
curves, threshold=solve_tol, beta=beta
347-
)
348-
for curves in solver_1_curves
349-
]
350-
),
351-
curve_utils.mean_of_curves(
352-
[
353-
curve_utils.quantile_cross_jump(
354-
curves, threshold=solve_tol, beta=beta
355-
)
356-
for curves in solver_2_curves # type: ignore
357-
]
358-
),
344+
PlotType.DIFFERENCE_OF_QUANTILE_SOLVABILITY: lambda: (
345+
curve_utils.difference_of_curves(
346+
curve_utils.mean_of_curves(
347+
[
348+
curve_utils.quantile_cross_jump(
349+
curves, threshold=solve_tol, beta=beta
350+
)
351+
for curves in solver_1_curves
352+
]
353+
),
354+
curve_utils.mean_of_curves(
355+
[
356+
curve_utils.quantile_cross_jump(
357+
curves, threshold=solve_tol, beta=beta
358+
)
359+
for curves in solver_2_curves # type: ignore
360+
]
361+
),
362+
)
359363
),
360364
PlotType.MEAN_FEASIBILITY_PROGRESS: lambda: curve_utils.mean_of_curves(
361365
single_curves

simopt/experiment/run_solver.py

Lines changed: 7 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@
22

33
import logging
44
import time
5+
from copy import deepcopy
56

67
import pandas as pd
78
from joblib import Parallel, delayed
@@ -111,13 +112,17 @@ def run_solver(
111112
logging.info(f"Running solver {solver.name} on problem {problem.name}.")
112113
logging.debug("Starting macroreplications")
113114

115+
# TODO: Long-term fix is to make Solver/Problem immutable (stateless)
116+
# so macroreps can share instances safely without deepcopy.
114117
if n_jobs == 1:
115118
results: list[tuple] = [
116-
_run_mrep(solver, problem, i) for i in range(n_macroreps)
119+
_run_mrep(deepcopy(solver), deepcopy(problem), i)
120+
for i in range(n_macroreps)
117121
]
118122
else:
119123
results: list[tuple] = Parallel(n_jobs=n_jobs)(
120-
delayed(_run_mrep)(solver, problem, i) for i in range(n_macroreps)
124+
delayed(_run_mrep)(deepcopy(solver), deepcopy(problem), i)
125+
for i in range(n_macroreps)
121126
)
122127

123128
dfs = []

simopt/models/amusementpark.py

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -245,8 +245,9 @@ class AmusementParkMinDepartConfig(BaseModel):
245245
initial_solution: Annotated[
246246
tuple[int, ...],
247247
Field(
248-
default_factory=lambda: (PARK_CAPACITY - NUM_ATTRACTIONS + 1,)
249-
+ (1,) * (NUM_ATTRACTIONS - 1),
248+
default_factory=lambda: (
249+
(PARK_CAPACITY - NUM_ATTRACTIONS + 1,) + (1,) * (NUM_ATTRACTIONS - 1)
250+
),
250251
description="Initial solution from which solvers start.",
251252
),
252253
]

simopt/models/example.py

Lines changed: 123 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -167,3 +167,126 @@ def get_random_solution(self, rand_sol_rng: MRG32k3a) -> tuple: # noqa: D102
167167
factorized=False,
168168
)
169169
)
170+
171+
172+
class Example2ModelConfig(BaseModel):
173+
"""Configuration model for Example-2 simulation.
174+
175+
A model that is a deterministic quadratic function evaluated with noise.
176+
"""
177+
178+
x: Annotated[
179+
tuple[int, ...],
180+
Field(
181+
default=(0, 0, 0, 0),
182+
description="point to evaluate",
183+
),
184+
]
185+
186+
187+
class Example2ProblemConfig(BaseModel):
188+
"""Configuration model for Example-2 Problem.
189+
190+
Base class to implement simulation-optimization problems.
191+
"""
192+
193+
initial_solution: Annotated[
194+
tuple[int, ...],
195+
Field(
196+
default=(0, 0, 0, 0),
197+
description="initial solution",
198+
),
199+
]
200+
budget: Annotated[
201+
int,
202+
Field(
203+
default=1000,
204+
description="max # of replications for a solver to take",
205+
gt=0,
206+
json_schema_extra={"isDatafarmable": False},
207+
),
208+
]
209+
210+
211+
class Example2Model(Model):
212+
"""A model that is a deterministic quadratic function evaluated with noise."""
213+
214+
class_name_abbr: ClassVar[str] = "EXAMPLE-2-MODEL"
215+
class_name: ClassVar[str] = "Quadratic Function + Noise (Discrete)"
216+
config_class: ClassVar[type[BaseModel]] = Example2ModelConfig
217+
n_rngs: ClassVar[int] = 1
218+
n_responses: ClassVar[int] = 1
219+
220+
def __init__(self, fixed_factors: dict | None = None) -> None:
221+
"""Initialize the model.
222+
223+
Args:
224+
fixed_factors (dict | None): fixed factors of the model.
225+
If None, use default values.
226+
"""
227+
# Let the base class handle default arguments.
228+
super().__init__(fixed_factors)
229+
self.noise_model = Normal()
230+
231+
def before_replicate(self, rng_list: list[MRG32k3a]) -> None: # noqa: D102
232+
self.noise_model.set_rng(rng_list[0])
233+
234+
def replicate(self) -> tuple[dict, dict]:
235+
"""Evaluate a quadratic function f(x) with stochastic noise."""
236+
x = np.array(self.factors["x"])
237+
target = np.array([1, 2, 3, 4])
238+
fn_eval_at_x = np.sum((x - target) ** 2) + self.noise_model.random()
239+
240+
responses = {"est_f(x)": fn_eval_at_x}
241+
return responses, {}
242+
243+
244+
class Example2Problem(Problem):
245+
"""Discrete quadratic minimization example with noise."""
246+
247+
class_name_abbr: ClassVar[str] = "EXAMPLE-2"
248+
class_name: ClassVar[str] = "Min Quadratic Function + Noise (Discrete)"
249+
config_class: ClassVar[type[BaseModel]] = Example2ProblemConfig
250+
model_class: ClassVar[type[Model]] = Example2Model
251+
n_objectives: ClassVar[int] = 1
252+
n_stochastic_constraints: ClassVar[int] = 0
253+
minmax: ClassVar[tuple[int, ...]] = (-1,)
254+
constraint_type: ClassVar[ConstraintType] = ConstraintType.UNCONSTRAINED
255+
variable_type: ClassVar[VariableType] = VariableType.DISCRETE
256+
gradient_available: ClassVar[bool] = False
257+
model_default_factors: ClassVar[dict] = {}
258+
model_decision_factors: ClassVar[set[str]] = {"x"}
259+
260+
@property
261+
def optimal_value(self) -> float | None: # noqa: D102
262+
return 0.0
263+
264+
@property
265+
def optimal_solution(self) -> tuple | None: # noqa: D102
266+
return (1, 2, 3, 4)
267+
268+
@property
269+
def dim(self) -> int: # noqa: D102
270+
return 4
271+
272+
@property
273+
def lower_bounds(self) -> tuple: # noqa: D102
274+
return (-4,) * self.dim
275+
276+
@property
277+
def upper_bounds(self) -> tuple: # noqa: D102
278+
return (4,) * self.dim
279+
280+
def vector_to_factor_dict(self, vector: tuple) -> dict: # noqa: D102
281+
return {"x": vector[:]}
282+
283+
def factor_dict_to_vector(self, factor_dict: dict) -> tuple: # noqa: D102
284+
return tuple(factor_dict["x"])
285+
286+
def replicate(self, _x: tuple) -> RepResult: # noqa: D102
287+
responses, _ = self.model.replicate()
288+
objectives = [Objective(stochastic=responses["est_f(x)"])]
289+
return RepResult(objectives=objectives)
290+
291+
def get_random_solution(self, rand_sol_rng: MRG32k3a) -> tuple: # noqa: D102
292+
return tuple(rand_sol_rng.randint(-4, 4) for _ in range(self.dim))
25 KB
Binary file not shown.

0 commit comments

Comments
 (0)