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
Shapes¶
Box
pydantic-model
¶
Bases: FrozenModel
Axis-aligned box: full extents size_um centered on center_um.
Fields:
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:
-
type(Literal['cylinder']) -
axis(AxisName) -
center_um(Vec3Um) -
radius_um(PositiveUm) -
inner_radius_um(NonNegativeUm) -
length_um(PositiveUm) -
angle_start_rad(float) -
angle_stop_rad(float)
Validators:
-
_check
Sphere
pydantic-model
¶
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:
-
type(Literal['polyslab']) -
axis(AxisName) -
vertices_um(Tuple[Tuple[float, float], ...]) -
slab_bounds_um(Tuple[float, float]) -
sidewall_angle(float) -
reference_plane(Literal['bottom', 'middle', 'top'])
Validators:
-
_check
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:
-
permittivity(float) -
conductivity_s_per_m(float) -
lorentz(Optional[LorentzPole]) -
poles(Optional[Tuple[LorentzPole, ...]]) -
drude(Optional[Tuple[DrudePole, ...]]) -
permittivity_xyz(Optional[Tuple[float, float, float]]) -
pec(Optional[bool]) -
permittivity_data(Optional[PermittivityArray])
Validators:
-
_aniso_rules -
_custom_data_rules -
_pec_excludes_other_response -
_pole_budget
is_dispersive
property
¶
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
¶
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(float) -
delta_eps(float) -
linewidth_hz(float)
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(float) -
linewidth_hz(float)
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:
-
shape(Tuple[int, int, int]) -
values(Tuple[float, ...]) -
coords_um(Optional[Tuple[Tuple[float, ...], Tuple[float, ...], Tuple[float, ...]]]) -
conductivity_s_per_m(Optional[Tuple[float, ...]])
Validators:
-
_consistent
coords_um
pydantic-field
¶
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.
coords_um
property
¶
The node coordinates (x_um, y_um, z_um).
same_grid
¶
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
¶
n0 + dn_dT * (T - T_ref), first order in temperature (kelvin).
carrier_plasma
¶
(dn, dalpha_per_cm) of silicon for excess electron and hole
densities in cm^-3 (each >= 0), from the named fit in
CARRIER_MODELS.