Skip to content

Data-Oriented Design and GPU Roadmap

This page tracks the status of the data-oriented container migration and the Warp-backed GPU work for particle-resolved aerosol simulations.

The roadmap is a single ordered sequence of epics with explicit boundaries. Epics are worked in the order below: when an epic meets its exit bar, the next pending epic in the sequence becomes the active one. Cross-cutting documentation, validation-infrastructure, and API-stability work is folded into each epic's feature list rather than running as a separate parallel epic.

Sizing convention: each epic targets roughly 5-10 features, each feature roughly 5-15 phases, and each phase roughly 100 lines of new source code plus its tests and documentation under the repository's canonical planning and documentation conventions.

Epic Sequence and Status

Order Epic Status ADW plan
1 Epic A: Data-Model and Numerical Foundations Shipped E2
2 Epic B: Non-Isothermal Condensation Public API (CPU) Shipped E1
3 Epic C: GPU Kernel Correctness and Low-Level API Hardening Shipped E3
4 Epic D: GPU Condensation Physics Parity Shipped E4
5 Epic E: GPU Coagulation Physics Coverage Shipped E5
6 Epic F: GPU Process Completeness Active not scheduled
7 Epic G: Backend Selection and GPU-Resident Simulation Pending not scheduled
8 Epic H: Graph Capture and Performance Pending not scheduled
9 Epic I: Differentiability and Global Optimization Pending not scheduled

The former suggested milestones are absorbed into the per-epic exit bars: Milestone 1 (documented low-level GPU API) is Epic C, Milestone 2 (backend selection) is Epic G, Milestone 2.5 (missing GPU physics) is Epics D-F, Milestone 2.75 (differentiable condensation) is Epic I, and Milestone 3 (production GPU workflows) is the combined Epic G and Epic H exit bars.

Quick links:

Motivation and Target Workloads

The GPU and data-oriented work exists to enable two driving use cases.

  • Large multi-box scaling. The primary performance target is many simulation boxes evolved together on the GPU, not just large particle counts in a single box. Multi-box throughput is the main scaling axis, and benchmarks should prioritize scaling in the number of boxes.
  • Global optimization against observations. A longer-term goal is to use Warp automatic differentiation to compute gradients through the simulation so model parameters can be fit to experiments or observations. Large multi-box ensembles support this by evaluating many conditions per optimization step.

These two goals reinforce each other: multi-box execution provides both the scaling target and the batch of conditions used during global optimization.

The intended physical scope spans new particle formation through cloud droplet sizes. This wide dynamic range drives the precision and mass-resolution decisions in Numerical Precision and Mass Resolution and the time-integration decisions in Time-Scale Stiffness. New particle formation is both a size-range driver and a planned process: a nucleation/particle-source process does not exist in particula today and is added as an Epic F work item so freshly formed particles can enter GPU-resident simulations through slot activation.

Non-Goals

The following are explicitly out of near-term scope.

  • No multi-GPU or distributed-memory execution yet. Single-device GPU-resident simulation comes first.
  • No full CFD coupling between boxes. Box communication is limited to prescribed advection, dilution, expansion, and simple mixing maps.
  • No dynamic in-kernel array resizing. Particle-count changes are represented as active/inactive fixed-shape slots.
  • DNS turbulence is deferred until the simpler GPU coagulation kernels are stable and validated.
  • No GPU staggered (Gauss-Seidel) condensation. The staggered strategy is inherently sequential per-particle and remains CPU-only; GPU condensation covers the isothermal and latent-heat variants.
  • No automatic silent CPU/GPU data movement inside long simulation loops. Fallback boundaries are explicit.

Current Status

Data-Oriented Design

The core data-oriented model is implemented.

  • ParticleData stores particle masses, concentrations, charges, densities, and box volumes with explicit batch dimensions.
  • GasData stores gas species names, molar masses, concentrations, and partitioning flags with explicit box dimensions.
  • Conversion helpers bridge legacy facades and new containers.
  • Condensation and coagulation strategies accept the new data containers in addition to legacy facades, but that compatibility is a container-boundary statement and not proof of shipped CPU multi-box execution across every box.
  • Migration documentation exists in ParticleData and GasData Migration.

For the canonical user-facing CPU support boundary, including the current n_boxes == 1 limitation for audited CPU condensation and CPU coagulation container workflows, refer to Data Containers and GPU Foundations. Use ParticleData and GasData Migration as the implementation walkthrough companion.

The work is still a migration, not a full replacement. ParticleRepresentation and GasSpecies remain available for compatibility.

Known data-model gaps:

  • EnvironmentData now provides a CPU-side home for per-box thermodynamic state, with temperature -> (n_boxes,), pressure -> (n_boxes,), and saturation_ratio -> (n_boxes, n_species). The shipped baseline is available from particula.gas.environment_data, exported from particula.gas, supports copy() for CPU-side state handling, and only crosses the CPU↔GPU boundary through public particula.gpu helpers: WarpEnvironmentData, to_warp_environment_data(), and from_warp_environment_data(). Those helpers are the explicit transfer boundary: kernels and runnables do not perform hidden CPU↔GPU environment synchronization or movement. Broad runtime integration still remains downstream work (see EnvironmentData Container).
  • ParticleData.density is shaped (n_species,) and shared across boxes, so boxes at different temperatures cannot carry per-box densities.
  • E6-F1 ships the CPU finite-step dilution oracle and E6-F2 ships P1–P4 direct GPU dilution. Import the low-level operation with from particula.gpu.kernels import dilution_step_gpu; this is not a high-level process or runnable reference. E6-F9 remains the planned integrated direct-call consumer.

Warp GPU Backend

The lower-level Warp backend is implemented and covered by targeted tests.

  • Warp-side particle and gas containers mirror the CPU-side data containers.
  • CPU/GPU transfer helpers support long GPU-resident workflows, including a gpu_context context manager and a WARP_AVAILABLE feature flag.
  • GPU condensation kernels provide a tested condensation_step_gpu API.
  • GPU Brownian coagulation kernels provide a tested coagulation_step_gpu API.
  • GPU benchmark scaffolding exists for CUDA-enabled environments.
  • Warp CPU is the baseline validation backend whenever Warp is installed, while CUDA validation remains optional/local/manual rather than a CI requirement. Missing Warp and missing CUDA are expected skip paths for guarded coverage, not release failures. Deterministic parity uses explicit numpy.testing.assert_allclose(..., rtol=..., atol=...) bounds, conservation checks stay separately tight, the matching pytest markers remain warp, cuda, gpu_parity, and stochastic, and CPU-only or standard CI environments are expected to skip CUDA-specific coverage cleanly instead of treating it as a regression. Focused examples include pytest particula/gpu/kernels/tests/condensation_test.py -q -m "warp and gpu_parity and not cuda" for the required Warp CPU parity path and pytest particula/gpu/kernels/tests/condensation_test.py -q -m "warp and cuda" for optional CUDA-if-available evidence. This documentation records the explicit helper surface rather than implying automatic GPU runtime integration.

The GPU backend is currently a directly callable lower-level API. It is not yet integrated as an automatic backend in the main Aerosol, Runnable, or high-level dynamics workflows.

Known GPU physics gaps remain:

  • Bounded direct GPU condensation is a shipped, verified direct-kernel contract. It applies an optional Warp-backed latent-heat rate correction, couples P2-finalized transfer to authoritative active-device gas concentration, and records a signed energy diagnostic. Temperature feedback and high-level Runnable integration remain future work outside this low-level publication.
  • Particle charge supports direct low-level particle-resolved charged hard-sphere coagulation and charge-aware merges in every approved additive mask.
  • The GPU coagulation path executes singleton masks 1, 2, 4, 8; the two-way masks 3, 5, 6, 9, 10, 12; and four-way mask 15. Non-turbulent three-way mask 7 rejects at capability preflight before particle metadata or enabled-term validation. Turbulent three-way masks 11, 13, and 14 proceed through particle metadata and enabled-term validation, then reject before downstream normalization, allocation, RNG setup, kernel launch, or mutation. Requested tuples normalize to canonical Brownian, charged, sedimentation, then turbulent order. Alternate charged models, broader distributions, and mechanism variants remain outside this direct-kernel contract.
  • CPU sedimentation coagulation and simple turbulent shear coagulation exist. The shipped GPU paths do not establish CPU-strategy parity; DNS and general turbulence remain deferred from the near-term GPU scope.
  • Direct GPU dilution is shipped as a fixed-shape, caller-owned kernel. It is imported with from particula.gpu.kernels import dilution_step_gpu. E6-F1 supplies its upstream CPU finite-step oracle and E6-F9 remains its future integrated direct-call consumer. It accepts a finite nonnegative scalar coefficient or a same-device wp.float64 (n_boxes,) coefficient array, returns the same particle and gas containers, and mutates only concentration fields in place. Callers own CPU↔Warp transfer, device placement, and synchronization; there is no hidden transfer or fallback. Complete ordered preflight may run validation scans that allocate or launch, but rejected calls have no update launch or caller mutation. A scalar-zero coefficient or zero time step completes preflight then returns as a write-free, no-update-kernel no-op; validation scans may still allocate or launch. Independent float64 particle and gas comparisons on Warp CPU use rtol=1e-12, atol=0; CUDA rows are optional and skip cleanly when unavailable. This is tolerance-based evidence, not bitwise equality.
  • GPU runnable and process orchestration, backend selection/fallback policy, scheduling, GPU-resident timestep integration, high-level/runnable or broader wall-loss integration, nucleation, resizing, graph capture, autodiff, and performance work remain future scope. The bounded direct neutral wall-loss P1–P6 kernel contract is shipped; it is not that deferred high-level integration. Those items are needed before a full simulation can remain GPU-resident for every timestep.
  • Direct GPU condensation supports ideal/kappa water activity and static/composition-weighted surface tension through fixed-shape, caller-owned wp.float64 sidecars. This is direct-kernel support only, not high-level GPU Aerosol or Runnable support. BAT and every other deferred activity or surface strategy remain CPU-only and are rejected with ValueError; they are never silently approximated or transferred.
  • Gas vapor pressure is a GPU-only derived field: WarpGasData.vapor_pressure defaults to zeros when not supplied and is dropped when converting back to GasData. Every successful supported direct GPU condensation call refreshes it on-device from the current temperature; see the data containers and GPU foundations guide for the supported constant/Buck scope and refresh contract.
  • No nucleation/particle-source process exists on CPU or GPU.

Known GPU kernel defects and design limits (see Known Kernel Issues):

  • coagulation_step_gpu supports a convenience allocate-and-seed path when rng_states are omitted, while caller-owned rng_states persist across repeated calls until explicitly reset with initialize_rng=True.
  • The Brownian coagulation kernel launches one thread per box with sequential pair selection inside the thread, which limits single-box scaling at large particle counts.
  • Rejection sampling uses a single k_max bound from the min/max radius pair, which degrades acceptance rates when small particles and large droplets coexist in one box.

Epic A: Data-Model and Numerical Foundations

Status: shipped as plan E2. This foundation work now defines the container schemas, per-box thermodynamic state, precision baseline, mass representation policy, and time-integration recommendation that downstream GPU roadmap work must build on.

Shipped E2 Foundation Baseline

E2 closed the foundation scope needed by later GPU roadmap epics:

  • ParticleData, GasData, WarpParticleData, and WarpGasData have a documented field-ownership and shape contract, including leading n_boxes dimensions for per-box state and species-only shapes for shared material or gas metadata.
  • EnvironmentData is the CPU owner for per-box temperature, pressure, and per-box/per-species saturation_ratio; simulation volume remains owned by ParticleData.volume.
  • WarpEnvironmentData, to_warp_environment_data(), and from_warp_environment_data() provide the explicit CPU↔GPU environment transfer boundary. Kernels and runnables still do not perform hidden CPU/GPU synchronization or movement.
  • Gas CPU/GPU restore semantics are locked down: GasData.name is CPU-only ordered metadata, shared CPU partitioning expands to an active-device, binary wp.int32 mask indexed by box and species on GPU, and WarpGasData.vapor_pressure is GPU helper state dropped on CPU restore.
  • The low-level GPU condensation and coagulation environment inputs now share a scalar-or-per-box normalization contract, including explicit WarpEnvironmentData inputs.
  • The mass-precision report keeps absolute per-species np.float64 / wp.float64 particle mass storage as the production baseline and defers any dtype or schema migration until stronger evidence exists.
  • The condensation-stiffness study recommends fixed_count_substeps_4 as the fixed-shape foundation for direct GPU condensation, including its current P2-finalized gas-coupling contract.
  • The foundation guide, migration guide, data-container example, and support boundary docs now distinguish multi-box-capable storage from still-limited CPU process execution.

