spin_render.py User Guide

Use scripts/spin_render.py to render a rotating camera animation of a VTK dataset and write a PNG sequence suitable for stitching into a movie.

What it does

  • Loads a VTU/VTP/VTI/VTR dataset and orbits the camera around its bounds.
  • Colors by a chosen point-data scalar (default log_field_value) using a ParaView colormap preset (default Fast).
  • Writes numbered PNG frames that can be stitched into an MP4 with ffmpeg.
  • Optionally animates elevation and zoom during the rotation.

Requirements

  • ParaView pvpython (e.g., /Applications/ParaView-6.0.1.app/Contents/bin/pvpython).
  • A readable VTK file (VTU/VTP/VTI/VTR).
  • ffmpeg (optional, for MP4 stitching).

To keep pvpython on PATH whenever you activate the conda environment:

mkdir -p "$CONDA_PREFIX/etc/conda/activate.d" "$CONDA_PREFIX/etc/conda/deactivate.d"

cat > "$CONDA_PREFIX/etc/conda/activate.d/paraview_path.sh" <<'EOF'
export _PV_OLD_PATH="$PATH"
export PATH="/usr/bin:/bin:/usr/sbin:/sbin:/Applications/ParaView-6.0.1.app/Contents/bin:$PATH"
EOF

cat > "$CONDA_PREFIX/etc/conda/deactivate.d/paraview_path.sh" <<'EOF'
export PATH="$_PV_OLD_PATH"
unset _PV_OLD_PATH
EOF

CLI

pvpython scripts/spin_render.py \
  --input <INPUT_VTK> \
  --output <OUTPUT_PATH> \
  [--resolution WIDTH HEIGHT] \
  [--background white|black] \
  [--frames N] \
  [--elev DEG] \
  [--zoom Z] \
  [--radius-scale R] \
  [--fps FPS] \
  [--scalar NAME] \
  [--range-min MIN --range-max MAX] \
  [--percentile-range PLOW PHIGH] \
  [--colormap PRESET] \
  [--animate-elev-zoom] \
  [--loops N] \
  [--slow-elev-amp DEG] \
  [--slow-zoom-amp Z] \
  [--slow-phase RAD] \
  [--azimuth-offset DEG] \
  [--representation surface|points] \
  [--point-size N] \
  [--overwrite]

Key options

  • --input – VTK file to render.
  • --output – Output path used as the PNG prefix; the script writes <OUTPUT_BASE>_0000.png, <OUTPUT_BASE>_0001.png, …
  • If --output ends with .mp4, the script also stitches the PNGs into that movie using ffmpeg.
  • --resolution – Output resolution in pixels (e.g., 1920 1080).
  • --background – Background color (white or black).
  • --frames – Number of frames (default 180).
  • --elev – Camera elevation in degrees for the orbit (default 20).
  • --zoom – Zoom factor baked into the orbit radius (default 1.2).
  • --radius-scale – Orbit radius scale as a fraction of dataset extent.
  • --fps – FPS for MP4 stitching (default 30).
  • --scalar – Point-data array to color by (default log_field_value).
  • --range-min/--range-max – Fixed color range. If omitted, auto-range is used.
  • --percentile-range – Clamp the colormap to the given percentiles (e.g., 1 99). Mutually exclusive with fixed range.
  • --colormap – ParaView preset name (default Fast).
  • --overwrite – Overwrite the MP4 if it already exists.
  • --animate-elev-zoom – Enables smooth sinusoidal elevation and zoom modulation.
  • --loops – Number of full rotations.
  • --slow-elev-amp/--slow-zoom-amp – Slow modulation amplitude across the full movie.
  • --slow-phase – Phase offset (radians) for slow modulation.
  • --azimuth-offset – Starting azimuth offset in degrees.
  • --representation – Render as surface or points.
  • --point-size – Point size when --representation points is used.

Animated elevation + zoom

pvpython scripts/spin_render.py \
  --input <INPUT_VTK> \
  --output <OUTPUT_PATH> \
  --frames 180 \
  --animate-elev-zoom \
  --elev-start 20 --elev-peak 60 \
  --zoom-start 0.6 --zoom-peak 2.0

This animates elevation and zoom smoothly (sinusoidal) while the camera continues to rotate.

Multi-loop variation (seamless)

