Skip to content
Merged
Show file tree
Hide file tree
Changes from all 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
3 changes: 1 addition & 2 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand Down
3 changes: 2 additions & 1 deletion docs/introduction/basic_usage.md
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +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 = Beams2D(seed=0)

# Inspect problem
problem.design_space # Box(0.0, 1.0, (50, 100), float64)
Expand All @@ -33,6 +33,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)
```
Expand Down
12 changes: 4 additions & 8 deletions engibench/core.py
Original file line number Diff line number Diff line change
Expand Up @@ -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.

Expand Down Expand Up @@ -99,13 +99,9 @@ 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.
"""
self.reset(**kwargs)
def __init__(self, seed: int = 0) -> None:
"""Initialize the problem."""
self.reset(seed=seed)

@property
def dataset(self) -> Dataset:
Expand Down
19 changes: 11 additions & 8 deletions engibench/problems/airfoil/v0.py
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -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.
Expand All @@ -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.

Expand All @@ -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
Expand Down Expand Up @@ -610,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
Expand All @@ -623,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=False)
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)
10 changes: 6 additions & 4 deletions engibench/problems/beams2d/v0.py
Original file line number Diff line number Diff line change
Expand Up @@ -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 {}))
Expand Down Expand Up @@ -252,6 +253,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
Expand Down Expand Up @@ -331,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
Expand Down Expand Up @@ -360,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)
Expand Down
8 changes: 4 additions & 4 deletions engibench/problems/heatconduction2d/v0.py
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand Down Expand Up @@ -283,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)
Expand Down
7 changes: 4 additions & 3 deletions engibench/problems/heatconduction3d/v0.py
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand Down Expand Up @@ -309,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)
Expand Down
9 changes: 6 additions & 3 deletions engibench/problems/photonics2d/v0.py
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand All @@ -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 {}))
Expand Down Expand Up @@ -704,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)
Expand All @@ -719,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)
Expand All @@ -734,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}")

Expand Down
9 changes: 6 additions & 3 deletions engibench/problems/power_electronics/v0.py
Original file line number Diff line number Diff line change
Expand Up @@ -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"
Expand All @@ -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,
Expand Down Expand Up @@ -204,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 = [
Expand Down Expand Up @@ -262,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]

Expand Down
11 changes: 5 additions & 6 deletions engibench/problems/thermoelastic2d/v0.py
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Expand Down Expand Up @@ -227,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
Expand All @@ -246,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))
4 changes: 2 additions & 2 deletions tests/test_problem_implementations.py
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -116,6 +115,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), (
Expand Down
Loading
Loading