Post-E2 Data-Oriented Work

  • Finish reducing dependence on legacy facade objects in new examples and documentation.
  • Decide when ParticleRepresentation and GasSpecies should move from compatibility facades to deprecated APIs.
  • Keep strategy APIs consistent when accepting either legacy facades or data containers.
  • Expand examples that start directly from ParticleData and GasData instead of converting from legacy objects.
  • Expand implementation-facing examples and migration notes that point back to the canonical shape-conventions section for single-box, multi-box, binned, and particle-resolved simulations.

Current container schema inventory

The table below inventories the current public stored schema for ParticleData, GasData, WarpParticleData, and WarpGasData, including shared-across-box fields, box-batched fields, validation hooks, and current CPU↔GPU round-trip behavior. Evidence entries point to the current enforcing constructor or conversion helper and to representative existing regression tests. When a behavior is source-only, that is called out explicitly rather than implying broader test coverage.

Container Field Canonical owner Shape Dtype Storage / mutability note Validation / coercion hook CPU↔GPU transfer behavior Evidence
ParticleData masses ParticleData (n_boxes, n_particles, n_species) float64 Stored, mutable, box-batched particle/species mass array ParticleData.__post_init__() requires 3D masses and uses it as the shape anchor for all other particle fields to_warp_particle_data() copies or zero-copies to WarpParticleData.masses; from_warp_particle_data() restores the same shape and values Source: particula/particles/particle_data.py:76-98, particula/gpu/conversion.py:72-139,244-287; tests: particula/particles/tests/particle_data_test.py:124-136, particula/gpu/tests/conversion_test.py:512-548
ParticleData concentration ParticleData (n_boxes, n_particles) float64 Stored, mutable, box-batched number concentration/count ParticleData.__post_init__() enforces (n_boxes, n_particles) Round-trips through to_warp_particle_data() / from_warp_particle_data() without schema change Source: particula/particles/particle_data.py:77,96-103, particula/gpu/conversion.py:113-115,283; tests: particula/particles/tests/particle_data_test.py:138-164, particula/gpu/tests/conversion_test.py:521-545
ParticleData charge ParticleData (n_boxes, n_particles) float64 Stored, mutable, box-batched particle charge array ParticleData.__post_init__() enforces (n_boxes, n_particles) Round-trips through to_warp_particle_data() / from_warp_particle_data() without dtype conversion Source: particula/particles/particle_data.py:78,104-109, particula/gpu/conversion.py:116,284; tests: particula/particles/tests/particle_data_test.py:166-192, particula/gpu/tests/conversion_test.py:524-526,546-548
ParticleData density ParticleData (n_species,) float64 Stored, mutable, shared across boxes ParticleData.__post_init__() requires a 1D density array, expands a length-1 density array to n_species, and fills empty density with zeros when n_species > 0 to_warp_particle_data() mirrors the shared 1D array to WarpParticleData.density; from_warp_particle_data() restores it to CPU storage Source: particula/particles/particle_data.py:79,119-137, particula/gpu/conversion.py:117-119,285; tests: particula/particles/tests/particle_data_test.py:208-234, particula/gpu/tests/warp_types_test.py:38-58
ParticleData volume ParticleData (n_boxes,) float64 Stored, mutable, per-box simulation volume ParticleData.__post_init__() enforces (n_boxes,) Round-trips through to_warp_particle_data() / from_warp_particle_data() without schema change Source: particula/particles/particle_data.py:80,111-117, particula/gpu/conversion.py:120,286; tests: particula/particles/tests/particle_data_test.py:194-206, particula/gpu/tests/conversion_test.py:530-548
GasData name GasData len == n_species list[str] Stored, mutable CPU-only species metadata GasData.__post_init__() rejects an empty species list; it does not enforce uniqueness or survive GPU transfer on its own Not transferred by to_warp_gas_data() because WarpGasData has no string field; from_warp_gas_data() restores caller-supplied names or generates placeholders such as species_0, and current restore validation checks only the supplied name-list length Source: GasData.__post_init__() and from_warp_gas_data() in particula/gas/gas_data.py and particula/gpu/conversion.py; tests: particula/gas/tests/gas_data_test.py:119-127, particula/gpu/tests/conversion_test.py:600-640
GasData molar_mass GasData (n_species,) float64 Stored, mutable, shared across boxes GasData.__post_init__() coerces to np.float64 and enforces (n_species,) to_warp_gas_data() mirrors it to WarpGasData.molar_mass; from_warp_gas_data() restores the same numeric field Source: particula/gas/gas_data.py:65,75-77,103-108, particula/gpu/conversion.py:214-216,354; tests: particula/gpu/tests/warp_types_test.py:168-184,218-232, particula/gpu/tests/conversion_test.py:583-595
GasData concentration GasData (n_boxes, n_species) float64 Stored, mutable, box-batched gas mass concentration GasData.__post_init__() coerces to np.float64, requires 2D, and checks width against n_species to_warp_gas_data() mirrors it to WarpGasData.concentration; from_warp_gas_data() restores the same shape Source: particula/gas/gas_data.py:66,76-78,89-101, particula/gpu/conversion.py:217-219,355; tests: particula/gas/tests/gas_data_test.py:99-117, particula/gpu/tests/conversion_test.py:593-595
GasData partitioning GasData (n_species,) bool Stored, mutable, shared-across-boxes partitioning mask GasData.__post_init__() coerces with np.asarray(..., dtype=np.bool_) and enforces (n_species,); this is dtype coercion, not a stricter semantic validation layer beyond NumPy truthiness to_warp_gas_data() converts bool → int32; from_warp_gas_data() converts int32 → bool on restore Source: GasData.__post_init__(), to_warp_gas_data(), and from_warp_gas_data() in particula/gas/gas_data.py and particula/gpu/conversion.py; tests: particula/gas/tests/gas_data_test.py:77-97, particula/gpu/tests/conversion_test.py:189-199,609-619
WarpParticleData masses WarpParticleData (n_boxes, n_particles, n_species) wp.float64 Stored, mutable GPU mirror of ParticleData.masses Declared as wp.array3d(dtype=wp.float64); populated by to_warp_particle_data() Receives CPU particle masses on transfer to GPU; from_warp_particle_data() returns the same values to ParticleData Source: particula/gpu/warp_types.py:73, particula/gpu/conversion.py:111-125,282; tests: particula/gpu/tests/warp_types_test.py:38-58,100-118, particula/gpu/tests/conversion_test.py:512-548
WarpParticleData concentration WarpParticleData (n_boxes, n_particles) wp.float64 Stored, mutable GPU mirror of ParticleData.concentration Declared as wp.array2d(dtype=wp.float64); populated by to_warp_particle_data() Receives CPU particle concentration/count state and round-trips back unchanged Source: particula/gpu/warp_types.py:74, particula/gpu/conversion.py:113-128,283; tests: particula/gpu/tests/warp_types_test.py:54-58,113-118, particula/gpu/tests/conversion_test.py:521-545
WarpParticleData charge WarpParticleData (n_boxes, n_particles) wp.float64 Stored, mutable GPU mirror of ParticleData.charge Declared as wp.array2d(dtype=wp.float64); populated by to_warp_particle_data() Receives CPU charge data and round-trips back unchanged Source: particula/gpu/warp_types.py:75, particula/gpu/conversion.py:116,129-131,284; tests: particula/gpu/tests/warp_types_test.py:55-58,114-118, particula/gpu/tests/conversion_test.py:524-526,546-548
WarpParticleData density WarpParticleData (n_species,) wp.float64 Stored, mutable GPU mirror of the shared-across-boxes ParticleData.density array Declared as wp.array(dtype=wp.float64); populated by to_warp_particle_data() Receives shared CPU density array and restores it on from_warp_particle_data() Source: particula/gpu/warp_types.py:76, particula/gpu/conversion.py:117-119,132-134,285; tests: particula/gpu/tests/warp_types_test.py:57-58,113-118,302-320, particula/gpu/tests/conversion_test.py:527-529
WarpParticleData volume WarpParticleData (n_boxes,) wp.float64 Stored, mutable GPU mirror of per-box ParticleData.volume Declared as wp.array(dtype=wp.float64); populated by to_warp_particle_data() Receives per-box CPU volume array and restores it on from_warp_particle_data() Source: particula/gpu/warp_types.py:77, particula/gpu/conversion.py:120,135-137,286; tests: particula/gpu/tests/warp_types_test.py:57-58,113-118,302-320, particula/gpu/tests/conversion_test.py:530-548
WarpGasData molar_mass WarpGasData (n_species,) wp.float64 Stored, mutable GPU mirror of GasData.molar_mass shared across boxes Declared as wp.array(dtype=wp.float64); populated by to_warp_gas_data() from CPU gas state Restores the same numeric field through from_warp_gas_data() within the intentionally lossy gas boundary (name remains CPU-only and vapor_pressure is still dropped on CPU restore) Source: particula/gpu/warp_types.py:131, particula/gpu/conversion.py:214-216,228-230,354; tests: particula/gpu/tests/warp_types_test.py:168-184,218-232, particula/gpu/tests/conversion_test.py:583-595
WarpGasData concentration WarpGasData (n_boxes, n_species) wp.float64 Stored, mutable GPU mirror of GasData.concentration Declared as wp.array2d(dtype=wp.float64); populated by to_warp_gas_data() from CPU gas state Restores the same box-batched numeric field through from_warp_gas_data() within the intentionally lossy gas boundary (name remains caller-managed CPU metadata and vapor_pressure stays GPU-only) Source: particula/gpu/warp_types.py:132, particula/gpu/conversion.py:217-219,231-233,355; tests: particula/gpu/tests/warp_types_test.py:180-184,229-231, particula/gpu/tests/conversion_test.py:593-595
WarpGasData vapor_pressure WarpGasData (n_boxes, n_species) wp.float64 Stored, mutable GPU-only helper state for condensation kernels to_warp_gas_data() validates an optional provided shape or creates a zero-filled default when vapor_pressure is None Present only on the GPU container; from_warp_gas_data() always drops it because GasData has no matching field, so CPU restore is intentionally lossy Source: WarpGasData plus to_warp_gas_data() / from_warp_gas_data() in particula/gpu/warp_types.py and particula/gpu/conversion.py; tests: particula/gpu/tests/warp_types_test.py:177-183,225-231, particula/gpu/tests/conversion_test.py:200-229,484-496,588-595
WarpGasData partitioning WarpGasData (n_boxes, n_species) Active-device binary wp.int32 Stored, mutable per-box/per-species GPU helper mask Declared as wp.array2d(dtype=wp.int32); to_warp_gas_data() expands the shared CPU bool mask per box and converts it to binary int32 Kernels index [box, species]; CPU restore converts int32 → bool only when every box agrees Source: particula/gpu/warp_types.py; particula/gpu/conversion.py; tests: particula/gpu/tests/warp_types_test.py, particula/gpu/tests/conversion_test.py

Derived ParticleData accessors are intentionally separate from the stored schema above: radii, total_mass, effective_density, and mass_fractions are computed properties, not constructor-owned fields (particula/particles/particle_data.py:166-218; tests: particula/particles/tests/particle_data_test.py:237-360).

Current gas round-trip behavior is intentionally lossy in two places:

  • from_warp_gas_data() restores GasData.name from caller input or placeholder values such as species_0; the GPU container itself never preserves string species names, so callers must preserve ordered species metadata externally if they need a lossless semantic restore.
  • WarpGasData.vapor_pressure is GPU-only helper state. It defaults to zeros in to_warp_gas_data() when omitted and is dropped when restoring CPU GasData.

CPU↔GPU restore boundary for ordered gas metadata

Keep testing round-trips for the current intentional CPU/GPU contract explicitly: WarpGasData drops name, stores partitioning as int32 instead of bool, and adds vapor_pressure that is not restored to the CPU gas container.

Authoritative field ownership decisions

This section is the canonical ownership and CPU↔GPU round-trip contract shipped by E2. The inventory table above remains the current-state evidence record; use the decision table below for policy when adding fields, conversion behavior, or future GPU environment state.

