Geometric-phase metalens: focal distance and spot size#

This notebook reconstructs the official Tidy3D metalens example with a compact 7×7 array of rotated TiO₂ pillars. It builds and voxelizes the actual FDTDX scene, launches a circularly polarized pulse, records an x-z field phasor, and extracts focal distance, transverse FWHM, and focal-core contrast.

Imports and parameters#

import os
os.environ.setdefault("XLA_PYTHON_CLIENT_PREALLOCATE", "false")
from time import perf_counter
import matplotlib.pyplot as plt
from matplotlib.patches import Polygon
import numpy as np
import pandas as pd
import jax
import fdtdx
from IPython import get_ipython

from benchmarks.cases.device_metalens.run import (
    DL, DOMAIN, LENS_TOP_Z, PML_LAYERS, RUN_TIME, UM, WAVELENGTH,
    _build, _centers, _fwhm,
)
from benchmarks.device_geometry import metalens_pillars_um

get_ipython().run_line_magic("matplotlib", "inline")
polygons_um, centers_um, focal_length_design_um = metalens_pillars_um()
pd.Series({
    "wavelength_um": WAVELENGTH / UM,
    "grid_pitch_um": DL / UM,
    "domain_um": tuple(value / UM for value in DOMAIN),
    "array": "7 × 7 pillars",
    "pillar_height_um": 0.600,
    "design_focal_length_um": focal_length_design_um,
    "maximum_time_ps": RUN_TIME * 1e12,
    "PML_cells": PML_LAYERS,
}, name="value").to_frame()
value
wavelength_um 0.6
grid_pitch_um 0.025
domain_um (3.625, 3.625, 6.35)
array 7 × 7 pillars
pillar_height_um 0.6
design_focal_length_um 2.606736
maximum_time_ps 0.3
PML_cells 12

1. Set and plot the pillar geometry#

fig, ax = plt.subplots(figsize=(6.5, 6.5))
for vertices, center in zip(polygons_um, centers_um, strict=True):
    ax.add_patch(Polygon(vertices + center, facecolor="#55c2b5", edgecolor="#087d83", lw=0.6))
ax.set(xlabel="x (µm)", ylabel="y (µm)", title="Exact rotated-pillar array", aspect="equal")
limit = 1.75
ax.set(xlim=(-limit, limit), ylim=(-limit, limit))
plt.show()
../../_images/7ae0d0ad7b49d5350d2361989bbe0e608bcd8f788cc287a6fbe30d58dfbaf41f.png

2. Build and voxelize the FDTDX scene#

object_list, constraints, config, focal_length_design_um = _build()
key = jax.random.PRNGKey(0)
objects, arrays, design_params, config, _ = fdtdx.place_objects(
    object_list, config, constraints, key
)
arrays, objects, _ = fdtdx.apply_params(arrays, objects, design_params, key)
print(f"Yee grid: {objects.volume.grid_shape}")
print(f"Time steps: {config.time_steps_total:,}")
Yee grid: (145, 145, 254)
Time steps: 6,294
fig, axes = plt.subplots(1, 2, figsize=(11, 4.4))
fdtdx.plot_material_from_side(config, arrays, "z", position=-1.875 * UM, ax=axes[0], plot_legend=True)
fdtdx.plot_material_from_side(config, arrays, "y", position=0.0, ax=axes[1], plot_legend=True)
axes[0].set_title("Voxelized pillar plane")
axes[1].set_title("Voxelized vertical section")
plt.tight_layout(); plt.show()
../../_images/3a9ac17ae2b0e887d60dbe9def4d9c82653630ad8f9a3ee21b964b420eb14375.png

3. Run the circularly polarized simulation#

started = perf_counter()
_, result_arrays = fdtdx.run_fdtd(arrays=arrays, objects=objects, config=config, key=key, show_progress=False)
print(f"Local FDTDX runtime: {perf_counter() - started:.1f} s")
Local FDTDX runtime: 13.4 s

4. Plot the simulated focal field#

detector = objects["xz"]
phasor = np.asarray(result_arrays.detector_states["xz"]["phasor"])[0, 0]
intensity = np.sum(np.abs(phasor) ** 2, axis=0).squeeze()
x_um = _centers(config, 0, detector.grid_slice_tuple[0]) / UM
z_from_lens_um = (_centers(config, 2, detector.grid_slice_tuple[2]) - LENS_TOP_Z) / UM

fig, ax = plt.subplots(figsize=(9, 5.5))
image = ax.pcolormesh(x_um, z_from_lens_um, intensity.T / intensity.max(), cmap="magma", shading="auto")
ax.axhline(focal_length_design_um, color="cyan", ls="--", label="design focus")
ax.set(xlabel="x (µm)", ylabel="distance from lens (µm)", title="Executed x-z intensity")
ax.legend(); fig.colorbar(image, ax=ax, label="normalized |E|²"); plt.show()
../../_images/d8b3c75b66da6d6192cffc62397aae1121e76542d532f3342d68b950c834fa11.png

5. Extract focus, FWHM, and contrast#

axis_intensity = intensity[np.argmin(np.abs(x_um)), :]
search = np.flatnonzero((z_from_lens_um > 1.0) & (z_from_lens_um < 4.0))
focus_index = int(search[np.argmax(axis_intensity[search])])
focal_distance_um = float(z_from_lens_um[focus_index])
fwhm_um = _fwhm(x_um, intensity[:, focus_index])
core = np.abs(x_um) <= 0.5
contrast = float(axis_intensity[focus_index] / np.mean(intensity[core, focus_index]))

fig, ax = plt.subplots(figsize=(8, 4.2))
profile = intensity[:, focus_index] / intensity[:, focus_index].max()
ax.plot(x_um, profile, color="#087d83")
ax.axhline(0.5, color="#8221a8", ls="--")
ax.set(xlabel="x (µm)", ylabel="normalized intensity", title=f"Focal plane · FWHM = {fwhm_um:.3f} µm")
ax.grid(alpha=0.25); plt.show()

golden = np.load("benchmarks/goldens/device_metalens.npz", allow_pickle=True)
pd.DataFrame({
    "FDTDX": [focal_distance_um, fwhm_um, contrast],
    "Tidy3D": [float(golden["focal_distance_um"][0]), float(golden["fwhm_um"][0]), float(golden["focal_core_contrast"][0])],
}, index=["focal distance (µm)", "FWHM (µm)", "focal-core contrast"])
../../_images/2c6ab26b00603d7b27f882314c6a9f42d8bec1c300e62f251ecc531a6bf0ade7.png
FDTDX Tidy3D
focal distance (µm) 1.962500 1.850000
FWHM (µm) 0.502048 0.497904
focal-core contrast 1.924352 1.876280

Reproduce#

For a production lens, increase the aperture and perform a grid/aperture convergence study.

print("uv run fdtdx-bench run --case device_metalens")
uv run fdtdx-bench run --case device_metalens