Plotly 3D visualization for plot3D - #3310
Merged
Merged
Conversation
stevengj
approved these changes
Sep 16, 2026
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
What is added
plot3Dgains 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.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_imagestays 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 atpython/simulation.py:5151.plot3D(:1520) dispatches onbackend;html_nameimpliesbackend="html". The vispy body below the dispatch is untouched, so no existing behavior moved._epsilon_isosurfaces(:1863) samplesget_epsilon_gridand runs marching cubes once per distinct permittivity, skipping the background material._plot3D_plotly(:1920) turns those intoMesh3dtraces 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); pass1.0if overlapping materials render wrong.The sampling grid is capped at
max_grid_points(default 96³) rather than always usingsim.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 usescell_size/Nover a grid ofNpoints, 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)/nsends the highest-index material to exactly black, and an unlit black surface shows no shading at all.Test coverage
test_plot3D_htmlandtest_plot3D_bad_argumentsinpython/tests/test_visualization.pycheck 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.