Instant Connection for Pixel Streaming

— New Feature Automated Setup

Guide

Automate ParaView Visualizations with Claude Code on Vagon

Automate ParaView visualizations with Claude Code on Vagon using Python Trace, pvpython, and computer use for repeatable exports and visual checks.

On this page

Producing one velocity slice in ParaView is straightforward. Producing the same view across dozens of simulation timesteps requires consistent field selection, slice position, camera settings, and color limits. If those settings drift, the images become harder to compare, even when every export succeeds.

Claude Code can help turn that repeated work into a reusable visualization script. ParaView’s Python Trace and scripting tools provide the foundation: establish one correct view, capture its setup, then have Claude parameterize the workflow for execution through ParaView’s own Python interpreter.

With Vagon Cloud Computer, you can keep ParaView, Claude Code, simulation files, and exported images in one working environment. Where supported and enabled, computer use can also help with interface tasks and visual inspection, while scripts control the repeated exports.

This guide follows a temporal dataset from an initial inspection to a three-timestep preview and a reusable image sequence. You will also save the ParaView state and check that the visualization can be reopened with its source data.

Define the Visualization Before Automating It

Start with a specific deliverable: velocity-magnitude images on one slice through a time-dependent simulation, exported with the same framing and legend. For this walkthrough, input/flow.pvd will identify the temporal dataset. Keep the data files it references alongside it, preserving their relative paths.

Before asking Claude Code to build the export script, write down the decisions the script must preserve. Some values will come from inspecting the dataset; others require your knowledge of the simulation.

Setting

Decision to record

Field

Actual velocity array name, point or cell association, and magnitude selection

Slice

Plane origin and normal in the dataset’s coordinate system

Time selection

First, middle, and last available timesteps for the preview

Color mapping

Palette, fixed minimum and maximum, and confirmed units

Camera

Position, focal point, view-up direction, and projection settings

Export

Image dimensions, legend formatting, and output directory

The field name alone is insufficient. A velocity magnitude and one directional component answer different questions. Likewise, choosing point data instead of cell data can change what the visualization represents. Record the intended choice explicitly rather than letting Claude select whichever array is easiest to access.

Diagram of two mesh cells with numeric values at their vertices and colors interpolated between the points.

Source: ParaView Documentation

Pay particular attention to ParaView’s color-range rescaling. A range that changes at each timestep can assign the same color to different values. For this comparison, choose a justified range and keep it fixed; inspect whether any values fall outside it.

ParaView Color Map Editor showing the automatic rescale range control and color transfer function.

Source: ParaView Documentation

The acceptance check is practical: every image must show the agreed field, slice, time, and display settings. That establishes a consistent visualization. Assessing whether the simulation itself is physically credible remains a separate engineering task.

Prepare ParaView and Claude Code on Your Vagon Computer

Create a Windows Vagon Cloud Computer and connect to its desktop. Install ParaView from the official distribution if it is not already available, then install Claude Code and sign in with your own account. Vagon supports running Claude Code alongside your applications, so the assistant can work with the same files and executables you use to inspect the visualization.

Choose resources around the dataset you need to load and the filters you intend to run. Leave memory capacity for intermediate results as well as the original data. A GPU can support rendering, but it does not remove memory constraints or make every processing step faster.

Create one project folder with a clear separation between source data and generated files:

  • input/: flow.pvd and its referenced data files, preserving their directory structure.

  • scripts/: the recorded trace and reusable Python scripts.

  • config/: the visualization settings you will establish.

  • outputs/preview/ and outputs/batch/: trial images and the subsequent sequence.

Open that project folder in Claude Code. Keep the source data unchanged and direct generated files into the output folders.

Next, locate pvpython.exe in your ParaView installation. Record its full path and the ParaView version before generating a launch command. ParaView supplies its own Python interpreter for running visualization scripts. Use the interpreter from the same installation as the GUI where you will record the trace. As a first check, have Claude launch that executable and confirm that import paraview.simple succeeds.

ParaView window showing a sphere generated through Python Shell and an autocomplete list of scripting commands.

Source: ParaView Documentation

For optional GUI assistance on Windows, use Claude Code through the Desktop app installed inside the Vagon computer. Anthropic currently documents Desktop computer use as a macOS and Windows research preview requiring a Pro, Max, or Ultra plan. Its CLI computer-use feature is documented for macOS, so installing the Windows CLI alone does not provide that GUI route.

Confirm that Desktop exposes the computer-use option in your session before relying on it. You can perform the interface steps manually and keep the same scripted export workflow.

Inspect Arrays, Units, and Available Timesteps

Give Claude Code an inspection task before asking it to create the slice or render images. The goal is a saved report that connects your visualization specification to the data actually available.

ParaView Information panel showing dataset statistics, array names, bounds, and a list of timesteps.

Source: ParaView Documentation

