PICurv 0.1.0
A Parallel Particle-In-Cell Solver for Curvilinear LES
 
Loading...
Searching...
No Matches
Configuration Reference: Postprocessor YAML

For the full commented template, see:

# ==============================================================================
#                 PICurv Master Post-Processing Profile
# ==============================================================================
#
# This file is the definitive template for post-processing tasks, derived
# directly from the capabilities of the C post-processor executable. All options
# here map to specific, implemented C functions and configuration keys.
#
# Use this as a guide to create specific analysis profiles (e.g., standard_analysis.yml).
#
# ==============================================================================
# SECTION 1: SOURCE DATA & RUN CONTROL
# ------------------------------------------------------------------------------
# Defines where to find the input data and which timesteps to process.
# This section directly configures the main processing loop in the C executable.
# ==============================================================================
# The input directory is not configurable: picurv always reads the run's own
# <run.output>, so a source_data.directory key is overwritten and plays no part in
# the recipe identity. Omit it.

run_control:
  # (Required) The first timestep in the logical analysis window.
  # Maps to 'startTime' in the generated C-side post.run recipe.
  # Keep this at the true beginning of the recipe window you care about.
  # Each `picurv run --post-process` processes only the steps of this window
  # whose output is missing or stale, so this value never needs editing.
  start_step: 0

  # (Required) The last timestep in the logical analysis window.
  # A value of -1 means "process up to the last available step".
  # On live solver runs, steps the solver has not committed yet are left for
  # a later run.
  # Maps to 'endTime' in the generated C-side post.run recipe.
  end_step: -1

  # (Optional) The interval for processing steps (e.g., 100 means process
  # steps 0, 100, 200...). Defaults to 1.
  # Must be positive. For a single checkpoint, set start_step = end_step and use 1.
  # For multiple checkpoints, use a multiple of monitor.yml -> io.data_output_frequency:
  # the solver only commits at that cadence, so a finer stride names steps that were
  # never written. Validation rejects a mismatch rather than letting the run stop
  # at the first missing step.
  # Maps to 'timeStep' in the generated C-side post.run recipe.
  step_interval: 100

# ==============================================================================
# SECTION 2: GLOBAL OPERATIONS
# ------------------------------------------------------------------------------
# These are operations that apply to the entire dataset at the beginning of each
# timestep's processing.
# ==============================================================================
global_operations:
  # (Optional) If true, converts all loaded fields (grid and data) to physical,
  # dimensional units before any other processing.
  # Corresponds to adding `DimensionalizeAllLoadedFields` to the C-side pipeline.
  dimensionalize: true

# ==============================================================================
# SECTION 3: PROCESSING PIPELINE
# ------------------------------------------------------------------------------
# Defines a sequence of operations to perform on the loaded data. The picurv
# script will serialize these lists into semicolon-separated strings for the
# C executable's 'process_pipeline' and 'particle_pipeline' keys.
# ==============================================================================
eulerian_pipeline:

  # Computes the Q-Criterion for vortex identification.
  # Creates a new cell-centred field named "Qcrit"; average it to nodes (below) before writing it.
  # Invokes the C function: `ComputeQCriterion()`.
  - task: q_criterion
  - task: nodal_average
    input_field: 'Qcrit'
    output_field: 'Qcrit_nodal'

  # (Optional) Performs cell-to-node averaging on specified fields.
  # For each field in the list (e.g., 'P'), this computes a new field
  # named with a "_nodal" suffix (e.g., 'P_nodal').
  # Invokes the C function: `ComputeNodalAverage(user, "P", "P_nodal")`.
  - task: nodal_average
    input_field:  'P'
    output_field: 'P_nodal'

  - task: nodal_average
    input_field:  'Ucat'
    output_field: 'Ucat_nodal'

  # (Optional) Normalizes a relative field (like pressure) by subtracting
  # the value at a specific grid index.
  # Invokes the C function: `NormalizeRelativeField()`.
  - task: normalize_field
    # The field to normalize. Currently, only "P" is supported by the C code.
    field: 'P'
    # The [i, j, k] grid indices of the reference point. The Python script will
    # generate 'reference_ip', 'reference_jp', 'reference_kp' keys in post.run.
    reference_point: [1, 1, 1]

# --- Lagrangian (Particle-Based) Pipeline ---
lagrangian_pipeline:
  # A list of operations performed on particle data.
  # Computes the specific kinetic energy (0.5 * |v|^2) for each particle.
  # Creates a new particle field named by output_field; list that same name in
  # io.particle_fields to write it. Invokes the C function: `ComputeSpecificKE()`.
  - task: specific_ke
    input_field: 'velocity'
    output_field: 'specific_ke'

# --- Global Particle Statistics Pipeline ---
# Optional reduction kernels that append CSV diagnostics per timestep.
# NOTE: Requires particle data in solver outputs (np > 0 at solve time).
statistics_pipeline:
  # Optional base name for CSV outputs (default: "Stats")
  output_prefix: "Stats"
  tasks:
    # Mean-square displacement from case.yml models.physics.particles.point_source.
    # Validation refuses it for a seeded case whose init_mode is not PointSource.
    - task: msd

# Deprecated: statistics_output_prefix at the top level was an older way to
# specify the statistics CSV prefix. Use statistics_pipeline.output_prefix instead.
# statistics_output_prefix: "Stats"

# --- Eulerian Field Statistics Recipe ---
# Derives saved accumulator state into scientific turbulence quantities. This is
# a separate contract from statistics_pipeline above: that one reduces particle
# trajectories, this one reads the field windows the solver accumulated through
# monitor.yml -> field_statistics.
#
# The recipe names windows rather than redescribing them, because the window
# definitions already reach the post-processor through the run's solver control.
# Omit this block to derive nothing.
# field_statistics:
#   # Window names, matching monitor.yml. Each window writes its own files, so a
#   # name may not be repeated.
#   windows: ["production"]
#
#   # Read every window's state from this step instead of from the step being
#   # processed. Omit to follow the processed step, which is what makes the CSV
#   # a convergence history rather than one row repeated.
#   # source_step: 4000
#
#   # Which derived quantities to write. Each resolves against what the window
#   # actually accumulated: a window keeping only first moments has no stresses,
#   # RMS, or TKE to give, and a recipe that would produce an empty file is
#   # rejected before the run rather than after it.
#   #   mean            -> the window mean of every accumulated field
#   #   reynolds_stress -> centered second moments; six components for a vector
#   #   rms             -> per-component root mean square of the fluctuation
#   #   tke             -> turbulent kinetic energy, for vector fields
#   #   flux            -> the cross-field co-moments listed under covariances
#   outputs: ["mean", "reynolds_stress", "rms", "tke", "flux"]
#
#   # vtk writes the derived fields as one .vts per window per processed step.
#   # csv appends one row per processed step to a per-window convergence history,
#   # recording sample count, weight, represented time, mask coverage, and the
#   # domain-mean TKE when the window has one.
#   formats: ["vtk", "csv"]