Field / group Authoritative owner CPU shape GPU shape Dtype Mutability Round-trip behavior Downstream consumers Evidence
ParticleData.masses Owned by ParticleData as the authoritative particle/species mass state (n_boxes, n_particles, n_species) (n_boxes, n_particles, n_species) via WarpParticleData.masses float64 on CPU / wp.float64 on GPU Mutable stored state on both containers Must round-trip without schema drift through to_warp_particle_data() and from_warp_particle_data() Particle property accessors, condensation, coagulation, and future GPU-resident timestep loops particula/particles/particle_data.py:76-98; particula/gpu/conversion.py:72-139,244-287; particula/particles/tests/particle_data_test.py:124-136; particula/gpu/tests/conversion_test.py:512-548
ParticleData.concentration Owned by ParticleData as authoritative per-box particle concentration/count state (n_boxes, n_particles) (n_boxes, n_particles) via WarpParticleData.concentration float64 / wp.float64 Mutable stored state on both containers Must round-trip without schema change Particle dynamics kernels and any box-batched particle workflow particula/particles/particle_data.py:76-109; particula/gpu/conversion.py:113-115,283; particula/particles/tests/particle_data_test.py:138-164; particula/gpu/tests/conversion_test.py:521-545
ParticleData.charge Owned by ParticleData as authoritative particle charge state (n_boxes, n_particles) (n_boxes, n_particles) via WarpParticleData.charge float64 / wp.float64 Mutable stored state on both containers Must round-trip without dtype drift Charged coagulation follow-on work and GPU parity paths that consume particle charge particula/particles/particle_data.py:76-109; particula/gpu/conversion.py:116,284; particula/particles/tests/particle_data_test.py:166-192; particula/gpu/tests/conversion_test.py:524-526,546-548
ParticleData.density Owned by ParticleData and must remain shared-across-boxes material state, not per-box environment state (n_species,) (n_species,) via WarpParticleData.density float64 / wp.float64 Mutable stored state, but shared across boxes Must round-trip as shared 1D species density state Radius, effective-density, and mass-fraction calculations plus future GPU particle property parity particula/particles/particle_data.py:67-68,119-137; particula/gpu/conversion.py:117-119,285; particula/particles/tests/particle_data_test.py:208-234; particula/gpu/tests/warp_types_test.py:38-58
ParticleData.volume Owned by ParticleData as the authoritative per-box simulation-volume carrier (n_boxes,) (n_boxes,) via WarpParticleData.volume float64 / wp.float64 Mutable stored state on both containers Must round-trip without schema change Per-box particle workflows, dilution-style process work, and future environment/process coordination particula/particles/particle_data.py:69-70,111-117; particula/gpu/conversion.py:120,286; particula/particles/tests/particle_data_test.py:194-206; particula/gpu/tests/conversion_test.py:530-548
GasData.name Owned by CPU GasData as authoritative species-name metadata len == n_species Not owned on GPU list[str] Mutable CPU metadata only Does not survive transfer on its own; CPU restore requires caller-supplied ordered species names from external metadata, but the current from_warp_gas_data() implementation validates only name-list length before reconstructing GasData, so ordering must be preserved by the caller rather than inferred or checked during restore CPU-facing reporting, facade compatibility, and any restore path back to GasData particula/gas/gas_data.py:52-73; particula/gpu/warp_types.py:82-99; particula/gpu/conversion.py:155-157,301-345; particula/gas/tests/gas_data_test.py:119-127; particula/gpu/tests/conversion_test.py:600-640
GasData.molar_mass Owned by GasData as authoritative gas species molar-mass state (n_species,) (n_species,) via WarpGasData.molar_mass float64 / wp.float64 Mutable stored state on both containers Must round-trip with the same numeric values inside the documented gas restore boundary (name still requires caller-managed ordered metadata and vapor_pressure still has no CPU field) Gas property calculations, condensation, and GPU gas kernels particula/gas/gas_data.py:53-67,75-108; particula/gpu/warp_types.py:100-111,131; particula/gpu/conversion.py:214-216,354; particula/gpu/tests/warp_types_test.py:168-184,218-232; particula/gpu/tests/conversion_test.py:583-595
GasData.concentration Owned by GasData as authoritative per-box gas concentration state (n_boxes, n_species) (n_boxes, n_species) via WarpGasData.concentration float64 / wp.float64 Mutable stored state on both containers Must round-trip without shape drift Condensation, gas-phase workflows, and future GPU-resident gas updates particula/gas/gas_data.py:55-57,76-101; particula/gpu/warp_types.py:104-105,132; particula/gpu/conversion.py:217-219,355; particula/gas/tests/gas_data_test.py:99-117; particula/gpu/tests/conversion_test.py:593-595
GasData.partitioning Owned by GasData as authoritative shared-across-boxes partitioning eligibility state (n_species,) (n_boxes, n_species) via WarpGasData.partitioning bool on CPU / active-device binary wp.int32 on GPU Mutable stored state on both containers CPU transfer expands bool per box; kernels index [box, species]; CPU restore converts back only when every box agrees Condensation partitioning decisions and GPU kernels that require a numeric mask particula/gas/gas_data.py; particula/gpu/warp_types.py; particula/gpu/conversion.py; particula/gas/tests/gas_data_test.py; particula/gpu/tests/conversion_test.py
WarpParticleData numeric mirrors Owned on GPU only as mirrors of authoritative ParticleData fields, not as a separate source of truth Mirrors CPU particle shapes Stored on GPU as declared in WarpParticleData wp.float64 Mutable GPU working state Must restore to the corresponding ParticleData fields without adding or dropping particle schema GPU-resident particle workflows and parity tests particula/gpu/warp_types.py:73-77; particula/gpu/conversion.py:111-137,281-287; particula/gpu/tests/warp_types_test.py:38-58,100-118,302-320; particula/gpu/tests/conversion_test.py:512-548
WarpGasData.molar_mass / concentration / partitioning Owned on GPU only as numeric mirrors/helpers of authoritative CPU GasData state (n_species,), (n_boxes, n_species), (n_boxes, n_species) Same declared GPU shapes wp.float64 / wp.float64 / active-device binary wp.int32 Mutable GPU working state CPU partitioning expands per box; kernels index [box, species]; restore recovers bool only when every box agrees GPU condensation and other gas-kernel workflows particula/gpu/warp_types.py; particula/gpu/conversion.py; particula/gpu/tests/warp_types_test.py; particula/gpu/tests/conversion_test.py
WarpGasData.vapor_pressure Owned by no CPU container; treated as GPU-helper/process state rather than authoritative GasData or shipped CPU EnvironmentData state Not owned on CPU containers (n_boxes, n_species) via WarpGasData.vapor_pressure wp.float64 Mutable GPU helper state Must be recomputed from the current authoritative thermodynamic inputs, explicitly provided, or carried as sidecar state; update order must prevent mixed stale/new state, and CPU restore from WarpGasData remains intentionally lossy because from_warp_gas_data() drops it GPU condensation kernels and future on-device thermodynamic updates particula/gpu/warp_types.py:98-108,133; particula/gpu/conversion.py:162-206,220-241,301-304; particula/gpu/tests/warp_types_test.py:177-183,225-231; particula/gpu/tests/conversion_test.py:200-229,484-496,588-595
EnvironmentData.temperature Owned by shipped CPU EnvironmentData, not by ParticleData or GasData (n_boxes,) (n_boxes,) via shipped WarpEnvironmentData float64 on CPU / wp.float64 on GPU Mutable per-box thermodynamic state Round-trips through to_warp_environment_data() / from_warp_environment_data(); broader GPU-resident runtime ownership remains future work Parcel/expansion workflows, latent-heat condensation, and other per-box thermodynamic updates particula/gas/environment_data.py; particula/gpu/warp_types.py; particula/gpu/conversion.py; particula/gpu/tests/conversion_test.py
EnvironmentData.pressure Owned by shipped CPU EnvironmentData, not by ParticleData or GasData (n_boxes,) (n_boxes,) via shipped WarpEnvironmentData float64 on CPU / wp.float64 on GPU Mutable per-box thermodynamic state Round-trips through to_warp_environment_data() / from_warp_environment_data(); broader GPU-resident runtime ownership remains future work Parcel/expansion workflows, kernel inputs, and per-box forcing profiles particula/gas/environment_data.py; particula/gpu/warp_types.py; particula/gpu/conversion.py; particula/gpu/tests/conversion_test.py
EnvironmentData.saturation_ratio Shipped CPU EnvironmentData helper state; not an independent source of truth separate from current environment and gas state (n_boxes, n_species) (n_boxes, n_species) via shipped WarpEnvironmentData or equivalent GPU environment state float64 on CPU / wp.float64 on GPU Mutable derived/cache state that must be refreshed after invalidating updates Round-trips through to_warp_environment_data() / from_warp_environment_data() and still must be invalidated/recomputed after upstream state changes; high-level process integration remains future work Latent-heat condensation, parcel expansion, and humidity-coupled follow-on work particula/gas/environment_data.py; particula/gpu/warp_types.py; particula/gpu/conversion.py; particula/gpu/tests/conversion_test.py
Simulation volume ownership Not owned by EnvironmentData; must remain owned by ParticleData.volume (n_boxes,) on ParticleData (n_boxes,) on WarpParticleData float64 / wp.float64 Mutable per-box simulation state under particle container ownership Must continue to round-trip only with particle container conversion helpers Per-box particle state, dilution-style workflows, and timestep orchestration that needs simulation volume particula/particles/particle_data.py:69-70,111-117; particula/gpu/conversion.py:120,286; particula/particles/tests/particle_data_test.py:194-206; .opencode/plans/sections/features/E2-F1/architecture_design.md:31-46

Canonical shape conventions for container workflows

Use this subsection as the single shape contract for container-first workflow documentation. Per-box arrays always keep a leading n_boxes dimension, even when n_boxes == 1. Shared arrays keep their species-only or metadata-only shape and do not gain a box dimension just because the workflow is batched.

Workflow rules:

  • Single-box workflows still store every per-box array with a leading 1.
  • Multi-box workflows store every per-box array with a leading n_boxes.
  • Particle-resolved workflows use ParticleData.masses (n_boxes, n_particles, n_species) and ParticleData.concentration (n_boxes, n_particles), where concentration carries count semantics.
  • Binned workflows keep the same container ranks, but particle concentration is interpreted as number per m^3 rather than count.
  • Shared arrays such as ParticleData.density, GasData.molar_mass, and GasData.partitioning remain species-only arrays with no box axis.
  • Storage shape and interpretation semantics are separate concerns: a particle-resolved workflow and a binned workflow may use the same stored ranks while assigning different physical meaning to the values.

Concrete example shapes backed by current constructors and tests:

  • ParticleData.masses -> (1, n_particles, n_species)
  • ParticleData.concentration -> (1, n_particles)
  • GasData.concentration -> (1, n_species)
  • ParticleData.density -> (n_species,)
  • WarpGasData.partitioning -> (n_boxes, n_species)
  • WarpGasData.vapor_pressure -> (n_boxes, n_species)

Verification evidence for the shape claims in this subsection:

  • particula/particles/tests/particle_data_test.py for single-box, multi-box, shared-density, and derived-shape behavior.
  • particula/gas/tests/gas_data_test.py and particula/gas/tests/gas_data_builder_test.py for gas constructor and builder-added leading box dimensions.
  • particula/gpu/tests/warp_types_test.py and particula/gpu/tests/conversion_test.py for Warp container parity and CPU↔GPU shape preservation.
  • particula/dynamics/condensation/tests/condensation_strategies_test.py and particula/dynamics/coagulation/coagulation_strategy/tests/coagulation_strategy_abc_test.py, plus the current helper entry points they exercise, for the current single-box CPU execution boundary documented below.

ParticleData

Per-box fields:

Field Shape Notes
masses (n_boxes, n_particles, n_species) Same stored rank for single-box, multi-box, particle-resolved, and binned workflows.
concentration (n_boxes, n_particles) Count semantics for particle-resolved workflows; number per m^3 semantics for binned workflows.
charge (n_boxes, n_particles) Per-box particle charge state.
volume (n_boxes,) Per-box simulation volume remains owned by ParticleData.

Shared-across-box fields:

Field Shape Notes
density (n_species,) Shared material density; never add a box dimension.

GasData

Per-box fields:

Field Shape Notes
concentration (n_boxes, n_species) Single-box gas workflows still use (1, n_species).

Shared-across-box and metadata fields:

Field Shape Notes
name len == n_species CPU-only ordered species metadata.
molar_mass (n_species,) Shared across boxes.
partitioning (n_species,) Shared across boxes; stored as bool on CPU.

Shipped CPU EnvironmentData fields:

Per-box fields:

Field Shape Notes
temperature (n_boxes,) Shipped per-box thermodynamic state owned by EnvironmentData on CPU.
pressure (n_boxes,) Shipped per-box thermodynamic state owned by EnvironmentData on CPU.
saturation_ratio (n_boxes, n_species) Shipped per-box, per-species thermodynamic helper state; refresh it after invalidating updates.

