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
¶
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
¶
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 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
¶
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 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
¶
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
¶
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 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
¶
Notebook display¶
sharp_inline_figures
¶
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.