# --- Turbulent Energy Spectra ---
# Measures energy spectra from the instantaneous velocity in each committed
# checkpoint. Unlike every other block on this page, spectra are not computed by
# the C post-processor: the conductor runs generators/spectra.gen against the raw
# checkpoint payloads, so this stage needs no field output and does not depend on
# eulerian_pipeline. Omit the block to measure nothing.
#
# Run it on its own with:
#   picurv run --post-process --only spectra --run-dir <run> --post post.yml
# Re-measuring a spectrum is cheap; rebuilding the .vts output is not, so the
# analysis loop belongs behind --only spectra.
#
# OUTPUTS. Each task writes two CSVs under <monitor output>/spectra/: the spectra
# themselves as step,time,k,energy, and a per-step history beside them recording
# resolved_kinetic_energy, spectrum_total_energy, parseval_residual,
# spectrum_peak_k, zero_mode_energy, integral_length_scale, taylor_microscale,
# and dissipation_over_viscosity. Dissipation is divided by the kinematic
# viscosity, which the measurement does not need; both length scales need none
# either. parseval_residual is a self-check rather than an input: summed shell
# energy must equal the resolved kinetic energy of the transformed field, so a
# residual above round-off means the measurement is wrong.
#
# View the results with:
#   picurv summarize --run-dir <run> --plot-spectrum
#   picurv summarize --run-dir <run> --plot spectra.resolved_kinetic_energy
#
# PRECONDITIONS. A spectrum is only defined where the transform direction is
# uniformly spaced, periodic, and statistically homogeneous. picurv validate
# checks each task against case.yml before any field is read, so a case that
# does not meet them is refused by name rather than producing a meaningless curve.
#   shell_spectrum  every face PERIODIC, one block, uniform axis-aligned Cartesian
#                   grid; bins 3-D modes into |k| shells.
#   plane_spectrum  one block, Cartesian; the two transformed axes
#                   periodic and uniform, the third may stretch. A channel's
#                   wall-parallel planes qualify.
#   line_spectrum   as plane_spectrum, one transformed axis.
# Plane and line tasks take symbol: continuum only, require `axes` and zero-based
# `fixed_indices` on every other axis, and accept subtract_mean: sample (the mean
# of the sampled plane or line). A curvilinear case such as a bend cannot use
# any of them, and every task needs a staged grid: grid.mode grid_gen or file,
# not programmatic_c.
# spectra:
#   # Prefix for the CSV basenames written under <monitor output>/spectra/.
#   output_prefix: "Spectrum"
#
#   tasks:
#     - task: shell_spectrum
#
#       # Catalogued field to transform. Ucont is component-staggered and has no
#       # single cell-centered spectrum, so it is not accepted.
#       field: Ucat
#
#       # Block index within the PICGRID.
#       block: 0
#
#       # Binning abscissa.
#       #   continuum -> bins by |k|
#       #   discrete  -> bins by the centered-difference symbol sin(k dx)/dx,
#       #                which is what the solver's own operator resolves.
#       # Reading a high-k rolloff against the continuum abscissa alone will
#       # attribute the scheme's damping to physics.
#       # The abscissa applies to the binning and to the k^2 moment, so
#       # dissipation_over_viscosity legitimately differs between the two: the
#       # discrete operator resolves less gradient and so dissipates less.
#       # integral_length_scale is the exception. Its 1/k moment is singular
#       # under the discrete symbol, which vanishes on the Nyquist planes, so it
#       # is always taken on the physical wavenumber and reads the same either way.
#       symbol: continuum
#
#       # Fluctuation definition.
#       #   none            -> transform the field as stored
#       #   domain          -> remove the volume mean of that snapshot
#       #   window:<name>   -> subtract an accumulated field-statistics window
#       #                      mean, which is the better estimate wherever the
#       #                      flow is not homogeneous
#       # A window mean is only defined where the window actually sampled, so a
#       # window that has not accumulated over the domain is refused rather than
#       # subtracted as zero.
#       subtract_mean: none
#
#       # Bundle the window mean is read from, independent of the step being
#       # transformed. Applies only to subtract_mean: window:<name>. Left unset,
#       # each step reads its own bundle, whose running mean is undefined at the
#       # moment the window activates. Pin it at or after the window closes to
#       # subtract the converged mean.
#       # mean_source_step: 4000
#
#       # Optional baseline to overlay when plotting. The placeholder resolves to
#       # the spectrum recorded for a generated initial condition.
#       # reference: "<ic_spectrum>"
#
#     # A wall-parallel plane of a channel whose walls are on j:
#     - task: plane_spectrum
#       field: Ucat
#       axes: [i, k]
#       fixed_indices: { j: 8 }
#       subtract_mean: sample
#
#     # A streamwise line:
#     - task: line_spectrum
#       field: Ucat
#       axes: [k]
#       fixed_indices: { i: 16, j: 8 }
#       subtract_mean: sample

# ==============================================================================
# SECTION 4: OUTPUT CONFIGURATION
# ------------------------------------------------------------------------------
# Controls what data is written to the final VTK files.
# ==============================================================================
io:
  # Compatibility input: the conductor replaces this with the canonical
  # <run.visualization>/<recipe_id>/ path. It does not redirect output.
  output_directory: "visualization/standard_analysis"

  # (Required) The base name for the Eulerian output files. The C code will append
  # the timestep and extension, e.g., "eulerian_data_00100.vts".
  # Combined with output_directory to form 'output_prefix' in post.run.
  output_filename_prefix: "eulerian_data"

  # (Optional) The base name for particle output files. Defaults to "Particle".
  # Combined with output_directory to form 'particle_output_prefix' in post.run.
  # Maps to the basename of 'particle_output_prefix' in post.run.
  particle_filename_prefix: "Particle"

  # (Optional) Write physical-time ParaView .pvd collections after successful field
  # postprocessing. "run" indexes only this run; "lineage" follows recorded restart
  # ancestry, clips each parent at its child's fork, and references compatible VTK
  # files in place. The setting does not change computational recipe identity.
  # Omission disables collections. Ancestors need the same recipe ID: changing
  # post step_interval or fields changes that ID; changing the window does not.
  paraview_series:
    enabled: true
    scope: lineage

  # (Required) List of Eulerian fields to save to the output .vts files.
  # You can include original fields ('P', 'Ucat') and newly computed fields
  # from the pipeline ('Qcrit_nodal', 'P_nodal', 'Ucat_nodal'). Cell-centred fields such as
  # 'Qcrit' cannot be written directly: nodal-average them first.
  # Maps to 'output_fields_instantaneous' in post.run.
  eulerian_fields:
    - 'Ucat_nodal'
    - 'Qcrit_nodal'
    - 'P_nodal'

  # (Optional) A master switch for all particle-related output. Defaults to false.
  # Maps to 'output_particles' in post.run.
  output_particles: true

  # (Optional) List of particle fields to save to the output .vtp files.
  # Can include original fields and newly computed fields like 'specific_ke'.
  # Standard Swarm Fields: 'position', 'velocity', 'pid', 'CellID', 'weight'
  # Maps to 'particle_fields_instantaneous' in post.run.
  particle_fields:
    - 'velocity'
    - 'pid'
    - 'specific_ke'

  # (Optional) The frequency for subsampling particles for output.
  # A value of 1 saves every particle. A value of 10 saves every 10th particle.
  # Defaults to 1 if not specified.
  # Maps to 'particle_output_freq' in post.run.
  particle_subsampling_frequency: 1

  # (Optional) Checkpoint payload extension contract. Committed bundles use
  # fixed PETSc-vector .dat payloads, so both values must remain "dat".
  input_extensions:
    eulerian: "dat"
    particle: "dat"

