# Pixel and spectral design bases

FDTDX separates the **optimization variables** from the material density seen
by Maxwell's equations. One forward and one adjoint solve still produce the
complete gradient, whether the variables are individual pixels or coefficients
of correlated spatial waves.

| Basis | Parameters | Useful bias |
|---|---|---|
| `PixelBasis2D` | one value per cell | unrestricted antennas and final local polishing |
| `CosineBasis2D` | selected DCT-II cosine amplitudes | mirror-reduced domains and photonic-crystal periods |
| `FourierBasis2D` | bias plus cosine/sine amplitudes | general plane waves with learned phase |
| `FFTGridBasis2D` | every nonredundant discrete Fourier amplitude | full pixel expressivity in spectral coordinates |
| `RadialCosineBasis2D` | circular/elliptical cosine and sine amplitudes | bullseyes and curved Bragg mirrors |
| `LevelSetBoundary2D` | signed displacement of an existing interface | binary, topology-preserving shape polishing |

Every basis implements the same small interface:

```python
basis.parameter_shape       # shape optimized by Adam/SGD/L-BFGS
basis.parameter_bounds      # (0, 1) for pixels; unbounded for coefficients
parameters = basis.encode(existing_density)
density = basis.decode(parameters)
```

```{figure} ../_static/generated/spectral_design_parameterizations.png
:alt: The same periodic target optimized with pixel, DCT cosine, Fourier, and elliptical radial bases

One fabrication-aware objective optimized with four interchangeable bases.
The spectral bases reproduce correlated periodic patterns with far fewer
variables; the radial basis deliberately searches a different family of
curved fronts.
```

## Switching bases

The only line that changes between pixel and spectral topology optimization is
the basis constructor:

```python
import fdtdx

shape = (180, 96)
spacing_um = 0.033333

pixel_basis = fdtdx.PixelBasis2D(shape)

cosine_basis = fdtdx.CosineBasis2D(
    shape,
    mode_shape=(72, 48),
    spacing=(spacing_um, spacing_um),
    minimum_period=0.20,
    maximum_period=0.90,
)

fourier_basis = fdtdx.FourierBasis2D(
    shape,
    max_mode_indices=(36, 24),
    spacing=(spacing_um, spacing_um),
    minimum_period=0.20,
    maximum_period=0.90,
)

dense_fourier_basis = fdtdx.FFTGridBasis2D(
    shape,
    spacing=(spacing_um, spacing_um),
)
```

`CosineBasis2D` is especially convenient when the stored design is an x/y
symmetry-reduced quadrant. Its waves have zero normal derivative at the design
box edges. `FourierBasis2D` supplies cosine and sine amplitudes for every unique
reciprocal vector, so phase and asymmetry remain free.

`FFTGridBasis2D` is the scalable choice when the desired Fourier basis is as
large as the material grid. It packs the Hermitian spectrum into exactly one
real variable per spatial cell and decodes it with `ifft2`. For an 183 x 75
design this means 13,725 independent real coefficients (6,863 reciprocal
waves), not a stored 13,725 x 13,725 basis matrix. The transform costs
O(N log N), preserves every representable spatial degree of freedom, and
rejects mode limits beyond the grid's Nyquist range rather than silently
creating aliased duplicate waves.

Physical period limits select a spectral annulus. Omitting them retains every
mode inside the requested rectangular mode range, including long-wavelength
components that can create smooth antenna-like structures.

## Fabrication transform and hard regions

All bases use one implementation of filtering, projection, immutable geometry,
and exact binarization:

```python
parameters = cosine_basis.encode(seed_density)

def objective(parameters, beta, fraction):
    del fraction
    density_2d = fdtdx.fabrication_density_2d(
        parameters,
        cosine_basis,
        beta=beta,
        eta=0.50,
        filter_radius=0.10,
        filter_spacing=spacing_um,
        trainable_mask=trainable,
        fixed_density=fixed,
        exact_binary=True,
    )

    z_fill = fdtdx.slab_fill_fractions(
        simulation_z_edges_um,
        thickness=0.20,
    )
    density_3d = fdtdx.extrude_density_2d(density_2d, z_fill)
    arrays = put_density_in_material_array(arrays_template, density_3d)
    output = run_differentiable_fdtd(arrays)
    return measured_objective(output), {"density": density_2d}

result = fdtdx.optimize_topology(
    objective,
    parameters,
    fdtdx.TopologyOptimizerConfig(
        iterations=200,
        learning_rate=0.02,
        lower_bound=cosine_basis.parameter_bounds[0],
        upper_bound=cosine_basis.parameter_bounds[1],
    ),
)
```