WarpParticleData

Per-box fields:

Field Shape Notes
masses (n_boxes, n_particles, n_species) GPU mirror of ParticleData.masses.
concentration (n_boxes, n_particles) GPU mirror of particle concentration/count state.
charge (n_boxes, n_particles) GPU mirror of particle charge state.
volume (n_boxes,) GPU mirror of per-box simulation volume.

Shared-across-box fields:

Field Shape Notes
density (n_species,) GPU mirror of shared particle density.

WarpGasData

Per-box fields:

Field Shape Notes
concentration (n_boxes, n_species) GPU mirror of gas concentration.
partitioning (n_boxes, n_species) Active-device binary wp.int32 mask, indexed by box and species.
vapor_pressure (n_boxes, n_species) GPU-only helper state; not restored to CPU GasData.

Shared-across-box fields:

Field Shape Notes
molar_mass (n_species,) GPU mirror of shared gas molar mass.

Current CPU execution limits for multi-box-ready containers

Caution

Container storage is already multi-box-capable, but shipped CPU execution remains single-box for the currently audited condensation and coagulation workflows. CPU condensation explicitly enforces n_boxes == 1, and CPU coagulation support for ParticleData is also documented and validated as n_boxes == 1 only. For the canonical user-facing support contract, see Data Containers and GPU Foundations. Use ParticleData and GasData Migration as the implementation walkthrough companion.

Rationale for issue-critical ownership decisions

  • ParticleData.density remains shared-across-boxes state with CPU/GPU shape (n_species,); the constructor enforces 1D species density semantics rather than per-box thermodynamic ownership (particula/particles/particle_data.py:67-68,119-137; tests: particula/particles/tests/particle_data_test.py:208-234, particula/gpu/tests/warp_types_test.py:38-58).
  • ParticleData.volume is the authoritative per-box simulation-volume carrier with shape (n_boxes,), and shipped CPU EnvironmentData plus any future GPU mirrors must not own or mutate simulation volume (particula/particles/particle_data.py:69-70,111-117; tests: particula/particles/tests/particle_data_test.py:194-206; .opencode/plans/sections/features/E2-F1/architecture_design.md:31-46).
  • vapor_pressure is process/GPU-helper state rather than owned CPU GasData or shipped CPU EnvironmentData state; CPU-facing workflows must recompute it or pass it as ordered sidecar state after any temperature or concentration update, and CPU restore from WarpGasData is intentionally lossy because to_warp_gas_data() injects zeros when absent and from_warp_gas_data() drops the field (particula/gpu/conversion.py:162-206,301-304; tests: particula/gpu/tests/conversion_test.py:200-229,484-496,588-595).
  • WarpGasData is numeric-only; restoring CPU gas names requires caller-supplied ordered names or equivalent external metadata because the GPU container excludes string fields and from_warp_gas_data() otherwise generates placeholder names such as species_0. Current restore logic checks only metadata length against n_species, so preserving the intended species ordering is a caller responsibility rather than a restore-time guarantee (particula/gpu/warp_types.py:82-99; particula/gpu/conversion.py:305-345; tests: particula/gpu/tests/conversion_test.py:600-640).
  • Shipped CPU environment ownership is temperature: (n_boxes,), pressure: (n_boxes,), and saturation_ratio: (n_boxes, n_species) under EnvironmentData, without moving simulation volume out of ParticleData.volume. saturation_ratio should be treated as a derived thermodynamic helper tied to the current environment and gas state, not as an independent source of truth. Any step that changes the upstream environment or gas state must invalidate and recompute it before downstream kernels consume it, so GPU/CPU paths never mix stale helpers with freshly updated particle or gas fields (EnvironmentData Container; .opencode/plans/sections/features/E2-F1/architecture_design.md:31-46).

Final downstream handoff map for sibling features

This phase publishes the finalized P2/P3 ownership and shape contract for downstream implementers; it does not add new schema semantics.

  • E2-F2: inherit shipped CPU EnvironmentData.temperature -> (n_boxes,), pressure -> (n_boxes,), and saturation_ratio -> (n_boxes, n_species); keep ParticleData.volume -> (n_boxes,) as the authoritative simulation volume owner rather than moving volume into EnvironmentData.
  • E2-F3: inherit the now-shipped exact CPU↔GPU schema mirroring between EnvironmentData and WarpEnvironmentData, with temperature -> (n_boxes,), pressure -> (n_boxes,), and saturation_ratio -> (n_boxes, n_species); do not add an extra volume field or an implicit transfer path.
  • E2-F4: inherit that GasData.name remains CPU-only ordered metadata and GasData.partitioning stays bool on CPU while WarpGasData.partitioning is an active-device binary wp.int32 (n_boxes, n_species) mask indexed by box and species. WarpGasData.vapor_pressure -> (n_boxes, n_species) remains explicit GPU-helper state dropped on CPU restore; callers must preserve ordered species names outside WarpGasData and pass them back on restore because GPU→CPU restore is intentionally lossy for gas metadata and helper state.
  • E2-F5: inherit the single-box leading-dimension contract for per-box arrays such as ParticleData.masses -> (1, n_particles, n_species), ParticleData.concentration -> (1, n_particles), and GasData.concentration -> (1, n_species) without changing the canonical storage ranks for multi-box-ready containers.
  • E2-F6: inherit the current float64 / wp.float64 schema baseline for stored particle and gas arrays. Treat the Mass Precision Recommendation Report as the canonical downstream reference before any production dtype-or-schema migration proceeds.
  • E2-F7: inherit the existing environment and gas ownership boundaries, including ParticleData.volume ownership and WarpGasData.vapor_pressure as lossy helper state, then build later condensation integration work on the P4 recommendation in condensation-stiffness-study.md: fixed_count_substeps_4 as the preferred fixed-shape foundation, bounded by the E2-F2 environment-shape contract, the E2-F6 float64 evidence envelope, and the shipped bounded direct-kernel gas-coupling hook. That hook applies validated, P2-finalized particle-transfer deltas to active-device gas state and has direct-kernel conservation evidence; broader CPU-strategy parity and runnable support remain future work outside the shipped Epic D scope.
  • E2-F8: inherit that container schemas are already multi-box capable through leading n_boxes dimensions, while current CPU condensation runtime support remains single-box and current CPU coagulation paths are still documented as single-box or explicitly transitional execution boundaries; do not infer broader multi-box CPU execution support from storage shape alone.
  • E2-F9: start user-facing docs and examples from the shipped Data Containers and GPU Foundations guide and the Data Containers example, then link back to this roadmap page only for implementation-planning detail such as field ownership, shape tables, CPU↔GPU transfer caveats, validation evidence, and downstream handoff anchors.

  • Keep the shipped particle mass storage representation unless a future production migration proposal satisfies the evidence requirements in the Mass Precision Recommendation Report.

EnvironmentData Container

Parcel/expansion workflows and latent-heat condensation require per-box thermodynamic state. That baseline now exists as particula.gas.environment_data.EnvironmentData, which validates per-box temperature, pressure, and per-box/per-species saturation_ratio. Current shipped scope is still intentionally narrow: it is a constructor- validated CPU container, requires at least one box, is exported from particula.gas, supports copy() for CPU-side state management, and now has public CPU↔GPU round-trip helpers only through particula.gpu.WarpEnvironmentData, to_warp_environment_data(), and from_warp_environment_data(). What remains future work is direct process integration and a broader GPU-resident runtime. Those named helpers are also the only shipped environment transfer boundary; kernels and runnables do not perform hidden CPU↔GPU environment movement or implicit synchronization.

  • Preserve the current EnvironmentData shape contract: temperature and pressure are (n_boxes,), and saturation_ratio is (n_boxes, n_species).
  • Build on the now-shipped WarpEnvironmentData mirror and CPU↔GPU conversion helpers when broader GPU-side thermodynamic state work begins.
  • Existing process APIs may still accept scalar temperature and pressure until later migration work lands; only migrated process code should read EnvironmentData directly.
  • Latent-heat condensation must read and update per-box temperature; rising parcels, expansion, and combustion boxes prescribe temperature and pressure per box, while any simulation-volume evolution continues through ParticleData.volume rather than EnvironmentData ownership.
  • Define ownership and update ordering: which processes read environment state, which mutate it, where prescribed (user-supplied) profiles are applied within a timestep, and when derived thermodynamic helpers such as saturation_ratio and GPU-side vapor_pressure must be invalidated and recomputed. Treat environment state as read-only unless the physical model owns the update. saturation_ratio should remain a derived/cache field tied to current thermodynamic inputs rather than a separately authoritative state.
  • Keep round-trip conversion coverage aligned with the existing ParticleData/GasData conversion patterns, including one-box and multi-box cases plus explicit synchronization behavior, with device-parametrized parity checks that always run on Warp cpu and add Warp cuda only when available. That test coverage confirms the helper surface only; it does not mean automatic environment transfers inside broader runtimes have shipped.
  • Decide how existing kernel APIs that accept scalar temperature and pressure migrate to per-box arrays without breaking the current low-level API.

Numerical Precision and Mass Resolution

The simulation must span new particle formation clusters through cloud droplets. That is roughly fifteen orders of magnitude in particle mass, which sits near the limit of double precision (~15-16 significant digits). This dynamic range, not raw speed, is the main precision driver.

  • Keep fp64 as the reference and production baseline. The E2 precision study found the current absolute per-species np.float64 / wp.float64 schema is the accepted baseline for shipped particle mass storage. Lower-precision or mixed-precision paths remain future proposals until they meet the report's conservation and small-particle fidelity evidence requirements.
  • Target mass resolution is on the order of 0.1 ng per tracked quantity so that both small freshly formed particles and large droplets remain representable in the same simulation.
  • Representation changes are deferred, not part of the shipped baseline. Per-species absolute mass remains the production representation. Log-mass, binned reference masses, and other alternatives require a future scoped migration proposal with reproducible evidence across the NPF-to-droplet cases.
  • fp64 doubles memory relative to fp32, which directly taxes the large multi-box goal. Precision and memory budget must be evaluated together (see Performance and Memory).
  • fp64 throughput is heavily reduced on consumer CUDA hardware. Record which target devices matter, and whether a validated fp32 or mixed-precision path is needed for those devices.
  • Use the shipped Mass Precision Recommendation Report as the acceptance gate before changing precision, dtype, or mass schema.

Time-Scale Stiffness

The same NPF-to-droplet range that stresses mass precision also stresses time integration: nanometer particles equilibrate with vapor in microseconds while cloud droplets evolve over seconds. The shipped direct GPU step uses exactly four equal substeps and P2-finalized particle/gas coupling. P2/P3 candidate comparisons are retained only as historical evidence and do not expand the shipped runtime boundary.

  • Detailed evidence lives in condensation-stiffness-study.md, including the recorded explicit grid, candidate comparison, and current bounded direct-kernel contract.
  • Every successful condensation_step_gpu(...) call runs four time_step / 4.0 substeps. Each optionally refreshes composition-weighted surface tension, refreshes vapor pressure and environment properties, produces a raw proposal, P2-finalizes and applies it, accumulates finalized transfer, and couples gas concentration. Later proposals read coupled gas; refresh does not read gas.
  • The case-specific stiffness evidence and limitations are in the condensation stiffness study; its recorded thresholds are not general accuracy or stable-timestep limits.
  • Adaptive stepping, graph capture/replay, and autodiff readiness are not shipped claims.
  • Do not extend this bounded direct-kernel coupling to temperature feedback, high-level strategies or Runnables, general CPU parity, graph capture, or autodiff support.
  • Downstream implementers must stay inside the E2-F2 environment-shape contract and the E2-F6 float64 evidence envelope when applying this recommendation.
  • Water condensation near cloud activation remains the hardest stress case for future follow-up work because of high vapor concentration, tight supersaturation coupling, and latent-heat temperature feedback.

Epic B: Non-Isothermal Condensation Public API (CPU)

Status: shipped as ADW plan E1. This epic completed the CPU-side public API, validation, documentation, and acceleration readiness for non-isothermal (latent-heat) condensation, giving the GPU parity work in Epic D a stable, builder-accessible CPU reference to match.

Shipped scope (tracked in plan E1):

  • Public builder and factory support for latent-heat condensation (E1-F1), exported through particula.dynamics and covered by export regression tests.
  • Regression coverage for the latent-heat CPU path at the unit level, including mass-transfer conservation and per-step last_latent_heat_energy bookkeeping checks.
  • User-facing feature documentation with worked code snippets in the condensation strategy system guide and the latent-heat section of the condensation equations theory page.
  • CPU API decisions recorded as the reference contract for GPU latent-heat parity (fixed-shape state, explicit environment inputs, no hidden CPU↔GPU movement).

