Skip to content

Structures and media

Geometry primitives and the material models they carry. A Structure pairs a shape with a Medium; dispersion is expressed as Lorentz or Drude poles, or sampled directly as PermittivityArray.

Structures

Structure pydantic-model

Bases: FrozenModel

One geometry filled with one medium; list order is paint order (last wins). name is optional authoring/display metadata and never changes mesh sampling or paint-order semantics.

Fields:

Validators:

  • _medium_or_material → medium
  • _custom_medium_needs_box

geometry pydantic-field

geometry: GeometryType

medium pydantic-field

medium: Union[Medium, MaterialEntry]

name pydantic-field

name: Optional[StructureName] = None

Shapes

Box pydantic-model

Bases: FrozenModel

Axis-aligned box: full extents size_um centered on center_um.

Fields:

type pydantic-field

type: Literal['box'] = 'box'

center_um pydantic-field

center_um: Vec3Um

size_um pydantic-field

size_um: Tuple[PositiveUm, PositiveUm, PositiveUm]

Cylinder pydantic-model

Bases: FrozenModel

Annular sector / solid disk / ring. axis is the extrusion (= propagation) axis; the two transverse axes carry the radial test. inner_radius_um = 0 is a solid disk; a full 2*pi sweep is a ring/cylinder (no angular test). A 90-degree waveguide bend is an annulus with a 90-degree sweep. The curved sidewall is exact (faceting-free). Angles are in radians, measured by atan2(v, u) in the transverse (u, v) plane. Curved subpixel averaging is not supported.

Fields:

Validators:

  • _check

type pydantic-field

type: Literal['cylinder'] = 'cylinder'

axis pydantic-field

axis: AxisName

center_um pydantic-field

center_um: Vec3Um

radius_um pydantic-field

radius_um: PositiveUm

inner_radius_um pydantic-field

inner_radius_um: NonNegativeUm = 0.0

length_um pydantic-field

length_um: PositiveUm

angle_start_rad pydantic-field

angle_start_rad: float = 0.0

angle_stop_rad pydantic-field

angle_stop_rad: float = 2.0 * math.pi

angle_start property

angle_start: float

Deprecated spelling of angle_start_rad.

angle_stop property

angle_stop: float

Deprecated spelling of angle_stop_rad.

Sphere pydantic-model

Bases: FrozenModel

Fields:

type pydantic-field

type: Literal['sphere'] = 'sphere'

center_um pydantic-field

center_um: Vec3Um

radius_um pydantic-field

radius_um: PositiveUm

Polygon pydantic-model

Bases: FrozenModel

Polygon cross-section extruded along axis with optional slanted sidewalls. vertices_um are the ordered (u, v) polygon in the two transverse axes (u = lower-indexed, v = higher-indexed), counter-clockwise. sidewall_angle > 0 (radians) narrows the cross-section toward +axis; the given vertices live at reference_plane. Curved and polygon subpixel averaging are not supported.

Fields:

Validators:

  • _check

type pydantic-field

type: Literal['polyslab'] = 'polyslab'

axis pydantic-field

axis: AxisName

vertices_um pydantic-field

vertices_um: Tuple[Tuple[float, float], ...]

slab_bounds_um pydantic-field

slab_bounds_um: Tuple[float, float]

sidewall_angle pydantic-field

sidewall_angle: float = 0.0

reference_plane pydantic-field

reference_plane: Literal["bottom", "middle", "top"] = (
    "middle"
)

Media

Medium pydantic-model

Bases: FrozenModel

Isotropic nonmagnetic medium: scalar relative permittivity plus an electric conductivity entering the lossy Ca/Cb update. sigma = 0 gives the lossless update.

A non-dispersive medium leaves lorentz/poles/drude unset and the fields are omitted from the wire entirely (back-compat: schema < 1.9 documents and every existing scene round-trip byte-identically). With any poles supplied, permittivity is the high-frequency limit eps_inf and the medium is dispersive (the ADE polarization update engages only in cells carrying poles). The engine consumes the poles in WIRE ORDER, lorentz first, then poles, then drude, up to MAX_ADE_POLES combined.

Fields:

Validators:

  • _aniso_rules
  • _custom_data_rules
  • _pec_excludes_other_response
  • _pole_budget

permittivity pydantic-field

permittivity: float

conductivity_s_per_m pydantic-field

conductivity_s_per_m: float = 0.0

lorentz pydantic-field

lorentz: Optional[LorentzPole] = None

poles pydantic-field

poles: Optional[Tuple[LorentzPole, ...]] = None

drude pydantic-field

drude: Optional[Tuple[DrudePole, ...]] = None

