diff --git a/docs/all_others.rst b/docs/all_others.rst index 468d55b..538670b 100644 --- a/docs/all_others.rst +++ b/docs/all_others.rst @@ -7,7 +7,7 @@ Fields Gradient Fields =============== -.. autoclass:: lumopt.utilities.gradients.Gradient_fields +.. autoclass:: lumopt.utilities.gradients.GradientFields :members: Others diff --git a/docs/foms.rst b/docs/foms.rst index c717710..3e242c0 100644 --- a/docs/foms.rst +++ b/docs/foms.rst @@ -3,9 +3,3 @@ Figures of Merit .. autoclass:: lumopt.figures_of_merit.modematch.ModeMatch - -.. autoclass:: lumopt.figures_of_merit.field_intensities.FieldIntensity - - -.. autoclass:: lumopt.figures_of_merit.field_intensities.FieldIntensities - diff --git a/docs/geometries.rst b/docs/geometries.rst index 90d6aec..431236b 100644 --- a/docs/geometries.rst +++ b/docs/geometries.rst @@ -3,4 +3,4 @@ Geometries .. autoclass:: lumopt.geometries.polygon.Polygon -.. autoclass:: lumopt.geometries.polygon.function_defined_Polygon +.. autoclass:: lumopt.geometries.polygon.FunctionDefinedPolygon diff --git a/docs/materials.rst b/docs/materials.rst index 21fa357..636ee91 100644 --- a/docs/materials.rst +++ b/docs/materials.rst @@ -2,4 +2,3 @@ Materials ========= .. autoclass:: lumopt.utilities.materials.Material - :members: \ No newline at end of file diff --git a/docs/optimization.rst b/docs/optimization.rst index aed361a..6c3e5a2 100644 --- a/docs/optimization.rst +++ b/docs/optimization.rst @@ -1,8 +1,6 @@ Optimization ============ -.. autoclass:: lumopt.optimization.Super_Optimization - +.. autoclass:: lumopt.optimization.SuperOptimization .. autoclass:: lumopt.optimization.Optimization - :members: run diff --git a/docs/optimizers.rst b/docs/optimizers.rst index 9a43663..5698ea1 100644 --- a/docs/optimizers.rst +++ b/docs/optimizers.rst @@ -1,9 +1,8 @@ Optimizers ========== -.. autoclass:: lumopt.optimizers.generic_optimizers.FixedStepGradientDescent - - .. autoclass:: lumopt.optimizers.generic_optimizers.ScipyOptimizers -.. autoclass:: lumopt.optimizers.generic_optimizers.Adaptive_Gradient_Descent \ No newline at end of file +.. autoclass:: lumopt.optimizers.fixed_step_gradient_descent.FixedStepGradientDescent + +.. autoclass:: lumopt.optimizers.adaptive_gradient_descent.AdaptiveGradientDescent \ No newline at end of file diff --git a/docs/user_facing.rst b/docs/user_facing.rst index 4280ffe..1cf5854 100644 --- a/docs/user_facing.rst +++ b/docs/user_facing.rst @@ -1,5 +1,5 @@ -User Functions -============== +User Classes and Functions +========================== .. toctree:: :maxdepth: 2 diff --git a/lumopt/figures_of_merit/modematch.py b/lumopt/figures_of_merit/modematch.py index cf660b4..d65af46 100644 --- a/lumopt/figures_of_merit/modematch.py +++ b/lumopt/figures_of_merit/modematch.py @@ -11,19 +11,24 @@ class ModeMatch(object): - """ Figure of Merit class based on the overlap integral between the fields recorded by a DFT monitor and the selected mode. - The class adds a mode expansion monitor to the simulation and links it to the provided DFT monitor. The mode expansion - monitor is used to retrieve the overlap result once the provided simulation has run. The overlap integral is that of - equation (7) of `https://doi.org/10.1364/OE.21.021693`, which is equivalent to the overlap integral built into FDTD and - described in `https://kb.lumerical.com/ref_sim_obj_using_mode_expansion_monitors.html`. - + """ Calculates the figure of merit from an overlap integral between the fields recorded by a field monitor and the slected mode. + A mode expansion monitor is added to the field monitor to calculate the overlap result, which appears as T_forward in the + list of mode expansion monitor results. The T_forward result is described in the following page: + + https://kb.lumerical.com/ref_sim_obj_using_mode_expansion_monitors.html + + This result is equivalent to equation (7) in the following paper: + + C. Lalau-Keraly, S. Bhargava, O. Miller, and E. Yablonovitch, "Adjoint shape optimization applied to electromagnetic design," + Opt. Express 21, 21693-21701 (2013). https://doi.org/10.1364/OE.21.021693 + Parameters ---------- - :param monitor_name: name of the DFT monitor that records the fields to be used in the mode overlap calculation. - :param mode_number: selected mode in the list generated by the mode expansion monitor. - :param direction: direction of propagation of the mode injected by the source. - :target_T_fwd: function describing the target T_forward (provided by mode expansion monitors); used to generate the FOM. - :norm_p: exponent of the p-norm used to generate the figure of merit; use to generate the FOM. + :param monitor_name: name of the field monitor that records the fields to be used in the mode overlap calculation. + :param mode_number: mode number in the list of modes generated by the mode expansion monitor. + :param direction: direction of propagation ('Forward' or 'Backward') of the mode injected by the source. + :param target_T_fwd: function describing the target T_forward vs wavelength (see documentation for mode expansion monitors). + :param norm_p: exponent of the p-norm used to generate the figure of merit; use to generate the FOM. """ def __init__(self, monitor_name, mode_number, direction, target_T_fwd = lambda wl: np.ones(wl.size), norm_p = 1): diff --git a/lumopt/geometries/polygon.py b/lumopt/geometries/polygon.py index 6023d38..98d5f3a 100644 --- a/lumopt/geometries/polygon.py +++ b/lumopt/geometries/polygon.py @@ -12,44 +12,34 @@ class Polygon(Geometry): - '''An polygon extruded in the z direction, where the points are allowed to move in any direction in the x-y plane. The points - and extrusion parameters must be defined, as well as the permittivity (or material) forming the inside of the polygon and the permittivity - (or material) surrounding the polygon. If the Polygon is surrounded by different materials, the shape derivatives will be wrong along the edges - where the wrong material surrounds the polygon. - - - :param points: - The points are defined as a numpy array of tupple coordinates np.array([(x0,y0),...,(xn,yn)]). THEY MUST BE DEFINED IN A - COUNTER CLOCKWISE DIRECTION. - :param z: - The center of the polygon along the z axis - :param depth: - The depth of the extrusion in the z direction (in meters) - :param eps_out: - The permittivity of the outer-material (square of refractive index), or the name of a Lumerical Material, from which the permittivity - will be extracted. Can also be a Material object from :class:`lumpot.utilities.materials.Material` with a defined mesh order. - :param eps_in: - The permittivity of the inner-material (square of refractive index), or the name of a Lumerical Material, from which the permittivity - will be extracted. Can also be a Material object from :class:`lumpot.utilities.materials.Material` with a defined mesh order. - :param edge_precision: - The edges will be discretized when calculating the gradients with respect to moving different points of the geometry. This parmeter - will define the number of discretization points per edge. It is strongly recommended to have at least a few points per mesh cell. - ''' - - self_update=False - - def __init__(self,points, z, depth, eps_out, eps_in, edge_precision, bounds, dx): - self.points=points - self.z=z - self.depth=depth - self.gradients=[] - self.edge_precision=edge_precision - self.dx=dx + """ + Defines a polygon with vertices on the (x,y)-plane that are extruded along the z direction to create a 3-D shape. The vertices are + defined as a numpy array of coordinate pairs np.array([(x0,y0),...,(xn,yn)]). THE VERTICES MUST BE ORDERED IN A COUNTER CLOCKWISE DIRECTION. + + :param points: array of shape (N,2) defining N polygon vertices. + :param z: center of polygon along the z-axis. + :param depth: span of polygon along the z-axis. + :param eps_out: permittivity of the material around the polygon. + :param eps_in: permittivity of the polygon material. + :param edge_precision: number of quadrature points along each edge for computing the FOM gradient using the shape derivative approximation method. + """ + + def __init__(self, points, z, depth, eps_out, eps_in, edge_precision): + self.points = points + self.z = float(z) + self.depth = float(depth) + self.edge_precision = int(edge_precision) self.eps_out = eps_out if isinstance(eps_out, Material) else Material(eps_out) self.eps_in = eps_in if isinstance(eps_in, Material) else Material(eps_in) + + if self.depth <= 0.0: + raise UserWarning("polygon depth must be positive.") + if self.edge_precision <= 0: + raise UserWarning("edge precision must be a positive integer.") + + self.gradients = list() self.make_edges() self.hash = random.getrandbits(64) - return def make_edges(self): '''Creates all the edge objects''' @@ -132,45 +122,41 @@ def plot(self,ax): class FunctionDefinedPolygon(Polygon): - '''This defines a polygon from a function that takes the optimization parameters and returns a set of points. - - :param func: - A function that takes as input a list of optimization parameters and returns a list of point coordinates forming - the polygon to optimize. See example :func:`~lumpot.geometries.polygon.taper_splitter`. - The points are defined as a numpy array of tupple coordinates np.array([(x0,y0),...,(xn,yn)]). - THEY MUST BE DEFINED IN A COUNTER CLOCKWISE DIRECTION. - :param initial_params: - The initial parameters, which when fed to the previously defined function, will generate the starting geometry of - the optimization - :param Bounds: - The bounds that should be applied on the optimization parameters - :param z: - see :class:`~lumpot.geometries.polygon.Polygon` - :param depth: - see :class:`~lumpot.geometries.polygon.Polygon` - :param eps_out: - see :class:`~lumpot.geometries.polygon.Polygon` - :param eps_in: - see :class:`~lumpot.geometries.polygon.Polygon` - :param edge_precision: - see :class:`~lumpot.geometries.polygon.Polygon` - ''' - - def __init__(self, func, initial_params, bounds, z, depth, eps_out, eps_in, edge_precision, dx): - self.points=func(initial_params) - self.func=func - self.z=z - self.current_params=initial_params - self.depth=depth - self.gradients=[] - self.edge_precision=edge_precision - self.bounds=bounds - self.params_hist=[initial_params] - self.eps_out = eps_out if isinstance(eps_out, Material) else Material(eps_out) - self.eps_in = eps_in if isinstance(eps_in, Material) else Material(eps_in) - self.make_edges() - self.dx=dx - self.hash = random.getrandbits(128) + """ + Constructs a polygon from a user defined function that takes the optimization parameters and returns a set of vertices defining a polygon. + The polygon vertices returned by the function must be defined as a numpy array of coordinate pairs np.array([(x0,y0),...,(xn,yn)]). THE + VERTICES MUST BE ORDERED IN A COUNTER CLOCKWISE DIRECTION. + + Parameters + ---------- + :param fun: function that takes the optimization parameter values and returns a polygon. + :param initial_params: initial optimization parameter values. + :param bounds: bounding ranges (min/max pairs) for each optimization parameter. + :param z: center of polygon along the z-axis. + :param depth: span of polygon along the z-axis. + :param eps_out: permittivity of the material around the polygon. + :param eps_in: permittivity of the polygon material. + :param edge_precision: number of quadrature points along each edge for computing the FOM gradient using the shape derivative approximation method. + :param dx: step size for computing the FOM gradient using permittivity perturbations. + """ + + def __init__(self, func, initial_params, bounds, z, depth, eps_out, eps_in, edge_precision = 5, dx = 1.0e-10): + self.func = func + self.current_params = initial_params + points = func(initial_params) + super(FunctionDefinedPolygon, self).__init__(points, z, depth, eps_out, eps_in, edge_precision) + self.bounds = bounds + self.dx = float(dx) + + if len(self.bounds) != self.current_params.size: + raise UserWarning("there must be one bound for each parameter.") + for bound in self.bounds: + if bound[1] - bound[0] <= 0.0: + raise UserWarning("bound ranges must be positive.") + if self.dx <= 0.0: + raise UserWarning("step size must be positive.") + + self.params_hist = list(initial_params) def update_geometry(self,params): self.points=self.func(params) diff --git a/lumopt/optimization.py b/lumopt/optimization.py index 42f8d9c..ccdad0f 100644 --- a/lumopt/optimization.py +++ b/lumopt/optimization.py @@ -14,19 +14,25 @@ from lumopt.utilities.gradients import GradientFields from lumopt.figures_of_merit.modematch import ModeMatch from lumopt.utilities.plotter import Plotter -from lumopt.utilities.scipy_wrappers import trapz3D +from lumopt.lumerical_methods.lumerical_scripts import get_fields -class Super_Optimization(object): - ''' Optimization base class which allows the user to use the addition operator to co-optimize two figures of merit - that take the same parameters. The figures of merit are simply added and the plotting functions use the first - figure of merit.''' +class SuperOptimization(object): + """ + Optimization super class to run two or more co-optimizations targeting different figures of merit that take the same parameters. + The addition operator can be used to aggregate multiple optimizations. All the figures of merit are simply added to generate + an overall figure of merit that is passed to the chosen optimizer. + + Parameters + ---------- + :param optimizations: list of co-optimizations (each of class Optimization). + """ def __init__(self,optimizations): self.optimizations=optimizations def __add__(self,other): optimizations=[self,other] - return Super_Optimization(optimizations) + return SuperOptimization(optimizations) def initialize(self,start_params=None,bounds=None): @@ -75,23 +81,26 @@ def run(self): return self.optimizer.fom_hist[-1],self.optimizer.params_hist[-1] -class Optimization(Super_Optimization): - """ Acts as a master class for all the optimization pieces. Calling the function run will perform the full optimization. - To perform an optimization, four key pieces are requred. These are: - 1) a script to generate the base simulation, - 2) an object that defines and collects the figure of merit, - 3) an object that generates the shape under optimization for a given set of optimization parameters and - 4) a SciPy gradient based minimizer. - - :base_script: string with script to generate the base simulation. - :fom: figure of merit object (see class ModeMatch). - :geometry: optimizable geometry (see class FunctionDefinedPolygon). - :optimizer: SciyPy minimizer wrapper (see class ScipyOptimizers). - :use_deps: use the numerical derivatives provided by FDTD. +class Optimization(SuperOptimization): + """ Acts as orchestrator for all the optimization pieces. Calling the member function run will perform the optimization, + which requires four key pieces: + 1) a script to generate the base simulation, + 2) an object that defines and collects the figure of merit, + 3) an object that generates the shape under optimization for a given set of optimization parameters and + 4) a gradient based optimizer. + + Parameters + ---------- + :param base_script: string with script to generate the base simulation (helper function load_from_lsf). + :param wavelengths: wavelength value (float) or range (class Wavelengths) with the spectral range for all simulations. + :param fom: figure of merit (class ModeMatch). + :param geometry: optimizable geometry (class FunctionDefinedPolygon). + :param optimizer: SciyPy minimizer wrapper (class ScipyOptimizers). + :param hide_fdtd: flag run FDTD CAD in the background. + :param use_deps: flag to use the numerical derivatives calculated directly from FDTD. """ def __init__(self, base_script, wavelengths, fom, geometry, optimizer, hide_fdtd_cad = False, use_deps = True): - self.base_script = base_script self.wavelengths = wavelengths if isinstance(wavelengths, Wavelengths) else Wavelengths(wavelengths) self.fom = fom @@ -126,143 +135,99 @@ def run(self): print('FINAL PARAMETERS = {}'.format(self.optimizer.params_hist[-1])) return self.optimizer.fom_hist[-1],self.optimizer.params_hist[-1] - def initialize(self): + """ Performs all steps that need to be carried only once at the beginning of the optimization. """ + start_params = self.geometry.get_current_params() callable_fom = self.callable_fom callable_jac = self.callable_jac bounds = np.array(self.geometry.bounds) def plotting_function(): self.plotter.update(self) - self.optimizer.initialize(start_params=start_params,callable_fom=callable_fom,callable_jac=callable_jac,bounds=bounds,plotting_function=plotting_function) - - def make_sim(self, geometry = None): - '''Creates the forward simulation by adding the geometry to the base simulation and adding D monitors to any field monitor in the simulation. - If the FOM object needs it's own monitors (or needs to manipulate those already present) they will also be added/manipulated. - - :param geometry: By default the current gometry of the Optimization #will be put in, but this can be overriden by inputing another geometry here - - :returns: sim, Handle to the simulation ''' - - # create the simulation object - sim = Simulation(self.workingDir, self.base_script, self.hide_fdtd_cad) - Optimization.set_global_wavelength(sim, self.wavelengths) - Optimization.set_source_wavelength(sim, 'source', len(self.wavelengths)) - sim.fdtd.setnamed('opt_fields', 'override global monitor settings', False) - sim.fdtd.setnamed('opt_fields', 'spatial interpolation', 'none') - Optimization.add_index_monitor(sim, 'opt_fields') - if(self.use_deps): - Optimization.set_use_legacy_conformal_interface_detection(sim, False) - + self.optimizer.initialize(start_params = start_params, callable_fom = callable_fom, callable_jac = callable_jac, bounds = bounds, plotting_function = plotting_function) + self.sim = Simulation(self.workingDir, self.hide_fdtd_cad) + + def make_forward_sim(self, geometry = None): + """ Creates the forward simulation by adding the geometry to the base simulation and adding a refractive index monitor overlaping + with the 'opt_fields' monitor. The 'source' object is modified to follow the global frequency settings. + + :geometry: the current gometry under optimization. + """ + + self.sim.fdtd.switchtolayout() + self.sim.fdtd.deleteall() + self.sim.fdtd.eval(self.base_script) + Optimization.set_global_wavelength(self.sim, self.wavelengths) + Optimization.set_source_wavelength(self.sim, 'source', len(self.wavelengths)) + self.sim.fdtd.setnamed('opt_fields', 'override global monitor settings', False) + self.sim.fdtd.setnamed('opt_fields', 'spatial interpolation', 'none') + Optimization.add_index_monitor(self.sim, 'opt_fields') + if self.use_deps: + Optimization.set_use_legacy_conformal_interface_detection(self.sim, False) time.sleep(0.1) - - # add the optimizable geometry if geometry is None: - self.geometry.add_geo(sim, params = None, only_update = False) + self.geometry.add_geo(self.sim, params = None, only_update = False) else: - geometry.add_geo(sim) - # add the index monitors - self.fom.add_to_sim(sim) + geometry.add_geo(self.sim) + self.fom.add_to_sim(self.sim) - return sim - - def get_fom_geo(self, geometry=None): - - '''Will make and run the simulation, extract the figure of merit and return it - - :param geometry: If None, the current geometry will be used - :returns: fom, The figure of merit calculated''' - - # create the simulation - sim = self.make_sim(geometry=geometry) - # run the simulation - sim.run(self.iteration) - # get the fom - fom = self.fom.get_fom(sim) - sim.close() - return fom - - def run_forward_solves(self, plotfields=False): - ''' - Generates the new forward simulations, runs them and computes the figure of merit and forward fields - - :param plotfields: Will plot the fields if True - - Since this assumes that this is used only in an optimization loop, the figure of merit is recorded and appended - to the fomHist - ''' + def run_forward_solves(self): + """ Generates the new forward simulations, runs them and computes the figure of merit and forward fields. """ print('Running forward solves') - - # create the simulation - forward_sim = self.make_sim() - - # run the simulation - forward_sim.run(name = 'forward', iter = self.optimizer.iteration) - - # get the fields used for gradient calculation - self.forward_fields = forward_sim.get_gradient_fields('opt_fields') - - # get the fom - fom = self.fom.get_fom(forward_sim) + self.make_forward_sim() + self.sim.run(name = 'forward', iter = self.optimizer.iteration) + self.forward_fields = get_fields(self.sim.fdtd, monitor_name = 'opt_fields', get_eps = True, get_D = True, get_H = True, nointerpolation = True) + fom = self.fom.get_fom(self.sim) self.fomHist.append(fom) - - forward_sim.close() - print('FOM = {}'.format(fom)) return fom - def run_adjoint_solves(self, plotfields=False): - ''' - Generates the adjoint simulations, runs them and extacts the adjoint fields - ''' + def run_adjoint_solves(self): + """ Generates the adjoint simulations, runs them and extacts the adjoint fields. """ print('Running adjoint solves') + self.make_forward_sim() + self.sim.fdtd.selectpartial('source') + self.sim.fdtd.delete() + self.fom.add_adjoint_sources(self.sim) + self.sim.run(name = 'adjoint', iter = self.optimizer.iteration) + self.adjoint_fields = get_fields(self.sim.fdtd, monitor_name = 'opt_fields', get_eps = True, get_D = True, get_H = True, nointerpolation = True) + self.adjoint_fields.scale(3, self.fom.get_adjoint_field_scaling(self.sim)) + + def callable_fom(self, params): + """ Function for the optimizers to retrieve the figure of merit. + :param params: geometry parameters. + :returns: figure of merit. + """ - adjoint_sim = self.make_sim() - - # Remove the forward sources and add the adjoint sources - adjoint_sim.remove_sources() - self.fom.add_adjoint_sources(adjoint_sim) - - adjoint_sim.run(name = 'adjoint', iter = self.optimizer.iteration) - - self.adjoint_fields = adjoint_sim.get_gradient_fields('opt_fields') - self.adjoint_fields.scale(3, self.fom.get_adjoint_field_scaling(adjoint_sim)) - adjoint_sim.close() - - - def callable_fom(self,params): - '''A callable function for the optimizers for the figure of merit - :param params: The geometry parameters - - :returns: the fom - ''' self.geometry.update_geometry(params) fom = self.run_forward_solves() return fom - def callable_jac(self,params): - '''A callable function for the optimizer that returns derivatives with respect to the parameters - - :param params: The geometry paramaters, but actually these aren't used - :returns: The gradients''' + def callable_jac(self, params): + """ Function for the optimizer to extract the figure of merit gradient. + :params: geometry paramaters, currently not used. + :returns: partial derivative of the figure of merit with respect to each optimization parameter. + """ self.run_adjoint_solves() gradients = self.calculate_gradients() return np.array(gradients) def calculate_gradients(self): - ''' Calculates the gradient of the figure of merit (FOM) with respect to each of the optimization parameters. + """ Calculates the gradient of the figure of merit (FOM) with respect to each of the optimization parameters. It assumes that both the forward and adjoint solves have been run so that all the necessary field results - have been collected.''' + have been collected. There are currently two methods to compute the gradient: + 1) using the permittivity derivatives calculated directly from meshing (use_deps == True) and + 2) using the shape derivative approximation described in Owen Miller's thesis (use_deps == False). + """ print('Calculating gradients') self.gradient_fields = GradientFields(forward_fields = self.forward_fields, adjoint_fields = self.adjoint_fields) if self.use_deps: - sim = self.make_sim() - d_eps = self.geometry.get_d_eps(sim) - sim.close() + self.make_forward_sim() + d_eps = self.geometry.get_d_eps(self.sim) fom_partial_derivs_vs_wl, wl = self.gradient_fields.spatial_gradient_integral(d_eps) self.gradients = self.fom.fom_gradient_wavelength_integral(fom_partial_derivs_vs_wl, wl) else: diff --git a/lumopt/optimizers/adaptive_gradient_descent.py b/lumopt/optimizers/adaptive_gradient_descent.py index 01a735d..7fdcd03 100644 --- a/lumopt/optimizers/adaptive_gradient_descent.py +++ b/lumopt/optimizers/adaptive_gradient_descent.py @@ -8,26 +8,25 @@ class AdaptiveGradientDescent(Optimizer): """ Almost identical to FixedStepGradientDescent, except that dx changes according to the following rule: - dx = min(max_dx,dx*dx_regrowth_factor) - while newfom < oldfom - dx = dx / 2 - if dx < min_dx: - dx = min_dx - return newfom - """ + dx = min(max_dx,dx*dx_regrowth_factor) + while newfom < oldfom + dx = dx / 2 + if dx < min_dx: + dx = min_dx + return newfom - def __init__(self, max_dx, min_dx, max_iter, dx_regrowth_factor, all_params_equal, scaling_factor): - ''' + Parameters + ---------- :param max_dx: maximum allowed change of a parameter per iteration. :param min_dx: minimum step size (for the largest parameter changing) allowed. :param dx_regrowth_factor: by how much dx will be increased at each iteration. :param max_iter: maximum number of iterations to run. :param all_params_equal: if true, all parameters will be changed by +/- dx depending on the sign of their associated shape derivative. :param scaling_factor: scaling factor to bring the optimization variables close to one. - ''' + """ + def __init__(self, max_dx, min_dx, max_iter, dx_regrowth_factor, all_params_equal, scaling_factor): super(AdaptiveGradientDescent,self).__init__(max_iter, scaling_factor) - self.max_dx = max_dx * self.scaling_factor self.all_params_equal = all_params_equal self.predictedchange_hist = [] diff --git a/lumopt/optimizers/fixed_step_gradient_descent.py b/lumopt/optimizers/fixed_step_gradient_descent.py index 1525ad3..681d4a4 100644 --- a/lumopt/optimizers/fixed_step_gradient_descent.py +++ b/lumopt/optimizers/fixed_step_gradient_descent.py @@ -7,31 +7,27 @@ from lumopt.optimizers.generic_optimizers import Optimizer class FixedStepGradientDescent(Optimizer): - """ Gradient descent with the option to add noise, and parameter scaling - - Update Equation: + """ Gradient descent with the option to add noise and a parameter scaling. The update equation is: \Delta p_i = \frac{\frac{dFOM}{dp_i}}{max_j(|\frac{dFOM}{dp_j}|)}\Delta x +\text{noise}_i - if all_params_equal = True + If all_params_equal = True, then the update equation is: \Delta p_i = sign(\frac{dFOM}{dp_i})\Delta x +\text{noise}_i - Noise can be added in the update equation, if the optimization has many local optima: noise = rand([-1,1])*noise_magnitude. - - """ + If the optimization has many local optima: noise = rand([-1,1])*noise_magnitude. - def __init__(self, max_dx, max_iter, all_params_equal, noise_magnitude, scaling_factor): - """ + Parameters + ---------- :param max_dx: maximum allowed change of a parameter per iteration. :param max_iter: maximum number of iterations to run. :param all_params_equal: if true, all parameters will be changed by +/- dx depending on the sign of their associated shape derivative. :param noise_magnitude: amplitude of the noise. :param scaling_factor: scaling factor to bring the optimization variables closer to 1 - """ + """ + def __init__(self, max_dx, max_iter, all_params_equal, noise_magnitude, scaling_factor): super(FixedStepGradientDescent, self).__init__(max_iter, scaling_factor) - self.max_dx = max_dx * self.scaling_factor self.all_params_equal = all_params_equal self.noise_magnitude = noise_magnitude * self.scaling_factor diff --git a/lumopt/optimizers/generic_optimizers.py b/lumopt/optimizers/generic_optimizers.py index 24b1df9..7f9c87e 100644 --- a/lumopt/optimizers/generic_optimizers.py +++ b/lumopt/optimizers/generic_optimizers.py @@ -7,29 +7,38 @@ from lumopt.optimizers.optimizer import Optimizer class ScipyOptimizers(Optimizer): - '''Using scipy's optimizers to perform the optimizations. Some of the algorithms (L-BFGS-G in particular) can approximate - the Hessian from the different optimization steps (also called Quasi-Newton Optimization). While this is very powerfull, - the derivatives calculated here using a continuous adjoint method can be noisy, which in turn can lead to poor behavior of - these Quasi-Newton methods, which expect machine precision derivatices. Therefore these methods are to be used with some caution. + """ Wrapper for the optimizers in SciPy's optimize package: - Checkout documentation of different optimization methods at :meth:`scipy.optimize.minimize` + https://docs.scipy.org/doc/scipy/reference/optimize.html#module-scipy.optimize - ''' + Some of the optimization algorithms available in the optimize package ('L-BFGS-G' in particular) can approximate the Hessian from the + different optimization steps (also called Quasi-Newton Optimization). While this is very powerfull, the figure of merit gradient calculated + from a simulation using a continuous adjoint method can be noisy. This can point Quasi-Newton methods in the wrong direction, so use them + with caution. - def __init__(self, max_iter, method, scaling_factor, pgtol): - ''' - :param max_iter: maximum number of iterations to run the optimizer for. This is not necessarily equal to the number of times a direct/adjoint simulation pair will be run, since these methods can make several calls for each iteration - :param method: a string which defines which optimization algorithm to use (see :meth:`scipy.optimize.minimize`) - :param scaling_factor: See :class:`~lumopt.optimzers.generic_optimizers.Optimizer`. The scaling factor is particularly important for scipy optimizers not to freak out. - ''' - super(ScipyOptimizers,self).__init__(max_iter,scaling_factor) + Parameters + ---------- + :param max_iter: maximum number of iterations; each iteration can make multiple figure of merit and gradient evaluations. + :param method: string with the chosen minimization algorithm. + :param scaling_factor: optimization parameters are scaled by this factor. + :param pgtol: gradient tolerance paramter 'gtol' (see 'BFGS' or 'L-BFGS-G' documentation). + """ - self.method=method - self.fom_calls=0 - self.pgtol=pgtol + def __init__(self, max_iter, method = 'L-BFGS-G', scaling_factor = 1.0, pgtol = 1.0e-5): + super(ScipyOptimizers,self).__init__(max_iter, scaling_factor) + self.method = str(method) + self.fom_calls = int(0) + self.pgtol = float(pgtol) def define_callables(self,callable_fom,callable_jac): - '''This makes the function that is callable by scipy's optimization method''' + """ Defines the functions that the optimizer will use to evaluate the figure of merit and its gradient. The sign + of the figure of merit and its gradient are flipped here to perform a maximization rather than a minimization. + + Parameters + ---------- + :param callable_fom: function taking a numpy vector of optimization parameters and returning the figure of merit. + :param callable_jac: function taking a numpy vector of optimization parameters and returning a vector of the same size with the figure of merit gradients. + """ def callable_fom_local(params): fom=callable_fom(params/self.scaling_factor) diff --git a/lumopt/utilities/fields.py b/lumopt/utilities/fields.py index cddec92..e4bace0 100644 --- a/lumopt/utilities/fields.py +++ b/lumopt/utilities/fields.py @@ -9,10 +9,10 @@ import matplotlib.pyplot as plt class Fields(object): - '''This object is created from fields loaded from Lumerical m=field monitors. Several iterpolation objects are then created internally to - enable the easy access of the fields in the simulation space - - Use :method:`~lumopt.lumerical_methods.lumerical_scripts.get_fields` to load the data properly''' + """ + Container for the raw fields from a field monitor. Several interpolation objects are created internally to evaluate the fields + at any point in space. Use the auxiliary :method:lumopt.lumerical_methods.lumerical_scripts.get_fields to create this object. + """ def __init__(self,x,y,z,wl,E,D,eps,H): @@ -63,7 +63,6 @@ def field_interpolator(x,y,z,wl): return field_interpolator def plot(self,ax,title,cmap): - '''Plots E^2 for the plotter''' ax.clear() xx, yy = np.meshgrid(self.x, self.y) z = (min(self.z) + max(self.z))/2 + 1e-10 @@ -79,7 +78,6 @@ def plot(self,ax,title,cmap): ax.set_ylabel('y (um)') def plot_full(self,D=False,E=True,eps=False,H=False,wl=1550e-9,original_grid=True): - '''Plot the different fields''' if E: self.plot_field(self.getfield,original_grid=original_grid,wl=wl,name='E') @@ -196,7 +194,6 @@ def field_interpolator(x, y, z, wl): return field_interpolator def plot(self,ax,title,cmap): - '''Plots E^2 for the plotter''' ax.clear() xx, yy = np.meshgrid(self.x[1:-1], self.y[1:-1]) z = (min(self.z) + max(self.z))/2 + 1e-10 @@ -212,10 +209,13 @@ def plot(self,ax,title,cmap): ax.set_ylabel('y (um)') def scale(self, dimension, factors): - """ Scales the E, D and H field arrays along the specified dimension using the provided weighting factors. + """ + Scales the E, D and H field arrays along the specified dimension using the provided weighting factors. - :dimension: 0 (x-axis), 1 (y-axis), 2 (z-axis), (3) frequency and (4) vector component. - :factors: list or vector of weighting factors of the same size as the target field dimension. + Parameters + ---------- + :param dimension: 0 (x-axis), 1 (y-axis), 2 (z-axis), (3) frequency and (4) vector component. + :param factors: list or vector of weighting factors of the same size as the target field dimension. """ if hasattr(self.E, 'dtype'): diff --git a/lumopt/utilities/load_lumerical_scripts.py b/lumopt/utilities/load_lumerical_scripts.py index f79b3a7..fdb3d9a 100644 --- a/lumopt/utilities/load_lumerical_scripts.py +++ b/lumopt/utilities/load_lumerical_scripts.py @@ -2,7 +2,13 @@ Copyright (c) 2019 Lumerical Inc. """ def load_from_lsf(script_file_name): - ''' Loads an *.lsf scritp file and strips out all comments. ''' + """ + Loads the provided scritp as a string and strips out all comments. + + Parameters + ---------- + :param script_file_name: string specifying a file name. + """ with open(script_file_name, 'r') as text_file: lines = [line.strip().split(sep = '#', maxsplit = 1)[0] for line in text_file.readlines()] diff --git a/lumopt/utilities/materials.py b/lumopt/utilities/materials.py index 2a63a4d..21d0a20 100644 --- a/lumopt/utilities/materials.py +++ b/lumopt/utilities/materials.py @@ -8,19 +8,19 @@ from lumopt.utilities.wavelengths import Wavelengths class Material(object): - ''' Permittivity of a material associated with a geometric primitive. - - In FDTD Solutions, a material can be given in two ways: + ''' Permittivity of a material associated with a geometric primitive. In FDTD Solutions, a material can be given in two ways: 1) By providing a material name from the material database (e.g. 'Si (Silicon) - Palik') that can be assigned to a geometric primitive. 2) By providing a refractive index value directly in geometric primitive. To use the first option, simply set the name to '' and enter the desired base permittivity value. - To use the second option, set the name to the desired material name and the base permittivity to none. + To use the second option, set the name to the desired material name (base permittivity will be ignored). - :name: string (such as "Si (Silicon) - Palik") with a valid material name. - :base_epsilon: scalar base permittivity value. - :mesh_order: order of material resolution for overlapping primitives. + Parameters + ---------- + :param name: string (such as 'Si (Silicon) - Palik') with a valid material name. + :param base_epsilon: scalar base permittivity value. + :param mesh_order: order of material resolution for overlapping primitives. ''' object_dielectric = str('') diff --git a/lumopt/utilities/plotter.py b/lumopt/utilities/plotter.py index 2039e26..7d2f809 100644 --- a/lumopt/utilities/plotter.py +++ b/lumopt/utilities/plotter.py @@ -45,12 +45,12 @@ def update(self, optimization): if not opt.geometry.plot(self.ax[1,0]): opt.gradient_fields.plot_eps(self.ax[1,0]) opt.gradient_fields.plot(self.fig, self.ax[1,1], self.ax[0,1]) - print('Plots updated with optimization {0} iteration {1} results'.format(i, optimization.optimizer.iteration)) + print('Plots updated with optimization {0} iteration {1} results'.format(i, optimization.optimizer.iteration - 1)) else: if not optimization.geometry.plot(self.ax[1,0]): optimization.gradient_fields.plot_eps(self.ax[1,0]) optimization.gradient_fields.plot(self.fig, self.ax[1,1], self.ax[0,1]) - print('Plots updated with iteration {} results'.format(optimization.optimizer.iteration)) + print('Plots updated with iteration {} results'.format(optimization.optimizer.iteration - 1)) plt.tight_layout() self.fig.canvas.draw() self.fig.canvas.flush_events() diff --git a/lumopt/utilities/simulation.py b/lumopt/utilities/simulation.py index a392305..e1321bf 100644 --- a/lumopt/utilities/simulation.py +++ b/lumopt/utilities/simulation.py @@ -1,49 +1,27 @@ """ Copyright chriskeraly Copyright (c) 2019 Lumerical Inc. """ -from lumopt import CONFIG -import sys import lumapi -import lumopt.lumerical_methods.lumerical_scripts as ls - class Simulation(object): - ''' - Class that handles making and running simulations - ''' + """ + Object to manage the FDTD CAD. + + Parameters + ---------- + :param workingDir: working directory for the CAD session. + :param hide_fdtd_cad: if true, runs the FDTD CAD in the background. + """ - def __init__(self, workingDir, script, hide_fdtd_cad): - ''' - :param workingDir: Working directory to save the simulation before running it - :param script: (String) Base Lumerical script to execute when making the simulation - ''' + def __init__(self, workingDir, hide_fdtd_cad): + """ Launches FDTD CAD and stores a handle. """ self.fdtd = lumapi.FDTD(hide = hide_fdtd_cad) self.fdtd.cd(workingDir) - self.fdtd.eval(script) - - def run(self,name='forward',iter=None): - ''' - Saves (in the working directory) then runs the simulation with filename 'name_iter.fsp' - - :param name: prefix to the file name - :param iter: suffix to the file name - ''' + def run(self, name, iter): + """ Saves simulation file and runs the simulation. """ self.fdtd.save('{}_{}'.format(name,iter)) self.fdtd.run() - - def get_gradient_fields(self,monitor_name): - '''Extracts the fields in the optimizable region. These fields are needed to create the gradient fields''' - return ls.get_fields(self.fdtd, monitor_name, get_eps = True, get_D = True, get_H = False, nointerpolation = True) - - def remove_sources(self): - '''Removes all the sources present in a simulation. Is used to create the basis for the adjoint simulation. - The sources need to have 'source' in their name''' - self.fdtd.selectpartial('source') - self.fdtd.delete() - return - - - def close(self): + def __del__(self): self.fdtd.close()