pvpython scripts/spin_render.py \
  --input <INPUT_VTK> \
  --output <OUTPUT_PATH> \
  --frames 450 \
  --animate-elev-zoom \
  --loops 3 \
  --slow-elev-amp 20 \
  --slow-zoom-amp 0.5 \
  --slow-phase 0.6

Slow modulation produces different loops while returning to the starting position at the end for seamless playback.

Outputs

  • <OUTPUT_BASE>_0000.png<OUTPUT_BASE>_####.png – PNG frames of the rotation.

Stitch frames to MP4

ffmpeg -framerate 30 \
  -i <OUTPUT_BASE>_%04d.png \
  -c:v libx264 -pix_fmt yuv420p \
  <OUTPUT_BASE>.mp4

Example

/Applications/ParaView-6.0.1.app/Contents/bin/pvpython scripts/spin_render.py \
  --input outputs/quijote_batches/crop_x500-1000_y500-1000_z0-100/crop_x500-1000_y500-1000_z0-100_s5_manifolds_JE1a_S020.vtu \
  --output outputs/quijote_batches/crop_x500-1000_y500-1000_z0-100/spin.mp4 \
  --frames 180 \
  --scalar log_field_value \
  --colormap "Inferno (matplotlib)" \
  --range-min -3 --range-max 1 \
  --resolution 1920 1080 \
  --background black

Example (multi-loop, smooth modulation)

pvpython scripts/spin_render.py \
  --input outputs/combined_for_video.vtu \
  --output outputs/combined_for_video_spin.mp4 \
  --scalar log_field_value \
  --colormap "Inferno (matplotlib)" \
  --range-min -3 \
  --range-max 1 \
  --resolution 1920 1080 \
  --background black \
  --frames 1800 \
  --fps 30 \
  --animate-elev-zoom \
  --loops 3 \
  --azimuth-offset=-120 \
  --elev-start=-40 \
  --elev-peak 80 \
  --zoom-start 0.8 \
  --zoom-peak 2.0 \
  --radius-scale 0.6 \
  --slow-elev-amp 20 \
  --slow-zoom-amp 0.5 \
  --slow-phase 0.6 \
  --overwrite

More Examples

pvpython scripts/spin_render.py \
  --input outputs/movie_loop_4/combined_components_4_6.vtu \
  --output outputs/movie_loop_4/combined_components_4_6_spin.mp4 \
  --scalar log_field_value \
  --colormap "Inferno (matplotlib)" \
  --range-min -4 \
  --range-max 4 \
  --resolution 1920 1080 \
  --background black \
  --frames 1800 \
  --fps 30 \
  --animate-elev-zoom \
  --loops 3 \
  --azimuth-offset=-120 \
  --elev-start=-40 \
  --elev-peak 80 \
  --zoom-start 0.8 \
  --zoom-peak 2.0 \
  --radius-scale 0.6 \
  --slow-elev-amp 20 \
  --slow-zoom-amp 0.5 \
  --slow-phase 0.6 \
  --point-size 4 \
  --line-width 2 \
  --overwrite
/Users/fules/Documents/disperse/outputs/movie_loop_4/four_layers_4_6.vtm
pvpython scripts/spin_render_layers.py \
  --input outputs/movie_loop_4/four_layers_4_6 \
  --output outputs/movie_loop_4/four_layers_4_6.mp4  \
  --scalar log_field_value \
  --colormap "Inferno (matplotlib)" \
  --range-min -4 \
  --range-max 4 \
  --resolution 1920 1080 \
  --background black \
  --frames 180 \
  --fps 30 \
  --animate-elev-zoom \
  --loops 5 \
  --azimuth-offset=-120 \
  --elev-start=-40 \
  --elev-peak 80 \
  --zoom-start 0.8 \
  --zoom-peak 2.0 \
  --radius-scale 0.6 \
  --slow-elev-amp 20 \
  --slow-zoom-amp 0.5 \
  --slow-phase 0.6 \
  --point-size 4 \
  --line-width 2 \
  --overwrite

