Headless topological data analysis and visualization with the Topology ToolKit (TTK) on top of ParaView. Use this skill when Claude needs to: (1) Compute and visualize persistence diagrams, contour trees, merge trees, and Morse-Smale segmentations, (2) Extract and render critical points of scalar/vector fields, (3) Apply persistence-based topological simplification to denoise scalar fields, (4) Run TTK filters headlessly via pvpython (loading TTK plugins and using the Scalars_ naming conventi...
Scanned 9/12/2026
Install to Claude Code
npx -y skills add stanfish06/skillquarium --skill ttk-viz --agent claude-codeInstalls into .claude/skills of the current project.
Are you the author of Ttk Viz?
Add the live security badge to your README — it updates automatically with every re-scan.
[](https://www.skillsdirectory.com/skills/stanfish06-ttk-viz)More formats (shields.io, HTML) on the badges page.
---
name: ttk-viz
description: >
Headless topological data analysis and visualization with the Topology ToolKit (TTK) on top of ParaView. Use this skill when Claude needs to:
(1) Compute and visualize persistence diagrams, contour trees, merge trees, and Morse-Smale segmentations,
(2) Extract and render critical points of scalar/vector fields,
(3) Apply persistence-based topological simplification to denoise scalar fields,
(4) Run TTK filters headlessly via pvpython (loading TTK plugins and using the Scalars_ naming convention).
---
# TTK Headless Topology Visualization
Execute all TTK tasks via self-contained Python scripts run with `pvpython`. Use batch/headless mode — never open a GUI.
TTK extends ParaView with topological analysis filters. Always import `from paraview.simple import *` and ensure TTK plugins are loaded before using TTK filters.
## Rules
1. **Never open a GUI** — always use `pvpython` for headless batch execution
2. Use `from paraview.simple import *` at the top of every script
3. Load TTK plugins explicitly before using TTK filters (see Canonical Template)
4. TTK filters expect the scalar array to be named `Scalars_` — rename on load if needed
5. Always call `UpdatePipeline()` after filters before accessing data information
6. After taking a screenshot, use the Read tool to view the image and verify correctness
7. For visual matching tasks, iterate: screenshot → assess → adjust → re-screenshot (max 5 rounds)
8. Use `pvpython` (not `python`) to run scripts
## Canonical Script Template
```python
from paraview.simple import *
# Load TTK plugins (required before using any TTK filter)
LoadPlugin("TopologyToolKit", remote=False, ns=globals())
# ============= Load Data =============
reader = XMLImageDataReader(FileName=['/path/to/data.vti'])
reader.PointArrayStatus = ['array_name']
reader.UpdatePipeline()
# Rename to standard name expected by TTK filters
rename = RenameArrays(Input=reader)
rename.PointArrays = ['array_name', 'Scalars_']
rename.UpdatePipeline()
# ============= TTK Filters =============
# (see sections below)
# ============= Render View =============
renderView = CreateView('RenderView')
renderView.ViewSize = [1920, 1080]
renderView.Background = [0.1, 0.1, 0.15]
layout = CreateLayout(name='Layout')
layout.AssignView(0, renderView)
# ============= Display =============
display = Show(rename, renderView)
ResetCamera(renderView)
# ============= Save Output =============
SaveScreenshot('/path/to/output.png', renderView,
ImageResolution=[1920, 1080],
OverrideColorPalette='WhiteBackground')
```
---
## Loading Data
```python
# VTI (VTK Image Data — standard for TTK scalar/vector/tensor fields)
reader = XMLImageDataReader(FileName=['/path/to/field.vti'])
reader.PointArrayStatus = ['Pressure'] # activate desired arrays
reader.TimeArray = 'None'
reader.UpdatePipeline()
# Rename to convention TTK expects
rename = RenameArrays(Input=reader)
rename.PointArrays = ['Pressure', 'Scalars_'] # [old_name, new_name, ...]
rename.UpdatePipeline()
# For vector fields (2D), rename components to 'u' and 'v'
rename = RenameArrays(Input=reader)
rename.PointArrays = ['velocity_x', 'u', 'velocity_y', 'v']
# VTU / PVTU (unstructured grids)
reader = XMLPartitionedUnstructuredGridReader(FileName=['/path/to/data.pvtu'])
# PVD (time series)
reader = PVDReader(FileName='/path/to/timeseries.pvd')
```
### Get Data Range
```python
reader.UpdatePipeline()
pd = reader.PointData
min_val, max_val = pd.GetArray('Scalars_').GetRange()
bounds = reader.GetDataInformation().GetBounds()
center = [(bounds[0]+bounds[1])/2, (bounds[2]+bounds[3])/2, (bounds[4]+bounds[5])/2]
```
---
## TTK Filters
### Persistence Diagram
```python
# Computes persistence diagram of a scalar field
pd_filter = TTKPersistenceDiagram(Input=rename)
pd_filter.ScalarField = ['POINTS', 'Scalars_']
pd_filter.InputOffsetField = ['POINTS', 'Scalars_']
pd_filter.Backend = 'FTM (IEEE TPDS 2019)'
pd_filter.UpdatePipeline()
# Get persistence pairs range
pd_data = pd_filter.PointData
# PersistenceDiagram output arrays: 'ttkVertexScalarField', 'CriticalType',
# 'Persistence', 'Birth', 'IsFinite', 'PairIdentifier', 'PairType', 'Coordinates'
# Visualize as spheres (birth-death pairs)
spheres = TTKIcospheresFromPoints(Input=pd_filter)
spheres.Radius = 0.02 # as fraction of data range
sphereDisplay = Show(spheres, renderView)
ColorBy(sphereDisplay, ('POINTS', 'Persistence'))
sphereDisplay.RescaleTransferFunctionToDataRange(True)
# Visualize connecting arcs (surface of pd_filter → tubes)
surf = ExtractSurface(Input=pd_filter)
tube = Tube(Input=surf)
tube.Radius = 0.01
Show(tube, renderView)
```
### Contour Tree
```python
# Computes the contour tree (join+split tree merged) of a scalar field
ct = TTKContourTree(Input=rename)
ct.ScalarField = ['POINTS', 'Scalars_']
ct.InputOffsetField = ['POINTS', 'Scalars_']
ct.UpdatePipeline()
# Output ports:
# Port 0: tree nodes (vertices — critical points)
# Port 1: tree edges (arcs connecting critical points)
ct_nodes = OutputPort(ct, 0)
ct_edges = OutputPort(ct, 1)
# Visualize nodes as spheres
spheres = TTKIcospheresFromPoints(Input=ct_nodes)
spheres.Radius = 0.5
sphereDisplay = Show(spheres, renderView)
# Color by CriticalType: 0=min, 1=1-saddle, 2=2-saddle, 3=max, 4=degenerate
ColorBy(sphereDisplay, ('POINTS', 'CriticalType'))
# Visualize edges as tubes
surf = ExtractSurface(Input=ct_edges)
tube = Tube(Input=surf)
tube.Radius = 0.1
Show(tube, renderView)
# Apply custom discrete color map for CriticalType
lut = GetColorTransferFunction('CriticalType')
# [value, R, G, B, ...] — minima=blue, 1-saddle=light blue,
# 2-saddle=pink, maxima=red, degenerate=black
lut.RGBPoints = [
0, 0.0, 0.188, 1.0,
1, 0.459, 0.561, 1.0,
2, 1.0, 0.871, 0.906,
3, 1.0, 0.0, 0.275,
4, 0.0, 0.0, 0.0,
]
lut.ColorSpace = 'RGB'
lut.ScalarRangeInitialized = 1.0
sphereDisplay.LookupTable = lut
```
### Merge Tree (Join or Split)
```python
# Join tree: tracks minima as scalar grows
join = TTKMergeTree(Input=rename)
join.ScalarField = ['POINTS', 'Scalars_']
join.InputOffsetField = ['POINTS', 'Scalars_']
# join.TreeType is 'Join Tree' by default
# Split tree: tracks maxima as scalar grows
split = TTKMergeTree(Input=rename)
split.ScalarField = ['POINTS', 'Scalars_']
split.InputOffsetField = ['POINTS', 'Scalars_']
split.TreeType = 'Split Tree'
# Visualize exactly like contour tree (ports 0=nodes, 1=edges)
```
### Scalar Field Critical Points
```python
cp = TTKScalarFieldCriticalPoints(Input=rename)
cp.ScalarField = ['POINTS', 'Scalars_']
cp.InputOffsetField = ['POINTS', 'Scalars_']
cp.UpdatePipeline()
# Output: point cloud with 'CriticalType' array
# CriticalType values: 0=min, 1=1-saddle, 2=2-saddle, 3=max, 4=degenerate
# Visualize all critical points as spheres
spheres = TTKIcospheresFromPoints(Input=cp)
spheres.Radius = 0.3
cpDisplay = Show(spheres, renderView)
ColorBy(cpDisplay, ('POINTS', 'CriticalType'))
# Filter to specific type (e.g., only minima = type 0)
thresh = Threshold(Input=cp)
thresh.Scalars = ['POINTS', 'CriticalType']
thresh.ThresholdMethod = 'Between'
thresh.LowerThreshold = 0 # 0=minima only
thresh.UpperThreshold = 0
minSpheres = TTKIcospheresFromPoints(Input=thresh)
minSpheres.Radius = 0.3
Show(minSpheres, renderView)
```
### Topological Simplification by Persistence
```python
# Remove topological noise below a persistence threshold
# PersistenceThreshold is a fraction of the function range (0-1) unless
# ThresholdIsAbsolute = 1, which switches it to scalar-field units
simplified = TTKTopologicalSimplificationByPersistence(Input=rename)
simplified.InputArray = ['POINTS', 'Scalars_']
simplified.PersistenceThreshold = 0.1 # 10% of scalar range
simplified.UpdatePipeline()
# 'simplified' outputs the same grid with a simplified Scalars_ field
# Use it as input to downstream TTK filters for denoised analysis
ct = TTKContourTree(Input=simplified)
ct.ScalarField = ['POINTS', 'Scalars_']
```
### Morse-Smale Segmentation
```python
# TTKPathCompression computes the Morse-Smale complex segmentation
# Each cell is assigned a region label in 'Scalars__MorseSmaleManifold'
msc = TTKPathCompression(Input=rename)
msc.ScalarField = ['POINTS', 'Scalars_']
msc.OffsetField = ['POINTS', 'Scalars_']
msc.UpdatePipeline()
# Visualize segmentation (for 2D data, Slice representation is ideal)
display = Show(msc, renderView, 'UniformGridRepresentation')
display.Representation = 'Slice'
ColorBy(display, ('POINTS', 'Scalars__MorseSmaleManifold'))
display.RescaleTransferFunctionToDataRange(True)
```
### TTK Icospheres from Points
```python
# Generates sphere geometry centered at each input point
# Used to visualize critical points, tree nodes, persistence pairs, etc.
spheres = TTKIcospheresFromPoints(Input=point_data)
spheres.Radius = 0.5 # world-space radius
```
---
## Render View & Camera
```python
renderView = CreateView('RenderView')
renderView.ViewSize = [1920, 1080]
renderView.Background = [0.1, 0.1, 0.15]
layout = CreateLayout(name='Layout')
layout.AssignView(0, renderView)
# Fit everything in view
ResetCamera(renderView)
# Manual camera
renderView.CameraPosition = [cx + d, cy, cz]
renderView.CameraFocalPoint = [cx, cy, cz]
renderView.CameraViewUp = [0, 0, 1]
SaveScreenshot('/path/to/output.png', renderView,
ImageResolution=[1920, 1080],
OverrideColorPalette='WhiteBackground')
```
---
## Color Transfer Functions
```python
# Continuous colormap (e.g., scalar field)
lut = GetColorTransferFunction('Scalars_')
lut.ApplyPreset('Cool to Warm', True)
# Or manually:
lut.RGBPoints = [min_val, 0.0, 0.0, 1.0,
(min_val+max_val)/2, 1.0, 1.0, 1.0,
max_val, 1.0, 0.0, 0.0]
lut.ScalarRangeInitialized = 1.0
# Discrete colormap for CriticalType (5 types: 0–4)
lut = GetColorTransferFunction('CriticalType')
lut.RGBPoints = [
0, 0.0, 0.188, 1.0, # minima — blue
1, 0.459, 0.561, 1.0, # 1-saddle — light blue
2, 1.0, 0.871, 0.906, # 2-saddle — pink
3, 1.0, 0.0, 0.275, # maxima — red
4, 0.0, 0.0, 0.0, # degenerate — black
]
lut.ColorSpace = 'RGB'
lut.ScalarRangeInitialized = 1.0
# Discrete colormap for vector critical point types (6 types: 0–5)
# 0=source, 1=saddle, 2=sink, 3=source spiral, 4=center, 5=sink spiral
lut = GetColorTransferFunction('CriticalType')
lut.RGBPoints = [
0, 0.616, 0.173, 0.0,
1, 0.941, 0.773, 0.443,
2, 0.043, 0.506, 0.635,
3, 0.886, 0.341, 0.349,
4, 0.494, 0.278, 0.580,
5, 0.349, 0.659, 0.612,
]
lut.ColorSpace = 'RGB'
lut.ScalarRangeInitialized = 1.0
# Scalar bar
colorBar = GetScalarBar(lut, renderView)
colorBar.Title = 'CriticalType'
colorBar.Visibility = 1
```
---
## Outline & Background
```python
# Draw a bounding box outline around the data
outline = Outline(Input=rename)
outlineDisplay = Show(outline, renderView)
outlineDisplay.DiffuseColor = [0.5, 0.5, 0.5]
# Thin tube for a cleaner outline
tube = Tube(Input=outline)
tube.Radius = max_dimension * 0.0025
Show(tube, renderView)
```
---
## Common Workflow Patterns
### Persistence Diagram Visualization
```python
from paraview.simple import *
LoadPlugin("TopologyToolKit", remote=False, ns=globals())
reader = XMLImageDataReader(FileName=['/path/to/field.vti'])
reader.PointArrayStatus = ['density']
reader.UpdatePipeline()
rename = RenameArrays(Input=reader)
rename.PointArrays = ['density', 'Scalars_']
rename.UpdatePipeline()
min_val, max_val = rename.PointData.GetArray('Scalars_').GetRange()
data_range = max_val - min_val
pd_filter = TTKPersistenceDiagram(Input=rename)
pd_filter.ScalarField = ['POINTS', 'Scalars_']
pd_filter.InputOffsetField = ['POINTS', 'Scalars_']
pd_filter.UpdatePipeline()
renderView = CreateView('RenderView')
renderView.ViewSize = [1920, 1080]
layout = CreateLayout(name='Layout')
layout.AssignView(0, renderView)
# Spheres for birth-death pairs, sized relative to data range
spheres = TTKIcospheresFromPoints(Input=pd_filter)
spheres.Radius = data_range * 0.02
sDisp = Show(spheres, renderView)
ColorBy(sDisp, ('POINTS', 'Persistence'))
sDisp.RescaleTransferFunctionToDataRange(True)
# Tubes for arcs
surf = ExtractSurface(Input=pd_filter)
tube = Tube(Input=surf)
tube.Radius = data_range * 0.01
Show(tube, renderView)
ResetCamera(renderView)
SaveScreenshot('/path/to/pd.png', renderView, ImageResolution=[1920, 1080])
```
### Contour Tree with Simplified Input
```python
from paraview.simple import *
LoadPlugin("TopologyToolKit", remote=False, ns=globals())
reader = XMLImageDataReader(FileName=['/path/to/field.vti'])
reader.PointArrayStatus = ['pressure']
rename = RenameArrays(Input=reader)
rename.PointArrays = ['pressure', 'Scalars_']
rename.UpdatePipeline()
min_val, max_val = rename.PointData.GetArray('Scalars_').GetRange()
data_range = max_val - min_val # used below for glyph radii, not for the threshold
# Remove noise below 5% persistence
simplified = TTKTopologicalSimplificationByPersistence(Input=rename)
simplified.InputArray = ['POINTS', 'Scalars_']
simplified.PersistenceThreshold = 0.05
ct = TTKContourTree(Input=simplified)
ct.ScalarField = ['POINTS', 'Scalars_']
ct.InputOffsetField = ['POINTS', 'Scalars_']
ct.UpdatePipeline()
renderView = CreateView('RenderView')
renderView.ViewSize = [1920, 1080]
layout = CreateLayout(name='Layout')
layout.AssignView(0, renderView)
# Background scalar field
fieldDisplay = Show(rename, renderView, 'UniformGridRepresentation')
fieldDisplay.Representation = 'Slice'
ColorBy(fieldDisplay, ('POINTS', 'Scalars_'))
lut = GetColorTransferFunction('Scalars_')
lut.ApplyPreset('Cool to Warm', True)
fieldDisplay.RescaleTransferFunctionToDataRange(True)
fieldDisplay.Opacity = 0.4
# Tree nodes as spheres
spheres = TTKIcospheresFromPoints(Input=OutputPort(ct, 0))
spheres.Radius = 0.01 * data_range
ctNodeDisp = Show(spheres, renderView)
ctLut = GetColorTransferFunction('CriticalType')
ctLut.RGBPoints = [
0, 0.0, 0.188, 1.0, # min
1, 0.459, 0.561, 1.0, # 1-saddle
2, 1.0, 0.871, 0.906, # 2-saddle
3, 1.0, 0.0, 0.275, # max
4, 0.0, 0.0, 0.0, # degenerate
]
ctLut.ScalarRangeInitialized = 1.0
ctNodeDisp.LookupTable = ctLut
ctNodeDisp.ColorArrayName = ['POINTS', 'CriticalType']
# Tree edges as tubes
surf = ExtractSurface(Input=OutputPort(ct, 1))
tube = Tube(Input=surf)
tube.Radius = 0.005 * data_range
Show(tube, renderView)
ResetCamera(renderView)
SaveScreenshot('/path/to/ct.png', renderView, ImageResolution=[1920, 1080])
```
### Critical Points Overlay on Scalar Field
```python
from paraview.simple import *
LoadPlugin("TopologyToolKit", remote=False, ns=globals())
reader = XMLImageDataReader(FileName=['/path/to/field.vti'])
reader.PointArrayStatus = ['temperature']
rename = RenameArrays(Input=reader)
rename.PointArrays = ['temperature', 'Scalars_']
rename.UpdatePipeline()
bounds = rename.GetDataInformation().GetBounds()
max_dim = max(bounds[1]-bounds[0], bounds[3]-bounds[2], bounds[5]-bounds[4])
renderView = CreateView('RenderView')
renderView.ViewSize = [1920, 1080]
layout = CreateLayout(name='Layout')
layout.AssignView(0, renderView)
# Scalar field background
fieldDisp = Show(rename, renderView, 'UniformGridRepresentation')
fieldDisp.Representation = 'Slice'
ColorBy(fieldDisp, ('POINTS', 'Scalars_'))
GetColorTransferFunction('Scalars_').ApplyPreset('Viridis (matplotlib)', True)
fieldDisp.RescaleTransferFunctionToDataRange(True)
# All critical points
cp = TTKScalarFieldCriticalPoints(Input=rename)
cp.ScalarField = ['POINTS', 'Scalars_']
cp.InputOffsetField = ['POINTS', 'Scalars_']
cp.UpdatePipeline()
spheres = TTKIcospheresFromPoints(Input=cp)
spheres.Radius = max_dim * 0.01
cpDisp = Show(spheres, renderView)
lut = GetColorTransferFunction('CriticalType')
lut.RGBPoints = [
0, 0.0, 0.188, 1.0,
1, 0.459, 0.561, 1.0,
2, 1.0, 0.871, 0.906,
3, 1.0, 0.0, 0.275,
4, 0.0, 0.0, 0.0,
]
lut.ScalarRangeInitialized = 1.0
cpDisp.LookupTable = lut
cpDisp.ColorArrayName = ['POINTS', 'CriticalType']
ResetCamera(renderView)
SaveScreenshot('/path/to/cp.png', renderView, ImageResolution=[1920, 1080])
```
### Morse-Smale Segmentation
```python
from paraview.simple import *
LoadPlugin("TopologyToolKit", remote=False, ns=globals())
reader = XMLImageDataReader(FileName=['/path/to/field.vti'])
reader.PointArrayStatus = ['elevation']
rename = RenameArrays(Input=reader)
rename.PointArrays = ['elevation', 'Scalars_']
rename.UpdatePipeline()
msc = TTKPathCompression(Input=rename)
msc.ScalarField = ['POINTS', 'Scalars_']
msc.OffsetField = ['POINTS', 'Scalars_']
msc.UpdatePipeline()
renderView = CreateView('RenderView')
renderView.ViewSize = [1920, 1080]
layout = CreateLayout(name='Layout')
layout.AssignView(0, renderView)
display = Show(msc, renderView, 'UniformGridRepresentation')
display.Representation = 'Slice'
ColorBy(display, ('POINTS', 'Scalars__MorseSmaleManifold'))
display.RescaleTransferFunctionToDataRange(True)
ResetCamera(renderView)
SaveScreenshot('/path/to/msc.png', renderView, ImageResolution=[1920, 1080])
```
---
## Export
```python
# Export tree nodes/edges
SaveData('/path/to/tree_nodes.vtk', proxy=OutputPort(ct, 0))
SaveData('/path/to/tree_edges.vtk', proxy=OutputPort(ct, 1))
# Export critical points
SaveData('/path/to/critical_points.vtk', proxy=cp)
# Export Morse-Smale segmentation (preserves grid + MSC array)
SaveData('/path/to/msc.vti', proxy=msc)
# Export persistence diagram
SaveData('/path/to/pd.vtk', proxy=pd_filter)
```
---
## Debugging & Error Handling
| Problem | Solution |
|---------|----------|
| `TTKPersistenceDiagram` not found | Call `LoadPlugin("TopologyToolKit", ...)` before any TTK filter — a bare filename is matched against registered plugin *names* and never resolves |
| TTK filter returns empty output | Check `UpdatePipeline()` was called; verify `ScalarField` array name is `Scalars_` |
| Array not found in filter | Use `RenameArrays` to rename input array to `Scalars_` before passing to TTK |
| Empty spheres / no geometry | `TTKIcospheresFromPoints` needs non-empty point input — check upstream filter output |
| Blank screenshot | Call `ResetCamera(renderView)` before `SaveScreenshot` |
| Wrong output port | Use `OutputPort(filter, 0)` for nodes, `OutputPort(filter, 1)` for edges (contour/merge tree) |
| `pvpython` not found | Add `$PARAVIEW_HOME/bin` to PATH or use full path |
| Plugin already loaded warning | Safe to ignore; TTK is loaded |
---
## Key Array Name Reference
| Source | Array Name | Values |
|--------|-----------|--------|
| After `RenameArrays` | `Scalars_` | Raw scalar values |
| `TTKScalarFieldCriticalPoints` | `CriticalType` | 0=min, 1=1-saddle, 2=2-saddle, 3=max, 4=degenerate |
| `TTKContourTree` (nodes port) | `CriticalType`, `Scalar`, `NodeId` | See above |
| `TTKPersistenceDiagram` | `Persistence`, `Birth`, `CriticalType`, `PairType` | — |
| `TTKPathCompression` | `Scalars__MorseSmaleManifold` | Region label integers |
---
## Task Execution
When given $ARGUMENTS:
1. Parse the task from the arguments
2. Write a self-contained Python script following the template above
3. Execute it with `pvpython script.py` (or `$PARAVIEW_HOME/bin/pvpython script.py`)
4. Read the output image with the Read tool to verify correctness
5. If the result needs adjustment, iterate (max 5 rounds)
6. Report the result to the user
Is this your skill, or is something wrong with this listing? Request removal or report an issue. Author removals are honored within 72 hours.
No comments yet. Be the first to comment!