skills/01-physics/mayavi-3d-viz/SKILL.md
Use this Skill for 3D scientific visualization with Mayavi: vector fields, isosurfaces, volume rendering, and animated 3D plots for physics data.
npx skillsauth add xjtulyc/awesome-rosetta-skills mayavi-3d-vizInstall this skill globally with one command. Works with Claude Code, Cursor, and Windsurf.
3 of 9 scanners reported clean
Some scanners were skipped, did not run, or reported a non-clean status. Review each row below.
One-line summary: Create publication-quality 3D scientific visualizations — vector fields, isosurfaces, streamlines, and volume rendering — using Mayavi and VTK.
Trigger keywords: Mayavi, VTK, 3D visualization, isosurface, vector field, streamlines, volume rendering, mlab, 3D plot, scientific visualization, scalar field
Mayavi is built on VTK (Visualization Toolkit) and provides a Python API (mlab) for rapid 3D visualization:
| Data Type | Mayavi Module |
|:----------|:-------------|
| Scalar field (volume) | mlab.contour3d, mlab.volume_slice |
| Vector field | mlab.quiver3d, mlab.flow |
| Isosurface | mlab.contour3d |
| Streamlines | mlab.flow |
| Point cloud | mlab.points3d |
For server/CI environments without a display:
from mayavi import mlab
mlab.options.offscreen = True
# Install via conda (recommended — VTK binary wheels)
conda install -c conda-forge mayavi
# Or via pip (requires VTK)
pip install mayavi>=4.8 numpy>=1.24 scipy>=1.11 matplotlib>=3.7
# On headless servers, also install:
pip install pyopengl
export DISPLAY=:0 # or use xvfb-run on Linux
import numpy as np
from mayavi import mlab
# Test off-screen rendering
mlab.options.offscreen = True
fig = mlab.figure(bgcolor=(1, 1, 1), size=(400, 400))
x, y, z = np.mgrid[-2:2:20j, -2:2:20j, -2:2:20j]
s = np.exp(-(x**2 + y**2 + z**2))
mlab.contour3d(s, contours=5, opacity=0.5)
mlab.savefig("test_mayavi.png")
mlab.close()
print("Mayavi off-screen rendering: OK")
import numpy as np
from mayavi import mlab
mlab.options.offscreen = True # Remove for interactive mode
# ------------------------------------------------------------------ #
# Visualize a 3D Gaussian scalar field with isosurfaces
# ------------------------------------------------------------------ #
N = 60
x, y, z = np.mgrid[-3:3:N*1j, -3:3:N*1j, -3:3:N*1j]
# Anisotropic Gaussian
sigma_x, sigma_y, sigma_z = 1.0, 1.5, 0.8
field = np.exp(-(x**2/(2*sigma_x**2) + y**2/(2*sigma_y**2) + z**2/(2*sigma_z**2)))
# ----- Figure 1: Isosurfaces ------------------------------------ #
fig1 = mlab.figure(bgcolor=(0.1, 0.1, 0.1), size=(800, 600))
# Multiple isosurfaces at different levels
for level, color, opacity in [
(0.8, (1.0, 0.2, 0.2), 0.9), # Inner (red)
(0.5, (0.2, 0.8, 0.2), 0.5), # Middle (green)
(0.2, (0.2, 0.2, 1.0), 0.2), # Outer (blue)
]:
mlab.contour3d(x, y, z, field, contours=[level],
color=color, opacity=opacity)
mlab.colorbar(title="Field value", orientation='vertical')
mlab.axes(xlabel='X', ylabel='Y', zlabel='Z')
mlab.title("3D Gaussian — Isosurfaces", size=0.3)
mlab.view(azimuth=45, elevation=60, distance=12)
mlab.savefig("scalar_field_isosurfaces.png", magnification=2)
print("Saved: scalar_field_isosurfaces.png")
# ----- Figure 2: Volume slices ---------------------------------- #
fig2 = mlab.figure(bgcolor=(0.95, 0.95, 0.95), size=(800, 600))
mlab.pipeline.volume(mlab.pipeline.scalar_field(x, y, z, field))
mlab.axes(xlabel='X', ylabel='Y', zlabel='Z')
mlab.view(azimuth=30, elevation=70, distance=14)
mlab.savefig("scalar_field_volume.png", magnification=2)
mlab.close(all=True)
print("Saved: scalar_field_volume.png")
import numpy as np
from mayavi import mlab
mlab.options.offscreen = True
# ------------------------------------------------------------------ #
# Visualize magnetic dipole field: B = (3(m·r̂)r̂ - m) / r³
# ------------------------------------------------------------------ #
N = 15
x, y, z = np.mgrid[-3:3:N*1j, -3:3:N*1j, -3:3:N*1j]
# Magnetic moment along z-axis
m = np.array([0, 0, 1])
eps = 0.3 # Avoid singularity at origin
r = np.sqrt(x**2 + y**2 + z**2) + eps
r_hat_x = x / r
r_hat_y = y / r
r_hat_z = z / r
# m · r̂
m_dot_r = m[2] * r_hat_z # Only z-component of m is non-zero
# B = (3(m·r̂)r̂ - m) / r³
Bx = (3 * m_dot_r * r_hat_x - m[0]) / r**3
By = (3 * m_dot_r * r_hat_y - m[1]) / r**3
Bz = (3 * m_dot_r * r_hat_z - m[2]) / r**3
B_mag = np.sqrt(Bx**2 + By**2 + Bz**2)
fig = mlab.figure(bgcolor=(0.05, 0.05, 0.15), size=(900, 700))
# Quiver plot (arrows) — subsample to avoid clutter
step = 2
mlab.quiver3d(
x[::step, ::step, ::step],
y[::step, ::step, ::step],
z[::step, ::step, ::step],
Bx[::step, ::step, ::step],
By[::step, ::step, ::step],
Bz[::step, ::step, ::step],
scalars=B_mag[::step, ::step, ::step],
mode='arrow',
scale_factor=0.3,
colormap='jet',
)
# Add magnetic moment marker (dipole location)
mlab.points3d(0, 0, 0, scale_factor=0.3, color=(1, 1, 0)) # Yellow dot
# Field lines (streamlines seeded on circle in xy-plane)
seed_x = np.cos(np.linspace(0, 2*np.pi, 12)) * 1.0
seed_y = np.sin(np.linspace(0, 2*np.pi, 12)) * 1.0
seed_z = np.zeros(12)
field_src = mlab.pipeline.vector_field(x, y, z, Bx, By, Bz, scalars=B_mag)
streamlines = mlab.pipeline.streamline(
field_src,
seedtype='point',
seed_resolution=0,
integration_direction='both',
linetype='tube',
tube_radius=0.05,
)
mlab.colorbar(title="|B| (a.u.)", orientation='vertical')
mlab.axes(xlabel='X', ylabel='Y', zlabel='Z')
mlab.title("Magnetic Dipole Field", size=0.3)
mlab.view(azimuth=45, elevation=55, distance=14)
mlab.savefig("vector_field_dipole.png", magnification=2)
mlab.close(all=True)
print("Saved: vector_field_dipole.png")
import numpy as np
from mayavi import mlab
mlab.options.offscreen = True
# ------------------------------------------------------------------ #
# Potential flow: flow past a sphere using superposition
# Uniform flow U₀ + doublet (Stokes stream function)
# ------------------------------------------------------------------ #
N = 25
x, y, z = np.mgrid[-3:3:N*1j, -3:3:N*1j, -3:3:N*1j]
U0 = 1.0 # Free stream velocity
R = 0.8 # Sphere radius
r = np.sqrt(x**2 + y**2 + z**2) + 1e-6
# Potential flow velocity components
# u = U0(1 - R³/r³ * (1 - 3x²/r²) * ...)
# Simplified for irrotational sphere flow:
factor = R**3 / r**5
u = U0 * (1 - factor * (r**2 - 3*x**2))
v = U0 * (3 * factor * x * y)
w = U0 * (3 * factor * x * z)
# Mask inside sphere
inside = r < R
u[inside] = 0; v[inside] = 0; w[inside] = 0
velocity_mag = np.sqrt(u**2 + v**2 + w**2)
fig = mlab.figure(bgcolor=(0.1, 0.15, 0.2), size=(1000, 700))
# Sphere surface
sphere_phi, sphere_theta = np.mgrid[0:np.pi:50j, 0:2*np.pi:50j]
sx = R * np.sin(sphere_phi) * np.cos(sphere_theta)
sy = R * np.sin(sphere_phi) * np.sin(sphere_theta)
sz = R * np.cos(sphere_phi)
mlab.mesh(sx, sy, sz, color=(0.7, 0.7, 0.9), opacity=0.8)
# Streamlines seeded on yz-plane upstream
src_y = np.linspace(-2, 2, 8)
src_z = np.linspace(-2, 2, 8)
SY, SZ = np.meshgrid(src_y, src_z)
SX = -2.5 * np.ones_like(SY)
field = mlab.pipeline.vector_field(x, y, z, u, v, w, scalars=velocity_mag)
stream = mlab.pipeline.streamline(
field,
seedtype='plane',
integration_direction='forward',
linetype='tube',
tube_radius=0.03,
colormap='cool',
)
stream.seed.widget.origin = np.array([-2.5, -2.0, -2.0])
stream.seed.widget.point1 = np.array([-2.5, 2.0, -2.0])
stream.seed.widget.point2 = np.array([-2.5, -2.0, 2.0])
stream.seed.widget.resolution = 7
mlab.colorbar(title="|u| (m/s)", orientation='vertical')
mlab.axes(xlabel='X', ylabel='Y', zlabel='Z')
mlab.title("Potential Flow Past a Sphere", size=0.25)
mlab.view(azimuth=20, elevation=70, distance=14)
mlab.savefig("streamlines_sphere.png", magnification=2)
mlab.close(all=True)
print("Saved: streamlines_sphere.png")
import numpy as np
from mayavi import mlab
import os
mlab.options.offscreen = True
# Create animated PNG sequence for a time-evolving field
N = 40
x, y, z = np.mgrid[-3:3:N*1j, -3:3:N*1j, -3:3:N*1j]
os.makedirs("frames", exist_ok=True)
for frame, t in enumerate(np.linspace(0, 2*np.pi, 36)):
field = np.sin(x + t) * np.cos(y - t/2) * np.exp(-z**2/4)
fig = mlab.figure(bgcolor=(0.0, 0.0, 0.0), size=(600, 600))
mlab.contour3d(x, y, z, field, contours=4, colormap='RdBu',
opacity=0.6, vmin=-1, vmax=1)
mlab.axes()
mlab.view(azimuth=frame * 10, elevation=60, distance=15)
mlab.savefig(f"frames/frame_{frame:03d}.png")
mlab.close()
print("Frames saved. Combine with: ffmpeg -r 12 -i frames/frame_%03d.png animation.mp4")
No display name and no $DISPLAY environment variableCause: Headless server, no X11 display.
Fix:
# Linux: use xvfb virtual display
pip install pyopengl
Xvfb :1 -screen 0 1024x768x24 &
export DISPLAY=:1
# OR use EGL offscreen (no Xvfb needed)
export ETS_TOOLKIT=null
from mayavi import mlab
mlab.options.offscreen = True # Must set BEFORE any mlab calls
tvtk or traitspip install traits traitsui pyface envisage
fig = mlab.figure(size=(1600, 1200)) # Higher resolution
mlab.savefig("output.png", magnification=3) # Anti-aliased output
| Package | Tested versions | Notes | |:--------|:----------------|:------| | mayavi | 4.8.x | Requires VTK ≥ 9.0 | | vtk | 9.0, 9.2 | Install via conda for prebuilt wheels | | traits | 6.x | API stable |
import numpy as np
from mayavi import mlab
mlab.options.offscreen = True
N = 20
x, y, z = np.mgrid[-3:3:N*1j, -3:3:N*1j, -3:3:N*1j]
k_e = 1.0 # Normalized Coulomb constant
def point_charge_field(qx, qy, qz, q, x, y, z):
"""Electric field of a point charge at (qx,qy,qz)."""
dx, dy, dz = x - qx, y - qy, z - qz
r3 = (dx**2 + dy**2 + dz**2 + 0.1)**1.5
return k_e * q * dx/r3, k_e * q * dy/r3, k_e * q * dz/r3
# Dipole: +1 at x=+1, -1 at x=-1
Ex1, Ey1, Ez1 = point_charge_field( 1, 0, 0, +1, x, y, z)
Ex2, Ey2, Ez2 = point_charge_field(-1, 0, 0, -1, x, y, z)
Ex = Ex1 + Ex2
Ey = Ey1 + Ey2
Ez = Ez1 + Ez2
E_mag = np.sqrt(Ex**2 + Ey**2 + Ez**2)
fig = mlab.figure(bgcolor=(0.05, 0.05, 0.1), size=(800, 600))
mlab.quiver3d(x[::2,::2,::2], y[::2,::2,::2], z[::2,::2,::2],
Ex[::2,::2,::2], Ey[::2,::2,::2], Ez[::2,::2,::2],
scalars=E_mag[::2,::2,::2], mode='arrow',
scale_factor=0.4, colormap='hot')
mlab.points3d([ 1, -1], [0, 0], [0, 0],
[1, -1], scale_factor=0.3, colormap='bwr', vmin=-1, vmax=1)
mlab.colorbar(title="|E| (a.u.)")
mlab.title("Electric Dipole Field")
mlab.view(azimuth=20, elevation=70, distance=12)
mlab.savefig("electric_dipole.png", magnification=2)
mlab.close(all=True)
import numpy as np
from mayavi import mlab
from scipy.special import sph_harm
from scipy.special import genlaguerre, factorial
mlab.options.offscreen = True
N = 60
# Hydrogen-like wavefunction: ψ_{nlm} = R_{nl}(r) * Y_l^m(θ,φ)
x, y, z = np.mgrid[-10:10:N*1j, -10:10:N*1j, -10:10:N*1j]
r = np.sqrt(x**2 + y**2 + z**2) + 1e-8
theta = np.arccos(z / r)
phi = np.arctan2(y, x)
# n=2, l=1, m=0 (2p_z orbital)
n, l, m_q = 2, 1, 0
a0 = 1.0 # Bohr radius (normalized)
# Radial part R_{21}: (1/sqrt(24)) * (r/a0) * exp(-r/(2a0))
R21 = (1/np.sqrt(24)) * (r/a0) * np.exp(-r/(2*a0))
# Spherical harmonic Y_1^0
Y10 = sph_harm(m_q, l, phi, theta).real
psi = R21 * Y10
prob_density = np.abs(psi)**2
fig = mlab.figure(bgcolor=(0.0, 0.0, 0.05), size=(800, 700))
# Isosurface at 50% of maximum
threshold = 0.5 * prob_density.max()
mlab.contour3d(x, y, z, prob_density, contours=[threshold],
colormap='hot', opacity=0.7)
mlab.axes(xlabel='x/a₀', ylabel='y/a₀', zlabel='z/a₀')
mlab.title("H atom 2p_z orbital |ψ|²", size=0.3)
mlab.view(azimuth=30, elevation=70, distance=30)
mlab.savefig("hydrogen_2pz.png", magnification=2)
mlab.close(all=True)
print("Saved: hydrogen_2pz.png")
Last updated: 2026-03-17 | Maintainer: @xjtulyc Issues: GitHub Issues
tools
R research package development with devtools, roxygen2 documentation, testthat testing, CRAN submission, and vignette creation for statistical methods.
development
Reproducible research reporting with Quarto covering parameterized reports, multi-format output, inline computation, and journal article templates.
development
Python research package development with pyproject.toml, testing with pytest, documentation with Sphinx, and publishing to PyPI for academic software.
development
Write, compile, and submit LaTeX papers: IMRaD structure, key packages, bibliography management, arXiv preparation, and common error fixes.