pvpython scripts/spin_render.py \
  --input outputs/movie_loop/particle_cube.vtu \
  --output outputs/movie_loop/particle_cube.mp4 \
  --scalar log_field_value \
  --colormap "Inferno (matplotlib)" \
  --range-min -3 \
  --range-max 1 \
  --resolution 1920 1080 \
  --background black \
  --frames 1800 \
  --fps 30 \
  --animate-elev-zoom \
  --loops 1 \
  --azimuth-offset=-120 \
  --elev-start=-40 \
  --elev-peak 80 \
  --zoom-start 0.8 \
  --zoom-peak 2.0 \
  --radius-scale 0.6 \
  --slow-elev-amp 20 \
  --slow-zoom-amp 0.5 \
  --slow-phase 0.6 \
  --representation points \
  --point-size 2.5 \
  --overwrite

Notes

  • Coloring uses point-data arrays. If your scalar is only in cell data, convert to point data before rendering.
  • If colors look washed out, try a tighter range with --range-min/--range-max.
  • If you’re re-running with different settings, change the output prefix or remove old *_####.png frames first.
  • Default colormap is Fast; use --colormap "Inferno (matplotlib)" to match the rest of the pipeline.

redshift_evolution.py User Guide

Use scripts/redshift_evolution.py to generate a smooth movie showing the evolution of the cosmic web across redshift snapshots. Given one Delaunay VTU per redshift, the script linearly interpolates point positions and scalar values between consecutive snapshots and assembles a PNG sequence and MP4.

Must be run via pvpython.

What it does

  • Reads one Delaunay VTU per snapshot (earliest universe first).
  • Optionally aligns particle ordering across snapshots using chained nearest-neighbour matching (via scipy.spatial.cKDTree), so interpolation traces the same physical particle rather than position-index correspondence.
  • Interpolates both coordinates and scalar values linearly between consecutive snapshots.
  • Renders each frame as a point cloud coloured by log_field_value (Inferno colormap, range −3 to 2 by default).
  • Shows a redshift label (e.g. z=3) in the lower-left corner of each frame.
  • Writes numbered PNGs and stitches them into an MP4 via ffmpeg.

Rendering modes

Mode Flag Description
Interpolation (default) Smoothly interpolates positions and scalars between snapshots. Requires scipy.
Cross-fade (automatic fallback) Opacity blending when point counts differ after alignment.
Slideshow --slideshow No interpolation — holds each snapshot for --frames-hold frames then cuts. Skips alignment entirely.

Requirements

  • pvpython (ParaView’s Python interpreter).
  • scipy (for proximity alignment; ships with recent ParaView builds).
  • ffmpeg (optional, for MP4 assembly).

CLI

pvpython scripts/redshift_evolution.py \
  --inputs SNAP_000.vtu SNAP_001.vtu ... \
  --labels "z=3" "z=2" "z=1" "z=0.5" "z=0" \
  --output-dir <OUTPUT_DIR> \
  --output-prefix <PREFIX> \
  [--scalar log_field_value] \
  [--range-min -3.0 --range-max 2.0] \
  [--colormap "Inferno (matplotlib)"] \
  [--frames-per-transition 30] \
  [--frames-hold 10] \
  [--point-size 2.0] \
  [--resolution 1920 1080] \
  [--background black|white] \
  [--fps 30] \
  [--label-position X Y] \
  [--label-font-size 28] \
  [--zoom 1.0] \
  [--no-align] \
  [--slideshow] \
  [--no-mp4]

Key options

  • --inputs — VTU files in time order (earliest redshift first, i.e. 000004).
  • --labels — Redshift label per snapshot, shown in the lower-left corner. Must match --inputs count.
  • --frames-per-transition — Interpolated frames between each pair (default 30).
  • --frames-hold — Frames to pause on each snapshot (default 10).
  • --scalar — Point-data array to colour by (default log_field_value). Use none for solid colour.
  • --range-min/--range-max — Fixed colour range (default −3 / 2).
  • --percentile-range — Derive colour range from percentiles across all snapshots (overrides fixed range).
  • --colormap — ParaView preset name (default Inferno (matplotlib)).
  • --zoom — Zoom factor applied after auto-framing (default 1.0). Values > 1 zoom in (less black border around the data); < 1 zoom out.
  • --no-align — Skip proximity alignment; assume all files share the same point ordering.
  • --slideshow — Cut directly between snapshots with no interpolation or alignment.
  • --no-mp4 — Keep PNGs only, skip ffmpeg.

Example (5 Quijote redshift snapshots, one crop)

