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, 13),
wavelength=np.linspace(400e-9, 800e-9, 41)).run()
res.R_total.shape # (13, 41)
res.order(1, 0, which="R") # +1 reflected order across the grid
Mind the solve count
A 2-D sweep is n_theta × n_wavelength full solves — there is no
eigenmode caching, so each grid point costs a complete solve (~3 s here at
n_orders=(9,9); see Need for Speed). The 13×41 grid
above is ~530 solves (minutes); a 31×81 grid is ~2,500 (a couple of hours).
Start coarse, and drop n_orders/resolution while you explore.
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
Reuse one RCWA and change only the source between solves — it keeps the
geometry fixed and is the correct, convenient pattern. Note it is not a speed
win: there is no eigenmode caching yet, so every set_source re-solve costs
a full solve (wavelength, angle and polarization alike). Budget one solve per
sweep point — see the cost table in Need for Speed.
The convergence ritual¶
Before you trust a sweep — let alone publish it — confirm the harmonic count is sufficient. The honest way is to watch the quantity you actually care about stop moving. For most designs that's the zeroth-order reflectance and its phase — not the energy balance:
from ikarus.tools.convergence import convergence_curve
rcwa.set_source(wavelength=600e-9)
orders, phase = convergence_curve(rcwa, range(4, 21, 2), metric="R_phase")
for M, p in zip(orders, phase):
print(f"n_orders={M:3d}: reflected phase = {p:+.2f} deg")
(convergence_curve politely restores your original n_orders afterward; pass
metric="R" for reflectance, "R_phase"/"T_phase" for phase.) Or delegate the
whole ritual — auto_converge raises the order count until the complex R/T
coefficients (magnitude and phase) settle:
rcwa.simulate(auto_converge="once", verbose=True) # finds & caches n_orders
print("using n_orders =", rcwa.n_orders)
# one-off solve? ask Ikarus to warn you if it's under-resolved:
T, R, res = rcwa.simulate(check_convergence=True)
R + T ≈ 1 is not convergence
A lossless structure conserves energy at every n_orders, even while its
reflectance and phase are still drifting. Converging on |R+T−1| will happily
declare victory on a wrong answer — this has cost real multi-hour optimization
runs. Converge on R and phase.
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))
# A design map is heights × wavelengths FULL solves. This 40×24 ≈ 960 at
# n_orders=(7,7) (~0.4 s each) runs in minutes; the paper-quality 60×40 at
# (9,9) is ~2 h. Coarsen while exploring, refine once.
wavelengths = np.linspace(400e-9, 800e-9, 40)
heights = np.linspace(100e-9, 400e-9, 24)
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=(7, 7))
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 reflectance and its phase stop moving with
n_orders; once the change per step is below your tolerance (say a few × 10⁻³ in the coefficient, ~0.1° of phase), extra harmonics buy nothing but heat. (Energy balances long before that, so don't use it as the gauge.) - The 2-D map shows resonance bands (bright
R) and broadband AR valleys (dark) — the design space at a glance.