Two follow-ups were deferred out of E1 and subsequently shipped in Epic C/E3: the runnable CPU latent-heat example (E3-F6) and the integration-level conservation case in particula/integration_tests/condensation_latent_heat_conservation_test.py (E3-F7). Together they provide the executable CPU reference handed to Epic D.

Exit bar (met): Latent-heat condensation is a documented, builder/factory accessible CPU workflow with validation coverage, and its API decisions are recorded as the reference contract for GPU latent-heat parity in Epic D.

Epic C: GPU Kernel Correctness and Low-Level API Hardening

Status: shipped as ADW plan E3 on 2026-07-12. All seven child feature tracks and their phases are marked shipped. The implementation made the existing low-level GPU kernels reproducible and usable from documentation alone before new physics is added. This epic absorbed the former "documented low-level GPU API" milestone, the cross-cutting validation-infrastructure work, and the two documentation/validation follow-ups deferred from Epic B (plan E1).

For the current shipped baseline on explicit CPU↔GPU transfer helpers, EnvironmentData ownership, current CPU/GPU support boundaries, and the canonical runnable entrypoint, start from the Data Containers and GPU Foundations guide and the Data Containers example; this epic and every later epic extend those contracts rather than redefining them.

Shipped scope:

  1. Persist coagulation RNG state: seed once at loop setup and keep per-box RNG state between coagulation_step_gpu calls instead of re-launching the initialization kernel on every call.
  2. Improve rejection sampling for wide size ranges: evaluate size-binned majorant kernels or stratified pair sampling so mixed NPF/droplet boxes do not collapse acceptance rates.
  3. Accept the current one-thread-per-box coagulation path for Epic C as an accepted-with-caveat, measured many-box and direct-kernel baseline, especially for box-level parallel throughput, while keeping caveats for large single-box workloads; keep the evidence anchored in the measured decision record for the current one-thread-per-box path rather than broadening it into a universal production guarantee or a parallel-within-box implementation claim.
  4. Direct GPU kernel step entry points are supported from particula.gpu.kernels:
from particula.gpu.kernels import (
    coagulation_step_gpu,
    condensation_step_gpu,
)

Keep top-level particula.gpu focused on Warp availability, transfer helpers, and context helpers rather than direct kernel-step imports. Raw helper kernels remain intentionally outside the documented package-level public surface; import them from their concrete modules when needed. 5. Device-aware pytest policy is part of the shipped guidance: Warp CPU is the default parity backend when Warp is installed, CUDA runs are optional local/manual additions when a device is present locally, the matching pytest markers remain warp, cuda, gpu_parity, and stochastic, and guarded suites should surface missing Warp or missing CUDA as expected skips rather than failures. CPU-only or standard CI environments must skip CUDA coverage cleanly when it is not available. 6. Tolerance policy stays split into three classes: deterministic parity uses explicit numpy.testing.assert_allclose(..., rtol=..., atol=...) bounds, conservation checks keep separately tight drift tolerances, and stochastic validation uses bounded aggregate expectations such as tolerance bands or 3-sigma behavior rather than exact per-seed equality. 7. The shipped canonical direct-kernel quick-start is docs/Examples/gpu_direct_kernels_quick_start.py. It demonstrates the supported ParticleData/GasData path, explicit to_warp_* and from_warp_* transfer helpers, the WARP_AVAILABLE guard, and deferred particula.gpu.kernels imports so the CPU↔Warp boundary stays explicit. Treat those direct kernels as low-level APIs, not as top-level particula.gpu convenience imports. Keep the supported public import path fixed at:

from particula.gpu.kernels import (
    coagulation_step_gpu,
    condensation_step_gpu,
)

Top-level particula.gpu remains the home for availability checks and transfer helpers, not direct kernel-step re-exports. Troubleshooting notes should stay aligned with the shipped example and regression tests:

- missing Warp means the quick-start stays in its CPU-only documentation
  path behind `WARP_AVAILABLE`, with deferred `particula.gpu.kernels`
  imports and no pretend kernel execution; guarded test suites may also skip
  through `pytest.importorskip("warp")`, which is expected and should not
  be treated as a validation failure
- CUDA is optional/local/manual, Warp `device="cpu"` is the default
  runnable validation path, and CPU-only or CI environments should skip
  CUDA-targeted coverage cleanly with the shared `Warp/CUDA not available`
  contract rather than fail
- particle, gas, environment, and sidecar buffers such as `rng_states`
  must stay on compatible devices
- `environment=` must not be mixed with scalar or Warp-array direct
  `temperature` / `pressure` inputs
- CPU↔GPU movement remains explicit through `to_warp_*` and `from_warp_*`
  helpers, including lossy gas restore for names and helper-only state
- no hidden container transfers or automatic top-level fallback imports
  should be implied by roadmap prose; read-only device validation may
  synchronize and read back status to surface invalid values
- focused local validation can use
  `pytest particula/gpu/tests/cuda_availability_test.py -q` for the shared
  skip contract and `pytest particula/gpu/kernels/tests/environment_test.py
  -q -m "warp and cuda"` for optional CUDA-if-available coverage
  1. The paired CondensationLatentHeat example now provides a runnable CPU-only bookkeeping workflow (E3-F6), completing the documentation follow-up deferred from E1.
  2. particula/integration_tests/condensation_latent_heat_conservation_test.py now establishes the CPU mass-conservation and latent-heat bookkeeping baseline that Epic D parity tests can reuse (E3-F7).

Exit bar (met): A new user can run both kernels from the docs on a CUDA device and on Warp CPU without reading source, repeated coagulation steps draw uncorrelated samples from a caller-initialized persistent RNG buffer, the device-aware test policy plus parity tolerances are recorded, and the deferred E1 latent-heat example and integration-conservation case have landed.

Known Kernel Issues

Concrete defects and design limits reviewed during Epic C. The RNG defect was fixed; the mixed-scale global-majorant and one-thread-per-box limits were explicitly accepted with evidence and remain bounded follow-up concerns.

  • RNG state ownership. coagulation_step_gpu seeds once and reuses a caller-owned per-box RNG buffer across timesteps. Repeated calls should keep the seed fixed unless an explicit reset is requested, and graph-captured loops should initialize or reset the buffer before capture or before the repeated-step loop (see Random Number Strategy).
  • Rejection-sampling acceptance collapse. The shipped E3-F2-P2 outcome was a bounded selector hardening improvement inside the existing Brownian sampler, not a new public API, not a container or transfer-path change, and not a full mixed-scale acceptance-rate fix. The remaining limitation is still the same kernel shape: one global k_max computed from the min/max radius pair plus one-thread-per-box execution inside the existing sampler. Wide mixed NPF/droplet size distributions can therefore still suffer low acceptance and high trial counts even after the selector hardening landed.

The shipped mixed-scale evidence comes from the private test-only fixture and diagnostics in particula/gpu/kernels/tests/coagulation_test.py: _make_mixed_npf_droplet_particle_data(), _brownian_coagulation_attempt_diagnostic_kernel(...), and _collect_test_local_attempt_diagnostics(...). These helpers stay test-local and do not imply implicit CPU/GPU synchronization or runtime-integrated diagnostics in production paths. The diagnostic interpretation follows the test terminology directly: scheduled trials are the bounded integer-like trials requested by the mirrored sampler, executed trials are the trials that actually run before early exit, accepted collisions are the realized coagulation events, and the Brownian reference uses only active upper-triangle particle pairs to compute active_pair_count, expected mean, and the Poisson-style sigma sqrt(expected_mean).

The measured evidence recorded in E3-F2-P3 is 139 accepted collisions across the fixed seeded replay range range(101, 201), compared with the Brownian expected mean 143.846, Poisson-style sigma 11.994, and documented 3-sigma tolerance 35.981. That tolerance is based on repeated fresh seeded runs against the same mixed-scale fixture, with the expected mean computed from active upper-triangle Brownian pairs only and sigma taken as sqrt(expected_mean).

The documented mixed-scale evidence currently cites test_mixed_scale_diagnostic_reports_attempted_and_accepted_counts(device), test_mixed_scale_brownian_collision_totals_match_expected_mean_within_sigma_tolerance(device), test_mixed_scale_expected_collision_statistics_use_active_pairs_only(), test_mixed_scale_acceptance_fraction_is_finite_and_nonnegative(device), test_mixed_scale_selector_only_emits_sorted_active_in_bounds_pairs(device), test_mixed_scale_sparse_or_degenerate_active_sets_return_zero_collisions(device, active_indices), test_mixed_scale_diagnostic_tracks_executed_trials_under_early_exit(device), test_mixed_scale_two_active_particles_accept_the_only_valid_pair(device), test_mixed_scale_diagnostic_clamps_scheduled_trials_to_int32_limit(device), test_mixed_scale_coagulation_conserves_total_mass(device), test_mixed_scale_repeated_seeded_runs_conserve_total_mass_even_with_zero_acceptance_trials(device), test_mixed_scale_caller_owned_rng_states_advance_without_hidden_reseed(device), and test_mixed_scale_initialize_rng_true_replays_seeded_state_and_outcome(device). The Brownian repeated-run evidence is the seeded range(101, 201) check in test_mixed_scale_brownian_collision_totals_match_expected_mean_within_sigma_tolerance(device): the documented bounded-selector path is accepted only when the observed accepted collision total stays within a 3 * sigma tolerance of the active-pairs-only Brownian expected mean computed by _get_expected_collision_statistics(...) for the documented time_step=0.5, max_collisions=8, and volume=1.0e-10 replay setup. The same documented test-local evidence also verifies active-pairs-only reference statistics, sorted in-bounds accepted pairs, finite zero-acceptance behavior, early-exit accounting for executed versus scheduled trials, bounded trial clamping, total-mass conservation, and caller-owned rng_states reuse/reset semantics. This roadmap follows the same validation policy as the testing guide: deterministic parity uses explicit numpy.testing.assert_allclose(..., rtol=..., atol=...) bounds, conservation assertions stay separately tight, Warp CPU always participates in parity validation when Warp is installed, optional CUDA runs apply the same policy when available locally, and stochastic acceptance coverage is judged by bounded aggregate expectations such as tolerance bands or 3-sigma behavior rather than exact per-seed equality. Treat this as bounded evidence that the selector hardening preserved expected Brownian behavior within stochastic tolerance inside the existing sampler, not as deterministic replay evidence and not as proof that the global-majorant acceptance collapse is solved for every mixed-scale distribution.

Reproduce the seeded checks with:

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

Documentation validation for this note is limited to markdown/path readback plus those focused pytest checks; no separate repo-specific docs formatter or link checker is claimed here. - One-thread-per-box coagulation. The kernel launches one GPU thread per box with sequential pair selection inside the thread. This matches the multi-box scaling priority but serializes large single-box workloads. Epic C accepted this as the current measured baseline with the single-box caution band in the decision record. A parallel-within-box variant requires a separate evidence-backed proposal.

Epic D: GPU Condensation Physics Parity

Status: shipped. E4-F1--E4-F7 recorded evidence is complete for the bounded low-level direct-condensation publication; it does not enable new production functionality beyond the verified direct kernel. Epic D inherits the supported particula.gpu.kernels entry point, explicit transfer boundary, and device-aware validation policy from Epic C.

Import the direct step with from particula.gpu.kernels import condensation_step_gpu. The canonical data containers and GPU foundations guide defines the supported schema, units, ordering, device ownership, and explicit-transfer boundary. The step uses caller-owned stable-shape fp64 (wp.float64) sidecars: transfer fields use (n_boxes, n_particles, n_species) and property fields use (n_boxes,). Supplied buffers preserve identity; omitted fields use fallback allocations.

Each successful call executes exactly four time_step / 4.0 substeps. Every substep refreshes derived vapor pressure and environment properties, produces a raw proposal, and applies a P2-finalized, inventory-limited transfer. Particle masses and the matching particle-concentration-weighted gas-concentration delta mutate in place. The total-transfer buffer accumulates P2-finalized transfers, while work storage retains the final raw proposal. Explicit simulation-state transfers remain caller responsibilities; entry-point validation may still perform synchronous device-to-host readbacks.