pvpython scripts/redshift_evolution.py \
    --inputs \
        outputs/quijote_batches_000_w_clusters_points/crop_x0-500_y0-500_z900-1000/crop_x0-500_y0-500_z900-1000_delaunay_S000.vtu \
        outputs/quijote_batches_001_w_clusters_points/crop_x0-500_y0-500_z900-1000/crop_x0-500_y0-500_z900-1000_delaunay_S000.vtu \
        outputs/quijote_batches_002_w_clusters_points/crop_x0-500_y0-500_z900-1000/crop_x0-500_y0-500_z900-1000_delaunay_S000.vtu \
        outputs/quijote_batches_003_w_clusters_points/crop_x0-500_y0-500_z900-1000/crop_x0-500_y0-500_z900-1000_delaunay_S000.vtu \
        outputs/quijote_batches_004_w_clusters_points/crop_x0-500_y0-500_z900-1000/crop_x0-500_y0-500_z900-1000_delaunay_S000.vtu \
    --labels "z=3" "z=2" "z=1" "z=0.5" "z=0" \
    --scalar log_field_value \
    --range-min -3.0 --range-max 2.0 \
    --frames-per-transition 30 \
    --frames-hold 10 \
    --output-dir outputs/evolution \
    --output-prefix evolution

Slideshow example

pvpython scripts/redshift_evolution.py \
    --inputs \
        outputs/quijote_batches_000_w_clusters_points/crop_x0-500_y0-500_z900-1000/crop_x0-500_y0-500_z900-1000_delaunay_S000.vtu \
        outputs/quijote_batches_001_w_clusters_points/crop_x0-500_y0-500_z900-1000/crop_x0-500_y0-500_z900-1000_delaunay_S000.vtu \
        outputs/quijote_batches_002_w_clusters_points/crop_x0-500_y0-500_z900-1000/crop_x0-500_y0-500_z900-1000_delaunay_S000.vtu \
        outputs/quijote_batches_003_w_clusters_points/crop_x0-500_y0-500_z900-1000/crop_x0-500_y0-500_z900-1000_delaunay_S000.vtu \
        outputs/quijote_batches_004_w_clusters_points/crop_x0-500_y0-500_z900-1000/crop_x0-500_y0-500_z900-1000_delaunay_S000.vtu \
    --labels "z=3" "z=2" "z=1" "z=0.5" "z=0" \
    --scalar log_field_value \
    --range-min -3.0 --range-max 1.0 \
    --slideshow \
    --slideshow-duration 2.0 \
    --output-dir outputs/evolution_slideshow_100 \
    --output-prefix evolution_slideshow_100

Slideshow example (10Mpc slab)

pvpython scripts/redshift_evolution.py \
    --inputs \
        outputs/evolution_slideshow_10/particle_simulation_0_10.vtu \
        outputs/evolution_slideshow_10/particle_simulation_1_10.vtu \
        outputs/evolution_slideshow_10/particle_simulation_2_10.vtu \
        outputs/evolution_slideshow_10/particle_simulation_3_10.vtu \
        outputs/evolution_slideshow_10/particle_simulation_4_10.vtu \
    --labels "z=3" "z=2" "z=1" "z=0.5" "z=0" \
    --scalar log_field_value \
    --range-min -4.0 --range-max 4.0 \
    --resolution 3840 2160 \
    --label-font-size 56 \
    --zoom 1.2 \
    --point-size 3.0 \
    --slideshow \
    --slideshow-duration 2.0 \
    --output-dir outputs/evolution_slideshow_10 \
    --output-prefix evolution_slideshow_10_10_3

Notes

  • The Delaunay VTU files produced by analyze_snapshot.py contain log_field_value (DTFE log-density) and a true_index array — these are the recommended input files.
  • Proximity alignment uses chained nearest-neighbour matching (snapshot i matched to snapshot i−1), so small per-step displacements accumulate correctly across many redshifts. This requires scipy; if unavailable the script falls back to positional ordering with a warning.
  • Total frame count = (N−1) × (frames_hold + frames_per_transition) + frames_hold for interpolation mode, or N frames (one per snapshot) for slideshow mode.
  • Label position is given in normalized viewport coordinates (--label-position X Y); default is 0.45 0.05 (center-bottom). Requires ParaView 6.0+; position is ignored on older builds.