permittivity_xyz pydantic-field

permittivity_xyz: Optional[Tuple[float, float, float]] = (
    None
)

pec pydantic-field

pec: Optional[bool] = None

permittivity_data pydantic-field

permittivity_data: Optional[PermittivityArray] = None

is_anisotropic property

is_anisotropic: bool

True when diagonal permittivity_xyz is supplied.

is_custom property

is_custom: bool

True when a node-based permittivity_data grid is supplied.

is_dispersive property

is_dispersive: bool

True when the medium carries any ADE pole (Lorentz or Drude).

from_eps_array classmethod

from_eps_array(
    values, *, conductivity_s_per_m: float = 0.0
) -> Medium

A custom medium from a 3-D array of permittivity node values. values is any (nx, ny, nz) nested sequence / numpy array with nx, ny, nz >= 2 and every value >= 1; node [0,0,0] sits on the structure box's low corner, node [-1,-1,-1] on the high corner, C order on the wire. permittivity is set to the mean (the wire requires it; the engine ignores it).

all_lorentz_poles

all_lorentz_poles() -> Tuple[LorentzPole, ...]

Every Lorentz pole in wire order (legacy lorentz first).

permittivity_at_hz

permittivity_at_hz(freq_hz: float) -> float

Real relative permittivity Re eps(omega) at freq_hz under the pole model, for a non-dispersive medium this is just permittivity; with poles, permittivity alone is only the high-frequency limit eps_inf and is the WRONG value to hand a mode solver or any other frequency-anchored consumer (a Si pole fit reads ~9.6 instead of ~12.1 at 1.55 um). Sums every Lorentz and Drude term; can go NEGATIVE for a metal below its plasma frequency. Raises at an exactly undamped Lorentz resonance, where eps diverges.

LorentzPole pydantic-model

Bases: FrozenModel

One Lorentz pole of a frequency-dependent (dispersive) medium. Under the engine's e^{-i omega t} convention the pole contributes

chi(omega) = delta_eps * omega0^2
             / (omega0^2 - omega^2 - i*gamma*omega)

where omega0 = 2piresonance_frequency_hz and gamma = 2pilinewidth_hz (both stored as ordinary Hz on the wire; the engine multiplies by 2*pi). delta_eps is the oscillator strength (the static contribution of this pole, delta of eps(0)). A medium carries up to MAX_ADE_POLES poles combined across lorentz/poles/drude.

Passivity (Im eps >= 0 for omega > 0) requires delta_eps >= 0 and gamma >= 0; the engine validates these. gamma = 0 (a lossless, undamped resonance) is allowed. Stability needs omega0dt < 2 per pole and, over all the medium's poles, the discrete-Nyquist bound eps_inf - sum delta_epsx/(1-x) - sum (wpdt/2)^2 >= courant^2 with x = (omega0dt/2)^2; the engine rejects a spec that fails it and names the largest stable courant.

Fields:

resonance_frequency_hz pydantic-field

resonance_frequency_hz: float

delta_eps pydantic-field

delta_eps: float

linewidth_hz pydantic-field

linewidth_hz: float = 0.0

DrudePole pydantic-model

Bases: FrozenModel

One Drude (free-carrier) term of a dispersive medium, the metal/plasmonics building block. Under e^{-i omega t} it contributes

chi(omega) = -wp^2 / (omega^2 + i*gamma*omega)

with wp = 2piplasma_frequency_hz and gamma = 2pilinewidth_hz (the collision rate; 0 = collisionless plasma). Below the plasma frequency the real permittivity goes strongly NEGATIVE, and the medium reflects like a metal. The engine realizes it as the omega0 = 0 ADE pole with strength wp^2. It lowers the medium's permittivity at the time-step Nyquist by (wp*dt/2)^2, so a metal fit with eps_inf near 1 needs a fine grid or a low courant (the engine rejects a spec that fails the bound and names the largest stable courant).

Fields:

plasma_frequency_hz pydantic-field

plasma_frequency_hz: float

linewidth_hz pydantic-field

linewidth_hz: float = 0.0

PermittivityArray pydantic-model

Bases: FrozenModel

Node-based permittivity data grid for a custom medium (schema 1.18). shape = (nx, ny, nz), each >= 2; values is the flat C-order (x-major) array of nxnynz node values, each >= 1. Node [0, 0, 0] sits ON the structure box's low corner and [nx-1, ny-1, nz-1] on the high corner; the engine trilinear-interpolates at its Yee sample points. Prefer Medium.from_eps_array.