The current executable CPU integration baseline remains particula/integration_tests/condensation_latent_heat_conservation_test.py. The direct step applies an optional latent-heat rate correction in each of its four fixed substeps, with CPU-oracle/Warp parity coverage. E4-F4's #1272 signed diagnostic is shipped: optional keyword-only caller-owned active-device wp.float64 latent_heat, (n_species,), and energy_transfer, (n_boxes, n_species), record finalized transfer times latent heat. thermal_work remains validated but deferred/unused.

Warp cpu baseline evidence is required whenever Warp is installed. Optional/local additive CUDA evidence skips cleanly when unavailable; it is additional local evidence rather than a default CI requirement. The focused direct-kernel evidence is summarized in the fixed-four decision record and the canonical foundations guide.

Temperature feedback, high-level Aerosol/Runnable integration, adaptive stepping, unsupported physics, graph capture/replay, broad autodiff, and general CPU-strategy parity remain future work outside the shipped scope. This leaves temperature feedback, gas mutation beyond the P2-finalized direct coupling, and all higher-level process integration outside this milestone.

The independent condensation walkthrough (docs/Examples/gpu_condensation_parity_walkthrough.py) provides fixed-four-substep low-level direct-kernel physics, conservation, and energy evidence. Run:

python docs/Examples/gpu_condensation_parity_walkthrough.py
pytest particula/gpu/tests/gpu_condensation_parity_walkthrough_test.py -q -Werror
pytest particula/tests/condensation_parity_walkthrough_docs_test.py -q -Werror

Warp CPU is the installed-Warp baseline; CUDA is optional additive evidence. The downstream condensation ownership record (condensation-parity-walkthrough.md) documents deferred work only, including temperature feedback, high-level integration, adaptive stepping, graph replay, broad autodiff, and performance; it does not change production capability. The caller-owned, write-only energy_transfer diagnostic is not a return value or temperature feedback (kg * J/kg = J).

Epic E: GPU Coagulation Physics Coverage

Status: shipped. E5-F9 P3 completed no later than its P4 closeout, and Epic E completed its bounded direct-kernel coagulation scope; Epic F is active.

E5 roadmap inventory

ID Title Status text
E5 GPU Coagulation Physics Coverage Shipped
E5-F1 Mechanism Configuration and Sampling Contract Shipped
E5-F2 Charged Pair Physics and Charge-Conserving Merges Shipped
E5-F3 Charged and Brownian-Plus-Charged GPU Execution Shipped
E5-F4 SP2016 Sedimentation GPU Execution Shipped
E5-F5 ST1956 Turbulent-Shear GPU Execution Shipped
E5-F6 Single-Pass Additive Multi-Mechanism Coagulation Shipped
E5-F7 Cross-Mechanism GPU Validation Matrix Shipped
E5-F8 Independent CPU-Warp Condensation Walkthrough Shipped
E5-F9 GPU Coagulation Support Documentation and Epic Closeout Shipped

E5-F7 owns the GPU coagulation validation record, E5-F8 owns the condensation parity walkthrough ownership record, and the GPU condensation parity walkthrough is runnable source.

Extend GPU coagulation beyond the Brownian kernel to the collision mechanisms already available on CPU. DNS turbulence remains deferred (see Non-Goals). Every kernel in this epic ships with CPU/GPU parity or statistically bounded tests under the Epic C device-aware test policy.

Epic E also owns the closeout disposition for Epic D carry-forward work. The independent CPU/Warp condensation parity walkthrough is in scope as an evidence and documentation feature. The closeout record must retain explicit downstream owners for phase-aware surface tension and BAT activity, consumption of the validated thermal_work sidecar and temperature feedback, adaptive stepping, high-level Aerosol/Runnable integration, graph capture/replay, and broad autodiff. Those runtime capabilities remain assigned to later roadmap epics or a separately approved physics expansion; recording them here does not claim that Epic E's coagulation implementation delivers them.

Import the supported direct step and its host-side configuration separately:

from particula.gpu.kernels import coagulation_step_gpu
from particula.gpu.kernels.coagulation import CoagulationMechanismConfig

Pass mechanism_config=CoagulationMechanismConfig(...) as keyword-only host metadata. The frozen dataclass is not device-resident simulation state and is not transferred, synchronized, or stored in a container schema. In contrast, WarpParticleData, supplied collision_pairs, supplied n_collisions, and supplied rng_states are caller-owned, same-device Warp resources. Omitted collision outputs and RNG state use call-local convenience allocation. A supplied RNG sidecar is reused and changes in place; it resets only with initialize_rng=True. Configuration and caller-owned sidecars have no implicit CPU↔GPU transfer. Reuse of a supplied persistent rng_states buffer is unsynchronized, while omitted RNG state and initialize_rng=True use the initialization path, which synchronizes after initializing or resetting state.

Only distribution_type="particle_resolved" is accepted. "discrete" and "continuous_pdf" raise ValueError; they do not fall back or convert.

Identifier Status Owner
brownian Bit 1; executable in approved masks Shipped direct-kernel term
charged_hard_sphere Bit 2; executable in approved masks Shipped direct-kernel term
sedimentation_sp2016 Bit 4; exact unit-efficiency term in approved masks Shipped direct-kernel term
turbulent_shear_st1956 Bit 8; executable in approved masks with turbulent inputs Shipped direct-kernel term

The charged-only tuple is ("charged_hard_sphere",). Exact SP2016 sedimentation is selected with CoagulationMechanismConfig(("sedimentation_sp2016",)). Its shipped pair rate is K = π (r_i + r_j)^2 |v_i - v_j| in m³/s, using Stokes settling velocities with Cunningham slip correction (Seinfeld & Pandis, 2016, Eq. 13A.4). Collision efficiency is fixed at 1 and no public efficiency argument exists. It is a direct-kernel-only mode: it uses fp64 particle mass state, but direct scalar temperature/pressure, supported-float same-device Warp arrays of shape (n_boxes,), or same-device WarpEnvironmentData are alternatives. Helper-owned, call-local fp64 work buffers hold per-particle properties as needed; scalar turbulence inputs use private device-local (n_boxes,) broadcast/property storage, while valid supplied arrays retain identity. ST1956 has no dedicated settling-velocity buffer; caller-owned particle, collision-output, and RNG resources retain their documented dtypes and identity. The executable matrix is singleton masks 1, 2, 4, 8; unordered two-way masks 3, 5, 6, 9, 10, 12; and four-way mask 15. Requested tuples normalize to canonical Brownian, charged, sedimentation, turbulent order. Non-turbulent three-way mask 7 raises ValueError("Additive coagulation execution is deferred.") during capability preflight before particle metadata or enabled-term validation. Turbulent three-way masks 11, 13, and 14 proceed through particle metadata and enabled-term validation, then raise that error before downstream normalization, allocation, RNG setup, kernel launch, or mutation. Malformed, duplicate, unknown, and non-particle_resolved configurations reject during structural preflight before particle state access. Sedimentation-specific preflight rejects invalid particle physics before output allocation, RNG initialization, or particle mutation; this does not make unrelated later validation failures atomic.

The exact turbulent-shear mode is selected with CoagulationMechanismConfig(("turbulent_shear_st1956",)). Its keyword-only turbulent_dissipation input is in m^2/s^3, and keyword-only fluid_density is in kg/m^3. Both are required positive finite inputs for structurally valid requests containing the turbulent mechanism, including a deferred three-way mask during enabled-term validation. Each accepts a Python or NumPy floating scalar with private device-local broadcast/property work storage, or an active-device wp.float64 Warp array shaped (n_boxes,) that retains supplied identity. They are not inferred from a shared container, and Python lists, NumPy arrays, and hidden host-to-device transfers are not accepted as array inputs. Approved mixed turbulent masks execute with these inputs; deferred turbulent three-way masks validate them, then raise the stable deferred-execution error. Non-turbulent masks ignore both turbulence arguments.

WarpParticleData.charge is caller-owned same-device wp.float64 state with shape (n_boxes, n_particles), not a sidecar or transfer result. Charged calls preflight finite charge before caller-output validation/allocation, RNG setup, or selector/apply work. An accepted representable merge adds donor charge to the recipient and clears the donor charge, mass, and concentration. A pair whose summed charge or species mass cannot be represented as finite, nonnegative float64 state is not applied, so both particle inventories remain unchanged.

Approved additive execution sanitizes and sums fp64 component pair rates. The safe component-majorant sum satisfies sum_m K_m(i, j) <= sum_m M_m; component maxima can occur on different pairs, so this bound can be conservative. Production selection obtains its majorant by an exact compact-active scan of the summed pair rate. One active set, candidate stream, acceptance stream, collision-buffer set, per-box RNG stream, and apply pass serve every approved mask rather than sequential mechanism steps. This is bounded implementation scope, not a performance claim. The return tuple is exactly (particles, collision_pairs, n_collisions): supplied collision buffers return by identity, while supplied rng_states mutates in place and is not returned. Warp CPU is the baseline when installed; CUDA is optional additive evidence and skips cleanly when unavailable.

Future mechanisms must provide a stable identifier; required host and device inputs; property preparation; a sanitized additive pair-rate term; a proven-safe additive majorant; a capability-table row; shared-dispatcher integration; and co-located *_test.py coverage. They must not add an independent sampling loop, acceptance pass, collision buffer, or RNG stream. This boundary excludes binned/discrete/continuous-PDF GPU coagulation, high-level Runnable integration, graph-capture claims, mechanism variants, and DNS/general turbulence. Non-unit efficiency, alternate drag physics, Runnable integration, CPU fallback, hidden simulation-state transfers, graph capture, and performance guarantees are unsupported. Read-only P2 validation may synchronize and read back device status, including on CUDA, to report invalid values. The direct path does not establish CPU-strategy parity. E5-F6 ships the additive contract and documentation; no additional planned work remains for the shipped approved additive combinations. E5-F7 is shipped with the cross-mechanism validation record. E5-F9 is shipped: its completed P3/P4 publish the consolidated direct example and closeout gate. Epic F is the active follow-on work.

Planned features:

  1. Sedimentation variants beyond the exact unit-efficiency SP2016 direct-kernel term.
  2. DNS/general turbulent-shear physics beyond the shipped ST1956 term.
  3. Parity and statistical validation: CPU/GPU parity or statistically bounded tests for each new kernel, with recorded tolerances.
  4. Distribution-support decision record: the current GPU path explicitly enforces particle-resolved semantics (per-slot merges, concentration zeroing); document which binned/moving-bin strategies remain CPU-only.
  5. Epic D carry-forward closure: publish an independent CPU/Warp condensation parity walkthrough that separates physics, conservation, and energy tolerances, and maintain the downstream ownership record for every deferred condensation capability listed above.

Exit bar: Each in-scope CPU coagulation mechanism has a GPU kernel with parity or statistically bounded tests, combined-mechanism kernels are validated, unsupported distribution types are documented as CPU-only, and the Epic D carry-forward walkthrough and ownership record are published.

Epic F: GPU Process Completeness

Add the remaining processes a complete aerosol simulation needs to stay GPU-resident between checkpoints — dilution, wall loss, and nucleation — plus the fixed-slot particle management that nucleation and graph capture depend on.

Delivered dilution scope:

  • E6-F1 provides the CPU finite-step oracle and E6-F2 delivers the P1–P4 direct GPU kernel: from particula.gpu.kernels import dilution_step_gpu. It is a low-level, fixed-shape operation, not a GPU process or runnable.
  • Callers own CPU↔Warp transfer, device placement, and synchronization. The kernel accepts finite nonnegative scalar or same-device wp.float64 (n_boxes,) coefficients, returns the same containers, and mutates only particle and gas concentrations in place.
  • Ordered complete preflight is read-only; scalar-zero and zero-time calls are fully preflighted write-free, no-update-kernel no-ops, though validation scans may still allocate or launch. Particle and gas parity evidence uses independent Warp CPU float64 checks at rtol=1e-12, atol=0; CUDA is optional and skips cleanly when unavailable. It is not bitwise parity. E6-F9 is the future integrated direct-call consumer.

Delivered bounded neutral wall-loss P1–P6 scope:

  • E6-F3 provides from particula.gpu.kernels import wall_loss_step_gpu for direct neutral, particle-resolved spherical or rectangular fixed-slot wall loss. NeutralWallLossConfig remains concrete-module-only at particula.gpu.kernels.wall_loss. SI inputs are wall eddy diffusivity in m²/s, chamber dimensions in m, temperature in K, pressure in Pa, and time in s. Spherical configurations require a positive radius and no dimensions; rectangular configurations require no radius and three positive dimensions.
from particula.gpu.kernels import wall_loss_step_gpu
from particula.gpu.kernels.wall_loss import NeutralWallLossConfig

