From 0787883b32ea6df9a345453f6502471b38a6a2f1 Mon Sep 17 00:00:00 2001 From: Florian Felten Date: Wed, 27 Aug 2025 15:43:29 +0200 Subject: [PATCH 1/7] User has to call reset explicitly --- docs/introduction/basic_usage.md | 2 ++ engibench/core.py | 19 ++++++++++++++++--- engibench/problems/.DS_Store | Bin 0 -> 6148 bytes engibench/problems/airfoil/v0.py | 4 ++++ engibench/problems/beams2d/v0.py | 5 +++++ engibench/problems/heatconduction2d/v0.py | 4 ++++ engibench/problems/heatconduction3d/v0.py | 4 ++++ engibench/problems/photonics2d/v0.py | 4 ++++ engibench/problems/power_electronics/v0.py | 2 ++ engibench/problems/thermoelastic2d/v0.py | 4 ++++ tests/test_problem_implementations.py | 1 + 11 files changed, 46 insertions(+), 3 deletions(-) create mode 100644 engibench/problems/.DS_Store diff --git a/docs/introduction/basic_usage.md b/docs/introduction/basic_usage.md index 536bbeb9..984da29f 100644 --- a/docs/introduction/basic_usage.md +++ b/docs/introduction/basic_usage.md @@ -7,6 +7,7 @@ from engibench.problems.beams2d.v0 import Beams2D # Create a problem problem = Beams2D() +problem.reset(seed=0) # Inspect problem problem.design_space # Box(0.0, 1.0, (50, 100), float64) @@ -33,6 +34,7 @@ violated_constraints = problem.check_constraints(generated_design, desired_conds if not violated_constraints: # Only simulate to get objective values objs = problem.simulate(design=generated_design, config=desired_conds) + problem.reset(seed=42) # Or run a gradient-based optimizer to polish the generate design opt_design, history = problem.optimize(starting_point=generated_design, config=desired_conds) ``` diff --git a/engibench/core.py b/engibench/core.py index 9ae2ea50..2bb13893 100644 --- a/engibench/core.py +++ b/engibench/core.py @@ -4,7 +4,7 @@ import dataclasses from enum import auto from enum import Enum -from typing import Any, Generic, TypeVar +from typing import Any, Callable, Generic, TypeVar from datasets import Dataset from datasets import load_dataset @@ -45,7 +45,7 @@ class Problem(Generic[DesignType]): - :meth:`check_constraints` - to check if a design and conditions violate any constraints. - :meth:`simulate` - to simulate a design and return the performance given some conditions. - :meth:`optimize` - to optimize a design starting from a given point, e.g., using adjoint solver included inside the simulator. - - :meth:`reset` - to reset the simulator and numpy random to a given seed. + - :meth:`reset` - to reset the simulator and numpy random to a given seed. Should be called before each call to `simulate` or `optimize`. - :meth:`render` - to render a design in a human-readable format. - :meth:`random_design` - to generate a valid random design. @@ -105,7 +105,7 @@ def __init__(self, **kwargs: Any) -> None: Args: **kwargs: Keyword arguments. """ - self.reset(**kwargs) + self.reset_called = False @property def dataset(self) -> Dataset: @@ -124,6 +124,18 @@ def conditions_keys(self) -> list[str]: """Returns the condition names as a list.""" return [f.name for f in dataclasses.fields(self.conditions)] + def _check_reset_called(self, func_name: str, *, toggle: bool = True) -> None: + """Check if reset() has been called before calling func(). + + Args: + func_name (str): The name of the function to check. + toggle (bool): Whether to toggle the reset_called attribute. + """ + if not self.reset_called: + raise RuntimeError(f"reset() must be called before each {func_name}()") + if toggle: + self.reset_called = False + def simulate(self, design: DesignType, config: dict[str, Any] | None = None) -> npt.NDArray: r"""Launch a simulation on the given design and return the performance. @@ -159,6 +171,7 @@ def reset(self, seed: int | None = None) -> None: Args: seed (int, optional): The seed to reset to. If None, a random seed is used. """ + self.reset_called = True self.seed = seed self.np_random = np.random.default_rng(seed) diff --git a/engibench/problems/.DS_Store b/engibench/problems/.DS_Store new file mode 100644 index 0000000000000000000000000000000000000000..25c27827c31f547f65aad43f7ce989784931a1ed GIT binary patch literal 6148 zcmeHK%}T>S5T0$TO({YS3gRi?wP61c#Y?F51&ruHr6x_W!I&*=Y7V842hbPtL3|!( zcDG8Y;zdNt49tEzvopKQw_!I60HQMu8vr%{Sg3@B3N~K|jgziO!FULTnxhXA5(r@m z{gr5T{6_|8?`#;t0DMSs*FN_zg-BT~2!m`g3St>x>}xQJqe)t?zllPzw6R&X%2w67 z^@cL@Qa_zeI{x^YtP3gQVA1!3%V?1G?Cn#Tq<)kP#wsBS2N-g56(ym}I&zwX6P4@f z0jpwFdiLII);w<2Yfhta)SlOz*>Mw%cI$9HuUI?#2Pfyfhxjp(&zfF=U!syNg9~`U z&X1KodE+FO$vws{=2_%3G6T#2Gq64km=n&bu1`r^2{XV9{1O9nJ~*g^uEAWRIy$hS z-$xoR5t3k^-V%h4LDyie5l2vjPDRwI!aOmAPDj6E;#`BdMx72qt&H=Sm4$hs2(>!; z9Tg73)yOR~zzi%iP}HVP_y5`V@Bif_wwM8CV67MsrLNcQU`gg~T`7+4S_kzGm4xDQ njUOr4QCBg>(p9{Rss#NGbr4;HxkmJ$@Q;9^fg5JvPZ@Xz?m|+` literal 0 HcmV?d00001 diff --git a/engibench/problems/airfoil/v0.py b/engibench/problems/airfoil/v0.py index 2857ea6c..24d88374 100644 --- a/engibench/problems/airfoil/v0.py +++ b/engibench/problems/airfoil/v0.py @@ -390,6 +390,8 @@ def simulate(self, design: DesignType, config: dict[str, Any] | None = None, mpi Returns: dict: The performance of the design - each entry of the dict corresponds to a named objective value. """ + self._check_reset_called("simulate") + if isinstance(design["angle_of_attack"], np.ndarray): design["angle_of_attack"] = design["angle_of_attack"][0] @@ -458,6 +460,8 @@ def optimize( Returns: tuple[dict[str, Any], list[OptiStep]]: The optimized design and its performance. """ + self._check_reset_called("optimize") + if isinstance(starting_point["angle_of_attack"], np.ndarray): starting_point["angle_of_attack"] = starting_point["angle_of_attack"][0] diff --git a/engibench/problems/beams2d/v0.py b/engibench/problems/beams2d/v0.py index a5f9f4d7..2dbc51c2 100644 --- a/engibench/problems/beams2d/v0.py +++ b/engibench/problems/beams2d/v0.py @@ -193,6 +193,8 @@ def simulate( Returns: npt.NDArray: The performance of the design in terms of compliance. """ + self._check_reset_called("simulate") + # This condition is needed to convert user-provided designs (images) to flat arrays. Normally does not apply, i.e., during optimization. if len(design.shape) > 1: design = image_to_design(design) @@ -225,6 +227,8 @@ def optimize( Returns: Tuple[np.ndarray, dict]: The optimized design and its performance. """ + self._check_reset_called("optimize") + base_config = self.Config(**{**dataclasses.asdict(self.simulate_config), **(config or {})}) self.__st = State.new(base_config.nelx, base_config.nely, base_config.rmin, base_config.forcedist) @@ -252,6 +256,7 @@ def optimize( while change > self.__st.min_change and loop < base_config.max_iter: ce = calc_sensitivity(xPrint, st=self.__st, cfg=dataclasses.asdict(base_config)) simulate_config = upcast(base_config) + self.reset_called = True # override for multiple reset calls in optimize c = self.simulate(xPrint, ce=ce, config=dataclasses.asdict(simulate_config)) # Record the current state in optisteps_history diff --git a/engibench/problems/heatconduction2d/v0.py b/engibench/problems/heatconduction2d/v0.py index 8ace8677..1114acf7 100644 --- a/engibench/problems/heatconduction2d/v0.py +++ b/engibench/problems/heatconduction2d/v0.py @@ -131,6 +131,8 @@ def simulate(self, design: npt.NDArray | None = None, config: dict[str, Any] | N Returns: float: The thermal compliance of the design. """ + self._check_reset_called("simulate") + config = config or {} volume = config.get("volume", self.config.volume) length = config.get("length", self.config.length) @@ -169,6 +171,8 @@ def optimize( Returns: Tuple[OptimalDesign, list[OptiStep]]: The optimized design and the optimization history. """ + self._check_reset_called("optimize") + config = config or {} volume = config.get("volume", self.config.volume) length = config.get("length", self.config.length) diff --git a/engibench/problems/heatconduction3d/v0.py b/engibench/problems/heatconduction3d/v0.py index 2b7283c6..fca108c4 100644 --- a/engibench/problems/heatconduction3d/v0.py +++ b/engibench/problems/heatconduction3d/v0.py @@ -131,6 +131,8 @@ def simulate(self, design: npt.NDArray | None = None, config: dict[str, Any] | N Returns: float: The thermal compliance of the design. """ + self._check_reset_called("simulate") + config = config or {} volume = config.get("volume", self.config.volume) area = config.get("area", self.config.area) @@ -169,6 +171,8 @@ def optimize( Returns: Tuple[OptimalDesign, list[OptiStep]]: The optimized design and the optimization history. """ + self._check_reset_called("optimize") + config = config or {} volume = config.get("volume", self.config.volume) area = config.get("area", self.config.area) diff --git a/engibench/problems/photonics2d/v0.py b/engibench/problems/photonics2d/v0.py index d4ba64fd..4988bb30 100644 --- a/engibench/problems/photonics2d/v0.py +++ b/engibench/problems/photonics2d/v0.py @@ -338,6 +338,8 @@ def simulate(self, design: npt.NDArray, config: dict[str, Any] | None = None, ** Returns: npt.NDArray: 1-element array: [total_overlap - penalty], where higher is better. """ + self._check_reset_called("simulate") + conditions = self._setup_simulation(config) # --- Run Simulation --- @@ -385,6 +387,8 @@ def optimize( # noqa: PLR0915 value of the internally optimized objective (i.e., `total_overlap - penalty`). The `step` attribute corresponds to the optimizer iteration. """ + self._check_reset_called("optimize") + conditions = self._setup_simulation(config) # Reset the current beta to one for the optimization diff --git a/engibench/problems/power_electronics/v0.py b/engibench/problems/power_electronics/v0.py index 01fbc5e3..50065806 100644 --- a/engibench/problems/power_electronics/v0.py +++ b/engibench/problems/power_electronics/v0.py @@ -153,6 +153,8 @@ def simulate(self, design: npt.NDArray, config: dict[str, Any] | None = None) -> Returns: simulation_results: a numpy array containing the simulation results [DcGain, VoltageRipple, Efficiency]. """ + self._check_reset_called("simulate") + self.config, rewrite_netlist_str, edge_map, _ = parse_topology(self.config) self.config = process_sweep_data(config=self.config, sweep_data=design.tolist()) rewrite_netlist(self.config, rewrite_netlist_str, edge_map) diff --git a/engibench/problems/thermoelastic2d/v0.py b/engibench/problems/thermoelastic2d/v0.py index e94a16a9..19de84f7 100644 --- a/engibench/problems/thermoelastic2d/v0.py +++ b/engibench/problems/thermoelastic2d/v0.py @@ -164,6 +164,8 @@ def simulate(self, design: npt.NDArray, config: dict[str, Any] | None = None) -> Returns: dict: The performance of the design - each entry of the dict corresponds to a named objective value. """ + self._check_reset_called("simulate") + boundary_dict = dataclasses.asdict(self.conditions) for key, value in (config or {}).items(): if key in boundary_dict: @@ -187,6 +189,8 @@ def optimize( Returns: Tuple[np.ndarray, dict]: The optimized design and its performance. """ + self._check_reset_called("optimize") + boundary_dict = dataclasses.asdict(self.conditions) boundary_dict.update({k: v for k, v in (config or {}).items() if k in boundary_dict}) results = FeaModel(plot=False, eval_only=False).run(boundary_dict, x_init=starting_point) diff --git a/tests/test_problem_implementations.py b/tests/test_problem_implementations.py index 9d405d9a..d7ff868e 100644 --- a/tests/test_problem_implementations.py +++ b/tests/test_problem_implementations.py @@ -116,6 +116,7 @@ def test_python_problem_impl(problem_class: type[Problem]) -> None: if problem_class.__module__.startswith("engibench.problems.power_electronics"): print(f"Skipping optimization test for power electronics problem {problem_class.__name__}") return + problem.reset(seed=1) optimal_design, history = problem.optimize(starting_point=design) if isinstance(problem.design_space, spaces.Box): assert np.all(optimal_design >= problem.design_space.low), ( From 7429b99ecdcd642ebcb4a7ae6ed407c34c7c5e5e Mon Sep 17 00:00:00 2001 From: Florian Felten Date: Wed, 27 Aug 2025 15:45:56 +0200 Subject: [PATCH 2/7] ruff --- engibench/core.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/engibench/core.py b/engibench/core.py index 2bb13893..7e5e14df 100644 --- a/engibench/core.py +++ b/engibench/core.py @@ -4,7 +4,7 @@ import dataclasses from enum import auto from enum import Enum -from typing import Any, Callable, Generic, TypeVar +from typing import Any, Generic, TypeVar from datasets import Dataset from datasets import load_dataset From 9fd1bcf51c7de6b458abac5a0d74b4a534e8281f Mon Sep 17 00:00:00 2001 From: Florian Felten Date: Wed, 27 Aug 2025 15:50:30 +0200 Subject: [PATCH 3/7] Ruff --- engibench/core.py | 8 ++------ 1 file changed, 2 insertions(+), 6 deletions(-) diff --git a/engibench/core.py b/engibench/core.py index 7e5e14df..fa7347b7 100644 --- a/engibench/core.py +++ b/engibench/core.py @@ -99,12 +99,8 @@ class Problem(Generic[DesignType]): # This handles the RNG properly np_random: np.random.Generator - def __init__(self, **kwargs: Any) -> None: - """Initialize the problem. - - Args: - **kwargs: Keyword arguments. - """ + def __init__(self) -> None: + """Initialize the problem.""" self.reset_called = False @property From 1f431e30fcb52f6c13eb8b96d03b28e65fef31b2 Mon Sep 17 00:00:00 2001 From: Florian Felten Date: Wed, 27 Aug 2025 15:51:33 +0200 Subject: [PATCH 4/7] Remove DS Store --- engibench/problems/.DS_Store | Bin 6148 -> 0 bytes 1 file changed, 0 insertions(+), 0 deletions(-) delete mode 100644 engibench/problems/.DS_Store diff --git a/engibench/problems/.DS_Store b/engibench/problems/.DS_Store deleted file mode 100644 index 25c27827c31f547f65aad43f7ce989784931a1ed..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 6148 zcmeHK%}T>S5T0$TO({YS3gRi?wP61c#Y?F51&ruHr6x_W!I&*=Y7V842hbPtL3|!( zcDG8Y;zdNt49tEzvopKQw_!I60HQMu8vr%{Sg3@B3N~K|jgziO!FULTnxhXA5(r@m z{gr5T{6_|8?`#;t0DMSs*FN_zg-BT~2!m`g3St>x>}xQJqe)t?zllPzw6R&X%2w67 z^@cL@Qa_zeI{x^YtP3gQVA1!3%V?1G?Cn#Tq<)kP#wsBS2N-g56(ym}I&zwX6P4@f z0jpwFdiLII);w<2Yfhta)SlOz*>Mw%cI$9HuUI?#2Pfyfhxjp(&zfF=U!syNg9~`U z&X1KodE+FO$vws{=2_%3G6T#2Gq64km=n&bu1`r^2{XV9{1O9nJ~*g^uEAWRIy$hS z-$xoR5t3k^-V%h4LDyie5l2vjPDRwI!aOmAPDj6E;#`BdMx72qt&H=Sm4$hs2(>!; z9Tg73)yOR~zzi%iP}HVP_y5`V@Bif_wwM8CV67MsrLNcQU`gg~T`7+4S_kzGm4xDQ njUOr4QCBg>(p9{Rss#NGbr4;HxkmJ$@Q;9^fg5JvPZ@Xz?m|+` From e40098dd1e890277965884f79993a7ad4a6d2c65 Mon Sep 17 00:00:00 2001 From: Florian Felten Date: Tue, 2 Sep 2025 09:12:53 +0200 Subject: [PATCH 5/7] API 4 --- README.md | 3 +-- docs/introduction/basic_usage.md | 3 +-- engibench/core.py | 17 ++--------------- engibench/problems/airfoil/v0.py | 19 ++++++++----------- engibench/problems/beams2d/v0.py | 13 +++++-------- engibench/problems/heatconduction2d/v0.py | 12 ++++-------- engibench/problems/heatconduction3d/v0.py | 11 ++++------- engibench/problems/photonics2d/v0.py | 13 ++++++------- engibench/problems/power_electronics/v0.py | 11 ++++++----- engibench/problems/thermoelastic2d/v0.py | 15 +++++---------- tests/test_problem_implementations.py | 3 +-- 11 files changed, 43 insertions(+), 77 deletions(-) diff --git a/README.md b/README.md index 704da360..108c107f 100644 --- a/README.md +++ b/README.md @@ -45,8 +45,7 @@ pip install "engibench[all]" from engibench.problems.beams2d.v0 import Beams2D # Create a problem -problem = Beams2D() -problem.reset(seed=0) +problem = Beams2D(seed=0) # Inspect problem problem.design_space # Box(0.0, 1.0, (50, 100), float64) diff --git a/docs/introduction/basic_usage.md b/docs/introduction/basic_usage.md index 984da29f..f914343d 100644 --- a/docs/introduction/basic_usage.md +++ b/docs/introduction/basic_usage.md @@ -6,8 +6,7 @@ Our API is designed to be simple and easy to use. Here is a basic example of how from engibench.problems.beams2d.v0 import Beams2D # Create a problem -problem = Beams2D() -problem.reset(seed=0) +problem = Beams2D(seed=0) # Inspect problem problem.design_space # Box(0.0, 1.0, (50, 100), float64) diff --git a/engibench/core.py b/engibench/core.py index fa7347b7..d4164079 100644 --- a/engibench/core.py +++ b/engibench/core.py @@ -99,9 +99,9 @@ class Problem(Generic[DesignType]): # This handles the RNG properly np_random: np.random.Generator - def __init__(self) -> None: + def __init__(self, seed: int = 0) -> None: """Initialize the problem.""" - self.reset_called = False + self.reset(seed=seed) @property def dataset(self) -> Dataset: @@ -120,18 +120,6 @@ def conditions_keys(self) -> list[str]: """Returns the condition names as a list.""" return [f.name for f in dataclasses.fields(self.conditions)] - def _check_reset_called(self, func_name: str, *, toggle: bool = True) -> None: - """Check if reset() has been called before calling func(). - - Args: - func_name (str): The name of the function to check. - toggle (bool): Whether to toggle the reset_called attribute. - """ - if not self.reset_called: - raise RuntimeError(f"reset() must be called before each {func_name}()") - if toggle: - self.reset_called = False - def simulate(self, design: DesignType, config: dict[str, Any] | None = None) -> npt.NDArray: r"""Launch a simulation on the given design and return the performance. @@ -167,7 +155,6 @@ def reset(self, seed: int | None = None) -> None: Args: seed (int, optional): The seed to reset to. If None, a random seed is used. """ - self.reset_called = True self.seed = seed self.np_random = np.random.default_rng(seed) diff --git a/engibench/problems/airfoil/v0.py b/engibench/problems/airfoil/v0.py index 24d88374..c7ad3764 100644 --- a/engibench/problems/airfoil/v0.py +++ b/engibench/problems/airfoil/v0.py @@ -199,10 +199,11 @@ def area_ratio_bound(area_ratio_min: float, area_initial: float | None, area_inp f"Config.area_ratio: {area_ratio} ∉ [area_ratio_min={area_ratio_min}, 1.2]" ) - def __init__(self, base_directory: str | None = None) -> None: + def __init__(self, seed: int = 0, base_directory: str | None = None) -> None: """Initializes the Airfoil problem. Args: + seed (int): The random seed for the problem. base_directory (str, optional): The base directory for the problem. If None, the current directory is selected. """ # This is used for intermediate files @@ -222,7 +223,7 @@ def __init__(self, base_directory: str | None = None) -> None: self.__docker_base_dir = "/home/mdolabuser/mount/engibench" self.__docker_target_dir = self.__docker_base_dir + "/engibench_studies/problems/airfoil" - super().__init__() + super().__init__(seed=seed) def reset(self, seed: int | None = None, *, cleanup: bool = False) -> None: """Resets the simulator and numpy random to a given seed. @@ -239,8 +240,6 @@ def reset(self, seed: int | None = None, *, cleanup: bool = False) -> None: self.__local_study_dir = self.__local_target_dir + "/" + self.current_study self.__docker_study_dir = self.__docker_target_dir + "/" + self.current_study - clone_dir(source_dir=self.__local_template_dir, target_dir=self.__local_study_dir) - def __design_to_simulator_input(self, design: DesignType, config: dict[str, Any], filename: str = "design") -> str: """Converts a design to a simulator input. @@ -252,6 +251,9 @@ def __design_to_simulator_input(self, design: DesignType, config: dict[str, Any] config (dict): A dictionary with configuration (e.g., boundary conditions) for the simulation. filename (str): The filename to save the design to. """ + # Creates the study directory + clone_dir(source_dir=self.__local_template_dir, target_dir=self.__local_study_dir) + tmp = os.path.join(self.__docker_study_dir, "tmp") # Calculate the off-the-wall distance @@ -390,8 +392,6 @@ def simulate(self, design: DesignType, config: dict[str, Any] | None = None, mpi Returns: dict: The performance of the design - each entry of the dict corresponds to a named objective value. """ - self._check_reset_called("simulate") - if isinstance(design["angle_of_attack"], np.ndarray): design["angle_of_attack"] = design["angle_of_attack"][0] @@ -460,8 +460,6 @@ def optimize( Returns: tuple[dict[str, Any], list[OptiStep]]: The optimized design and its performance. """ - self._check_reset_called("optimize") - if isinstance(starting_point["angle_of_attack"], np.ndarray): starting_point["angle_of_attack"] = starting_point["angle_of_attack"][0] @@ -614,8 +612,7 @@ def random_design(self, dataset_split: str = "train", design_key: str = "initial if __name__ == "__main__": # Initialize the problem - problem = Airfoil() - problem.reset(seed=0, cleanup=True) + problem = Airfoil(seed=0) # Retrieve the dataset dataset = problem.dataset @@ -630,7 +627,7 @@ def random_design(self, dataset_split: str = "train", design_key: str = "initial print(problem.simulate(design, config=config, mpicores=8)) # Cleanup the study directory; will delete the previous contents from simulate in this case - problem.reset(seed=0, cleanup=False) + problem.reset(seed=0, cleanup=True) # Get design and conditions from the dataset, render design opt_design, optisteps_history = problem.optimize(design, config=config, mpicores=8) diff --git a/engibench/problems/beams2d/v0.py b/engibench/problems/beams2d/v0.py index 2dbc51c2..5e2b47db 100644 --- a/engibench/problems/beams2d/v0.py +++ b/engibench/problems/beams2d/v0.py @@ -159,13 +159,14 @@ def rmin_bound(rmin: float, nelx: int, nely: int) -> None: dataset_id = f"IDEALLab/beams_2d_{Config.nely}_{Config.nelx}_v{version}" container_id = None - def __init__(self, config: dict[str, Any] | None = None): + def __init__(self, seed: int = 0, config: dict[str, Any] | None = None): """Initializes the Beams2D problem. Args: + seed (int): The random seed for the problem. config (dict): A dictionary with configuration (e.g., boundary conditions) for the simulation. """ - super().__init__() + super().__init__(seed=seed) # Replace the config with any new configs passed in self.config = self.Config(**(config or {})) @@ -193,8 +194,6 @@ def simulate( Returns: npt.NDArray: The performance of the design in terms of compliance. """ - self._check_reset_called("simulate") - # This condition is needed to convert user-provided designs (images) to flat arrays. Normally does not apply, i.e., during optimization. if len(design.shape) > 1: design = image_to_design(design) @@ -227,8 +226,6 @@ def optimize( Returns: Tuple[np.ndarray, dict]: The optimized design and its performance. """ - self._check_reset_called("optimize") - base_config = self.Config(**{**dataclasses.asdict(self.simulate_config), **(config or {})}) self.__st = State.new(base_config.nelx, base_config.nely, base_config.rmin, base_config.forcedist) @@ -336,8 +333,7 @@ def random_design(self, dataset_split: str = "train", design_key: str = "optimal # Possible sets of nely and nelx: (25, 50), (50, 100), and (100, 200) # If a new nely and nelx are not passed in, uses the default conditions. - problem = Beams2D() - problem.reset(seed=0) + problem = Beams2D(seed=0) print(f"Loading dataset for nely={problem.nely}, nelx={problem.nelx}.") dataset = problem.dataset @@ -365,6 +361,7 @@ def random_design(self, dataset_split: str = "train", design_key: str = "optimal # Sample Optimization print("\nNow conducting a sample optimization with the given configs:", config) + problem.reset(seed=1) # NOTE: optimal_design and optisteps_history[-1].stored_design are interchangeable. optimal_design, optisteps_history = problem.optimize(config=config) diff --git a/engibench/problems/heatconduction2d/v0.py b/engibench/problems/heatconduction2d/v0.py index 1114acf7..dcb798b5 100644 --- a/engibench/problems/heatconduction2d/v0.py +++ b/engibench/problems/heatconduction2d/v0.py @@ -109,13 +109,14 @@ class Config(Conditions): dataset_id = "IDEALLab/heat_conduction_2d_v0" container_id = "quay.io/dolfinadjoint/pyadjoint:master" - def __init__(self, **kwargs: Any) -> None: + def __init__(self, seed: int = 0, **kwargs: Any) -> None: """Initialize the HeatConduction2D problem. Args: + seed (int): The random seed for the problem. kwargs: Arguments are passed to :class:`HeatConduction2D.Config`. """ - super().__init__() + super().__init__(seed=seed) self.config = self.Config(**kwargs) resolution = self.config.resolution self.conditions = self.Conditions(self.config.volume, self.config.length) @@ -131,8 +132,6 @@ def simulate(self, design: npt.NDArray | None = None, config: dict[str, Any] | N Returns: float: The thermal compliance of the design. """ - self._check_reset_called("simulate") - config = config or {} volume = config.get("volume", self.config.volume) length = config.get("length", self.config.length) @@ -171,8 +170,6 @@ def optimize( Returns: Tuple[OptimalDesign, list[OptiStep]]: The optimized design and the optimization history. """ - self._check_reset_called("optimize") - config = config or {} volume = config.get("volume", self.config.volume) length = config.get("length", self.config.length) @@ -287,8 +284,7 @@ def render(self, design: npt.NDArray, *, open_window: bool = False) -> Any: # Check if the script is run directly if __name__ == "__main__": # Create a HeatConduction2D problem instance - problem = HeatConduction2D() - problem.reset(seed=0) + problem = HeatConduction2D(seed=0) design_as_list = problem.dataset["train"]["optimal_design"][0] design_as_array = np.array(design_as_list) des, traj = problem.optimize(starting_point=design_as_array) diff --git a/engibench/problems/heatconduction3d/v0.py b/engibench/problems/heatconduction3d/v0.py index fca108c4..0bcd0966 100644 --- a/engibench/problems/heatconduction3d/v0.py +++ b/engibench/problems/heatconduction3d/v0.py @@ -109,13 +109,14 @@ class Config(Conditions): dataset_id = "IDEALLab/heat_conduction_3d_v0" container_id = "quay.io/dolfinadjoint/pyadjoint:master" - def __init__(self, **kwargs) -> None: + def __init__(self, seed: int = 0, **kwargs) -> None: """Initialize the HeatConduction3D problem. Args: + seed (int): The random seed for the problem. kwargs: Arguments are passed to :class:`HeatConduction3D.Config`. """ - super().__init__() + super().__init__(seed=seed) self.config = self.Config(**kwargs) resolution = self.config.resolution self.conditions = self.Conditions(self.config.volume, self.config.area) @@ -131,8 +132,6 @@ def simulate(self, design: npt.NDArray | None = None, config: dict[str, Any] | N Returns: float: The thermal compliance of the design. """ - self._check_reset_called("simulate") - config = config or {} volume = config.get("volume", self.config.volume) area = config.get("area", self.config.area) @@ -171,8 +170,6 @@ def optimize( Returns: Tuple[OptimalDesign, list[OptiStep]]: The optimized design and the optimization history. """ - self._check_reset_called("optimize") - config = config or {} volume = config.get("volume", self.config.volume) area = config.get("area", self.config.area) @@ -313,7 +310,7 @@ def render(self, design: npt.NDArray, *, open_window: bool = False) -> Any: # Check if the script is run directly if __name__ == "__main__": # Create a HeatConduction3D problem instance - problem = HeatConduction3D() + problem = HeatConduction3D(seed=0) design_as_list = problem.dataset["train"]["optimal_design"][0] numpy_array = np.array(design_as_list) des, traj = problem.optimize(starting_point=numpy_array) diff --git a/engibench/problems/photonics2d/v0.py b/engibench/problems/photonics2d/v0.py index 4988bb30..374fa50a 100644 --- a/engibench/problems/photonics2d/v0.py +++ b/engibench/problems/photonics2d/v0.py @@ -231,6 +231,7 @@ class Config(Conditions): def __init__( self, + seed: int = 0, config: dict[str, Any] | None = None, num_elems_x: int = Config.num_elems_x, num_elems_y: int = Config.num_elems_y, @@ -239,12 +240,13 @@ def __init__( """Initializes the Photonics2D problem. Args: + seed (int): The random seed for the problem. config (dict): A dictionary with configuration (e.g., boundary conditions) for the simulation. num_elems_x (int): Number of grid cells in x (default: 120). num_elems_y (int): Number of grid cells in y (default: 120). **kwargs: Additional keyword arguments. """ - super().__init__(**kwargs) + super().__init__(seed=seed, **kwargs) # Replace the conditions with any new configs passed in self.config = self.Config(num_elems_x=num_elems_x, num_elems_y=num_elems_y, **(config or {})) @@ -338,8 +340,6 @@ def simulate(self, design: npt.NDArray, config: dict[str, Any] | None = None, ** Returns: npt.NDArray: 1-element array: [total_overlap - penalty], where higher is better. """ - self._check_reset_called("simulate") - conditions = self._setup_simulation(config) # --- Run Simulation --- @@ -387,8 +387,6 @@ def optimize( # noqa: PLR0915 value of the internally optimized objective (i.e., `total_overlap - penalty`). The `step` attribute corresponds to the optimizer iteration. """ - self._check_reset_called("optimize") - conditions = self._setup_simulation(config) # Reset the current beta to one for the optimization @@ -708,8 +706,7 @@ def reset(self, seed: int | None = None, **kwargs) -> None: "lambda2": 0.84, "blur_radius": 1, } - problem = Photonics2D(config=problem_config, num_elems_x=120, num_elems_y=120) - problem.reset(seed=42) # Use a seed + problem = Photonics2D(config=problem_config, num_elems_x=120, num_elems_y=120, seed=42) start_design, _ = problem.random_design(noise=0.1, blur=1) # Randomized design with noise fig_start = problem.render(start_design) @@ -723,6 +720,7 @@ def reset(self, seed: int | None = None, **kwargs) -> None: # Optimization Example # Advanced Usage: Modifying optimization parameters opt_config = {"num_optimization_steps": 200, "save_frame_interval": 2, "initial_beta": 1.0} + problem.reset(seed=42) print(f"Optimizing design with ({opt_config})...") # Optimize maximizes (overlap - penalty) optimized_design, opti_history = problem.optimize(start_design, config=opt_config) @@ -738,6 +736,7 @@ def reset(self, seed: int | None = None, **kwargs) -> None: print("Simulating the final optimized design...") # Simulate returns the raw objective = penalty - overlap1*overlap2 + problem.reset(seed=43) obj_opt_raw = problem.simulate(optimized_design) print(f"Optimized Raw Objective ({problem.objectives_keys[0]}): {obj_opt_raw[0]:.4f}") diff --git a/engibench/problems/power_electronics/v0.py b/engibench/problems/power_electronics/v0.py index 50065806..fb31655a 100644 --- a/engibench/problems/power_electronics/v0.py +++ b/engibench/problems/power_electronics/v0.py @@ -111,6 +111,7 @@ class Conditions: def __init__( self, + seed: int = 0, target_dir: str = os.getcwd(), original_netlist_path: str = os.path.join( os.path.dirname(os.path.abspath(__file__)), "data/5_4_3_6_10-dcdc_converter_1.net" @@ -121,13 +122,14 @@ def __init__( """Initializes the Power Electronics problem. Args: + seed (int): The random seed for the problem. target_dir: The target directory for the rewritten netlist, log and raw files. Default to os.getcwd(). original_netlist_path: The path to the original netlist file. Accepts both relative and absolute paths. bucket_id: The bucket ID for the netlist file. E.g. "5_4_3_6_10". mode: The mode for the simulation. Default to "control". mode = "batch" is for development. ngspice_path: The path to the ngspice executable for Windows. """ - super().__init__() + super().__init__(seed=seed) self.config = Config( target_dir=target_dir, @@ -153,8 +155,6 @@ def simulate(self, design: npt.NDArray, config: dict[str, Any] | None = None) -> Returns: simulation_results: a numpy array containing the simulation results [DcGain, VoltageRipple, Efficiency]. """ - self._check_reset_called("simulate") - self.config, rewrite_netlist_str, edge_map, _ = parse_topology(self.config) self.config = process_sweep_data(config=self.config, sweep_data=design.tolist()) rewrite_netlist(self.config, rewrite_netlist_str, edge_map) @@ -206,10 +206,10 @@ def reset(self, seed: int | None = None) -> None: if __name__ == "__main__": # Test with absolute path and a different bucket_id - problem = PowerElectronics(mode="batch") + problem = PowerElectronics(seed=0, mode="batch") # Initialize the problem with default values - problem = PowerElectronics() + problem = PowerElectronics(seed=0) # Manually add the sweep data sweep_data = [ @@ -264,6 +264,7 @@ def reset(self, seed: int | None = None) -> None: ] # Simulate the problem with the provided design variable + problem.reset(seed=0) simulation_results = problem.simulate(design=np.array(sweep_data)) print(simulation_results) # [-1.27858 -0.025081 0.7827396] diff --git a/engibench/problems/thermoelastic2d/v0.py b/engibench/problems/thermoelastic2d/v0.py index 19de84f7..14084ab1 100644 --- a/engibench/problems/thermoelastic2d/v0.py +++ b/engibench/problems/thermoelastic2d/v0.py @@ -137,14 +137,13 @@ def rmin_bound(rmin: float, nelx: int, nely: int) -> None: """Constraint for rmin ∈ (0.0, max{ nelx, nely }].""" assert 0.0 < rmin <= max(nelx, nely), f"Params.rmin: {rmin} ∉ (0, max(nelx, nely)]" - def __init__(self) -> None: + def __init__(self, seed: int = 0) -> None: """Initializes the thermoelastic2D problem. Args: - base_directory (str, optional): The base directory for the problem. If None, the current directory is selected. + seed (int): The random seed for the problem. """ - super().__init__() - self.seed = None + super().__init__(seed=seed) def reset(self, seed: int | None = None) -> None: """Resets the simulator and numpy random to a given seed. @@ -164,8 +163,6 @@ def simulate(self, design: npt.NDArray, config: dict[str, Any] | None = None) -> Returns: dict: The performance of the design - each entry of the dict corresponds to a named objective value. """ - self._check_reset_called("simulate") - boundary_dict = dataclasses.asdict(self.conditions) for key, value in (config or {}).items(): if key in boundary_dict: @@ -189,8 +186,6 @@ def optimize( Returns: Tuple[np.ndarray, dict]: The optimized design and its performance. """ - self._check_reset_called("optimize") - boundary_dict = dataclasses.asdict(self.conditions) boundary_dict.update({k: v for k, v in (config or {}).items() if k in boundary_dict}) results = FeaModel(plot=False, eval_only=False).run(boundary_dict, x_init=starting_point) @@ -231,8 +226,7 @@ def random_design(self) -> tuple[npt.NDArray, int]: if __name__ == "__main__": # --- Create a new problem - problem = ThermoElastic2D() - problem.reset() + problem = ThermoElastic2D(seed=0) # --- Load the problem dataset dataset = problem.dataset @@ -250,5 +244,6 @@ def random_design(self) -> tuple[npt.NDArray, int]: problem.render(design, open_window=True) # --- Evaluate a design --- + problem.reset(seed=0) design, _ = problem.random_design() print(problem.simulate(design)) diff --git a/tests/test_problem_implementations.py b/tests/test_problem_implementations.py index d7ff868e..f805ed6e 100644 --- a/tests/test_problem_implementations.py +++ b/tests/test_problem_implementations.py @@ -97,8 +97,7 @@ def test_python_problem_impl(problem_class: type[Problem]) -> None: """ print(f"Testing optimization and simulation for {problem_class.__name__}...") # Initialize problem and get a random design - problem = problem_class() - problem.reset(seed=1) + problem = problem_class(seed=1) design, _ = problem.random_design() # Test simulation outputs From 4e4c9b086c177ac2e2a579a2814e42cb82ae4078 Mon Sep 17 00:00:00 2001 From: Florian Felten Date: Tue, 2 Sep 2025 09:59:07 +0200 Subject: [PATCH 6/7] Add more prints to airfoil --- engibench/problems/airfoil/v0.py | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/engibench/problems/airfoil/v0.py b/engibench/problems/airfoil/v0.py index c7ad3764..2f9d0cc3 100644 --- a/engibench/problems/airfoil/v0.py +++ b/engibench/problems/airfoil/v0.py @@ -624,13 +624,15 @@ def random_design(self, dataset_split: str = "train", design_key: str = "initial config = dataset["train"].select_columns(problem.conditions_keys)[idx] # Simulate the design - print(problem.simulate(design, config=config, mpicores=8)) + print("Simulation results: ", problem.simulate(design, config=config, mpicores=8)) # Cleanup the study directory; will delete the previous contents from simulate in this case - problem.reset(seed=0, cleanup=True) + problem.reset(seed=1, cleanup=True) # Get design and conditions from the dataset, render design opt_design, optisteps_history = problem.optimize(design, config=config, mpicores=8) + print("Optimized design: ", opt_design) + print("Optimization history: ", optisteps_history) # Render the final optimized design problem.render(opt_design, open_window=False, save=True) From ce186a600114f3c52f0dbf4d59dfe86f16d53df9 Mon Sep 17 00:00:00 2001 From: Florian Felten Date: Tue, 2 Sep 2025 14:46:25 +0200 Subject: [PATCH 7/7] Update notebook --- tutorial.ipynb | 15 +++++++++------ 1 file changed, 9 insertions(+), 6 deletions(-) diff --git a/tutorial.ipynb b/tutorial.ipynb index 7c3dddee..46de1116 100644 --- a/tutorial.ipynb +++ b/tutorial.ipynb @@ -114,13 +114,12 @@ }, { "cell_type": "code", - "execution_count": 4, + "execution_count": null, "id": "44ed0ebe-b218-44ee-819b-7d211050b8c9", "metadata": {}, "outputs": [], "source": [ - "problem = Beams2D()\n", - "problem.reset(seed=9)" + "problem = Beams2D(seed=9)" ] }, { @@ -329,7 +328,7 @@ }, { "cell_type": "code", - "execution_count": 11, + "execution_count": null, "id": "34b42db8", "metadata": {}, "outputs": [ @@ -344,7 +343,10 @@ "source": [ "config = {\"volfrac\": 0.3, \"forcedist\": 0.5}\n", "violations = problem.check_constraints(my_design, config)\n", - "print(violations)" + "if violations:\n", + " print(\"Violations found\", violations)\n", + "else:\n", + " print(\"No violations found\")" ] }, { @@ -380,7 +382,7 @@ }, { "cell_type": "code", - "execution_count": 13, + "execution_count": null, "id": "b7562ff7-1cb1-4538-b726-f2df8e18d980", "metadata": {}, "outputs": [ @@ -396,6 +398,7 @@ } ], "source": [ + "problem.reset(seed=0)\n", "opt_design, opt_history = problem.optimize(my_design)\n", "opt_history[-1]" ]