post.yml defines postprocessing input range, processing pipelines, statistics tasks, and VTK output selection.

It carries two separate statistics contracts, which are deliberately not combined. statistics_pipeline reduces particle trajectories; field_statistics derives the Eulerian field windows the solver accumulated. Particle MSD and resolved turbulence statistics answer different questions and share no state. A third block, spectra, measures energy spectra from the instantaneous velocity; unlike every other block here it runs in the conductor rather than in the C post-processor.

1. File Structure

run_control:
start_step: 100
end_step: 1000
step_interval: 100
global_operations:
dimensionalize: true
eulerian_pipeline:
- task: nodal_average
input_field: Ucat
output_field: Ucat_nodal
- task: q_criterion
- task: nodal_average
input_field: Qcrit
output_field: Qcrit_nodal
lagrangian_pipeline:
- task: specific_ke
input_field: velocity
output_field: SpecificKE
statistics_pipeline:
output_prefix: "Stats"
tasks:
- task: msd
field_statistics:
windows: [production]
outputs: [mean, reynolds_stress, rms, tke, flux]
formats: [vtk, csv]
spectra:
output_prefix: "Spectrum"
tasks:
- task: shell_spectrum
field: Ucat
io:
output_directory: "viz"
output_filename_prefix: "Field"
particle_filename_prefix: "Particle"
paraview_series:
enabled: true
scope: lineage
output_particles: true
eulerian_fields: [Ucat_nodal, Qcrit_nodal]
particle_fields: [velocity, SpecificKE]

2. run_control

Mappings in generated post.run:

  • start_step -> startTime
  • end_step -> endTime
  • step_interval -> timeStep

step_interval must be positive; zero and negative values are rejected by both YAML validation and the C postprocessor. To process one checkpoint, set start_step and end_step to that checkpoint and use step_interval: 1.

Requested steps must be steps the solver committed. The solver writes a checkpoint every monitor.yml -> io.data_output_frequency completed steps, plus the initial and final states. A selection requesting multiple checkpoints must use an interval that is a multiple of that cadence. A bounded selection containing only one checkpoint is exempt from this alignment check. An off-cadence start_step draws a warning because it may be an initial or final checkpoint; the selected checkpoint must still exist. A requested step without a committed checkpoint is not processed; it waits until the solver writes it.

Operational semantics when launched through picurv:

  • keep start_step and end_step as the full logical analysis window you want the recipe to represent.
  • picurv run --post-process --run-dir ... --post post.yml processes only the requested steps whose output is missing or stale, so you never edit start_step during batch catch-up. A step is up to date while all its output files exist and its record in <recipe>/state.json names the current recipe and the checkpoint now on disk. A field-statistics window's file is required only at steps whose source checkpoint records a sample for it. --recompute regenerates every step (see 4. run: Single-Case Workflow).
  • if the recipe changes in a way that affects generated outputs, such as step_interval, pipeline tasks, output prefixes, or selected fields, it is a new recipe with its own output directory, and its whole window is processed.
  • on live solver runs, end_step: -1 still means "up to the last available step"; each invocation processes the steps whose checkpoints are committed and leaves the rest for a later run.
  • the conductor hands the postprocessor an explicit step list (step_list_file in post.run, written when the job starts); startTime/endTime/timeStep then only describe the requested window.

3. source_data

  • source_data is optional; omit it. picurv always sets source_directory to the run's own <run.output>, so a configured source_data.directory (including the legacy <solver_output_dir> placeholder) is overwritten, and picurv validate warns about any other value. Omitting the block, leaving it empty, or carrying the old directory all give the same recipe ID.
  • for live post-processing while the solver is still running, PICurv treats a timestep as source-available only when its checkpoint bundle has a valid checkpoint.meta/COMMITTED pair and complete payload inventory. A recipe that needs particles also requires committed particle state.

4. Processing Pipelines

Eulerian tasks (eulerian_pipeline):

  • q_criterion -> ComputeQCriterion
  • nodal_average -> CellToNodeAverage:<in>><out>
  • normalize_field -> NormalizeRelativeField:<field>

Global operation:

  • global_operations.dimensionalize: true reports every output in physical units. It is a setting, not a pipeline stage: each producer scales its own values once, where they are made. Loaded fields are scaled by their catalog dimension as they are read (Ucat by U_ref, Ucont by U_ref L_ref^2, P by rho U_ref^2), grid coordinates by L_ref once, particle positions and velocities before the Lagrangian tasks run, derived statistics in the accumulator, spectra in their generator, Qcrit by (U_ref / L_ref)^2, and the msd columns as they are written. Without it, every output is in solver units. The per-producer table and the per-field index are on 5. Output.

Lagrangian tasks (lagrangian_pipeline):

  • specific_ke -> ComputeSpecificKE:<in>><out>

4.1 Eulerian Post-Processing Kernel Entries

Value Maps to
nodal_averagenodal_average
normalize_fieldnormalize_field
q_criterionq_criterion

q_criterion

Identity. eulerian_pipeline: [{task: q_criterion}] -> ComputeQCriterion in the -process_pipeline chain.

What it does. Computes the Q-criterion, the second invariant of the velocity-gradient tensor, as a new cell field. Positive Q marks regions where rotation dominates strain.

