Monitors¶
What a run records. Flux and DFT monitors accumulate in the frequency domain during the run; time and snapshot monitors record raw fields.
Frequency domain¶
PowerMonitor
pydantic-model
¶
Bases: FrozenModel
Poynting-flux monitor perpendicular to axis at position_um,
snapped to a plane index 1 <= kp <= n_axis - 1. Positive values mean power toward +axis. The value
RunResult returns is the time-averaged power in
WATTS of a continuous-wave excitation at each frequency by the sources
as declared: the engine accumulates the flux from phasors normalized by
the first wire-order source's A0*S(f) (the response to a
unit-amplitude drive), and the reader multiplies A0^2 back in (the
array's norm_amplitude attr; its normalization attr says so). A
unit dipole therefore reads its Hertzian power, and a beam or mode
launched with power_watts=1 reads 1 W on a full plane below it.
Only a result whose simulation is unknown (an output directory or HDF5
bundle without its sim.json) keeps the engine's unit-amplitude
values. Ratios such as R and T (divide by an empty reference run) are
amplitude-free either way. The plane average of the staggered H
components makes this exactly the flux the solver conserves from plane to
plane on any mesh: the power a port's modal readout reports, and the one
a port or beam launch built cell by cell (the default) normalizes
power_watts to; a paraxial beam and an auxiliary-line
mode source keep the continuum normalization and read cos(k*dl/2) of
it. A
plane wave of field amplitude E0 in an empty domain carries
cos(k*dl/2) of the continuum value E0^2*A/(2*eta0) (-0.5 % at 31
cells per wavelength, -2 % at 15), which cancels in ratios. At its pulse
centre f0, an oblique one
(with_oblique_plane_wave) in a medium of
index n carries E0^2*A*n*cos(theta)/(2*eta0) through a plane
normal to its axis, with k becoming k*cos(theta) in that factor.
That closed form covers f0 only: off it the constant wavevector
leaves a forward wave E0*(1+r)/2, with r = cos(theta)/cos(theta(f))
and sin(theta(f)) = f0*sin(theta)/f, and a backward leak
E0*(1-r)/2, and the band must stay clear of the cutoff
f0*sin(theta). Every reading on a Bloch axis has this same
normalization. A solver from photonhub 0.1.6 or earlier reads 4 times
the power, and twice the field of a ProfileMonitor, on a Bloch
axis. Time convention is e^{-i omega t}.
Resolving a resonance peak (Q from a spectrum): the FWHM of a
quality-factor-Q peak at f0 is f0/Q, so freqs_hz must be
spaced much finer than that, df << f0/Q, or the fitted width (and
hence Q) is dominated by sampling. E.g. a Q ~ 400 cavity peak needs a
dedicated narrow band around f0, not the source's full bandwidth
(measured: a full-band 300-point sweep under-read Q by ~13%; a
peak-centred band recovered it, the ring-down ResonanceAnalysis route
avoids the issue entirely).
By default the monitor integrates the FULL transverse plane. Schema 1.17
adds an optional sub-region WINDOW: pass BOTH center_um and
size_um as (u, v) pairs in the plane's CYCLIC transverse order
u = (axis+1) % 3, v = (axis+2) % 3 (z-normal -> (x, y); x-normal ->
(y, z); y-normal -> (z, x)). The realized window snaps to whole cells by
cell-centre membership and must cover at least one cell (the engine
validates). None/None (default) keeps the legacy full plane and is
omitted from the wire. A window runs on the CPU, on one GPU and on
several GPUs (device="gpu:0,1" and the like). A solver from
photonhub 0.1.6 or earlier refuses a window on several GPUs; run it on
one GPU there.
With symmetry planes (symmetry=) the value is the
power of the whole device through the plane, what the simulation without
the planes reads, the convention of ports and of every launch's
power_watts: the engine integrates the part of the
plane the simulation models, and the reader multiplies it by 2 for every
symmetry plane that cuts the monitor plane (one whose normal lies in it,
reached by the window; the array's symmetry_factor attr, absent when
no plane cuts the monitor). A window that stops short of the symmetry
plane reads its own region and gets no factor, and so does a plane
parallel to a symmetry plane: a closed box of monitors around a source
therefore balances only with the image of each such face added.
RunResult.wire(name) keeps the modeled part.
Fields:
-
type(Literal['flux']) -
name(MonitorName) -
axis(AxisName) -
position_um(float) -
freqs_hz(Tuple[FreqHz, ...]) -
center_um(Optional[Tuple[float, float]]) -
size_um(Optional[Tuple[PositiveUm, PositiveUm]])
Validators:
-
_unique_freqs→freqs_hz -
_window_both_or_neither
ProfileMonitor
pydantic-model
¶
Bases: FrozenModel
Running-DFT field monitor over a box region:
fp64 accumulation every step over the full run, raw Yee-located phasors.
The engine normalizes them by the first wire-order source's A0 * S(f)
and RunResult multiplies A0 back in, so the
arrays are the continuous-wave phasors of the sources as declared, in
V/m and A/m (PowerMonitor states the convention). size_um
components may be 0 (plane/line/point regions); the region is snapped per
component to that component's Yee sublattice, and the engine validator
REJECTS boxes whose per-component snaps disagree (the output carries one
shape/origin per monitor). When fields mixes Yee offsets along an
axis, a face belongs strictly between an integer cell boundary and the
next half-cell plane, canonically (k + 0.25) * dl, which every
component snaps to cell k with quarter-cell fp margin.
You do not have to place faces there yourself: building a
Simulation AUTO-SNAPS every box face that would fail
(or pass only on float rounding luck) that engine check to the nearest
quarter-cell plane of its local cell, clamped into the grid, and reports
each adjustment on the photonhub.components.simulation DEBUG log.
The policy is deterministic and minimal: faces already strictly inside a
first half-cell, axes whose listed components share one Yee offset, and
faces at/beyond the domain edges (where the engine's index clamp makes
every component agree, full-domain boxes stay full-domain) are left
byte-identical, and a face and its quarter point snap to the SAME cell,
so any scene the engine already accepted keeps its exact recorded
region. A sub-half-cell box straddling a cell boundary cannot be snapped
and is rejected with guidance at construction. Client-side only: the
wire schema is unchanged, documents ingested via from_wire_json /
from_file are never adjusted, and
with_auto_mesh/with_mesh_overrides re-apply the snap against the
regenerated grid.
Fields:
-
type(Literal['field_dft']) -
name(MonitorName) -
center_um(Vec3Um) -
size_um(Tuple[NonNegativeUm, NonNegativeUm, NonNegativeUm]) -
fields(Tuple[FieldComponentName, ...]) -
freqs_hz(Tuple[FreqHz, ...]) -
interval_space(Optional[Tuple[int, int, int]]) -
apodization(Optional[Apodization]) -
mode_port(Optional[ModePort]) -
span(SkipJsonSchema[Optional[Literal['interior:x', 'interior:y', 'interior:z']]]) -
station(SkipJsonSchema[Optional[Tuple[int, int]]])
Validators:
-
_unique_fields→fields -
_unique_freqs→freqs_hz -
_strides_positive→interval_space
span
pydantic-field
¶
plane
classmethod
¶
plane(
name: str,
axis: str,
position_um: float,
*,
wlens_um: Optional[
Union[float, Sequence[float]]
] = None,
freqs_hz: Optional[Sequence[float]] = None,
fields: Tuple[str, ...] = ("Ex", "Ey", "Ez"),
size_um: Optional[Tuple[float, float, float]] = None,
center_um: Optional[Tuple[float, float, float]] = None,
**kw,
) -> ProfileMonitor
A zero-thickness field monitor on the plane axis = position_um.
With no size_um the plane spans the PML-free interior of the
domain: the in-plane centre and extent are filled in when the monitor
joins a Simulation (a periodic or PEC axis spans
fully, a symmetry axis from the mirror to the far PML face). An
explicit size_um is a 3-tuple whose component along axis is
ignored and needs center_um alongside. Frequencies come from
wlens_um (microns, one value or several) or freqs_hz, exactly
one of them. Other keywords (apodization, interval_space,
mode_port) pass through.
sections
classmethod
¶
sections(
name: str,
axis: str,
n: int = 6,
*,
wlens_um: Optional[
Union[float, Sequence[float]]
] = None,
freqs_hz: Optional[Sequence[float]] = None,
fields: Tuple[str, ...] = ("Ex", "Ey", "Ez"),
**kw,
) -> List[ProfileMonitor]
n cross-section planes normal to axis, spread evenly along
the domain interior: the planes a long device's featured figure stands
along it (photonhub.viz.export_scene with plane=name draws
them together).
Each is a plane spanning the interior, named {name}_0 to
{name}_{n-1}. Their positions are filled in when they join a
Simulation: plane i sits at
lo + (i + 1/2) (hi - lo) / n of the PML-free interior along
axis, so a simulation rebuilt around a longer device moves them
with it. Frequencies and other keywords are as for plane.
Returns a list: add it to monitors with *.
FarFieldMonitor
pydantic-model
¶
Bases: FrozenModel
A near-field surface recorded for far-field projection: a box
(box, closed, or open with faces left out) or a single plane
(plane).
Client-side only, like a ProfileMonitor.plane span: when it joins a
Simulation it is compiled into ordinary
field_dft monitors, one per face, named <name>.x_minus,
<name>.x_plus and so on, recorded the way
far_field reads them best: the four
tangential E and H components, one cell thick along the normal (two
samples, so E and H can be brought onto one surface), and, for a box,
every cell, one cell past the box in each face's plane. The wire
document and the solver see only those monitors. The recipe (the faces,
their outward normals, the medium the faces sit in, the declared
geometry, the faces left out, the symmetry planes and the edge window)
is kept beside the run (client.json), so far_field(data,
"<name>", ...) projects the surface by its name and into its medium;
a result reloaded without that record still projects from the face
names, with a warning (and without an open box's edge or a window).
On a simulation with symmetry planes only the kept part is recorded: a
face across a plane starts on it, and the box face on the far side of a
plane is not recorded (far_field reads it as the mirror image of the
near one). The box must then reach across each plane symmetrically, or
start on it.
Building the simulation refuses, naming the face: a face in a PML or absorber layer, a face that crosses a structure or a medium boundary, faces in different media, a medium the projection cannot use (anisotropic, custom, PEC or lossy), a box on a quasi-2-D axis, a box or a plane's extent that does not reach a symmetry plane or crosses it off centre, a plane on a symmetry plane or in its mirrored half, and leaving out a box face normal to a symmetry plane. Faces left out are neither recorded nor checked. It warns when a face is closer than half a wavelength (in its medium) to a structure, when a source or a structure lies within 2 cells of a face's innermost sample (the readout's error grows sharply there; each source measured where the solver runs it, a point dipole on its component's nearest grid point, a plane source on its grid line), and, on a graded mesh, when a box face is nearer than 0.35 wavelengths to a grid line where the cell size changes, along its normal, or a source is within 0.1 wavelengths of one: the asymmetry the projection reads on a graded mesh is shown to stay within 2 % of the peak (measured up to 0.93 %) at 30 background cells per wavelength (refinement ratios 2 and 3) only for boxes clear of such changes.
Fields:
-
type(Literal['far_field']) -
name(MonitorName) -
kind(Literal['box', 'plane']) -
center_um(Optional[Vec3Um]) -
size_um(Optional[Tuple[NonNegativeUm, NonNegativeUm, NonNegativeUm]]) -
axis(Optional[AxisName]) -
position_um(Optional[float]) -
direction(DirectionName) -
freqs_hz(Tuple[FreqHz, ...]) -
pts_per_wlen(Optional[float]) -
apodization(Optional[Apodization]) -
exclude(Tuple[FaceSideName, ...]) -
window(Optional[Tuple[float, float]])
Validators:
size_um
pydantic-field
¶
faces
property
¶
(monitor name, axis, outward sign) of each face it declares, in
the form far_field(..., faces=...) takes; a box's faces left out
are not among them. On a simulation with symmetry planes the far-side
face of each plane is not recorded either:
Simulation.far_field_monitors lists the faces a simulation
records.
box
classmethod
¶
box(
name: str,
center_um: Tuple[float, float, float],
size_um: Tuple[float, float, float],
*,
wlens_um: Optional[
Union[float, Sequence[float]]
] = None,
freqs_hz: Optional[Sequence[float]] = None,
exclude: Union[str, Sequence[str]] = (),
**kw,
) -> FarFieldMonitor
A box of six faces around center_um, size_um wide
(microns). Each face sits on the grid line nearest its plane. The
frequencies come from wlens_um (microns) or freqs_hz, exactly
one of them; apodization passes through to the face monitors.
Box faces record every cell: a stride (pts_per_wlen) is refused,
since a decimated box face reads the power up to 17 % off with the
source near a corner.
exclude leaves faces out, an open box: ("-z",) drops the
face whose outward normal is -z, for instance where a substrate
lies below the box (the faces kept must still sit in one medium, so
the open edge stays above the substrate). A face left out is neither
recorded nor checked. The faces kept still record one cell past the
box on the open side too, and far_field ends them at the plane
the face left out is declared on (the box's centre plus or minus half
its size), with the closed box's fourth-order edge rule wherever that
plane falls between samples, so the open edge stays where it was
declared on any mesh. The projection is the closed box's less that
face's own contribution. It no longer sees the light leaving through
the open side, and what
reaches the kept faces from there is read as if the box were closed,
so the result approximates the pattern on the side the box is closed
on and is not a full-sphere pattern. A window is refused on a box:
tapering the currents at a box's edges opens the surface the
projection relies on being closed.
plane
classmethod
¶
plane(
name: str,
axis: str,
position_um: float,
*,
direction: str = "+",
wlens_um: Optional[
Union[float, Sequence[float]]
] = None,
freqs_hz: Optional[Sequence[float]] = None,
pts_per_wlen: Optional[float] = None,
center_um: Optional[Tuple[float, float, float]] = None,
size_um: Optional[Tuple[float, float, float]] = None,
window: Optional[Tuple[float, float]] = None,
**kw,
) -> FarFieldMonitor
A single plane axis = position_um radiating toward
direction ("+" or "-" along axis), on the grid line
nearest it. With no size_um it spans the PML-free interior of
the domain; an explicit size_um (its component along axis is
ignored) needs center_um alongside. pts_per_wlen sets an
in-plane stride so the plane keeps at least that many samples per
wavelength in its medium (at the shortest wavelength, across the
largest cell); without it every cell is recorded.
window=(f1, f2) tapers the currents toward the plane's edges
before they are integrated, off by default: over the outer fraction
f1 of the plane's declared extent at each end of its first
in-plane axis, and f2 along its second (the in-plane axes in
xyz order: x and y for a plane normal to z, y and z for x, x and z
for y), the currents are scaled by sin^2, rising from 0 at the
edge to 1. The declared extent is size_um, or the PML-free
interior, mirrored across a symmetry plane, so a run with symmetry
planes and the full run taper the same surface. Each fraction is 0 (no taper along that axis) to 0.5
(the two tapers meet in the middle). An open plane is a truncated
surface: the field cut off at its edges radiates an edge ripple
into the pattern, which the window trades for a smoother pattern
that underweights light leaving near the edges.
Time domain¶
TimeMonitor
pydantic-model
¶
Bases: FrozenModel
Scalar time-series probe at the Yee node nearest center_um. Samples
are raw, non-colocated Yee values; H lags E by dt/2.
Fields:
-
type(Literal['field_time']) -
name(MonitorName) -
center_um(Vec3Um) -
fields(Tuple[FieldComponentName, ...]) -
interval_steps(int)
Validators:
-
_unique_fields→fields
SnapshotMonitor
pydantic-model
¶
Bases: FrozenModel
Full-domain dump of selected components. interval_steps = 0 (the
default) records only the final step.
Fields:
-
type(Literal['field_snapshot']) -
name(MonitorName) -
fields(Tuple[FieldComponentName, ...]) -
interval_steps(int)
Validators:
-
_unique_fields→fields
Ports¶
ModePort
pydantic-model
¶
Bases: FrozenModel
Authoring recipe that turns a DFT field plane into a modal port.
This block has no native time-stepping semantics. The engine validates and
ignores it; result post-processing solves the requested Yee modes on the
saved cross-section and projects this monitor's four tangential fields onto
them. out_direction points away from the device. source_index marks
the one driven port in the current run, whose incident direction is the
opposite sign.
center_um / size_um use the natural horizontal/vertical axes of the
monitor plane (x-normal -> y,z; y-normal -> x,z; z-normal -> x,y).
TE means electric field primarily along waveguide width (orthogonal to
thickness_axis), not necessarily the Yee solver's natural-horizontal
family.
num_modes is the total eigensolver frame size even though each requested
mode_index is counted independently within its TE/TM family.
Fields:
-
solver(Literal['yee']) -
out_direction(Literal['+', '-']) -
center_um(Tuple[float, float]) -
size_um(Tuple[PositiveUm, PositiveUm]) -
dl_um(PositiveUm) -
supersample(int) -
num_modes(Optional[int]) -
modes(Tuple[PortMode, ...]) -
source_index(Optional[int]) -
thickness_axis(Optional[AxisName])
Validators:
-
_unique_modes→modes -
_trial_mode_count_covers_channels
PortMode
pydantic-model
¶
Bases: FrozenModel
One polarization-family channel requested on a ModePort.
Fields:
-
polarization(Literal['TE', 'TM']) -
mode_index(int)
Shaping¶
Apodization
pydantic-model
¶
Bases: FrozenModel
Time window applied to a monitor's running DFT (the standard
ApodizationSpec analogue). A Gaussian roll-ON of
standard deviation width_s for t < start_s, flat (== 1) on
[start_s, end_s], and a Gaussian roll-OFF for t > end_s, used to
isolate the late-time steady state of a resonant structure (suppressing the
source-injection transient). start_s/end_s (seconds) are each
optional, omit a side to leave it ungated; width_s (seconds) is the
Gaussian standard deviation of each roll and must be positive.
Fields:
Validators:
-
_ordered