Photonic Crystal Bandstructure (FDTD)#
This example demonstrates a photonic crystal simulation utilizing Structure and Analysis Group objects. Based on: https://optics.ansys.com/hc/en-us/articles/360041566614-Rectangular-Photonic-Crystal-Bandstructure
In Part 1, we build the structure and set the FDTD simulation region. In this case, the spheres are holes (filled with air, n = 1) and the background material is a simple dielectric material. Some advanced simulation objects, including the dipole cloud source and bandstructure analysis groups, are imported from the Object Library. We run a single simulation and visualize the resulting spectrum.
In Part 2, we set up a series of sweeps to collect the resonant frequencies. In this example, we use the built-in sweep tool in Lumerical, but the parameter sweeps could also be set up from Python. We then run the sweeps and plot the results.
Prerequisites: Valid FDTD license is required.
Perform required imports
[ ]:
1from collections import OrderedDict
2import itertools
3
4import matplotlib.pyplot as plt # Only required for plotting
5import numpy as np
6
7import ansys.lumerical.core as lumapi
Part 1: Set up structures and simulation objects#

[ ]:
8# Define parameters
9
10# Set filename for saving and loading
11filename = "photonic_crystal_bandstructure.fsp"
12
13# set desired period of the array (3D)
14ax = 0.5e-6
15ay = 0.5e-6
16az = 0.5e-6
17
18# number of spheres in each direction
19nx = 3
20ny = 3
21nz = 3
22
23sphere_radius = 0.25e-6
24sphere_index = 1 # Specify index if "Object defined dielectric" is used; otherwise, specify material from database below
25sphere_material = "etch" # Etch n = 1 (air)
26background_index = 3.6055
27
28# Define frequencies / wavelengths of interest
29f1 = 1e12 # THz
30f2 = 220e12 # THz
[ ]:
31# Initialize session and build simulation objects. Set hide = True to hide the Lumerical GUI.
32# This block will build an array of spheres for the photonic crystal structure.
33# We add an FDTD region, sources, and monitors. The sources and monitors are imported from the Object Library.
34# Finally, the simulation file is saved to the file name set earlier.
35
36with lumapi.FDTD(hide=False) as fdtd:
37 # Add rectangular lattice array of spheres
38 # Note you can also use Lumerical's built-in "rect_pc_3D" object using fdtd.addobject("rect_pc_3D")
39 # In this example, we construct the array programmatically
40 fdtd.addstructuregroup({"name": "rect_pc_3D"})
41 for ix, iy, iz in itertools.product(*map(range, (nx, ny, nz))):
42 objname = f"c{ix}{iy}{iz}"
43 fdtd.addsphere(
44 {"name": objname, "x": (ix - 1) * ax, "y": (iy - 1) * ay, "z": (iz - 1) * az, "radius": sphere_radius, "material": sphere_material}
45 )
46 fdtd.addtogroup("rect_pc_3D") # Adds the recently created sphere to the structure group
47
48 # Add FDTD region
49 dx = 0.025e-6 # Mesh dx, dy, dz
50 fdtd_geometry_props = {"x": 0, "x span": ax, "y": 0, "y span": ay, "z": 0, "z span": az}
51 fdtd_mesh_props = {"index": background_index, "mesh type": "uniform", "dx": dx, "dy": dx, "dz": dx}
52 fdtd_boundary_props = {"x min bc": "Bloch", "y min bc": "Bloch", "z min bc": "Bloch", "set based on source angle": False, "bloch units": "SI"}
53 # Combine properties settings into one dictionary
54 fdtd_props = OrderedDict({**fdtd_geometry_props, **fdtd_mesh_props, **fdtd_boundary_props})
55
56 fdtd.addfdtd(properties=fdtd_props)
57
58 # Set up sources (dipole cloud) and monitors (bandstructure)
59 # These are both Analysis Groups available from the object library
60 dipole_geometry_props = {"n dipoles": 3, "lattice type": 3, "z span": az, "ax": ax, "ay": ay, "az": az, "a": az, "x": 0, "y": 0, "z": 0}
61 dipole_frequency_props = {"f1": f1, "f2": f2, "kx": 0.5, "ky": 0, "kz": 0}
62 dipole_props = OrderedDict({**dipole_geometry_props, **dipole_frequency_props})
63
64 fdtd.addobject("dipole_cloud", properties=dipole_props)
65
66 bandstructure_geometry_props = {"n monitors": 10, "x": 0, "x span": ax, "y": 0, "y span": ay, "z": 0, "z span": az}
67 bandstructure_frequency_props = {"f1": f1, "f2": f2}
68 bandstructure_props = OrderedDict({**bandstructure_geometry_props, **bandstructure_frequency_props})
69 fdtd.addobject("bandstructure", properties=bandstructure_props)
70
71 # zoom CAD view around simulation region
72 fdtd.select("FDTD")
73 fdtd.setview("extent")
74
75 fdtd.save(filename)
76 print("File saved to folder as: " + filename)
[ ]:
77# Open the file and run a single simulation. Visualize the spectrum.
78
79with lumapi.FDTD(filename, hide=False) as fdtd:
80 print("Simulation running...")
81 fdtd.run()
82 print("Run completed. Analyzing and plotting results...")
83 fdtd.runanalysis()
84
85 # Plot results
86 single_spectrum = fdtd.getresult("bandstructure", "spectrum")
87 spectrum = single_spectrum["fd"]
88 frequencies = single_spectrum["f"]
89
90 plt.figure()
91 plt.plot(frequencies, spectrum)
92 plt.title("Single Simulation Spectrum")
93 plt.xlabel("Frequency")
94 plt.show(block=False)
95 plt.pause(2)