config = NeutralWallLossConfig(
    geometry="spherical",
    wall_eddy_diffusivity=1.0e-4,  # m^2/s
    chamber_radius=0.5,  # m
)
wall_loss_step_gpu(
    particles,
    temperature=298.15,  # K
    pressure=101325.0,  # Pa
    time_step=1.0,  # s
    config=config,
    rng_states=rng_states,
    initialize_rng=True,
)

This non-executed direct-call example uses caller-owned same-device Warp state. Later calls reuse rng_states and omit initialize_rng=True. - The Crump--Seinfeld coefficient basis uses the 1981 spherical and 1982 rectangular chamber relations. Eligible finite-rate slots survive with exp(-k * time_step); selected slots clear all mass lanes, concentration, and charge, while density, volume, dtype, device, capacity, and unselected storage are preserved. Inactive and unusable slots are neither sampled nor reactivated. The source citations are Crump, J. G., & Seinfeld, J. H. (1981), Turbulent deposition and gravitational sedimentation of an aerosol in a vessel of arbitrary shape, Journal of Aerosol Science, 12(5), https://doi.org/10.1016/0021-8502(81)90036-7; and Crump, J. G., Flagan, R. C., & Seinfeld, J. H. (1982), Particle wall loss rates in vessels, Aerosol Science and Technology, 2(3), 303--309, https://doi.org/10.1080/02786828308958636. - Preflight is read-only. Successful asynchronous calls mutate caller-owned, same-device state in place; callers own transfer, synchronization, and checkpointing. Rejected pre-launch calls do not mutate caller state, zero time is write-free after preflight, and post-launch rollback is not promised. Temperature and pressure are direct scalars or same-device (n_boxes,) Warp arrays, or come exclusively from environment= with both direct inputs set to None. - Omitted RNG state is private for each successful nonzero call. Supplied (n_boxes,) same-device wp.uint32 state advances in place; only initialize_rng=True resets it. Consumption is sequential per box over eligible finite positive-rate slots; positive infinite-rate removal is deterministic and consumes no draw, so exact CPU/Warp or per-seed replay is not supported. - P6 evidence is geometry-specific deterministic coefficient tolerances plus 100-seed, 3-sigma aggregate survival checks. Warp CPU is the installed-Warp baseline; CUDA is optional additive evidence and skips cleanly when absent. This is not CPU-strategy parity or a performance claim. - Deferred scope includes E6-F4 charged/image-charge/electric-field physics, CPU fallback or hidden transfers, runnable/scheduler/backend integration, dynamic slots, compaction/activation, graph capture, differentiability, performance guarantees, and exact RNG replay. E6-F9 remains the future integration and closeout work.

Future features:

  1. GPU process orchestration, backend selection/fallback policy, scheduling, and GPU-resident timestep integration for the delivered direct kernels.
  2. GPU charged wall loss, after the bounded neutral path and the core condensation and coagulation GPU paths are stable.
  3. Nucleation/particle-source CPU reference process following the nucleation equations (no nucleation code exists in particula today).
  4. GPU nucleation via slot activation (see Particle Slot Management).
  5. Particle slot management: inactive zero-mass slots, activation, per-box active-count diagnostics, and an exhaustion policy (resampling or volume scaling).
  6. Slot and conservation validation: tests for inactive slots, activation, slot exhaustion handling, and conservation across resampling or volume scaling.
  7. Fixed-shape GPU workflow extensions: resizing policy, graph capture, autodiff, and performance evidence.

Particle Slot Management

  • Use fixed particle slot counts per box for GPU-resident simulations.
  • Represent inactive particle slots as particles with zero mass, zero radius, and zero concentration or count.
  • Avoid dynamic allocation inside timestep kernels. Processes that create new particles, including the planned nucleation process, should activate inactive slots when available.
  • Add a resampling or volume-scaling policy for cases that would exceed the available particle slots in a box.
  • Track per-box active particle counts as diagnostics, but keep the underlying arrays fixed-shape for GPU kernels and graph capture.
  • Define compaction rules only if needed; inactive zero-mass slots are simpler and graph-friendly.

Exit bar: A complete GPU-resident timestep can run condensation, coagulation, wall loss, and dilution together with persistent RNG state, and new-particle creation works through validated, conservation-checked slot activation.

Epic G: Backend Selection and GPU-Resident Simulation

Make GPU execution reachable from user-facing APIs and keep full simulations resident on the device between checkpoints. This epic also owns the cross-cutting CPU-fallback and API-stability decisions formerly tracked as a separate epic.

Planned features:

  1. Backend-selection API design: decide whether selection belongs on strategies, runnables, builders, or a separate execution context.
  2. Backend selection for at least one condensation workflow, with explicit return types and mutation semantics.
  3. Backend selection for at least one Brownian coagulation workflow.
  4. GPU simulation loop abstraction that keeps WarpParticleData, WarpGasData, and WarpEnvironmentData resident on the selected device across all enabled dynamics.
  5. Deterministic process ordering/scheduler for full aerosol simulations (condensation, coagulation, wall loss, dilution, environment updates, and gas updates between steps).
  6. CPU fallback and API-stability policy: mark low-level particula.gpu.* APIs experimental until selection and full-loop validation land; missing GPU processes raise clear errors or use explicit fallback boundaries; no silent CPU/GPU data movement in long simulations.
  7. Multi-box communication: prescribed advection, dilution, expansion, and simple mixing maps, including per-box volume changes for parcel and combustion cases.
  8. Per-box RNG streams so independent boxes remain reproducible, with GPU-resident RNG state exercised in loop tests.
  9. Documentation and regression coverage: a GPU-resident multi-timestep example that transfers back only at checkpoints, plus larger multi-box and particle-resolved regression cases for independent boxes, prescribed advection, dilution, and expansion.

High-Level Integration

  • Use the shipped Data Containers and GPU Foundations guide and Data Containers example as the current baseline for explicit transfer helpers, EnvironmentData ownership, support boundaries, and runnable entrypoint wording before layering new high-level GPU execution APIs on top.

  • Add user-facing APIs that can choose CPU or GPU execution without requiring users to call kernel modules directly.

  • Decide whether backend selection belongs on strategies, runnables, builders, or a separate execution context.
  • Preserve CPU behavior as the reference path and make GPU fallback behavior explicit when Warp or CUDA is unavailable.
  • Add a GPU simulation loop abstraction that keeps WarpParticleData, WarpGasData, and WarpEnvironmentData resident on the selected device across all enabled dynamics.
  • Support process ordering for full aerosol simulations, for example condensation, coagulation, wall loss, dilution, environment updates, and any gas updates that must occur between those steps.

Full GPU-Resident Simulation

  • Define the minimum dynamics set required for a complete GPU-only aerosol simulation loop.
  • Keep particles, gases, environment state, temporary work buffers, and random-number state on the GPU between timesteps.
  • Transfer data back to CPU only for checkpoints, diagnostics, visualization, or final results.
  • Add a process graph or scheduler that can execute supported GPU processes in a deterministic order.
  • Provide explicit errors or CPU fallback boundaries when a requested dynamics process has no GPU implementation.
  • Treat multiple independent boxes as a first-class simulation mode. Each box should keep its own particle, gas, environment, volume, and diagnostic state while sharing process configuration when appropriate.
  • Support simple box communication through prescribed advection or mixing maps. This should cover early 1D parcel workflows such as rising parcels, expanding parcels, and flame/combustion-style expansion where box volumes and gas states evolve in a prescribed way.

Candidate GPU process coverage:

  • Condensation: isothermal and latent-heat variants (staggered stays CPU-only).
  • Coagulation: approved particle-resolved singleton, two-way, and four-way masks. Three-way masks plus DNS/general turbulence remain deferred from the near-term GPU scope.
  • Wall loss: neutral spherical/rectangular first, then charged wall loss.
  • Dilution: particle and gas concentration dilution on GPU-resident arrays.
  • Nucleation: particle-source process activating inactive slots, after the CPU reference exists.
  • Gas updates: partitioning-related gas concentration changes needed by coupled condensation workflows, plus scheduler orchestration that uses the existing on-device vapor-pressure refresh after temperature changes.
  • Environment updates: prescribed or process-driven per-box temperature and pressure updates, plus recomputation of derived thermodynamic fields; simulation-volume evolution remains owned by ParticleData.volume.
  • Diagnostics: optional GPU-side reductions for total mass, number concentration, latent heat energy, and conservation checks.
  • Box transport: prescribed advection, dilution, expansion, and simple mixing between neighboring boxes or user-defined box pairs.

Multi-Box Communication

  • Start with prescribed communication, not full CFD coupling.
  • Represent box-to-box transport with fixed-shape maps or sparse edge lists so graph-captured GPU loops can reuse the same allocation layout.
  • Support independent boxes by default; communication should be opt-in per simulation loop.
  • Include per-box volume changes for rising parcel, expansion, and combustion use cases.
  • Keep particle and gas transport rules explicit. Gas concentrations, particle concentrations, and particle slot contents may need different update kernels.
  • Validate simple 1D advection and expansion cases against CPU references before adding more complex coupling.

Fixed-slot particle management for these loops is defined in Particle Slot Management under Epic F.

Random Number Strategy

  • Define deterministic RNG seeding for stochastic coagulation on GPU. The shipped baseline is seed-once initialization for caller-owned persistent rng_states, while omitted rng_states still use a convenience allocate-and-seed path per call.
  • Support per-box RNG streams so independent boxes remain reproducible when the number of boxes changes or when selected boxes are disabled.
  • Track caller-owned RNG state on the GPU between timesteps and include it in graph-captured execution tests.
  • Document expected reproducibility limits across CPU, Warp CPU, and CUDA.

Exit bar: At least one condensation and one coagulation workflow run on GPU through the selection API, match CPU within recorded tolerance, and are used in a documented example, and a multi-box GPU-resident loop runs all supported processes between checkpoints with per-box RNG streams and no hidden CPU transfers.

Epic H: Graph Capture and Performance

Reduce launch overhead with graph capture and establish performance and memory targets aligned with the multi-box scaling goal.

Planned features:

  1. Warp graph capture for repeated timestep execution with fixed process order and stable array shapes.
  2. Separation of graph-capturable kernels from setup work (allocation, validation, host-side scheduling).
  3. Preallocated buffer reuse for mass transfer, collision pairs, wall-loss rates, dilution factors, diagnostics, and RNG state.
  4. Full-loop validation tests comparing CPU, uncaptured GPU, and graph-captured GPU execution for the same process sequence.
  5. Multi-box scaling benchmarks with box count as the primary axis, plus secondary particles-per-box scaling; benchmarks stay opt-in (--benchmark-style gating) and CUDA-gated, separate from the default parity suite.
  6. Memory-budget model covering state arrays, inactive slots, temporary buffers, diagnostics, communication maps, and autodiff tape storage.
  7. Graph-capture example plus documented limitations and re-capture triggers.
  8. CUDA kernel profiling (occupancy, memory access patterns) and captured-vs-uncaptured launch-overhead benchmarks.

Exit bar: A multi-box GPU-resident simulation runs graph-captured timesteps, matches CPU and uncaptured GPU references, and has published scaling numbers across box counts plus a recorded memory-budget model.

Warp Graph Capture

  • Preserve the shipped explicit CPU↔GPU helper boundary and documented container ownership model from the Data Containers and GPU Foundations guide while designing graph-captured GPU loops; performance work should not reintroduce implicit synchronization or move the canonical example entrypoint away from the Data Containers example.

  • Investigate Warp graph capture for repeated timestep execution with a fixed process order and stable array shapes.

  • Separate graph-capturable kernels from setup work such as allocation, validation, and host-side scheduling.
  • Reuse preallocated buffers for mass transfer, collision pairs, wall-loss rates, dilution factors, diagnostics, and RNG state before capture begins. For coagulation, seed or explicitly reset persistent rng_states before the repeated-step loop or before graph capture, not through hidden reseeding inside the captured path.
  • Add tests that compare captured-graph execution against uncaptured GPU execution and CPU reference results.
  • Document graph-capture limitations, including shape changes, dynamic process selection, stochastic coagulation state ownership, and device availability.
  • Require fixed array shapes during graph capture. Changing n_boxes, n_particles, or n_species should invalidate the captured graph and require a new setup/capture step.
  • Handle changing active particle counts through inactive slots rather than resizing arrays.
  • Use resampling, merging, or volume scaling before a box exhausts inactive particle slots.
  • Keep graph-captured loops focused on repeated timesteps with stable process order, stable buffer shapes, and stable communication maps.

