Testing Guide
July 26, 2026 · View on GitHub
Project: particula
Last Updated: 2026-07-26
particula uses pytest as its primary testing framework. Tests should be close to the code they validate and should exercise scientific correctness, edge cases, and regression behavior.
Framework
- pytest: test discovery and execution.
- pytest-cov: coverage reporting.
- NumPy testing helpers: numerical comparisons and tolerances.
File Naming
All test files must use the *_test.py suffix.
Correct:
activity_coefficients_test.py
coagulation_test.py
vapor_pressure_test.py
Wrong:
test_activity_coefficients.py
activity_coefficients_tests.py
streamTest.py
This pattern matters because pytest discovery, ruff per-file ignores, and agent tooling all rely on it.
Test Locations
Place tests in tests/ subdirectories alongside source modules.
particula/
├── activity/
│ ├── activity_coefficients.py
│ └── tests/
│ └── activity_coefficients_test.py
├── gas/
│ └── tests/
└── particles/
└── tests/
Integration tests live in particula/integration_tests/.
Commands
# Run all tests
pytest
# Run with coverage
pytest --cov=particula --cov-report=term-missing
# Run a module's tests
pytest particula/activity/tests/
# Run a single file
pytest particula/activity/tests/activity_coefficients_test.py
# Run a single test
pytest particula/activity/tests/activity_coefficients_test.py::test_function_name
# Match CI warning behavior
pytest -Werror
Marker Policy
Repository-wide pytest marker registration lives in particula/conftest.py and
pyproject.toml.
- Registered markers include
slow,performance,benchmark,warp,cuda,gpu_parity, andstochastic. - Marker registration is descriptive by default. Plain
pytestpreserves normal collection behavior unless a test module opts into its ownpytest.importorskip("warp")or similar runtime guard. --benchmarkremains the only collection-affecting pytest option in the repository. Benchmark-marked tests are skipped unless you pass that flag.
Use the GPU-oriented markers to describe intent clearly:
@pytest.mark.warp: Warp-dependent or Warp-targeted coverage.@pytest.mark.cuda: CUDA-specific or CUDA-if-available coverage.@pytest.mark.gpu_parity: CPU/Warp/CUDA parity validation.@pytest.mark.stochastic: stochastic or tolerance-band regression coverage.
Warnings
CI treats warnings as errors with pytest -Werror. Tests that pass locally may
fail in CI if they emit RuntimeWarning, DeprecationWarning, or similar.
Preferred handling order:
- Fix the underlying warning condition.
- Use
pytest.warns()when warning emission is intentional behavior. - Use a specific warning filter only when the warning is expected and not the subject of the test.
import pytest
def test_expected_warning():
"""Test that the warning is part of the public behavior."""
with pytest.warns(RuntimeWarning, match="radius values are zero"):
result = function_that_warns()
assert result is not None
Scientific Test Patterns
Use numpy.testing for numerical comparisons.
import numpy as np
import numpy.testing as npt
def test_physical_property():
"""Test a known physical-property value."""
temperature = 298.15 # K
pressure = 101325.0 # Pa
result = calculate_density(temperature, pressure)
expected = 1.184
npt.assert_allclose(result, expected, rtol=1e-3)
For conservation laws, compare initial and final totals with an appropriate tolerance.
Performance Benchmarks
The staggered condensation benchmark suite is heavy and excluded from normal CI. Run it manually when changing staggered condensation behavior:
pytest particula/dynamics/condensation/tests/staggered_performance_test.py -v -m "slow and performance"
This suite verifies O(n) scaling at 1k/10k/100k particles, theta-mode behavior, and deterministic seeded behavior. Staggered stepping uses sequential Gauss-Seidel updates, so high overhead compared to simultaneous vectorized stepping is expected.
Wall Loss Coverage
Wall loss strategy tests should cover both package export paths where relevant:
particula/dynamics/wall_loss/tests/wall_loss_strategies_test.pyparticula/dynamics/tests/wall_loss_strategies_test.py
Coverage should include spherical and rectangular geometry, rectangular chamber dimension validation, supported distribution types, zero concentration, empty particle-resolved inputs, and parity with helper functions.
NVIDIA Warp Tests
GPU code must match Python/NumPy reference implementations. Warp CPU is the
default parity backend whenever Warp is installed. CUDA coverage is optional
and local/manual when a CUDA-capable device is available; standard CI must skip
cleanly when CUDA is unavailable. Use the existing markers exactly as
registered: warp, cuda, gpu_parity, and stochastic.
Release-validation command sets
Use focused Warp runs for the default supported validation path. Warp CPU is the required baseline whenever Warp is installed; the deterministic focused command also exercises CUDA when it is available. These are the shipped release-validation commands:
pytest particula/gpu/tests/cuda_availability_test.py -q
pytest particula/gpu/kernels/tests/environment_test.py -q
pytest particula/gpu/kernels/tests/thermodynamics_test.py -q -Werror
pytest particula/gpu/kernels/tests/dilution_test.py -q -Werror
pytest particula/gpu/kernels/tests/exhaustion_test.py -q -Werror
pytest particula/gpu/kernels/tests/nucleation_test.py -q -Werror
pytest particula/gpu/kernels/tests/slot_management_test.py -q -Werror
pytest particula/gpu/kernels/tests/wall_loss_test.py particula/gpu/kernels/tests/wall_loss_parity_test.py -q -Werror
pytest particula/gpu/kernels/tests/condensation_test.py -q -Werror
pytest particula/gpu/kernels/tests/coagulation_validation_test.py -q -m "warp and gpu_parity" -Werror
pytest particula/gpu/kernels/tests/coagulation_stochastic_validation_test.py -q -m "warp and stochastic and not cuda" -Werror
pytest particula/gpu/kernels/tests/coagulation_test.py -q -Werror
# Validate private resident composition of shipped direct GPU boundaries.
pytest particula/gpu/tests/process_sequence_test.py -q -Werror
# Validate the published direct-GPU coagulation example and documentation.
pytest particula/gpu/tests/gpu_coagulation_direct_example_test.py -q -Werror
# Validate the explicit-transfer complete-process example.
pytest particula/gpu/tests/gpu_complete_process_sequence_example_test.py -q -Werror
# Validate documentation links and the closeout projection without Warp or CUDA.
pytest particula/tests/gpu_coagulation_docs_test.py -q -Werror
# Validate the E6 complete-process publication and blocked closeout projection.
pytest particula/tests/gpu_complete_process_sequence_docs_test.py -q -Werror
Use CUDA-targeted runs only for optional local/manual validation when a CUDA-capable device is available. CUDA is additive evidence, not the default path, and the same Warp-marked modules should continue to collect safely when CUDA is absent:
pytest particula/gpu/kernels/tests/environment_test.py -q -m "warp and cuda"
pytest particula/gpu/kernels/tests/condensation_test.py -q -m "warp and cuda" -Werror
pytest particula/gpu/kernels/tests/coagulation_stochastic_validation_test.py -q -m "warp and cuda" -Werror
For the full fixed-mask contract, see the GPU coagulation validation record. These commands match the shipped marker and helper contract:
- Warp CPU is the baseline parity backend when Warp is installed.
- CUDA validation is additional local/manual evidence until dedicated CI exists.
- Missing Warp or missing CUDA should produce expected skips, not release failures, when a command reaches a guarded suite.
- Warp-marked tests should avoid eager module-level
pytest.importorskip("warp")patterns so marker deselection does not force a collection-time Warp dependency. - Benchmark coverage stays opt-in behind
--benchmarkand remains separate from the default release-validation path above. - Marker selection describes test intent; it does not select a device. In particular, deterministic P2 public-step checks enumerate available Warp devices, so they exercise CUDA when it is available as well as the Warp CPU baseline.
particula/gpu/tests/process_sequence_test.py provides private, test-only
resident composition coverage for the existing exported direct condensation,
coagulation, dilution, wall-loss, and nucleation boundaries. It keeps the same
caller-owned Warp containers and sidecars resident between calls, checks
identity, accounting, no-op/preflight, and RNG-sidecar contracts, and guards
against intermediate CPU restoration. This is contributor test evidence only:
it does not create a production coordinator, public integration API, hidden
transfer, CPU fallback, or runnable. Run its CUDA-marked row separately when
available:
pytest particula/gpu/tests/process_sequence_test.py -q \
-m "warp and cuda" -Werror
docs/Examples/gpu_complete_process_sequence.py is a standalone, direct-Warp
example with a focused regression suite in
particula/gpu/tests/gpu_complete_process_sequence_example_test.py. The suite
checks the deterministic no-Warp path without requiring Warp, then checks the
enabled path's one explicit conversion of each CPU container, five-call order,
one synchronization, and one final restore. It also verifies that direct errors
propagate without an intermediate restore or CPU fallback. This example is
documentation for explicit caller-owned transfers and sidecars; it does not
create a scheduler, high-level runnable, backend selector, or integration API.
The coagulation validation matrix supports exactly the singleton masks 1,
2, 4, and 8; two-way masks 3, 5, 6, 9, 10, and 12; and
four-way mask 15. The three-way masks 7, 11, 13, and 14 remain
deferred/fail closed. P1 uses rtol=1e-7, atol=0 for Brownian pair-rate
comparisons. Brownian property and selector-majorant checks, along with other
applicable positive/additive rate, property, and majorant comparisons, use
rtol=1e-6, atol=0; physical zeros are exact. P2 keeps concentration-weighted,
per-box/per-species inventory at rtol=1e-12, atol=1e-30 and separates that
ownership evidence from stochastic acceptance.
The P3 coagulation stochastic-validation matrix uses 100 fresh seeds for every
executable row and device. It compares aggregate accepted collisions with an
independent initial-state expectation using 3 * sqrt(expected_mean). This
bound is neither conservation evidence nor an exact accepted-pair, seed, RNG,
CPU/Warp, or CUDA replay requirement.
The record is limited to the existing direct path: it makes no mandatory-CUDA, production-API, new-physics, performance, CPU-fallback, runnable, graph-capture, autodiff, or adaptive-stepping conclusion.
GPU thermodynamics refresh coverage belongs in
particula/gpu/kernels/tests/thermodynamics_test.py. Test explicit
device-resident refresh behavior against CPU vapor-pressure references, cover
constant and canonical Buck modes across the freezing boundary, and verify
invalid inputs leave the caller-owned vapor-pressure buffer unchanged. The
refresh primitive remains a concrete-module API and is not condensation
integration coverage.
GPU dilution P2--P4 coverage belongs in
particula/gpu/kernels/tests/dilution_test.py. Mark it warp, defer Warp
imports so missing Warp skips cleanly, and import dilution_step_gpu from
particula.gpu.kernels. Cover scalar and metadata-valid per-box coefficient
forms, identity return, protected-field preservation, write-free scalar-zero
and zero-time paths, and the independent finite-step oracle
c_new = c * exp(-alpha * time_step) for particle and gas concentrations.
P4 runs a deterministic float64 one-/multi-box matrix against that NumPy
oracle on Warp CPU, with matching CUDA rows that skip cleanly when unavailable.
Keep particle and gas parity assertions separate, preserve caller-owned per-box
coefficient identity and values, and use exact equality for no-op checks. This
is direct-kernel test evidence only; it does not establish CPU-runnable parity.
GPU resampling coverage belongs in
particula/gpu/kernels/tests/exhaustion_test.py. Mark device cases warp and
gpu_parity, defer Warp imports, and import only resampling_step_gpu from
particula.gpu.kernels; ResamplingBuffers remains concrete-module-only at
particula.gpu.kernels.exhaustion. Test against an independent NumPy P2
equal-weight remapping oracle, including stable source ordering, original-slot
retention/release, diagnostics, and separate tight inventory conservation.
Preflight failures must preserve every caller array. Planning failures may alter
documented buffer lanes but must skip commit and preserve particles; all-zero
demand and empty-box calls must be exact write-free no-ops. Warp CPU is the
baseline and CUDA is optional with clean skips. Keep scaling evidence marked
slow, performance, and benchmark, behind --benchmark; it must not imply
a CPU fallback, resizing, policy-resolution, runnable, or broad performance
claim.
Private GPU nucleation P2 coverage belongs in
particula/gpu/kernels/tests/nucleation_test.py. Test the concrete module seam
only; do not add a package export, runnable, or user-facing example. Use an
independent NumPy float64 oracle for survival-included activation or kinetic
rate, E_pot = J * dt, common inventory admission, removal, and gate-code
precedence. Assert that successful planning changes only its documented
caller-owned planning, finalized-demand, and diagnostic sidecars; particle and
gas fields, plus P3-owned sidecar lanes, must remain unchanged. Exercise Warp
CPU when installed and optional CUDA with clean skips. Keep invalid-preflight
and derived-demand failures non-mutating, and keep this evidence separate from
future activation or transaction behavior.
GPU slot activation P4 coverage belongs in
particula/gpu/kernels/tests/slot_management_test.py. Defer Warp imports so
missing Warp skips cleanly, import only activate_slots_gpu from
particula.gpu.kernels, and retain get_slot_diagnostics_gpu as a
concrete-module-only P3 test helper. Compare deterministic float64 copies and
all caller-owned int32 sidecars exactly against independent CPU activation and
post-call diagnostics. Cover ascending-free-slot mapping, selected-prefix-only
validation, zero prefixes, zero boxes, zero capacity, exact capacity, repeated
activation, and sparse capacity. Reusable fixed-slot coverage should keep an
independent CPU oracle and separately assert exact values, shapes, dtypes,
devices, and sidecar identity. Include selected-prefix coverage and snapshot
accessible particle, request, and output arrays for preflight rejection.
Warp CPU is the baseline; CUDA parity is optional and must skip cleanly when
unavailable. The complementary CPU contract evidence is:
pytest particula/particles/tests/slot_management_test.py -q -Werror
This tests the bounded direct-Warp boundary only, not a runnable, implicit transfer, storage resizing, or rollback after writer launch.
Direct GPU wall-loss parity coverage belongs in
particula/gpu/kernels/tests/wall_loss_parity_test.py. Keep the NumPy
CPU/system-state oracle independent of GPU helpers and compare complete
particle-resolved slots with the CPU system-state wall-loss functions. The Warp
diagnostic intentionally uses approved production GPU property helpers to mirror
the supported GPU calculation path; it is not independently derived. Cover
spherical and rectangular geometries, one- and multi-box inputs, per-box
environment state, particle scales, and inactive or unusable slots. Charged
coverage must use an independent charged CPU oracle, retain exact neutral
fallback checks for zero-charge slots, and preserve caller-owned rectangular
field storage. Keep deterministic coefficient agreement separate from
stochastic evidence. For stochastic removal, use aggregate survival counts
across repeated fresh seeds and a documented binomial bound; do not require
CPU/Warp RNG-stream, per-seed, or trajectory replay. Include distinct exact
no-op and invalid-preflight non-mutation checks for zero time and all-inactive
inputs, including caller-owned RNG state. This suite validates the existing
direct-kernel boundary only and must not imply a new public API or physics
capability.
Device-aware tolerance policy
Keep GPU assertions in three separate classes. Deterministic parity, conservation checks, and stochastic validation each need their own pass criteria so stochastic expectations never relax conservation assertions or imply exact replay requirements.
- Deterministic parity: use explicit
numpy.testing.assert_allclose(..., rtol=..., atol=...)bounds for CPU vs Warp CPU comparisons and for optional CUDA comparisons when run locally. This is the parity rule for deterministic reference agreement. - Conservation checks: keep mass or count drift tolerances tight and assert them separately from parity checks. Do not relax conservation bounds just because the surrounding kernel, seeded replay, or diagnostic fixture uses stochastic sampling.
- Stochastic validation: compare aggregate behavior across repeated seeds or time steps with documented tolerance bands or sigma-based bounds. Use those bounded aggregate expectations instead of exact per-seed equality across CPU, Warp CPU, or CUDA.
Document the chosen tolerances in the test body or nearby comments when they are not already obvious from the physics or baseline study.
For GPU coagulation acceptance work, including charged-hard-sphere-only and
canonical Brownian-plus-charged coverage, keep attempted-vs-accepted collision
instrumentation private to particula/gpu/kernels/tests/coagulation_test.py.
The shipped coagulation_step_gpu(...) API and production synchronization
behavior should stay unchanged when the work is diagnostic-only.
The direct ST1956 turbulent-shear singleton uses explicit positive finite
turbulent_dissipation (m²/s³) and fluid_density (kg/m³) P2 inputs. Cover
scalar and active-device wp.float64 (n_boxes,) inputs. Executable masks are
the singletons 1, 2, 4, and 8; two-term masks 3, 5, 6, 9, 10,
and 12; and the four-term mask 15. The three-term masks are deferred:
mask 7 rejects at capability preflight before particle metadata or enabled-term
validation, while masks 11, 13, and 14 validate particle metadata and all
enabled terms (including turbulent P2 inputs where enabled) before raising
ValueError("Additive coagulation execution is deferred."). Deferred errors
occur before downstream normalization, output/RNG work, executable launches, or
mutation. Test turbulent, charged, and sedimentation validation according to the
enabled mechanism bits, while proving non-turbulent masks ignore turbulent
arguments and caller-owned particle, collision-output, and persistent-RNG state
is unchanged after preflight errors. Test the turbulent singleton's O(A)
two-largest-active-radii majorant against an independent NumPy oracle. For every
executable additive mask, public-path multi-pair tests must independently sum
enabled component rates and use finite, discriminating timesteps so omission of
one contribution changes bounded selection outcomes. Keep mass conservation
separate from stochastic acceptance checks and use aggregate tolerance or sigma
bounds rather than exact seeded pair replay.
For the GPU condensation suite, keep shared helpers in support modules only when
discoverable *_test.py wrappers expose the runnable cases. The current entry
points are:
particula/gpu/kernels/tests/condensation_test.pyparticula/gpu/kernels/tests/condensation_stiffness_test.pyparticula/gpu/kernels/tests/condensation_graph_capture_test.pyparticula/gpu/kernels/tests/condensation_autodiff_test.py
import numpy as np
import numpy.testing as npt
import pytest
wp = pytest.importorskip("warp")
def test_gpu_matches_numpy():
"""Warp computation matches the NumPy reference."""
expected = numpy_reference(...)
result = warp_result(...)
npt.assert_allclose(result, expected, rtol=1e-10, atol=0.0)
For conservation checks, keep the assertion separate and tight:
def test_total_mass_is_conserved():
"""Accepted collisions conserve total mass."""
initial_total = np.sum(initial_mass)
final_total = np.sum(final_mass)
npt.assert_allclose(final_total, initial_total, rtol=1e-12, atol=0.0)
The direct GPU condensation hook has deterministic fp64 parity-matrix coverage
in particula/gpu/kernels/tests/condensation_test.py. It runs one-box and
multi-box/multi-species fixtures against an independent NumPy fixed-four-
substep, P2 inventory-finalized, gas-coupled oracle. Compare final particle
masses and gas concentrations independently, with explicit tolerance bounds;
do not replace these checks with an aggregate inventory assertion. The matrix
covers uptake, evaporation, disabled partitioning, latent heat, zero gas, and
inactive particle slots. Warp CPU is required whenever Warp is installed;
the matching CUDA matrix is optional and must skip cleanly when unavailable.
This is scoped direct-kernel evidence, not CPU-strategy or runnable parity.
The public GPU condensation hook also has separate deterministic per-box,
per-species inventory regression coverage. Compare the change in particle
inventory (particle mass weighted by particle concentration) plus the gas
change against zero at rtol=1e-12, atol=1e-30. Keep this conservation
assertion separate from CPU-oracle particle and gas parity.
For direct-condensation P1--P4 coverage, keep parity, conservation, capture, and derivative assertions separate. Warp CPU is the baseline for supported P1 parity/P2 conservation and bounded P4 raw-rate autodiff probes when Warp is installed; CUDA is optional local/manual evidence. P3 is the exception: CPU graph capture is capability-skipped, and only the CUDA public-step host validation readback within capture is a strict expected failure because it is not capture-safe. Setup and normal calls remain ordinary assertions. These precise guarded skips/xfails document an unsupported capture capability, not a CPU-baseline or replay-support claim. The P4 wrapper covers only an interior, out-of-place raw-rate Tape derivative; it does not relax P2 boundary or in-place-mutation limits.
For stochastic kernels, assert bounded aggregate behavior instead of replaying
exact accepted-collision sequences or per-seed trajectories. A seeded range can
be used to gather repeated evidence, but the pass condition should be a
documented aggregate bound such as a tolerance interval or 3-sigma window
around the expected mean rather than exact seed-by-seed replay.
Use constants from particula.util.constants; do not hardcode physical
constants in kernels.
For CPU↔GPU container helpers, add round-trip coverage that checks exact value
and shape preservation on the Warp CPU backend. For EnvironmentData, cover
single-box and multi-box cases, the default synchronized path, any supported
manual sync=False path, and malformed-schema failures surfaced by CPU-side
validation.
When evaluating experimental GPU integrator candidates, keep prototypes test-local until they are production-qualified. Prefer deterministic fixed-shape helpers, caller-owned scratch or buffer reuse, repeated-run equality checks, finite and non-negative mass assertions, and explicit CPU-reference error bounds recorded in the test or companion study note. If public runtime behavior is unchanged, say so directly in the related documentation.
For deterministic GPU baseline studies, prefer explicit np.float64 fixture
construction over random inputs so later precision comparisons have a stable
reference. The mass-precision baseline suite is the reference pattern:
pytest particula/gpu/tests/mass_precision_cases_test.py -q
Keep those tests warning-clean under -Werror, assert canonical
(n_boxes, n_particles, n_species) shapes where relevant, and document any
baseline assumptions in the matching roadmap page.
For the mixed NPF/droplet coagulation diagnostic coverage added in
particula/gpu/kernels/tests/coagulation_test.py, keep selector-validity and
low-active regressions focused on seeded invariants: accepted pairs stay
sorted, in bounds, and limited to originally active slots; zero/one-active
inputs early-return cleanly; exactly-two-active inputs fall back to the only
valid pair; and accepted collisions conserve total mass. For exact charged-only
coverage, also assert signed-charge conservation and test aggregate stochastic
behavior against an independent oracle rather than exact pair replay. Use
focused local runs such as:
pytest particula/gpu/kernels/tests/coagulation_test.py -q -k mixed_scale
pytest particula/gpu/kernels/tests/coagulation_test.py -q -k "mixed_scale or sparse or degenerate or conservation" -Werror
These checks are intended for seeded regression and warning-clean acceptance sanity, not for exact CPU/CUDA equality or user-facing feature documentation.
Test Quality
- Use descriptive test names such as
test_coagulation_conserves_total_mass. - Keep tests independent; do not rely on test execution order.
- Use parametrization for related input variants.
- Prefer focused assertions, but include enough checks to validate the behavior.
- Add regression tests when fixing bugs.
Troubleshooting
- If tests are not discovered, check
*_test.pynaming and runpytest --collect-only. - For the GPU condensation suite, verify collection via
pytest particula/gpu/kernels/tests/condensation_test.py --collect-only -qandpytest particula/gpu/kernels/tests/condensation_stiffness_test.py --collect-only -q. - If imports fail, install the package in development mode with
pip install -e .[dev]. - If coverage looks wrong, run
pytest --cov=particula --cov-report=term. - If CI fails but local tests pass, rerun locally with
pytest -Werror. - If Warp is not installed, Warp-marked suites may skip through
pytest.importorskip("warp"); treat that as the expected missing-Warp path, not a regression. - If CUDA is unavailable, CUDA-targeted coverage should skip cleanly instead of
failing CPU-only or CI validation. Some guarded paths use the shared
Warp/CUDA not availablemessage, while others keep more specific skip reasons when that context is more useful. - Use marker selection such as
-m "warp and gpu_parity",-m "warp and stochastic", or-m "warp and cuda"for targeted local GPU validation, and keeppytest particula/gpu/tests/benchmark_test.py --benchmark -v -sseparate as opt-in benchmark evidence rather than default release validation.