PICurv 0.1.0
A Parallel Particle-In-Cell Solver for Curvilinear LES
 
Loading...
Searching...
No Matches
Workflow Recipes and Config Cookbook

This page is the practical companion to the reference docs. Use it when you already understand the config roles and want proven patterns for combining them cleanly.

1. Core Workflow Idea

PICurv is designed around modular config roles:

  • case.yml: physics, grid, BCs, run duration
  • solver.yml: numerical strategy
  • monitor.yml: logging, diagnostics, output cadence, directories
  • post.yml: analysis recipe
  • optional cluster.yml: scheduler policy
  • optional study.yml: parameter sweep definition

The intended UX is:

  • keep each file focused on one concern,
  • reuse stable profiles across many runs,
  • only clone and specialize the roles that actually changed.

2. Common Mix-and-Match Patterns

Pattern A: same case, different monitoring

  • keep one trusted case.yml
  • pair it with a lightweight monitor.yml for fast iteration
  • switch to a richer monitor.yml for production output

Pattern B: same case, different analysis

  • run once with a standard monitor profile
  • post-process the same run directory multiple times with different post.yml recipes

Pattern C: same solver profile reused across many cases

  • keep one validated solver.yml for a known momentum strategy
  • pair it with multiple physical cases as long as the strategy is appropriate

Pattern D: same cluster profile across many jobs

  • reuse one cluster.yml for site policy
  • vary only case/solver/monitor/post inputs

Pattern E: same case, different local launch environment

  • keep case/solver/monitor/post unchanged
  • use default mpiexec on ordinary workstations
  • add a repo-local or case-local .picurv-execution.yml when a cluster login node or cluster batch job needs site-specific launcher flags

3. Command Recipes

First-time local workflow:

./bin/picurv init flat_channel --dest my_case
./bin/picurv validate --case my_case/flat_channel.yml --solver my_case/Imp-MG-Standard.yml --monitor my_case/Standard_Output.yml --post my_case/standard_analysis.yml
./bin/picurv run --solve --post-process -n 4 --case my_case/flat_channel.yml --solver my_case/Imp-MG-Standard.yml --monitor my_case/Standard_Output.yml --post my_case/standard_analysis.yml

Solver-only run:

./bin/picurv run --solve -n 8 --case case.yml --solver solver.yml --monitor monitor.yml

Solver-only run on a cluster login node with a persistent site launcher config:

# Preferred: create one repo-root `.picurv-execution.yml` in your cluster clone.
# `picurv init` seeds new cases from it automatically.
# For an existing case, copy config/runtime/execution.example.yml once and rename it.
./bin/picurv run --solve -n 8 --case case.yml --solver solver.yml --monitor monitor.yml

Cluster run that reuses the same site launcher defaults unless cluster.yml overrides them:

# Preferred: create one repo-root `.picurv-execution.yml` in your cluster clone.
# `picurv init` seeds new cases from it automatically.
# For an existing case, copy config/runtime/execution.example.yml once and rename it.
./bin/picurv run --solve --post-process --case case.yml --solver solver.yml --monitor monitor.yml --post post.yml --cluster cluster.yml

Post-process an existing run with a different recipe:

./bin/picurv run --post-process --run-dir runs/<run_id> --post alt_analysis.yml

Catch up the same recipe in batches while the solver is still producing new source steps:

./bin/picurv run --post-process --run-dir runs/<run_id> --post post.yml

Operational notes:

  • keep the full logical analysis window in post.yml; each run processes only the steps whose output is missing or stale.
  • if every requested step is up to date, PICurv does not launch the postprocessor.
  • steps whose checkpoints the solver has not committed yet are left for a later run instead of failing.
  • if another post job already holds the same run directory, PICurv refuses the new writer immediately.

For restart-lineage visualization, merge this into the recipe's io mapping:

paraview_series:
enabled: true
scope: lineage

Postprocess ancestors with the same recipe first, then catch up the child using the command above and the full desired window. Open the child's .pvd in <run.visualization>/<recipe_id>/. Checkpoint times supply the time axis even when solver dt changes. The post stride and fields must retain the same recipe ID; otherwise ancestor results need to be generated for the new recipe. Catch-up refreshes the PVD atomically; reopen it in ParaView to see newly added frames. Use scope: run when ancestry should not be included. Full rules are in Configuration Reference: Postprocessor YAML.

Dry-run planning:

./bin/picurv run --solve --post-process --case case.yml --solver solver.yml --monitor monitor.yml --post post.yml --dry-run --format json

Stage artifacts without starting execution:

./bin/picurv run --solve --post-process --case case.yml --solver solver.yml --monitor monitor.yml --post post.yml --no-submit

Add --cluster cluster.yml to stage Slurm scripts instead of local command metadata.

Delayed submit from existing staged artifacts:

./bin/picurv submit --run-dir runs/<run_id>

Cancel a submitted run by directory:

./bin/picurv cancel --run-dir runs/<run_id> --stage solve

Request a solver final-output shutdown before exiting:

./bin/picurv cancel --run-dir runs/<run_id> --stage solve --graceful

Use plain cancel if the solver is wedged or not reaching runtime checkpoints.

Generated Slurm solver jobs already enable the runtime walltime guard by default. Override it only when needed:

execution:
walltime_guard:
enabled: true
warmup_steps: 10
multiplier: 2.0
min_seconds: 60
estimator_alpha: 0.35

Keep an early signal as fallback protection for preemption/manual termination:

execution:
extra_sbatch:
signal: "USR1@300"

For direct mpirun batch launches, use signal: "B:USR1@300" and prefer exec mpirun ....

Run naming note:

  • cluster.yml does not provide a run-name field.
  • picurv creates runs/<case_basename>_<timestamp>/ automatically.
  • generated Slurm job names are derived from that run ID.

4. Example Config Combinations

Programmatic baseline:

  • case: examples/flat_channel/flat_channel.yml
  • solver: examples/flat_channel/Imp-MG-Standard.yml
  • monitor: examples/flat_channel/Standard_Output.yml
  • post: examples/flat_channel/standard_analysis.yml

File-grid baseline:

  • case: examples/bent_channel/bent_channel.yml
  • solver: examples/bent_channel/Imp-MG-Standard.yml
  • monitor: examples/bent_channel/Standard_Output.yml
  • post: examples/bent_channel/standard_analysis.yml

Analytical zero-flow Brownian verification:

  • case: examples/brownian_motion/brownian_motion.yml
  • solver: examples/brownian_motion/Analytical-Zero.yml
  • monitor: examples/brownian_motion/Standard_Output.yml
  • post: examples/brownian_motion/brownian_analysis.yml

Analytical uniform-flow deterministic drift verification:

  • case: examples/drift_uniform_flow/drift_uniform_flow.yml
  • solver: examples/drift_uniform_flow/Analytical-UniformFlow.yml
  • monitor: examples/drift_uniform_flow/Standard_Output.yml
  • post: examples/drift_uniform_flow/uniform_flow_analysis.yml

Analytical zero-flow plus verification-source diffusivity drift verification:

  • case: examples/drift_diffusivity_gradient/drift_diffusivity_gradient.yml
  • solver: examples/drift_diffusivity_gradient/Analytical-Zero-Verification-LinearDiffusivity.yml
  • monitor: examples/drift_diffusivity_gradient/Standard_Output.yml
  • post: examples/drift_diffusivity_gradient/drift_diffusivity_gradient_analysis.yml

These are examples, not fixed bundles. You can intentionally swap roles when the contract makes sense.

5. Drift Verification Workflow

Use the drift examples as separate, end-to-end checks for the two deterministic particle-position terms:

  • examples/drift_uniform_flow/ isolates the carrier-velocity term in X_new = X_old + (vel + diffusivitygradient) * dt + Brownian.
  • examples/drift_diffusivity_gradient/ isolates the diffusivity-gradient term while keeping the carrier flow at zero.

The intended policy is:

  • prefer ordinary production pathways first,
  • use analytical solver modes when the solver already supports them,
  • only use verification.* overrides when there is no cleaner end-to-end way to prescribe the field needed for verification.

For the current diffusivity-drift case, the curated source override lives behind:

verification:
sources:
diffusivity:
mode: "analytical"
profile: "LINEAR_X"
gamma0: 1.0e-3
slope_x: 2.0e-4

Contributor note:

  • new verification source injections must be implemented in include/verification_sources.h and src/verification_sources.c
  • production call sites should stay thin and delegate into that module
  • do not introduce one-off verification flags directly into unrelated solver files unless there is no alternative and the delegation point remains explicit

6. Configuration Patterns Worth Reusing

Minimal zero-flow startup:

properties:
initial_conditions:
mode: generated
generator: zero

Constant cartesian velocity (channel with explicit components):

properties:
initial_conditions:
mode: generated
generator: constant
params:
u_physical: 0.0
v_physical: 0.0
w_physical: 1.0

Constant streamwise velocity for a periodic box (no INLET face):

properties:
initial_conditions:
mode: generated
generator: streamwise_constant
params:
velocity_physical: 1.0
flow_direction: "+Zeta"

Poiseuille with a clear scalar input (INLET face present — no flow_direction needed):

properties:
initial_conditions:
mode: generated
generator: poiseuille
params:
peak_velocity_physical: 1.25

Poiseuille for a periodic domain (no INLET face):

properties:
initial_conditions:
mode: generated
generator: poiseuille
params:
peak_velocity_physical: 1.25
flow_direction: "+Zeta"

Generated grid workflow:

grid:
mode: grid_gen
generator:
config_file: config/grids/coarse_square_tube_curved.cfg
grid_type: sweep
cli_args: ["--ncells-i", "96", "--ncells-j", "96"]

Generate once, inspect, then reuse as a file grid:

./bin/picurv run --solve --no-submit \
--case case.yml \
--solver solver.yml \
--monitor monitor.yml

The staged run contains the generated dimensional grid at <run.inputs>/grid/grid.generated.picgrid - physically <workspace>/runs/<run_id>/inputs/grid/grid.generated.picgrid.

After checking that file, a later case can reuse it without running grid.gen:

grid:
mode: file
source_file: ../runs/<run_id>/inputs/grid/grid.generated.picgrid

PICurv will validate and non-dimensionalize that source file into the new run's own <run.config>/grid.run.

Generated square-duct Poiseuille inlet profile:

boundary_conditions:
- face: "-Zeta"
type: INLET
handler: prescribed_flow
params:
source:
type: generated
generator: square_duct_poiseuille
params:
bulk_velocity: 1.0
n_terms: 101

picurv run --solve writes the dimensional generated profile and profile.info under <run.config>/, stages the solver-scale .picslice, and writes that staged path into bcs.run. For square_duct_poiseuille, bulk_velocity is the target inlet bulk speed. When a target PICGRID is available, generated profiles are sampled at inlet face centers and normalized by geometric quad face areas; profile.info reports the area-weighted mean before and after normalization, total area, face-area range, discrete sampled mean, and Umax/Ubulk.

File-backed prescribed inlet profile:

boundary_conditions:
- face: "-Zeta"
type: INLET
handler: prescribed_flow
params:
source:
type: file
path: profiles/inlet.picslice

For 64 x 64 x 1024 cells, a -Zeta or +Zeta profile is 64 x 64. The first file value maps to tangential solver index (j=1, i=1).

Field-sliced prescribed inlet profile from an old run:

boundary_conditions:
- face: "-Zeta"
type: INLET
handler: prescribed_flow
params:
source:
type: field_slice
field_file: ../old_run/output/checkpoints/step_000000010000/eulerian/block_0000/Ucat.dat
grid_file: ../old_run/inputs/grid/grid.run
source_case: ../old_run/config/case.yml
slice:
face: "+Zeta"
orientation: opposite

This extracts Cartesian Ucat normal speeds from the old slice, validates the old and new face normals, writes *.sliced.picslice, then stages it through the same file-backed C path. V1 requires matching source and target slice sizes.

Precompute deterministic artifacts without launching the solver:

./bin/picurv precompute --case config/case.yml

This mirrors the <run.config>/ layout, so inspected generated artifacts can later be reused through grid.mode: file and source.type: file.

Restart with explicit particle re-initialization:

run_control:
start_step: 200
models:
physics:
particles:
count: 10000
restart_mode: "init"

Continue a run that ended at step 500 for 1000 more steps:

run_control:
start_step: 500
total_steps: 1000

solver.yml keeps operation_mode.eulerian_field_source: "solve": with start_step > 0 the solver loads the step-500 state and advances it. (load would instead replay stored fields and needs a checkpoint at every step.)

Optional particle choice:

models:
physics:
particles:
restart_mode: "load" # or "init"
./bin/picurv run --solve --post-process \
--case restart_case/case.yml \
--solver restart_case/solver.yml \
--monitor restart_case/monitor.yml \
--post restart_case/post.yml

