Skip to content

Plotly 3D visualization for plot3D - #3310

Merged
stevengj merged 1 commit into
NanoComp:masterfrom
oskooi:plot3D_plotly
Sep 16, 2026
Merged

stevengj merged 1 commit into
NanoComp:masterfrom
oskooi:plot3D_plotly

Conversation

@oskooi

@oskooi oskooi commented Sep 13, 2026

Copy link
Copy Markdown
Collaborator

What is added

plot3D gains a second rendering backend that produces an interactive viewer runnable in a browser — rotate (left-drag), pan (right-drag), zoom (scroll) — instead of only a native OpenGL desktop window.

sim.plot3D(html_name="sim.html")    # self-contained file, openable or <iframe>-embeddable
sim.plot3D(backend="html")          # inline under Jupyter, else a new browser tab
sim.plot3D()                        # unchanged: native vispy window
sim.plot3D(save_to_image=True)      # unchanged: vispy render → PNG

Each material isosurface, the sources, the monitors, the PML, and the cell get a legend entry that toggles visibility (see example below). Boxes of one class share a single entry, so many monitors don't produce a wall of legend rows; hover still identifies them individually.

Why both backends

They fail in opposite directions, so neither replaces the other. vispy renders natively on the GPU and handles the million-triangle meshes a high-resolution cell produces, but its only browser path (jupyter_rfb) streams pixels from a live kernel and can't make a static page. Plotly serializes every vertex into the HTML, so mesh size is bounded by document size, but it's the only option that yields something you can email or commit to docs. save_to_image stays on vispy; plotly's static export needs Kaleido and loses the interaction that's the whole point.

Implementation

All of it is in python/visualization.py, with a thin wrapper at python/simulation.py:5151.

plot3D (:1520) dispatches on backend; html_name implies backend="html". The vispy body below the dispatch is untouched, so no existing behavior moved.

_epsilon_isosurfaces (:1863) samples get_epsilon_grid and runs marching cubes once per distinct permittivity, skipping the background material. _plot3D_plotly (:1920) turns those into Mesh3d traces and builds the sources, monitors, PML, and cell from _box_mesh_trace / _box_wireframe_trace (:1799, :1834), which expand a shared unit-cube vertex/face/edge table (:1737).

Four decisions worth recording:

PML is drawn as a wireframe, not the translucent green solid the native backend uses. Six nested translucent slabs is precisely where plotly's per-trace depth sorting breaks down. Material isosurfaces stay translucent via opacity (default 0.85); pass 1.0 if overlapping materials render wrong.

The sampling grid is capped at max_grid_points (default 96³) rather than always using sim.resolution, shrinking all three axes by a common factor to preserve aspect ratio. Without it a resolution-40 cell produces a document too large to open.

Coordinates are physical. The isosurface uses spacing extent/(N-1) and offsets by the true origin. The vispy path uses cell_size/N over a grid of N points, which shrinks the geometry ~2.4% relative to the cell box drawn around it on the 4×4×4 test cell. vispy is left alone rather than changing its output as a side effect.

Shades are bounded to [0.2, 0.7]. The native formula 1 - (i+1)/n sends the highest-index material to exactly black, and an unlit black surface shows no shading at all.

Test coverage

test_plot3D_html and test_plot3D_bad_arguments in python/tests/test_visualization.py check the trace inventory, legend grouping, that every vertex lies inside the cell, the grid cap, self-contained HTML output, and the three error paths. They skip if plotly or scikit-image are absent, and run headless (i.e., no display attached — no X11/Wayland session, no window manager, no monitor. Just a process and a shell.) — unlike the native backend, which still has no test.

Note: the plotly backend never draws anything in Python. It assembles trace data and layout into a figure object and serializes it to JSON inside an HTML document. That's pure data manipulation, so it runs anywhere, and the test can assert on trace names, vertex bounds, and the document's contents without a pixel existing. This is the same principle as matplotlib.use("agg") at the top of the test file — a rasterize-to-memory backend rather than a windowed one.

Example

The example is a Si photonic-crystal nanobeam (ε=11.9) with six air holes on an SiO₂ substrate (ε=2.1), one source plane, two flux monitors, and PML on all six sides. Script is here. The output is an HTML file from which the following two screenshots are obtained.

holey_waveguide_plot3D holey_waveguide_plot3D_2

@stevengj
stevengj merged commit 0a4e71f into NanoComp:master Sep 16, 2026
6 checks passed
@oskooi
oskooi deleted the plot3D_plotly branch September 16, 2026 21:35
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants