Skip to content

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:

Validators:

  • _unique_freqs → freqs_hz
  • _window_both_or_neither

type pydantic-field

type: Literal['flux'] = 'flux'

name pydantic-field

name: MonitorName

axis pydantic-field

axis: AxisName

position_um pydantic-field

position_um: float

freqs_hz pydantic-field

freqs_hz: Tuple[FreqHz, ...]

center_um pydantic-field

center_um: Optional[Tuple[float, float]] = None

size_um pydantic-field

size_um: Optional[Tuple[PositiveUm, PositiveUm]] = None

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:

Validators:

type pydantic-field

type: Literal['field_dft'] = 'field_dft'

name pydantic-field

name: MonitorName

center_um pydantic-field

center_um: Vec3Um

size_um pydantic-field

size_um: Tuple[NonNegativeUm, NonNegativeUm, NonNegativeUm]

fields pydantic-field

fields: Tuple[FieldComponentName, ...]

freqs_hz pydantic-field

freqs_hz: Tuple[FreqHz, ...]

interval_space pydantic-field

interval_space: Optional[Tuple[int, int, int]] = None

apodization pydantic-field

apodization: Optional[Apodization] = None

mode_port pydantic-field

mode_port: Optional[ModePort] = None

span pydantic-field

span: SkipJsonSchema[
    Optional[
        Literal["interior:x", "interior:y", "interior:z"]
    ]
] = None

station pydantic-field

station: SkipJsonSchema[Optional[Tuple[int, int]]] = None

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:

Validators:

  • _room_for_the_face_names → name
  • _unique_freqs → freqs_hz
  • _kind_fields

type pydantic-field

type: Literal['far_field'] = 'far_field'

name pydantic-field

name: MonitorName

kind pydantic-field

kind: Literal['box', 'plane']

center_um pydantic-field

center_um: Optional[Vec3Um] = None

size_um pydantic-field

size_um: Optional[
    Tuple[NonNegativeUm, NonNegativeUm, NonNegativeUm]
] = None

axis pydantic-field

axis: Optional[AxisName] = None

position_um pydantic-field

position_um: Optional[float] = None

direction pydantic-field

direction: DirectionName = '+'

freqs_hz pydantic-field

freqs_hz: Tuple[FreqHz, ...]

pts_per_wlen pydantic-field

pts_per_wlen: Optional[float] = None

apodization pydantic-field

apodization: Optional[Apodization] = None

exclude pydantic-field

exclude: Tuple[FaceSideName, ...] = ()

window pydantic-field

window: Optional[Tuple[float, float]] = None

faces property

faces: Tuple[Tuple[str, str, float], ...]

(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:

Validators:

type pydantic-field

type: Literal['field_time'] = 'field_time'

name pydantic-field

name: MonitorName

center_um pydantic-field

center_um: Vec3Um

fields pydantic-field

fields: Tuple[FieldComponentName, ...]

interval_steps pydantic-field

interval_steps: int = 1

SnapshotMonitor pydantic-model

Bases: FrozenModel

Full-domain dump of selected components. interval_steps = 0 (the default) records only the final step.

Fields:

Validators:

type pydantic-field

type: Literal['field_snapshot'] = 'field_snapshot'

name pydantic-field

name: MonitorName

fields pydantic-field

fields: Tuple[FieldComponentName, ...]

interval_steps pydantic-field

interval_steps: int = 0

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:

Validators:

  • _unique_modes → modes
  • _trial_mode_count_covers_channels

solver pydantic-field

solver: Literal['yee'] = 'yee'

out_direction pydantic-field

out_direction: Literal['+', '-']

center_um pydantic-field

center_um: Tuple[float, float]

size_um pydantic-field

size_um: Tuple[PositiveUm, PositiveUm]

dl_um pydantic-field

dl_um: PositiveUm

supersample pydantic-field

supersample: int = 8

num_modes pydantic-field

num_modes: Optional[int] = None

modes pydantic-field

modes: Tuple[PortMode, ...]

source_index pydantic-field

source_index: Optional[int] = None

thickness_axis pydantic-field

thickness_axis: Optional[AxisName] = None

PortMode pydantic-model

Bases: FrozenModel

One polarization-family channel requested on a ModePort.

Fields:

polarization pydantic-field

polarization: Literal['TE', 'TM']

mode_index pydantic-field

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

start_s pydantic-field

start_s: Optional[float] = None

end_s pydantic-field

end_s: Optional[float] = None

width_s pydantic-field

width_s: float