Set `trainable_mask=0, fixed_density=0` for hard air and
`trainable_mask=0, fixed_density=1` for immutable dielectric. Constraints are
reimposed after filtering, so material cannot bleed into an atom-clearance
region. With `exact_binary=True`, the forward solve sees only the two endpoint
materials while the backward pass uses the smooth projection derivative.
Running the same parameters at several projection thresholds supplies robust
erosion/dilation variants.

## Curved and localized waves

Circular or facing curved mirrors can use a radial/elliptical bank:

```python
radial_basis = fdtdx.RadialCosineBasis2D(
    shape,
    spacing=(spacing_um, spacing_um),
    periods=(0.24, 0.30, 0.36, 0.44, 0.54, 0.68),
    axis_scale=(1.0, 1.5),
    window_sigma=(2.0, 1.2),
)
```

`axis_scale` turns circular fronts into ellipses. `window_sigma` provides a
Gaussian envelope, allowing a periodic mirror field to taper into a defect
rather than forcing one infinite uniform crystal. Sine amplitudes are enabled
by default, so each radial period can learn its phase.

## What the basis does—and does not—guarantee

A spectral basis correlates geometry updates and can restrict their length
scales. It does not change the optimization objective. A pure beta objective
can still prefer emission suppression over a resonant cavity; LDOS and pole
diagnostics remain necessary when resonance formation matters. Conversely,
using too few modes can exclude the best device. A practical workflow is
spectral discovery followed by pixel or boundary polishing.

A complete FFT grid is not a low-dimensional regularizer: it has exactly the
same number of independent variables as the pixel grid. Its benefit is the
coordinate system—each derivative perturbs a correlated periodic wave. Use a
truncated `FourierBasis2D` or `CosineBasis2D` when an actual band limit or
parameter-count reduction is desired.

The runnable
`solver/examples/spectral_design_parameterizations.py` example applies the
same fabrication-aware objective to all four bases.

## Boundary-normal optimization

The spectral and pixel bases are appropriate while the topology is still being
discovered. Once a grey design has useful connected components and holes,
`LevelSetBoundary2D` freezes that topology and changes the question asked by
the adjoint solve. A parameter no longer means “make this cell more SiN.” It
means “move this nearby material boundary outward or inward.”

```python
boundary = fdtdx.LevelSetBoundary2D.from_density(
    grey_density,
    spacing=(dx_um, dy_um),
    threshold=0.5,
    narrow_band_width=4 * max(dx_um, dy_um),
    maximum_displacement=2 * max(dx_um, dy_um),
    supersample=6,
    # min_x and min_y are mirror planes, not open exterior boundaries.
    exterior_edges=("max_x", "max_y"),
)
displacement = boundary.initial_parameters()

def objective(candidate):
    density = boundary.decode(candidate)
    output = run_differentiable_fdtd(density)
    pole = fit_tracked_pole(output)
    peak = cavity_peak_ldos(pole, output)
    fixed_ldos = fdtdx.fixed_frequency_cavity_ldos(
        peak_resonant_ldos=peak,
        quality_factor=pole.quality_factor,
        pole_frequency=pole.frequency,
        emitter_frequency=atomic_frequency,  # immutable throughout the run
        background_ldos=1.0,
    )
    return jnp.log(fixed_ldos)

value, normal_gradient = jax.value_and_grad(objective)(displacement)
proposal, topology_scale = fdtdx.topology_safe_boundary_step(
    boundary,
    displacement,
    normal_gradient,
    step_size=0.5 * min(dx_um, dy_um),
)
```

The reference is a positive-in-material signed-distance field. Adding a
positive scalar at an interface advances material along its outward normal;
adding a negative scalar retreats it. A compact cosine window makes the
derivative exactly zero away from existing interfaces, so a remote island or
hole cannot nucleate from a bulk pixel. `topology_safe_boundary_step` rejects
steps that merge, split, create, or erase components. It checks one-to-one
lineage as well as component counts, so deleting one hole while creating a
replacement elsewhere is also rejected. After several accepted small steps,
`boundary.rebase(...)` refits a signed-distance field and resets the
displacement to zero without changing the shape.

The density returned at an intersected Yee cell is a supersampled geometric
area fraction. It is not a freely optimizable grey material: every subcell in
the forward raster is one of the two endpoint materials. This retains subpixel
boundary motion without the dishonest final act of projecting an arbitrary
grey image to binary.

There is one important limitation. Fixing topology is a deliberate search-space
restriction, not a proof that the chosen topology is globally optimal. The
recommended sequence is therefore grey pixel/spectral discovery, one explicit
topology-selection threshold, and then boundary optimization. If a better
device genuinely needs a new hole, return to the discovery stage instead of
letting a boundary step create it accidentally.
