v2026.11.16174
Guide / Working with… / Post-processing and visualisation

Post-processing and visualisation

zCFD by default will output data in VTK HDF format (.vtkhdf). This format can be read directly by many post-processing tools including ParaView by Kitware.

zCFD can also write EnSight output; see EnSight Output.

VTK HDF Output

By default, zCFD will output data in VTK HDF format (.vtkhdf). To force VTK HDF output explicitly, set format in the solution block of output_settings:

{ "output_settings": { "solution": { "format": "vtk" } } }

EnSight Output

To output in EnSight format, set format to ensight:

{ "output_settings": { "solution": { "format": "ensight" } } }

ParaView Catalyst Scripts

ParaView provides a lightweight interface that allows parallel processing of simulation data during program execution called ParaView Catalyst. This is driven by scripts (Python files) that can be called when data is output. The solver reads the list of scripts from scripts in the solution block. The output format must be vtk, and the scripts should be in the same directory as the input files:

{
  "output_settings": {
    "solution": {
      "format": "vtk",
      "scripts": ["paraview_catalyst1.py", "paraview_catalyst2.py"]
    }
  }
}

Output variables

The variables written to the solution files are named in surface_variables and volume_variables in the solution block. A monitor point samples the names in its own variables list from the volume set. The output variables page lists every name, with its meaning and units.

{
  "output_settings": {
    "solution": {
      "surface_variables": ["p", "cp", "yplus"],
      "volume_variables": ["V", "p", "T", "mach"]
    }
  }
}

Which names are valid depends on the equation type, set by type in equations. The tiers are cumulative: each equation type offers every name of the types above it in the table, and adds its own. The incompressible solver offers the same names as the compressible solver for the same equation type.

Equation typeNames offered
eulerThe base names, such as pressure, velocity and cp, and the Euler tier, such as vorticity and the pressure forces on a surface
viscousAdds the viscous tier, such as viscosity, nu, yplus, cf and the friction forces
rans or lesAdds the LES and RANS tiers, such as reynoldstress, ti and the turbulence model variables

Two further sets are offered on top of the equation type. The transition variables, turbulenceintermittency and transitionreynoldsnumber, are offered when the turbulence model is sst-transition, the model that solves for them. The modal variables, such as modaldeformation and meshvelocity, are offered when a wall boundary condition sets fsi.

Surface and volume sets differ. Some names, such as yplus, ut and cf, exist only on a surface; others, such as vorticity and ti, only in the volume.

In an unsteady run, compute_average_and_rms keeps a running average and RMS of velocity, pressure and temperature in each cell, updated once per real time step. Request them as V_avg, V_rms, p_avg, p_rms, T_avg and T_rms. Averaging starts at the real time step set by average_start_time_cycle; before that the statistics are reset every step. The RMS is of the value itself, not of its fluctuation, so the standard deviation is √(rms² − avg²). A restart reads the statistics back from the checkpoint and continues them. In a compressible run, requesting one of them without compute_average_and_rms stops the run.

var_i, resvar_i, resshare_i and vargrad_i are the solution value, residual, share of the total squared residual and gradient of solved equation i. They are offered for i from 1 up to the number of equations solved:

Equation typeTurbulence modelEquations solved
euler, viscous, lesnot used5
ranssa-neg6
ranssst-transition9
ransany other model7

A name outside the valid set fails validation, and the deck is rejected before the solver starts. The message names the variable, the equation type and the context; see Output variable rejected for the names most often mistaken.

ParaView Client

To download the free ParaView software for visualising VTK HDF format data on the local device, visit the ParaView download page. ParaView 6.1.1 or later is required for .vtkhdf file support.

Opening a run in ParaView

generate_paraview_helpers writes ParaView scripts that load a run's output, so a case with several models or boundary types opens in one step. Run it in the zCFD environment once the solver has written output, naming the control file:

(zCFD)> generate_paraview_helpers naca0012.py

It writes these scripts beside the control file:

ScriptWhat it loads
paraview_view_volumes.pyThe volume solution of each model
paraview_view_surfaces_<boundary>.pyOne boundary type, such as wall, for every model
paraview_view_all_surfaces.pyEvery boundary type, grouped by type
paraview_view_all.pyThe volumes and every boundary type

Each model of an overset case is loaded as its own source, named after its key under model, and given its own colour. A script loads only the files the run has written.

Open a script in the ParaView GUI with File > Run Script…, or run it with pvpython:

(zCFD)> pvpython paraview_view_all.py

When run with pvpython, a script also saves the pipeline as a ParaView state file of the same name, such as paraview_view_all.pvsm, which opens with File > Load State…. When connected to a ParaView server, it does not save the state.

