Grid, units and boundaries

SI units, the Yee grid, how the physical domain relates to PML cells, subpixel averaging, and how Maxwell picks the FDTD time step.

This page explains how Maxwell turns a domain and a grid specification into the discrete grid it steps, and which quantities you control. Read it before you choose cell sizes or boundary conditions for a new problem.

Units#

Maxwell uses SI units throughout:

QuantityUnit
Position, size, cell size, Domain.boundsmetre
Frequency, fwidthhertz
Time, dtsecond
Electric fieldV/m
Magnetic fieldA/m
Conductivity sigma_eS/m
Permittivity, permeabilityrelative (eps_r, mu_r), dimensionless
Bloch wavevectorrad/m

There is no length-unit setting. A photonics scene at 1.55 µm is written in metres (1.55e-6), exactly like an antenna at 1 GHz. The vacuum constants live in witwin.maxwell.constants (C_0, EPSILON_0, MU_0, ETA_0, CODATA 2018).

The Yee grid#

FDTD stores the six field components on a staggered (Yee) grid: electric components on cell edges, magnetic components on cell faces, each offset by half a cell from the others. Maxwell samples materials at each component's own location, so a structure edge that falls between grid lines gets a different value on Ex than on Ez. This is why result.materials returns per-component tensors.

The grid is described by a GridSpec:

ConstructorGrid
GridSpec.uniform(dl)Cubic cells of size dl
GridSpec.anisotropic(dx, dy, dz)Uniform per axis, different per axis
GridSpec.custom(x_coords, y_coords, z_coords)Explicit node coordinates per axis (nonuniform)
GridSpec.auto(min_steps_per_wavelength=10, wavelength=None, max_ratio=1.4, ...)Nonuniform grid that resolves the local wavelength in each material, grows by at most max_ratio between neighbour cells, and honours MeshOverrideStructure boxes and LayerRefinementSpec thin-layer refinement

A practical rule: use at least 10 to 20 cells per wavelength in the densest material, and more where fields vary quickly (metal edges, thin layers, resonators).

Domain and PML#

Domain.bounds is the physical region: the space where your structures, sources and monitors live. Absorbing PML layers are appended outside it. With Domain(bounds=((-0.64, 0.64),) * 3), a 0.025 m grid and BoundarySpec.pml(num_layers=8), the solver grid has 52 physical cells plus 8 PML cells on each side, 68 per axis. Monitors that span a whole plane, and result.plot.field, include those PML cells.

Keep sources and monitors a few cells away from the PML, and leave some space between scatterers and the PML so evanescent fields decay first.

BoundarySpec sets what happens at each face of the domain:

KindConstructorUse
pmlBoundarySpec.pml(num_layers=15, strength=1e6)Open space. The FDTD absorber defaults to CPML.
periodicBoundarySpec.periodic()Infinite periodic structures at normal incidence
blochBoundarySpec.bloch(wavevector)Periodic structures with a phase shift per period (oblique incidence, band structure)
pecBoundarySpec.pec()Perfect electric conductor wall
pmcBoundarySpec.pmc()Perfect magnetic conductor wall
murBoundarySpec.mur()First-order Mur absorbing boundary
noneBoundarySpec.none()No boundary treatment (the Scene default)

BoundarySpec.faces(default=..., x=..., z_low=..., ...) mixes kinds per axis or per face, for example Bloch on x and y with PML on z for a grating. Scene symmetry=(...) adds PEC or PMC mirror planes that halve the computed volume per symmetric axis. See Set up grids and boundaries.

Subpixel averaging#

When a material interface cuts through a cell, Maxwell can average the material over sub-samples instead of taking the value at one point. Set it with Scene(subpixel_samples=...):

  • an integer or a 3-tuple: the number of samples per axis inside each cell (1 means point sampling);
  • a SubpixelSpec(samples=..., averaging=..., pec=...): averaging="arithmetic" (volume fraction, the default) or "polarized" (harmonic mean normal to the interface, arithmetic mean along it), and pec="staircase" (the default) or "conformal" for partial-fill PEC edges.

Subpixel averaging is also what makes geometry differentiable: a small change in a box position changes the fill fraction of the cut cells continuously.

Time step and run length#

FDTD is stable only below the Courant limit. During preparation Maxwell computes a stability bound from the smallest cell spacings and the lowest high-frequency permittivity and permeability in the scene, adds a term for dispersive poles and full-tensor conductivity, and multiplies it by a safety factor of 0.9. It then takes the smallest of:

  • that safe bound;
  • 1/30 of the period of the highest characteristic source frequency;
  • 1/30 of the fastest material pole time scale.

result.stats()["time_step"] reports the bound, the chosen dt and which limit won ("material_cfl" or "frequency_resolution"). You can force a value with TimeConfig(dt=...); Maxwell rejects one above the safe bound.

The number of steps comes from TimeConfig:

  • TimeConfig.auto(steady_cycles=20, transient_cycles=15) (the default) runs long enough for the sources to settle and the fields to cross the domain, plus steady_cycles periods of the reference frequency;
  • TimeConfig(time_steps=N) runs exactly N steps.

Simulation.fdtd(shutoff=...) stops early when the field energy falls below that fraction of its peak, checked every shutoff_check_interval steps. It is off (0.0) by default.