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#

24da1295709b4207af5b02430e224c39

[ ]:
 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)

e8120b5fa647456898f7ebd6d039fdd1

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)

39413eacc3884f5295706fc6d0dd1e6a