Part 2: Set up and run sweeps to extract resonant frequencies and plot the bandstructure#
[ ]:
96# Normalization factor for SI units; see note above.
97norm_x = 2 * np.pi / ax
98norm_y = 2 * np.pi / ay
99norm_z = 2 * np.pi / az
100
101# The total number of simulations will be num_points*3
102num_points = 5
103
104
105# Define a function to add the sweeps
106def add_bandstructure_sweep(sweep_name, x_start, x_stop, y_start, y_stop, z_start, z_stop):
107 """Set up a sweep for bandstructure calculations."""
108 fdtd.addsweep()
109 fdtd.setsweep("sweep", "name", sweep_name)
110 fdtd.setsweep(sweep_name, "type", "Ranges")
111 fdtd.setsweep(sweep_name, "number of points", num_points)
112
113 # set the sweep properties
114 props_kx = {"Name": "kx", "Parameter": "::model::FDTD::kx", "Type": "Number", "Start": x_start, "Stop": x_stop}
115 fdtd.addsweepparameter(sweep_name, props_kx)
116 props_ky = {"Name": "ky", "Parameter": "::model::FDTD::ky", "Type": "Number", "Start": y_start, "Stop": y_stop}
117 fdtd.addsweepparameter(sweep_name, props_ky)
118 props_kz = {"Name": "kz", "Parameter": "::model::FDTD::kz", "Type": "Number", "Start": z_start, "Stop": z_stop}
119 fdtd.addsweepparameter(sweep_name, props_kz)
120
121 # define results
122 result_fs = {"Name": "fs", "Result": "::model::bandstructure::fs"}
123 fdtd.addsweepresult(sweep_name, result_fs)
124 result_spectrum = {"Name": "fs", "Result": "::model::bandstructure::spectrum"}
125 fdtd.addsweepresult(sweep_name, result_spectrum)
126
127
128# Now add the sweeps to the file
129with lumapi.FDTD(filename, hide=False) as fdtd:
130 # Add Gamma-X sweep
131 add_bandstructure_sweep("Gamma-X", 0 * norm_x, 0.5 * norm_x, 0 * norm_y, 0 * norm_y, 0 * norm_z, 0 * norm_z)
132 # Add X-M sweep
133 add_bandstructure_sweep("X-M", 0.5 * norm_x, 0.5 * norm_x, 0 * norm_y, 0.5 * norm_y, 0 * norm_z, 0 * norm_z)
134 # Add M-R sweep
135 add_bandstructure_sweep("M-R", 0.5 * norm_x, 0.5 * norm_x, 0.5 * norm_y, 0.5 * norm_y, 0 * norm_z, 0.5 * norm_z)
136 fdtd.save(filename)
137 print("Sweeps have been set up.")
[ ]:
138# Now run all the sweeps - this may take a few minutes
139with lumapi.FDTD(filename, hide=False) as fdtd:
140 print("Running sweeps...")
141 fdtd.runsweep()
142 fdtd.save(filename)
143 # If you have previously run the sweeps, you can load them using this line instead:
144 # fdtd.loadsweep()
145print("Sweeps have been run.")
[ ]:
146# Retrieve and analyze data from the sweeps
147with lumapi.FDTD(filename, hide=False) as fdtd:
148 fs_all = np.zeros((50, 3 * num_points)) # 50 is the number of frequencies, and there are 3*num_points bandstructure points
149
150 sweepname = "Gamma-X"
151 resonance = fdtd.getsweepresult(sweepname, "fs") # Results are returned as Python dict
152 resonance_fs = resonance["fs"]
153 fs_all[0:50, 0:num_points] = resonance_fs
154
155 sweepname = "X-M"
156 resonance = fdtd.getsweepresult(sweepname, "fs") # Results are returned as Python dict
157 resonance_fs = resonance["fs"]
158 fs_all[0:50, num_points : 2 * num_points] = resonance_fs
159
160 sweepname = "M-R"
161 resonance = fdtd.getsweepresult(sweepname, "fs") # Results are returned as Python dict
162 resonance_fs = resonance["fs"]
163 fs_all[0:50, 2 * num_points : 3 * num_points] = resonance_fs
[ ]:
164k = np.linspace(1, 3 * num_points, 3 * num_points)
165c = 3 * 10**8
166plt.figure()
167for f in range(0, 50):
168 plt.scatter(k, fs_all[f] * ax / c)
169plt.xlabel("k (Gamma-X-M-R-Gamma")
170plt.ylabel("Resonant Frequency f (Hz*a/c)")
171plt.show(block=False)
172plt.pause(10)
