Lesson 4 · Sweeping Gracefully¶
Mission: sweep parameters without wasting a single eigensolve (or writing a single for-loop you don't have to), perform the convergence ritual every trustworthy result rests on, and paint a 2-D design map.
The easy way: Sweep¶
For source sweeps — wavelength, angle, polarization — skip the loop entirely.
Sweep varies the source over a grid and hands back arrays,
with one progress bar for the whole run (so you can see the ETA):
import numpy as np
from ikarus import RCWA, shapes, Sweep
period, N = 450e-9, 96
disk = shapes.circle(radius=0.3, grid_shape=(N, N))
rcwa = RCWA(period_x=period, period_y=period, resolution=(N, N), n_orders=(9, 9))
rcwa.add_uniform_layer(np.inf, "Air")
rcwa.add_layer(200e-9, disk, ["Air", "Si3N4"])
rcwa.add_uniform_layer(np.inf, "SiO2")
rcwa.set_source(wavelength=600e-9, theta=0, polarization="linear")
res = Sweep(rcwa).over(wavelength=np.linspace(400e-9, 800e-9, 81)).run()
R = res.R_total # array aligned to the sweep axis
A 2-D grid is the same call with two axes — and still one bar:
res = Sweep(rcwa).over(theta=np.linspace(0, 60, 31),
wavelength=np.linspace(400e-9, 800e-9, 81)).run()
res.R_total.shape # (31, 81)
res.order(1, 0, which="R") # +1 reflected order across the grid
The manual pattern (and your toggle)¶
When you need full control — or you're sweeping geometry (which rebuilds the
structure) — write the loop, and wrap it in progress
for one bar with an on/off switch:
from ikarus import progress
wavelengths = np.linspace(400e-9, 800e-9, 81)
R = np.empty_like(wavelengths)
for i, wl in enumerate(progress(wavelengths, desc="λ", enable=True)):
rcwa.set_source(wavelength=wl) # set_source remembers theta & polarization
R[i] = rcwa.simulate()[2].R_total
Changing the source is cheap. Changing the geometry triggers a fresh eigensolve — unavoidable, that's the physics changing.
The convergence ritual¶
Before you trust a sweep — let alone publish it — confirm the harmonic count is sufficient. The honest way is to watch a metric stop moving:
from ikarus.tools.convergence import convergence_curve
rcwa.set_source(wavelength=600e-9)
orders, defect = convergence_curve(rcwa, range(4, 21, 2), metric="energy")
harmonics = (2 * orders + 1) ** 2
for h, d in zip(harmonics, defect):
print(f"{h:5d} harmonics: |R+T-1| = {d:.2e}")
(convergence_curve politely restores your original n_orders afterward.)
Or delegate the whole ritual:
rcwa.simulate(auto_converge="once", verbose=True) # finds & caches n_orders
print("using n_orders =", rcwa.n_orders)
Converge at your worst point, not your favorite one
TM polarization, the highest contrast, the shortest wavelength, the steepest resonance — that's where convergence is slowest. A study run at a benign wavelength is a false sense of security with extra steps.
A 2-D design map¶
Reflectance vs. wavelength and pillar height — the kind of plot that finds
designs for you. Height is structural (it rebuilds the layer), so it's the
outer loop with a progress bar; wavelength is a
source axis, so the inner sweep is a Sweep:
import numpy as np
from ikarus import RCWA, shapes, Sweep, progress
period, N = 450e-9, 96
disk = shapes.circle(radius=0.3, grid_shape=(N, N))
wavelengths = np.linspace(400e-9, 800e-9, 60)
heights = np.linspace(100e-9, 400e-9, 40)
Rmap = np.empty((heights.size, wavelengths.size))
for j, h in enumerate(progress(heights, desc="height")):
rcwa = RCWA(period_x=period, period_y=period, resolution=(N, N), n_orders=(9, 9))
rcwa.add_uniform_layer(np.inf, "Air")
rcwa.add_layer(h, disk, ["Air", "Si3N4"])
rcwa.add_uniform_layer(np.inf, "SiO2")
rcwa.set_source(wavelength=600e-9, theta=0, polarization="linear")
Rmap[j] = Sweep(rcwa).over(wavelength=wavelengths).run(progress=False).R_total
import matplotlib.pyplot as plt
plt.pcolormesh(wavelengths * 1e9, heights * 1e9, Rmap, shading="auto", cmap="inferno")
plt.xlabel("wavelength (nm)"); plt.ylabel("pillar height (nm)")
plt.colorbar(label="Reflectance"); plt.savefig("Rmap.png", dpi=150)
Resonance bands glow; anti-reflection valleys go dark. Design by sightseeing.
Need it faster?¶
- Pin BLAS to one thread before importing NumPy — for these small matrices, threaded BLAS is a traffic jam, not a speedup (the full story).
- Fan out across processes — every solve is independent; Aerobatics → Batch simulations has the recipe.
Expected results¶
- The energy defect shrinks monotonically with
n_orders; once below your tolerance (say 10⁻⁴), extra harmonics buy nothing but heat. - The 2-D map shows resonance bands (bright
R) and broadband AR valleys (dark) — the design space at a glance.