Data Containers and GPU Foundations¶
This page is the canonical reference for Particula's shipped data-container schemas, leading-axis shape conventions, explicit CPU↔GPU transfer helpers, the direct-kernel troubleshooting contract, and current CPU/GPU support boundaries.
For a runnable walkthrough of the shipped single-box container construction and optional Warp CPU-backend round trips, see the Data Containers example and run:
python docs/Examples/data_containers_and_gpu_foundations.py
For the low-level direct-kernel path, use the canonical quick-start at
docs/Examples/gpu_direct_kernels_quick_start.py.
That example keeps direct step imports explicit through particula.gpu.kernels
while top-level particula.gpu stays focused on WARP_AVAILABLE and the
to_warp_* / from_warp_* transfer helpers.
Use this guide when you need the current contract for:
ParticleData,GasData, andEnvironmentDataWarpParticleData,WarpGasData, andWarpEnvironmentData- explicit transfer helpers in
particula.gpu - single-box and multi-box shape conventions
- current shipped limitations for CPU and GPU workflows
For migration walkthroughs and before/after examples, see Particle & Gas Data Migration. For future work and planned expansions, see the Data-Oriented Design and GPU Roadmap.
Public imports¶
Prefer the currently exported package-level imports:
from particula.gas import EnvironmentData, GasData
from particula.gpu import (
from_warp_particle_data,
from_warp_environment_data,
from_warp_gas_data,
to_warp_particle_data,
to_warp_environment_data,
to_warp_gas_data,
)
from particula.particles import ParticleData
WarpParticleData, WarpEnvironmentData, and WarpGasData are exported from
particula.gpu only when Warp is available, so import them only behind an
optional Warp guard:
from particula.gpu import WARP_AVAILABLE
if WARP_AVAILABLE:
from particula.gpu import (
WarpEnvironmentData,
WarpGasData,
WarpParticleData,
)
Canonical container schemas¶
ParticleData¶
ParticleData owns particle-side stored state. Per-box arrays always keep a
leading n_boxes axis.
| Field | Shape | Meaning |
|---|---|---|
masses |
(n_boxes, n_particles, n_species) |
Authoritative per-particle, per-species masses. |
concentration |
(n_boxes, n_particles) |
Per-box particle concentration or count, depending on workflow. |
charge |
(n_boxes, n_particles) |
Per-box particle charge state as dimensionless elementary-charge counts. |
density |
(n_species,) |
Shared-across-box species density. |
volume |
(n_boxes,) |
Authoritative per-box simulation volume. |
Derived properties such as radii, total_mass, and effective_density are
computed from stored fields rather than stored separately.
GasData¶
GasData owns CPU-side gas species data and ordered species metadata.
| Field | Shape | Meaning |
|---|---|---|
name |
len == n_species |
Authoritative ordered CPU species metadata. |
molar_mass |
(n_species,) |
Shared-across-box molar masses. |
concentration |
(n_boxes, n_species) |
Authoritative per-box gas concentration. |
partitioning |
(n_species,) |
Authoritative CPU boolean partitioning mask. |
GasData does not own per-box thermodynamic state. Temperature, pressure, and
saturation ratio belong to EnvironmentData.
EnvironmentData and WarpEnvironmentData¶
EnvironmentData is the shipped CPU-side owner of per-box thermodynamic state.
WarpEnvironmentData mirrors that schema across the explicit helper boundary.
| Field | CPU owner | CPU shape | GPU shape | Meaning |
|---|---|---|---|---|
temperature |
EnvironmentData |
(n_boxes,) |
(n_boxes,) |
Per-box temperature in kelvin. |
pressure |
EnvironmentData |
(n_boxes,) |
(n_boxes,) |
Per-box pressure in pascals. |
saturation_ratio |
EnvironmentData |
(n_boxes, n_species) |
(n_boxes, n_species) |
Per-box, per-species thermodynamic helper state. |
EnvironmentData is available from particula.gas, while
WarpEnvironmentData is available from particula.gpu when Warp is installed.
Shape conventions¶
Per-box arrays always keep a leading n_boxes dimension, even when
n_boxes == 1.
- Single-box particle masses:
(1, n_particles, n_species) - Single-box particle concentration:
(1, n_particles) - Single-box gas concentration:
(1, n_species) - Single-box environment temperature:
(1,) - Single-box environment saturation ratio:
(1, n_species)
Shared arrays keep their shared rank and do not gain a box axis:
ParticleData.density -> (n_species,)GasData.molar_mass -> (n_species,)GasData.partitioning -> (n_species,)
Example single-box container construction:
import numpy as np
from particula.gas import EnvironmentData, GasData
from particula.particles import ParticleData
particle_data = ParticleData(
masses=np.array([[[1e-18, 2e-18]]]),
concentration=np.array([[1.0]]),
charge=np.array([[0.0]]),
density=np.array([1000.0, 1200.0]),
volume=np.array([1e-6]),
)
gas_data = GasData(
name=["Water", "H2SO4"],
molar_mass=np.array([0.018, 0.098]),
concentration=np.array([[1e-6, 2e-10]]),
partitioning=np.array([True, True]),
)
environment = EnvironmentData(
temperature=np.array([298.15]),
pressure=np.array([101325.0]),
saturation_ratio=np.array([[0.5, 0.2]]),
)
Explicit CPU↔GPU transfer helpers¶
Particula uses explicit helper calls for CPU↔GPU container movement. Kernels and runnables do not perform hidden container transfers. Read-only device validation may synchronize and read back status to surface invalid values.
Available public helpers:
to_warp_particle_data()from_warp_particle_data()to_warp_gas_data()from_warp_gas_data()to_warp_environment_data()from_warp_environment_data()
Optional Warp availability can be checked with WARP_AVAILABLE.
For the direct-kernel example path, keep the import split explicit:
- Import direct step functions from
particula.gpu.kernels. - Import
WARP_AVAILABLE,to_warp_*, andfrom_warp_*helpers fromparticula.gpu. - Do not expect top-level
particula.gputo re-exportcondensation_step_gpuorcoagulation_step_gpu.
Direct GPU dilution¶
Import the direct dilution operation from the low-level kernel namespace:
from particula.gpu.kernels import dilution_step_gpu
This is distinct from the CPU↔Warp transfer helpers in particula.gpu.
Callers own conversion, device placement, synchronization and checkpoint
transfer, and the WarpParticleData and WarpGasData instances. The kernel
does not provide hidden transfer or CPU fallback.
dilution_step_gpu is a fixed-shape, caller-owned in-place operation. It
returns the same particle and gas objects and changes only their
concentration fields; masses and every other caller-owned particle or gas
field remain unchanged. It applies alpha = Q / V in s^-1 and
c_new = c * exp(-alpha * time_step). The coefficient is either a finite,
nonnegative scalar or a caller-owned, same-device wp.float64 Warp array with
shape (n_boxes,).
Entry preflight is ordered and read-only. Rejected calls are atomic before kernel launch. A zero scalar coefficient or zero time step completes full preflight and is a write-free, no-update-kernel no-op; preflight validation scans may still allocate or launch. The contract does not promise rollback after a launched-kernel failure.
E6-F1 supplies the upstream CPU finite-step oracle; E6-F9 is the planned
integrated direct-call consumer. Independent NumPy comparisons of particle and
gas concentrations run on Warp CPU with float64 rtol=1e-12, atol=0; CUDA is
optional and skips cleanly when unavailable. This tolerance-based evidence is
not bitwise parity.
Direct neutral GPU wall loss¶
The bounded direct wall-loss path is neutral and particle-resolved only. Import
the step and its concrete-module-only configuration separately; the
configuration is not re-exported by particula.gpu.kernels or particula.gpu:
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,
)
The snippet is illustrative: particles and rng_states are caller-owned
same-device Warp state. Later calls reuse rng_states and omit
initialize_rng=True. Direct temperature and pressure each accept a scalar
or same-device (n_boxes,) Warp array. Alternatively, pass
environment=... and set both temperature=None and pressure=None; direct
values and environment= are mutually exclusive.
Wall eddy diffusivity is in m²/s, dimensions are in m, temperature is in K, pressure is in Pa, and time is in s. Spherical configurations require a positive radius and no dimensions; rectangular configurations require no radius and exactly three positive dimensions. The neutral coefficients use the Crump--Seinfeld spherical and rectangular chamber relations, including turbulent deposition and gravitational settling:
- Crump, J. G., & Seinfeld, J. H. (1981), Journal of Aerosol Science, 12(5). https://doi.org/10.1016/0021-8502(81)90036-7
- Crump, J. G., Flagan, R. C., & Seinfeld, J. H. (1982), Aerosol Science and Technology, 2(3), 303--309. https://doi.org/10.1080/02786828308958636
After read-only preflight, eligible finite positive-rate fixed slots survive with
exp(-k * time_step). A selected slot has every mass lane, concentration, and
charge cleared. Density, volume, dtype, device, capacity, and unselected
storage are preserved; inactive or unusable slots are neither sampled nor
reactivated. The asynchronous call mutates caller-owned state in place. Callers
own CPU↔Warp transfer, device placement, synchronization, and any checkpoint.
There is no hidden CPU checkpoint transfer or fallback. Rejected pre-launch
calls do not mutate caller-owned state, while rollback is not promised after a
mutation kernel launches. Zero time completes preflight but is write-free.
Omitted RNG state is private to each successful nonzero call. Supplied
same-device (n_boxes,) wp.uint32 state mutates in place;
initialize_rng=True is the only reset, so repeating rng_seed alone does not
reset persistent state. Consumption is sequential per box over eligible finite
positive-rate slots; positive infinite-rate removal is deterministic and
consumes no draw. Exact CPU/Warp or per-seed RNG replay is not promised.
| Scope | Status |
|---|---|
| Neutral particle-resolved spherical/rectangular direct execution | Supported |
| Geometry-specific deterministic coefficient tolerances and 100-seed, 3-sigma aggregate survival evidence | Supported bounded evidence |
| CUDA validation | Optional additive evidence; guarded skips are expected when unavailable |
| Charged, image-charge, or electric-field physics; E6-F4 charged wall loss | Deferred |
| CPU fallback or hidden transfer; high-level runnable, scheduler, or backend integration | Deferred (E6-F9 covers integration/closeout) |
| Dynamic slots, compaction/activation, graph capture, differentiability, performance guarantees, or exact RNG replay | Deferred |
Warp CPU is the baseline when Warp is installed; CUDA is optional and skips cleanly when unavailable. The evidence is not CPU-strategy parity or per-seed trajectory replay.
GPU process orchestration, backend selection and scheduling, GPU-resident timestep integration, resizing, graph capture, autodiff, performance claims, and nucleation remain deferred.
Particle transfer boundary¶
WarpParticleData is the explicit Warp mirror for ParticleData. Use the
helper boundary when particle-resident arrays must move between CPU-owned
containers and Warp-owned storage:
from particula.gpu import from_warp_particle_data, to_warp_particle_data
gpu_particle = to_warp_particle_data(particle_data, device="cpu")
restored_particle = from_warp_particle_data(gpu_particle)
Across this boundary, the shipped particle schema stays aligned:
| Field | CPU ParticleData |
GPU WarpParticleData |
Contract |
|---|---|---|---|
masses |
(n_boxes, n_particles, n_species) |
(n_boxes, n_particles, n_species) |
Authoritative per-particle, per-species masses round-trip without shape drift. |
concentration |
(n_boxes, n_particles) |
(n_boxes, n_particles) |
Preserves the leading n_boxes axis for single-box and multi-box storage. |
charge |
(n_boxes, n_particles) |
(n_boxes, n_particles) |
Preserves dimensionless elementary-charge counts without hidden conversion. |
density |
(n_species,) |
(n_species,) |
Shared species density remains shared; it does not gain a box axis. |
volume |
(n_boxes,) |
(n_boxes,) |
Per-box simulation volume stays particle-owned across the helper boundary. |
As with the gas and environment helpers, kernels and runnables do not perform hidden CPU↔GPU synchronization or implicit container transfers for particle state.
For direct GPU coagulation, WarpParticleData.charge is caller-owned,
device-resident particle state, not a sidecar or hidden transfer result. It
must be a same-device wp.float64 array matching shape
(n_boxes, n_particles). Charged-containing execution scans charge for finite
values before caller-output validation/allocation, RNG setup, or selector/apply
work. Brownian-only execution scans it only with validate_charge_finite=True.
When a collision is accepted, the recipient receives the donor charge and the
donor charge is cleared together with donor mass and concentration. Supported
evidence conserves charge independently in each box. This merge bookkeeping
skips a collision whose finite charge sum cannot be represented as finite
float64, leaving both particles unchanged rather than producing infinity.
Before recording a selected collision or removing its candidates from the
active set, selection also verifies that every merged species-mass component
and the merged charge remain finite and nonnegative where applicable. The
apply pass repeats those checks defensively; rejected entries are compacted
using only the populated collision count, so unused capacity in an oversized
caller buffer is never interpreted as a collision.
GPU coagulation configuration and sidecar ownership¶
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 immutable mechanism_config=CoagulationMechanismConfig(...) as keyword-only
host metadata. The frozen dataclass is not re-exported by
particula.gpu.kernels, is not device-resident simulation state, and neither
transfers nor owns Warp state. 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.
Only distribution_type="particle_resolved" is accepted. "discrete" and
"continuous_pdf" raise ValueError; they do not fall back or convert.
Malformed configuration and an unsupported distribution fail before particle
access. Mask 7 fails at capability preflight before particle metadata or
enabled-term validation. Masks 11, 13, and 14 validate particle metadata
and enabled terms before they fail closed. These read-only preflight boundaries
do not make a later runtime failure transactional: callers retain their mutable
state, and no rollback is guaranteed after such a failure.
| Identifier | Bit | Status |
|---|---|---|
brownian |
1 |
Executable singleton |
charged_hard_sphere |
2 |
Executable singleton |
sedimentation_sp2016 |
4 |
Executable singleton |
turbulent_shear_st1956 |
8 |
Executable singleton |
brownian + charged_hard_sphere |
3 |
Executable two-way mask |
brownian + sedimentation_sp2016 |
5 |
Executable two-way mask |
charged_hard_sphere + sedimentation_sp2016 |
6 |
Executable two-way mask |
brownian + turbulent_shear_st1956 |
9 |
Executable two-way mask |
charged_hard_sphere + turbulent_shear_st1956 |
10 |
Executable two-way mask |
sedimentation_sp2016 + turbulent_shear_st1956 |
12 |
Executable two-way mask |
brownian + charged_hard_sphere + sedimentation_sp2016 + turbulent_shear_st1956 |
15 |
Executable four-way mask |
brownian + charged_hard_sphere + sedimentation_sp2016 |
7 |
Rejected three-way mask |
brownian + charged_hard_sphere + turbulent_shear_st1956 |
11 |
Rejected three-way mask |
brownian + sedimentation_sp2016 + turbulent_shear_st1956 |
13 |
Rejected three-way mask |
charged_hard_sphere + sedimentation_sp2016 + turbulent_shear_st1956 |
14 |
Rejected three-way mask |
The executable singleton masks are 1, 2, 4, and 8; executable two-term
masks are 3, 5, 6, 9, 10, and 12; and executable four-term mask is
15. Deferred masks are exactly 7, 11, 13, and 14.
Requested tuples normalize to the canonical Brownian, charged, sedimentation,
then turbulent order. 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, where settling velocities use
Stokes settling with Cunningham slip correction (Seinfeld & Pandis, 2016,
Eq. 13A.4). Collision efficiency is fixed at 1; no public efficiency argument
exists. This mode uses fp64 particle mass state, validates finite particle
charge, finite/nonnegative mass and concentration, plus finite/positive density
before mutable work, and requires equal positive particle concentrations within
each box for representable inventory-preserving merges. Non-finite charge is
rejected during host-side preflight before caller-owned outputs, persistent RNG,
or particle state can be mutated.
The exact turbulent-shear singleton is selected with
CoagulationMechanismConfig(("turbulent_shear_st1956",)); approved mixed masks
that include this term use the same inputs. Keyword-only turbulent_dissipation
is in m^2/s^3 and fluid_density is in kg/m^3. Both are positive finite
Python or NumPy floating scalars, or active-device wp.float64 Warp arrays
shaped (n_boxes,); supplied arrays retain identity and scalars use private
device-local broadcast/property storage. They are required for structurally
valid turbulent requests, including rejected three-way requests during
enabled-term validation, and ignored by non-turbulent masks. They are not
inferred from a container: NumPy arrays, Python lists, and hidden host-to-device
conversion are not supported as array inputs.
Supply temperature and pressure either as direct scalars, supported-float Warp
arrays of shape (n_boxes,) on the particle device, or same-device
WarpEnvironmentData with both direct inputs set to None. These thermodynamic
inputs are not required to be fp64. Caller-owned particle, collision-output, and
RNG resources retain their documented dtypes and identity. 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. Exact SP2016 sedimentation
uses settling storage only for that mechanism; ST1956 has no dedicated
settling-velocity buffer.
It is a direct-kernel path only: it establishes neither CPU-strategy parity nor
general accuracy or performance claims.
Approved masks sanitize and sum fp64 component rates. The safe summed
component-majorant bound satisfies sum_m K_m(i, j) <= sum_m M_m; component
maxima can occur on different pairs, so that bound can be conservative.
Production selection obtains its majorant by an exact compact-active scan of the
summed pair rate. One shared active set, candidate stream, acceptance stream,
collision-buffer set, per-box RNG stream, and apply pass serve each call;
mechanisms are not sequential steps. Invalid, nonfinite, or nonpositive terms do
not add to totals, and invalid candidates do not mutate particle or
collision-output state. Persistent RNG state can advance before an invalid-rate
rejection. This documents bounded implementation scope, not throughput or
scaling evidence.
Read-only preflight can use a private device-status buffer and a bounded
synchronization/readback to report invalid caller state without copying,
mutating, or CPU-falling-back caller simulation state.
The return tuple is exactly (particles, collision_pairs, n_collisions); RNG
state is not returned. Accepted collisions mutate caller-owned particle mass,
concentration, and charge in place. Supplied collision buffers are returned by
identity. Supplied rng_states are caller-owned same-device Warp state, are
reused and advanced in place, and reset only when initialize_rng=True
explicitly opts in; omitted buffers and RNG state are call-local conveniences.
Sedimentation-specific read-only preflight fails before output allocation, RNG
initialization, or particle mutation, but does not make unrelated later runtime
failures atomic. Transfers are explicit. Warp CPU is the baseline when Warp is
installed. CUDA is optional additive evidence and guarded suites skip cleanly
when unavailable. This direct path provides no high-level Runnable
integration, automatic transfer, CPU fallback, mandatory CUDA requirement, or
general-turbulence support.
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, alternate mechanisms, DNS or general
turbulence, CPU fallback, hidden transfer, and performance or broad-accuracy
claims. See the validation/evidence record
for the focused fixed-mask and stochastic command record.
Environment transfer boundary¶
Use the environment helpers when thermodynamic state must cross the CPU↔GPU boundary:
from particula.gpu import (
from_warp_environment_data,
to_warp_environment_data,
)
gpu_environment = to_warp_environment_data(environment, device="cpu")
restored_environment = from_warp_environment_data(gpu_environment)
Across this boundary, shapes stay fixed:
temperature -> (n_boxes,)pressure -> (n_boxes,)saturation_ratio -> (n_boxes, n_species)
Gas transfer boundary and intentional lossiness¶
WarpGasData is a numeric GPU mirror, not a complete semantic copy of
GasData.
| Field | CPU GasData |
GPU WarpGasData |
Restore contract |
|---|---|---|---|
name |
Ordered species names | Not stored | Restore requires caller-supplied ordered names, or placeholder names such as species_0. |
molar_mass |
(n_species,) |
(n_species,) |
Round-trips without shape drift. |
concentration |
Mass concentration in kg/m^3, (n_boxes, n_species) |
Authoritative active-device wp.float64 mass concentration in kg/m^3, (n_boxes, n_species) |
Round-trips without shape drift. Direct condensation mutates the GPU field in place. |
partitioning |
bool, shape (n_species,) |
Active-device binary wp.int32, shape (n_boxes, n_species) |
CPU masks expand per box as bool → int32; kernels index [box, species]. CPU restore converts back to bool and requires every box to agree. |
vapor_pressure |
Not owned by GasData |
(n_boxes, n_species) |
GPU-only helper state. Pass it explicitly when needed; CPU restore always drops it. |
This makes GPU→CPU gas restore intentionally lossy unless the caller preserves
ordered names and any vapor-pressure sidecar state outside WarpGasData.
concentration remains the authoritative gas inventory while a direct kernel
is running. In contrast, vapor_pressure is derived GPU-only helper state and
is intentionally dropped by CPU restore; it is not an alternate concentration
field or a source of hidden CPU↔Warp synchronization.
Example explicit gas handoff:
import numpy as np
from particula.gpu import from_warp_gas_data, to_warp_gas_data
vapor_pressure = np.array([[2330.0, 120.0]])
gpu_gas = to_warp_gas_data(
gas_data,
device="cpu",
vapor_pressure=vapor_pressure,
)
restored_gas = from_warp_gas_data(gpu_gas, name=gas_data.name)
GPU thermodynamics and condensation refresh¶
Import the supported direct step only with
from particula.gpu.kernels import condensation_step_gpu.
Import its required ThermodynamicsConfig from the concrete module only:
from particula.gpu.kernels.thermodynamics import ThermodynamicsConfig
ThermodynamicsConfig is caller-owned, device-local process configuration.
Its Warp arrays have no species names: species identity is positional, and
molar_mass_reference must exactly equal ordered gas.molar_mass on the
active device. WarpGasData.vapor_pressure, in contrast, is a mutable,
derived GPU helper buffer rather than authoritative GasData state. It is
omitted by from_warp_gas_data().
The fixed configuration and refresh contracts are:
| Field | Required Warp dtype and shape | Meaning |
|---|---|---|
modes |
wp.int32, (n_species,) |
Per-species model code. |
parameters |
wp.float64, (n_species, 4) |
Per-species model parameters. |
molar_mass_reference |
wp.float64, (n_species,) |
Ordered compatibility fingerprint for gas.molar_mass. |
| Refresh temperature | wp.float64, (n_boxes,) |
Device-local temperature at the standalone refresh boundary. |
vapor_pressure output |
wp.float64, (n_boxes, n_species) |
Derived pressure matrix in Pa. |
Only two vapor-pressure model codes are shipped: constant wp.int32(0), which reads
parameters[:, 0] in Pa, and canonical Buck wp.int32(1), which evaluates
the water/ice equations and ignores all four reserved parameter slots.
condensation_step_gpu() requires keyword-only thermodynamics= while
preserving the positional mass_transfer slot. Its accepted temperature
sources are a direct scalar, a direct same-device Warp array with shape
(n_boxes,), hybrid direct scalar/Warp-array inputs, or keyword-only
environment= when both direct values are None. NumPy arrays and Python
lists are not accepted as direct arrays, and the step does not perform hidden
transfers. Temperature and pressure must be positive finite values. Direct and
environment temperature/pressure arrays are active-device (n_boxes,) inputs;
non-wp.float64 temperature arrays are normalized into a device-local fp64
buffer. This normalization does not make every environment input fp64-only.
On every successful condensation call, the step validates aggregate caller
state and optional sidecars first. It then casts a non-wp.float64 direct or
environment temperature array into a device-local float64 buffer when needed
and performs exactly four equal time_step / 4.0 substeps. Each substep
overwrites vapor_pressure, prepares environment properties, gates disabled
partitioning entries and inactive particle slots, and creates a raw proposal.
P2 finalizes that proposal against particle and gas inventory limits, then
derives the matching concentration-weighted opposite gas delta. The step
validates that delta and all pending commit values before mutating particle
masses, accumulating the finalized transfer in the returned total-transfer
buffer, and mutating gas.concentration. Raw proposal work storage is
intermediate state, not returned transfer. Later proposals read the coupled gas
concentration; vapor-pressure refresh does not. A failed aggregate preflight
leaves the vapor-pressure output buffer unchanged. Import the
standalone refresh_vapor_pressure_gpu only from
particula.gpu.kernels.thermodynamics, never particula.gpu.kernels.
Condensation activity and surface sidecars¶
The optional CondensationActivitySurfaceConfig is a direct-kernel-only,
caller-owned configuration sidecar for condensation_step_gpu(). Activity mode
0 is ideal and mode 1 is kappa; surface-tension mode 0 is static and mode
1 is composition-weighted. Activity applies only to water_species_index;
all non-water species use unit activity. Static tension uses the current
species' supplied value, while composition-weighted tension computes one
particle-wide value from the per-species tensions.
kappas and molar_mass_reference must be same-device wp.float64 arrays of
shape (n_species,). Kappas must be finite and nonnegative, and ordered molar
masses must exactly match gas.molar_mass; this preserves the positional
ordered-molar-mass compatibility contract. The frozen dataclass prevents field
rebinding, not mutation of its arrays. Callers retain those arrays and must not
mutate them concurrently with a launch.
Successful direct calls mutate particle masses and gas concentration in place,
overwrite the derived GPU-only vapor-pressure buffer, and return the accumulated
P2-finalized, inventory-limited transfer. A supplied total-transfer buffer
holds that finalized accumulated output and is returned by identity; separate
supplied work storage retains only the final gated raw proposal and is never the
returned transfer. The two-item return assignment is
particles_out, mass_transfer = condensation_step_gpu(...);
energy_transfer is caller-owned output, not a third return value. Disabled
(box, species) entries in the
per-box (n_boxes, n_species) partitioning mask and inactive particle slots
are zeroed before P2 finalization or reductions. CPU↔Warp conversion expands
the CPU shared species mask to this per-box layout. to_per_box_partitioning()
is the public migration helper for legacy one-dimensional masks. CPU restoration
rejects masks that differ by box and rejects zero-box mirrors because no box can
authoritatively supply the shared CPU mask.
P2 demand, release, scale, and accumulator sidecars are validated and used for
finalization; aggregate invalid state, metadata, or ownership fails with
ValueError before mutable physics or caller-state mutation. Preflight may run
read-only validation kernels. A later failure caused by a fresh raw proposal
does not roll back completed substeps, although it occurs before P2 mutation in
its failing substep. Unsupported activity or surface
strategies are not silently copied or approximated; passing
activity_surface=None retains legacy unit activity and static tension.
CondensationScratchBuffers is a concrete-module-only optional sidecar at
particula.gpu.kernels.condensation; it is not another step entry point.
Particle mass, transfer, and scratch transfer arrays are active-device
wp.float64 with shape (n_boxes, n_particles, n_species); gas concentration
and energy arrays are active-device wp.float64 with shape
(n_boxes, n_species); scratch property arrays are active-device wp.float64
with shape (n_boxes,); and latent heat and thermal work arrays are
active-device wp.float64 with shape (n_species,). Supplied
transfer fields must be active-device, stable-shape wp.float64 arrays with
shape (n_boxes, n_particles, n_species), and supplied property fields must be
active-device, stable-shape wp.float64 arrays with shape (n_boxes,).
The P2 demand, release, and scale sidecars must likewise be caller-owned,
active-device, stable-shape wp.float64 arrays with shape
(n_boxes, n_species). Every supplied field is metadata-validated for its
applicable shape, dtype, and active device before launch; each may be omitted
independently, in which case the step allocates only that field's fallback
storage. Supplied fields preserve identity and remain caller-owned, but
successful calls intentionally write their mutable work/output storage. No new
container field, sidecar API, host conversion, synchronization, or transfer is
implied by this ownership contract.
The direct condensation parity suite exercises all four activity/surface pairs
(ideal/static, ideal/composition-weighted, kappa/static, and
kappa/composition-weighted) against an independent NumPy float64 reference.
It uses deterministic one-box and multi-box inputs, including pure and mixed
particle compositions, a clamp-to-zero evaporation case, and constant plus
both Buck vapor-pressure branches. Parity checks cover raw transfer, final
particle mass, refreshed vapor pressure, finalized totals, and coupled gas
concentration. Tighter conservation checks separately verify the weighted
particle transfer equals the opposing gas change. These checks remain a bounded
direct-step contract, not a general CPU-strategy parity claim.
Latent-rate correction and energy diagnostic¶
The direct step accepts optional keyword-only caller-owned active-device
wp.float64 latent_heat with shape (n_species,). For species i, its
shipped latent-rate correction is
dm_i/dt = isothermal_rate_i / correction_i, where
isothermal_rate_i = k_i * Delta_p_i * M_i / (R * T),
R_specific_i = R / M_i, and
correction_i = thermal_factor_i / (R_specific_i * T). The source thermal
factor is
thermal_factor_i = (D_i * L_i * p_surface_i / (T * k_thermal)) * (L_i / (T * R_specific_i) - 1) + R_specific_i * T.
Here p_surface_i is the activity- and Kelvin-adjusted surface vapor pressure.
An omitted sidecar, or exactly L_i = 0, follows the exact isothermal branch.
Optional keyword-only energy_transfer is a caller-owned active-device
wp.float64, (n_boxes, n_species), write-only output. It requires valid
latent_heat, is overwritten only after successful preflight, and is not a
third return value. Its signed whole-call diagnostic, shipped in issue #1272,
is Q[box, species] = sum_particles(Delta m_applied) * L[species]: Delta m
is applied transfer in kg, L is latent heat in J/kg, and Q is J. Thus
condensation is positive and evaporation is negative. The strict #1272
energy-regression tolerance is rtol=1e-12, atol=1e-18.
thermal_work has the same validated sidecar shape but remains deferred and
unused: the step neither reads nor writes it. It does not evolve temperature or
add a Runnable, adaptive substeps, graph capture/replay, or autodiff.
Use to_warp_* and from_warp_* as the sole data CPU↔Warp boundary. There is
no high-level Aerosol or Runnable GPU path. CUDA preflight reads one-element
device validation flags with .numpy(); those status reads synchronize but do
not transfer or mutate caller-owned simulation buffers. Callers requiring retry
or rollback must snapshot and restore particle masses, gas concentration,
derived vapor pressure, and caller-owned output/work buffers. Deterministic
fp64 tests compare an independent NumPy reference with explicit parity
rtol/atol, then apply tighter separate ownership invariants. Warp cpu is
the baseline whenever Warp is installed; CUDA is optional, availability-guarded,
and separately marked. Warp-dependent test imports are lazy, so marker-
deselected collection does not require Warp. Focused direct-kernel and CPU
integration smoke commands are:
pytest particula/gpu/kernels/tests/condensation_test.py --collect-only -q -m "not warp"
pytest particula/gpu/kernels/tests/condensation_test.py -q -Werror
pytest particula/integration_tests/condensation_particle_resolved_test.py -q -Werror
Run CUDA-marked kernel cases separately only on CUDA-capable hosts; unavailable Warp or CUDA remains an expected guarded skip, not evidence of a fallback path.
Current shipped support boundaries¶
The containers are multi-box capable, but current execution support is narrower than storage support.
| Area | Shipped support | Notes |
|---|---|---|
CPU ParticleData / GasData storage |
Multi-box-capable | Leading n_boxes axis is part of the stored schema. |
| CPU condensation with data containers | n_boxes == 1 only |
Multi-box CPU execution still requires a caller-managed per-box loop. |
| CPU coagulation with data containers | n_boxes == 1 only |
Multi-box CPU execution is not yet a built-in runtime path. |
| CPU↔GPU transfer | Explicit helper calls only | No hidden container movement or hidden environment synchronization. |
| Warp/CUDA support | Optional | Warp device="cpu" is the baseline when Warp is installed; CUDA is additive local evidence and unavailable devices skip cleanly. |
| Low-level GPU condensation direct-kernel path | Shipped bounded direct-kernel contract | Executes four fixed coupled substeps with active-device P2 inventory and gas coupling. This is direct-kernel evidence, not broad GPU-condensation support. |
| Low-level GPU coagulation direct-kernel path | Direct, particle-resolved direct-kernel contract | Exact mask and rejection boundaries follow below. This path establishes no Runnable support, CPU parity, or performance claim. |
| Fixed-shape GPU/runtime roadmap work | Not current runtime behavior | Graph-capture-oriented and fixed-shape runtime constraints remain roadmap handoff material, not shipped behavior. |
Additional shipped boundaries:
- Executable coagulation masks are singletons
1,2,4,8; unordered pairs3,5,6,9,10,12; and four-way mask15. Tuples normalize to canonical order. Non-turbulent three-way mask7rejects at capability preflight before particle metadata or enabled-term validation. Turbulent three-way masks11,13,14proceed through particle metadata and enabled-term validation, then reject before downstream normalization, allocation, RNG setup, kernel launch, or mutation. Turbulent masks require explicit turbulent inputs. - Supplied coagulation particle state, collision outputs, and persistent RNG are
caller-owned same-device Warp resources. Persistent RNG reuse has no implicit
transfer or synchronization; omitted state allocates call-local storage, and
initialize_rng=Trueexplicitly resets the caller-owned buffer. ParticleData.volumeremains the authoritative per-box simulation-volume owner; it does not move intoEnvironmentData.EnvironmentDatais the shipped CPU thermodynamic owner fortemperature,pressure, andsaturation_ratio.WarpGasData.vapor_pressureis helper state only and has no CPUGasDatafield.- Coagulation
mechanism_configis host metadata, while suppliedcollision_pairs,n_collisions, andrng_statesare caller-owned same-device Warp sidecars. None are fields onParticleData,GasData,EnvironmentData, or any Warp container schema. - Omitted coagulation collision outputs and RNG state are call-local convenience
allocations. Supplied RNG state advances in place and resets only with
initialize_rng=True. - Kernels and runnables do not perform hidden simulation-state transfers.
Explicitly supplied step and output buffers do mutate in place: particle
masses, gas concentration, scratch transfer fields, total transfer, and
energy output retain their documented write semantics.
Callers remain responsible for synchronization before
host observation or restoration.
Validation
.numpy()readbacks at their documented preflight and per-substep boundaries may synchronize without transferring simulation state. CUDA preflight validation-flag readbacks may synchronize without transferring simulation state. - Direct condensation does not add a high-level
Aerosol/Runnablepath, automatic backend selection or fallback, implicit transfer or synchronization, adaptive stepping, new container fields, kernels, or physics, BAT, or staggered/Gauss-Seidel support. It also does not claim general CPU-strategy parity or broader unproven physics support.
Guidance for current users¶
- Use package-level imports for public containers and helpers.
- Preserve the leading box axis in all per-box arrays, even for single-box workflows.
- Preserve ordered gas names outside GPU containers if you need a semantic gas round trip.
- Preserve or recompute vapor pressure separately on the CPU side after GPU restore.
- Use the current low-level GPU coagulation path only within its documented direct-kernel workflow on a Warp-supported device.
- The current low-level GPU coagulation path remains bounded direct-kernel implementation scope; it does not establish a general production-support claim beyond its documented helper boundary and supported inputs.
- Do not expect kernels or runnables to perform hidden CPU↔GPU transfers for particle, gas, or environment state; use the explicit helper calls when state must cross the device boundary. Read-only device validation may synchronize and read back status to surface invalid values.
- Treat Warp and CUDA as optional runtime capabilities: without Warp, this
low-level GPU path is unavailable. Warp
device="cpu"is the baseline; CUDA support is optional and tests skip cleanly when CUDA is unavailable. - Treat roadmap pages as future-work references, not as evidence that broader runtime support has already shipped.
Direct-kernel troubleshooting¶
Use
docs/Examples/gpu_direct_kernels_quick_start.py
as the canonical runnable reference when troubleshooting the low-level GPU
path.
- Warp missing (
WARP_AVAILABLE == False) - The direct-kernel path is unavailable until Warp is installed.
- The canonical quick-start keeps
particula.gpu.kernelsimports deferred and completes in a CPU-only documentation mode without importing kernel steps or pretending kernels ran. - CUDA unavailable
- CUDA is optional.
- The default supported runnable path is Warp
device="cpu"; only opt intodevice="cuda"when a CUDA device is actually available. - Explicit CPU↔GPU transfer boundary
- Kernels operate on Warp-backed container mirrors, not CPU
ParticleData,GasData, orEnvironmentDataobjects directly. - Move simulation state across the boundary explicitly with
to_warp_*andfrom_warp_*; kernels and runnables do not perform hidden simulation-state transfers or top-level fallback imports. Read-only device validation may synchronize and read back status to surface invalid values, including on CUDA. - Device mismatch across particle, gas, environment, and sidecar buffers
- Keep Warp arrays and sidecar buffers such as
rng_stateson the same device as the particle/gas/environment inputs used by the kernel call. - Treat device-mismatch
ValueErrorfailures as input validation, not as a signal that Particula will migrate arrays automatically. - Condensation scratch buffers
- Preserve leading
(n_boxes, ...)layouts. Keep suppliedCondensationScratchBuffersfields on the active device with their required stablewp.float64shapes: transfer fields use(n_boxes, n_particles, n_species)and property fields use(n_boxes,). Only omitted fields allocate fallback storage independently. - Supplied fields preserve caller ownership and identity but are mutable work/output storage; do not expect a successful step to leave them unmodified.
- Mixed
environment=plus scalartemperature/pressureinputs - Pass either
environment=or direct temperature/pressure inputs. - Do not mix scalar or Warp-array
temperature/pressurevalues withenvironment=; current kernels reject that combination explicitly. - When
environment=is absent, provide both direct inputs. They must be positive finite physical values and any direct Warp arrays must be compatible with the active device. - Species metadata and thermodynamics sidecars
- Preserve ordered CPU gas-name metadata when restoring
GasData, and keep thermodynamics-sidecar species order aligned withgas.molar_mass. - Confirm the configured water-species index addresses a valid species.
- P2 transfer and energy diagnostics
- P2 inventory-limited applied transfers are expected bounded behavior, not CPU-strategy or runnable parity evidence.
energy_transferis caller-owned whole-call P2-finalized output. Explicitly synchronize before observing it on the host.- Gas/environment restore expectations
from_warp_gas_data()is intentionally lossy across the helper boundary: it restores ordered species names only when you supply them; otherwise it generates placeholder names such asspecies_0.- GPU-only helper state such as
vapor_pressureis not restored onto CPUGasData. from_warp_environment_data(..., sync=False)is an explicit expert path; manual synchronization remains the caller's responsibility before NumPy access.
Focused reproduction commands¶
These focused commands provide distinct evidence; none establishes either of the other evidence classes.
| Command | Evidence |
|---|---|
python docs/Examples/gpu_direct_kernels_quick_start.py |
Canonical explicit-transfer walkthrough. |
pytest particula/gpu/tests/gpu_direct_kernels_example_test.py -q |
Quick-start regression. |
pytest particula/gpu/kernels/tests/condensation_test.py -q -Werror |
Primary direct CPU-oracle particle-mass/gas-concentration parity matrix. |
pytest particula/gpu/kernels/tests/condensation_stiffness_test.py -q -Werror |
Bounded direct-step stiffness coverage. |
pytest particula/gpu/dynamics/tests/coagulation_funcs_test.py -q -Werror |
Deterministic GPU coagulation pair-helper parity. |
pytest particula/gpu/kernels/tests/coagulation_test.py -q -Werror |
Direct coagulation coverage, including singleton sedimentation and ST1956 configurations, direct/environment inputs, caller-owned output/RNG behavior, conservation, and rejected-call state safety. |
pytest particula/gpu/kernels/tests/coagulation_validation_test.py -q -m "warp and gpu_parity" -Werror |
Fixed-mask deterministic/ownership evidence. |
pytest particula/gpu/kernels/tests/coagulation_stochastic_validation_test.py -q -m "warp and stochastic and not cuda" -Werror |
Stochastic evidence. |
pytest particula/tests/gpu_coagulation_docs_test.py -q -Werror |
Hardware-free documentation-contract coverage. |
pytest particula/integration_tests/condensation_latent_heat_conservation_test.py -q |
CPU integration/inventory-conservation evidence (separate particle-plus-gas inventory conservation checks); not direct-GPU validation. |
pytest particula/integration_tests/condensation_particle_resolved_test.py -q |
CPU integration evidence for particle-resolved condensation; not direct-GPU validation. |
pytest particula/tests/condensation_latent_heat_docs_test.py -q -Werror |
Latent-heat energy/bookkeeping documentation checks. |
The required baseline is Warp device="cpu" when Warp is installed. The
parity matrix, inventory conservation checks, and latent-heat energy/bookkeeping
checks are separate: no one class proves either of the others.
Optional/local CUDA evidence:
pytest particula/gpu/kernels/tests/condensation_test.py -q -m "warp and cuda" -Werror
pytest particula/gpu/kernels/tests/coagulation_stochastic_validation_test.py -q -m "warp and cuda" -Werror
This is additive to the required Warp device="cpu" baseline and skips
cleanly when CUDA is unavailable.
The GPU condensation parity walkthrough
and its condensation parity walkthrough ownership record
document bounded physics, conservation, and energy evidence only. They retain
the fixed-four-substep low-level direct-kernel boundary: Warp CPU is the
installed-Warp baseline and CUDA is optional additive evidence, not required
support. energy_transfer remains caller-owned, write-only diagnostic output,
not a return value or temperature feedback mechanism (kg * J/kg = J).
Related references¶
The fixed-four-substep low-level direct-kernel walkthrough can be run with
python docs/Examples/gpu_condensation_parity_walkthrough.py. Its focused
regressions are
pytest particula/gpu/tests/gpu_condensation_parity_walkthrough_test.py -q -Werror
and
pytest particula/tests/condensation_parity_walkthrough_docs_test.py -q -Werror.