When to choose it. For vortex visualisation - isosurfaces of Q are the standard way to see coherent structures in a turbulent field. It answers 'where are the vortices', not 'how strong is the turbulence'; use field statistics for the latter.

Parameters it owns. None. It reads the loaded velocity field and writes one derived field.

Interactions. Requires Ucat to be loaded, which the standard source data provides. Order within eulerian_pipeline matters only against tasks that rewrite velocity; it does not consume the output of nodal_average.

Diagnostics. The derived field appears in the written .vts and in the post-processor's field listing. Its absence there means the task never ran.

Evidence. Unit verified - make unit-post. Analytically verified - q-criterion-nodal-tgv-2026-09-18: Qcrit_nodal matched the analytic Q of the TGV3D field to 3.2% RMS at the node positions a .vts assigns it.

Limitations. Q is a diagnostic, not a threshold: the isovalue that reveals structure is flow-dependent and this task chooses none for you. Qcrit is computed at cell centres, and a .vts carries point data only, so it cannot be written directly - validation refuses it. Follow the task with nodal_average from Qcrit to Qcrit_nodal and write that; averaging to nodes smooths it slightly, as it does every nodal field.

normalize_field

Identity. eulerian_pipeline: [{task: normalize_field, field: P, reference_point: [i,j,k]}] -> NormalizeRelativeField:<field>.

What it does. Subtracts the value at a chosen reference point from a field, so what is written is the field relative to that point rather than its absolute level.

When to choose it. For pressure, always, in any flow without a pressure boundary condition. The incompressible pressure is determined only up to a constant, so absolute values are not comparable between runs, timesteps, or solvers - and a plot of them is a plot of an arbitrary offset.

Parameters it owns. field (currently P only) and reference_point, a three-item cell index that becomes reference_ip/jp/kp.

Interactions. Applies to the loaded field before any writer sees it. Choosing a reference point inside a recirculation or a boundary layer makes the normalized field harder to compare, not easier - prefer a quiescent interior cell.

Diagnostics. The written field carries the same name; the difference is visible as a shifted range. A reference point outside the block is a validation error naming the index.

Evidence. Unit verified - make unit-post. Analytically verified - post-pipeline-dimensionalize-2026-09-21: P normalized at a cell and averaged to nodes equals the nodal mean of P - P[4,4,4] to 2.2e-16, and scales by rho U_ref^2 exactly once when dimensionalized.

Limitations. Only P is accepted today, and the reference point is a fixed index rather than a coordinate, so it does not follow a moving feature or survive a grid change.

nodal_average

Identity. eulerian_pipeline: [{task: nodal_average, input_field: X, output_field: Y}] -> CellToNodeAverage:X>Y.

What it does. Averages a cell-centred field onto grid nodes, writing the result as a separate named field. Accepted inputs are P, Ucat, Psi, and Qcrit, written to P_nodal, Ucat_nodal, Psi_nodal, and Qcrit_nodal; a .vts carries point data only, so a cell-centred field reaches the file through this task.

When to choose it. When a downstream consumer expects nodal data - some visualisation filters and line-extraction tools interpolate badly from cell data - or when comparing against a reference that is defined at nodes. Leave cell fields alone otherwise: averaging is a smoothing operation and loses information.

Parameters it owns. input_field and output_field, which must differ. The audit rejects an in-place average because it would destroy the source the rest of the pipeline reads.

Interactions. Runs after any task that produces the input field, so a q_criterion output can be averaged by listing nodal_average later in the pipeline.

Diagnostics. Both fields appear in the written .vts. A missing output field means the input name did not match anything loaded.

Evidence. Unit verified - make unit-post. Analytically verified - post-pipeline-dimensionalize-2026-09-21: Ucat_nodal equals the eight-cell mean, ghost cells included, to 2.2e-16; q-criterion-nodal-tgv-2026-09-18 measures the smoothing it adds to Qcrit_nodal.

Limitations. Averaging is a low-pass filter: peak values move toward their neighbourhood mean, so nodal fields understate extrema and should not be used for max-value claims. A node on the domain boundary also averages the dummy cells beyond it, so what they hold decides the boundary node. Ucat's put the face average on the boundary velocity; P's and Psi's repeat the adjacent cell (zero normal gradient), so P_nodal and Psi_nodal continue the interior there (UpdateDummyCells()). Qcrit and the statistics staging fields are filled on periodic faces only (SynchronizePeriodicCellFields()); on a wall, inlet, or outlet their dummy cells stay zero, so the outermost node layer of Qcrit_nodal and of a statistics field is diluted toward zero - see 12. Derived Statistics At Non-Periodic Boundaries for why no single rule is applied there. Runs written before 2026-09-28 carry zero P and Psi dummy cells in their checkpoints, and their boundary nodes are diluted the same way.

4.2 Lagrangian Post-Processing Kernel Entries

Value Maps to
specific_kespecific_ke

specific_ke

Identity. lagrangian_pipeline: [{task: specific_ke, input_field: X, output_field: Y}] -> ComputeSpecificKE:X>Y.

What it does. Computes specific kinetic energy - half the squared magnitude of a per-particle vector field - and stores it as a new scalar swarm field.

When to choose it. When particle energetics are the question: comparing swarm energy against the carrier flow, or watching a swarm equilibrate after seeding. For dispersion questions use the particle statistics pipeline instead.

Parameters it owns. input_field (a particle vector field, typically the particle velocity) and output_field (the scalar to write).

Interactions. Reads a swarm field, so it requires particle data in the source bundle. It does not interact with the Eulerian pipeline, which runs over a different data structure.

Diagnostics. The derived scalar appears in the written particle output. An input name that matches no swarm field leaves the output absent rather than zero.

Evidence. Unit verified - make unit-post. Analytically verified - post-pipeline-dimensionalize-2026-09-21: half the squared particle velocity exactly, and U_ref^2 times that when dimensionalized.

Limitations. Specific energy per particle, not per unit mass of a distribution: it carries no particle mass or number weighting, so summing it across a swarm is not a physical total unless every particle represents the same mass.

5. Particle Statistics Pipeline

This is the particle-reduction path. For Eulerian turbulence statistics see 6. field_statistics.

statistics_pipeline supports either:

  • list form, or
  • mapping with tasks and optional output_prefix

Currently supported statistics task is msd, which picurv serializes as the ComputeMSD pipeline token consumed by the C dispatcher before it calls ComputeParticleMSD.

Mappings:

  • tasks -> statistics_pipeline
  • output_prefix -> statistics_output_prefix

6. field_statistics

Derives the accumulated field windows configured at monitor.yml -> field_statistics into scientific turbulence quantities. Omit the block to derive nothing.

