Skip to content

Visualization (photonhub.viz)

Plot geometry, recorded fields, modes, spectra, and far-field patterns. Import with from photonhub import viz. Two-dimensional plots return matplotlib axes. Three-dimensional plots and scene export need the optional photonhub[viz] extra.

Geometry

plot

plot(
    sim,
    x=None,
    y=None,
    z=None,
    *,
    ax=None,
    legend=True,
    grid=False,
    unfold=True,
    **kw,
)

Draw the analytic scene cross-section on the selected cut plane and return the matplotlib Axes.

Exactly one of x/y/z (microns) selects the constant-coordinate cut plane; not exactly one -> ValueError. A cut outside the realized domain warns and omits structure cross-sections and the grid. The background, applicable overlays, symmetry markers, and domain limits are retained. grid=True overlays the realized Yee cell edges (the mesh-resolution sanity check). unfold (the default) mirrors the half domain back across each in-plane symmetry plane, so the picture is the whole device; unfold=False draws the reduced domain the solver actually steps. Extra **kw is forwarded to each structure patch.

plot_index

plot_index(
    sim,
    x=None,
    y=None,
    z=None,
    *,
    ax=None,
    cmap=None,
    legend=True,
    grid=False,
    unfold=True,
    subpixel=None,
    supersample=None,
    **kw,
)

Render a heatmap of ε sampled on the mesh on the selected cut plane. Returns the matplotlib Axes.

Exactly one of x/y/z (microns) selects the constant-coordinate cut plane. The plot uses the real µm node coordinates (pcolormesh), so graded meshes render with correct, non-uniform cell widths. grid=True overlays the realized cell edges, useful to confirm features are well resolved. unfold (the default) mirrors the sampled half back across each in-plane symmetry plane, so the heatmap shows the whole device; unfold=False shows the reduced domain the solver steps.

subpixel follows Simulation.subpixel by default, so the view shows what the run samples on the mesh: the volume-fraction average when the run smooths, the hard point sample when it does not. Force either with True/False, and raise supersample for a finer fill fraction at proportional cost (see smooth_eps_plane for what the scalar average does and does not reproduce).

plot_3d

plot_3d(sim, show_pml=False, **kw)

Build and return a plotly Figure of the 3D scene geometry. Raises ImportError (with the install hint) when plotly is not installed.

By default the PML is not drawn: the wireframe marks the inner (non-PML) boundary and structures / planes are clipped to it. show_pml=True restores the full-domain wireframe, translucent PML regions, and unclipped geometry.

Fields and sources

plot_field

plot_field(
    data,
    monitor,
    field="Ex",
    x=None,
    y=None,
    z=None,
    *,
    freq=None,
    time=None,
    val="real",
    structures=True,
    simulation=None,
    ax=None,
    cmap=None,
    legend=True,
    unfold=True,
    scale="max",
    db_floor=-40.0,
    **kw,
)

Heatmap of a field component on a 2D slice of data[monitor].

data is a RunResult; monitor is its key. freq= picks a frequency on a DFT monitor; time= (seconds) picks a recorded sample on a time/snapshot monitor (default: the last frame). See the module docstring for the rest.

scale sets the colour scale. "max" (the default) divides by the slice's own maximum, so the colorbar runs 0 to 1 (or -1 to 1 for a signed part) instead of showing a per-unit-source number like 1.7e-5 that no reader can place. "db" shows a magnitude in decibels relative to that maximum down to db_floor, which is how a feature a hundred times weaker than the guide (a crossing's crosstalk arm) becomes visible at all; it needs a magnitude (field="E", "H", "intensity" or val="abs"). "raw" keeps the recorded values.

unfold (the default) mirrors the recorded half back across each in-plane symmetry plane, with the component's own parity about that plane, so the picture is the whole device's field. It needs a simulation to know the symmetry, the same object the structure outlines need. unfold=False shows only the half that was stepped. Returns the matplotlib Axes.

plot_source_time

plot_source_time(st, *, ax=None, num: int = 512)

Plot the source drive J(t) (with its Gaussian envelope) and, when building its own figure, the spectral envelope |J(f)| alongside.

ax= draws only the time-domain trace on the given Axes; otherwise a 1×2 (time | spectrum) figure is created. Returns the time-domain Axes.

Modes and spectra

plot_mode

plot_mode(mode, *, ax=None, cmap=None, legend=False, **kw)

