Core Concepts¶
The cast of characters. Every Ikarus simulation is the same short play: a stage (the unit cell), a stack of layers wearing materials, one light source, and a result that tells you how the performance went. This page introduces each character and how they fit together — with links to their full API pages.
flowchart LR
subgraph You compose
M["Materials"] --> L["Layers"]
G["Geometry<br/>periods · resolution · n_orders"] --> R["RCWA"]
L --> R
S["Source"] --> R
end
R -->|simulate| Res["SimulationResult"]
Res --> F["Field maps"]
Res --> IO["HDF5"]
The stage — geometry¶
Everything happens on one unit cell with periods period_x, period_y
(meters), repeated to infinity in both directions. Two numbers govern the
numerics, and confusing them is the classic rookie mistake:
| Dial | What it controls | Mental model |
|---|---|---|
n_orders |
How many Fourier harmonics describe the field | the accuracy/cost dial — turn with intention |
resolution |
How many pixels sample the geometry | just needs to draw your shape — auto-raised to ≥ 4*M+1 |
For a 1-D grating (pattern varies only along x), store the topology as an
(Nx, 2) map and set n_orders=(M, 0) — you'll get the physics of a 1-D
problem at a 1-D price.
The wardrobe — materials¶
A Material maps wavelength to a complex
permittivity \(\varepsilon = (n+ik)^2\). Anywhere the API wants a material, you
can hand it:
| You pass | Example | You get |
|---|---|---|
| A database name | "Si" |
dispersive tabulated data, spline-interpolated |
| A bare number | 1.5, 2.4 + 0.01j |
constant index — instant test material |
| A JSON path | "my_mat.json" |
loaded on demand |
A Material object |
Material.constant(2.0) |
used as-is |
The MaterialLibrary resolves and
caches all of these; a shared default_library ships with Air, Au, GaN, GaP,
Si, Si₃N₄, SiO₂, TiO₂, aSi. Adding your own from CSV or a Lorentz model takes
one call — see Aerobatics → Custom materials.
The sign that bites
Ikarus speaks \(\exp(-i\omega t)\): absorbers have \(k > 0\). Import data with the opposite sign and your structure becomes a laser — the energy balance exceeding 1 is the tell.
The stack — layers¶
A Layer is a thickness plus a recipe for
\(\varepsilon(x, y)\). Two flavors:
rcwa.add_uniform_layer(np.inf, "Air") # uniform: one material
rcwa.add_layer(200e-9, topology, ["Air", "Si"]) # patterned: pixel map
The patterned topology is an integer array; index 0 wears materials[0],
index 1 wears materials[1], and so on.
The stack rules (Ikarus validates them and refuses bad stacks):
- Layers are listed cover → … → substrate (sky to ground).
- First and last layer: uniform + semi-infinite (
height=np.inf). The cover's index sets the incident wavevector. - Interior layers: finite thickness, any number, any mix of flavors.
The scenery — topologies and shapes¶
You can draw a topology by hand (it's just a NumPy integer array) or use the
shapes primitives, which work in fractional unit-cell
coordinates \([0, 1)\) so your design is resolution-independent:
from ikarus import shapes
disk = shapes.circle(center=(0.5, 0.5), radius=0.3, grid_shape=(128, 128))
ring = shapes.ring(inner_radius=0.2, outer_radius=0.35, grid_shape=(128, 128))
both = shapes.combine(disk, ring) # overlay several maps
The layer resamples its topology (nearest-neighbour) onto the solver's grid, so
topology resolution and resolution don't need to match.
The light — sources¶
A Source is one monochromatic plane wave: vacuum
wavelength, direction (theta from \(+z\), phi from \(+x\), degrees), and
polarization (linear + linear_pol_angle, or RCP/LCP).
The killer convenience: set_source(**kwargs) remembers. After the first
call it updates only what you pass — so sweeps mutate one parameter at a time:
rcwa.set_source(wavelength=600e-9, theta=0, polarization="linear")
rcwa.set_source(theta=15) # wavelength & polarization retained
The physics you get for free — boundary conditions¶
Nothing to configure:
- Sideways: Bloch periodicity over the unit cell — harmonic \((m,n)\) carries wavevector \(k_{x0} - m\,2\pi/\Lambda_x\), etc.
- Up/down: radiation conditions in the semi-infinite cover and substrate — only outgoing or decaying modes survive, enforced by the solver's forward-branch eigenvalue selection.
The engine room — solvers¶
The numerically heavy lifting lives in the stateless
ikarus.core.solver (solve_stack): geometry and source in, modal solution
out, no hidden state. The RCWA object you actually touch is a thin director —
it validates the stack, calls the engine, and packages the output. You'll
likely never call the solver directly, but it's public and documented in the
Low-level API if you want to lift the hood.
Harmonic convergence can be automated:
simulate(auto_converge="once") finds and caches a sufficient n_orders
(Tools).
The flight report — results¶
simulate() returns (T, R, result):
T,R— zero-order complex amplitudes (or{"co", "cross"}dicts for circular light).result— aSimulationResult: totals, per-order efficiencies with(p, q)labels, exit angles (NaN= evanescent),energy_balance, zero-order phases, and the rawsolutionfor field reconstruction.
From there, get_fields(...) turns the modal solution into real-space
FieldMaps, and
save_results writes everything to self-describing HDF5.
Characters introduced. See them act: Flight School →