Ejemplo n.º 1
0
    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)
Ejemplo n.º 2
0
 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 = 3)
     # simulation
     self.sim = Simulation(workingDir = self.file_dir, hide_fdtd_cad = True)
     self.sim.fdtd.eval(self.base_script)
     Optimization.set_global_wavelength(self.sim, self.wavelengths)
     # reference
     self.ref_fom = 0.6643986
Ejemplo n.º 3
0
    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 initialize(self, working_dir):
        """
            Performs all steps that need to be carried only once at the beginning of the optimization.
        """
        working_dir = 'opts' if working_dir is None else working_dir
        self.prepare_working_dir(working_dir)

        ## Store a copy of the script file
        if hasattr(self.base_script, 'script_str'):
            with open('script_file.lsf', 'a') as file:
                file.write(self.base_script.script_str.replace(';', ';\n'))

        # FDTD CAD
        # WARNING: NOT THREAD SAFE, ADD LOCK
        # lock.acquire()
        self.sim = Simulation(self.workingDir, self.use_var_fdtd,
                              self.hide_fdtd_cad)
        self.geometry.check_license_requirements(self.sim)
        # lock.release()

        # FDTD model
        self.base_script(self.sim.fdtd)
        Optimization.set_global_wavelength(self.sim, self.wavelengths)
        Optimization.set_source_wavelength(self.sim, self.source_name,
                                           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 = self.geometry.bounds

        self.fom.initialize(self.sim)

        def plotting_function_fwd(params):
            self.plotting_function(params)

        self.optimizer.initialize(start_params=start_params,
                                  callable_fom=callable_fom,
                                  callable_jac=callable_jac,
                                  bounds=bounds,
                                  plotting_function=plotting_function_fwd)

        self.fom_hist = []
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
        :param save_global_index: Should the project save the result of a global index monitor to file after each iteration (for visualization purposes)
        :param label:          If the optimization is part of a super-optimization, this string is used for the legend of the corresponding FOM plot
        :param source_name:    Name of the source object in the simulation project (default is "source")
        :param fields_on_cad_only: Process all field data on the CAD only and don't transfer to Python. Reduces memory and improves performance but disables plotting of field/gradient information.
    """
    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,
                 save_global_index=False,
                 label=None,
                 source_name='source',
                 fields_on_cad_only=False):
        super().__init__(plot_history=plot_history,
                         fields_on_cad_only=fields_on_cad_only)
        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.source_name = source_name

        if callable(use_deps):
            self.use_deps = True
            self.custom_deps = use_deps
        else:
            self.use_deps = bool(use_deps)
            self.custom_deps = None

        self.store_all_simulations = store_all_simulations
        self.save_global_index = save_global_index
        self.unfold_symmetry = geometry.unfold_symmetry
        self.label = label
        self.plot_fom_on_log_scale = (float(fom.target_fom) != 0.0)

        if self.use_deps:
            print("Accurate interface detection enabled")

        ## Figure out from which file this method was called (most likely the driver script)
        frame = inspect.stack()[1]
        self.calling_file_name = os.path.abspath(frame[0].f_code.co_filename)
        self.base_file_path = os.path.dirname(self.calling_file_name)

    def check_gradient(self, test_params, dx, working_dir=None):
        self.initialize(working_dir)

        ## Calculate the gradient using the adjoint method:
        adj_grad = self.callable_jac(test_params)
        fd_grad = np.zeros_like(adj_grad)

        ## Calculate the gradient using finite differences
        cur_dx = dx / 2.
        for i, param in enumerate(test_params):
            d_params = test_params.copy()
            d_params[i] = param + cur_dx
            f1 = self.callable_fom(d_params)
            d_params[i] = param - cur_dx
            f2 = self.callable_fom(d_params)

            fd_grad[i] = (f1 - f2) / dx

            print(
                "Checking gradient #{} : Adjoint={:.4f}, FD={:.4f}, Rel. Diff={:.4f}"
                .format(
                    i, adj_grad[i], fd_grad[i],
                    2. * abs(adj_grad[i] - fd_grad[i]) /
                    abs(adj_grad[i] + fd_grad[i])))

        ## More meaningful comparison of the vector norm:
        print(" fd_grad: {}".format(
            np.array2string(fd_grad, separator=', ', max_line_width=10000)))
        print("adj_grad: {}".format(
            np.array2string(adj_grad, separator=', ', max_line_width=10000)))
        vec_error = np.linalg.norm(fd_grad -
                                   adj_grad) / np.linalg.norm(fd_grad)
        print("norm of vec. diff: {:.4f}".format(vec_error))

    def run(self, working_dir=None):

        self.initialize(working_dir)
        self.init_plotter()
        if self.plotter.movie:
            with self.plotter.writer.saving(
                    self.plotter.fig,
                    os.path.join(self.workingDir, 'optimization.png'), 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 = self.continuation_max_iter
            while self.geometry.progress_continuation():
                self.optimizer.reset_start_params(
                    self.params_hist[-1],
                    0.05)  # < Run the scaling analysis again
                self.optimizer.run()

        #final_fom = np.abs(self.fom_hist[-1])
        return  #final_fom, self.params_hist[-1]

    def plotting_function(self, params):
        ## Add the last FOM evaluation to the list of FOMs that we wish to plot. This removes entries caused by linesearches etc.
        self.fom_hist.append(self.full_fom_hist[-1])

        ## In a multi-FOM optimization, only the first optimization has a plotter
        if self.plotter is not None:

            self.params_hist.append(params)
            self.grad_hist.append(self.last_grad /
                                  self.optimizer.scaling_factor)

            self.plotter.clear()
            self.plotter.update_fom(self)
            self.plotter.update_gradient(self)
            self.plotter.update_geometry(self)
            self.plotter.draw_and_save()

            self.save_index_to_vtk(self.optimizer.iteration)

            if hasattr(self.geometry, 'to_file'):
                self.geometry.to_file(
                    os.path.join(self.workingDir, 'parameters_{}.npz').format(
                        self.optimizer.iteration))

            with open(os.path.join(self.workingDir, 'convergence_report.txt'),
                      'a') as f:
                f.write('{}, {}'.format(self.optimizer.iteration,
                                        self.fom_hist[-1]))

                if hasattr(self.geometry, 'write_status'):
                    self.geometry.write_status(f)

                if len(self.params_hist[-1]) < 250:
                    f.write(', {}'.format(
                        np.array2string(self.params_hist[-1],
                                        separator=', ',
                                        max_line_width=10000)))

                if len(self.grad_hist[-1]) < 250:
                    f.write(', {}'.format(
                        np.array2string(self.grad_hist[-1],
                                        separator=', ',
                                        max_line_width=10000)))

                f.write('\n')

    def initialize(self, working_dir):
        """
            Performs all steps that need to be carried only once at the beginning of the optimization.
        """
        working_dir = 'opts' if working_dir is None else working_dir
        self.prepare_working_dir(working_dir)

        ## Store a copy of the script file
        if hasattr(self.base_script, 'script_str'):
            with open('script_file.lsf', 'a') as file:
                file.write(self.base_script.script_str.replace(';', ';\n'))

        # FDTD CAD
        # WARNING: NOT THREAD SAFE, ADD LOCK
        # lock.acquire()
        self.sim = Simulation(self.workingDir, self.use_var_fdtd,
                              self.hide_fdtd_cad)
        self.geometry.check_license_requirements(self.sim)
        # lock.release()

        # FDTD model
        self.base_script(self.sim.fdtd)
        Optimization.set_global_wavelength(self.sim, self.wavelengths)
        Optimization.set_source_wavelength(self.sim, self.source_name,
                                           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 = self.geometry.bounds

        self.fom.initialize(self.sim)

        def plotting_function_fwd(params):
            self.plotting_function(params)

        self.optimizer.initialize(start_params=start_params,
                                  callable_fom=callable_fom,
                                  callable_jac=callable_jac,
                                  bounds=bounds,
                                  plotting_function=plotting_function_fwd)

        self.fom_hist = []

    def save_fields_to_vtk(self, cur_iteration):
        if self.save_global_index:
            self.sim.save_fields_to_vtk(
                os.path.join(self.workingDir,
                             'global_fields_{}').format(cur_iteration))

    def save_index_to_vtk(self, cur_iteration):
        if self.save_global_index:
            self.sim.save_index_to_vtk(
                os.path.join(self.workingDir,
                             'global_index_{}').format(cur_iteration))

    def make_forward_sim(self, params, iter):
        self.sim.fdtd.switchtolayout()
        self.geometry.update_geometry(params, self.sim)
        self.geometry.add_geo(self.sim, params=None, only_update=True)
        Optimization.deactivate_all_sources(self.sim)
        self.sim.fdtd.setnamed(self.source_name, 'enabled', True)

        self.fom.make_forward_sim(self.sim)
        forward_name = 'forward_{}'.format(iter)
        return self.sim.save(forward_name)

    def process_forward_sim(self, iter):
        forward_name = 'forward_{}'.format(iter)
        self.sim.load(forward_name)
        Optimization.check_simulation_was_successful(self.sim)

        if self.fields_on_cad_only:
            get_fields_on_cad(
                self.sim.fdtd,
                monitor_name='opt_fields',
                field_result_name='forward_fields',
                get_eps=True,
                get_D=not self.use_deps,
                get_H=False,
                nointerpolation=not self.geometry.use_interpolation(),
                unfold_symmetry=self.unfold_symmetry)
            self.forward_fields_wl = get_lambda_from_cad(
                self.sim.fdtd, field_result_name='forward_fields')
        else:
            self.forward_fields = get_fields(
                self.sim.fdtd,
                monitor_name='opt_fields',
                field_result_name='forward_fields',
                get_eps=True,
                get_D=not self.use_deps,
                get_H=False,
                nointerpolation=not self.geometry.use_interpolation(),
                unfold_symmetry=self.unfold_symmetry)
            assert hasattr(self.forward_fields, 'E')
            self.forward_fields_wl = self.forward_fields.wl

        self.forward_fields_iter = int(iter)
        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?

        dist_to_target_fom = self.fom.target_fom - fom  # < For plotting/logging we store the distance to a target
        self.full_fom_hist.append(dist_to_target_fom)
        if self.fom.target_fom == 0.0:
            print('FOM = {}'.format(fom))
        else:
            print('FOM = {} ({} - {})'.format(dist_to_target_fom,
                                              self.fom.target_fom, fom))
        return fom

    def callable_fom(self, params):
        """ Function for the optimizers to retrieve the figure of merit.
            :param params:  optimization parameters.
            :param returns: figure of merit.
        """

        self.sim.fdtd.clearjobs()
        iter = self.optimizer.iteration if self.store_all_simulations else 0
        # print('Making forward solve')
        # print("With params:", params)
        forward_job_name = self.make_forward_sim(params, iter)
        self.sim.fdtd.addjob(forward_job_name)
        if self.optimizer.concurrent_adjoint_solves():
            print('Making adjoint solve')
            adjoint_job_name = self.make_adjoint_sim(params, iter)
            self.sim.fdtd.addjob(adjoint_job_name)
        print('Running solves')
        self.sim.fdtd.runjobs()

        print('Processing forward solve')
        fom = self.process_forward_sim(iter)

        ## If the geometry class has an additional penalty term (e.g. min feature size for topology)
        if hasattr(self.geometry, 'calc_penalty_term'):
            fom_penalty = self.geometry.calc_penalty_term(self.sim, params)
            print('Actual fom: {}, Penalty term: {}, Total fom: {}'.format(
                fom, fom_penalty, (fom + fom_penalty)))
            fom += fom_penalty

        return fom

    def make_adjoint_sim(self, params, iter):
        assert np.allclose(params, self.geometry.get_current_params())
        adjoint_name = 'adjoint_{}'.format(iter)
        self.sim.fdtd.switchtolayout()
        self.geometry.add_geo(self.sim, params=None, only_update=True)
        self.sim.fdtd.setnamed(self.source_name, 'enabled', False)
        self.fom.make_adjoint_sim(self.sim)
        return self.sim.save(adjoint_name)

    def process_adjoint_sim(self, iter):
        adjoint_name = 'adjoint_{}'.format(iter)
        self.sim.load(adjoint_name)
        if self.sim.fdtd.layoutmode():
            self.sim.fdtd.run()
        Optimization.check_simulation_was_successful(self.sim)

        if self.fields_on_cad_only:
            get_fields_on_cad(
                self.sim.fdtd,
                monitor_name='opt_fields',
                field_result_name='adjoint_fields',
                get_eps=not self.use_deps,
                get_D=not self.use_deps,
                get_H=False,
                nointerpolation=not self.geometry.use_interpolation(),
                unfold_symmetry=self.unfold_symmetry)
        else:
            self.adjoint_fields = get_fields(
                self.sim.fdtd,
                monitor_name='opt_fields',
                field_result_name='adjoint_fields',
                get_eps=not self.use_deps,
                get_D=not self.use_deps,
                get_H=False,
                nointerpolation=not self.geometry.use_interpolation(),
                unfold_symmetry=self.unfold_symmetry)
            assert hasattr(self.adjoint_fields, 'E')
            self.adjoint_fields.iter = int(iter)

        self.scaling_factor = self.fom.get_adjoint_field_scaling(self.sim)

        if not self.fields_on_cad_only:
            self.adjoint_fields.scale(3, self.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_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.sim.fdtd.clearjobs()
        iter = self.optimizer.iteration if self.store_all_simulations else 0
        no_forward_fields = not hasattr(self, 'forward_fields')
        params_changed = not np.allclose(params,
                                         self.geometry.get_current_params())
        redo_forward_sim = no_forward_fields or params_changed
        do_adjoint_sim = redo_forward_sim or not self.optimizer.concurrent_adjoint_solves(
        ) or self.forward_fields_iter != iter
        if redo_forward_sim:
            print('Making forward solve')
            forward_job_name = self.make_forward_sim(params, iter)
            self.sim.fdtd.addjob(forward_job_name)
        if do_adjoint_sim:
            print('Making adjoint solve')
            adjoint_job_name = self.make_adjoint_sim(params, iter)
            self.sim.fdtd.addjob(adjoint_job_name)
        if len(self.sim.fdtd.listjobs()) > 0:
            print('Runing solves')
            self.sim.fdtd.runjobs()
        if redo_forward_sim:
            print('Processing forward solve')
            fom = self.process_forward_sim(iter)
        print('Processing adjoint solve')
        self.process_adjoint_sim(iter)
        print('Calculating gradients')
        grad = self.calculate_gradients()
        self.last_grad = grad

        if hasattr(self.geometry, 'calc_penalty_term'):
            print('Calculating Penalty Terms')
            penalty_grad = self.geometry.calc_penalty_gradient(
                self.sim, params)
            grad += penalty_grad
        ## Print Gradients
        print("Gradients Calculated:", grad)
        return grad

    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).
        """
        if not self.fields_on_cad_only:
            self.gradient_fields = GradientFields(
                forward_fields=self.forward_fields,
                adjoint_fields=self.adjoint_fields)

        self.sim.fdtd.switchtolayout()
        if self.use_deps:
            if self.custom_deps:
                self.custom_deps(self.sim, self.geometry)
            else:
                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.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'):
                grad_name = self.geometry.calculate_gradients_on_cad(
                    self.sim, 'forward_fields', 'adjoint_fields',
                    self.scaling_factor)
                self.gradients = self.fom.fom_gradient_wavelength_integral_on_cad(
                    self.sim, grad_name, 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

    def plot_gradient(self, fig, ax1, ax2):
        self.gradient_fields.plot(fig, ax1, ax2)

    @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.'
            )

    @staticmethod
    def check_simulation_was_successful(sim):
        if sim.fdtd.getnamednumber('FDTD') == 1:
            simulation_status = sim.fdtd.getresult('FDTD', 'status')
        elif sim.fdtd.getnamednumber('varFDTD') == 1:
            simulation_status = sim.fdtd.getresult('varFDTD', 'status')
        else:
            raise UserWarning(
                'no FDTD or varFDTD solver object could be found.')
        if simulation_status != 1 and simulation_status != 2:  # run to full simulation time (1) or autoshutoff triggered (2)
            raise UserWarning(
                'FDTD simulation did not complete successfully: status {0}'.
                format(simulation_status))
        return simulation_status

    @staticmethod
    def deactivate_all_sources(sim):
        sim.fdtd.selectall()
        numElements = int(sim.fdtd.getnumber())
        for i in range(numElements):
            objType = sim.fdtd.get("type", i + 1)
            if "Source" in objType:
                sim.fdtd.set("enabled", False, i + 1)
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)
Ejemplo n.º 7
0
 def setUp(self):
     self.sim = Simulation(workingDir=self.file_dir,
                           use_var_fdtd=True,
                           hide_fdtd_cad=True)
Ejemplo n.º 8
0
 def make_base_sim(self):
     '''Creates the substrate simulation without the optimizable geometry'''
     base_sim = Simulation(workingDir=self.workingDir,
                           script=self.base_script)
     return base_sim
Ejemplo n.º 9
0
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.'
            )
Ejemplo n.º 10
0
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.'
            )