OptionEffect
-o, --output-dirWrite the scripts to this directory, creating it if needed
--pythonWrite scripts that load the output without saving a state file

The scripts find the output through _base_dir, set near the top of each script to the directory they were written to. To open them where the run is mounted at a different path, change _base_dir to match.

ParaView Client-Server

In addition to running entirely locally on a device, ParaView can interface to a remote server. This allows users to run ParaView to access data that is held (and processed) on a different computer, potentially running larger simulation tasks that would not be possible on the local machine. This also means that there is no need to transfer the large output files back to a local machine for post-processing. The remote machine can be set up to run a ParaView Server in parallel - meaning that very large post-processing jobs can be run quickly.

ParaView Server

To download and install ParaView Server, you can either use the version on the main ParaView website and follow the official documentation.

Alternatively, use the version bundled with zCFD. The 'pvserver' included in the zCFD distribution includes plugins for reading the zCFD HDF5 mesh format directly.

To start a ParaView Server on your computer within the zCFD environment run:

> pvserver

ParaViewConnect

ParaViewConnect is a helper utility for connecting to remote ParaView servers. It reduces the complexity of setting up the required ssh connections and works well with zCFD. More information and installation instructions can be found on the GitHub page.

ParaView Post-Processing Best Practices for Parallel zCFD Data

When post-processing solution data generated in parallel by zCFD using ParaView, it is important to understand the differences between various ParaView filters and their impact on data interpolation. This section provides guidance on recommended pipelines for accurate results.

CleantoGrid vs MergeBlocks

Two commonly used ParaView filters for processing parallel datasets are CleantoGrid and MergeBlocks. Each has distinct characteristics:

CleantoGrid Filter:

  • Purpose: Merges points that are exactly coincident and removes degenerate cells
  • Interpolation: Performs minimal interpolation, preserving original data values at cell centres and vertices
  • Use case: Recommended when data accuracy is critical and you want to preserve the original computed values
  • Characteristics: May result in slightly fragmented visualisations at block boundaries but maintains data fidelity

MergeBlocks Filter:

  • Purpose: Combines multiple blocks into a single unstructured grid
  • Interpolation: May perform interpolation across block boundaries to create smooth transitions
  • Use case: Better for smooth visualisations and streamline generation across block boundaries
  • Characteristics: Creates visually smoother results but may alter original computed values through interpolation

For quantitative analysis and data extraction (probes, line plots, force calculations):

# Recommended pipeline for quantitative analysis
import paraview.simple as pvs

# Load data
reader = pvs.OpenDataFile('solution.vtkhdf')

# Use CleantoGrid to preserve data accuracy
clean_data = pvs.CleantoGrid(Input=reader)

# Convert to point data if needed
point_data = pvs.CellDatatoPointData(Input=clean_data)

# Proceed with analysis (probes, calculators, etc.)

For visualisation purposes (streamlines, contours, volume rendering):

# Pipeline for smooth visualisations
import paraview.simple as pvs

# Load data
reader = pvs.OpenDataFile('solution.vtkhdf')

# Use MergeBlocks for smoother visualisations
merged_data = pvs.MergeBlocks(Input=reader)

# Convert to point data for smooth contouring
point_data = pvs.CellDatatoPointData(Input=merged_data)

# Apply visualisation filters (streamlines, contours, etc.)

Important Considerations

  1. Data Accuracy: Always use CleantoGrid when extracting quantitative data for analysis or validation
  2. Visualisation Quality: MergeBlocks may provide better visual results for presentations and general visualisation
  3. Performance: CleantoGrid is typically faster as it performs less processing
  4. Block Boundaries: Be aware that different filters may show artefacts differently at parallel decomposition boundaries
  5. Regression Testing: When comparing results across different runs, ensure consistent filter usage

Loading data in a script

When processing zCFD parallel data, load the output one way for analysis and another for visualisation:

import paraview.simple as pvs


def load_for_analysis(filename):
    reader = pvs.OpenDataFile(filename)
    reader.UpdatePipeline()
    return pvs.CleantoGrid(Input=reader)


def load_for_visualisation(filename):
    reader = pvs.OpenDataFile(filename)
    reader.UpdatePipeline()
    merged = pvs.MergeBlocks(Input=reader)
    return pvs.CellDatatoPointData(Input=merged)

This keeps quantitative analysis faithful to the computed values and gives smooth visualisations.

Batch post-processing with Python scripts

Post-process a run with a Python script stored beside its control file. A script gives the same result on every run, sits under version control with the case, and runs unattended on a cluster login node or as a batch job. Every zCFD validation case is post-processed this way.