field_statistics:
windows: [production]
source_step: 4000 # optional; defaults to the step being processed
outputs: [mean, reynolds_stress, rms, tke, flux]
formats: [vtk, csv]

Mappings:

  • windows -> field_statistics_windows
  • outputs -> field_statistics_outputs
  • formats -> field_statistics_formats
  • source_step -> field_statistics_source_step

The recipe names windows rather than redescribing them. The post-processor is launched with the run's own control file and resolves the window definitions through the same parser the solver used, so there is no second description to fall out of step with the first.

Available outputs, each resolved against what the window actually accumulated:

Output Produces Requires
mean the window mean of every accumulated field —
reynolds_stress centered second moments; six components for a vector moments: [second]
rms per-component root mean square of the fluctuation moments: [second]
tke turbulent kinetic energy moments: [second] on a vector
flux the cross-field co-moments a covariances entry

A recipe that would produce an empty file — asking for rms from a window that kept only first moments, for instance — is rejected before the run rather than after it.

vtk writes one .vts per window per processed step. csv appends one row per processed step to a per-window convergence history recording sample count, accumulated weight, represented time, mask coverage, and the domain-mean TKE. The CSV is what answers whether a window has run long enough; no single snapshot can.

Because the pipeline runs once per processed step, a recipe spanning a range produces a history. Pinning source_step reads every processed step's output from that one bundle instead, collapsing the history to a single answer.

Each window writes its own files, so a window name may not be listed twice.

Derived statistics follow global_operations.dimensionalize. Each carries the source field's reference scale raised to its own power: a mean and an RMS are linear in it, a Reynolds stress, a turbulent kinetic energy and a co-moment flux quadratic. Every statistics-eligible field has a catalog dimension; the time columns of the history CSV (represented_time, and total_weight under physical_time weighting) are reported in seconds too. See 8. Derived Quantities and Post-Processing.

6.1 Field Statistics Output Entries

Value Maps to
fluxflux
meanmean
reynolds_stressreynolds_stress
rmsrms
tketke

Every output below was checked against a known answer by field-statistics-tgv3d-2026-09-21: on the analytic TGV3D field, each written value equals the eight-cell average of the exact cell statistic to round-off. Written fields are node averages of cell statistics, so a nodal rms is the mean of the neighbouring cell RMS values, not the root of an averaged variance. None has yet been compared against a turbulent reference profile.

Note
**formats is a parameter, not a choice between behaviours.** vtk writes the derived fields listed below into the window's bundle; csv appends one row per processed step carrying sample count, total weight, represented time, the per-point valid-fraction range, and the mean turbulent kinetic energy. Both are written from the same accumulated state, so requesting both costs output volume rather than computation. The csv is the artefact that answers whether a window converged; the vtk is the one that shows what it converged to. tests/tooling/family_census.json records why this is classified as a parameter of these entries rather than as a family of its own.

mean

Identity. field_statistics.outputs: [mean].

What it does. Writes the accumulated first moment of each tracked field, divided by the window's total weight - the time-averaged field over the window.

When to choose it. Always, effectively: every other output is defined against the mean, and a mean field is what most comparisons against a reference profile need. It is also the cheapest output, since the first moment is accumulated regardless.

Parameters it owns. None. The fields averaged, the window bounds, and the weighting are properties of the window, not of this output.

Interactions. Requires an accumulated window. Reads the same state reynolds_stress, rms, and tke are derived from, so requesting several outputs costs one accumulation.

Diagnostics. Written into the window's vtk bundle; the csv row reports the sample count and total weight behind it.

Evidence. Unit verified - make unit-statistics. Analytically verified - field-statistics-tgv3d-2026-09-21: the window mean of Ucat and P equals the exact weighted mean of the sampled states to 7e-16.

Limitations. A mean over a window that has not converged is still a mean: the output carries no statement about whether the averaging interval was long enough. The csv convergence history is what answers that.

reynolds_stress

Identity. field_statistics.outputs: [reynolds_stress].

What it does. Writes the full Reynolds stress tensor R_ij = C_ij / W, the centred second moment divided by total weight - six independent components.

When to choose it. For anything that needs the anisotropy of the turbulence rather than its magnitude: budget terms, near-wall structure, comparison against DNS stress profiles. Choose rms instead if only the diagonal matters, and tke if only the trace does.

Parameters it owns. None; it is derived from the accumulated second moment.

Interactions. Requires the window to accumulate second moments. rms and tke are functions of the same tensor, so requesting all three adds output volume, not computation.

Diagnostics. Six fields per window in the vtk bundle. One window carrying every output already writes fifteen fields against a per-file limit of twenty, which is why windows cannot share a file.

Evidence. Unit verified - make unit-statistics. Analytically verified - field-statistics-tgv3d-2026-09-21: all six Ucat components and the P variance equal their exact weighted values to 3e-18.

Limitations. Off-diagonal components converge more slowly than the diagonal, so a window long enough for rms is not necessarily long enough for the shear stress.

rms

Identity. field_statistics.outputs: [rms].

What it does. Writes the root-mean-square fluctuation of each component, sqrt(R_ii) - the diagonal of the Reynolds stress tensor under a square root.

When to choose it. When the question is fluctuation magnitude per direction: turbulence intensity profiles, and the usual first comparison against a reference. It is reynolds_stress with the cross terms dropped and a square root applied.

Parameters it owns. None.

Interactions. Derived from the same second moment as reynolds_stress and tke. Small negative variances from floating-point cancellation are clamped only when taking the square root, and only within a documented tolerance - stored state is never mutated.

Diagnostics. Three fields per window. The csv row's valid-fraction range is the check on whether every point had enough samples.

Evidence. Unit verified - make unit-statistics. Analytically verified - field-statistics-tgv3d-2026-09-21: Ucat and P RMS values equal their exact values to 5e-17.

Limitations. The clamp means an RMS of exactly zero can mean 'no fluctuation' or 'variance below the cancellation floor'; the sample count distinguishes them.

tke

Identity. field_statistics.outputs: [tke].

What it does. Writes turbulent kinetic energy, k = (R_xx + R_yy + R_zz) / 2 - half the trace of the Reynolds stress tensor.

When to choose it. For a single scalar summarising turbulence level, and for anything that plots decay or growth of turbulence over time. It is the natural quantity for isotropic decay studies, where the anisotropy reynolds_stress reports is not the point.

Parameters it owns. None.

Interactions. A scalar contraction of the same tensor reynolds_stress and rms come from. The csv output reports the mean value of this field per processed step, which is the convergence signal for the window as a whole.

Diagnostics. One field per window, plus the per-step mean in the csv.

