Skip to content

Lesson 7 · Designing the Shape Itself

Mission: stop choosing parameters by hand. State a goal — "maximum transmission at 1300 nm" — and let a genetic algorithm evolve a meta-atom's own shape parameters: arm lengths, widths, rotation, height.

Every previous lesson was forward design: you set the geometry, Ikarus told you the optics. This one runs the arrow backwards.

Two ways to hold a degree of freedom

Ikarus's inverse module (ikarus.inverse) optimizes two kinds of topology DOF:

DOF What it is Best for
Parametric shape a shape class (Cross, SplitRing, …) with named, bounded parameters physically interpretable meta-atoms with a few meaningful knobs
Pixel map a free binary grid (pixels(nx, ny, symmetry=...)) freeform / topology optimization with no shape prior

This lesson is about the first — the one your intuition can read. (For the pixel route, see the broadband AR coating.)

Needs pymoo

pip install "ikarus-rcwa[inverse]".

Parametric shapes

A parametric Shape carries named parameters and a rotation angle. Used normally, it's just a tidy topology:

from ikarus.shapes import Cross
topo = Cross(arm_length=0.7, arm_width=0.2, angle=30).to_grid((128, 128))

The trick: any parameter may be a free(lo, hi) range instead of a number. Mark a few free, and they become the optimization variables — no pixel grids, no loss of physical meaning.

from ikarus.inverse import free
from ikarus.shapes import Cross

shape = Cross(arm_length=free(0.3, 0.95),   # free: the GA will choose
              arm_width=free(0.1, 0.45),    # free
              angle=free(0, 90))            # free: rotation is a knob too
shape.free_parameters()
# {'angle': (0.0, 90.0), 'arm_length': (0.3, 0.95), 'arm_width': (0.1, 0.45)}

The three-step recipe

Declare the meta-atom, state the target, optimize:

import os
os.environ.setdefault("OMP_NUM_THREADS", "1")   # single-thread BLAS for the GA loop

from ikarus.inverse import MetaAtom, free, optimize, Target
from ikarus.shapes import Cross

# 1. a Si cross on glass whose arms, rotation and height are all free
atom = MetaAtom(period=700e-9, cover="Air", substrate="SiO2")
atom.add_pattern(topology=Cross(arm_length=free(0.3, 0.95),
                                arm_width=free(0.1, 0.45),
                                angle=free(0, 90), grid_shape=(96, 96)),
                 materials=["Air", "Si"],
                 height=free(0.3e-6, 0.9e-6))

# 2. what we want: maximum transmission at 1300 nm
target = Target.maximize("T", at=1300e-9)

# 3. evolve it
best = optimize(atom, target, n_orders=6, pop=16, n_gen=10, seed=0)
print(best.report())

design = best.metaatom          # a ready-to-simulate RCWA

The optimizer enumerates the free shape parameters automatically — they appear in the report as shape__arm_length, shape__angle, and so on, alongside the free height:

Inverse-design result:
  objective = 0.016  (max(T))
    height = 7.89e-07
    shape__angle = 12.6
    shape__arm_length = 0.85
    shape__arm_width = 0.40
Evolved Si cross meta-atom and its spectrum
The genetic algorithm chose the arm dimensions, rotation and height of a Si cross to maximize transmission at 1300 nm (dotted line). Left: the evolved topology. Right: its full spectrum, computed by Ikarus.

Why parametric beats pixels (sometimes)

  • Fewer variables. A Cross has 4 knobs; a 16×16 pixel map has 256. The GA converges far faster on the smaller, smoother space.
  • Manufacturable by construction. The result is a clean cross with a definite arm width — not a speckle pattern that a fab process may not resolve.
  • Physically legible. You can read why it works (a longer arm red-shifts the resonance) and transfer the intuition.

Pixels win when you have no good shape prior and want the optimizer to invent topology you wouldn't have guessed. Many real workflows do both: parametric to get close, pixels to polish.

Bringing your own shape class

add_layer and the inverse module accept any object that exposes an img array (or a to_grid() method), so an external topology library drops straight in:

# any class with a binary `.img` numpy array works as a topology
rcwa.add_layer(200e-9, MyTopologySpecies(lx=0.4, ly=0.7), ["Air", "Si"])

To make a custom shape optimizable, subclass ikarus.shapes.Shape: declare its parameters in _PARAMS and implement _mask. It then inherits free_parameters, rotation and the inverse-design plumbing for free.

Expected results

  • A converging objective (the GA's f_min drops each generation), ending in a high-transmission cross with a specific rotation.
  • A spectrum with the target wavelength sitting in a transmission window — and, often, a sharp resonance nearby that the optimizer steered away from the target.

Pilot habits

  • Pin BLAS to one thread (why) — GA loops are many small solves.
  • Start with small pop/n_gen to gauge runtime and sanity, then scale up.
  • Bound parameters physically (an arm_width can't exceed the period) — tight, honest ranges make the search both faster and manufacturable.
  • Free the rotation: angle is a cheap extra knob that unlocks polarization-dependent and chiral responses (try SplitRing).

🎓 Flight School complete — for real this time. Where to fly next: Aerobatics · The Hangar · Need for Speed · API Reference