Use a prompt such as:

Use the recorded pvpython executable to inspect input/flow.pvd. Create scripts/inspect_data.py and save its report as outputs/dataset_report.json. Report the reader type, available timestep values, dataset bounds, point and cell counts, and point/cell array names with component counts. Update the reader at the first, middle, and last available timesteps and report each sample separately. Keep the inputs unchanged. Do not create filters or images, infer units, or select a velocity array without confirmation. Report read errors and missing arrays explicitly.

ParaView exposes data information through its Python interface, including dataset metadata and separate point-data and cell-data array information. Have Claude use those interfaces after updating the reader, rather than relying only on filenames or parsing the PVD header.

Review the time list carefully. The middle preview should use an existing entry in that list, not the arithmetic midpoint between the first and last time values. If fewer than three distinct timesteps exist, use the available ones without duplicating them. An empty time list needs investigation before treating the input as a temporal dataset.

Source: ParaView Documentation

Confirm which reported array represents velocity and whether its components support the intended magnitude visualization. Resolve units from the simulation setup or trustworthy metadata; an array name does not establish them. Dataset bounds also help you choose a slice origin that intersects the domain.

If the selected array disappears at a sampled time, or the reader reports missing referenced files, resolve that issue before exporting. Once the report agrees with the intended field and time selection, use it to complete the visualization settings you will carry into the first reference view.

Capture One Correct View with Python Trace and Computer Use

Build the reference visualization in a fresh ParaView session, using the field and timestep choices confirmed by the inspection report. Start tracing before opening the dataset so the recording includes the reader and pipeline creation.

Python Trace translates relevant interface actions into Python. This gives Claude Code a starting point drawn from your installed ParaView version, including property names and export options.

Follow this sequence:

  1. Choose Tools > Start Trace, then open input/flow.pvd and click Apply. Select the first available timestep recorded in the report.

  2. Select the reader in the Pipeline Browser and add a Slice filter. Set its plane origin and normal from your visualization specification, then apply it. Confirm that the plane intersects the intended region.

  3. Show the slice and hide the original volume or surface if it obscures the result. Color the slice by the confirmed velocity array, association, and magnitude. Set the agreed palette and custom range, with automatic range rescaling disabled.

  4. Position the camera, choose the projection, and format the legend with confirmed units. Hide the slice-plane widget before exporting. Keep the view’s aspect ratio consistent with the intended image dimensions.

  5. Use File > Save Screenshot to export outputs/preview/reference.png at the chosen resolution. Then choose Tools > Stop Trace and save the generated script as scripts/trace.py.

ParaView Edit Color Legend Properties dialog with title, font, label-format, and legend-layout controls.

Source: ParaView Documentation

Computer use can assist with the interface work where it is available. In Claude Desktop inside the Vagon computer, open Settings and enable computer use under This computer > System or Capabilities, depending on the layout. Grant ParaView access when the session requests it, following Anthropic’s computer-use setup.

Give Claude a bounded task rather than asking it to improve the visualization freely:

Inspect the active ParaView slice against our agreed settings. Check the selected array and magnitude, slice origin and normal, legend limits, and camera framing. Report mismatches before changing anything. Do not choose a different field, range, or timestep.

If screen control is unavailable or unreliable in your session, perform those checks manually. The trace remains the same input to the scripting stage.

Finally, open the saved PNG itself. Check for a clipped legend, an obscured slice, or an unexpected field. Keep this image as the visual reference, and have Claude inspect the trace for the final camera and display settings before refactoring it. The trace still needs a replay check; recording it does not establish that every action will reproduce correctly.

Four-panel comparison of ParaView slice outputs with crinkle and triangulation options, alongside a clip result.

Source: ParaView Documentation

Turn the Trace into a Parameterized pvpython Script

Keep scripts/trace.py as the record of the reference view. Ask Claude Code to create scripts/render_views.py separately, moving the confirmed choices into config/view.json. The script should rebuild the reader and slice in a fresh process, then apply the saved presentation settings.

Give Claude the trace, dataset report, and visualization specification together:

Refactor the trace into render_views.py and config/view.json. Preserve the reviewed slice, camera, palette, legend, and image dimensions. Use explicit reader, slice, view, and display references. Support preview and batch modes. Stop if the selected array is missing, has the wrong component count, or produces an empty slice. Keep the original trace and input files unchanged. Do not run the full sequence yet.

Save this template as config/view.json. Replace every null before running the script; these are missing values, not defaults. Take array, association (POINTS or CELLS), and components from the confirmed dataset report. Copy slice geometry, camera properties, resolution, and palette from the reviewed trace. Set the range and legend title using your agreed limits and confirmed units.