Evidence. Unit verified - make unit-statistics. Analytically verified - field-statistics-tgv3d-2026-09-21: the written TKE equals its exact value to 2e-18, and the CSV mean_tke to 3.7e-12.

Limitations. Being a trace, it is blind to anisotropy: two flows with very different stress structure can report the same k.

flux

Identity. field_statistics.outputs: [flux].

What it does. Writes the centred cross-moment of each vector-scalar pair the window lists under covariances, u_i's' = C_is / W, one field per pair named <window>_<vector>_<scalar>_flux. With Ucat and P it is the velocity-pressure correlation u_i'p' that appears in the turbulent transport of kinetic energy; with Ucat and a transported scalar it is that scalar's turbulent flux.

When to choose it. When the question is how turbulence carries a quantity: the pressure-transport term of a kinetic-energy budget, or a heat, species or other scalar flux against which a gradient-transport closure is assessed.

Parameters it owns. None of its own; which pairs exist is a property of the window's covariances list.

Interactions. Requires at least one covariances entry on the window, both members cell-centred; a recipe asking for flux from a window without one is rejected before the run. A pair with a transported scalar produces a flux only when the particle scalar is non-zero, which today only the verification scalar source makes it (1. IEM Mixing Update In Current Code).

Diagnostics. Three fields per window, one per velocity component.

Evidence. Unit verified - make unit-statistics. Analytically verified - field-statistics-tgv3d-2026-09-21: the Ucat-P co-moment equals its exact value to 1.4e-18.

Limitations. A cross-moment converges more slowly than either factor's own variance, so flux is the output most likely to be under-converged in a given window.

7. Particle Statistics Task Entries

Value Maps to
msdmsd

msd

Identity. Statistics pipeline task msd -> pipeline keyword ComputeMSD -> ComputeParticleMSD.

What it does. Computes the mean squared displacement of the particle swarm over time and writes it as a CSV time series, together with the cloud's centre of mass. Displacement is measured from the configured point source (point_source.x/y/z), not from each particle's own start and not from the cloud's centre, so a drifting cloud's MSD_total includes its squared drift: the spread about the centre is MSD_total - |com|^2.

When to choose it. Characterizing dispersion. MSD against time is the standard way to read a diffusion coefficient out of a particle simulation, which is what the Brownian and drift verification cases do.

Parameters it owns. The statistics pipeline block that selects it; the output file name follows the configured statistics prefix.

Interactions. Requires particles. It reads particle positions only, so it is unaffected by the Eulerian field source.

Diagnostics. The emitted CSV is the diagnostic: a linear MSD in time indicates diffusive behaviour, a quadratic one indicates ballistic transport. The comparison with r_rms_theory = sqrt(6 D t), D = 1/(Re Sc), is made in solver units. Under dimensionalize: true the dimensional columns are written physically (t times L_ref/U_ref, MSD_* times L_ref^2, radii and centre of mass times L_ref); the relative error and shell fractions are ratios and are the same in either system.

Evidence. Unit verified - make unit-statistics covers the MSD kernel including its empty-swarm behaviour. Analytically verified - brownian-msd-2026-09-18: the MSD slope of a Brownian cloud matched the Einstein relation to 0.17%.

Limitations. The only statistics task currently exposed. It is a whole-swarm measure with no spatial conditioning. It is meaningful only for init_mode: PointSource: with any other seeding the reference point would be the unset point source, the coordinate origin, so validation refuses msd for a seeded case that does not use PointSource.

8. spectra

Measures turbulent energy spectra from the instantaneous velocity in each committed checkpoint. Omit the block to measure nothing.

spectra:
output_prefix: "Spectrum"
tasks:
- task: shell_spectrum
field: Ucat
block: 0
symbol: continuum # continuum | discrete
subtract_mean: none # none | domain | window:<name>
mean_source_step: 4000 # optional; only with window:<name>

Unlike every other block on this page, spectra are not computed by the C post-processor. They are measured by generators/spectra.gen, which the conductor runs against the raw checkpoint payloads, so the stage needs no field output and does not depend on eulerian_pipeline. The only key that reaches the C recipe is spectra_signature, a digest the post-processor accepts and ignores; it exists so a changed spectra recipe reaches the recipe fingerprint each step's record carries.

Running spectra alone

picurv run --post-process --only spectra --run-dir runs/my_run --post post.yml

--only selects among the post stages, fields (the field post-processor) and spectra, and defaults to both. Re-measuring a spectrum is cheap; rebuilding the field output is not, so the analysis loop belongs behind --only spectra.

Each task's spectrum and history CSVs accumulate across invocations of one recipe. Like field output, a step is measured only when its rows are missing, its spectra settings changed, or its checkpoint changed, and --recompute measures every committed step. An invocation replaces the rows of the steps it measures and keeps the rest. A file whose columns differ from the current format is rewritten whole.

Preconditions

A spectrum is only defined where the transform direction is uniformly spaced, periodic, and statistically homogeneous. Each task declares what it needs, and picurv validate checks it against the case before any field is read:

Task Status Requires
shell_spectrum supported every face PERIODIC, a single block, and a uniform axis-aligned Cartesian grid
plane_spectrum supported two selected periodic uniform axes; remaining axis may stretch
line_spectrum supported one selected periodic uniform axis; remaining axes may stretch
temporal_spectrum planned no homogeneous direction needed; see 14. Temporal Spectra From Bounded Probe Histories

Plane and line tasks sample actual cell-center planes and lines in a single-block, axis-aligned Cartesian grid. They do not average parallel samples. Use axes: [i, k] and fixed_indices: {j: 32} for a plane, or axes: [k] and fixed_indices: {i: 32, j: 32} for a line. Indices are zero-based physical cells, excluding dummy layers. Out-of-range selections fail when the staged grid is read. Every spectra task reads the staged PICGRID, so the case must use grid.mode: grid_gen or file; a programmatic_c case is refused at validation.

Boundary conditions and block count are checked from case.yml; grid uniformity is checked by the generator against the staged PICGRID, which is the only place the node coordinates are known. A wall-bounded or curvilinear case is refused by name rather than yielding a curve with no meaning:

spectra task 'shell_spectrum' requires every face of block 0 to be PERIODIC,
because a shell-averaged spectrum is only defined for a triply periodic
homogeneous box. Non-periodic faces: ['+Eta', '+Xi', ...].

This is why a 90-degree bend cannot produce a spatial spectrum at all: it develops streamwise and is bounded on all four sides, so it has no homogeneous direction and needs 14. Temporal Spectra From Bounded Probe Histories. A straight duct or channel is a different case — it does have periodic directions that the plane and line tasks can transform.