Run the script with the Python in the zCFD environment, which provides zutil, ParaView's paraview.simple and matplotlib:

(zCFD)> python naca0012_post.py

get_zcfd_result reads the control file and finds everything the run wrote, so a script need not build output paths by hand:

AttributeWhat it holds
report.dataThe report file as a pandas DataFrame, one column per heading
force_prefix()The prefix of the force columns, for building names such as wall_Ftx
volume_file_pathThe volume solution, <case>.vtkhdf
wall_boundary_pathThe wall surface solution; other boundary types have matching attributes, such as farfield_boundary_path
model_names, model_reportsFor an overset case, the model names and each model's report

The following script plots the density residual and a chordwise pressure coefficient for an aerofoil:

import matplotlib.pyplot as plt
import paraview.simple as pvs

from zutil.fileutils import get_zcfd_result
from zutil.post import clean_vtk, cp_profile

result = get_zcfd_result("naca0012.py")

report = result.report.data
fig, ax = plt.subplots()
ax.semilogy(report["Cycle"], report["rho"])
ax.set_xlabel("Cycle")
ax.set_ylabel("Density residual")
fig.savefig("naca0012_residuals.png", dpi=200)

wall = clean_vtk(pvs.VTKHDFReader(FileName=result.wall_boundary_path))
profile = cp_profile(wall, [0.0, 1.0, 0.0], [0.0, 0.5, 0.0])
fig, ax = plt.subplots()
ax.plot(profile[0]["chord"], profile[0]["cp"])
ax.invert_yaxis()
ax.set_xlabel("x/c")
ax.set_ylabel("Cp")
fig.savefig("naca0012_cp.png", dpi=200)

zutil.post also provides cf_profile for skin friction, calc_force_wall and calc_moment_wall for loads integrated from the surface, and get_monitor_data for monitor points. zutil.plot provides the axis, legend and logo helpers used for zCFD's own figures.

The output directory

The output directory is created in the working directory from which the solver is run, and is named after the control file:

<case_name>_OUTPUT

It holds one sub-directory for each entry under model, named by that entry's key, and a LOGGING sub-directory with one log for each MPI rank.

For the 'vtk' format the volume data of a model is written to its sub-directory with the file name:

<case_name>_OUTPUT/<model>/<model>.vtkhdf

For the 'vtk' format the surface data is written to the same sub-directory, one file for each boundary condition type:

<case_name>_OUTPUT/<model>/<model>_<boundary>.vtkhdf

The FWH interpolated surface data (fwh_interpolate) and FWH wall data (fwh_wall_data) are written to the ACOUSTIC_DATA sub-directory of the model's sub-directory, <case_name>_OUTPUT/<model>/ACOUSTIC_DATA.

What a checkpoint records about the run

A <model>_results.h5 checkpoint carries the run's own configuration alongside the flow solution, so the file explains where it came from without depending on the run directory still being intact:

Location in the fileContents
version (root attribute)Layout version of the checkpoint, currently 2. This is the file layout, not the deck's 'config version'.
/provenance/statusThe <case>_status.yaml file verbatim — zCFD version, date, host names, devices, partition count and the mesh MD5 checksums. Written once.
/provenance/control/<filename>The source of every Python file the control-file read executed, one dataset each, with the full path and mtime as attributes. The set is the whole include chain, not only the file named on the command line, so a deck built from a base plus overrides is recorded in full.

The control deck can be edited while a run is in progress — the solver polls it and applies any change the schema marks updatable — so it is re-read on each checkpoint and rewritten whenever it has actually changed. Reading it back is a plain HDF5 string read:

import h5py

with h5py.File("case_OUTPUT/background/background_results.h5") as f:
    print(f.attrs["version"])
    print(f["provenance/status"][()].decode())
    for name in f["provenance/control"]:
        print("---", f[f"provenance/control/{name}"].attrs["path"])
        print(f[f"provenance/control/{name}"][()].decode())

Recording these is best effort: a checkpoint that could not be annotated is still a valid, restartable checkpoint, and the run reports a warning rather than stopping.

The report file

zCFD has the capability to output certain parameters to a <casename>_report.csv file as the solver progresses. These can be used to monitor the solver's convergence. By default, the solver outputs volume integrated residuals for each solution variable to the <casename>_report.csv file, but adding to the report block means other quantities of interest can also be monitored as the simulation progresses. zMon plots these columns while the solver runs; see Monitoring a run.

'frequency' sets how frequently a new line is appended to the <casename>_report.csv file. If frequency = x, the solver will output to the <casename>_report.csv every x solver cycles. For a dual time-stepping simulation, this is the inner (pseudo-time), not outer, cycles.

