Skip to content
Open
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
1 change: 1 addition & 0 deletions docs/introduction/api.md
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,7 @@ A dataset is generally composed of several columns:
```{eval-rst}
.. automethod:: engibench.core.Problem.check_constraints
.. automethod:: engibench.core.Problem.simulate
.. automethod:: engibench.core.Problem.simulate_verbose
.. automethod:: engibench.core.Problem.optimize

Where an OptiStep is defined as:
Expand Down
37 changes: 36 additions & 1 deletion engibench/core.py
Original file line number Diff line number Diff line change
Expand Up @@ -42,6 +42,17 @@ class ObjectiveDirection(Enum):
MAXIMIZE = auto()


@dataclasses.dataclass
class SimulationResult:
"""Base type for the return value of `Problem.simulate_with_details()`.

Derived problem classes may return instances of derived `SimulationResult` classes, to return more fields.
"""

objective_values: npt.NDArray
"""Performance of the simulated design -- each entry corresponds to an objective value."""


class Problem(Generic[DesignType]):
r"""Main class for defining an engineering design problem.

Expand All @@ -54,6 +65,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:`simulate_verbose` - to simulate a design and return the performance and problem specific additional data 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. Should be called before each call to `simulate` or `optimize`.
- :meth:`render` - to render a design in a human-readable format.
Expand Down Expand Up @@ -130,9 +142,32 @@ def conditions_keys(self) -> list[str]:
"""Returns the condition names as a list."""
return [f.name for f in dataclasses.fields(self.conditions)]

def simulate_verbose(self, design: DesignType, config: dict[str, Any] | None = None) -> SimulationResult:
r"""Launch a simulation on the given design and return the performance.

Similar to :py:meth:`Problem.simulate()` but usually returns more values, which can lead to
an increase of computational intensity when compared to :py:meth:`Problem.simulate()`.

Implementations returning more than just the objective values should return an instance of a
class which is derived from :class:`SimulationResult` and document the additional fields.

Args:
design (DesignType): The design to simulate.
config (dict): A dictionary with configuration (e.g., boundary conditions, filenames) for the optimization.
**kwargs: Additional keyword arguments.

Returns:
An instance of `SimulationResult` or a derived class.
"""
raise NotImplementedError

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.

The default implementation just calls :py:meth:`Problem.simulate_verbose()` and only returns the objective values.
If the problem allows to determine the objective values in a simpler, less computationally intensive way than
:py:meth:`Problem.simulate_verbose()`, the default implementation should be overloaded by a custom implementation.

Args:
design (DesignType): The design to simulate.
config (dict): A dictionary with configuration (e.g., boundary conditions, filenames) for the optimization.
Expand All @@ -141,7 +176,7 @@ def simulate(self, design: DesignType, config: dict[str, Any] | None = None) ->
Returns:
np.array: The performance of the design -- each entry corresponds to an objective value.
"""
raise NotImplementedError
return self.simulate_verbose(design, config).objective_values

def optimize(
self, starting_point: DesignType, config: dict[str, Any] | None = None
Expand Down
20 changes: 18 additions & 2 deletions engibench/problems/airfoil/v0.py
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,7 @@
from engibench.core import ObjectiveDirection
from engibench.core import OptiStep
from engibench.core import Problem
from engibench.core import SimulationResult
from engibench.problems.airfoil.pyopt_history import History
from engibench.problems.airfoil.templates import cli_interface
from engibench.problems.airfoil.utils import calc_area
Expand Down Expand Up @@ -315,7 +316,22 @@ def simulate(self, design: DesignType, config: dict[str, Any] | None = None, mpi
mpicores (int): The number of MPI cores to use in the simulation.

Returns:
dict: The performance of the design - each entry of the dict corresponds to a named objective value.
Objective values as the performance of the design.
"""
return self.simulate_verbose(design, config, mpicores=mpicores).objective_values

def simulate_verbose(
self, design: DesignType, config: dict[str, Any] | None = None, mpicores: int = 4
) -> SimulationResult:
"""Simulates the performance of an airfoil design.

Args:
design (dict): The design to simulate.
config (dict): A dictionary with configuration (e.g., boundary conditions, filenames) for the simulation.
mpicores (int): The number of MPI cores to use in the simulation.

Returns:
`SimulationResult` instance containing objective values as the performance of the design.
"""
if isinstance(design["angle_of_attack"], np.ndarray):
design["angle_of_attack"] = design["angle_of_attack"][0]
Expand Down Expand Up @@ -353,7 +369,7 @@ def simulate(self, design: DesignType, config: dict[str, Any] | None = None, mpi
outputs = np.load(self.__local_study_dir + "/output/outputs.npy")
lift = float(outputs[3])
drag = float(outputs[4])
return np.array([drag, lift])
return SimulationResult(np.array([drag, lift]))

def optimize(
self, starting_point: DesignType, config: dict[str, Any] | None = None, mpicores: int = 4
Expand Down
20 changes: 18 additions & 2 deletions engibench/problems/beams2d/v0.py
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,7 @@
from engibench.core import ObjectiveDirection
from engibench.core import OptiStep
from engibench.core import Problem
from engibench.core import SimulationResult
from engibench.problems.beams2d.backend import calc_sensitivity
from engibench.problems.beams2d.backend import design_to_image
from engibench.problems.beams2d.backend import image_to_design
Expand Down Expand Up @@ -146,7 +147,22 @@ def simulate(
config (dict): A dictionary with configuration (e.g., boundary conditions) for the simulation.

Returns:
npt.NDArray: The performance of the design in terms of compliance.
The performance of the design in terms of compliance.
"""
return self.simulate_verbose(design, config, ce=ce).objective_values

def simulate_verbose(
self, design: npt.NDArray, config: dict[str, Any] | None = None, *, ce: npt.NDArray | None = None
) -> SimulationResult:
"""Simulates the performance of a beam design.

Args:
design (np.ndarray): The design to simulate.
ce: (np.ndarray, optional): If applicable, the pre-calculated sensitivity of the current design.
config (dict): A dictionary with configuration (e.g., boundary conditions) for the simulation.

Returns:
SimulationResult: The performance of the design in terms of compliance.
"""
# 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:
Expand All @@ -166,7 +182,7 @@ def simulate(
c = (
(self.__st.Emin + design**simulate_config.penal * (self.__st.Emax - self.__st.Emin)) * ce
).sum() # compliance (objective)
return np.array([c])
return SimulationResult(np.array([c]))

def optimize(
self, starting_point: npt.NDArray | None = None, config: dict[str, Any] | None = None
Expand Down
7 changes: 4 additions & 3 deletions engibench/problems/heatconduction2d/v0.py
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,7 @@
from engibench.core import ObjectiveDirection
from engibench.core import OptiStep
from engibench.core import Problem
from engibench.core import SimulationResult
from engibench.problems.heatconduction2d.shared import load_float
from engibench.problems.heatconduction2d.shared import run_container_script
from engibench.utils.cli import np_array_to_bytes
Expand Down Expand Up @@ -95,15 +96,15 @@ def __init__(self, seed: int = 0, **kwargs: Any) -> None:
self.conditions = self.Conditions(self.config.volume, self.config.length)
self.design_space = spaces.Box(low=0.0, high=1.0, shape=(resolution, resolution), dtype=np.float64)

def simulate(self, design: npt.NDArray | None = None, config: dict[str, Any] | None = None) -> npt.NDArray:
def simulate_verbose(self, design: npt.NDArray | None = None, config: dict[str, Any] | None = None) -> SimulationResult:
"""Simulate the design.

Args:
design (Optional[np.ndarray]): The design to simulate.
config (dict): A dictionary with configuration (e.g., volume (float): Volume constraint,length (float): Length constraint,resolution (int): Resolution of the design space) for the simulation.

Returns:
float: The thermal compliance of the design.
A `SimulationResult` instance containing the thermal compliance of the design.
"""
config = config or {}
volume = config.get("volume", self.config.volume)
Expand All @@ -122,7 +123,7 @@ def simulate(self, design: npt.NDArray | None = None, config: dict[str, Any] | N
)
)

return np.array([perf])
return SimulationResult(np.array([perf]))

def optimize(
self, starting_point: npt.NDArray | None = None, config: dict[str, Any] | None = None
Expand Down
9 changes: 5 additions & 4 deletions engibench/problems/heatconduction3d/v0.py
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,7 @@
from engibench.core import ObjectiveDirection
from engibench.core import OptiStep
from engibench.core import Problem
from engibench.core import SimulationResult
from engibench.problems.heatconduction2d.shared import load_float
from engibench.problems.heatconduction2d.shared import run_container_script
from engibench.utils import cli
Expand Down Expand Up @@ -91,15 +92,15 @@ def __init__(self, seed: int = 0, **kwargs) -> None:
self.conditions = self.Conditions(self.config.volume, self.config.area)
self.design_space = spaces.Box(low=0.0, high=1.0, shape=(resolution, resolution, resolution), dtype=np.float64)

def simulate(self, design: npt.NDArray | None = None, config: dict[str, Any] | None = None) -> npt.NDArray:
"""Simulate the design.
def simulate_verbose(self, design: npt.NDArray | None = None, config: dict[str, Any] | None = None) -> SimulationResult:
r"""Launch a simulation on the given design and return the performance.

Args:
design (Optional[np.ndarray]): The design to simulate.
config (dict): A dictionary with configuration (e.g., volume (float): Volume constraint,area (float): Area constraint,resolution (int): Resolution of the design space) for the simulation.

Returns:
float: The thermal compliance of the design.
A `SimulationResult` instance containing the thermal compliance of the design.
"""
config = config or {}
volume = config.get("volume", self.config.volume)
Expand All @@ -118,7 +119,7 @@ def simulate(self, design: npt.NDArray | None = None, config: dict[str, Any] | N
)
)

return np.array([float(perf)])
return SimulationResult(np.array([float(perf)]))

def optimize(
self, starting_point: npt.NDArray | None = None, config: dict[str, Any] | None = None
Expand Down
7 changes: 4 additions & 3 deletions engibench/problems/photonics2d/v0.py
Original file line number Diff line number Diff line change
Expand Up @@ -37,6 +37,7 @@
from engibench.core import ObjectiveDirection
from engibench.core import OptiStep
from engibench.core import Problem
from engibench.core import SimulationResult
from engibench.problems.photonics2d.backend import epsr_parameterization

# --- EngiBench Problem-Specific Backend ---
Expand Down Expand Up @@ -232,7 +233,7 @@ def _run_fdfd(

return epsr, ez1, ez2, source1, source2, probe1, probe2

def simulate(self, design: npt.NDArray, config: dict[str, Any] | None = None, **kwargs) -> npt.NDArray: # noqa: ARG002
def simulate_verbose(self, design: npt.NDArray, config: dict[str, Any] | None = None, **kwargs) -> SimulationResult: # noqa: ARG002
"""Simulates the performance of a design, returning the raw objective value.

Stores simulation fields (`Ez1`, `Ez2`, `epsr`) internally in `_last_Ez1`,
Expand All @@ -244,7 +245,7 @@ def simulate(self, design: npt.NDArray, config: dict[str, Any] | None = None, **
**kwargs: Additional keyword arguments (ignored).

Returns:
npt.NDArray: 1-element array: [total_overlap - penalty], where higher is better.
SimulationResult: Containing an 1-element array: [total_overlap - penalty], where higher is better.
"""
conditions = self._setup_simulation(config)

Expand All @@ -267,7 +268,7 @@ def simulate(self, design: npt.NDArray, config: dict[str, Any] | None = None, **
penalty_weight = conditions.get("penalty_weight", self._penalty_weight_default)
penalty = penalty_weight * np.linalg.norm(design)

return np.array([total_overlap - penalty], dtype=np.float64)
return SimulationResult(np.array([total_overlap - penalty], dtype=np.float64))

def optimize( # noqa: PLR0915
self,
Expand Down
7 changes: 4 additions & 3 deletions engibench/problems/power_electronics/v0.py
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@

from engibench.core import ObjectiveDirection
from engibench.core import Problem
from engibench.core import SimulationResult
from engibench.problems.power_electronics.utils.config import Config
from engibench.problems.power_electronics.utils.netlist_handler import parse_topology
from engibench.problems.power_electronics.utils.netlist_handler import rewrite_netlist
Expand Down Expand Up @@ -88,7 +89,7 @@ def __init__(
)
self.ngspice_path = ngspice_path

def simulate(self, design: npt.NDArray, config: dict[str, Any] | None = None) -> npt.NDArray: # noqa: ARG002
def simulate_verbose(self, design: npt.NDArray, config: dict[str, Any] | None = None) -> SimulationResult: # noqa: ARG002
"""Simulates the performance of a Power Electronics design.

Args:
Expand All @@ -103,7 +104,7 @@ def simulate(self, design: npt.NDArray, config: dict[str, Any] | None = None) ->
config: ignored

Returns:
simulation_results: a numpy array containing the simulation results [DcGain, VoltageRipple, Efficiency].
An instance of `SimulationResult` containing `objective_values=[DcGain, VoltageRipple]`.
"""
self.config, rewrite_netlist_str, edge_map, _ = parse_topology(self.config)
self.config = process_sweep_data(config=self.config, sweep_data=design.tolist())
Expand All @@ -112,7 +113,7 @@ def simulate(self, design: npt.NDArray, config: dict[str, Any] | None = None) ->
ngspice = NgSpice(ngspice_windows_path=self.ngspice_path)
ngspice.run(self.config.rewrite_netlist_path, self.config.log_file_path)
DcGain, VoltageRipple = process_log_file(self.config.log_file_path)
return np.array([DcGain, VoltageRipple])
return SimulationResult(np.array([DcGain, VoltageRipple]))

def optimize(self, starting_point: npt.NDArray, config: dict[str, Any] | None = None) -> NoReturn:
"""Optimize the design variable. Not applicable for this problem."""
Expand Down
11 changes: 7 additions & 4 deletions engibench/problems/thermoelastic2d/v0.py
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,7 @@
from engibench.core import ObjectiveDirection
from engibench.core import OptiStep
from engibench.core import Problem
from engibench.core import SimulationResult
from engibench.problems.thermoelastic2d.model import fea_model
from engibench.problems.thermoelastic2d.model.fea_model import FeaModel
from engibench.problems.thermoelastic2d.utils import get_res_bounds
Expand Down Expand Up @@ -110,15 +111,15 @@ def reset(self, seed: int | None = None) -> None:
"""
super().reset(seed)

def simulate(self, design: npt.NDArray, config: dict[str, Any] | None = None) -> npt.NDArray:
"""Simulates the performance of a design topology.
def simulate_verbose(self, design: npt.NDArray, config: dict[str, Any] | None = None) -> SimulationResult:
r"""Launch a simulation on the given design topology and return the performance.

Args:
design (np.ndarray): The design to simulate.
config (dict): A dictionary with configuration (e.g., boundary conditions, filenames) for the simulation.

Returns:
dict: The performance of the design - each entry of the dict corresponds to a named objective value.
A `SimulationResult` instance.
"""
boundary_dict = dataclasses.asdict(self.conditions)
for key, value in (config or {}).items():
Expand All @@ -129,7 +130,9 @@ def simulate(self, design: npt.NDArray, config: dict[str, Any] | None = None) ->
boundary_dict[key] = value

results = FeaModel(plot=False, eval_only=True).run(boundary_dict, x_init=design)
return np.array([results["structural_compliance"], results["thermal_compliance"], results["volume_fraction_error"]])
return SimulationResult(
np.array([results["structural_compliance"], results["thermal_compliance"], results["volume_fraction_error"]])
)

def optimize(
self, starting_point: npt.NDArray, config: dict[str, Any] | None = None
Expand Down
11 changes: 7 additions & 4 deletions engibench/problems/thermoelastic3d/v0.py
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@
from engibench.core import ObjectiveDirection
from engibench.core import OptiStep
from engibench.core import Problem
from engibench.core import SimulationResult
from engibench.problems.thermoelastic3d.model import fem_model
from engibench.problems.thermoelastic3d.model.fem_model import FeaModel3D

Expand Down Expand Up @@ -132,15 +133,15 @@ def reset(self, seed: int | None = None) -> None:
"""
super().reset(seed)

def simulate(self, design: npt.NDArray, config: dict[str, Any] | None = None) -> npt.NDArray:
"""Simulates the performance of a design topology.
def simulate_verbose(self, design: npt.NDArray, config: dict[str, Any] | None = None) -> SimulationResult:
r"""Launch a simulation on the given design topology and return the performance.

Args:
design (np.ndarray): The design to simulate.
config (dict): A dictionary with configuration (e.g., boundary conditions, filenames) for the simulation.

Returns:
dict: The performance of the design - each entry of the dict corresponds to a named objective value.
A `SimulationResult` instance.
"""
boundary_dict = dataclasses.asdict(self.conditions)
for key, value in (config or {}).items():
Expand All @@ -151,7 +152,9 @@ def simulate(self, design: npt.NDArray, config: dict[str, Any] | None = None) ->
boundary_dict[key] = value

results = FeaModel3D(plot=False, eval_only=True).run(boundary_dict, x_init=design)
return np.array([results["structural_compliance"], results["thermal_compliance"], results["volume_fraction"]])
return SimulationResult(
np.array([results["structural_compliance"], results["thermal_compliance"], results["volume_fraction"]])
)

def optimize(
self, starting_point: npt.NDArray, config: dict[str, Any] | None = None
Expand Down
Loading
Loading