For shipped coagulation behavior, rng_states are Warp-resident sidecar state owned by the caller when persistence is needed across timesteps. Passing a fixed rng_seed with a reused rng_states buffer does not trigger hidden re-seeding; explicit initialization happens only during omitted-buffer convenience allocation or when initialize_rng=True is requested before the captured loop or repeated-step run.

For graph capture, particle count changes should be represented as changes in active slots, not changes in array shape. If a simulation would create more particles than the fixed slots allow, the GPU loop should trigger a documented resampling or volume-scaling policy before slot exhaustion.

Performance and Memory

  • Establish benchmark targets with box count as the primary axis (for example 1, 10, 100, and 1000 boxes), varying particles per box at each box count.
  • Establish secondary benchmark targets for GPU-resident simulations at 1k, 10k, 100k, and larger particle counts per box.
  • Record the single-box scaling limit of the shipped one-thread-per-box coagulation kernel as part of the accepted-with-caveat Epic C decision, using the measured decision record below to bound when large single-box use stops being a strong recommendation (see Known Kernel Issues).
  • Minimize CPU/GPU round trips in example workflows and high-level APIs.
  • Reuse temporary buffers for repeated timesteps to avoid repeated allocation.
  • Profile CUDA kernels for occupancy, memory access patterns, and scaling across boxes and particle counts.
  • Benchmark graph-captured GPU loops against uncaptured GPU loops to quantify launch-overhead savings for small and medium timestep workloads.
  • Track end-to-end performance using GPU-resident loops, not only isolated single-kernel timings.
  • Add memory-budget estimates for n_boxes × n_particles × n_species state, inactive slots, temporary buffers, collision-pair buffers, diagnostics, communication maps, and autodiff tape storage for multi-step gradient runs (see Epic I).
  • Include benchmark cases that vary boxes, particles per box, species count, active-slot fraction, and process combinations.

Shipped coagulation benchmark evidence (2026-07-10 UTC)

  • Command evidence: run pytest particula/gpu/tests/benchmark_test.py --benchmark -v -s. This remains opt-in benchmark evidence for local CUDA-capable environments, not part of the standard Warp CPU release-validation command set. The recorded source-of-record artifact at .artifacts/benchmarks/gpu_benchmark_results.json preserves the exact executed local command in benchmark_metadata.command; the 2026-07-10 UTC capture came from a local coverage-wrapped invocation of that same benchmark target, but the plain user-facing reproduction command remains the one above.
  • Hardware/context: the same artifact records warp_version=1.15.0, device.alias=cuda:0, device.name=NVIDIA GeForce RTX 5060, and device.total_memory_bytes=8082096128 (about 8.08 GiB), with started_at=2026-07-10T01:59:34.215800+00:00 and updated_at=2026-07-10T02:00:43.918911+00:00.
  • Artifact output: .artifacts/benchmarks/gpu_benchmark_results.json
  • Mixed-scale fixture note: the coagulation benchmark path uses a dedicated deterministic mixed NPF/droplet fixture aligned with the shipped E3-F2 baseline, while condensation benchmarks keep the generic helper.
Measured decision record for the current one-thread-per-box path

Epic C accepts the shipped one-thread-per-box coagulation design with caveats: it is the current documented baseline for many independent boxes, especially when CUDA can deliver box-level parallel throughput, and for low-level direct-kernel workflows, but large single-box use remains bounded by the caution band below and does not imply a broader long-term scaling decision.

  • Single-box caution band: treat the recorded 1x10k to 1x50k range as the current caution band for large single-box workloads on the shipped one-thread-per-box kernel. The measured GPU time per step rises from 0.6809s at 1x10k to 1.3689s at 1x20k and 3.3984s at 1x50k, so this path is no longer a strong recommendation for large single-box work in that band. Smaller 1x500 to 1x5k cases (0.0361s, 0.1366s, 0.3411s) remain a modest-throughput regime, not a decisive GPU win.
  • Multi-box effective region: independent-box parallelism remains effective in the recorded many-box cases, where equivalent or larger total-particle workloads stay near the smaller timings seen at lower box counts: 10x500 0.0359s, 10x1k 0.0699s, 50x1k 0.0714s, 10x5k 0.3437s, 50x5k 0.3528s, 100x1k 0.0766s, and 10x10k 0.6938s. Record this as the region where the current GPU path still benefits from many independent boxes keeping threads busy.
  • Machine-bounded caveat: this is measured evidence from the 2026-07-10 capture on the recorded Warp/device context above, not a universal cutoff for every GPU or every future kernel variant. Treat it as the current decision record for the shipped one-thread-per-box design, not as a final acceptance decision on the long-term scaling strategy.

Epic I: Differentiability and Global Optimization

A longer-term goal is gradient-based global optimization: using Warp automatic differentiation to fit model parameters to experiments or observations. Differentiability constrains how kernels are written, so it must be considered while the Epic D and Epic E kernels are authored, not added afterward. For the shipped container, transfer, and runnable-example baseline that this differentiability work builds on, start from the Data Containers and GPU Foundations guide and the Data Containers example.

See Warp Autodiff: Limitations and Stochastic Process Handling for the detailed implementation-planning companion on autodiff mechanics, kernel-authoring constraints, offline code patterns, and the options for differentiating stochastic coagulation.

The optimization targets are initial state, not process parameters. Physical coagulation and condensation parameters (accommodation coefficient, diffusivity, and similar) are prescribed and held fixed. The inverse problem fits the initial aerosol state to a measured final state. Examples: initial versus final size distribution, initial versus final hygroscopicity, or initial versus final mixing state. Coagulation and condensation are the size-dependent forward operators that connect initial to final state; their parameters are not tuned.

The consequence for differentiability is important: because the loss is on the final state and the unknowns are the initial state, gradients must flow through each process operator with respect to the state it acts on (per-bin or per-particle masses, concentrations, and composition), not with respect to its parameters. Prescribed parameters mean no parameter adjoints are needed, but the operators themselves must still be differentiable in state.

Planned features:

  1. Differentiable-friendly variants of apply_mass_transfer_kernel and apply_coagulation_kernel compatible with wp.Tape (no gradient-breaking in-place mutation or control flow on the optimization path).
  2. End-to-end gradient from a final-state loss back to the initial state through a multi-step deterministic condensation loop, with recorded tape memory usage.
  3. Tape memory budgeting and gradient-checkpointing evaluation for long differentiable loops, feeding the Epic H memory-budget model.
  4. Global-optimization example that recovers an initial size distribution from a synthetic final distribution across multiple boxes, with process parameters held fixed.
  5. Decision record for differentiable coagulation (deterministic binned/sectional operator versus relaxed particle-resolved formulation).
  6. Decision record for the mixing-state and hygroscopicity state representation (composition-resolved sectional versus particle-resolved with a differentiable surrogate).
  7. Combined validation of autodiff, graph capture, and per-box RNG streams operating together.
  8. Loss-function definitions on state (size distribution, hygroscopicity, mixing-state metrics), each with a confirmed differentiable path back to the initial state.

Design constraints and open decisions:

  • Author optimization-path kernels against Warp's wp.Tape requirements. In-place mutation and some control-flow patterns can break or zero gradients. The current apply_mass_transfer_kernel and apply_coagulation_kernel use in-place updates and would need differentiable-friendly variants for the optimization path.
  • Start with deterministic condensation. Isothermal and latent-heat condensation are natural first differentiable targets because mass transfer is deterministic. Prove an end-to-end gradient and optimization loop here before extending to other processes.
  • Budget tape memory and plan checkpointing. Backpropagating through a multi-step loop stores intermediates per step, so tape memory scales with timesteps times n_boxes × n_particles × n_species fp64 state and can dominate the memory budget. Evaluate gradient checkpointing (recompute segments of the forward pass during the backward pass) before committing to long differentiable loops, and include tape storage in the Epic H memory-budget model.
  • Stochastic coagulation blocks state gradients as currently implemented. The GPU Brownian coagulation kernel uses stochastic acceptance-rejection and discrete pair selection, so gradients do not flow through it back to the initial state, even though its parameters are fixed. How coagulation participates in the initial-state inversion is an open decision. Candidate approaches:
  • A deterministic binned or sectional coagulation operator (Smoluchowski) for the gradient path, distinct from the particle-resolved stochastic forward model, validated to agree in the mean.
  • A relaxed or differentiable particle-resolved formulation if per-particle fidelity is required.
  • Mixing state and hygroscopicity favor composition-resolved state. Fitting initial versus final mixing state or hygroscopicity requires tracking per-species composition, which coagulation redistributes when particles of different composition merge. Decide the representation that is both physically adequate and differentiable, for example composition-resolved sectional bins (size by composition) versus particle-resolved with a differentiable surrogate. The limited direct-kernel ideal/kappa water- activity support shipped in Epic D is not yet a differentiable or high-level runtime path; a suitable differentiable hygroscopicity implementation remains required.
  • Autodiff, graph capture, and RNG state interact. Differentiable execution, graph-captured timestep loops, and per-box RNG streams must coexist for gradient-based fitting. Validate them together, not only in isolation.
  • Multi-box supports optimization directly. Evaluating many boxes per optimization step provides the batch of conditions used to match experiments or observations.
  • Define the loss functions on state, for example distance between initial and final size distributions, hygroscopicity distributions, or mixing-state metrics, and confirm each has a differentiable path back to the initial state.

Exit bar: A gradient-based optimizer recovers a known initial state from synthetic multi-box final-state data within recorded tolerance, using GPU autodiff, without fitting any process parameters, and the differentiable coagulation and state-representation decisions are recorded.

Risks and Key Decisions

Risk / Decision Impact Direction
fp64 throughput on consumer CUDA Slower GPU, weaker speedups Keep fp64 as the shipped reference and production baseline; evaluate mixed precision only through a future evidence-backed migration proposal
NPF-to-droplet dynamic range near fp64 limits Lost small-mass resolution Use the shipped mass-precision report as the baseline; keep per-species absolute mass until a future proposal proves an alternative across the E2 cases
fp64 memory doubling vs large multi-box Fewer boxes fit in memory Build a memory-budget model; treat precision and box count together
Time-scale stiffness across NPF-to-droplet range Unstable or wastefully slow explicit stepping Use the shipped four-substep direct condensation step with P2-finalized particle/gas coupling; retain case-specific evidence in the stiffness study and keep temperature feedback, independent-device, graph, and autodiff work gated
No fully integrated GPU-ready per-box thermodynamic runtime path Blocks parcels, expansion, latent-heat feedback from running end to end on GPU Keep shipped CPU EnvironmentData as the authoritative CPU owner, build on the shipped WarpEnvironmentData CPU↔GPU round-trip helpers, and add integration work
Rejection-sampling acceptance collapse for wide size ranges Coagulation trial counts explode in mixed NPF/droplet boxes Evaluate binned majorant kernels or stratified pair sampling
One-thread-per-box coagulation Serializes large single-box workloads Record as the current measured multi-box tradeoff for the shipped path; any parallel-within-box follow-up would require a new evidence-backed proposal
Caller must seed or explicitly reset persistent coagulation RNG state before repeated loops or graph capture Reused buffers continue their stream unless intentionally reset, which can surprise callers expecting hidden reseeding Keep the shipped seed-once contract, document caller-owned sidecar setup, and reset only via explicit initialization before the loop or capture
Autodiff through stochastic coagulation Blocks global optimization if coag must be fit Open decision; start with deterministic condensation, defer coag choice
In-place kernels break gradients Autodiff path unusable Author differentiable-friendly kernel variants for the optimization path
Tape memory for multi-step differentiable loops Gradient runs exhaust GPU memory Budget tape storage; evaluate gradient checkpointing
RNG reproducibility across CPU/Warp CPU/CUDA Non-reproducible stochastic results Per-box RNG streams; document tolerances and reproducibility limits
Caller drops ordered gas metadata or vapor-pressure sidecar across WarpGasData restore Placeholder names or missing GPU-helper state break name-keyed or condensation follow-up logic Preserve ordered names externally. The supported direct condensation path automatically refreshes derived vapor pressure on-device, but CPU restore remains intentionally lossy and this does not add latent-heat temperature feedback or broader runtime behavior.
Graph capture fragility (shape/process changes) Invalid captured graphs Fixed shapes, inactive slots, documented re-capture triggers
GPU condensation supports only limited activity/kappa physics Unsupported activity/surface strategies and the lack of a differentiable/high-level path still constrain hygroscopicity targets Retain E4-F2's direct ideal/kappa and static/weighted sidecars; add broader or differentiable support only through scoped, evidence-backed Epic D work