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

- Initialize a python variable that specifies the path to the base file. Example Grating coupler.**Predefined simulation file**

`base_sim = os.path.join(os.path.dirname(__file__), 'grating_base.fsp')`

- Create a python variable using the**An lsf set-up script***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')

- This can be a function defined in the same file or an imported function. Example Y-branch.**Callable python code that does the set-up using the API**

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

Typically named source, but can be set in the Optimization class.**A mode source -**This is a static object, and the pixel/voxel size should be uniform.**A mesh overide region covering the optimization volume -**Should be named opt_field, these field values are important for the adjoint method.**A frequency monitor over the optimization volume -**Used to calculate the FOM. The name of this monitor is passed to the ModeMatch class.**A frequency monitor over the output waveguides -**

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)