{
  "array": null,
  "association": null,
  "components": null,
  "slice_origin": [null, null, null],
  "slice_normal": [null, null, null],
  "palette": null,
  "range": [null, null],
  "resolution": [null, null],
  "camera": {
    "CameraPosition": [null, null, null],
    "CameraFocalPoint": [null, null, null],
    "CameraViewUp": [null, null, null],
    "CameraParallelProjection": null,
    "CameraParallelScale": null,
    "CameraViewAngle": null
  },
  "legend": {"Title": null, "ComponentTitle": ""}
}
{
  "array": null,
  "association": null,
  "components": null,
  "slice_origin": [null, null, null],
  "slice_normal": [null, null, null],
  "palette": null,
  "range": [null, null],
  "resolution": [null, null],
  "camera": {
    "CameraPosition": [null, null, null],
    "CameraFocalPoint": [null, null, null],
    "CameraViewUp": [null, null, null],
    "CameraParallelProjection": null,
    "CameraParallelScale": null,
    "CameraViewAngle": null
  },
  "legend": {"Title": null, "ComponentTitle": ""}
}
{
  "array": null,
  "association": null,
  "components": null,
  "slice_origin": [null, null, null],
  "slice_normal": [null, null, null],
  "palette": null,
  "range": [null, null],
  "resolution": [null, null],
  "camera": {
    "CameraPosition": [null, null, null],
    "CameraFocalPoint": [null, null, null],
    "CameraViewUp": [null, null, null],
    "CameraParallelProjection": null,
    "CameraParallelScale": null,
    "CameraViewAngle": null
  },
  "legend": {"Title": null, "ComponentTitle": ""}
}

CameraParallelProjection uses 0 for perspective or 1 for parallel projection. Copy both scale and angle from the trace so the configuration records either projection consistently. Add any additional legend-formatting properties from the reference view. Use a preset available in a fresh ParaView process; custom presets require importing their definitions first.

The script below rejects missing values and malformed numeric vectors before reading data. It expects the completed configuration and inspected PVD dataset; it has not been executed against your data. Check ParaView property names against your installed-version trace.

# scripts/render_views.py
import argparse
import hashlib
import json
import math
from pathlib import Path
import paraview.simple as pvs

def file_hash(path):
    digest = hashlib.sha256()
    with path.open("rb") as handle:
        for chunk in iter(lambda: handle.read(1024 * 1024), b""):
            digest.update(chunk)
    return digest.hexdigest()


def vector(value, size):
    if (not isinstance(value, list) or len(value) != size or
            any(type(x) not in (int, float) or not math.isfinite(x) for x in value)):
        raise ValueError(f"Expected {size} finite numbers, got {value!r}")


def validate(cfg):
    for key in ("array", "palette"):
        if not isinstance(cfg[key], str) or not cfg[key].strip():
            raise ValueError(f"Fill in {key} from the report or trace")
    if cfg["association"] not in ("POINTS", "CELLS"):
        raise ValueError("Choose POINTS or CELLS explicitly")
    if type(cfg["components"]) is not int or cfg["components"] < 2:
        raise ValueError("Choose a confirmed multicomponent velocity array")
    for key, size in (("slice_origin", 3), ("slice_normal", 3),
                      ("range", 2), ("resolution", 2)):
        vector(cfg[key], size)
    if not any(cfg["slice_normal"]) or not cfg["range"][0] < cfg["range"][1]:
        raise ValueError("Use a nonzero normal and increasing range")
    if any(type(x) is not int or x <= 0 for x in cfg["resolution"]):
        raise ValueError("Resolution must contain two positive integers")
    camera = cfg["camera"]
    for key in ("CameraPosition", "CameraFocalPoint", "CameraViewUp"):
        vector(camera[key], 3)
    if camera["CameraParallelProjection"] not in (0, 1):
        raise ValueError("Set projection to 0 or 1")
    vector([camera["CameraParallelScale"], camera["CameraViewAngle"]], 2)
    if not camera["CameraParallelScale"] > 0 or not 0 < camera["CameraViewAngle"] < 180:
        raise ValueError("Use a positive camera scale and angle between 0 and 180")
    if not isinstance(cfg["legend"]["Title"], str) or not cfg["legend"]["Title"].strip():
        raise ValueError("Set the confirmed legend title and units")


parser = argparse.ArgumentParser()
parser.add_argument("--mode", choices=["preview", "batch"], default="preview")
parser.add_argument("--out", required=True)
args = parser.parse_args()
root = Path(__file__).resolve().parents[1]
config_path = root / "config/view.json"
cfg = json.loads(config_path.read_text())
validate(cfg)
out = (root / args.out).resolve()
if root / "outputs" not in out.parents:
    raise ValueError("Output must be a new folder inside outputs/")
lo, hi = cfg["range"]

reader = pvs.PVDReader(FileName=str(root / "input/flow.pvd"))
reader.UpdatePipelineInformation()
times = sorted(set(reader.TimestepValues))
if not times:
    raise RuntimeError("No temporal values reported by the reader")