Schema 1.22 adds coords_um, which places the nodes by their own coordinates (any geometry; constant extrapolation beyond the outermost node), and a per-node conductivity_s_per_m. Build those with photonhub.perturbed_medium from an imported map. CPU solver only in this release: a GPU run carrying custom media is refused before it starts.

Fields:

Validators:

  • _consistent

shape pydantic-field

shape: Tuple[int, int, int]

values pydantic-field

values: Tuple[float, ...]

coords_um pydantic-field

coords_um: Optional[
    Tuple[
        Tuple[float, ...],
        Tuple[float, ...],
        Tuple[float, ...],
    ]
] = None

conductivity_s_per_m pydantic-field

conductivity_s_per_m: Optional[Tuple[float, ...]] = None

has_coords property

has_coords: bool

True when the grid is placed by coords_um, not by the box.

Media from imported maps

SpatialMap dataclass

SpatialMap(
    x_um: Tuple[float, ...],
    y_um: Tuple[float, ...],
    z_um: Tuple[float, ...],
    values: ndarray,
)

A scalar map on a rectilinear grid in the user's frame (microns).

values has shape (len(x_um), len(y_um), len(z_um)). Each coordinate axis is strictly increasing; an axis given ONE coordinate means the map does not vary along it (a 2-D cross-section of a straight heater, say). Beyond the outermost node on an axis the map keeps that node's value.

x_um instance-attribute

x_um: Tuple[float, ...]

y_um instance-attribute

y_um: Tuple[float, ...]

z_um instance-attribute

z_um: Tuple[float, ...]

values instance-attribute

values: ndarray

coords_um property

coords_um: Tuple[
    Tuple[float, ...], Tuple[float, ...], Tuple[float, ...]
]

The node coordinates (x_um, y_um, z_um).

same_grid

same_grid(other: 'SpatialMap') -> bool

True when other has exactly the same node coordinates.

from_function classmethod

from_function(
    fn: Callable,
    x_um: Sequence[float],
    y_um: Sequence[float],
    z_um: Sequence[float],
) -> "SpatialMap"

A map sampled from fn(x, y, z) (vectorized, microns) on the grid x_um x y_um x z_um.

perturbed_medium

perturbed_medium(
    base,
    *,
    temperature: Optional[SpatialMap] = None,
    dn_dT: Optional[float] = None,
    reference_temperature_k: float = 300.0,
    electrons_cm3: Optional[SpatialMap] = None,
    holes_cm3: Optional[SpatialMap] = None,
    carrier_model: str = "nedeljkovic2011_1550",
    wavelength_um: Optional[float] = None,
) -> Medium

A custom medium for base perturbed by an imported temperature map (kelvin, with dn_dT per kelvin) and/or excess carrier-density maps (cm^-3, silicon fits in CARRIER_MODELS), on the maps' grid.

The maps share one grid, drawn in the user's frame; the simulation moves the nodes with its structures. The returned medium's scalar permittivity is the base value, its node values carry the perturbation (the port mode solver paints them too, real part only, so a port inside the perturbed region solves the perturbed, lossless mode), and a base conductivity plus the free-carrier absorption become a per-node conductivity grid. Put it on any geometry: the geometry decides where the medium is, the map what it is there, and beyond the map's outermost node the edge value holds.

wavelength_um is required with carrier maps and must lie in 1.50 to 1.60 um: the carrier models are 1550 nm fits. Give the wavelength the simulation runs at. Only this argument is checked, not the run's own band, so a medium built for 1.55 um and run at 1.31 um silently uses the 1550 nm values. Within the allowed range the 1550 nm values can be off by up to about 8 % in the index change and up to about 14 % in the absorption change at 1.50 and 1.60 um (interpolated between the 1.3, 1.55 and 2.0 um rows of Table 1 in Nedeljkovic 2011, which has no rows at 1.50 or 1.60 um). The default fit (nedeljkovic2011_1550) was made over 3.2e17 to 4e19 cm^-3 electrons and 5e17 to 1e20 cm^-3 holes; densities outside that span extrapolate it. Without carriers, wavelength_um (1.55 when omitted) only resolves a materials-library base.

thermo_optic_index

thermo_optic_index(
    n0,
    dn_dT: float,
    temperature_k,
    reference_temperature_k: float = 300.0,
)

n0 + dn_dT * (T - T_ref), first order in temperature (kelvin).

carrier_plasma

carrier_plasma(
    electrons_cm3,
    holes_cm3,
    model: str = "nedeljkovic2011_1550",
)

(dn, dalpha_per_cm) of silicon for excess electron and hole densities in cm^-3 (each >= 0), from the named fit in CARRIER_MODELS.