Heatmap of an FDE Mode's transverse field on its µm cross-section. Returns the matplotlib Axes.

mode is a Mode (carrying .field, .n_eff, .polarization, .wavelength_um and .field_dataarray()). cmap= overrides the diverging colormap (the symmetric zero-centered normalization is kept). ax= draws into an existing Axes. Extra **kw pass through to pcolormesh.

plot_overlap

plot_overlap(
    mode_a,
    mode_b,
    *,
    axes=None,
    center_a=(0.0, 0.0),
    center_b=(0.0, 0.0),
    direction="+",
    cmap=None,
)

Three-panel view of a mode⇄mode overlap (photonhub.analysis.mode_overlap): |E| of each mode on the shared grid and the overlap integrand Re(E_A*·E_B), so you can see where the two profiles match (the integrand is positive where they reinforce, negative/zero where they mismatch). The suptitle carries the power coupling, the field overlap and the mismatch loss.

mode_a/mode_b are any of scalar Mode, VectorMode, or gaussian_mode. center_b shifts mode B (lateral misalignment), direction="-" uses mode B's backward partner, both forwarded to the overlap so the panels match the reported numbers. Returns the three matplotlib Axes; never calls plt.show().

plot_spectrum

plot_spectrum(spectra, *, ax=None, ymax=1.05, **kw)

Plot transmission T versus wavelength (nm).

spectra is a single spectrum (a {freq_hz: T} mapping or the labelled array photonhub.analysis.transmission_spectrum returns) for one trace, or a {label: spectrum} mapping (one labelled trace per key, with a legend). ax= draws into an existing Axes; ymax caps the y-axis (default ~1.05). Extra **kw pass through to ax.plot. Returns the matplotlib Axes.

plot_comparison

plot_comparison(
    x_nm,
    values=None,
    *,
    reference=None,
    reference_scale=1.0,
    stated=None,
    model=None,
    markers=None,
    points=None,
    ylabel="",
    xlabel="wavelength (nm)",
    label="PhotonHub",
    ax=None,
    ylim=None,
    yscale=None,
    **kw,
)

The result figure of an example: our observable as a line, the paper's extracted series as markers, the paper's stated values as grey dashed lines or points, and a model as a coloured dashed line.

x_nm / values are our curve, in whatever unit the observable has (dB, a ratio, a Q). A spectrum from photonhub.analysis.transmission_spectrum may be passed alone as x_nm and plots against its own wavelength coordinate. values may also be a mapping {label: y} (several of our series over x_nm) or {label: (x, y)} (each over its own x) or {label: spectrum}: each keeps a fixed colour in the order given, so the same series has the same colour in every figure (a single series takes the axes' next colour; a line drawn by hand after the mapping form does not advance past it). A series of fewer than MARKER_POINTS points (a sweep of discrete runs) is drawn with markers as well; markers=True or False forces it. points names series drawn as markers alone, with no line joining them (a few discrete runs beside another method's sampled curve): one label or a sequence of labels of values, or label for a single series. Each keeps the colour of its place and its legend entry shows the marker only; markers= does not apply to it, and a name that is not one of our series raises ValueError. Extra keywords apply to every series, so a linestyle passed in kw joins the points again. label names a single series.

reference is one series or a list of series from a reference file, {"x": [...], "y": [...], "label": "..."}, drawn as markers, each y multiplied by reference_scale (-1 turns a digitized transmittance in dB into an insertion loss). stated draws the paper's stated values, the form a page uses when the paper's license does not allow its figure to be re-plotted: (value, label) is a horizontal line (several are told apart by their dash pattern), (x, y, label) a point at its own x (a stated Q at a stated radius). Either form, or a list mixing them. model is (x, y, label) or a list of them: an analytic curve as a coloured dashed line.

ylim fixes the y-range so the agreement is visible. yscale sets the axis scale, "linear" or "log" (the latter for an observable that spans decades, a resonator Q); left at None the axes keep the scale they have, so a log ax passed in, or one a first call set to log, stays log. When every x of our series is a whole number (a count of segments or periods) the ticks are whole numbers too. The figure has no title: the paragraph above it is its caption. Returns the matplotlib Axes and never calls plt.show().

Far field

plot_far_field_cut

plot_far_field_cut(
    pattern,
    *,
    phi_rad: float = 0.0,
    quantity: str = "directivity",
    freq_hz=None,
    scale: str = "raw",
    db_floor: float = -40.0,
    ax=None,
    **kw,
)