selected = times
if args.mode == "preview":
    selected = sorted({times[0], times[len(times) // 2], times[-1]})
reader.UpdatePipeline(time=selected[0])

slice_view = pvs.Slice(Input=reader)
slice_view.SliceType = "Plane"
slice_view.SliceType.Origin = cfg["slice_origin"]
slice_view.SliceType.Normal = cfg["slice_normal"]
view = pvs.CreateView("RenderView")
view.ViewSize = cfg["resolution"]
display = pvs.Show(slice_view, view)
display.Representation = "Surface"
pvs.Hide(reader, view)
pvs.Hide3DWidgets(proxy=slice_view.SliceType)
scene = pvs.GetAnimationScene()
out.mkdir(parents=True, exist_ok=False)

# Inventory all input files once per run, before rendering.
input_files = [{"file": str(path.relative_to(root)),
                "bytes": path.stat().st_size, "sha256": file_hash(path)}
               for path in sorted((root / "input").rglob("*")) if path.is_file()]
manifest = {"mode": args.mode, "requested_settings": cfg,
            "input": str((root / "input/flow.pvd").resolve()),
            "input_files": input_files,
            "paraview_version": pvs.GetParaViewSourceVersion(),
            "script_sha256": file_hash(Path(__file__).resolve()),
            "config_sha256": file_hash(config_path), "frames": []}
for index, time in enumerate(selected):
    scene.AnimationTime = time
    view.ViewTime = time
    reader.UpdatePipeline(time=time)
    slice_view.UpdatePipeline(time=time)
    for source in (reader, slice_view):
        arrays = (source.PointData if cfg["association"] == "POINTS"
                  else source.CellData)
        if cfg["array"] not in arrays.keys():
            raise RuntimeError(f"Missing array at time {time}")
        if arrays[cfg["array"]].GetNumberOfComponents() != cfg["components"]:
            raise RuntimeError(f"Component count changed at time {time}")
    if slice_view.GetDataInformation().GetNumberOfCells() == 0:
        raise RuntimeError(f"Empty slice at time {time}")
    pvs.ColorBy(display, (cfg["association"], cfg["array"], "Magnitude"))
    lut = pvs.GetColorTransferFunction(cfg["array"])
    if not lut.ApplyPreset(cfg["palette"], True):
        raise RuntimeError("Failed to apply the configured palette")
    lut.AutomaticRescaleRangeMode = "Never"
    lut.RescaleTransferFunction(lo, hi)
    display.SetScalarBarVisibility(view, True)
    legend = pvs.GetScalarBar(lut, view)
    pvs.SetProperties(legend, **cfg["legend"])
    pvs.Render(view)
    pvs.SetProperties(view, **cfg["camera"])
    pvs.Render(view)
    filename = f"frame_{index:04d}.png"
    pvs.SaveScreenshot(str(out / filename), view,
                       ImageResolution=cfg["resolution"])
    if not (out / filename).is_file() or (out / filename).stat().st_size == 0:
        raise RuntimeError(f"Image was not written: {filename}")
    manifest["frames"].append({"file": filename, "time": time})
    print(f"Saved {filename} at time {time}", flush=True)

pvs.SaveState(str(out / "visualization.pvsm"))
(out / "manifest.json").write_text(json.dumps(manifest, indent=2))
# scripts/render_views.py
import argparse
import hashlib
import json
import math
from pathlib import Path
import paraview.simple as pvs

def file_hash(path):
    digest = hashlib.sha256()
    with path.open("rb") as handle:
        for chunk in iter(lambda: handle.read(1024 * 1024), b""):
            digest.update(chunk)
    return digest.hexdigest()


def vector(value, size):
    if (not isinstance(value, list) or len(value) != size or
            any(type(x) not in (int, float) or not math.isfinite(x) for x in value)):
        raise ValueError(f"Expected {size} finite numbers, got {value!r}")


def validate(cfg):
    for key in ("array", "palette"):
        if not isinstance(cfg[key], str) or not cfg[key].strip():
            raise ValueError(f"Fill in {key} from the report or trace")
    if cfg["association"] not in ("POINTS", "CELLS"):
        raise ValueError("Choose POINTS or CELLS explicitly")
    if type(cfg["components"]) is not int or cfg["components"] < 2:
        raise ValueError("Choose a confirmed multicomponent velocity array")
    for key, size in (("slice_origin", 3), ("slice_normal", 3),
                      ("range", 2), ("resolution", 2)):
        vector(cfg[key], size)
    if not any(cfg["slice_normal"]) or not cfg["range"][0] < cfg["range"][1]:
        raise ValueError("Use a nonzero normal and increasing range")
    if any(type(x) is not int or x <= 0 for x in cfg["resolution"]):
        raise ValueError("Resolution must contain two positive integers")
    camera = cfg["camera"]
    for key in ("CameraPosition", "CameraFocalPoint", "CameraViewUp"):
        vector(camera[key], 3)
    if camera["CameraParallelProjection"] not in (0, 1):
        raise ValueError("Set projection to 0 or 1")
    vector([camera["CameraParallelScale"], camera["CameraViewAngle"]], 2)
    if not camera["CameraParallelScale"] > 0 or not 0 < camera["CameraViewAngle"] < 180:
        raise ValueError("Use a positive camera scale and angle between 0 and 180")
    if not isinstance(cfg["legend"]["Title"], str) or not cfg["legend"]["Title"].strip():
        raise ValueError("Set the confirmed legend title and units")


parser = argparse.ArgumentParser()
parser.add_argument("--mode", choices=["preview", "batch"], default="preview")
parser.add_argument("--out", required=True)
args = parser.parse_args()
root = Path(__file__).resolve().parents[1]
config_path = root / "config/view.json"
cfg = json.loads(config_path.read_text())
validate(cfg)
out = (root / args.out).resolve()
if root / "outputs" not in out.parents:
    raise ValueError("Output must be a new folder inside outputs/")
lo, hi = cfg["range"]

reader = pvs.PVDReader(FileName=str(root / "input/flow.pvd"))
reader.UpdatePipelineInformation()
times = sorted(set(reader.TimestepValues))
if not times:
    raise RuntimeError("No temporal values reported by the reader")
selected = times
if args.mode == "preview":
    selected = sorted({times[0], times[len(times) // 2], times[-1]})
reader.UpdatePipeline(time=selected[0])

slice_view = pvs.Slice(Input=reader)
slice_view.SliceType = "Plane"
slice_view.SliceType.Origin = cfg["slice_origin"]
slice_view.SliceType.Normal = cfg["slice_normal"]
view = pvs.CreateView("RenderView")
view.ViewSize = cfg["resolution"]
display = pvs.Show(slice_view, view)
display.Representation = "Surface"
pvs.Hide(reader, view)
pvs.Hide3DWidgets(proxy=slice_view.SliceType)
scene = pvs.GetAnimationScene()
out.mkdir(parents=True, exist_ok=False)

# Inventory all input files once per run, before rendering.
input_files = [{"file": str(path.relative_to(root)),
                "bytes": path.stat().st_size, "sha256": file_hash(path)}
               for path in sorted((root / "input").rglob("*")) if path.is_file()]
manifest = {"mode": args.mode, "requested_settings": cfg,
            "input": str((root / "input/flow.pvd").resolve()),
            "input_files": input_files,
            "paraview_version": pvs.GetParaViewSourceVersion(),
            "script_sha256": file_hash(Path(__file__).resolve()),
            "config_sha256": file_hash(config_path), "frames": []}
for index, time in enumerate(selected):
    scene.AnimationTime = time
    view.ViewTime = time
    reader.UpdatePipeline(time=time)
    slice_view.UpdatePipeline(time=time)
    for source in (reader, slice_view):
        arrays = (source.PointData if cfg["association"] == "POINTS"
                  else source.CellData)
        if cfg["array"] not in arrays.keys():
            raise RuntimeError(f"Missing array at time {time}")
        if arrays[cfg["array"]].GetNumberOfComponents() != cfg["components"]:
            raise RuntimeError(f"Component count changed at time {time}")
    if slice_view.GetDataInformation().GetNumberOfCells() == 0:
        raise RuntimeError(f"Empty slice at time {time}")
    pvs.ColorBy(display, (cfg["association"], cfg["array"], "Magnitude"))
    lut = pvs.GetColorTransferFunction(cfg["array"])
    if not lut.ApplyPreset(cfg["palette"], True):
        raise RuntimeError("Failed to apply the configured palette")
    lut.AutomaticRescaleRangeMode = "Never"
    lut.RescaleTransferFunction(lo, hi)
    display.SetScalarBarVisibility(view, True)
    legend = pvs.GetScalarBar(lut, view)
    pvs.SetProperties(legend, **cfg["legend"])
    pvs.Render(view)
    pvs.SetProperties(view, **cfg["camera"])
    pvs.Render(view)
    filename = f"frame_{index:04d}.png"
    pvs.SaveScreenshot(str(out / filename), view,
                       ImageResolution=cfg["resolution"])
    if not (out / filename).is_file() or (out / filename).stat().st_size == 0:
        raise RuntimeError(f"Image was not written: {filename}")
    manifest["frames"].append({"file": filename, "time": time})
    print(f"Saved {filename} at time {time}", flush=True)

pvs.SaveState(str(out / "visualization.pvsm"))
(out / "manifest.json").write_text(json.dumps(manifest, indent=2))
# scripts/render_views.py
import argparse
import hashlib
import json
import math
from pathlib import Path
import paraview.simple as pvs

def file_hash(path):
    digest = hashlib.sha256()
    with path.open("rb") as handle:
        for chunk in iter(lambda: handle.read(1024 * 1024), b""):
            digest.update(chunk)
    return digest.hexdigest()


def vector(value, size):
    if (not isinstance(value, list) or len(value) != size or
            any(type(x) not in (int, float) or not math.isfinite(x) for x in value)):
        raise ValueError(f"Expected {size} finite numbers, got {value!r}")


def validate(cfg):
    for key in ("array", "palette"):
        if not isinstance(cfg[key], str) or not cfg[key].strip():
            raise ValueError(f"Fill in {key} from the report or trace")
    if cfg["association"] not in ("POINTS", "CELLS"):
        raise ValueError("Choose POINTS or CELLS explicitly")
    if type(cfg["components"]) is not int or cfg["components"] < 2:
        raise ValueError("Choose a confirmed multicomponent velocity array")
    for key, size in (("slice_origin", 3), ("slice_normal", 3),
                      ("range", 2), ("resolution", 2)):
        vector(cfg[key], size)
    if not any(cfg["slice_normal"]) or not cfg["range"][0] < cfg["range"][1]:
        raise ValueError("Use a nonzero normal and increasing range")
    if any(type(x) is not int or x <= 0 for x in cfg["resolution"]):
        raise ValueError("Resolution must contain two positive integers")
    camera = cfg["camera"]
    for key in ("CameraPosition", "CameraFocalPoint", "CameraViewUp"):
        vector(camera[key], 3)
    if camera["CameraParallelProjection"] not in (0, 1):
        raise ValueError("Set projection to 0 or 1")
    vector([camera["CameraParallelScale"], camera["CameraViewAngle"]], 2)
    if not camera["CameraParallelScale"] > 0 or not 0 < camera["CameraViewAngle"] < 180:
        raise ValueError("Use a positive camera scale and angle between 0 and 180")
    if not isinstance(cfg["legend"]["Title"], str) or not cfg["legend"]["Title"].strip():
        raise ValueError("Set the confirmed legend title and units")


parser = argparse.ArgumentParser()
parser.add_argument("--mode", choices=["preview", "batch"], default="preview")
parser.add_argument("--out", required=True)
args = parser.parse_args()
root = Path(__file__).resolve().parents[1]
config_path = root / "config/view.json"
cfg = json.loads(config_path.read_text())
validate(cfg)
out = (root / args.out).resolve()
if root / "outputs" not in out.parents:
    raise ValueError("Output must be a new folder inside outputs/")
lo, hi = cfg["range"]

reader = pvs.PVDReader(FileName=str(root / "input/flow.pvd"))
reader.UpdatePipelineInformation()
times = sorted(set(reader.TimestepValues))
if not times:
    raise RuntimeError("No temporal values reported by the reader")
selected = times
if args.mode == "preview":
    selected = sorted({times[0], times[len(times) // 2], times[-1]})
reader.UpdatePipeline(time=selected[0])

slice_view = pvs.Slice(Input=reader)
slice_view.SliceType = "Plane"
slice_view.SliceType.Origin = cfg["slice_origin"]
slice_view.SliceType.Normal = cfg["slice_normal"]
view = pvs.CreateView("RenderView")
view.ViewSize = cfg["resolution"]
display = pvs.Show(slice_view, view)
display.Representation = "Surface"
pvs.Hide(reader, view)
pvs.Hide3DWidgets(proxy=slice_view.SliceType)
scene = pvs.GetAnimationScene()
out.mkdir(parents=True, exist_ok=False)

# Inventory all input files once per run, before rendering.
input_files = [{"file": str(path.relative_to(root)),
                "bytes": path.stat().st_size, "sha256": file_hash(path)}
               for path in sorted((root / "input").rglob("*")) if path.is_file()]
manifest = {"mode": args.mode, "requested_settings": cfg,
            "input": str((root / "input/flow.pvd").resolve()),
            "input_files": input_files,
            "paraview_version": pvs.GetParaViewSourceVersion(),
            "script_sha256": file_hash(Path(__file__).resolve()),
            "config_sha256": file_hash(config_path), "frames": []}
for index, time in enumerate(selected):
    scene.AnimationTime = time
    view.ViewTime = time
    reader.UpdatePipeline(time=time)
    slice_view.UpdatePipeline(time=time)
    for source in (reader, slice_view):
        arrays = (source.PointData if cfg["association"] == "POINTS"
                  else source.CellData)
        if cfg["array"] not in arrays.keys():
            raise RuntimeError(f"Missing array at time {time}")
        if arrays[cfg["array"]].GetNumberOfComponents() != cfg["components"]:
            raise RuntimeError(f"Component count changed at time {time}")
    if slice_view.GetDataInformation().GetNumberOfCells() == 0:
        raise RuntimeError(f"Empty slice at time {time}")
    pvs.ColorBy(display, (cfg["association"], cfg["array"], "Magnitude"))
    lut = pvs.GetColorTransferFunction(cfg["array"])
    if not lut.ApplyPreset(cfg["palette"], True):
        raise RuntimeError("Failed to apply the configured palette")
    lut.AutomaticRescaleRangeMode = "Never"
    lut.RescaleTransferFunction(lo, hi)
    display.SetScalarBarVisibility(view, True)
    legend = pvs.GetScalarBar(lut, view)
    pvs.SetProperties(legend, **cfg["legend"])
    pvs.Render(view)
    pvs.SetProperties(view, **cfg["camera"])
    pvs.Render(view)
    filename = f"frame_{index:04d}.png"
    pvs.SaveScreenshot(str(out / filename), view,
                       ImageResolution=cfg["resolution"])
    if not (out / filename).is_file() or (out / filename).stat().st_size == 0:
        raise RuntimeError(f"Image was not written: {filename}")
    manifest["frames"].append({"file": filename, "time": time})
    print(f"Saved {filename} at time {time}", flush=True)

pvs.SaveState(str(out / "visualization.pvsm"))
(out / "manifest.json").write_text(json.dumps(manifest, indent=2))

The loop deliberately reapplies the fixed range and recorded camera at each time. ParaView’s screenshot and state APIs save the images and reopenable scene, while the manifest connects each indexed filename to its simulation time. It also records the requested settings, ParaView version, script/configuration hashes, and a checksum inventory of files under input/. These records identify a run; they do not replace checking the rendered scene.

Keep every referenced data file under input/ and unchanged during execution. Confirm the inventory covers those dependencies: this directory scan does not resolve references outside the folder. Hashing reads each input file once per run and can add startup time for large datasets.

Use a new output subfolder for each attempt, such as outputs/preview/run-01. A failed run may leave partial images; only a completed run writes the manifest. If the reference uses additional display or background properties, have Claude carry those trace settings into the script before comparing its first export with reference.png.

Export a Three-Timestep Preview Before the Full Sequence

Run the preview from PowerShell in the project folder on your Vagon computer. Use the pvpython.exe path you verified during setup. The prompt below accepts that path without assuming a particular ParaView version or installation directory; paste it without surrounding quotation marks.

$pvpython = Read-Host "Full path to the verified pvpython.exe"
& $pvpython scripts/render_views.py --mode preview --out outputs/preview/run-01 2>&1 |
    Tee-Object -FilePath outputs/preview/run-01.log
$renderExit = $LASTEXITCODE
if ($renderExit -ne 0) { throw "Preview failed with exit code $renderExit" }
$pvpython = Read-Host "Full path to the verified pvpython.exe"
& $pvpython scripts/render_views.py --mode preview --out outputs/preview/run-01 2>&1 |
    Tee-Object -FilePath outputs/preview/run-01.log
$renderExit = $LASTEXITCODE
if ($renderExit -ne 0) { throw "Preview failed with exit code $renderExit" }
$pvpython = Read-Host "Full path to the verified pvpython.exe"
& $pvpython scripts/render_views.py --mode preview --out outputs/preview/run-01 2>&1 |
    Tee-Object -FilePath outputs/preview/run-01.log
$renderExit = $LASTEXITCODE
if ($renderExit -ne 0) { throw "Preview failed with exit code $renderExit" }

Choose an unused run name. The script creates the run folder itself and refuses to reuse an existing one, keeping earlier attempts available for comparison. Its log sits beside that folder so you can retain error messages even if execution stops before the manifest is written.

With at least three distinct timesteps, expect three PNGs, manifest.json, and visualization.pvsm. For a shorter dataset, expect one image per distinct selected time. Compare the manifest’s time values with dataset_report.json; filenames such as frame_0001.png are sequence indexes, not physical times.

Open the first exported frame beside reference.png. They should represent the same timestep and agreed view. Then compare all preview images for framing, slice visibility, legend limits, and readable units. Physical changes in the field are expected; unexplained changes in presentation need correction before proceeding.

If the preview fails, give Claude the command, log, and configuration and ask it to identify the failing operation. Keep the chosen field and visualization settings fixed while resolving runtime or property-name errors. After a correction, repeat the preview in a new folder.

Once the preview passes, keep the same configuration and export every available timestep:

& $pvpython scripts/render_views.py --mode batch --out outputs/batch/run-01 2>&1 |
    Tee-Object -FilePath outputs/batch/run-01.log
$renderExit = $LASTEXITCODE
if ($renderExit -ne 0) { throw "Batch failed with exit code $renderExit" }
& $pvpython scripts/render_views.py --mode batch --out outputs/batch/run-01 2>&1 |
    Tee-Object -FilePath outputs/batch/run-01.log
$renderExit = $LASTEXITCODE
if ($renderExit -ne 0) { throw "Batch failed with exit code $renderExit" }
& $pvpython scripts/render_views.py --mode batch --out outputs/batch/run-01 2>&1 |
    Tee-Object -FilePath outputs/batch/run-01.log
$renderExit = $LASTEXITCODE
if ($renderExit -ne 0) { throw "Batch failed with exit code $renderExit" }

Here, --mode batch is an option in your script. It selects the complete time list; it does not enable distributed execution. ParaView’s pvbatch interpreter is a separate tool whose parallel use requires an appropriate MPI setup. This workflow continues through the verified pvpython executable.

Verify the Images and Reopen the Saved ParaView State

A successful exit means the script finished its operations. Before using the images in a report, check the saved files and the visualization they represent.

Ask Claude Code to audit outputs/batch/run-01 against its manifest and the dataset report. The audit should confirm that:

  • Every expected timestep appears once, in order, with a corresponding PNG.

  • Each PNG opens successfully and has the configured pixel dimensions.

  • The manifest’s requested_settings match the reviewed configuration, and its input inventory and script/configuration hashes identify the files used.

  • The log contains no unresolved reader, filter, or rendering errors.

The example script checks for nonempty files, but that is not an image-decoding test. Have Claude use an available image library for that additional check, or open the files yourself. Matching configuration records also does not prove that every setting reached the rendered scene; that requires inspecting the results.

Next, start a fresh ParaView session and choose File > Load State. Open outputs/batch/run-01/visualization.pvsm. ParaView’s state-loading options let you retain recorded file paths, search another directory, or choose replacement filenames. Keep flow.pvd and all its referenced files available: the PVSM stores the visualization setup, not a bundled copy of the simulation data.

ParaView Load State Options dialog with Search files under specified directory selected and a Data Directory field.

Source: ParaView Documentation

The script saves state after the last exported timestep. Check that the reopened session shows that time, the intended reader-to-slice connection, and the correct field and magnitude selection. Then visit the preview times and inspect the camera and legend. Interface widgets may need hiding again because temporary UI settings are not all saved in state files.

Where computer use is enabled, give Claude a focused review task:

Inspect the reopened ParaView state and the saved first, middle, and last batch images. Compare the pipeline, time values, framing, and legend with the manifest and reference image. Report discrepancies with the affected filename or property. Leave the configuration unchanged.

For a blank image, check whether the slice contains cells and whether its representation is visible. For changing colors, inspect the active array, magnitude selection, and transfer-function limits. For missing geometry after reopening, resolve the data paths before adjusting the camera.

Finally, rerun the preview into a new folder with unchanged inputs and configuration. Compare its times and presentation with the first preview. Retain the source data, trace, script, configuration, dataset report, logs, manifest, PVSM, and checked images together. That package lets you inspect the result now and reproduce it when the next visualization is needed.

FAQs

Can I run the visualization script with ordinary Python?

For this workflow, use pvpython from the ParaView installation that produced your trace. An unrelated Python environment may not expose the required ParaView modules and libraries. Installing a package with a similar name does not establish that the script has the correct runtime.

Do I need a ParaView MCP server?

No. Claude Code can prepare files and launch the documented ParaView Python runtime without an MCP server. A separate integration may help with other tasks, but this workflow does not require one or assume that Claude can inspect an open ParaView session through shell access alone.

Can Claude Code use the ParaView interface on Windows?

Use computer use in Desktop where your account and session support it. The Windows CLI alone does not establish screen-control access. Manual inspection remains available alongside scripted exports.

Is the saved PVSM enough to share the visualization?

No. Include its referenced simulation files. Add the script, configuration, and manifest for reproducible exports, then check the data paths in the recipient’s environment.

What changes when the dataset is too large for one computer?

Reassess memory requirements, selected arrays, filter intermediates, and where the data resides. ParaView supports remote and parallel execution, but configuring those resources is a separate step from switching this script to batch mode. Establish a correct three-timestep preview on representative data before extending the workflow to a larger execution setup.

Vagon Computer

Run it on a cloud computer

Open it from any browser, Mac, iPad or Chromebook. No local hardware needed.

Vagon Computer

Run it on a cloud computer

Open it from any browser, Mac, iPad or Chromebook. No local hardware needed.

Written by

Mert Boz

Mert is a philosophy student with a strong interest in economics, motorsports, and basketball. He enjoys thinking about how the world works, both in theory and in real life. In his free time, you can often find him watching a race, following a basketball game, or reading about economic ideas.

More posts by this writer →

Vagon Computer

Run this guide on a cloud computer.

Open it from any browser, Mac, iPad or Chromebook. No local hardware needed.

Vagon Computer

Run this guide on a cloud computer.

Open it from any browser, Mac, iPad or Chromebook. No local hardware needed.