Inverse design using lumopt can be run from the CAD script editor, the command line or any python IDE. In the first sub-section, we will briefly describe how to import the lumopt and lumapi modules; although an experienced python user can likely skip this part. As a starting point, it is recommended to run the AppGallery examples and use these as templates for your own project. It may be helpful following along with these files as you read this page or you may simply reference this page when running the examples later. In the project Init section, we outline the project inputs and necessary simulation objects that should be included. Important lumopt specific considerations are highlighted; however, a valid simulation set-up is imperative, so convergence testing should be considered a pre-requisite. Next important lumopt set-up classes, that should be updated to reflect your specifications are documented. Finally, a description of the scipy optimizer and lumopt optimization classes are presented. Shape and topology optimization primarily differ in how they handle the optimizable geometry which is the subject of the next page in this series.
Install
Running from the CAD script editor has the advantage that it requires no set-up and uses a Python 3 distro that ships with the Lumerical installer, so there is no need to install a separate python. This method automatically configures the workspace to find the lumopt and lumapi modules and would be the preferred method for users with little experience in python. Using your own version of python and running from an IDE or the command line may be preferable for more experienced users. To do this one simply needs to import lumopt and lumapi which will require specifying the correct path. Either pass an explicit path using importlibutil, or updating the search path permanently appending the PythonPath object. Advanced user working with numerous libraries might want to create a Lumerical virtual environment. For more information on these methods and os specific paths, see Session management - Python API.
Note Lumerical ships with a version of Python 3, including lumapi and lumopt modules, already installed. To run any of our examples 'out of the box' simply run the scripts from the script file editor in the CAD. |
Project Init
Base Sim
The base simulation needs to be defined using one of the following options
- Predefined simulation file - Initialize a python variable that specifies the path to the base file. Example Grating coupler.
base_sim = os.path.join(os.path.dirname(__file__), 'grating_base.fsp')
- An lsf set-up script - Create a python variable using the load_from_lsf function. Example Waveguide crossing.
from lumopt.utilities.load_lumerical_scripts import load_from_lsf
crossing_base = load_from_lsf('varFDTD_crossing.lsf')
- Callable python code that does the set-up using the API - This can be a function defined in the same file or an imported function. Example Y-branch.
sys.path.append(os.path.dirname(__file__)) #Add current directory to Python path
from varFDTD_y_branch import y_branch_init_ #Import y_branch_init function from file
y_branch_base = y_branch_init_ #New handle for function
Each method produces a file which the optimizer updates and runs. Since the resulting project files should be equivalent; the method each user employs is a matter of preference or convenience.
Required Objects
In the varFDTD, and FDTD base simulation it is also necessary to provide input/output geometry and define the following simulation objects that are used by lumopt.
Figure 1: Required simulation inputs
- A mode source - Typically named source, but can be set in the Optimization class.
- A mesh overide region covering the optimization volume - This is a static object, and the pixel/voxel size should be uniform.
- A frequency monitor over the optimization volume - Should be named opt_field, these field values are important for the adjoint method.
- A frequency monitor over the output waveguides - Used to calculate the FOM. The name of this monitor is passed to the ModeMatch class.
The mode source should have a large enough span and the modes should be compared to expectations see FDE convergence. This is used as the forward source. A mesh override is placed over the optimization region to ensure that a fine uniform grid covers this space, and the opt_field monitor is used to extract the fields in this region. The FOM monitor should be aligned to the interface of a mesh cell to avoid interpolation errors; therefore, it is a good idea to have a mesh override co-located with the FOM monitor. In the adjoint simulation, the adjoint source will take the place of the FOM monitor. Passing the name of the FOM monitor, to modematch class, allows multiple FOM monitors to be defined in the same base file which is helpful for SuperOptimization.
Set-up Classes
Two important lumopt classes that should be updated with your parameters are wavelengths and modematch. These are used to define the spectrum and mode number(or polarization) of the FOM respectively. It should be noted here that the FOM we accept is power coupling of guided modes. Other figures of merit, such as optimizing the target phase or power to a specified grating order are not supported. To compute the broadband figure of merit we take the mean of the target minus the error using the p-norm.
$$F=\left(\frac{1}{\lambda_{2}-\lambda_{1}} \int_{\lambda_{1}}^{\lambda_{2}}\left|T_{0}(\lambda)\right|^{p} d \lambda\right)^{1 / p}-\left(\frac{1}{\lambda_{2}-\lambda_{1}} \int_{\lambda_{1}}^{\lambda_{2}}\left|T(\lambda)-T_{0}(\lambda)\right|^{p} d \lambda\right)^{1 / p}$$
where
- \( T_{0} \) is the target_T_fwd
- \( \lambda_{1} \text{ and } \lambda_{2} \) are the lower and upper limits of the wavelength points
- \( T \) is the actual mode expansion power transmission
- \( p \) is the value of the generalized p-norm
Wavelengths
Defines the simulation bandwidth, and wavelength resolution. Defining the target FOM spectrum is done in modematch.
from lumopt.utilities.wavelengths import Wavelengths
class Wavelengths(start,
stop,
points)
: start: float
Shortest wavelength [m]
: stop: float
Longest wavelength [m]
: points: int
The number of points, uniformly spaced including the endpoints.
Example
wavelengths = Wavelengths(start = 1260e-9, stop = 1360e-9, points = 11)
ModeMatch
This class is used to define target mode, propagation direction, and specify the broadband power coupling.
from lumopt.figures_of_merit.modematch import ModeMatch
class ModeMatch(monitor_name,
mode_number,
direction,
target_T_fwd,
norm_p,
target_fom)
: monitor_name: str
Name of the FOM monitor in the file.
: mode_number : str or int
Used to specify the mode.
If the varFDTD solver is used:
- ‘fundamental mode’
- int - user select mode number
If the FDTD solver is used:
- 'fundamental mode'
- 'fundamental TE mode'
- 'fundamental TM mode'
- int - user select mode number
: direction : str
The direction is determined by the FDTD coordinates; for mode traveling in the positive direction the direction is forward.
- 'Backward'
- 'Forward'
: multi_freq_source: boolean, optional
Should only be enabled by advanced users. See frequency Frequency dependent mode profile for more info. Default = False
: target_T_fwd: float or function
A function which will take the number of Wavelengths points and return values [0,1]. Usually passed as a lambda function or a single float value for single wavelength FOM. To specify a more advanced spectrum one can define a function, it may be helpful to use, numpy windows as a template.
: norm_p: float
Is the generalized p-norm used in the FOM calculation. The p-norm, with \( p \geq 1 \) allows the user to increase the weight of the error. Since \( p=1 \) provides a lower bound on this function, a higher p-number will increase the weight of the error term. Default p =1.0
: target_fom: float
A target value for the figure of merit. This will change the behavior of the printing and plotting only. If this is enabled, by setting a value other than 0.0, the distance of the current FOM is given. Default = 0.0
Example
class ModeMatch(monitor_name = 'fom', mode_number = 3, direction = 'Backward', target_T_fwd = lambda wl: np.ones(wl.size), norm_p = 1)
Optimization Classes
Here we describe the generic ScipyOptimizer wrapper, and lumopt Optimization class which is used to encapsulate the project.
ScipyOptimizer
This is a wrapper for the generic and powerful SciPy optimization package.
from lumopt.optimizers.generic_optimizers import ScipyOptimizers
Class ScipyOptimizers(max_iter,
method,
scaling_factor,
pgtol,
ftol,
scale_initial_gradient_to,
penalty_fun,
penalty_jac)
: max_iter: int
Maximum number of iterations; each iteration can make multiple figure of merit and gradient evaluations. Default = 100
: method: str
Chosen minimization algorithm; experimenting with this option should only be done by advanced users. Default = ‘L-BFGS-B'
: scaling_factor: none, float, np.array
None, scalar or a vector the same length as the optimization parameters. This is used to scale the optimization parameters. As of 2021R1.1, the default behavior in shape optimization is to automatically map the parameters the range [0,1] within the optimization routines; which was always the case in topology. The bounds, defined in the geometry class, or eps_min/eps_max are used for this. Default = None
: pgtol: float
The iteration will stop when \( \max( |\text{proj }g_i | \text{ i = 1, ..., n} ) <= pgtol| \) where \( g_i \) is the i-th component of the projected gradient. Default = 1.0e-5
: ftol: float
The iteration stops when \( \left(( f^k - f^{k+1}) / \max (| f^k |\text{ , }|f^{k+1}|\text{ , }1 ) \right) <=ftol \). Default = 1.0e-5
: scale_initial_gradient_to: float
Enforces a rescaling of the gradient to change the optimization parameters by at least this much; the default value of zero disables automatic scaling. Default = 0.0
: penalty_fun: function, optional
Penalty function to be added to the figure of merit; it must be a function that takes a vector with the optimization parameters and returns a single value. Advanced feature. Default = None
:penalty_jac: function, optional
The gradient of the penalty function; must be a function that takes a vector with the optimization parameters and returns a vector of the same length. If a penalty_fun is included with no penalty_jac, lumopt will approximate the derivative. Advanced feature. Default = None
Example
optimizer = ScipyOptimizers(max_iter = 200,
method = 'L-BFGS-B',
scaling_factor = 1.0,
pgtol = 1.0e-5,
ftol = 1.0e-5,
scale_initial_gradient_to = 0.0,
penalty_fun = penalty_fun,
penalty_jac = None)
Optimization
Encapuslates and orchestrates all of the optimization pieces, and routines. Calling the opt.run method will perform the optimization.
from lumopt.optimization import Optimization
class Optimization(base_script,
wavelengths,
fom,
geometry,
optimizer,
use_var_fdtd,
hide_fdtd_cad,
use_deps,
plot_history,
store_all_simulations,
save_global_index,
label,
source_name)
: base_script: callable, or str
Base simulation - See project init.
- Python function in the workspace
- String that points to base file
- Variable that loads from lsf script
: wavelengths: float or class Wavelengths
Provides the optimization bandwidth. Float value for single wavelength optimization and Wavelengths class provides a broadband spectral range for all simulations.
: fom: class ModeMatch
The figure of merit FOM, see ModeMatch
: geometry: Lumopt geometry class
This defines the optimizable geometry, see Optimizeable Geometry
: optimizer: class ScipyOptimizers
See ScipyOptimizer for more information.
: hide_fdtd_cad: bool
Flag to run FDTD CAD in the background. Default = False
: use_deps: bool
Flag to use the numerical derivatives calculated directly from FDTD. Default = True
: plot_history: bool
Plot the history of all parameters (and gradients). Default = True
: store_all_simulations: bool
Indicates if the project file for each iteration should be stored or not. Default = True
: save_global_index: bool
Flag to save the results from a global index monitor to file after each iteration (used for visualization purposes). Default = False
: label: str, optional
If the optimization is part of a super-optimization, this string is used for the legend of the corresponding FOM plot. Default = None
: source_name: str, optional
Name of the source object in the simulation project. Default = "source"
Example
opt_2d = Optimization(base_script = base_sim_2d,
wavelengths = wavelengths,
fom = fom,
geometry = geometry,
optimizer = optimizer,
use_var_fdtd = False,
hide_fdtd_cad = True,
use_deps = True,
plot_history = True,
store_all_simulations = True,
save_global_index = False,
label = None)
Note(Advanced): For 2020R2 we exposed, a prototype user-requested debugging function that allows the user to perform checks of the gradient calculation. This is a method of the optimization class and can be called as follows.
opt.check_gradient(intitial_guess, dx=1e-3)
Where initial_guess is a numpy array that specifies the optimization parameters at which the gradient should be checked. The scalar parameter dx is used for the central finite difference approximation
Has two limitations; one the check performs an unnecessary adjoint simulation. Two the check_gradient is only available for regular optimizations and not superoptimizations of multiple FOMs.
Superoptimization
The + operator has been overloaded in the optimization class so it is trivial to simultaneously optimize:
- Different output waveguides and\or wavelength bands CWDM
- Ensure balanced TE\TM performance or suppress one over the other
- Robust manufacturing by simultaneously optimizing underetch\overtech\nominal variations.
- Etc ...
Simply adds the various Optimization classes together to create a new SuperOptimization object. Then you will call run on this object to co-optimize the various optimization definitions. Each FOM calculation requires 1 optimization object so the number of simulations at each iteration will be \( N_{sim} = 2\times N_{FOM} \).
Example
opt = opt_TE + opt_TM
opt.run(working_dir = working_dir)