Expected step range:

  • saved state loaded from step 500
  • first new step is 501
  • final step after this run is 1500

7. Which Example To Start From

Choose flat_channel when:

  • you want the simplest first runnable case,
  • you are learning programmatic_c,
  • you want a clean baseline for solver/monitor/post swaps.

Choose bent_channel when:

  • you need file-based curvilinear geometry,
  • you want to understand grid.mode: file,
  • you plan to compare against grid_gen later.

Choose brownian_motion when:

  • you want an analytical zero-flow validation case,
  • you want a particle-focused test with minimal flow complexity,
  • you want a reference for ZERO_FLOW analytical mode.

7. Scatter Verification Recipe

Use solver.yml -> verification.sources.scalar when you need a known particle scalar truth field and there is no ordinary production workflow that can create that truth end-to-end.

Recommended pattern:

  • keep operation_mode.eulerian_field_source: analytical
  • use analytical_type: ZERO_FLOW for pure deposition checks
  • choose a scalar profile under verification.sources.scalar
  • reuse the production scatter operator rather than adding a second deposition path
  • read the runtime metric from <run.analysis.metrics>/scatter_metrics.csv

Separation rules:

  • when verification.sources.scalar is absent, ordinary production runs are unchanged
  • only the particle Psi update is bypassed in verification mode
  • carrier-flow selection remains independent, so the same scalar truth can later be paired with UNIFORM_FLOW or TGV3D

This pathway is useful for more than one study. It enables:

  • static deposition verification
  • moving-cloud scatter checks
  • conservation diagnostics on deposited scalar fields
  • grid-sensitivity studies using scatter_metrics.csv

A complete runnable example is provided in examples/scatter_verification/.

8. LES Closure Recipe

The dynamic model's defaults are the ones that are safe on any geometry, not the ones that are best for yours. Two settings usually want changing, and both follow from the flow rather than from taste.

Pick the averaging set from the flow's homogeneous directions. averaging.mode defaults to local, which assumes nothing and is correspondingly noisy: the denominator of the coefficient collapses wherever the resolved strain is briefly small. Where the flow has a homogeneous direction, say so:

# Triply periodic box: one coefficient for the whole domain.
averaging: {mode: homogeneous}
# Plane channel periodic in xi and zeta: a wall-normal coefficient profile.
averaging: {mode: homogeneous}

Both read the same because homogeneous derives its directions from the case's periodicity. Name them explicitly only when the flow is homogeneous along an axis the boundary conditions do not declare periodic.

Match the filter width to the cell aspect ratio. filter_width defaults to cube_root_volume, which is exact for a cube and progressively optimistic as a cell is stretched. On a wall-resolved mesh with moderately stretched cells, scotti corrects that optimism without inflating the width at the wall; max_edge suits wall-modelled or hybrid runs, where near-wall cells are meant to be coarse.

Turn the diagnostics on before trusting anything.

diagnostics: {enabled: true, cadence: 1}

Then read <run.analysis.metrics>/les_coefficient.csv, or plot it directly:

./bin/picurv summarize --run-dir runs/<run_id> --list-plot-series
./bin/picurv summarize --run-dir runs/<run_id> --plot les.cs_effective

What to look for:

  • cs_effective should settle; for decaying isotropic turbulence at 64^3 with the box test filter it settled at 0.18-0.19 on the HOM02 benchmark (Lilly's 0.16-0.17 assumes a spectral cutoff). A curve that drifts or oscillates is the signal to stop.
  • limited_fraction near zero means the max_cs ceiling is not shaping the result. A large value means the clip is doing the modelling.
  • backscatter_fraction is what the clipping modes discard. Under local averaging it is routinely large, which is the argument for averaging rather than clipping.
  • nu_t_over_nu_mean far above one means the model, not the molecular viscosity, is setting the dissipation; far below one means it is doing nothing.

Judging resolution. Compare k_sgs_mean against the resolved turbulent kinetic energy from monitor.yml -> field_statistics. Pope's criterion asks the resolved share to exceed roughly 80%. The two halves come from different pipelines deliberately: resolved turbulent energy needs the mean subtracted correctly, which the statistics windows do and the closure does not attempt.

Full option reference at Configuration Reference: Case YAML; the formulation is derived in LES Turbulence Closure.

9. Related Pages