3D viewer#
model.show() opens an interactive 3D view of a
GeometryModel: the solids coloured by material, the
declared features, and — with a mesh — the FIT grid. The same call
serves three situations:
in a Jupyter notebook it is a widget, rendered in the browser;
in a script it opens a window;
in a documentation build it becomes a figure with two tabs: a screenshot, and the same scene as a rotatable view in the browser (this is how the 3D figures in the tutorials are made). The browser tab has no toolbar — the cutting plane stays where the script put it.
model.show() # geometry only
model.show(mesh=mesh, cut=("y", 0.0)) # opened along y = 0, grid cells on the cut
What is drawn#
Item |
Appearance |
|---|---|
Solids |
Material colour of the 2D cross-sections (metals grey, dielectrics tinted by permittivity, air and vacuum as faint translucent shells); imported CAD colours are honoured. |
Grid ( |
On the cutting plane, the exposed cell layer as a sheet of cell faces, each coloured by the material the mesher assigned — the discretised model as the solver sees it. The grid is shown nowhere else: a wireframe of the domain faces was tried and dropped, its foreshortened lines in front of the cut confused more than they informed. |
Thin wires |
Tubes in the wire colour. |
Discrete ports, lumped elements |
Tubes between their two end points (red for ports, green for elements), with their name beside them. |
Waveguide ports |
A translucent red window on the domain face they occupy — the declared sub-window, or the whole face — with the port name written in the window’s plane. |
Symmetry planes |
Tinted sheets on the declared symmetry planes ( |
Domain box |
Outline of the computational domain — the grid extent when a mesh is given, including the absorbing buffer at open faces; the bounds of the solids and the field otherwise. |
Lengths are shown in millimetres (scale_mm=False for metres). The
projection is parallel, as in engineering drawings.
The cutting plane#
The cut is axis-aligned: a normal (x, y or z), a position
along that axis, and a side to remove. In the widget it lives in the
toolbar:
Cut —
off,x,y,z;the position slider across the domain extent;
Flip — remove the other half;
undo (last change) and reset (initial state);
Show — a menu of the object groups (solids, grid on cut, ports, lumped elements, wires, labels, symmetry planes, domain box); untick a group to hide it.
Before the cut controls sit the camera buttons: reset, isometric view,
a view along x, y or z, and the projection toggle (parallel, as the
scene opens, or perspective). Then a ruler (a measured box around
the scene, its axes titled in the display unit), a screenshot of
the view as it stands, an HTML export — the scene as a standalone
page that opens without a kernel — a pop-out button that opens the
same view in a browser tab of its own (size= sets the height of the
widget in the notebook), and help, a dialog listing every control
and the mouse bindings.
A single plane cuts every solid; the openings are capped, so a cut metal body reads as solid metal, not as a hollow shell. With a mesh, the cells the cut exposes are laid over the cut faces as a translucent sheet — the caps stay visible through it. The features follow the cut: a wire is clipped with the solids, and a port, element or label in the removed half disappears with it.
cut=("y", 0.0) sets the initial state of the plane (and flip=True
the side); it is the only way to place the plane for a screenshot, and
the way a tutorial fixes the picture it wants.
Why not a plane grabbed and turned freely in 3D? A FIT grid carries information only on its own planes — an oblique cut through cells shows triangles that mean nothing — and a 3D handle competes with the camera for the mouse. Axis-aligned, slider-driven cutting planes are also what users of commercial EM suites expect.
Fields on the cut#
The same viewer shows a field: a monitor’s recording, or a
FieldState such as an eigenmode. The cell
layer the cut exposes is laid over the cut as a coloured sheet, and the
position slider walks that layer through the recorded volume.
monitor.show() # |E| with arrows, mid-plane of the region
monitor.show("Ez", normal="y", position=0.0) # one signed component, diverging colours
monitor.show(geometry=model, mesh=mesh) # with the solids; metal cut out of the sheet
pattern.show(f=10e9, phase=90.0) # a frequency monitor at a phase
eigen.show("H", geometry=model) # an eigenmode result: a slider over the modes
eigen.show(frame=2, glyph="cone") # starting at mode 2, cones instead of arrows
Item |
Appearance |
|---|---|
Field sheet |
The exposed layer of cells, each coloured by the magnitude of |
Vectors |
For |
Metal |
With |
Grid |
With |
Symmetry |
With |
Lines and points |
A monitor of one cell along two or three axes shows its row of cells, or its one cell, with the arrows on it. |
The field controls sit in a second toolbar row: a play button
and a frame slider (time, frequency or mode, the value beside it
in a readout of fixed width), a play button and a phase slider
for complex data (a frequency monitor’s pattern at Re(F·e^{+jφ}) —
the instant at ωt = φ, so the phase advances with time and a
travelling wave runs the way it ran in the simulation; the play turns
the phase in steps of ten degrees), a Field selector
that switches between the recorded components, and the level and
density sliders of the volume representations below; Field on cut
and Vectors on cut join the Show menu. Play runs the frames in a
loop at fps= (default 4) — each frame is one layer computed and sent
to the browser, so the rate is bounded by the size of the layer. In a
script or a documentation build the initial frame is chosen with t=,
f= or frame=.
An eigenmode result shows the same way, result.show(): its modes are
the frames, labelled with index and eigenfrequency (degenerate pairs
share the frequency, so the index leads), the amplitudes in arbitrary
units; a complex Bloch mode is turned to the instant of its maximum
energy first, the phase slider turns it from there.
Fields in the volume#
The cut shows one layer; the volume behind it can carry the field too, in the two forms every 3D field plot offers.
monitor.show(volume="arrows") # arrows on a 3D lattice over the kept half
monitor.show(volume="isosurface", iso_level=0.4) # |E| at 40 % of the ceiling
monitor.show("Hx", volume="both", levels=[2.0]) # ±2 A/m surfaces, arrows too
Item |
Appearance |
|---|---|
Field vectors |
The same arrows as on the cut, on an even 3D lattice over the region ( |
Isosurfaces |
Translucent surfaces where the magnitude equals a level — by default one at |
Both are entries of the Show menu whenever the source is a volume
(isosurfaces need at least two cells along every axis, so a plane
monitor offers arrows only); volume= only chooses what is on at
first. Without a cut (Cut set to off) the whole region is drawn.
A volume representation costs the whole region per frame instead of
one layer — a large monitor plays slower with it on. The cell values
are interpolated to the grid nodes before the surfaces are contoured,
so a surface is as fine as the grid. Volume rendering with an opacity
ramp is not offered: the browser renderer takes only uniform image
data, and a graded grid resampled onto one would lose its resolution.
One thing the picture is not: a plane. Every value is the cell-centre average of the staggered components in one layer of cells, the same convention as the 2D slice plots.
A monitor read back from a project store (project.monitors[...])
shows the same way; its frames are read from disk one at a time as the
slider moves, so a volume monitor of any size opens at once.
Rendering modes#
|
Where the picture is rendered |
When to use it |
|---|---|---|
|
In the browser (vtk.js). The scene is sent once; orbit, pan and zoom cost nothing on the kernel side. Needs no OpenGL in the kernel. |
Everyday use. |
|
In the kernel (VTK); images are streamed to the browser. |
Scenes too large for the browser (many millions of triangles); needs OpenGL in the kernel. |
|
Both, with a toggle in the toolbar. |
Comparing the two. |
|
A screenshot embedded in the notebook. |
Notebooks meant to be read without a kernel. |
|
Not shown; the |
Scripts that want |
The mouse in the notebook widget: left drag orbits, middle drag (or alt + left drag) pans, right drag, the wheel or ctrl + left drag zoom, alt + shift + left drag rolls. The rotatable views in these documentation pages use vtk.js’s own bindings instead: shift + left drag pans there, alt + left drag zooms. The toolbar’s help button lists the bindings of the widget.
Requirements#
The viewer is built on PyVista, which is a core dependency. The notebook widget additionally needs the trame stack:
pip install "magnelio[jupyter]"
# or, with conda-forge:
conda install trame trame-vtk trame-vuetify nest-asyncio2
Without it the view falls back to a static image and says so. The
widget talks to the kernel over its own websocket on localhost; on a
remote JupyterHub set PYVISTA_TRAME_JUPYTER_MODE as described in the
PyVista documentation.
The first plot() in a kernel starts that websocket server on the
kernel’s own event loop and fills the cell’s output the moment the
server is up — a fraction of a second after the cell returns when you
run cells by hand. Under Run All the cells queued behind it hold the
loop, so the view appears once they have run. Nothing is nested into
the running loop, so Run All works like running the cells one by one;
nest-asyncio2 is what PyVista’s own first-call path uses and is not
needed for Magnelio’s viewer.
Limitations#
Names are flat 3D text placed in the scene (the browser renderer has no screen-space labels): port names lie in the port plane, element names face the initial camera; both scale with the model.
The cutting plane is axis-aligned by design (see above).
Fields are drawn on the cut and, as arrows and isosurfaces, in the volume — not as a volume rendering; the ParaView export covers that.
The camera presets do not turn the flat labels: they face the camera the view opened with.