'scale_residuals_by_volume' scales the reported volume-integrated residuals by cell volume. It applies to the resvar_i and resshare_i output variables as well as to the report, so the field and the norm of that field always describe the same quantity.

'monitor', 'forces' and 'mass_flow' are dictionaries keyed by whatever name is chosen for each block; the keys are not otherwise validated, but naming them consecutively with a prefix (e.g. MR_1, FR_1, MF_1) keeps a large control file readable:

Reporting onSuggested key prefix
'monitor'MR_?
'forces'FR_?
'mass_flow'MF_?

Example usage:

For a simulation with 1000's of cycles, it can be unnecessary to report every time cycle, which slows the solver down. For a dual timestepping simulation with 20 inner time cycles, outputting every 10 cycles generally gives enough information without substantially slowing the solver down

# Report every 10 cycles
"report": {
    "frequency": 10,
},

A simulation with two monitor points and one forces block

In addition to standard flow field outputs (see below), zCFD can provide information at monitor points in the flow domain, and integrated forces across all parallel partitions. Any number of monitor, forces or mass flow blocks may be specified.

"report": {
    "frequency": 10,
    "monitor": {
        "MR_1": {
            "point": [0.0, 0.0, 0.0],
            "variables": ["p", "V"],
        },
        "MR_2": {
            "point": [1.0, 0.0, 0.0],
            "variables": ["p", "V"],
        },
    },
    "forces": {
        "FR_1": {
            "zones": [4],
        },
    },
},

Cycle time column

Every row carries an 'AvgCycleTime' column: the mean wall-clock time, in seconds, of a solver cycle over every cycle of the run so far. It is the same measurement the console report block and the end-of-run summary give, recorded per row so the cost of a run can be read from its history. The last row is the run's mean cycle time.

The cycle timed is the solve - mesh mapping, the solver advance and source term updates - and excludes writing the report and the solution files, which happen after the cycle is timed.

Residual statistics columns

Setting 'residual_statistics' adds four columns per equation, describing the true residual R(Q)R(Q) rather than the solution update. They answer a question the RMS cannot: an RMS says "not converged" without saying that the field is converged everywhere except a handful of cells, which is the case that takes longest to diagnose.

The columns are named prefix-first (Rinf_rho, not rho_Rinf) and are written to <casename>_report.csv only - they are deliberately kept off the console residual line, which four extra columns per equation would otherwise swamp.

ColumnMeaning
Rinf_<eq>∥R∥∞\lVert R \rVert_\infty, the largest residual anywhere in the mesh for that equation.
Rrms_<eq>RMS of the true residual. Note this is not the equation's own residual column, which is the RMS of the solution update ΔQ/Δτ\Delta Q/\Delta\tau; the two coincide only when the linear solve is exact.
Neff_<eq>Participation ratio (∑R2)2/∑R4(\sum R^2)^2 / \sum R^4: the number of cells the residual is effectively spread over. Equal to the cell count when every cell contributes equally, and to 1 when a single cell carries everything. A small value against a large mesh means a localised stall, not a stalled field.
Rtop_<eq>Fraction of ∑R2\sum R^2 held by the single worst cell.

To find where those cells are, add the resshare_i volume output variables. resshare_i is each cell's share of the same ∑R2\sum R^2, summing to one over the mesh, so thresholding it at 1/Neff_<eq> isolates the cells carrying more than an equal share - the cells Neff is counting.

On by default. Set 'residual_statistics' to False to save the extra pass over the residual field and the two MPI reductions per report.

Monitor points

Monitor points report the specified variable at fixed mesh locations.

'point' should be the x,y,z location of the point of interest in the mesh as it appears in the solver (i.e. after any mesh scaling due to parameters scaling parameters).