A polar cut through a far-field pattern on a meshed angle grid: the quantity against the polar angle theta in the plane of azimuth phi_rad (the grid's azimuth nearest it), drawn on a polar Axes with theta = 0 (+z) at the top and theta growing clockwise. Where the grid also holds the opposite azimuth phi_rad + pi, that half of the plane is drawn on the left, so a full grid gives the whole great circle. Returns the Axes.

quantity is "directivity" (the default) or "intensity" (W/sr). scale: "raw" (the values), "max" (divided by the cut's maximum) or "db" (decibels: the directivity in dBi, the intensity relative to the cut's maximum, both drawn down to db_floor below the cut's peak). freq_hz picks the frequency nearest it (required when the projection has several; the projected frequencies may be in any order). ax= draws into an existing polar Axes; extra **kw pass through to plot.

plot_far_field_map

plot_far_field_map(
    pattern,
    *,
    quantity: str = "intensity",
    freq_hz=None,
    scale: str = "max",
    db_floor: float = -40.0,
    half_space: str = "+",
    ax=None,
    cmap=None,
    **kw,
)

A map of a far-field pattern on a meshed grid. Returns the Axes.

  • A k-space grid: over its direction cosines (ux, uy), with the unit circle (the edge of the propagating directions) drawn.
  • An angle grid: the half space half_space ("+", theta <= pi/2, or "-") over its direction cosines along x and y, (sin(theta) cos(phi), sin(theta) sin(phi)), the view down the z axis.
  • A Cartesian grid: over the plane's two axes (um).

quantity is "intensity" (W/sr, the default) or "directivity" (a meshed angle grid). scale: "max" (the default, divided by the map's maximum), "db" (decibels, the directivity in dBi and the intensity relative to its maximum, down to db_floor below the peak) or "raw". freq_hz picks the frequency nearest it (required when there are several). cmap= overrides the sequential colormap; ax= draws into an existing Axes; extra **kw pass through to pcolormesh (vmin, vmax or norm replace the default color range).

Interactive views

interactive_preview

interactive_preview(
    sim,
    *,
    axis: str = "z",
    eps: bool = True,
    grid: bool = True,
)

Build a Jupyter widget that scrubs the cut plane through sim.

A cut-axis dropdown, a position slider (auto-ranged to the realized domain), and ε / grid toggles drive a live cross-section. Returns the ipywidgets container, which displays inline as a cell's last expression. Requires the photonhub[viz] extra and a notebook.

interactive_field

interactive_field(data, monitor, *, simulation=None)

A Jupyter scrubber over a recorded field monitor: field component, real/imag/abs/phase, frequency (DFT monitors) or a time slider + Play button for field-propagation animation (time/snapshot monitors), and (for a volumetric monitor) the cut plane, with optional structure outlines. Returns the ipywidgets container. Requires the photonhub[viz] extra and a notebook.

render_slice

render_slice(
    sim,
    axis: str,
    value: float,
    *,
    eps: bool = True,
    grid: bool = True,
    ax=None,
)

Render one preview frame: the meshed-ε heatmap (eps=True) or the analytic scene (eps=False) at axis=value, optionally with the grid overlay. Returns the matplotlib Axes (pure, no widgets).

render_field_slice

render_field_slice(
    data,
    monitor,
    *,
    field="Ex",
    val="real",
    freq=None,
    time=None,
    axis=None,
    value=None,
    structures=False,
    simulation=None,
    ax=None,
)

Render one field-preview frame via plot_field, the pure, testable core of interactive_field. axis/value pick the cut plane for a volumetric monitor; freq=/time= pick the DFT frequency / time frame. Omit the cut for an already-planar monitor.

Scene export

export_scene

export_scene(
    sim,
    data,
    *,
    plane: str | Sequence[str],
    freq_hz: float | None = None,
    ports: Mapping[str, Mapping[str, Any]] | None = None,
    port_in: str | None = None,
    port_through: str | None = None,
    ports_offset_um: tuple[float, float] = (0.0, 0.0),
    label: str | None = None,
    caption: str | None = None,
    mode: str = "TE0",
    periodic_extent_um: float = 3.0,
    max_samples: int = 256,
    simplify_um: float = 0.002,
    propagate_um: float | None = None,
    propagate_extent_um: float | None = None,
    phase_component: str | Sequence[str] | None = None,
    front_spacing: int = 1,
    structures: Sequence[int] | None = None,
) -> Scene

Build the featured-figure scene of sim from its run data.

``plane`` names the field-DFT monitor (a `ProfileMonitor`
with one zero-size axis) to draw; ``freq_hz`` picks the frequency when the
monitor records several. Naming ``port_in`` or ``port_through`` without
``ports`` draws the simulation's own ports (``sim.ports``), each facing the
wall the simulation infers for it. ``ports`` is otherwise a port dict, ``name ->
{"center": (x, y), "outward": (dx, dy), "width": w}`` in the notebook's
own coordinates: the user's frame of a simulation fitted with ``domain=``
(its ``origin_um`` is taken off here), shifted by ``ports_offset_um`` when
the layout was placed at an offset of its own. ``port_in`` is
labelled "in" and its arrow points inward; ``port_through`` is labelled
"through"; every other port carries its own name unless it declares a
``label``. ``caption`` defaults to "|E|, λ = … nm" plus ", TE₀ in" when
ports are given. Quasi-2D runs (periodic transverse axes thinner than
``periodic_extent_um``) are widened to that extent, a structure that
does not span the period (a pillar in its unit cell) repeated at it; a
wider periodic domain is shown as it is. ``propagate_um`` draws the recorded plane's field
reconstructed that far downstream of it instead (its plane-wave spectrum
propagated through the homogeneous medium it sits in, a lens's focal
spot floating above the device); ``propagate_extent_um`` crops that
plane to a square of this side, centred on the shown region.

phase_component names the recorded component whose phase animates the figure; by default it is the E component carrying the most power over the plane. A guide that turns in the plane (a bend, a ring) rotates its field with it, so no single Cartesian E component follows the light around the turn: record Hz on the plane and pass phase_component="Hz", the one component a rotation about the plane's normal leaves unchanged. A device that turns the polarization hands the light from one component to another, so that no single one is lit from end to end: pass the pair, phase_component=("Ey", "Ez"), and the figure animates the field along the major axis of the ellipse the two trace, which is the whole transverse field of a guided mode. front_spacing draws the wave fronts that many times further apart along the plane's long axis, and the caption says so. It is for a device some hundred wavelengths long, whose fronts are otherwise a pixel or two apart: the envelope, the mode's lobes and the beating between modes are untouched, only the carrier common to a whole line of the plane is slowed. It follows the wave that carries the power, so it suits a travelling wave, not a cavity. The still figure is |E| either way. structures lists the indices of sim.structures to draw, for a figure that shows the guide alone and not a substrate several microns of oxide below it; by default every structure is drawn. Air carve-outs carve and claddings are left out either way, and the scene's height and a plan view's placement follow the bodies that are drawn.

``plane`` may also name a group recorded with
`sections`: the monitors ``{plane}_0``,
``{plane}_1``, ... are drawn together, each a cross-section on its own
brightness scale, each sampled at ``max_samples / sqrt(n)`` per axis so
the scene keeps its size budget. A list of monitor names draws those
planes together the same way: one plan view inside each layer of a
stacked device paints the field on every layer's top face. The viewer draws a device much longer
than it is wide with its long axis compressed and says so in the caption.

Returns a `Scene`; ``Scene.write(path)`` stores the JSON the viewer
loads.

Scene dataclass

Scene(doc: dict[str, Any])

The exported scene document. write(path) stores it as compact JSON.

doc instance-attribute

doc: dict[str, Any]

size_bytes property

size_bytes: int

UTF-8 byte length of the compact serialized scene document.

to_dict

to_dict() -> dict[str, Any]

Return the scene document itself. Mutating it also changes this scene.

to_json

to_json() -> str

Serialize the scene as compact JSON. Reject non-finite numeric values.

write

write(path) -> Path

Write compact scene JSON to path, creating parent directories as needed.

Notebook display

sharp_inline_figures

sharp_inline_figures(enable: Optional[bool] = None) -> bool

Show notebook figures at twice the pixel density ("retina" PNG).

Jupyter's inline backend renders a figure as a PNG at the figure's own 100 dpi, which a high-density screen, and the HTML page made from the notebook, blur. Retina output renders the same figure at 200 dpi and shows it at the same size, so every built-in plot is sharp at no change in layout. Called once when photonhub.viz is imported; it does nothing outside an IPython kernel, and PHOTONHUB_SHARP_FIGURES=0 (or enable=False) leaves the notebook's own setting alone. Returns whether the retina format is now selected.