Keys

  • symbol selects the shell binning abscissa. Plane/line tasks require continuum. continuum bins by \(|k|\); discrete bins by the centered-difference symbol \(\sin(k\,\Delta x)/\Delta x\), which is what the solver's own operator resolves. Comparing a measured rolloff against the continuum abscissa alone will attribute the scheme's damping to physics.
  • subtract_mean chooses the fluctuation. none transforms the field as stored; sample removes the selected plane/line mean (plane/line tasks only); domain removes the arithmetic mean of all physical cells of that snapshot; window:<name> subtracts the accumulated mean of a field-statistics window, which is the better estimate wherever the flow is not homogeneous. A window mean is only defined where the window actually sampled, so a window that has not accumulated over the domain is refused rather than subtracted as zero.
  • mean_source_step pins the bundle the window mean is read from, independently of the step being transformed. It applies only to window:<name>. Left unset, each step reads its own bundle, whose mean is the running mean at that step — undefined at the moment the window activates, and built from few samples shortly after. Pin it to a step at or after the window closes to subtract the converged mean, which is almost always what is wanted. This mirrors field_statistics.source_step.
  • block selects the block, which is always block 0; field is Ucat. Ucont is component-staggered and has no single cell-centered spectrum, so it is refused.

Two tasks differing in task, field, block, symbol, or plane/line selection write different files and may coexist; two identical tasks are refused, since the second would overwrite the first.

Outputs

Each task writes a spectrum CSV and a history CSV under the canonical run spectra directory. For shell spectra:

File Columns Purpose
<prefix>_<task>_<field>_block<nnnn>_<symbol>.csv step,time,k,energy the spectra themselves, long format
..._history.csv step,time, then the scalars below one row per processed step

The spectrum file is long format so a family of curves stays one file, which is also what makes comparison across a sweep a glob rather than a special case.

Shell history records per step: resolved_kinetic_energy, spectrum_total_energy, parseval_residual, spectrum_peak_k, zero_mode_energy, integral_length_scale, taylor_microscale, and dissipation_over_viscosity.

Plane/line filenames also identify the transformed axes and fixed cell indices. Their mode columns and sample diagnostics are described in p10_cap_spec_plane_spectrum and p10_cap_spec_line_spectrum below; the shell isotropic length-scale diagnostics do not apply to these samples.

parseval_residual is a self-check rather than an input: summed spectral energy must equal the resolved kinetic energy of the transformed field or sample, and a residual above round-off means the measurement is wrong.

Dissipation is reported divided by the kinematic viscosity — \(\varepsilon = \nu \times\) dissipation_over_viscosity — because the measurement needs no fluid properties. Both length scales need none either; \(\nu\) cancels out of their isotropic definitions.

Cross-checking against field statistics

parseval_residual verifies the transform against itself. An independent check is available against 6. field_statistics, which computes the same turbulent kinetic energy through an entirely separate path — a C accumulator over every accepted state rather than a Python transform of written snapshots:

field_statistics:
windows: [early_decay]
outputs: [tke]
formats: [csv]
source_step: 400
spectra:
tasks:
- task: shell_spectrum
field: Ucat
subtract_mean: window:early_decay
mean_source_step: 400

Time-average the spectra history's resolved_kinetic_energy across the window and compare with mean_tke in the statistics CSV. Weight by the represented interval, not by sample: the window itself weights by physical time, and a plain mean over a curved history is biased by its endpoints.

Exact agreement is not expected and is not the point. The window accepts states at its own step_cadence, while spectra can only read the steps a checkpoint exists at, so the two sides average different sample sets. Agreement to a few percent confirms the paths measure the same quantity; a large gap does not.

Viewing them

picurv summarize --run-dir runs/my_run --plot-spectrum
picurv summarize --run-dir runs/my_run --plot spectra.resolved_kinetic_energy

--plot-spectrum draws one curve per processed step on log-log axes, overlaid on <run.analysis>/spectra/initial_condition_spectrum.csv when the run has one. Pass a task-name substring when a recipe measured more than one spectrum. The per-step scalars are ordinary series, so --list-plot-series finds them and --plot draws them.

A decaying flow is a different statistical state at every step, so the family is plotted rather than averaged, and the stage never averages across steps. Smoothing a spectrum comes from ensembling over seeds at matched times, not from pooling times.

8.1 Spectra Task Entries

Value Maps to
line_spectrumline_spectrum
plane_spectrumplane_spectrum
shell_spectrumshell_spectrum

shell_spectrum

Identity. spectra.tasks: [{task: shell_spectrum, field: Ucat, block: 0}], measured by generators/spectra.gen rather than by the C post-processor.

What it does. Computes a three-dimensional energy spectrum by transforming the velocity field and binning the result into spherical shells in wavenumber space, producing E(k).

When to choose it. For homogeneous turbulence where the question is how energy is distributed across scales - resolving whether an inertial range exists, or where the dissipation cutoff sits. It is the wrong tool for an inhomogeneous flow, where a shell average mixes physically distinct directions.

Parameters it owns. field (the vector field to transform), block, symbol (continuum or discrete, the binning abscissa), subtract_mean (none, domain, or window:<name>), and mean_source_step when a window mean is used.

Interactions. Requires a uniformly spaced, geometrically periodic, single-block domain, and picurv validate checks all three against the case before any field is read. Independent of eulerian_pipeline: it reads raw checkpoint payloads, so no field output stage is needed. --only spectra runs it alone.

Diagnostics. Writes one file per task per checkpoint under the configured output_prefix. A precondition failure is a validation error naming the violated requirement, not a silent empty spectrum.

Evidence. Unit verified - make test-python runs the spectra generator's tests. Analytically verified - post-spectra-analytic-2026-09-21: three single modes on a 32-cubed periodic box landed in shells 2, 3 and 4 at exactly their energies, every other shell below 1e-32, with zero Parseval residual, and the dissipation, integral length and Taylor microscale each equal their closed forms; initial-conditions-2026-09-21 recovered a spectral initial condition's envelope to 4.7%, energy-weighted. The same measurement found picurv summarize --plot-spectrum unable to find what a post recipe wrote, because recipes write into a subdirectory; it now searches there.

Limitations. Shell averaging assumes isotropy that a wall-bounded or bent geometry does not have, and subtract_mean: window:<name> depends on an accumulated window existing at the named step. Measured on single modes and one broadband seed; no convergence or windowing guidance for a turbulent spectrum is established here.

plane_spectrum

Identity. spectra.tasks[].task: plane_spectrum dispatches spectra.gen plane-spectrum; no C task is added.

What it does. A two-dimensional FFT of one actual cell-center plane, retaining both signed wavenumbers and separate Cartesian component energies.

