Rocflu Regression Tests

Small cases (a few hundred cells, 1–40 particles) that run in seconds each. Every case is compared against a stored golden, and most cases also have physics checks that judge whether the result is correct, not just unchanged.

This suite tests the coupled code end to end: grid, pre-processing, the Rocflu solver and ppiclF together, on 1, 2 and 4 MPI ranks. It replaces ppiclF's own unit tests (libpicl/unit_tests, short_tests and the #ifdef TEST code), which were removed in 3c41db2.

There are two kinds of test:

Particle fields are read by name (vp_x, rho_g, dp, ...; see libpicl/user_files/ppiclf_user_OutputFields.f). The template input templates/rectshktb.inp turns on PARTICLE_WRT, GAS_WRT and GRADGAS_WRT, because the goldens and checks use the particle temperature, spin, density and volume and the gas values at the particles; keep them on. Gas gradients are written only when a model that is on needs them (ppiclf_user_Layout), so cases without pressure-gradient, added-mass, viscous-unsteady or rotational-collision models have no dpdx_g and similar fields. A field that is in a golden but no longer written fails the comparison.

The 63 cases fall into four groups: shock tube (force models, one- and two-way coupling), box (conservation, interpolation, projection, collisions, walls), gas only (the flow solver without particles) and 2D (DIMENS 2). One unit test runs with them.

Running

1. Build

Build the code in the tree you want to test: ./rocflu.sh build (see Getting Started). The suite runs the executables rflumap, rflupart, rfluinit and rflump from build/bin (the last configuration built), or from the directory given with --exe. ./rocflu.sh test [options] runs ./run_tests.py run with the modules loaded and passes the options on.

2. Run

From this directory:

source ../rocflu.sh load                   # the machine's modules (see Machines)
./run_tests.py list                        # show cases
./run_tests.py run                         # all cases, compare to goldens (~40 s)
./run_tests.py run -o shocktube/two-way    # cases whose name contains a pattern
./run_tests.py run -o walls np1            # several patterns: any of them
./run_tests.py run --exe /path/to/tree     # test executables from another tree
./run_tests.py run -j 21                   # cases run at the same time (default 8)
./run_tests.py run --keep                  # keep run directories of passing cases

Load the modules in the same shell. Without them every case fails with a GLIBCXX error.

Pre-processing (rflumap, rflupart, rfluinit) runs serially where you launch the script. The solver runs through srun:

Each case's input file goes through the case validator (utilities/scripts/case_validator.py, see docs/Running.md) before it runs; a case it rejects fails with the validator's messages. The validator has its own tests, which need no cluster: python3 tests/test_case_validator.py.

A case whose solver run fails is set up again and retried once; srun on a shared partition can fail when the Slurm controller is unresponsive. The retry and any Slurm messages are shown next to the case.

3. Read the Result

The script prints one line per case, a total (e.g. 43 PASS) and a closing line (✔ All 43 cases passed in 29 s, or ✗ 2 of 43 cases failed). On a terminal the statuses are coloured (PASS green, FAIL red, XFAIL yellow, XPASS cyan); NO_COLOR=1 turns that off. See Results for what each status means.

Run directories go to _work/runs/<case> (git-ignored). They are deleted for cases that pass, unless you pass --keep. A failed case keeps its directory, with the input files and one log per step: mesh.log, rflumap.log, rflupart.log, rfluinit.log, rflump.log. A golden mismatch names the worst particle or cell and its value against the golden.

4. After Changing the Code

Results

Status Meaning
PASS Matches the golden, and the physics checks pass
FAIL The run failed, the result differs from the golden, or a physics check failed
XFAIL The case has a known_issue. Its physics checks fail as expected, and it still matches its golden
XPASS The physics checks of a known_issue case now pass. Remove the marker

Tolerances:

What a Case Is

Cases are defined in rocpicl_tests/cases.py. Each one specifies:

Particles never start on a cell face, so round-off can't decide which cell or rank owns them. A case's id changes whenever its definition changes. A stale golden is reported rather than silently compared.

Particles are matched by their line number in points.dat, which is recovered from the ppiclF tags. That's what lets same_as compare a 2-rank run with a 1-rank run particle by particle.

Physics Checks (rocpicl_tests/checks.py)

