All-dielectric structural colors

All-dielectric structural colors#

Structural colors are produced through the light manipulation at the nanoscale, where the periodic arrangement of materials dictates light phenomena such as reflection, diffraction, and interference. This approach offers a wide range of bright and vivid colors in various applications, ranging from displays and sensors to optical communication devices, without the need for traditional dyes or pigments. In this notebook we will use Tidy3D to simulate the optical response of an all-dielectric structural color metasurface, as proposed by Bo Yang, Wenwei Liu, Zhancheng Li, Hua Cheng, Duk-Yong Choi, Shuqi Chen, and Jianguo Tian, "Ultrahighly Saturated Structural Colors Enhanced by Multipolar-Modulated Metasurfaces," Nano Letters 19(7), 4221-4228 (2019) DOI: 10.1021/acs.nanolett.8b04923.

Here, we will show how to use the Design plugin, introduced in Tidy3d version 2.6.0, to programmatically define and run a parameter sweep and then use its convenient container for managing the resulting data. See the Design Space Exploration Plugin notebook for a detailed example on how to use it.

MIM

You can find related content in MIM resonator, Dielectric metasurface absorber, and CMOS RGB image sensor.

If you are new to the finite-difference time-domain (FDTD) method, we highly recommend going through our FDTD101 tutorials. FDTD simulations can diverge due to various reasons. If you run into any simulation divergence issues, please follow the steps outlined in our troubleshooting guide to resolve it.

First we will include all the libraries needed to run the notebook and also include the Design plugin as tdd.

[1]:
# Standard python imports.
import numpy as np
import matplotlib.pylab as plt
from scipy.signal import find_peaks
import pandas as pd

# Import regular tidy3d.
import tidy3d as td
import tidy3d.web as web
import tidy3d.plugins.design as tdd

Simulation Set Up#

The device geometry is comprised of a periodic pattern of dielectric multilayer stacks. Each unit cell consists of a 60 nm thick \(SiO_{3}N_{4}\) layer, a 140 nm thick \(TiO_{2}\) spacer layer, and a 100 nm thick \(SiO_{2}\) capping layer. The thin film multilayer was built by successive material depositions into a silica substrate. Below, we will define the device geometric parameters.

[2]:
period = 0.35  # Unit cell period (um).
gap = 0.15  # Gap between adjacent unit cells (um).
sio2_thick = 0.10  # SiO2 dielectric thin film thickness (um).
tio2_thick = 0.14  # TiO2 dielectric thin film thickness (um).
si3n4_thick = 0.06  # Si3N4 dielectric thin film thickness (um).
sub_thick = 0.6  # SiO2 substrate thickness (um).
air_thick = 1.0  # Upper air layer thickness (um).

Material definition.

[3]:
n_sio2 = 1.45  # SiO2 refractive index.
n_tio2 = 2.41  # TiO2 refractive index.
n_si3n4 = 2.00  # Si3N4 refractive index.

mat_sio2 = td.Medium(permittivity=n_sio2**2)  # SiO2.
mat_tio2 = td.Medium(permittivity=n_tio2**2)  # TiO2.
mat_si3n4 = td.Medium(permittivity=n_si3n4**2)  # Si3N4.
mat_air = td.Medium(permittivity=1)  # Air.

Next, we define the wavelength, frequencies, and other simulation parameters.

[4]:
wl_min = 0.400  # Minimum simulation wavelength (um).
wl_max = 0.700  # Maximum simulation wavelength (um).
n_wl = 151  # Number of wavelength points within the simulation bandwidth.
wl_res = np.asarray([0.63, 0.52, 0.45])  # Resonance wavelengths of RGB pixels.
run_time = 2e-12  # Simulation run time (s).

wl_c = (wl_min + wl_max) / 2  # Central simulation wavelength (um).
wl_range = np.linspace(wl_min, wl_max, n_wl)  # Simulation wavelength range (um).
freq_c = td.C_0 / wl_c  # Central simulation frequency (Hz).
freq_range = td.C_0 / wl_range  # Simulation frequency range (Hz).
freq_bw = 0.5 * (freq_range[0] - freq_range[-1])  # Source bandwidth (Hz).
freq_res = td.C_0 / wl_res  # Resonance frequencies (Hz).

size_z = (
    sub_thick + si3n4_thick + tio2_thick + sio2_thick + air_thick
)  # Simulation size in the z-direction (um).

The next function defines the device unit cell. Here, we will define the dielectric stack structure, a plane wave source, and include a flux monitor to calculate the reflectance. The function accepts the unit cell period as an input parameter and returns a Simulation object. We will use it later as a pre-processing function to run the parameter sweep.

[5]:
def build_sim(period: float = period) -> td.Simulation:
    """Builds the device simulation and returns a simulation object.
    Parameters:
        period: float
            Device unit cell period (um).
    Returns:
        sim: tidy3d.Simulation
            FDTD simulation object.
    """


    _inf = 10

    # Dielectric stack width (um).
    width = period - gap

    # Simulation size.
    size_x = period
    size_y = period

    # Substrate.
    substrate = td.Structure(
        geometry=td.Box.from_bounds(
            rmin=(-_inf, -_inf, -_inf), rmax=(_inf, _inf, -size_z / 2 + sub_thick)
        ),
        medium=mat_sio2,
    )

    # Si3N4 layer.
    si3n4_layer = td.Structure(
        geometry=td.Box.from_bounds(
            rmin=(-width / 2, -width / 2, -size_z / 2 + sub_thick),
            rmax=(width / 2, width / 2, -size_z / 2 + sub_thick + si3n4_thick),
        ),
        medium=mat_si3n4,
    )

    # TiO2 layer.
    tio2_layer = td.Structure(
        geometry=td.Box.from_bounds(
            rmin=(-width / 2, -width / 2, -size_z / 2 + sub_thick + si3n4_thick),
            rmax=(
                width / 2,
                width / 2,
                -size_z / 2 + sub_thick + si3n4_thick + tio2_thick,
            ),
        ),
        medium=mat_tio2,
    )

    # SiO2 layer.
    sio2_layer = td.Structure(
        geometry=td.Box.from_bounds(
            rmin=(
                -width / 2,
                -width / 2,
                -size_z / 2 + sub_thick + si3n4_thick + tio2_thick,
            ),
            rmax=(
                width / 2,
                width / 2,
                -size_z / 2 + sub_thick + si3n4_thick + tio2_thick + sio2_thick,
            ),
        ),
        medium=mat_sio2,
    )

    # Plane wave excitation source.
    plane_wave = td.PlaneWave(
        source_time=td.GaussianPulse(freq0=freq_c, fwidth=freq_bw),
        size=(td.inf, td.inf, 0),
        center=(0, 0, size_z / 2 - wl_c / 2),
        direction="-",
        pol_angle=0,
        angle_theta=0,
    )

    # Flux monitor to measure the reflectance.
    ref_monitor = td.FluxMonitor(
        center=(0, 0, size_z / 2 - wl_c / 4),
        size=(td.inf, td.inf, 0),
        freqs=freq_range,
        name="R",
    )

    # Simulation
    sim = td.Simulation(
        size=(size_x, size_y, size_z),
        center=(0, 0, 0),
        grid_spec=td.GridSpec.auto(
            min_steps_per_wvl=40, wavelength=(wl_min + wl_max) / 2
        ),
        structures=[substrate, si3n4_layer, tio2_layer, sio2_layer],
        sources=[plane_wave],
        monitors=[ref_monitor],
        run_time=run_time,
        boundary_spec=td.BoundarySpec(
            x=td.Boundary.periodic(), y=td.Boundary.periodic(), z=td.Boundary.pml()
        ),
        symmetry=(-1, 1, 0),
        medium=mat_air,
    )
    return sim

Before running any simulation, we will visually inspect the simulation setup.

[6]:
sim = build_sim()
sim.plot_3d()

Design Set Up#

Now, we want to visualize how the device reflectance spectrum evolves when we vary the unit cell period from 0.3 \(\mu m\) to 0.4 \(\mu m\). Then, we will use the Design plugin to perform a parameter sweep.

The Design plugin uses the concept of DesignSpace, which gathers the design method and design parameters in a single object. So, we will start by defining our design parameter as a tdd.ParameterFloat instance and give it a range.

Note: we need to ensure that the parameter name argument matches the function argument name in our build_sim() function, which we will use to construct the simulations for the parameter sweep.

[7]:
param_p = tdd.ParameterFloat(name="period", span=(0.3, 0.4), num_points=11)

Next, we will define a tdd.MethodGrid which allows for a grid search over the parameter space, and then combine everything into a tdd.DesignSpace.

[8]:
method = tdd.MethodGrid()
design_space = tdd.DesignSpace(parameters=[param_p], method=method)

To run the parameter sweep, we need to define pre- and post-processing functions to respectively create the simulations and process their results. That enables the design plugin to take advantage of parallelism to perform Batch processing under the hood.

Our pre-processing function is the build_sim() function defined before. In our post-processing function, we will illustrate how to return single values, like the reflectance peak value and wavelength, as well as multidimensional data as the raw monitor dataset.

[9]:
# Post-processing function.
def fn_post(sim_data: td.SimulationData = None) -> dict:
    """Pos-processing function to calculate and return the reflectance peak
    in addition to the reflectance FluxMonitor.
    Parameters:
        sim_data: tidy3d.SimulationData
            Simulation data object where to get results from.
    Returns:
        dict:
            A dictionary containing the reflectance monitor,
            the reflectance peak and wavelength.
    """
    R_data = sim_data["R"]
    R = R_data.flux.values
    wavelength = td.C_0 / R_data.flux.f.values
    peaks_id, _ = find_peaks(R, height=0.6)
    peak_R = R[peaks_id[0]]
    peak_wvl = wavelength[peaks_id[0]]
    return {
        "Reflectance": R_data,
        "Peak Reflectance": peak_R,
        "Peak Wavelength": peak_wvl,
    }

Run Design#

Finally, we will pass our pre-processing and post-processing functions to the DesignSpace.run_batch() method to get our results.

[10]:
results = design_space.run_batch(fn_pre=build_sim, fn_post=fn_post)
20:29:55 -03 Created task '{'period': 0.3}' with task_id 
             'fdve-9b2a528d-fe89-46d1-add3-2908cafda49b' and task_type 'FDTD'.
20:29:57 -03 Created task '{'period': 0.31}' with task_id 
             'fdve-7d435325-28b0-4f2e-85bd-284cb1608d1e' and task_type 'FDTD'.
20:29:58 -03 Created task '{'period': 0.32}' with task_id 
             'fdve-20fc4a7a-2a2b-4ea8-8353-9854d5fccbfc' and task_type 'FDTD'.
20:30:00 -03 Created task '{'period': 0.33}' with task_id 
             'fdve-83f30131-b607-4fb4-b034-c8d9e367b275' and task_type 'FDTD'.
20:30:01 -03 Created task '{'period': 0.34}' with task_id 
             'fdve-f7685166-e804-4dfa-9e69-f5f69acffc88' and task_type 'FDTD'.
20:30:03 -03 Created task '{'period': 0.35}' with task_id 
             'fdve-d575b9e7-55d3-4458-a3ed-8e06affa8b9f' and task_type 'FDTD'.
20:30:04 -03 Created task '{'period': 0.36}' with task_id 
             'fdve-9bd4073d-9a71-4a67-be4d-755b5aa303b6' and task_type 'FDTD'.
20:30:06 -03 Created task '{'period': 0.37}' with task_id 
             'fdve-b8876ba3-598a-4ca9-a7b6-59e134e16f51' and task_type 'FDTD'.
20:30:07 -03 Created task '{'period': 0.38}' with task_id 
             'fdve-b8880447-2054-4996-8bc0-177b61aaaf73' and task_type 'FDTD'.
20:30:09 -03 Created task '{'period': 0.39}' with task_id 
             'fdve-55144cae-1c65-4895-9942-bca2ba98ae5c' and task_type 'FDTD'.
20:30:10 -03 Created task '{'period': 0.4}' with task_id 
             'fdve-061bc94e-2ef2-405b-8a48-231e3e728d55' and task_type 'FDTD'.
20:30:18 -03 Started working on Batch.
20:31:52 -03 Maximum FlexCredit cost: 0.275 for the whole batch.
             Use 'Batch.real_cost()' to get the billed FlexCredit cost after the
             Batch has completed.
20:32:02 -03 Batch complete.
20:32:07 -03 loading simulation from
             ./fdve-9b2a528d-fe89-46d1-add3-2908cafda49b.hdf5
20:32:08 -03 loading simulation from
             ./fdve-7d435325-28b0-4f2e-85bd-284cb1608d1e.hdf5
20:32:10 -03 loading simulation from
             ./fdve-20fc4a7a-2a2b-4ea8-8353-9854d5fccbfc.hdf5
20:32:12 -03 loading simulation from
             ./fdve-83f30131-b607-4fb4-b034-c8d9e367b275.hdf5
20:32:14 -03 loading simulation from
             ./fdve-f7685166-e804-4dfa-9e69-f5f69acffc88.hdf5
20:32:16 -03 loading simulation from
             ./fdve-d575b9e7-55d3-4458-a3ed-8e06affa8b9f.hdf5
20:32:18 -03 loading simulation from
             ./fdve-9bd4073d-9a71-4a67-be4d-755b5aa303b6.hdf5
20:32:20 -03 loading simulation from
             ./fdve-b8876ba3-598a-4ca9-a7b6-59e134e16f51.hdf5
20:32:22 -03 loading simulation from
             ./fdve-b8880447-2054-4996-8bc0-177b61aaaf73.hdf5
20:32:24 -03 loading simulation from
             ./fdve-55144cae-1c65-4895-9942-bca2ba98ae5c.hdf5