When to choose it. Use it for scale content at a channel wall-normal station. Choose line_spectrum for one physical line, or shell_spectrum for a triply periodic box reduced to radial shells.

Parameters it owns. Required axes contains two distinct i/j/k axes; required fixed_indices selects the other axis. subtract_mean: sample removes this plane's mean. symbol must be continuum.

Interactions. Transform axes must be periodic and uniformly spaced; the transverse coordinate may stretch. Reads raw Ucat checkpoints, with existing window-mean subtraction available. Initial-condition staging can invoke the same computation without a checkpoint.

Diagnostics. CSV rows contain two signed k_<axis> columns in storage order, energy_u/v/w, total energy, and position_<fixed-axis>. Energies are per-mode contributions, not spectral densities: summing all rows equals half the sample mean squared speed. The history CSV reports sample kinetic energy and relative Parseval residual. Coordinates/wavenumbers/energies scale with length/velocity references when dimensionalization is requested. Task filenames include axes and fixed indices.

Evidence. Unit verified - make test-python (tests/test_spectra_shell_spectrum.py: known modes, Parseval closure, transform-axis uniformity). Production exercised in examples/turbulent_channel. Analytically verified - post-plane-line-spectra-2026-09-24: on a checkpoint of known Fourier content over a stretched wall-normal axis, every signed mode's per-component energy matched a direct DFT of the same samples to 1.9e-15 of the plane energy, rows summed to the sample kinetic energy, no energy appeared outside the designed modes, and positions matched the cell centres; designed-mode energies differed from the staged expression only by the runtime's reconstruction factor cos^4(k h/2).

Limitations. Serial whole-field input, one block, Cartesian geometry, no masked/immersed lines. A checkpoint spectrum measures the field the runtime holds, smoothed by its reconstruction relative to a staged expression. No developed turbulent-channel acceptance is established; that is a flow result, not a property of the measurement. No 2D heatmap is added to --plot-spectrum; use the CSV for 2D plotting. A seed spectrum is not a developed-turbulence benchmark.

line_spectrum

Identity. spectra.tasks[].task: line_spectrum dispatches spectra.gen line-spectrum; no C task is added.

What it does. A one-dimensional FFT of one selected physical line, retaining signed k and Cartesian component energies.

When to choose it. Use it for a streamwise line through a duct, or a single line in a channel. It is not a directional spectrum averaged over a plane; select plane_spectrum when the second wavenumber matters.

Parameters it owns. Required axes contains one i/j/k axis; required fixed_indices selects both transverse axes. subtract_mean: sample removes the line mean; symbol must be continuum.

Interactions. Only the transformed direction must be uniform and periodic. Other directions may stretch. Checkpoint discovery, window means, and dimensionalization use the same path as plane/shell tasks.

Diagnostics. Rows contain signed k, energy_u/v/w, total energy, and both fixed-position columns. Sum all signed modes for sample kinetic energy; do not double the already retained negative partners. The history reports kinetic energy and Parseval residual. Filenames distinguish both transverse indices.

Evidence. Unit verified - make test-python: known-mode tests sample a physical line with a different amplitude at each transverse station. Production exercised in examples/periodic_test/driven_duct (post_spectral.yml). Analytically verified - post-plane-line-spectra-2026-09-24: lines along a uniform periodic axis of a grid stretched in another direction matched a direct DFT of the same samples to round-off, with exact positions; a line along a stretched axis is refused. No developed duct-flow validation is claimed.

Limitations. Single-block Cartesian grids without immersed masks. This does not produce line ensembles or temporal spectra. Existing curve plotting displays positive modes only; the CSV retains both signs.

9. io

Mappings:

  • output_directory + output_filename_prefix -> output_prefix
  • output_directory + particle_filename_prefix -> particle_output_prefix
  • output_particles -> output_particles
  • particle_subsampling_frequency -> particle_output_freq
  • eulerian_fields -> output_fields_instantaneous
  • particle_fields -> particle_fields_instantaneous — must be non-empty when output_particles: true; an empty list produces no particle VTP output even if particle output is enabled. Standard swarm field names: position, velocity, pid, CellID, weight.
  • input_extensions.eulerian/particle, when present, must be dat; committed bundle payload names are fixed
  • paraview_series.enabled writes one .pvd beside each requested VTK family after successful field postprocessing. scope: run indexes this run only; scope: lineage follows the run manifest's restart ancestry, clips every parent at its child's fork, and lets the child own a duplicate fork frame. The PVD timestep is the authoritative checkpoint_time, so changed timestep size and changed output cadence remain ordered without adding time to the VTS/VTP payload. The option is a presentation product and does not change computational recipe identity.

The postprocessor reads the catalogued .dat PETSc vectors inventoried by each committed checkpoint; input extensions are not runtime-selectable. The conductor routes statistics_pipeline.output_prefix to <run.analysis.statistics>/<recipe_id>/<basename>; only the configured basename is retained. Relative or absolute directory components do not redirect the result. When the same timestep is post-processed again, PICurv now rewrites same-step VTK/VTP outputs and rewrites same-step statistics rows so the final CSV still contains one row per step. picurv run --post-process may be repeated while a solver is active: each invocation processes the newly committed steps, adds their VTK frames, and atomically refreshes the PVD. Ancestor frames must already exist for the same recipe. Particle ancestry begins again when particles use restart_mode: init, and field statistics ancestry begins again after --statistics-state reset.

Omitting io.paraview_series disables collection generation. The boolean shorthand true enables lineage scope; a mapping defaults to enabled: true, scope: lineage. The standard analysis profiles and master template explicitly enable lineage scope. The setting is consumed by the conductor and does not add a PETSc option or change VTK payload fields. A solve-only invocation does not generate collections, and --only spectra does not refresh them.

Lineage indexing requires the same recipe ID in each contributing run. Changing the logical start/end window, toggling collection generation, or omitting source_data preserves that ID; changing the post step_interval, fields, or pipelines does not. Different solver dt values and checkpoint cadences can share a collection when the post recipe remains compatible and its requested steps exist. The index uses available VTK frames, but does not discover or combine outputs from other recipe IDs.

Missing parents or ancestor segments with no matching frames produce an error. Every indexed frame needs committed checkpoint time metadata, or a retained PVD from which its time can be recovered after checkpoint pruning. Collections reference VTK files by relative path, so moving or deleting individual runs can break those references; retain the directory relationships or regenerate after restoring them. Refreshing a collection is atomic under the run's post writer lock. Reopen the PVD in ParaView after a refresh; PICurv does not push updates into an open GUI session.

10. Next Steps

Proceed to User How-To Guides for goal-oriented recipes.

For mapping and extension details: