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: callable, file name or plain string with script to generate the base simulation. :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_cad: flag run FDTD CAD in the background. :param use_deps: flag to use the numerical derivatives calculated directly from FDTD. :param plot_history: plot the history of all parameters (and gradients) :param store_all_simulations: Indicates if the project file for each iteration should be stored or not """ def __init__(self, base_script, wavelengths, fom, geometry, optimizer, use_var_fdtd=False, hide_fdtd_cad=False, use_deps=True, plot_history=True, store_all_simulations=True): self.base_script = base_script if isinstance( base_script, BaseScript) else BaseScript(base_script) self.wavelengths = wavelengths if isinstance( wavelengths, Wavelengths) else Wavelengths(wavelengths) self.fom = fom self.geometry = geometry self.optimizer = optimizer self.use_var_fdtd = bool(use_var_fdtd) self.hide_fdtd_cad = bool(hide_fdtd_cad) self.use_deps = bool(use_deps) self.plot_history = bool(plot_history) self.store_all_simulations = store_all_simulations self.unfold_symmetry = geometry.unfold_symmetry if self.use_deps: print("Accurate interface detection enabled") self.plotter = None #< Initialize later, when we know how many parameters there are self.fomHist = [] self.paramsHist = [] frame = inspect.stack()[1] calling_file_name = os.path.abspath(frame[0].f_code.co_filename) Optimization.goto_new_opts_folder(calling_file_name, base_script) self.workingDir = os.getcwd() def __del__(self): Optimization.go_out_of_opts_folder() def init_plotter(self): if self.plotter is None: self.plotter = Plotter(movie=True, plot_history=self.plot_history) return self.plotter def run(self): self.initialize() self.init_plotter() if self.plotter.movie: with self.plotter.writer.saving(self.plotter.fig, "optimization.mp4", 100): self.optimizer.run() else: self.optimizer.run() ## For topology optimization we are not done yet ... if hasattr(self.geometry, 'progress_continuation'): print(' === Starting Binarization Phase === ') self.optimizer.max_iter = 20 while self.geometry.progress_continuation(): self.optimizer.reset_start_params( self.optimizer.params_hist[-1], 0.05) #< Run the scaling analysis again self.optimizer.run() final_fom = np.abs(self.optimizer.fom_hist[-1]) print('FINAL FOM = {}'.format(final_fom)) print('FINAL PARAMETERS = {}'.format(self.optimizer.params_hist[-1])) return final_fom, self.optimizer.params_hist[-1] def initialize(self): """ Performs all steps that need to be carried only once at the beginning of the optimization. """ # FDTD CAD self.sim = Simulation(self.workingDir, self.use_var_fdtd, self.hide_fdtd_cad) # FDTD model self.base_script(self.sim.fdtd) Optimization.set_global_wavelength(self.sim, self.wavelengths) Optimization.set_source_wavelength(self.sim, 'source', self.fom.multi_freq_src, 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) # Optimizer start_params = self.geometry.get_current_params() # We need to add the geometry first because it adds the mesh override region self.geometry.add_geo(self.sim, start_params, only_update=False) # If we don't have initial parameters yet, try to extract them from the simulation (this is mostly for topology optimization) if start_params is None: self.geometry.extract_parameters_from_simulation(self.sim) 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) if hasattr(self.geometry, 'to_file'): self.geometry.to_file('parameters_{}.npz'.format( self.optimizer.iteration)) with open('convergence_report.txt', 'a') as f: f.write('{}, {}'.format(self.optimizer.iteration, self.optimizer.fom_hist[-1])) if hasattr(self.geometry, 'write_status'): self.geometry.write_status(f) f.write('\n') self.fom.initialize(self.sim) self.optimizer.initialize(start_params=start_params, callable_fom=callable_fom, callable_jac=callable_jac, bounds=bounds, plotting_function=plotting_function) def make_forward_sim(self, params): self.sim.fdtd.switchtolayout() self.geometry.update_geometry(params, self.sim) self.geometry.add_geo(self.sim, params=None, only_update=True) self.sim.fdtd.setnamed('source', 'enabled', True) self.fom.make_forward_sim(self.sim) def run_forward_solves(self, params): """ Generates the new forward simulations, runs them and computes the figure of merit and forward fields. """ print('Running forward solves') self.make_forward_sim(params) iter = self.optimizer.iteration if self.store_all_simulations else 0 self.sim.run(name='forward', iter=iter) get_eps = True get_D = not self.use_deps nointerpolation = not self.geometry.use_interpolation() self.forward_fields = get_fields(self.sim.fdtd, monitor_name='opt_fields', field_result_name='forward_fields', get_eps=get_eps, get_D=get_D, get_H=False, nointerpolation=nointerpolation, unfold_symmetry=self.unfold_symmetry) fom = self.fom.get_fom(self.sim) if self.store_all_simulations: self.sim.remove_data_and_save( ) #< Remove the data from the file to save disk space. TODO: Make optional? self.fomHist.append(fom) print('FOM = {}'.format(fom)) return fom def make_adjoint_sim(self, params): self.sim.fdtd.switchtolayout() assert np.allclose(params, self.geometry.get_current_params()) self.geometry.add_geo(self.sim, params=None, only_update=True) self.sim.fdtd.setnamed('source', 'enabled', False) self.fom.make_adjoint_sim(self.sim) def run_adjoint_solves(self, params): """ Generates the adjoint simulations, runs them and extacts the adjoint fields. """ has_forward_fields = hasattr(self, 'forward_fields') and hasattr( self.forward_fields, 'E') params_changed = not np.allclose(params, self.geometry.get_current_params()) if not has_forward_fields or params_changed: fom = self.run_forward_solves(params) print('Running adjoint solves') self.make_adjoint_sim(params) iter = self.optimizer.iteration if self.store_all_simulations else 0 self.sim.run(name='adjoint', iter=iter) get_eps = not self.use_deps get_D = not self.use_deps nointerpolation = not self.geometry.use_interpolation() #< JN: Try on CAD self.adjoint_fields = get_fields(self.sim.fdtd, monitor_name='opt_fields', field_result_name='adjoint_fields', get_eps=get_eps, get_D=get_D, get_H=False, nointerpolation=nointerpolation, unfold_symmetry=self.unfold_symmetry) self.adjoint_fields.scaling_factor = self.fom.get_adjoint_field_scaling( self.sim) self.adjoint_fields.scale(3, self.adjoint_fields.scaling_factor) if self.store_all_simulations: self.sim.remove_data_and_save( ) #< Remove the data from the file to save disk space. TODO: Make optional? def callable_fom(self, params): """ Function for the optimizers to retrieve the figure of merit. :param params: optimization parameters. :param returns: figure of merit. """ return self.run_forward_solves(params) def callable_jac(self, params): """ Function for the optimizer to extract the figure of merit gradient. :param params: optimization paramaters. :param returns: partial derivative of the figure of merit with respect to each optimization parameter. """ self.run_adjoint_solves(params) return self.calculate_gradients() def calculate_gradients(self): """ 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. 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') fdtd = self.sim.fdtd self.gradient_fields = GradientFields( forward_fields=self.forward_fields, adjoint_fields=self.adjoint_fields) self.sim.fdtd.switchtolayout() if self.use_deps: self.geometry.d_eps_on_cad(self.sim) fom_partial_derivs_vs_wl = GradientFields.spatial_gradient_integral_on_cad( self.sim, 'forward_fields', 'adjoint_fields', self.adjoint_fields.scaling_factor) self.gradients = self.fom.fom_gradient_wavelength_integral( fom_partial_derivs_vs_wl.transpose(), self.forward_fields.wl) else: if hasattr(self.geometry, 'calculate_gradients_on_cad'): fom_partial_derivs_vs_wl = self.geometry.calculate_gradients_on_cad( self.sim, 'forward_fields', 'adjoint_fields', self.adjoint_fields.scaling_factor) self.gradients = self.fom.fom_gradient_wavelength_integral( fom_partial_derivs_vs_wl, self.forward_fields.wl) else: fom_partial_derivs_vs_wl = self.geometry.calculate_gradients( self.gradient_fields) self.gradients = self.fom.fom_gradient_wavelength_integral( fom_partial_derivs_vs_wl, self.forward_fields.wl) return self.gradients @staticmethod def goto_new_opts_folder(calling_file_name, base_script): ''' Creates a new folder in the current working directory named opt_xx to store the project files of the various simulations run during the optimization. Backup copiesof the calling and base scripts are placed in the new folder.''' calling_file_path = os.path.dirname( calling_file_name) if os.path.isfile( calling_file_name) else os.path.dirname(os.getcwd()) calling_file_path_split = os.path.split(calling_file_path) if calling_file_path_split[1].startswith('opts_'): calling_file_path = calling_file_path_split[0] calling_file_path_entries = os.listdir(calling_file_path) opts_dir_numbers = [ int(entry.split('_')[-1]) for entry in calling_file_path_entries if entry.startswith('opts_') ] opts_dir_numbers.append(-1) new_opts_dir = os.path.join( calling_file_path, 'opts_{}'.format(max(opts_dir_numbers) + 1)) os.mkdir(new_opts_dir) os.chdir(new_opts_dir) if os.path.isfile(calling_file_name): shutil.copy(calling_file_name, new_opts_dir) if hasattr(base_script, 'script_str'): with open('script_file.lsf', 'a') as file: file.write(base_script.script_str.replace(';', ';\n')) @staticmethod def go_out_of_opts_folder(): cwd_split = os.path.split(os.path.abspath(os.getcwd())) if cwd_split[1].startswith('opts_'): os.chdir(cwd_split[0]) @staticmethod def add_index_monitor(sim, monitor_name): sim.fdtd.select(monitor_name) if sim.fdtd.getnamednumber(monitor_name) != 1: raise UserWarning( "a single object named '{}' must be defined in the base simulation." .format(monitor_name)) index_monitor_name = monitor_name + '_index' if sim.fdtd.getnamednumber('FDTD') == 1: sim.fdtd.addindex() elif sim.fdtd.getnamednumber('varFDTD') == 1: sim.fdtd.addeffectiveindex() else: raise UserWarning( 'no FDTD or varFDTD solver object could be found.') sim.fdtd.set('name', index_monitor_name) sim.fdtd.setnamed(index_monitor_name, 'override global monitor settings', True) sim.fdtd.setnamed(index_monitor_name, 'frequency points', 1) sim.fdtd.setnamed(index_monitor_name, 'record conformal mesh when possible', True) monitor_type = sim.fdtd.getnamed(monitor_name, 'monitor type') geometric_props = ['monitor type'] geometric_props.extend( Optimization.cross_section_monitor_props(monitor_type)) for prop_name in geometric_props: prop_val = sim.fdtd.getnamed(monitor_name, prop_name) sim.fdtd.setnamed(index_monitor_name, prop_name, prop_val) sim.fdtd.setnamed(index_monitor_name, 'spatial interpolation', 'none') @staticmethod def cross_section_monitor_props(monitor_type): geometric_props = ['x', 'y', 'z'] if monitor_type == '3D': geometric_props.extend(['x span', 'y span', 'z span']) elif monitor_type == '2D X-normal': geometric_props.extend(['y span', 'z span']) elif monitor_type == '2D Y-normal': geometric_props.extend(['x span', 'z span']) elif monitor_type == '2D Z-normal': geometric_props.extend(['x span', 'y span']) elif monitor_type == 'Linear X': geometric_props.append('x span') elif monitor_type == 'Linear Y': geometric_props.append('y span') elif monitor_type == 'Linear Z': geometric_props.append('z span') else: raise UserWarning( 'monitor should be 2D or linear for a mode expansion to be meaningful.' ) return geometric_props @staticmethod def set_global_wavelength(sim, wavelengths): sim.fdtd.setglobalmonitor('use source limits', True) sim.fdtd.setglobalmonitor('use linear wavelength spacing', True) sim.fdtd.setglobalmonitor('frequency points', len(wavelengths)) sim.fdtd.setglobalsource('set wavelength', True) sim.fdtd.setglobalsource('wavelength start', wavelengths.min()) sim.fdtd.setglobalsource('wavelength stop', wavelengths.max()) @staticmethod def set_source_wavelength(sim, source_name, multi_freq_src, freq_pts): if sim.fdtd.getnamednumber(source_name) != 1: raise UserWarning( "a single object named '{}' must be defined in the base simulation." .format(source_name)) if sim.fdtd.getnamed(source_name, 'override global source settings'): print( 'Wavelength range of source object will be superseded by the global settings.' ) sim.fdtd.setnamed(source_name, 'override global source settings', False) sim.fdtd.select(source_name) if sim.fdtd.haveproperty('multifrequency mode calculation'): sim.fdtd.setnamed(source_name, 'multifrequency mode calculation', multi_freq_src) if multi_freq_src: sim.fdtd.setnamed(source_name, 'frequency points', freq_pts) elif sim.fdtd.haveproperty('multifrequency beam calculation'): sim.fdtd.setnamed(source_name, 'multifrequency beam calculation', multi_freq_src) if multi_freq_src: sim.fdtd.setnamed(source_name, 'number of frequency points', freq_pts) @staticmethod def set_use_legacy_conformal_interface_detection(sim, flagVal): if sim.fdtd.getnamednumber('FDTD') == 1: sim.fdtd.select('FDTD') elif sim.fdtd.getnamednumber('varFDTD') == 1: sim.fdtd.select('varFDTD') else: raise UserWarning( 'no FDTD or varFDTD solver object could be found.') if bool( sim.fdtd.haveproperty( 'use legacy conformal interface detection')): sim.fdtd.set('use legacy conformal interface detection', flagVal) sim.fdtd.set('conformal meshing refinement', 51) sim.fdtd.set('meshing tolerance', 1.0 / 1.134e14) else: raise UserWarning( 'install a more recent version of FDTD or the permittivity derivatives will not be accurate.' )
class TestModeMatchParallelPlateWaveguideTM(TestCase): """ Unit test for the ModeMatch class: it performs a quick check that the figure of merit is computed correctly using a simple a parallel plate waveguide partially filled by a dielectric. The waveguide has a material interface in the middle, and the figure of merit should be the same regardless of the material in which the source is placed. This is used to verify that the ModeMatch inputs monitor_name, direction and mode number work correctly. """ file_dir = os.path.abspath(os.path.dirname(__file__)) def setUp(self): # base script self.base_script = load_from_lsf( os.path.join(self.file_dir, 'modematch_parallel_plate_waveguide_TM_base.lsf')) # bandwidth self.wavelengths = Wavelengths(start=1540e-9, stop=1560e-9, points=11) # simulation self.sim = Simulation(workingDir=self.file_dir, use_var_fdtd=False, hide_fdtd_cad=True) self.sim.fdtd.eval(self.base_script) Optimization.set_global_wavelength(self.sim, self.wavelengths) # reference self.ref_fom = 0.65161635 def test_forward_injection_in_3D(self): """ Test forward injection in 3D with mode source in vacuum. """ self.fom = ModeMatch(monitor_name='figure_of_merit', mode_number=1, direction='Forward', multi_freq_src=True, target_T_fwd=lambda wl: np.ones(wl.size), norm_p=1) Optimization.set_source_wavelength(self.sim, 'source', self.fom.multi_freq_src, len(self.wavelengths)) self.sim.fdtd.setnamed('FDTD', 'dimension', '3D') self.fom.initialize(self.sim) self.fom.make_forward_sim(self.sim) self.sim.run(name='modematch_forward_injection_in_3D', iter=0) FOM = self.fom.get_fom(self.sim) self.assertAlmostEqual(FOM, self.ref_fom, 4) def test_backward_injection_in_3D(self): """ Test backward injection in 3D with mode source in dielectric region. """ self.fom = ModeMatch(monitor_name='figure_of_merit', mode_number=1, direction='Backward', multi_freq_src=True, target_T_fwd=lambda wl: np.ones(wl.size), norm_p=1) Optimization.set_source_wavelength(self.sim, 'source', self.fom.multi_freq_src, len(self.wavelengths)) self.sim.fdtd.setnamed('FDTD', 'dimension', '3D') self.sim.fdtd.setnamed('source', 'x', -self.sim.fdtd.getnamed('source', 'x')) self.sim.fdtd.setnamed('source', 'direction', 'Backward') self.sim.fdtd.setnamed('figure_of_merit', 'x', -self.sim.fdtd.getnamed('figure_of_merit', 'x')) self.fom.initialize(self.sim) self.fom.make_forward_sim(self.sim) self.sim.run(name='modematch_backward_injection_in_3D', iter=1) FOM = self.fom.get_fom(self.sim) self.assertAlmostEqual(FOM, self.ref_fom, 4) def test_forward_injection_in_2D(self): """ Test forward injection in 2D with mode source in vacuum. """ self.fom = ModeMatch(monitor_name='figure_of_merit', mode_number=1, direction='Forward', multi_freq_src=True, target_T_fwd=lambda wl: np.ones(wl.size), norm_p=1) Optimization.set_source_wavelength(self.sim, 'source', self.fom.multi_freq_src, len(self.wavelengths)) self.sim.fdtd.setnamed('FDTD', 'dimension', '2D') self.fom.initialize(self.sim) self.fom.make_forward_sim(self.sim) self.sim.run(name='modematch_forward_injection_in_2D', iter=2) FOM = self.fom.get_fom(self.sim) self.assertAlmostEqual(FOM, self.ref_fom, 4) def test_no_forward_injection_in_2D(self): """ Test no forward injection in 2D with mode source in vacuum. """ self.fom = ModeMatch( monitor_name='figure_of_merit', mode_number=2, # evanescent mode direction='Forward', multi_freq_src=False, target_T_fwd=lambda wl: np.ones(wl.size), norm_p=1) Optimization.set_source_wavelength(self.sim, 'source', self.fom.multi_freq_src, len(self.wavelengths)) self.sim.fdtd.setnamed('FDTD', 'dimension', '2D') self.fom.initialize(self.sim) self.fom.make_forward_sim(self.sim) self.sim.run(name='modematch_no_forward_injection_in_2D', iter=3) FOM = self.fom.get_fom(self.sim) self.assertAlmostEqual(FOM, 0.0, 5) def test_backward_injection_in_2D(self): """ Test backward injection in 2D with mode source in dielectric region. """ self.fom = ModeMatch(monitor_name='figure_of_merit', mode_number=1, direction='Backward', multi_freq_src=True, target_T_fwd=lambda wl: np.ones(wl.size), norm_p=1) Optimization.set_source_wavelength(self.sim, 'source', self.fom.multi_freq_src, len(self.wavelengths)) self.sim.fdtd.setnamed('FDTD', 'dimension', '2D') self.sim.fdtd.setnamed('source', 'x', -self.sim.fdtd.getnamed('source', 'x')) self.sim.fdtd.setnamed('source', 'direction', 'Backward') self.sim.fdtd.setnamed('figure_of_merit', 'x', -self.sim.fdtd.getnamed('figure_of_merit', 'x')) self.fom.initialize(self.sim) self.fom.make_forward_sim(self.sim) self.sim.run(name='modematch_backward_injection_in_2D', iter=4) FOM = self.fom.get_fom(self.sim) self.assertAlmostEqual(FOM, self.ref_fom, 4)
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: callable, file name or plain string with script to generate the base simulation. :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 if isinstance( base_script, BaseScript) else BaseScript(base_script) self.wavelengths = wavelengths if isinstance( wavelengths, Wavelengths) else Wavelengths(wavelengths) self.wavelengths = wavelengths if isinstance( wavelengths, Wavelengths) else Wavelengths(wavelengths) self.fom = fom self.geometry = geometry self.optimizer = optimizer self.hide_fdtd_cad = bool(hide_fdtd_cad) self.use_deps = bool(use_deps) if self.use_deps: print("Accurate interface detection enabled") self.plotter = Plotter() self.forward_fields = None self.adjoint_fields = None self.gradients = None self.fomHist = [] self.paramsHist = [] self.gradient_fields = None frame = inspect.stack()[1] calling_file_name = os.path.abspath(frame[0].f_code.co_filename) Optimization.goto_new_opts_folder(calling_file_name, base_script) self.workingDir = os.getcwd() def __del__(self): Optimization.go_out_of_opts_folder() def run(self): self.initialize() if self.plotter.movie: with self.plotter.writer.saving(self.plotter.fig, "optimization.mp4", 100): self.optimizer.run() else: self.optimizer.run() print('FINAL FOM = {}'.format(self.optimizer.fom_hist[-1])) 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) 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.base_script(self.sim.fdtd) Optimization.set_global_wavelength(self.sim, self.wavelengths) Optimization.set_source_wavelength(self.sim, 'source', self.fom.multi_freq_src, 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) if geometry is None: self.geometry.add_geo(self.sim, params=None, only_update=False) else: geometry.add_geo(self.sim) self.fom.add_to_sim(self.sim) 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') 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) print('FOM = {}'.format(fom)) return fom 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. """ self.geometry.update_geometry(params) fom = self.run_forward_solves() return fom 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. It assumes that both the forward and adjoint solves have been run so that all the necessary field results 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: 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: fom_partial_derivs_vs_wl = self.geometry.calculate_gradients( self.gradient_fields) wl = self.gradient_fields.forward_fields.wl self.gradients = self.fom.fom_gradient_wavelength_integral( fom_partial_derivs_vs_wl.transpose(), wl) return self.gradients @staticmethod def goto_new_opts_folder(calling_file_name, base_script): ''' Creates a new folder in the current working directory named opt_xx to store the project files of the various simulations run during the optimization. Backup copiesof the calling and base scripts are placed in the new folder.''' calling_file_path = os.path.dirname( calling_file_name) if os.path.isfile( calling_file_name) else os.path.dirname(os.getcwd()) calling_file_path_split = os.path.split(calling_file_path) if calling_file_path_split[1].startswith('opts_'): calling_file_path = calling_file_path_split[0] calling_file_path_entries = os.listdir(calling_file_path) opts_dir_numbers = [ int(entry.split('_')[-1]) for entry in calling_file_path_entries if entry.startswith('opts_') ] opts_dir_numbers.append(-1) new_opts_dir = os.path.join( calling_file_path, 'opts_{}'.format(max(opts_dir_numbers) + 1)) os.mkdir(new_opts_dir) os.chdir(new_opts_dir) if os.path.isfile(calling_file_name): shutil.copy(calling_file_name, new_opts_dir) if hasattr(base_script, 'script_str'): with open('script_file.lsf', 'a') as file: file.write(base_script.script_str.replace(';', ';\n')) @staticmethod def go_out_of_opts_folder(): cwd_split = os.path.split(os.path.abspath(os.getcwd())) if cwd_split[1].startswith('opts_'): os.chdir(cwd_split[0]) @staticmethod def add_index_monitor(sim, monitor_name): sim.fdtd.select(monitor_name) if sim.fdtd.getnamednumber(monitor_name) != 1: raise UserWarning( "a single object named '{}' must be defined in the base simulation." .format(monitor_name)) index_monitor_name = monitor_name + '_index' sim.fdtd.addindex() sim.fdtd.set('name', index_monitor_name) sim.fdtd.setnamed(index_monitor_name, 'override global monitor settings', True) sim.fdtd.setnamed(index_monitor_name, 'frequency points', 1) sim.fdtd.setnamed(index_monitor_name, 'record conformal mesh when possible', True) monitor_type = sim.fdtd.getnamed(monitor_name, 'monitor type') geometric_props = ['monitor type'] geometric_props.extend( Optimization.cross_section_monitor_props(monitor_type)) for prop_name in geometric_props: prop_val = sim.fdtd.getnamed(monitor_name, prop_name) sim.fdtd.setnamed(index_monitor_name, prop_name, prop_val) sim.fdtd.setnamed(index_monitor_name, 'spatial interpolation', 'none') @staticmethod def cross_section_monitor_props(monitor_type): geometric_props = ['x', 'y', 'z'] if monitor_type == '3D': geometric_props.extend(['x span', 'y span', 'z span']) elif monitor_type == '2D X-normal': geometric_props.extend(['y span', 'z span']) elif monitor_type == '2D Y-normal': geometric_props.extend(['x span', 'z span']) elif monitor_type == '2D Z-normal': geometric_props.extend(['x span', 'y span']) elif monitor_type == 'Linear X': geometric_props.append('x span') elif monitor_type == 'Linear Y': geometric_props.append('y span') elif monitor_type == 'Linear Z': geometric_props.append('z span') else: raise UserWarning( 'monitor should be 2D or linear for a mode expansion to be meaningful.' ) return geometric_props @staticmethod def set_global_wavelength(sim, wavelengths): sim.fdtd.setglobalmonitor('use source limits', True) sim.fdtd.setglobalmonitor('use linear wavelength spacing', True) sim.fdtd.setglobalmonitor('frequency points', len(wavelengths)) sim.fdtd.setglobalsource('set wavelength', True) sim.fdtd.setglobalsource('wavelength start', wavelengths.min()) sim.fdtd.setglobalsource('wavelength stop', wavelengths.max()) @staticmethod def set_source_wavelength(sim, source_name, multi_freq_src, freq_pts): if sim.fdtd.getnamednumber(source_name) != 1: raise UserWarning( "a single object named '{}' must be defined in the base simulation." .format(source_name)) if sim.fdtd.getnamed(source_name, 'override global source settings'): print( 'Wavelength range of source object will be superseded by the global settings.' ) sim.fdtd.setnamed(source_name, 'override global source settings', False) sim.fdtd.select(source_name) if sim.fdtd.haveproperty('multifrequency mode calculation'): sim.fdtd.setnamed(source_name, 'multifrequency mode calculation', multi_freq_src) if multi_freq_src: sim.fdtd.setnamed(source_name, 'frequency points', freq_pts) elif sim.fdtd.haveproperty('multifrequency beam calculation'): sim.fdtd.setnamed(source_name, 'multifrequency beam calculation', multi_freq_src) if multi_freq_src: sim.fdtd.setnamed(source_name, 'number of frequency points', freq_pts) else: raise UserWarning('unable to determine source type.') @staticmethod def set_use_legacy_conformal_interface_detection(sim, flagVal): sim.fdtd.select('FDTD') has_legacy_prop = bool( sim.fdtd.haveproperty('use legacy conformal interface detection')) if has_legacy_prop: sim.fdtd.setnamed('FDTD', 'use legacy conformal interface detection', flagVal) sim.fdtd.setnamed('FDTD', 'conformal meshing refinement', 51) else: raise UserWarning( 'install a more recent version of FDTD or the permittivity derivatives will not be accurate.' )