diff --git a/docs/_static/img/design.png b/docs/_static/img/design.png
new file mode 100644
index 0000000000..6d1fc935ff
Binary files /dev/null and b/docs/_static/img/design.png differ
diff --git a/docs/api/plugins/design.rst b/docs/api/plugins/design.rst
index 653b2c5c0d..fea1730806 100644
--- a/docs/api/plugins/design.rst
+++ b/docs/api/plugins/design.rst
@@ -11,14 +11,13 @@ Design Space Exploration
:toctree: ../_autosummary/
:template: module.rst
- tidy3d.plugins.design.Parameter
tidy3d.plugins.design.ParameterFloat
tidy3d.plugins.design.ParameterInt
tidy3d.plugins.design.ParameterAny
- tidy3d.plugins.design.Method
tidy3d.plugins.design.MethodGrid
tidy3d.plugins.design.MethodMonteCarlo
- tidy3d.plugins.design.MethodRandomCustom
- tidy3d.plugins.design.MethodRandom
+ tidy3d.plugins.design.MethodBayOpt
+ tidy3d.plugins.design.MethodGenAlg
+ tidy3d.plugins.design.MethodParticleSwarm
tidy3d.plugins.design.DesignSpace
- tidy3d.plugins.design.Results
+ tidy3d.plugins.design.Result
diff --git a/tidy3d/plugins/design/README.md b/tidy3d/plugins/design/README.md
index 23d0d3fb79..0de30b11ff 100644
--- a/tidy3d/plugins/design/README.md
+++ b/tidy3d/plugins/design/README.md
@@ -1,12 +1,12 @@
-# Parameter Sweep Plugin
+# Design Plugin
## Basics
-The parameter sweep plugin `tidy3d.plugins.design` is a wrapper designed to make it simple and convenient for `tidy3d` users to define their parameter scans.
+The `Design` plugin `tidy3d.plugins.design` is a wrapper designed to make it simple and convenient for `Tidy3D` users to define parameter scans and optimizations.
-In short, users define the dimensions of their design design, as well as the method they wish to explore the design space. These specifications are combined in a `DesignSpace` object.
+In short, users define the dimensions of their design, as well as the method used to explore the design space. These specifications are combined in a `DesignSpace` object.
-Then, the user passes a function that defines the input / output relationship they wish to explore. The function arguments correspond to the dimensions defined in the `DesignSpace` and the function outputs can be anything.
+The user then passes a function that defines the input / output relationship they wish to explore. The function arguments correspond to the dimensions defined in the `DesignSpace` and the function outputs can be anything.
The result is stored as a `Result` object, which can be easily converted to a `pandas.DataFrame` for analysis, post processing, or visualization. The columns in this `DataFrame` correspond to the function inputs and outputs and each datapoint corresponds to a row in this `DataFrame`.
@@ -18,9 +18,9 @@ import tidy3d.plugins.design as tdd
## Function
-The first step in using the parameter sweep is to write the design design as a function that we would like to explore. This function could ideally involve a `tidy3d` simulation, but actually does not have to.
+The first step in using the `Design` plugin is to write the design as a function that can be explored. This function typically involves a `Tidy3D` simulation, but actually does not have to.
-For a concrete example, say we are analyzing a system comprised of a set of `n` spheres, each with radius `r`. We could write a function to compute the transmission through this system as follows (with some pseudo-code used for brevity).
+Say we are analyzing a system comprised of a set of `n` spheres, each with radius `r`. We could write a function to compute the transmission through this system as follows (with some pseudo-code used for brevity).
```py
def transmission(n: int, r: float) -> float:
@@ -48,11 +48,11 @@ def transmission_split(n: int, r: float) -> float:
return post(data=data)
```
-As we'll see, putting it in this form will be useful although it is not necessary.
+Putting it in this form is useful as it allows the `Design` plugin to automate parallelization through `Batch` objects, although it is not necessary.
## Parameters
-Now, we could query our transmission function directly to construct a parameter scan, but it would be more convenient to simply **define** our parameter scan as a specification and have the `tidy3d` wrapper do the accounting for us.
+Now, we could query our transmission function directly to construct a parameter scan, but it would be more convenient to simply **define** our parameter scan as a specification and have the `Tidy3D` wrapper do the accounting for us.
The first step is to define the design "parameters" (or dimensions), which also serve as inputs to our function defined earlier.
@@ -79,7 +79,7 @@ By defining our design parameters like this, we are mainly specifying what type,
## Method
-Now that we've defined our parameters, we also need to define the procedure that we should use to query the design space we've defined. One approach is to randomly sample points within the design bounds, another is to perform a grid search to uniformly scan the bounds. There are also more complex methods, such as Bayesian Optimization, which are not implemented yet.
+Now that we've defined our parameters, we also need to define the procedure that we should use to query the design space we've defined. One approach is to randomly sample points within the design bounds, another is to perform a grid search to uniformly scan the bounds. There are also more complex methods, such as Bayesian Optimization or genetic algorithms.
For this example, let's define a random sampling of the design parameters with 20 points.
@@ -95,15 +95,9 @@ With the design parameters and our method defined, we can combine everything int
design_space = tdd.DesignSpace(parameters=[param_n, param_r], method=method)
```
-Note, we haven't told the `DesignSpace` anything about our function. For that, it has two methods `.run()` and `.run_batch()`, which accept our function.
-
-`DesignSpace.run(f)` samples the design parameters according to the `method` and then evaluates each point one by one. Note that it has to be sequential because of the generality of this approach, in short `f` could be anything so we don't make any assumptions about whether it can be parallelized.
-
-On the other hand, `DesignSpace.run_batch(f_pre, f_post)` accepts our pre and post processing functions and assumes that a simulation is run in between. Because of this, this method can safely construct a `web.Batch` under the hood and run the tasks in parallel, stitching together the final results.
-
## Results
-The `DesignSpace.run()` and `DesignSpace.run_batch()` functions described before both return a `Result` object, which is basically a dataset containing the function inputs, outputs, source code, and any task ID information corresponding to each data point. Note that task ID can only be gathered if using `run_batch` because in the more general `run()` function, `tidy3d` can't know which tasks were involved in each data point.
+The `DesignSpace.run()` function returns a `Result` object, which is basically a dataset containing the function inputs, outputs, source code, and any task ID information corresponding to each data point.
The `Results` can be converted to a `pandas.DataFrame` where each row is a separate data point and each column is either an input or output for a function. It also contains various methods for plotting and managing the data.
diff --git a/tidy3d/plugins/design/design.py b/tidy3d/plugins/design/design.py
index 8716c0f4a7..73fd47a8e2 100644
--- a/tidy3d/plugins/design/design.py
+++ b/tidy3d/plugins/design/design.py
@@ -25,27 +25,31 @@
class DesignSpace(Tidy3dBaseModel):
"""Manages all exploration of a parameter space within specified parameters using a supplied search method.
-
The ``DesignSpace`` forms the basis of the ``Design`` plugin, and receives a ``Method`` and ``Parameter`` list that
define the scope of the design space and how it should be searched. ``DesignSpace.run()`` can then be called with
a function(s) to generate different solutions from parameters suggested by the ``Method``. The ``Method`` can either
sample the design space systematically or randomly, or can optimize for a given problem through an iterative search
and evaluate approach.
- Schematic outline of how to use the ``Design`` plugin to explore a design space.
- .. image:: ../../_static/img/design.png
- :width: 50%
- :align: left
- The `Design '_ notebook contains an overview of the
- ``Design`` plugin and is the best place to learn how to get started.
+ Notes
+ -----
+
+ Schematic outline of how to use the ``Design`` plugin to explore a design space.
+
+ .. image:: ../../../../_static/img/design.png
+ :width: 80%
+ :align: center
- Detailed examples using the ``Design`` plugin can be found in the following notebooks:
- `All-Dielectric Structural Colors '_
- `Bayesian Optimization of Y-Junction '_
- `Genetic Algorithm Reflector '_
- `Particle Swarm Optimizer PBS '_
- `Particle Swarm Optimizer Bullseye Cavity '_
+ The `Design `_ notebook contains an overview of the
+ ``Design`` plugin and is the best place to learn how to get started.
+ Detailed examples using the ``Design`` plugin can be found in the following notebooks:
+
+ * `All-Dielectric Structural Colors `_
+ * `Bayesian Optimization of Y-Junction `_
+ * `Genetic Algorithm Reflector `_
+ * `Particle Swarm Optimizer PBS `_
+ * `Particle Swarm Optimizer Bullseye Cavity `_
Example
-------
@@ -76,7 +80,8 @@ class DesignSpace(Tidy3dBaseModel):
task_name: str = pd.Field(
"",
title="Task Name",
- description="Task name assigned to tasks along with a simulation counter in the form of {task_name}_{counter}. "
+ description="Task name assigned to tasks along with a simulation counter in the form of {task_name}_{sim_index}_{counter} where ``sim_index`` is "
+ "the index of the ``Simulation`` from the pre function output. "
"If the pre function outputs a dictionary the key will be included in the task name as {task_name}_{dict_key}_{counter}. "
"Only used when pre-post functions are supplied.",
)
@@ -139,7 +144,6 @@ def get_fn_source(function: Callable) -> str:
def run(self, fn: Callable, fn_post: Callable = None, verbose: bool = True) -> Result:
"""Explore a parameter space with a supplied method using the user supplied function.
-
Supplied functions are used to evaluate the design space and are called within the method.
For optimization methods these functions act as the fitness function. A single function can be
supplied which will contain the preprocessing, computation, and analysis of the desired problem.
@@ -156,17 +160,30 @@ def run(self, fn: Callable, fn_post: Callable = None, verbose: bool = True) -> R
parallel computation on the cloud. The original structure is then restored for output; all ``Simulation`` objects are replaced by ``SimulationData`` objects.
Example pre return formats and associated post inputs can be seen in the table below.
- | fn_pre return | fn_post call |
- |-------------------------------------------|---------------------------------------------------|
- | 1.0 | fn_post(1.0) |
- | [1,2,3] | fn_post(1,2,3) |
- | {'a': 2, 'b': 'hi'} | fn_post(a=2, b='hi') |
- | Simulation | fn_post(SimulationData) |
- | Batch | fn_post(BatchData) |
- | [Simulation, Simulation] | fn_post(SimulationData, SimulationData) |
- | [Simulation, 1.0] | fn_post(SimulationData, 1.0) |
- | [Simulation, Batch] | fn_post(SimulationData, BatchData) |
- | {'a': Simulation, 'b': Batch, 'c': 2.0} | fn_post(a=SimulationData, b=BatchData, c=2.0) |
+ .. list-table:: Pre return formats and post input formats
+ :widths: 50 50
+ :header-rows: 1
+
+ * - fn_pre return
+ - fn_post call
+ * - 1.0
+ - fn_post(1.0)
+ * - [1,2,3]
+ - fn_post(1,2,3)
+ * - {'a': 2, 'b': 'hi'}
+ - fn_post(a=2, b='hi')
+ * - Simulation
+ - fn_post(SimulationData)
+ * - Batch
+ - fn_post(BatchData)
+ * - [Simulation, Simulation]
+ - fn_post(SimulationData, SimulationData)
+ * - [Simulation, 1.0]
+ - fn_post(SimulationData, 1.0)
+ * - [Simulation, Batch]
+ - fn_post(SimulationData, BatchData)
+ * - {'a': Simulation, 'b': Batch, 'c': 2.0}
+ - fn_post(a=SimulationData, b=BatchData, c=2.0)
The output of ``fn_post`` (or ``fn`` if only one function is supplied) must be a float
or a container where the first element is a ``float`` and second element is a ``list`` / ``dict`` e,g. [float {"aux_1": str}].
diff --git a/tidy3d/plugins/design/method.py b/tidy3d/plugins/design/method.py
index 81ac9d7636..2a7143fc69 100644
--- a/tidy3d/plugins/design/method.py
+++ b/tidy3d/plugins/design/method.py
@@ -124,7 +124,6 @@ def _run(self, parameters: Tuple[ParameterType, ...], run_fn: Callable, console)
class MethodGrid(MethodSample):
"""Select parameters uniformly on a grid.
-
Size of the grid is specified by the parameter type,
either as the number of unique discrete values (``ParameterInt``, ``ParameterAny``)
or with the num_points argument (``ParameterFloat``).
@@ -159,7 +158,7 @@ def sample(parameters: Tuple[ParameterType, ...]) -> Dict[str, Any]:
class MethodOptimize(Method, ABC):
- """A method for handling design searches that optimize the design"""
+ """A method for handling design searches that optimize the design."""
# NOTE: We could move this to the Method base class but it's not relevant to MethodGrid
seed: pd.PositiveInt = pd.Field(
@@ -192,9 +191,13 @@ def _handle_param_convert(self, param_converter: dict, sol_dict_list: list[dict]
class MethodBayOpt(MethodOptimize, ABC):
- """A standard method for performing a Bayesian optimization search.
-
+ """A standard method for performing a Bayesian optimization search, built around the `Bayesian Optimization `_ package.
The fitness function is maximising by default.
+
+ Example
+ -------
+ >>> import tidy3d.plugins.design as tdd
+ >>> method = tdd.MethodBayOpt(initial_iter=4, n_iter=10)
"""
initial_iter: pd.PositiveInt = pd.Field(
@@ -212,19 +215,19 @@ class MethodBayOpt(MethodOptimize, ABC):
acq_func: Literal["ucb", "ei", "poi"] = pd.Field(
default="ucb",
title="Type of Acquisition Function",
- description="The type of acquisition function that should be used to suggest parameter values. More detail available `here `_.",
+ description="The type of acquisition function that should be used to suggest parameter values. More detail available in the `package docs `_.",
)
kappa: pd.PositiveFloat = pd.Field(
default=2.5,
title="Kappa",
- description="The kappa coefficient used by the ``ucb`` acquisition function. More detail available `here `_.",
+ description="The kappa coefficient used by the ``ucb`` acquisition function. More detail available in the `package docs `_.",
)
xi: pd.NonNegativeFloat = pd.Field(
default=0.0,
title="Xi",
- description="The Xi coefficient used by the ``ei`` and ``poi`` acquisition functions. More detail available `here `_.",
+ description="The Xi coefficient used by the ``ei`` and ``poi`` acquisition functions. More detail available in the `package docs `_.",
)
def _get_run_count(self, parameters: list = None) -> int:
@@ -236,7 +239,7 @@ def _run(self, parameters: Tuple[ParameterType, ...], run_fn: Callable, console)
Uses the ``bayes_opt`` package to carry out a Bayesian optimization. Utilizes the ``.suggest`` and ``.register`` methods instead of
the ``BayesianOptimization`` helper class as this allows more control over batching and preprocessing.
- More details of the package can be found `here '_.
+ More details of the package can be found `here `_.
"""
try:
from bayes_opt import BayesianOptimization, UtilityFunction
@@ -331,9 +334,13 @@ def _run(self, parameters: Tuple[ParameterType, ...], run_fn: Callable, console)
class MethodGenAlg(MethodOptimize, ABC):
- """A standard method for performing genetic algorithm search
-
+ """A standard method for performing genetic algorithm search, built around the `PyGAD `_ package.
The fitness function is maximising by default.
+
+ Example
+ -------
+ >>> import tidy3d.plugins.design as tdd
+ >>> method = tdd.MethodGenAlg(solutions_per_pop=2, n_generations=1, n_parents_mating=2)
"""
# Args for the user
@@ -358,38 +365,38 @@ class MethodGenAlg(MethodOptimize, ABC):
stop_criteria_type: Literal["reach", "saturate"] = pd.Field(
default=None,
title="Early Stopping Criteria Type",
- description="Define the early stopping criteria. Supported words are 'reach' or 'saturate'. 'reach' stops at a desired fitness, 'saturate' stops when the fitness stops improving. Must set ``stop_criteria_number``. See PyGAD docs https://pygad.readthedocs.io/en/latest/pygad.html for more details.",
+ description="Define the early stopping criteria. Supported words are 'reach' or 'saturate'. 'reach' stops at a desired fitness, 'saturate' stops when the fitness stops improving. Must set ``stop_criteria_number``. See the `PyGAD docs `_ for more details.",
)
stop_criteria_number: pd.PositiveFloat = pd.Field(
default=None,
title="Early Stopping Criteria Number",
- description="Must set ``stop_criteria_type``. If type is 'reach' the number is acceptable fitness value to stop the optimization. If type is 'saturate' the number is the number generations where the fitness doesn't improve before optimization is stopped. See PyGAD docs https://pygad.readthedocs.io/en/latest/pygad.html for more details.",
+ description="Must set ``stop_criteria_type``. If type is 'reach' the number is acceptable fitness value to stop the optimization. If type is 'saturate' the number is the number generations where the fitness doesn't improve before optimization is stopped. See the `PyGAD docs `_ for more details.",
)
parent_selection_type: Literal["sss", "rws", "sus", "rank", "random", "tournament"] = pd.Field(
default="sss",
title="Parent Selection Type",
- description="The style of parent selector. See the PyGAD docs https://pygad.readthedocs.io/en/latest/pygad.html for more details.",
+ description="The style of parent selector. See the `PyGAD docs `_ for more details.",
)
keep_parents: Union[pd.PositiveInt, Literal[-1, 0]] = pd.Field(
default=-1,
title="Keep Parents",
- description="The number of parents to keep unaltered in the population of the next generation. Default value of -1 keeps all current parents for the next generation. This value is overwritten if ``keep_parents`` is > 0. See the PyGAD docs https://pygad.readthedocs.io/en/latest/pygad.html for more details.",
+ description="The number of parents to keep unaltered in the population of the next generation. Default value of -1 keeps all current parents for the next generation. This value is overwritten if ``keep_parents`` is > 0. See the `PyGAD docs `_ for more details.",
)
keep_elitism: Union[pd.PositiveInt, Literal[0]] = pd.Field(
default=1,
title="Keep Elitism",
- description="The number of top solutions to be included in the population of the next generation. Overwrites ``keep_parents`` if value is > 0. See the PyGAD docs https://pygad.readthedocs.io/en/latest/pygad.html for more details.",
+ description="The number of top solutions to be included in the population of the next generation. Overwrites ``keep_parents`` if value is > 0. See the `PyGAD docs `_ for more details.",
)
crossover_type: Union[None, Literal["single_point", "two_points", "uniform", "scattered"]] = (
pd.Field(
default="single_point",
title="Crossover Type",
- description="The style of crossover operation. See the PyGAD docs https://pygad.readthedocs.io/en/latest/pygad.html for more details.",
+ description="The style of crossover operation. See the `PyGAD docs `_ for more details.",
)
)
@@ -403,7 +410,7 @@ class MethodGenAlg(MethodOptimize, ABC):
pd.Field(
default="random",
title="Mutation Type",
- description="The style of gene mutation. See the PyGAD docs https://pygad.readthedocs.io/en/latest/pygad.html for more details.",
+ description="The style of gene mutation. See the `PyGAD docs `_ for more details.",
)
)
@@ -416,7 +423,7 @@ class MethodGenAlg(MethodOptimize, ABC):
save_solution: pd.StrictBool = pd.Field(
default=False,
title="Save Solutions",
- description="Save all solutions from all generations within a numpy array. Can be accessed from the optimizer object stored in the Result. May cause memory issues with large populations or many generations. See the PyGAD docs https://pygad.readthedocs.io/en/latest/pygad.html for more details.",
+ description="Save all solutions from all generations within a numpy array. Can be accessed from the optimizer object stored in the Result. May cause memory issues with large populations or many generations. See the `PyGAD docs _` for more details.",
)
# TODO: See if anyone is interested in having the full suite of PyGAD options - there's a lot!
@@ -432,7 +439,7 @@ def _run(self, parameters: Tuple[ParameterType, ...], run_fn: Callable, console)
Uses the ``pygad`` package to carry out a particle search optimization. Additional development has ensured that
previously suggested solutions are not repeatedly computed, and that all computed solutions are captured.
- More details of the package can be found `here '_.
+ More details of the package can be found `here `_.
"""
try:
import pygad
@@ -615,9 +622,13 @@ def capture_init_pop_fitness(ga_instance: pygad.GA, population_fitness) -> None:
class MethodParticleSwarm(MethodOptimize, ABC):
- """A standard method for performing particle swarm search.
-
+ """A standard method for performing particle swarm search, build around the `PySwarms `_ package.
The fitness function is maximising by default.
+
+ Example
+ -------
+ >>> import tidy3d.plugins.design as tdd
+ >>> method = tdd.MethodParticleSwarm(n_particles=5, n_iter=3)
"""
n_particles: pd.PositiveInt = pd.Field(
@@ -653,7 +664,7 @@ class MethodParticleSwarm(MethodOptimize, ABC):
ftol: Union[pd.confloat(ge=0, le=1), Literal[-inf]] = pd.Field(
default=-inf,
title="Relative Error for Convergence",
- description="Relative error in ``objective_func(best_solution)`` acceptable for convergence. See https://pyswarms.readthedocs.io/en/latest/examples/tutorials/tolerance.html for details. Off by default.",
+ description="Relative error in ``objective_func(best_solution)`` acceptable for convergence. See the `PySwarms docs `_ for details. Off by default.",
)
ftol_iter: pd.PositiveInt = pd.Field(
@@ -676,7 +687,7 @@ def _run(self, parameters: Tuple[ParameterType, ...], run_fn: Callable, console)
"""Defines the particle search optimization algorithm for the method.
Uses the ``pyswarms`` package to carry out a particle search optimization.
- More details of the package can be found `here '_.
+ More details of the package can be found `here `_.
"""
try:
from pyswarms.single.global_best import GlobalBestPSO
diff --git a/tidy3d/plugins/design/result.py b/tidy3d/plugins/design/result.py
index fac840f442..3bc31c56c8 100644
--- a/tidy3d/plugins/design/result.py
+++ b/tidy3d/plugins/design/result.py
@@ -89,7 +89,7 @@ class Result(Tidy3dBaseModel):
None,
title="Optimizer object",
description="The optimizer returned at the end of an optimizer run. Can be used to analyze and plot how the optimization progressed. "
- "Attributes depend on the optimizer used; a full explaination of the optimizer can be found on associated library doc pages. Will be None for sampling based methods.",
+ "Attributes depend on the optimizer used; a full explaination of the optimizer can be found on associated library doc pages. Will be ``None`` for sampling based methods.",
)
@pd.validator("coords", always=True)