diff --git a/.gitignore b/.gitignore index a6411fb..3bea398 100644 --- a/.gitignore +++ b/.gitignore @@ -125,4 +125,7 @@ layout_tests sim_tests *.rar SPLayout.egg-info -new_features.md \ No newline at end of file +new_features.md +*.fsp +boolean*.gds +temporary_figures/ \ No newline at end of file diff --git a/README.md b/README.md index df4f09a..feb1ceb 100644 --- a/README.md +++ b/README.md @@ -5,7 +5,7 @@ [![GitHub stars](https://img.shields.io/github/stars/Hideousmon/SPLayout.svg?style=social&label=Star&maxAge=8640)](https://GitHub.com/Hideousmon/SPLayout/stargazers/) -SPLayout (**S**ilicon **P**hotonics **Layout** Design Tools) is a package for silicon photonics structures design. It provides commonly used silicon photonics structure classes for fast integration and pixelized blocks for inverse design and optimization. Some inverse design algorithms are also integrated in it like DBS (Direct Binary Search) and BBA (Binary Bat Algorithm). +SPLayout (**S**ilicon **P**hotonics **Layout** Design Tools) is a silicon photonics structures design package. It provides commonly used silicon photonics structures for fast integration and pixelated blocks for inverse design. The GDSII streaming is based on [gdspy](https://github.com/heitzmann/gdspy) and FDTD simulation is executed on Ansys Lumerical. @@ -34,11 +34,19 @@ python setup.py install The documentation can be found [here](https://splayout.readthedocs.io/en/latest/). -## Inverse Design Examples +## Forward and Inverse Design Examples +### Simulation + +A basic simulation example for waveguide transmission can be found [here](https://github.com/Hideousmon/SPLayout/tree/main/examples/simulation/waveguide.py). +![process](__img/waveguide_simulation.png) + + ### Direct Binary Search -A polarization beam splitter inverse design example can be found [here](https://github.com/Hideousmon/SPLayout/tree/main/examples/inversedesign/PBS_DBS.py) .The [Extinction Ratio](__img/PBS_extinction_ratio.png) and [Transmission](__img/PBS_transmission.png) are comparable to the original [paper](https://doi.org/10.1038/nphoton.2015.80). + +A polarization beam splitter inverse design example can be found [here](https://github.com/Hideousmon/SPLayout/tree/main/examples/inversedesign/PBS_DBS.py). The [Extinction Ratio](__img/PBS_extinction_ratio.png) and [Transmission](__img/PBS_transmission.png) are comparable to the original [paper](https://doi.org/10.1038/nphoton.2015.80). ![process](__img/PBS_process.gif) + ## References for Inverse Design Methods [1] Mirjalili, S., Mirjalili, S.M. & Yang, XS. Binary bat algorithm. Neural Comput &Applic 25, 663–681 (2014). https://doi.org/10.1007/s00521-013-1525-5 diff --git a/README.rst b/README.rst index 725c6c1..c78d652 100644 --- a/README.rst +++ b/README.rst @@ -3,7 +3,7 @@ SPLayout |GitHub repository| |GitHub license| -SPLayout (**S**\ilicon **P**\hotonics **Layout** Design Tools) is a package for silicon photonics structures design. It provides commonly used silicon photonics structure classes for fast integration and pixelized blocks for inverse design and optimization. Some inverse design algorithms are also integrated in it like DBS (Direct Binary Search) and BBA (Binary Bat Algorithm). +SPLayout (**S**\ilicon **P**\hotonics **Layout** Design Tools) is a silicon photonics structures design package. It provides commonly used silicon photonics structures for fast integration and pixelated blocks for inverse design. The GDSII streaming is based on gdspy(https://github.com/heitzmann/gdspy) and FDTD simulation is executed on Ansys Lumerical. diff --git a/__img/waveguide_simulation.png b/__img/waveguide_simulation.png new file mode 100644 index 0000000..d24f03b Binary files /dev/null and b/__img/waveguide_simulation.png differ diff --git a/docs/_static/boolean_add.png b/docs/_static/boolean_add.png new file mode 100644 index 0000000..25dd375 Binary files /dev/null and b/docs/_static/boolean_add.png differ diff --git a/docs/_static/boolean_before.png b/docs/_static/boolean_before.png new file mode 100644 index 0000000..8ffec45 Binary files /dev/null and b/docs/_static/boolean_before.png differ diff --git a/docs/_static/boolean_common.png b/docs/_static/boolean_common.png new file mode 100644 index 0000000..57539ee Binary files /dev/null and b/docs/_static/boolean_common.png differ diff --git a/docs/_static/boolean_cut.png b/docs/_static/boolean_cut.png new file mode 100644 index 0000000..1495ab4 Binary files /dev/null and b/docs/_static/boolean_cut.png differ diff --git a/docs/_static/boolean_dilation.png b/docs/_static/boolean_dilation.png new file mode 100644 index 0000000..229cc8c Binary files /dev/null and b/docs/_static/boolean_dilation.png differ diff --git a/docs/_static/boolean_inversion.png b/docs/_static/boolean_inversion.png new file mode 100644 index 0000000..7f7f220 Binary files /dev/null and b/docs/_static/boolean_inversion.png differ diff --git a/docs/apireference.rst b/docs/apireference.rst index bca8f08..0044f98 100644 --- a/docs/apireference.rst +++ b/docs/apireference.rst @@ -177,7 +177,7 @@ Rectangle :show-inheritance: ****************************************** -Functions for Self-define Components +Functions for Customizing Components ****************************************** MAKE_AEMD_GRATING @@ -271,6 +271,25 @@ BinaryGeneticAlgorithm :inherited-members: :show-inheritance: +****************************************** +Pixelated Region for Inverse Design +****************************************** + +RectanglePixelsRegion +============================ +.. autoclass:: splayout.RectanglePixelsRegion + :members: + :inherited-members: + :show-inheritance: + +CirclePixelsRegion +============================ +.. autoclass:: splayout.CirclePixelsRegion + :members: + :inherited-members: + :show-inheritance: + + ****************************************** Inverse Design Blocks for Adjoint Method ****************************************** diff --git a/docs/gettingstarted.md b/docs/gettingstarted.md index 2dafe0e..461cc85 100644 --- a/docs/gettingstarted.md +++ b/docs/gettingstarted.md @@ -1,11 +1,7 @@ # Getting Started - - -SPLayout aims to accelerate the layout design process in Silicon Photonics. Most of the operations are simplified by losing some flexibility compared with its dependency [gdspy](https://github.com/heitzmann/gdspy) . The basic thought of SPLayout is that the connection between two components should be quickly made according to the center points of thier ports. - - +SPLayout aims to expedite the layout design process in Silicon Photonics. While some flexibility is sacrificed compared to its dependency [gdspy](https://github.com/heitzmann/gdspy), most operations are simplified. The fundamental principle behind SPLayout is to facilitate swift connections between components based on the center points of their ports. All structures are designed to be functional for both layout generation and simulation verification. ## First GDSII @@ -35,7 +31,42 @@ Firstly, we create a cell named "waveguide". We define a waveguide from Point(0, We can check the gdsii file "waveguide.gds" with some gdsii editors such as [KLayout](https://klayout.de/). +## First FDTD Simulation + +Create a simulation for a waveguide. + +```python +from splayout import * + +# initialize the simulation frame +fdtd = FDTDSimulation(fdtd_path="C:\\Program Files\\Lumerical\\v202\\api\\python") + +# draw waveguides on Lumerical +waveguide = Waveguide(start_point=Point(-3,0), end_point=Point(3,0), width=1, z_start=-0.11, z_end=0.11, material=Si) +waveguide.draw_on_lumerical_CAD(fdtd) + +# add simulation region, source, and monitor +fdtd.add_fdtd_region(bottom_left_corner_point=Point(-2, -1.5), top_right_corner_point=Point(2, 1.5), background_index=1.444,dimension=3, height=0.8) + +fdtd.add_mode_source(position=Point(-1.5,0), width=1.5, height=0.8, wavelength_start=1.54, wavelength_end=1.57, mode_number=1) +fdtd.add_mode_expansion(position=Point(1.5, 0), width=1.5, height=0.8, points=101, mode_list=[1]) + +# run simulation +fdtd.run("./temp") + +# get result return: (number of modes, 2, frequency points) +# where (number of modes, 0, frequency points) are wavelengths, +# (number of modes, 1, frequency points) are transmissions +transmission = fdtd.get_mode_transmission(expansion_name="expansion") + +# plot figure +plt.figure() +plt.plot(transmission[0, 0, :]*1e9, transmission[0, 1, :]) +plt.xlabel("Wavelength(nm)") +plt.ylabel("Transmission") +plt.show() +``` ## Components @@ -327,7 +358,7 @@ right_grating.draw(cell) -### Self-define Components +### Customize Components We can get a "Class" from the function "MAKE_COMPONENT" that can be used to define our own sub cell from another gdsii file. More details can be found in "API Reference". @@ -349,16 +380,16 @@ component.draw(cell) All the components have functions for returning their port points to simplify the interconnecting operations. ```python -## first, a waveguide +# first, a waveguide waveguide = Waveguide(Point(0,-350),Point(10,-350),width=0.5) waveguide.draw(cell,wg_layer) -## second, a double connector +# second, a double connector doubleconnector = DoubleBendConnector(waveguide.get_end_point(),waveguide.get_end_point()+(10,-10),width=0.5) doubleconnector.draw(cell,wg_layer) -## third, add grating at the end of the double connector +# third, add grating at the end of the double connector rightgrating = AEMDgrating(doubleconnector.get_end_point(),RIGHT) rightgrating.draw(cell) -## fourth, add grating at the start of the waveguide +# fourth, add grating at the start of the waveguide leftgrating = AEMDgrating(waveguide.get_start_point(), LEFT) leftgrating.draw(cell) ``` @@ -396,7 +427,70 @@ make_gdsii_file("basic_inverse_and_cover.gds",inv_source_layer=wg_layer,inv_targ basic_inverse_and_cover +## Boolean operations + +Boolean operations can be made for layers. + +```python +from splayout import * + +# prepare the initial pattern +cell = Cell("Boolean") +layer1 = Layer(1, 0) +layer2 = Layer(2, 0) +result_layer = Layer(31,0) + +circle1 = Circle(Point(-1, 0), radius=2) +circle1.draw(cell, layer1) +circle2 = Circle(Point(1, 0), radius=2) +circle2.draw(cell, layer2) + +make_gdsii_file("boolean_before.gds") +``` + +![](_static/boolean_before.png) + +```python +# cut operation +layer1.cut(layer2, output_layer=result_layer) +make_gdsii_file("boolean_cut.gds") +``` + +![](_static/boolean_cut.png) + +```python +# add operation +layer1.add(layer2, output_layer=result_layer) +make_gdsii_file("boolean_add.gds") +``` + +![](_static/boolean_add.png) + +```python +# common operation +layer1.common(layer2, output_layer=result_layer) +make_gdsii_file("boolean_common.gds") +``` + +![](_static/boolean_common.png) + +```python +# dilation operation +layer1.dilation(distance=2, output_layer=result_layer) +make_gdsii_file("boolean_dilation.gds") +``` + +![](_static/boolean_dilation.png) + + + +```python +# inversion operation +layer1.inversion(distance=2, output_layer=result_layer) +make_gdsii_file("boolean_inversion.gds") +``` +![](_static/boolean_inversion.png) ## Examples diff --git a/examples/boolean_operations.py b/examples/boolean_operations.py new file mode 100644 index 0000000..14751c9 --- /dev/null +++ b/examples/boolean_operations.py @@ -0,0 +1,36 @@ +""" https://github.com/Hideousmon/SPLayout """ +from splayout import * + +if __name__ == "__main__": + # prepare the initial pattern + cell = Cell("Boolean") + layer1 = Layer(1, 0) + layer2 = Layer(2, 0) + result_layer = Layer(31,0) + + circle1 = Circle(Point(-1, 0), radius=2) + circle1.draw(cell, layer1) + circle2 = Circle(Point(1, 0), radius=2) + circle2.draw(cell, layer2) + + make_gdsii_file("boolean_before.gds") + + # cut operation + layer1.cut(layer2, output_layer=result_layer) + make_gdsii_file("boolean_cut.gds") + + # add operation + layer1.add(layer2, output_layer=result_layer) + make_gdsii_file("boolean_add.gds") + + # common operation + layer1.common(layer2, output_layer=result_layer) + make_gdsii_file("boolean_common.gds") + + # dilation operation + layer1.dilation(distance=2, output_layer=result_layer) + make_gdsii_file("boolean_dilation.gds") + + # inversion operation + layer1.inversion(distance=2, output_layer=result_layer) + make_gdsii_file("boolean_inversion.gds") \ No newline at end of file diff --git a/examples/inversedesign/PBS_DBS.py b/examples/inversedesign/PBS_DBS.py index 7e306d8..2b5a0e9 100644 --- a/examples/inversedesign/PBS_DBS.py +++ b/examples/inversedesign/PBS_DBS.py @@ -12,25 +12,25 @@ import matplotlib.pyplot as plt if __name__ == "__main__": - ## definitions for gdsii file creation + # definitions for gdsii file creation cell = Cell("PBS") wg_layer = Layer(1, 0) etch_layer = Layer(2, 0) oxide_layer = Layer(3, 0) - ## parameters for structures + # parameters for structures waveguide_width = 0.44 - waveguide_length =3 + waveguide_length = 3 waveguide_gap = 1 design_region_width = 2.4 design_region_length = 2.4 oxide_thickness = 2 Air = "etch" - ## initialize the simulation frame + # initialize the simulation frame fdtd = FDTDSimulation(fdtd_path="C:\\Program Files\\Lumerical\\v202\\api\\python") - ## draw the silicon-oxide substrate and waveguides into the gdsii file + # draw the silicon-oxide substrate and waveguides into the gdsii file waveguide_input = Waveguide(Point(-design_region_length / 2 - waveguide_length, 0), Point(-design_region_length / 2, 0), width=waveguide_width) @@ -49,10 +49,10 @@ height=design_region_width + 1) substrate.draw(cell, oxide_layer) - ## make the gdsii file + # make the gdsii file make_gdsii_file("PBS.gds") - ## add sources and monitors for the FDTD simulation + # add sources and monitors for the FDTD simulation fdtd.add_mode_source(waveguide_input.get_start_point() + (2, 0), source_name="source_TE", width=1, mode_number=1, wavelength_start=1.525, wavelength_end=1.575) fdtd.add_mode_source(waveguide_input.get_start_point() + (2, 0), source_name="source_TM", width=1, mode_number=2, @@ -66,7 +66,7 @@ fdtd.add_mesh_region(waveguide_input.get_start_point() + (1.5, -1.5), waveguide_output1.get_end_point() + (-1.5, 1), x_mesh=0.03, y_mesh=0.03, z_mesh=0.03, height=1) - ## add structures from the gdsii file to the FDTD simulation + # add structures from the gdsii file to the FDTD simulation fdtd.add_structure_from_gdsii("PBS.gds", "PBS", layer=wg_layer.layer, datatype=wg_layer.datatype, material=Si, z_start=-0.15, z_end=0.15, rename="WG") @@ -74,10 +74,13 @@ datatype=oxide_layer.datatype, material=SiO2, z_start=-2.15, z_end=-0.15, rename="OXIDE") - ## pixels region definition - pixels = RectanglePixelsRegion(Point(-design_region_length/2,-design_region_width/2),Point(design_region_length/2,design_region_width/2),pixel_x_length=0.12,pixel_y_length=0.12,fdtd_engine=fdtd,material=Air,z_start=-0.15,z_end=0.15) + # pixels region definition + pixels = RectanglePixelsRegion(Point(-design_region_length / 2, -design_region_width / 2), + Point(design_region_length / 2, design_region_width / 2), + pixel_x_length=0.12, pixel_y_length=0.12, fdtd_engine=fdtd, + material=Air, z_start=-0.15, z_end=0.15) - ## Define Optimization + # Define Optimization max_iteration = 3 loS = 400 FoMTE_list = np.zeros((max_iteration * loS + 1)) @@ -87,13 +90,13 @@ def cost_function(based_matrix): based_matrix = based_matrix.reshape(20, 20) pixels.update(based_matrix) - ## TE + # TE fdtd.switch_to_layout() fdtd.set_enable("source_TE") fdtd.set_disable("source_TM") fdtd.run("PBS") FoMTE = np.mean(fdtd.get_mode_transmission("mode1")[0, 1, :]) - ## TM + # TM fdtd.switch_to_layout() fdtd.set_enable("source_TM") fdtd.set_disable("source_TE") @@ -115,19 +118,19 @@ def call_back(): DBS = DirectBinarySearchAlgorithm(loS, cost_function, max_iteration, call_back) - ## save initial solution into the file + # save initial solution into the file np.save("init_solution", DBS.best_solution) - ## start DBS optimization + # start DBS optimization DBS.run() - ## save results into files + # save results into files np.save("solution", DBS.best_solution) np.save("cg_curve", DBS.cg_curve) np.save("FoMTE", np.array(FoMTE_list)) np.save("FoMTM", np.array(FoMTM_list)) - ## plot for the cost + # plot for the cost x = range(0, DBS.cg_curve.shape[0]) y = DBS.cg_curve xlabel = "Operation Times" diff --git a/examples/inversedesign/PBS_DBS_result_to_layout.py b/examples/inversedesign/PBS_DBS_result_to_layout.py new file mode 100644 index 0000000..6ab48e6 --- /dev/null +++ b/examples/inversedesign/PBS_DBS_result_to_layout.py @@ -0,0 +1,60 @@ +""" +https://github.com/Hideousmon/SPLayout (Version >= 0.5.2) +generate gdsii file with the result from polarization beam splitter inverse design example. +(Reference: Shen, B., Wang, P., Polson, R. et al. +An integrated-nanophotonics polarization beamsplitter with 2.4 × 2.4 μm2 footprint. +Nature Photon 9, 378–382 (2015). https://doi.org/10.1038/nphoton.2015.80) +""" + +from splayout import * +import numpy as np + +if __name__ == "__main__": + # result import + solution = np.load("./solution.npy") + + # definitions for gdsii file creation + cell = Cell("PBS") + wg_layer = Layer(1, 0) + etch_layer = Layer(2, 0) + + # parameters for structures + waveguide_width = 0.44 + waveguide_length = 3 + waveguide_gap = 1 + design_region_width = 2.4 + design_region_length = 2.4 + oxide_thickness = 2 + + # draw waveguides into the gdsii file + waveguide_input = Waveguide(Point(-design_region_length / 2 - waveguide_length, 0), + Point(-design_region_length / 2, 0), + width=waveguide_width, z_start=-0.15, z_end=0.15, material=Si) + waveguide_input.draw(cell, wg_layer) + waveguide_output1 = Waveguide(Point(design_region_length / 2, waveguide_gap / 2), + Point(design_region_length / 2 + waveguide_length, waveguide_gap / 2), + width=waveguide_width, z_start=-0.15, z_end=0.15, material=Si) + waveguide_output1.draw(cell, wg_layer) + waveguide_output2 = Waveguide(Point(design_region_length / 2, -waveguide_gap / 2), + Point(design_region_length / 2 + waveguide_length, -waveguide_gap / 2), + width=waveguide_width, z_start=-0.15, z_end=0.15, material=Si) + waveguide_output2.draw(cell, wg_layer) + design_region = Rectangle(Point(0, 0), width=design_region_length, height=design_region_width, + z_start=-0.15, z_end=0.15, material=Si) + design_region.draw(cell, wg_layer) + + # pixels region definition + pixels = RectanglePixelsRegion(Point(-design_region_length/2,-design_region_width/2), + Point(design_region_length/2,design_region_width/2), + pixel_x_length=0.12, pixel_y_length=0.12, fdtd_engine=None, + material=None, z_start=-0.15, z_end=0.15) + + # etch pixels to gdsii + solution = solution.reshape(20, 20) + pixels.draw_layout(solution, cell, etch_layer) + + # boolean operation to get the silicon pattern on wg_layer (optional) + wg_layer.cut(etch_layer) + + # make gdsii file + make_gdsii_file("./PBS") diff --git a/examples/inversedesign/PBS_DBS_without_gdsii.py b/examples/inversedesign/PBS_DBS_without_gdsii.py new file mode 100644 index 0000000..5e7e63f --- /dev/null +++ b/examples/inversedesign/PBS_DBS_without_gdsii.py @@ -0,0 +1,135 @@ +""" +https://github.com/Hideousmon/SPLayout (Version >= 0.5.2) +polarization beam splitter inverse design example without gdsii intermedia. +(Reference: Shen, B., Wang, P., Polson, R. et al. +An integrated-nanophotonics polarization beamsplitter with 2.4 × 2.4 μm2 footprint. +Nature Photon 9, 378–382 (2015). https://doi.org/10.1038/nphoton.2015.80) +""" + +from splayout import * +import numpy as np +import matplotlib.pyplot as plt + +if __name__ == "__main__": + # definitions for gdsii file creation + cell = Cell("PBS") + wg_layer = Layer(1, 0) + etch_layer = Layer(2, 0) + oxide_layer = Layer(3, 0) + + # parameters for structures + waveguide_width = 0.44 + waveguide_length = 3 + waveguide_gap = 1 + design_region_width = 2.4 + design_region_length = 2.4 + oxide_thickness = 2 + Air = "etch" + + # initialize the simulation frame + fdtd = FDTDSimulation(fdtd_path="C:\\Program Files\\Lumerical\\v202\\api\\python") + + # draw waveguides on Lumerical + waveguide_input = Waveguide(Point(-design_region_length / 2 - waveguide_length, 0), + Point(-design_region_length / 2, 0), + width=waveguide_width, z_start=-0.15, z_end=0.15, material=Si) + waveguide_input.draw_on_lumerical_CAD(fdtd) + waveguide_output1 = Waveguide(Point(design_region_length / 2, waveguide_gap / 2), + Point(design_region_length / 2 + waveguide_length, waveguide_gap / 2), + width=waveguide_width, z_start=-0.15, z_end=0.15, material=Si) + waveguide_output1.draw_on_lumerical_CAD(fdtd) + waveguide_output2 = Waveguide(Point(design_region_length / 2, -waveguide_gap / 2), + Point(design_region_length / 2 + waveguide_length, -waveguide_gap / 2), + width=waveguide_width, z_start=-0.15, z_end=0.15, material=Si) + waveguide_output2.draw_on_lumerical_CAD(fdtd) + design_region = Rectangle(Point(0, 0), width=design_region_length, height=design_region_width, + z_start=-0.15, z_end=0.15, material=Si) + design_region.draw_on_lumerical_CAD(fdtd) + substrate = Rectangle(Point(0, 0), width=design_region_length + 2 * waveguide_length + 2, + height=design_region_width + 1, z_start=-2.15, z_end=-0.15, material=SiO2) + substrate.draw_on_lumerical_CAD(fdtd) + + # add sources and monitors for the FDTD simulation + fdtd.add_mode_source(waveguide_input.get_start_point() + (2, 0), source_name="source_TE", width=1, mode_number=1, + wavelength_start=1.525, wavelength_end=1.575) + fdtd.add_mode_source(waveguide_input.get_start_point() + (2, 0), source_name="source_TM", width=1, mode_number=2, + wavelength_start=1.525, wavelength_end=1.575) + fdtd.add_mode_expansion(waveguide_output1.get_end_point() - (2, 0), mode_list=[1, 2], width=1, + expansion_name="mode1", points=101) + fdtd.add_mode_expansion(waveguide_output2.get_end_point() - (2, 0), mode_list=[1, 2], width=1, + expansion_name="mode2", points=101) + fdtd.add_fdtd_region(waveguide_input.get_start_point() + (1, -1.5), waveguide_output1.get_end_point() + (-1, 1), + z_symmetric=0, background_index=1) + fdtd.add_mesh_region(waveguide_input.get_start_point() + (1.5, -1.5), waveguide_output1.get_end_point() + (-1.5, 1), + x_mesh=0.03, y_mesh=0.03, z_mesh=0.03, height=1) + + # pixels region definition + pixels = RectanglePixelsRegion(Point(-design_region_length/2,-design_region_width/2), + Point(design_region_length/2,design_region_width/2), + pixel_x_length=0.12, pixel_y_length=0.12, fdtd_engine=fdtd, + material=Air, z_start=-0.15, z_end=0.15) + + # Define Optimization + max_iteration = 3 + loS = 400 + FoMTE_list = np.zeros((max_iteration * loS + 1)) + FoMTM_list = np.zeros((max_iteration * loS + 1)) + list_counter = 0 + + def cost_function(based_matrix): + based_matrix = based_matrix.reshape(20, 20) + pixels.update(based_matrix) + # TE + fdtd.switch_to_layout() + fdtd.set_enable("source_TE") + fdtd.set_disable("source_TM") + fdtd.run("PBS") + FoMTE = np.mean(fdtd.get_mode_transmission("mode1")[0, 1, :]) + # TM + fdtd.switch_to_layout() + fdtd.set_enable("source_TM") + fdtd.set_disable("source_TE") + fdtd.run("PBS") + FoMTM = np.mean(fdtd.get_mode_transmission("mode2")[1, 1, :]) + + global list_counter + FoMTE_list[list_counter] = FoMTE + FoMTM_list[list_counter] = FoMTM + list_counter += 1 + + return -FoMTE - FoMTM + + def call_back(): + print("Size of remained:", DBS.get_remained_size()) + print("Number of iteration:", DBS.get_iteration_number()) + print("The minimum cost:", DBS.get_cost()) + print("Best Solution:", DBS.get_best_solution()) + + DBS = DirectBinarySearchAlgorithm(loS, cost_function, max_iteration, call_back) + + # save initial solution into the file + np.save("init_solution", DBS.best_solution) + + # start DBS optimization + DBS.run() + + # save results into files + np.save("solution", DBS.best_solution) + np.save("cg_curve", DBS.cg_curve) + np.save("FoMTE", np.array(FoMTE_list)) + np.save("FoMTM", np.array(FoMTM_list)) + + # plot for the cost + x = range(0, DBS.cg_curve.shape[0]) + y = DBS.cg_curve + xlabel = "Operation Times" + ylabel = "Cost (Lower Better)" + plt.figure() + plt.xlabel(xlabel) + plt.ylabel(ylabel) + plt.plot(x, y) + plt.savefig("PBS", dpi=600) + plt.show() + plt.close() + plt.clf() + diff --git a/examples/simulation/waveguide.py b/examples/simulation/waveguide.py new file mode 100644 index 0000000..2b6c080 --- /dev/null +++ b/examples/simulation/waveguide.py @@ -0,0 +1,37 @@ +""" +https://github.com/Hideousmon/SPLayout (Version >= 0.4.0) +waveguide simulation example with Lumerical FDTD. +""" + +from splayout import * +import matplotlib.pyplot as plt + +if __name__ == '__main__': + # initialize the simulation frame + fdtd = FDTDSimulation(fdtd_path="C:\\Program Files\\Lumerical\\v202\\api\\python") + + # draw waveguides on Lumerical + waveguide = Waveguide(start_point=Point(-3,0), end_point=Point(3,0), width=1, z_start=-0.11, z_end=0.11, material=Si) + waveguide.draw_on_lumerical_CAD(fdtd) + + # add simulation region, source, and monitor + fdtd.add_fdtd_region(bottom_left_corner_point=Point(-2, -1.5), top_right_corner_point=Point(2, 1.5), background_index=1.444,dimension=3, height=0.8) + + fdtd.add_mode_source(position=Point(-1.5,0), width=1.5, height=0.8, wavelength_start=1.54, wavelength_end=1.57, mode_number=1) + + fdtd.add_mode_expansion(position=Point(1.5, 0), width=1.5, height=0.8, points=101, mode_list=[1]) + + # run simulation + fdtd.run("./temp") + + # get result return: (number of modes, 2, frequency points) + # where (number of modes, 0, frequency points) are wavelengths, + # (number of modes, 1, frequency points) are transmissions + transmission = fdtd.get_mode_transmission(expansion_name="expansion") + + # plot figure + plt.figure() + plt.plot(transmission[0, 0, :]*1e9, transmission[0, 1, :]) + plt.xlabel("Wavelength(nm)") + plt.ylabel("Transmission") + plt.show() \ No newline at end of file diff --git a/history.md b/history.md index ab31ab9..d63e7c4 100644 --- a/history.md +++ b/history.md @@ -210,4 +210,15 @@ * Add api reference for ScalableToOptRegion3D and AdjointForMultiTO. ### Version 0.5.2 (Sep 11, 2023) -* Enable backward direction calculations for monitors in the adjoint method. \ No newline at end of file +* Enable backward direction calculations for monitors in the adjoint method. + +### Version 0.5.3 (May 19, 2024) +* Topology region can be defined with fdtd_engine=None. +* Add auto_update option for mode expansion. +* Add waveguide simulation example. +* Refine inverse design example for direct binary search. +* Add layout generation example for direct binary search. +* Update readme.md +* Update api reference for pixelated regions. +* New functions in FDTDSimulation. +* Add boolean operations example and docs. \ No newline at end of file diff --git a/splayout/__init__.py b/splayout/__init__.py index f0bda5a..9204bea 100644 --- a/splayout/__init__.py +++ b/splayout/__init__.py @@ -1,4 +1,4 @@ -__version__ = "0.5.2" +__version__ = "0.5.3" ## Submodules from . import utils diff --git a/splayout/adjointmethod/scalabletoregion3d.py b/splayout/adjointmethod/scalabletoregion3d.py index 882211d..72570c8 100644 --- a/splayout/adjointmethod/scalabletoregion3d.py +++ b/splayout/adjointmethod/scalabletoregion3d.py @@ -80,7 +80,8 @@ def __init__(self, bottom_left_corner_point, top_right_corner_point, fdtd_engine self.rename = rename self.index_region_name = self.rename + "_index" self.field_region_name = self.rename + "_field" - self.__initialize() + if not (self.fdtd_engine is None): + self.__initialize() self.epsilon_figure = None self.field_figure = None diff --git a/splayout/adjointmethod/shaperegion2d.py b/splayout/adjointmethod/shaperegion2d.py index c28f074..966d228 100644 --- a/splayout/adjointmethod/shaperegion2d.py +++ b/splayout/adjointmethod/shaperegion2d.py @@ -57,7 +57,8 @@ def __init__(self, bottom_left_corner_point, top_right_corner_point, fdtd_engine self.rename = rename self.index_region_name = self.rename + "_index" self.field_region_name = self.rename + "_field" - self.__initialize() + if not (self.fdtd_engine is None): + self.__initialize() self.epsilon_figure = None self.field_figure = None diff --git a/splayout/adjointmethod/shaperegion3d.py b/splayout/adjointmethod/shaperegion3d.py index a812996..d7ce273 100644 --- a/splayout/adjointmethod/shaperegion3d.py +++ b/splayout/adjointmethod/shaperegion3d.py @@ -57,7 +57,8 @@ def __init__(self, bottom_left_corner_point, top_right_corner_point, fdtd_engine self.rename = rename self.index_region_name = self.rename + "_index" self.field_region_name = self.rename + "_field" - self.__initialize() + if not (self.fdtd_engine is None): + self.__initialize() self.epsilon_figure = None self.field_figure = None diff --git a/splayout/adjointmethod/topologyregion2d.py b/splayout/adjointmethod/topologyregion2d.py index 058a2a7..76e52c6 100644 --- a/splayout/adjointmethod/topologyregion2d.py +++ b/splayout/adjointmethod/topologyregion2d.py @@ -70,7 +70,8 @@ def __init__(self, bottom_left_corner_point, top_right_corner_point, fdtd_engine self.beta = beta self.index_region_name = self.rename + "_index" self.field_region_name = self.rename + "_field" - self.__initialize() + if not (self.fdtd_engine is None): + self.__initialize() self.epsilon_figure = None self.field_figure = None diff --git a/splayout/adjointmethod/topologyregion3d.py b/splayout/adjointmethod/topologyregion3d.py index 4775422..e3b6bc4 100644 --- a/splayout/adjointmethod/topologyregion3d.py +++ b/splayout/adjointmethod/topologyregion3d.py @@ -67,7 +67,8 @@ def __init__(self, bottom_left_corner_point, top_right_corner_point, fdtd_engine self.rename = rename self.index_region_name = self.rename + "_index" self.field_region_name = self.rename + "_field" - self.__initialize() + if not(self.fdtd_engine is None): + self.__initialize() self.epsilon_figure = None self.field_figure = None diff --git a/splayout/components/__init__.py b/splayout/components/__init__.py index 8b7d03f..562c19d 100644 --- a/splayout/components/__init__.py +++ b/splayout/components/__init__.py @@ -18,4 +18,4 @@ from .text import Text from .waveguide import Waveguide, ArbitraryAngleWaveguide from .sbend import SBend,ASBend -from .filledpattern import Circle,Rectangle \ No newline at end of file +from .filledpattern import Circle, Rectangle \ No newline at end of file diff --git a/splayout/lumericalcommun/fdtdapi.py b/splayout/lumericalcommun/fdtdapi.py index 382d195..393c43a 100644 --- a/splayout/lumericalcommun/fdtdapi.py +++ b/splayout/lumericalcommun/fdtdapi.py @@ -130,7 +130,9 @@ def add_power_monitor(self,position,width=2,height=0.8, z_min = None, z_max = No self.global_monitor_set_flag = 1 - def add_mode_expansion(self,position, mode_list, width=2, height=0.8, z_min = None, z_max = None, expansion_name="expansion", points = 251, update_mode = 0, normal_direction = HORIZONTAL): + def add_mode_expansion(self,position, mode_list, width=2, height=0.8, z_min = None, z_max = None, + expansion_name="expansion", points = 251, update_mode = 0, + normal_direction = HORIZONTAL, auto_update = 1): """ Add mode expansion monitor in Lumerical FDTD. @@ -156,6 +158,8 @@ def add_mode_expansion(self,position, mode_list, width=2, height=0.8, z_min = No Whether update the mode after defining FDTD and mesh (default: 0). normal_direction : HORIZONAL or VERTICAL The direction of the monitor. HORIZONAL: x-normal, VERTICAL: y-normal. + update_mode : Int or bool + Whether enable auto update in Lumerical. Notes ----- @@ -202,10 +206,13 @@ def add_mode_expansion(self,position, mode_list, width=2, height=0.8, z_min = No self.fdtd.eval("set(\"mode selection\",\"user select\");") self.fdtd.eval("set(\"selected mode numbers\"," + self.str_list(mode_list) + ");") - if (update_mode): + if update_mode: self.fdtd.updatemodes() self.fdtd.eval("set(\"override global monitor settings\",0);") - self.fdtd.eval("set(\"auto update\",1);") + if auto_update: + self.fdtd.eval("set(\"auto update\",1);") + else: + self.fdtd.eval("set(\"auto update\",0);") def reset_mode_expansion_modes(self, expansion_name, mode_list): """ @@ -319,7 +326,7 @@ def add_mode_source(self,position, width=2,height=0.8, z_min = None, z_max = Non self.wavelength_end = wavelength_end*1e-6 self.global_source_set_flag = 1 - def add_imported_source(self, position, width, height, origin_x, origin_y, origin_z, E, H = None, + def add_imported_source(self, position, width, height, origin_x=None, origin_y=None, origin_z=None, E=None, H = None, z_min = None, z_max = None, source_name = "source", amplitude=1 , phase = 0, direction = FORWARD, normal_direction = HORIZONTAL): """ @@ -363,14 +370,6 @@ def add_imported_source(self, position, width, height, origin_x, origin_y, origi If z_min and z_max are specified, the height property will be invalid. """ position = tuple_to_point(position) - wavelength = np.flip(self.get_wavelength()) - frequency = np.flip(self.get_frequency()) - - self.fdtd.putv("lam", wavelength) - self.fdtd.putv("f", frequency) - self.fdtd.putv("Ex", E[:, :, :, :, 0]) - self.fdtd.putv("Ey", E[:, :, :, :, 1]) - self.fdtd.putv("Ez", E[:, :, :, :, 2]) self.fdtd.eval("addimportedsource;") self.fdtd.eval("set(\"name\",\"" + source_name + "\");") @@ -407,18 +406,28 @@ def add_imported_source(self, position, width, height, origin_x, origin_y, origi self.fdtd.eval("set(\"amplitude\"," + "%.6f" % (amplitude) + ");") self.fdtd.eval("set(\"phase\"," + "%.6f" % (phase) + ");") - self.fdtd.putv("x", origin_x) - self.fdtd.putv("y", origin_y) - self.fdtd.putv("z", origin_z) - self.fdtd.eval("EM = rectilineardataset(\"EM fields\",x,y,z);") - self.fdtd.eval("EM.addparameter(\"lambda\", lam, \"f\", f);") - self.fdtd.eval("EM.addattribute(\"E\", Ex, Ey, Ez);") - if (type(H) != type(None)): - self.fdtd.putv("Hx", H[:, :, :, :, 0]) - self.fdtd.putv("Hy", H[:, :, :, :, 1]) - self.fdtd.putv("Hz", H[:, :, :, :, 2]) - self.fdtd.eval("EM.addattribute(\"H\", Hx, Hy, Hz);") - self.fdtd.eval("importdataset(EM);") + if not((origin_x is None) and (origin_y is None) and (origin_z is None) and (E is None) and + (H is None)): + wavelength = np.flip(self.get_wavelength()) + frequency = np.flip(self.get_frequency()) + + self.fdtd.putv("lam", wavelength) + self.fdtd.putv("f", frequency) + self.fdtd.putv("Ex", E[:, :, :, :, 0]) + self.fdtd.putv("Ey", E[:, :, :, :, 1]) + self.fdtd.putv("Ez", E[:, :, :, :, 2]) + self.fdtd.putv("x", origin_x) + self.fdtd.putv("y", origin_y) + self.fdtd.putv("z", origin_z) + self.fdtd.eval("EM = rectilineardataset(\"EM fields\",x,y,z);") + self.fdtd.eval("EM.addparameter(\"lambda\", lam, \"f\", f);") + self.fdtd.eval("EM.addattribute(\"E\", Ex, Ey, Ez);") + if (type(H) != type(None)): + self.fdtd.putv("Hx", H[:, :, :, :, 0]) + self.fdtd.putv("Hy", H[:, :, :, :, 1]) + self.fdtd.putv("Hz", H[:, :, :, :, 2]) + self.fdtd.eval("EM.addattribute(\"H\", Hx, Hy, Hz);") + self.fdtd.eval("importdataset(EM);") def reset_source_mode(self, source_name, mode_number): @@ -1728,6 +1737,202 @@ def eval(self, command): self.fdtd.eval(command) + def add_electric_dipole(self, center_point, source_name = "source", z_min = 0 + , amplitude = 1, phase = 0, wavelength_start = 1.54, + wavelength_end = 1.57): + """ + Add source in Lumerical FDTD. + + Parameters + ---------- + center_point : Point or tuple + Center point of the source. + z_min : Float + The position on z-axis (unit: μm, default: 0). + source_name : String + Name of the source in Lumerical FDTD (default: "source"). + amplitude : Float or Int + The amplitude of the source (default: 1). + phase : Float or Int + The phase of the source (default: 0). + wavelength_start : Float + The start wavelength of the source (unit: μm, default: 1.540). + wavelength_end : Float + The end wavelength of the source (unit: μm, default: 1.570). + """ + position = tuple_to_point(center_point) + self.fdtd.eval("adddipole;") + self.fdtd.eval("set(\"name\",\"" + source_name + "\");") + + self.fdtd.eval("set(\"dipole type\",\"Electric dipole\");") + + self.fdtd.eval("set(\"x\"," + "%.6f"%(position.x) + "e-6);") + self.fdtd.eval("set(\"y\"," + "%.6f"%(position.y) + "e-6);") + self.fdtd.eval("set(\"z\"," + "%.6f" % (z_min) + "e-6);") + + + self.fdtd.eval("set(\"override global source settings\",0);") + self.fdtd.eval("set(\"amplitude\"," + "%.12f"%(amplitude) + ");") + self.fdtd.eval("set(\"phase\"," + "%.6f"%(phase) + ");") + if not self.global_source_set_flag: + self.fdtd.setglobalsource('set wavelength', True) + self.fdtd.setglobalsource('wavelength start', wavelength_start*1e-6) + self.fdtd.setglobalsource('wavelength stop', wavelength_end*1e-6) + self.wavelength_start = wavelength_start*1e-6 + self.wavelength_end = wavelength_end*1e-6 + self.global_source_set_flag = 1 + + + def add_field_point(self, center_point, z_min = 0, field_monitor_name="field", points=1): + """ + Add a point field monitor in Lumerical FDTD (DFT Frequency monitor). + + Parameters + ---------- + center_point : Point or tuple + Center point of the monitor. + z_min : Float + The position on z-axis (unit: μm, default: 0). + field_monitor_name : String + Name of the monitor in Lumerical FDTD (default: "field"). + points : Int + The number of the frequency points that will be monitored (default: 1). + """ + self.fdtd.eval("addpower;") + self.fdtd.eval("set(\"name\",\"" + field_monitor_name + "\");") + + position = center_point + self.fdtd.eval("set(\"monitor type\",1);") + self.fdtd.eval("set(\"x\"," + "%.6f" % (position.x) + "e-6);") + self.fdtd.eval("set(\"y\"," + "%.6f" % (position.y) + "e-6);") + self.fdtd.eval("set(\"z\"," + "%.6f" % (z_min) + "e-6);") + + self.fdtd.eval("set(\"override global monitor settings\",0);") + if not self.global_monitor_set_flag: + self.fdtd.setglobalmonitor('use source limits', True) + self.fdtd.setglobalmonitor('use wavelength spacing', True) + self.fdtd.setglobalmonitor('frequency points', points) + self.frequency_points = points + self.global_monitor_set_flag = 1 + + def get_dipole_power(self, source_name=None, wavelengths = None,datafile = None): + """ + Get source power spectrum from source. + + Parameters + ---------- + source_name : String + Name of the dipole source. + datafile : String + The name of the file for saving the data, None means no saving (default: None). + + Returns + ------- + out : Array + Spectrum, size: (1,frequency points). + + Notes + ----- + This function should be called after setting the frequency points in any frequency domain monitor. + """ + if type(wavelengths) != type(None): + frequency = scipy.constants.speed_of_light / np.array([wavelengths]).flatten() + if (type(source_name) == type(None)): + source_power = self.fdtd.dipolepower(frequency) + else: + self.lumapi.putMatrix(self.fdtd.handle, "frequency", frequency) + self.fdtd.eval("data = dipolepower(frequency,\""+source_name+"\");") + source_power = self.lumapi.getVar(self.fdtd.handle, varname="data") + elif self.global_source_set_flag and self.global_monitor_set_flag: + wavelength = np.linspace(self.wavelength_start, self.wavelength_end, self.frequency_points) + frequency = scipy.constants.speed_of_light / wavelength + if (type(source_name) == type(None)): + source_power = self.fdtd.sourcepower(frequency) + else: + self.lumapi.putMatrix(self.fdtd.handle, "frequency", frequency) + self.fdtd.eval("data = dipolepower(frequency,\""+source_name+"\");") + source_power = self.lumapi.getVar(self.fdtd.handle, varname="data") + else: + raise Exception("The source is not well defined!") + if (datafile != None): + np.save(datafile, source_power.flatten()) + return np.asarray(source_power).flatten() + + def get_dipole_base_amplitude(self, source_name, datafile = None): + """ + Get source power spectrum from source. + + Parameters + ---------- + source_name : String + Name of the dipole source. + datafile : String + The name of the file for saving the data, None means no saving (default: None). + + Returns + ------- + out : Array + Base amplitude, size: (1,frequency points). + + Notes + ----- + This function should be called after setting the frequency points in any frequency domain monitor. + """ + self.fdtd.eval("data = getnamed(\""+source_name+"\", 'base amplitude');") + base_amplitued = self.lumapi.getVar(self.fdtd.handle, varname="data") + + if (datafile != None): + np.save(datafile, base_amplitued.flatten()) + return np.asarray(base_amplitued).flatten() + + def reset_imported_source(self, origin_x, origin_y, origin_z, E, H = None, source_name = "source", amplitude=1 , phase = 0): + """ + Reset imported source in Lumerical FDTD. + + Parameters + ---------- + origin_x : Array + The origin nodes distribution on x-axis. + origin_y : Array + The origin nodes distribution on y-axis. + origin_z : Array + The origin nodes distribution on z-axis. + E : Array + The imported electric field distribution. + H : Array + The imported magnetic field distribution. + source_name : String + Name of the source in Lumerical FDTD (default: "source"). + amplitude : Float or Int + The amplitude of the source. + phase : Float or Int + The phase of the source. + """ + wavelength = np.flip(self.get_wavelength()) + frequency = np.flip(self.get_frequency()) + + self.fdtd.putv("lam", wavelength) + self.fdtd.putv("f", frequency) + self.fdtd.putv("Ex", E[:, :, :, :, 0]) + self.fdtd.putv("Ey", E[:, :, :, :, 1]) + self.fdtd.putv("Ez", E[:, :, :, :, 2]) + self.fdtd.eval("select(\"" + source_name + "\");") + + self.fdtd.eval("set(\"amplitude\"," + "%.6f" % (amplitude) + ");") + self.fdtd.eval("set(\"phase\"," + "%.6f" % (phase) + ");") + + self.fdtd.putv("x", origin_x) + self.fdtd.putv("y", origin_y) + self.fdtd.putv("z", origin_z) + self.fdtd.eval("EM = rectilineardataset(\"EM fields\",x,y,z);") + self.fdtd.eval("EM.addparameter(\"lambda\", lam, \"f\", f);") + self.fdtd.eval("EM.addattribute(\"E\", Ex, Ey, Ez);") + if (type(H) != type(None)): + self.fdtd.putv("Hx", H[:, :, :, :, 0]) + self.fdtd.putv("Hy", H[:, :, :, :, 1]) + self.fdtd.putv("Hz", H[:, :, :, :, 2]) + self.fdtd.eval("EM.addattribute(\"H\", Hx, Hy, Hz);") + self.fdtd.eval("importdataset(EM);")