Check What it requires
particles_moved Something happened, so the other checks aren't vacuous
mass, gas_energy Gas totals are conserved (closed or periodic box, no inflow)
momentum, energy The gas loses what the particles gain (periodic box); energy includes particle kinetic and thermal energy
particles_heat_exchange Particles at rest in gas at another temperature exchange heat
quiescent Gas at rest stays at rest (fixed particles)
linear_interp Particles read a linear gas density exactly (INITFLOW IVAL1 = 1, density RVAL2*(1 + RVAL7*x + RVAL8*y + RVAL9*z) at rest)
projection_shape One particle's volume fraction is centred on it (within 0.06 cell) and spread 0.35-0.50 cell lengths in each direction
in_plane DIMENS 2: particles stay on the mid-plane with vz = 0 and spin only about z; the gas has no z velocity
collision_pairs Overlapping pairs pushed apart by collisions alone conserve each pair's momentum, separate, and all move like the first pair, whether the partner is real, a ghost from another rank, or a periodic image
binary_collisions Colliding pairs (ids 2k−1, 2k) conserve momentum (1e-5) and angular momentum about their centre of mass (0.5%), and separate; head-on pairs rebound with the input EREST (within 1%)
fluctuations_active Every particle feels a nonzero quasi-steady force fluctuation at the end (Ffluct_*, written with MODEL_WRT 1), so the noise is really tested
wall_collisions Every particle that reaches a wall of the box rebounds from it and ends inside the box, clear of the wall; one that hits a single wall rebounds with the input EREST (within 0.035)
pg_impulse With the pressure-gradient force alone, each particle the shock has passed has vp_x = (p2 − p1)/(ρ_p V_s), V_s = ρ2 u2/(ρ2 − ρ1), within 2%
planar Gas density and energy vary by less than 2e-3 across y-z in each x-column (a plane shock between slip walls)
same_as Particles match the referenced case, on a different number of ranks or with a different setting (all particle fields, or only those named by the case's same_as_fields)

The state a case is judged by (goldens, physics checks) comes from its dumps in restart_data/ (rocpicl_tests/state.py), in the form of the old-style flow files: with particles rho_g, u, v, w, E and phi, without the conserved state.

The template switches the old-style output off (OLD_OUTPUT_WRT 0, as for a user's run). Five cases switch it on (OLD_OUTPUT_CASES in cases.py: 2 regions and 1, NSCBC outflow with history, random streams, gas only); for them the harness also checks that each SOL_* flow file matches the fluid dump of the same time (1e-12 relative), that each par*.vtu matches the particle dump of the same time field by field (single precision), and that rfluinit's step 0 dump exists.

Post program: every case also runs rflupost -s all; the cases with the old-style output compare post/ with it at every step: each fluid field of each region with PARAVIEW_* of the same time (1e-12 relative to the field's largest value; the template turns every *_WRT field on), each particle field and position with the par*.vtu of the same step (single precision). Without particles the old Velocity and Energy are the momentum and the energy per volume, and are compared as such.

Restart test (cases with restart_test=True): after the run, the harness copies the run directory to <case>_restart, sets START_STEP to the first dump after step 0, runs rflump again to the same end time and requires every final dump (fluid of each region, particles) to be byte-identical to the uninterrupted run's. It runs on box/momentum-exchange/np2 (two-way coupling), shocktube/two-way/viscous-unsteady/np2 (history of past steps, NSCBC outflow), box/qs-fluctuations/np2 (random streams) and box/binary-collisions/np2 (collisions).

The Test Cases

Every case writes two solution dumps and is compared at the last one. "npN" in a name is the number of MPI ranks. Where a case exists on several rank counts, the 2- and 4-rank versions also run same_as against the 1-rank one (except where noted), so they test that the result does not depend on the decomposition.

Shock Tube Cases

A 40 × 4 × 4 mm tube of 1 mm cells, periodic in y and z, with a Mach 1.27 shock starting at x = −10 mm and running into particles at rest (100 µm, quasi-steady drag, Osnes). They run to 20 µs.

Case Ranks Setup Checks What it guards
shocktube/one-way/1-particle 1, 2 One particle, FEEDBACK 0 particles_moved, same_as ppiclF bins around a single particle (this once gave a filter width of 0)
shocktube/one-way/4-particles 1, 2 Four particles, FEEDBACK 0 particles_moved, same_as Interpolation and drag with several particles
shocktube/one-way/pg-impulse 2 Two particles, FEEDBACK 0, QSFORCE 0, PGFORCE 1, run to 40 µs pg_impulse The pressure-gradient force gets the whole pressure jump of the shock (the particle gradients are not smoothed)
shocktube/two-way/4-particles 1, 2 Four particles, FEEDBACK 1 particles_moved, same_as Feedback to the gas, across a rank border
shocktube/two-way/pressure-gradient 2 + PGFORCE 1 particles_moved Pressure-gradient force
shocktube/two-way/viscous-unsteady 2 + VISCOUSUNSTEADY 1 particles_moved History force
shocktube/two-way/heat-transfer 2 + HEATTRANSFER 3 particles_moved Heat-transfer closure
shocktube/two-way/qs-fluctuations 2 + QSFLUCT 2 particles_moved Quasi-steady force fluctuations run; the four particles have no neighbours, so the noise amplitude is 0 (see box/qs-fluctuations)
shocktube/two-way/qs-parmar 2 QSFORCE 2 particles_moved Parmar drag
shocktube/two-way/qs-gidaspow 2 QSFORCE 1 particles_moved Gidaspow drag

| shocktube/slip-walls/creconst0 | 2 | Slip walls on y and z, 40 × 8 × 8 cells, two-way, PGFORCE 1, CRECONSTC 0 | particles_moved, planar | Unconstrained cell gradients next to slip walls | | shocktube/slip-walls/creconst1 | 2 | Same with CRECONSTC 1 | particles_moved, planar, same_as (x, vp_x of creconst0) | Constrained cell gradients keep the gradients of variables without a wall value (e7daa9b; they were zeroed: 5-21% in vp_x) |

The force-model cases have no physics check beyond particles_moved. Their golden is what guards them: any change in the model's result shows up as a golden mismatch.

Box Cases

An 8 mm cube of 1 mm cells (8 × 8 × 8), either periodic on all faces or closed by slip walls. The gas is uniform (1.2 kg/m³, 101 325 Pa) unless stated otherwise.

Case Ranks Setup Checks What it guards
box/momentum-exchange 1, 2 Periodic; gas at 50 m/s through 40 particles (20 µm) at rest, two-way mass, momentum, energy, same_as The gas loses exactly the momentum and energy the particles gain
box/qs-fluctuations 1, 2 Periodic; gas at 50 m/s through 40 particles (20 µm) with a ±1 m/s velocity spread, QSFLUCT 2, MODEL_WRT 1, NEIGHBORWIDTH 3e-3 particles_moved, fluctuations_active, same_as Each particle draws from its own random stream: 1 and 2 ranks agree (96b28a4; one generator per rank failed this)
box/quiescent-cloud 2, 4 Periodic; 30 fixed particles (300 µm, uneven) in gas at rest, VELOCITYMODEL 0, PGFORCE 1 quiescent, mass Stale virtual cells at rank borders set the gas moving (e3ed370)
box/resting-heat-exchange 1 Periodic; particles at rest at 296.4 K in gas at rest at 294.2 K, HEATTRANSFER 2 particles_heat_exchange, mass, energy Heat transfer at zero slip used to be skipped (f0ae123)
box/sun-dilute 1 Periodic; gas at 5 m/s through 40 particles (450 µm) at 394 K, two-way, HEATTRANSFER 5; cell ε_s up to 0.075 particles_heat_exchange, mass, momentum, energy Sun's model below ε_s = 0.1, blended into Ranz–Marshall (955ba8e)
box/linear-interp 1, 2, 4 Closed; gas at rest with density linear in x, y, z; 40 tracers (1 µm) linear_interp, linear_grad_rhog, mass Interpolation is exact for a linear field (89f9661); so is the cell gradient of ρ_g in every cell, walls included
box/elongated-projection 1, 2 Closed, 2:1:1 cells; one fixed particle projection_shape, mass The projection kernel spreads ~0.44 cell lengths in every direction (414ce7f, affad85)
box/ghost-collisions 1, 2, 4 Periodic; 9 overlapping pairs at rest, collisions as the only force collision_pairs, same_as Pairs across bin faces, rank borders, periodic faces, edges and corners all behave like a pair inside one bin (ghost particles)
box/binary-collisions 1, 2, 4 Periodic; 4 approaching pairs (equal and 8:1 mass, head-on and oblique, 1–10 m/s), COLLISIONS 2 binary_collisions, same_as Momentum and angular momentum conserved, rebound with EREST (3afd23a)
box/walls 1, 2, 4 Closed, all walls PARTWALL 1; 7 particles hitting faces, a quad diagonal, an edge and a corner at 1–300 m/s wall_collisions, same_as (particle state only) Particle–wall contacts (2ff0cd8)

All box cases with particles also check particles_moved where the particles are meant to move.

Gas-Only Cases

No particles (USED 0): a pressure and density jump at x = 4.3 mm in the box, to test the flow solver's own conservation.

Case Ranks Setup Checks What it guards
box/lattice-torque 1, 2, 4 Periodic; gas at 50 m/s through a moving lattice of one particle per cell (726 µm), COLLISIONS 2 uniform_gas_cells Runs the torque path, whose gradient of the feedback force reads the virtual cells; the gas must stay uniform to round-off
box/gas-jump/closed 2 Slip walls, two regions mass, gas_energy Conservation across a region border
box/gas-jump/closed-creconst0 2 Same with CRECONSTC 0 mass, gas_energy The unconstrained cell-gradient path in a flow that pushes on the walls
box/gas-jump/periodic 1, 2 Periodic on all faces mass, gas_energy Conservative fluxes across periodic faces (11dc640)
box/gas-jump/symmetry 1, 2 Symmetry on all faces mass, gas_energy Conservative fluxes across symmetry faces (11dc640)

Unit Test

unit/bin-counts compiles bin_counts/bin_counts_test.f with ppiclF's bin-count routine (libpicl/source/ppiclf_bincounts.f) and runs it, without MPI. It checks a table of bin boxes against the counts they must get (the wedge bed as it stretches, a cube, a slab, a long bed, one rank) and that every layout over about 330 000 boxes and rank counts is valid: no more bins than ranks, bins at least the minimum length. The cases run on at most 4 ranks with small boxes, so they never reach these layouts.

Known Issues Tracked by the Suite

None at the moment. Fixed and now guarded:

Guards with no known bug behind them:

Flow-File Conventions

What the five arrays of SOL_*/<case>.mixt.cva_* hold depends on whether particles are on (RFLU_TimeStepping converts real cells just before each dump):

The fluid dumps (restart_data/fluid_<step>_<region>.dat) hold the state as it is in memory: (1-phi)*rho, ... with particles; rfluinit's step 0 dump the pure gas state. Layouts of both dump files: modflu/RFLU_ModRestartData.F90; readers: rocpicl_tests/outputs.py.

Goldens store the arrays in the old-style form above (made from the dumps). Conservation totals convert to bulk quantities with phi from the fluid dump (at step 0, the particle volume projected at the start of the run over the cell volume).

2D Cases

DIMENS 2: one cell thick in z, BC_VIRTUAL z patches, particles on the mid-plane. All particle cases also run in_plane: z, vz and the x and y spin stay fixed, and the gas has no z velocity.

Case Ranks Setup Checks What it guards
2d/shocktube/two-way 1, 2, 4 Shock over four particles, feedback on particles_moved, same_as The whole coupling in 2D
2d/momentum-exchange 1, 2 Periodic in x and y; gas at 50 m/s through 40 particles at rest; restart test and old-style output on 2 ranks particles_moved, mass, momentum, energy, same_as Conservation in 2D; bins written as quads
2d/linear-interp 1, 2 Slip walls; density linear in x and y linear_interp, mass The fit drops z and stays exact
2d/elongated-projection 1, 2 One particle in 2:1 cells projection_shape, mass Volume fraction centred and spread in x and y
2d/ghost-collisions 1, 2, 4 Periodic in x and y; overlapping pairs across bin, rank and periodic faces particles_moved, collision_pairs, same_as Neighbour search and ghosts in 2D (its subbin grid once had no z layer, so collisions were silently off)
2d/binary-collisions 1, 2, 4 Approaching pairs, COLLISIONS 2 particles_moved, binary_collisions, same_as Restitution, angular momentum, spin only about z
2d/walls 1, 2, 4 Particle walls on x and y: faces, a quad diagonal, a corner particles_moved, wall_collisions Particle walls in 2D
2d/gas-jump/lz1mm 1, 2 Gas only, slip walls mass, gas_energy Flow solver conservation in 2D
2d/gas-jump/lz100um 1 Same, 0.1 mm thick mass, gas_energy, gas fields equal to lz1mm The gas does not depend on the thickness: virtual patches set no time-step limit

Adding a Case

  1. Add it to rocpicl_tests/cases.py.
  2. Run ./run_tests.py run --generate -o <name>. The golden is written only if the physics checks pass, or if the case has a known_issue.
  3. Commit the new goldens/<name>/ directory.