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:
- Current container schema inventory
- Shipped E2 foundation baseline
- Authoritative field ownership decisions
- Shipped foundation guide
- Runnable Data Containers example
- Final downstream handoff map for sibling features
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.
ParticleDatastores particle masses, concentrations, charges, densities, and box volumes with explicit batch dimensions.GasDatastores 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:
EnvironmentDatanow provides a CPU-side home for per-box thermodynamic state, withtemperature -> (n_boxes,),pressure -> (n_boxes,), andsaturation_ratio -> (n_boxes, n_species). The shipped baseline is available fromparticula.gas.environment_data, exported fromparticula.gas, supportscopy()for CPU-side state handling, and only crosses the CPU↔GPU boundary through publicparticula.gpuhelpers:WarpEnvironmentData,to_warp_environment_data(), andfrom_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.densityis 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_contextcontext manager and aWARP_AVAILABLEfeature flag. - GPU condensation kernels provide a tested
condensation_step_gpuAPI. - GPU Brownian coagulation kernels provide a tested
coagulation_step_gpuAPI. - 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 remainwarp,cuda,gpu_parity, andstochastic, and CPU-only or standard CI environments are expected to skip CUDA-specific coverage cleanly instead of treating it as a regression. Focused examples includepytest particula/gpu/kernels/tests/condensation_test.py -q -m "warp and gpu_parity and not cuda"for the required Warp CPU parity path andpytest 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
Runnableintegration 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 masks3,5,6,9,10,12; and four-way mask15. Non-turbulent three-way mask7rejects at capability preflight before particle metadata or enabled-term validation. Turbulent three-way masks11,13, and14proceed 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-devicewp.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 usertol=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.float64sidecars. This is direct-kernel support only, not high-level GPUAerosolorRunnablesupport. BAT and every other deferred activity or surface strategy remain CPU-only and are rejected withValueError; they are never silently approximated or transferred. - Gas vapor pressure is a GPU-only derived field:
WarpGasData.vapor_pressuredefaults to zeros when not supplied and is dropped when converting back toGasData. 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_gpusupports a convenience allocate-and-seed path whenrng_statesare omitted, while caller-ownedrng_statespersist across repeated calls until explicitly reset withinitialize_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_maxbound 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, andWarpGasDatahave a documented field-ownership and shape contract, including leadingn_boxesdimensions for per-box state and species-only shapes for shared material or gas metadata.EnvironmentDatais the CPU owner for per-boxtemperature,pressure, and per-box/per-speciessaturation_ratio; simulation volume remains owned byParticleData.volume.WarpEnvironmentData,to_warp_environment_data(), andfrom_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.nameis CPU-only ordered metadata, shared CPUpartitioningexpands to an active-device, binarywp.int32mask indexed by box and species on GPU, andWarpGasData.vapor_pressureis 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
WarpEnvironmentDatainputs. - The mass-precision report keeps absolute per-species
np.float64/wp.float64particle 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_4as 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
ParticleRepresentationandGasSpeciesshould 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
ParticleDataandGasDatainstead 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()restoresGasData.namefrom caller input or placeholder values such asspecies_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_pressureis GPU-only helper state. It defaults to zeros into_warp_gas_data()when omitted and is dropped when restoring CPUGasData.
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)andParticleData.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, andGasData.partitioningremain 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.pyfor single-box, multi-box, shared-density, and derived-shape behavior.particula/gas/tests/gas_data_test.pyandparticula/gas/tests/gas_data_builder_test.pyfor gas constructor and builder-added leading box dimensions.particula/gpu/tests/warp_types_test.pyandparticula/gpu/tests/conversion_test.pyfor Warp container parity and CPU↔GPU shape preservation.particula/dynamics/condensation/tests/condensation_strategies_test.pyandparticula/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.densityremains 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.volumeis the authoritative per-box simulation-volume carrier with shape(n_boxes,), and shipped CPUEnvironmentDataplus 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_pressureis process/GPU-helper state rather than owned CPUGasDataor shipped CPUEnvironmentDatastate; CPU-facing workflows must recompute it or pass it as ordered sidecar state after any temperature or concentration update, and CPU restore fromWarpGasDatais intentionally lossy becauseto_warp_gas_data()injects zeros when absent andfrom_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).WarpGasDatais numeric-only; restoring CPU gas names requires caller-supplied ordered names or equivalent external metadata because the GPU container excludes string fields andfrom_warp_gas_data()otherwise generates placeholder names such asspecies_0. Current restore logic checks only metadata length againstn_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,), andsaturation_ratio: (n_boxes, n_species)underEnvironmentData, without moving simulation volume out ofParticleData.volume.saturation_ratioshould 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 CPUEnvironmentData.temperature -> (n_boxes,),pressure -> (n_boxes,), andsaturation_ratio -> (n_boxes, n_species); keepParticleData.volume -> (n_boxes,)as the authoritative simulation volume owner rather than moving volume intoEnvironmentData.E2-F3: inherit the now-shipped exact CPU↔GPU schema mirroring betweenEnvironmentDataandWarpEnvironmentData, withtemperature -> (n_boxes,),pressure -> (n_boxes,), andsaturation_ratio -> (n_boxes, n_species); do not add an extra volume field or an implicit transfer path.E2-F4: inherit thatGasData.nameremains CPU-only ordered metadata andGasData.partitioningstaysboolon CPU whileWarpGasData.partitioningis an active-device binarywp.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 outsideWarpGasDataand 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 asParticleData.masses -> (1, n_particles, n_species),ParticleData.concentration -> (1, n_particles), andGasData.concentration -> (1, n_species)without changing the canonical storage ranks for multi-box-ready containers.E2-F6: inherit the currentfloat64/wp.float64schema 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, includingParticleData.volumeownership andWarpGasData.vapor_pressureas lossy helper state, then build later condensation integration work on the P4 recommendation incondensation-stiffness-study.md:fixed_count_substeps_4as the preferred fixed-shape foundation, bounded by the E2-F2 environment-shape contract, the E2-F6float64evidence 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 leadingn_boxesdimensions, 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
EnvironmentDatashape contract:temperatureandpressureare(n_boxes,), andsaturation_ratiois(n_boxes, n_species). - Build on the now-shipped
WarpEnvironmentDatamirror and CPU↔GPU conversion helpers when broader GPU-side thermodynamic state work begins. - Existing process APIs may still accept scalar
temperatureandpressureuntil later migration work lands; only migrated process code should readEnvironmentDatadirectly. - 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.volumerather thanEnvironmentDataownership. - 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_ratioand GPU-sidevapor_pressuremust be invalidated and recomputed. Treat environment state as read-only unless the physical model owns the update.saturation_ratioshould 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/GasDataconversion patterns, including one-box and multi-box cases plus explicit synchronization behavior, with device-parametrized parity checks that always run on Warpcpuand add Warpcudaonly 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.float64schema 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 fourtime_step / 4.0substeps. 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
float64evidence 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.dynamicsand 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_energybookkeeping 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:
- Persist coagulation RNG state: seed once at loop setup and keep per-box
RNG state between
coagulation_step_gpucalls instead of re-launching the initialization kernel on every call. - 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.
- 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.
- 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
- The paired
CondensationLatentHeatexample now provides a runnable CPU-only bookkeeping workflow (E3-F6), completing the documentation follow-up deferred from E1. particula/integration_tests/condensation_latent_heat_conservation_test.pynow 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_gpuseeds 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-P2outcome 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 globalk_maxcomputed 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:
- Sedimentation variants beyond the exact unit-efficiency SP2016 direct-kernel term.
- DNS/general turbulent-shear physics beyond the shipped ST1956 term.
- Parity and statistical validation: CPU/GPU parity or statistically bounded tests for each new kernel, with recorded tolerances.
- 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.
- 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_gpufor direct neutral, particle-resolved spherical or rectangular fixed-slot wall loss.NeutralWallLossConfigremains concrete-module-only atparticula.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:
- GPU process orchestration, backend selection/fallback policy, scheduling, and GPU-resident timestep integration for the delivered direct kernels.
- GPU charged wall loss, after the bounded neutral path and the core condensation and coagulation GPU paths are stable.
- Nucleation/particle-source CPU reference process following the nucleation equations (no nucleation code exists in particula today).
- GPU nucleation via slot activation (see Particle Slot Management).
- Particle slot management: inactive zero-mass slots, activation, per-box active-count diagnostics, and an exhaustion policy (resampling or volume scaling).
- Slot and conservation validation: tests for inactive slots, activation, slot exhaustion handling, and conservation across resampling or volume scaling.
- 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:
- Backend-selection API design: decide whether selection belongs on strategies, runnables, builders, or a separate execution context.
- Backend selection for at least one condensation workflow, with explicit return types and mutation semantics.
- Backend selection for at least one Brownian coagulation workflow.
- GPU simulation loop abstraction that keeps
WarpParticleData,WarpGasData, andWarpEnvironmentDataresident on the selected device across all enabled dynamics. - Deterministic process ordering/scheduler for full aerosol simulations (condensation, coagulation, wall loss, dilution, environment updates, and gas updates between steps).
- 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. - Multi-box communication: prescribed advection, dilution, expansion, and simple mixing maps, including per-box volume changes for parcel and combustion cases.
- Per-box RNG streams so independent boxes remain reproducible, with GPU-resident RNG state exercised in loop tests.
- 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,
EnvironmentDataownership, 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, andWarpEnvironmentDataresident 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 omittedrng_statesstill 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:
- Warp graph capture for repeated timestep execution with fixed process order and stable array shapes.
- Separation of graph-capturable kernels from setup work (allocation, validation, host-side scheduling).
- Preallocated buffer reuse for mass transfer, collision pairs, wall-loss rates, dilution factors, diagnostics, and RNG state.
- Full-loop validation tests comparing CPU, uncaptured GPU, and graph-captured GPU execution for the same process sequence.
- 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. - Memory-budget model covering state arrays, inactive slots, temporary buffers, diagnostics, communication maps, and autodiff tape storage.
- Graph-capture example plus documented limitations and re-capture triggers.
- 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_statesbefore 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, orn_speciesshould 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_speciesstate, 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.jsonpreserves the exact executed local command inbenchmark_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, anddevice.total_memory_bytes=8082096128(about 8.08 GiB), withstarted_at=2026-07-10T01:59:34.215800+00:00andupdated_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
1x10kto1x50krange 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 from0.6809sat1x10kto1.3689sat1x20kand3.3984sat1x50k, so this path is no longer a strong recommendation for large single-box work in that band. Smaller1x500to1x5kcases (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:
10x5000.0359s,10x1k0.0699s,50x1k0.0714s,10x5k0.3437s,50x5k0.3528s,100x1k0.0766s, and10x10k0.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:
- Differentiable-friendly variants of
apply_mass_transfer_kernelandapply_coagulation_kernelcompatible withwp.Tape(no gradient-breaking in-place mutation or control flow on the optimization path). - 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.
- Tape memory budgeting and gradient-checkpointing evaluation for long differentiable loops, feeding the Epic H memory-budget model.
- Global-optimization example that recovers an initial size distribution from a synthetic final distribution across multiple boxes, with process parameters held fixed.
- Decision record for differentiable coagulation (deterministic binned/sectional operator versus relaxed particle-resolved formulation).
- Decision record for the mixing-state and hygroscopicity state representation (composition-resolved sectional versus particle-resolved with a differentiable surrogate).
- Combined validation of autodiff, graph capture, and per-box RNG streams operating together.
- 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.Taperequirements. In-place mutation and some control-flow patterns can break or zero gradients. The currentapply_mass_transfer_kernelandapply_coagulation_kerneluse 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_speciesfp64 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 |