'variables' controls which variables are reported at this point. In the <casename>_report.csv the column name will be a combination of the 'name' (or, if omitted, the block's own dictionary key in lower case, with spaces written as underscores) and the variable name.

'name' overrides the column heading used for variables from this monitor point. If omitted, the dictionary key given to the block is used instead, in lower case with spaces written as underscores: a block keyed MR_1 reports its pressure as mr_1_p, and one keyed Inlet Probe as inlet_probe_p.

Example usage:

Tracking pressure and velocity is useful for determining shedding cycle behaviour, or in the case of a scale resolving simulation, to determine the turbulence spectrum which is being resolved. It may be important to track other variables (e.g. turbulence intensity, ti) depending on the simulation setup.

"report": {
    "frequency": 10,
    "monitor": {
        "MR_1": {
            "name": "monitor_1",
            "point": [180.0, 100.0, 50.0],
            "variables": ["V", "p", "T", "ti"],
        },
    },
},

Forces

Force reporting outputs the x,y,z total forces and moments on a surface or set of surfaces. These are presented as force coefficients (using the non-dimensionalisation convention used for aerofoil lift, drag and moment coefficients), as opposed to dimensional values. The force and moment coefficients resulting exclusively from viscous forces and exclusively from pressure forces are also given.

Example usage:

"report": {
    "frequency": 10,
    # Report force coefficients in the grid axes, using a user defined transform
    "forces": {
        "FR_1": {
            "name": "force_1",
            # Required: Zones to be included
            "zones": [1, 2, 3],
            # Transformation function
            "transform": my_transform,
            "reference_area": 0.112032,
            "reference_length": 1.0,
            "reference_point": [0.0, 0.0, 0.0],
            # Calculate forces using a specified reference pressure rather than absolute
            "reference_pressure": 0.0,
        },
    },
},

Output names

The forces will be written into the <casename>_report.csv prefixed with the supplied 'name' (or, if not given, the block's own dictionary key in lower case, with spaces written as underscores, so a block keyed Main Wing writes main_wing_Fx). The forces will be written with the following names:

ItemReporting output name
Total forces and moments'name'_F[x,y,z] and 'name'_M[x,y,z]
Forces and moments due only to pressure forces'name'_Fp[x,y,z] and 'name'_Mp[x,y,z]
Forces and moments due only to viscous (friction) forces'name'_Ff[x,y,z] and 'name'_Mf[x,y,z]
Transformed total forces and moments (if transform function is set)'name'_Ft[x,y,z] and 'name'_Mt[x,y,z]
Transformed forces and moments due only to pressure forces (if transform function is set)'name'_Ftp[x,y,z] and 'name'_Mtp[x,y,z]
Transformed forces and moments due only to viscous forces (if transform function is set)'name'_Ftf[x,y,z] and 'name'_Mtf[x,y,z]

Total force

Total force, F\mathbf{F}, is Fp+Fv\mathbf{F}_p + \mathbf{F}_v, and likewise for moments.

Non-dimensionalisation

Forces are non-dimensionalised into force coefficients as Cx=Fxq∞SC_{x} = \frac{ \mathbf{F}_x }{ q_{\infty} S }, where q∞q_{\infty} is the reference dynamic pressure and SS is the reference area.

Moments are non-dimensionalised into moment coefficients as CMx=Mxq∞ScC_{Mx} = \frac{\mathbf{M}_x }{ q_{\infty} S c}, where cc is the reference length.

Dimensional pressure force

Fp=∮−(p−pref) n^ dA\mathbf{F_p} = \oint - (p-p_{ref}) \ \hat{\mathbf{n}} \ \textrm d A

Where pp is surface pressure and n^\hat{\mathbf{n}} is the surface normal (pointing towards the wetted area).

Dimensional viscous force

Fv=∮τw dA\mathbf{F}_v = \oint \boldsymbol{\tau}_w \ \textrm d A

Here τw\boldsymbol{\tau}_w is the wall shear stress vector. It lies in the surface and points along the velocity of the fluid relative to the wall.

Dimensional pressure moment

Mp=∮(x−xref)×(−(p−pref) n^) dA\mathbf{M}_p = \oint (\mathbf{x} - \mathbf{x}_{ref}) \times \left( -(p-p_{ref}) \ \hat{\mathbf{n}} \right) \ \mathrm d A

Where x\mathbf{x} is the surface position and xref\mathbf{x}_{ref} is the reference_point.

Dimensional viscous moment

Mv=∮(x−xref)×τw dA\mathbf{M}_v = \oint (\mathbf{x} - \mathbf{x}_{ref}) \times \boldsymbol{\tau}_w \ \mathrm d A

Mass flow

'zones' The mass flows through all the zones are summed to give a single output value for the mass flow monitor. Flow out of the domain is considered positive massflow, flow into the domain is considered positive massflow and the massflows are dimensional (typically kg per second).

The <casename>_report.csv column is always named after the block's dictionary key in lower case, with spaces written as underscores — e.g. a block keyed MF_1 reports as mf_1_massflow, and one keyed Total Inlet as total_inlet_massflow.

Two monitor, force or mass flow blocks whose keys differ only in case or spacing, such as Total Inlet and total_inlet, would write the same columns, so zCFD refuses the control file and names both keys.

Example usage:

"report": {
    "frequency": 10,
    # Report massflow
    "mass_flow": {
        "MF_1": {
            # Zones to be included
            "zones": [1, 2, 3],
        },
    },
},