20:32:26 -03 loading simulation from
             ./fdve-061bc94e-2ef2-405b-8a48-231e3e728d55.hdf5

Design Results#

To analyze the design results we will create a dataframe using the to_dataframe() method. This dataframe shows the sweep parameter period as well as the post-processing function results. As one can see, we can return single values like Peak Reflectance and Peak Wavelength, or higher dimensional datasets such as FluxMonitor dataset included in the Reflectance column.

[11]:
df = results.to_dataframe()
df
[11]:
period Reflectance Peak Reflectance Peak Wavelength
0 0.30 type='FluxData' monitor=FluxMonitor(type='Flux... 1.013392 0.444
1 0.31 type='FluxData' monitor=FluxMonitor(type='Flux... 1.012020 0.464
2 0.32 type='FluxData' monitor=FluxMonitor(type='Flux... 0.999940 0.482
3 0.33 type='FluxData' monitor=FluxMonitor(type='Flux... 0.996101 0.502
4 0.34 type='FluxData' monitor=FluxMonitor(type='Flux... 0.995063 0.520
5 0.35 type='FluxData' monitor=FluxMonitor(type='Flux... 0.997233 0.538
6 0.36 type='FluxData' monitor=FluxMonitor(type='Flux... 0.996590 0.556
7 0.37 type='FluxData' monitor=FluxMonitor(type='Flux... 0.998571 0.572
8 0.38 type='FluxData' monitor=FluxMonitor(type='Flux... 0.992530 0.588
9 0.39 type='FluxData' monitor=FluxMonitor(type='Flux... 0.990036 0.606
10 0.40 type='FluxData' monitor=FluxMonitor(type='Flux... 0.989468 0.622

We can use the built-in dataframe functions to plot and analyze information. For instance, letโ€™s plot the peak reflectance values and wavelengths to observe how these values change with respect to device period.

[12]:
f, (ax1, ax2) = plt.subplots(1, 2, figsize=(10, 4), tight_layout=True)
df.plot(
    x="period",
    y="Peak Reflectance",
    kind="scatter",
    ax=ax1,
    title="Peak Reflectance X Period",
    xlabel="Period (um)",
    ylabel="R",
    ylim=[0, 1.2],
    color="Red",
)
df.plot(
    x="period",
    y="Peak Wavelength",
    kind="scatter",
    ax=ax2,
    title="Peak Wavelength X Period",
    xlabel="Period (um)",
    ylabel="Wavelength (um)",
    color="Blue",
)
plt.show()
../_images/notebooks_AllDielectricStructuralColor_24_0.png

As one can see, the reflectance peak is very robust with respect to device period. On the other hand, an almost linear relationship is shown between the reflectance peak and the device period. These results are in agreement to the ones in the reference paper.

Now, we will create another dataframe to plot the reflectance spectrum versus wavelength for the different device periods. Here, we will unpack the FluxMonitor dataset and access the flux values and frequencies.

[13]:
df_wl = pd.DataFrame({"Wavelength": td.C_0 / df.loc[0, "Reflectance"].flux.f.values})
df_R = pd.DataFrame(
    {df.loc[i, "period"]: df.loc[i, "Reflectance"].flux.values for i in df.index}
)
df_R_wl = pd.concat([df_wl, df_R], axis=1)
df_R_wl.set_index("Wavelength", inplace=True)
df_R_wl.head()
[13]:
0.3 0.31 0.32 0.33 0.34 0.35 0.36 0.37 0.38 0.39 0.4
Wavelength
0.400 0.027893 0.012195 0.006991 0.004796 0.002578 0.002097 0.002713 0.004102 0.006654 0.107062 0.011689
0.402 0.029418 0.012601 0.007088 0.005307 0.002887 0.002009 0.002290 0.003307 0.005032 0.334151 0.007038
0.404 0.031144 0.012834 0.007161 0.005754 0.003348 0.002062 0.002017 0.002701 0.003981 0.207237 0.009618
0.406 0.033140 0.013216 0.007265 0.006089 0.003873 0.002279 0.001882 0.002263 0.003112 0.048685 0.013553
0.408 0.035495 0.013779 0.007109 0.006282 0.004485 0.002585 0.001875 0.001946 0.002427 0.016134 0.021842
[22]:
ax = df_R_wl.plot(
    title="Reflectance Spectrum for Different Device Period",
    xlabel="Wavelength (um)",
    ylabel="R",
)
ax.legend([f"period = {p:.2f} $\\mu m$" for p in df_R_wl.columns],
          bbox_to_anchor=(1.02, 1.02),
          loc='upper left')
plt.show()
../_images/notebooks_AllDielectricStructuralColor_27_0.png