Coagulation Strategy System
July 28, 2026 · View on GitHub
Complete direct-process context
The direct coagulation call is part of the illustrative explicit-transfer sequence, with private P2 evidence and a P3 regression. Persistent RNG remains caller-owned; the example is not a backend selector, scheduler, Runnable, resident loop, or CPU fallback.
Strategy-based coagulation kernels with unified builders, factory selection, runnable integration, and particle-resolved support.
Overview
The coagulation strategy system lets you model particle collisions using the same object-oriented patterns as condensation and wall loss. Instead of calling standalone kernel functions, you work with strategy objects that operate on ParticleRepresentation, support multiple distribution types, and expose a consistent kernel / rate / step interface. Strategies cover Brownian motion, electrostatic enhancement, turbulence (shear and DNS), gravitational sedimentation, and user-defined combinations.
This feature is built around user-facing APIs exposed via particula.dynamics:
CoagulationStrategyABC– abstract base class defining kernel, gain/loss/net rate, andstepfor discrete, continuous, or particle-resolved distributions.BrownianCoagulationStrategy,ChargedCoagulationStrategy,TurbulentShearCoagulationStrategy,TurbulentDNSCoagulationStrategy,SedimentationCoagulationStrategy– concrete physical models.CombineCoagulationStrategy– sums multiple kernels of the same distribution type for combined physics.- Builders for each strategy plus mixins for distribution type, turbulent dissipation, fluid density, and charged-kernel validation.
CoagulationFactory– factory for selecting a coagulation strategy by name with builder defaults.Coagulationrunnable – delegates to a coagulation strategy, splitstime_stepacrosssub_steps, and composes with other runnables using the|operator.
Key Benefits
- Consistent dynamics workflow: Same strategy-based API (
kernel,rate,step,distribution_type) and runnable integration as condensation and wall loss. - Builder/factory parity with validation: Fluent builders enforce distribution type, turbulent parameters, fluid density, and charged kernel strategy selection with optional unit conversion.
- Coverage of major coagulation regimes: Brownian, charged/electrostatic, turbulent shear, turbulent DNS, and sedimentation kernels plus additive combinations.
- Distribution-type flexibility: Works with "discrete", "continuous_pdf", and "particle_resolved" modes so you can reuse existing particle builders.
- Pipeline ready:
Coagulationrunnable usessub_steps, clamps through strategy logic, and chains with other runnables via|for end-to-end simulations.
Who It's For
This feature is designed for:
- Chamber and box-model users: Running time-dependent simulations where coagulation competes with condensation, wall loss, dilution, or emissions.
- Particle-resolved modelers: Requiring discrete collision pairs, kernel radius binning, and stochastic pair selection.
- Turbulence and charged-aerosol researchers: Exploring electrostatic enhancement or turbulence-driven collision rates without re-implementing kernels.
- Model developers: Extending with new kernels while reusing validation, distribution handling, and runnable plumbing.
Capabilities
Unified coagulation API in particula.dynamics
Coagulation is exposed alongside other dynamics components:
import particula as par
# Abstract interface
par.dynamics.CoagulationStrategyABC
# Concrete strategies
par.dynamics.BrownianCoagulationStrategy
par.dynamics.ChargedCoagulationStrategy
par.dynamics.TurbulentShearCoagulationStrategy
par.dynamics.TurbulentDNSCoagulationStrategy
par.dynamics.SedimentationCoagulationStrategy
# Combination and factory
par.dynamics.CombineCoagulationStrategy
par.dynamics.CoagulationFactory
All strategies share a common shape:
- Initialize with physical parameters (e.g., turbulent dissipation, fluid density, charged kernel strategy) and
distribution_type. - Call
kernel(particle, temperature, pressure)to compute the dimensional kernel [m^3/s]. - Call
net_rate(...),loss_rate(...), orgain_rate(...)as needed. - Call
step(particle, temperature, pressure, time_step)to advance the system.
Runnable entry point: Coagulation
Coagulation is a RunnableABC exported as par.dynamics.Coagulation. It operates on an Aerosol, delegates step to the provided coagulation strategy, splits time_step across any sub_steps, and composes with other runnables using |:
import particula as par
aerosol = ... # your Aerosol instance
coagulation = par.dynamics.Coagulation(
coagulation_strategy=par.dynamics.BrownianCoagulationStrategy(
distribution_type="discrete",
),
)
# Sub-steps split time_step internally
updated = coagulation.execute(
aerosol,
time_step=60.0,
sub_steps=4,
)
# Chain with wall loss or condensation in one pipeline
wall_loss = par.dynamics.WallLoss(
wall_loss_strategy=par.dynamics.SphericalWallLossStrategy(
wall_eddy_diffusivity=1e-3,
chamber_radius=0.5,
distribution_type="discrete",
),
)
pipeline = coagulation | wall_loss
updated = pipeline.execute(aerosol, time_step=60.0)
Brownian coagulation strategy
BrownianCoagulationStrategy implements the classical Brownian kernel using get_brownian_kernel_via_system_state. It works across discrete, continuous, and particle-resolved distributions.
Typical initialization:
brownian = par.dynamics.BrownianCoagulationStrategy(
distribution_type="discrete",
)
Charged coagulation with kernel strategies
ChargedCoagulationStrategy wraps electrostatic kernel strategies (e.g., HardSphereKernelStrategy, Coulomb* variants). Provide a ChargedKernelStrategyABC instance:
charged_kernel = par.dynamics.HardSphereKernelStrategy()
charged = par.dynamics.ChargedCoagulationStrategy(
distribution_type="discrete",
kernel_strategy=charged_kernel,
)
Use the builder for validation and readability:
charged = (
par.dynamics.ChargedCoagulationBuilder()
.set_distribution_type("discrete")
.set_charged_kernel_strategy(par.dynamics.HardSphereKernelStrategy())
.build()
)
Turbulent shear coagulation
TurbulentShearCoagulationStrategy uses the Saffman–Turner (1956) shear kernel. Requires turbulent dissipation [m^2/s^3] and fluid density [kg/m^3]:
turb_shear = par.dynamics.TurbulentShearCoagulationStrategy(
distribution_type="continuous_pdf",
turbulent_dissipation=1e-3,
fluid_density=1.225,
)
Builder example with unit conversion and validation:
turb_shear = (
par.dynamics.TurbulentShearCoagulationBuilder()
.set_distribution_type("continuous_pdf")
.set_turbulent_dissipation(1e-3, "m^2/s^3")
.set_fluid_density(1.225, "kg/m^3")
.build()
)
Turbulent DNS coagulation
TurbulentDNSCoagulationStrategy follows Ayala et al. (2008) DNS fits. Requires turbulent dissipation, fluid density, Reynolds lambda (dimensionless), and relative velocity [m/s]:
turb_dns = par.dynamics.TurbulentDNSCoagulationStrategy(
distribution_type="discrete",
turbulent_dissipation=0.01,
fluid_density=1.225,
reynolds_lambda=74.0,
relative_velocity=0.5,
)
Factory configuration dict example (via CoagulationFactory):
factory = par.dynamics.CoagulationFactory()
turb_dns = factory.get_strategy(
strategy_type="turbulent_dns",
parameters={
"distribution_type": "discrete",
"turbulent_dissipation": 1e-2, # m^2/s^3
"fluid_density": 1.225, # kg/m^3
"reynolds_lambda": 150.0, # dimensionless
"relative_velocity": 0.6, # m/s
},
)
Sedimentation coagulation
SedimentationCoagulationStrategy models gravitational-settling-driven collisions (Seinfeld & Pandis, 2016) and works across supported distribution types. No extra parameters beyond distribution_type are required.
Combined coagulation strategies
CombineCoagulationStrategy sums kernels from multiple strategies (distribution type must match). Example: Brownian + turbulent shear:
combined = par.dynamics.CombineCoagulationStrategy(
strategies=[
par.dynamics.BrownianCoagulationStrategy(distribution_type="discrete"),
par.dynamics.TurbulentShearCoagulationStrategy(
distribution_type="discrete",
turbulent_dissipation=1e-3,
fluid_density=1.225,
),
]
)
Support for multiple distribution types
The strategy system operates on the same distribution types used elsewhere in particula:
- "discrete" – radius-binned distributions.
- "continuous_pdf" – continuous probability-density representations.
- "particle_resolved" – ensembles of individual particles with stochastic pair selection.
Select the appropriate mode at initialization (or via builders/factory). Particle-resolved paths use kernel-radius binning and collide_pairs updates; discrete/continuous paths apply gain/loss rate updates.
Particle-resolved coagulation
CoagulationStrategyABC.step handles particle-resolved updates by mapping to a kernel radius grid, drawing collision pairs, and updating via collide_pairs:
particle_resolved = par.dynamics.BrownianCoagulationStrategy(
distribution_type="particle_resolved",
)
aerosol.particles = particle_resolved.step(
particle=aerosol.particles,
temperature=298.15,
pressure=101325.0,
time_step=1.0,
)
Builder and factory workflow
Builders provide validated, unit-aware construction; the factory selects a strategy by name using those builders under the hood:
import particula as par
# Brownian via builder
brownian = (
par.dynamics.BrownianCoagulationBuilder()
.set_distribution_type("discrete")
.build()
)
# Charged via builder with kernel strategy
charged = (
par.dynamics.ChargedCoagulationBuilder()
.set_distribution_type("continuous_pdf")
.set_charged_kernel_strategy(par.dynamics.HardSphereKernelStrategy())
.build()
)
# Factory selection (combine)
factory = par.dynamics.CoagulationFactory()
combined = factory.get_strategy(
strategy_type="combine",
parameters={
"strategies": [brownian, charged],
"distribution_type": "continuous_pdf",
},
)
Getting Started
Quick start: Brownian coagulation on a discrete distribution
import particula as par
# 1. Build a radius-binned particle distribution
particle = par.particles.PresetParticleRadiusBuilder().build()
# 2. Configure Brownian coagulation
brownian = par.dynamics.BrownianCoagulationStrategy(
distribution_type="discrete",
)
# 3. Compute instantaneous kernel and rates
T = 298.15 # K
P = 101325.0 # Pa
kernel = brownian.kernel(particle, temperature=T, pressure=P)
net = brownian.net_rate(particle, temperature=T, pressure=P)
# 4. Advance by one time step
particle = brownian.step(
particle=particle,
temperature=T,
pressure=P,
time_step=10.0, # s
)
Prerequisites
particulaversion 0.2.6 or later installed.- A
ParticleRepresentationinstance (e.g., from preset particle builders). - Basic familiarity with particula dynamics and the runnable pipeline (
|).
Typical Workflows
1. Build a ParticleRepresentation
Use preset builders or custom particle-resolved constructors:
particle = (
par.particles.PresetParticleRadiusBuilder()
.set_volume(1.0, "m^3")
.build()
)
2. Configure a coagulation strategy
Pick a physical model and distribution type:
coag_strategy = par.dynamics.TurbulentShearCoagulationStrategy(
distribution_type="discrete",
turbulent_dissipation=5e-4,
fluid_density=1.2,
)
3. Run through the Coagulation runnable (with sub_steps)
coagulation = par.dynamics.Coagulation(coagulation_strategy=coag_strategy)
updated = coagulation.execute(
aerosol,
time_step=120.0,
sub_steps=3,
)
4. Compose with other dynamics in a single pipeline
Order processes explicitly using the | operator:
condensation = par.dynamics.MassCondensation(
condensation_strategy=par.dynamics.CondensationIsothermal(
molar_mass=0.18,
diffusion_coefficient=2e-5,
accommodation_coefficient=1.0,
),
)
wall_loss = par.dynamics.WallLoss(
wall_loss_strategy=par.dynamics.SphericalWallLossStrategy(
wall_eddy_diffusivity=1e-3,
chamber_radius=0.5,
distribution_type="discrete",
),
)
pipeline = condensation | coagulation | wall_loss
updated = pipeline.execute(aerosol, time_step=60.0, sub_steps=2)
5. Builder/factory workflow for reproducibility
factory = par.dynamics.CoagulationFactory()
coag_strategy = factory.get_strategy(
strategy_type="brownian",
parameters={"distribution_type": "continuous_pdf"},
)
coagulation = par.dynamics.Coagulation(coagulation_strategy=coag_strategy)
6. Particle-resolved collision loop
particle_resolved = par.dynamics.BrownianCoagulationStrategy(
distribution_type="particle_resolved",
)
aerosol.particles = particle_resolved.step(
particle=aerosol.particles,
temperature=300.0,
pressure=101325.0,
time_step=0.5,
)
Use Cases
Use case 1: Brownian-only chamber decay
Scenario: You need size-dependent Brownian coagulation over a 10-minute chamber experiment.
Solution: Use BrownianCoagulationStrategy with Coagulation runnable and sub_steps to maintain stability at small time_step slices.
Use case 2: Charged aerosol aging
Scenario: You are studying charge-enhanced coagulation (e.g., ion-induced charging or corona discharge) and want to compare kernel strategies.
Solution: Instantiate ChargedCoagulationStrategy with HardSphereKernelStrategy (or other Coulomb models) and sweep voltages/charges externally while keeping the same runnable pipeline.
Use case 3: Turbulent industrial flow
Scenario: You want turbulence-driven coagulation rates for an industrial duct with known dissipation and Reynolds lambda.
Solution: Configure TurbulentDNSCoagulationStrategy via CoagulationFactory using measured turbulent_dissipation, fluid_density, reynolds_lambda, and relative_velocity; run through Coagulation with other dynamics chained.
Configuration
| Option | Description | Default / Units | Applies To |
|---|---|---|---|
distribution_type | "discrete", "continuous_pdf", or "particle_resolved" | Required | All strategies & builders |
charged_kernel_strategy | Electrostatic kernel (e.g., HardSphereKernelStrategy) | Required | Charged |
turbulent_dissipation | Turbulent energy dissipation rate | Required, m^2/s^3 | Turbulent shear, Turbulent DNS |
fluid_density | Fluid density for the medium | Required, kg/m^3 | Turbulent shear, Turbulent DNS |
reynolds_lambda | Taylor-scale Reynolds number | Required | Turbulent DNS |
relative_velocity | Relative particle-flow velocity | Required, m/s | Turbulent DNS |
strategies | List of strategies to sum | Required | Combine |
particle_resolved_kernel_radius | Kernel-radius grid (optional) | Optional | Particle-resolved modes |
particle_resolved_kernel_bins_number | Number of kernel bins | Optional | Particle-resolved modes |
particle_resolved_kernel_bins_per_decade | Bins per radius decade | 10 | Particle-resolved modes |
Best Practices
- Match distribution type to particle builder: Keep
distribution_typeconsistent with howParticleRepresentationwas constructed to avoid unintended kernel application. - Use builders/factory for validation: Let builders enforce turbulence units, fluid density, and charged-kernel selection; avoid manual instantiation when sharing configurations.
- Start with smaller
time_stepand usesub_steps: Especially for particle-resolved or highly turbulent cases to reduce overshoot in collisions. - Combine kernels deliberately: Use
CombineCoagulationStrategywhen you need additive physics (e.g., Brownian + turbulent shear); ensure all strategies share the same distribution type. - Keep parameters physical: Validate dissipation rates, Reynolds lambda, and relative velocities against your experiment or CFD results before running long simulations.
Limitations
- Dimensionless kernels are not implemented for combined, turbulent, or sedimentation strategies; use dimensional forms via
kernel. - All strategies in
CombineCoagulationStrategymust share the samedistribution_type. - No built-in high-level orchestrator; you control the time loop and pipeline ordering.
- DNS parameterization assumes applicability of Ayala et al. (2008) fits; verify for extreme regimes.
GPU direct-kernel foundations and limitations
This direct-kernel-only path is separate from CPU strategies, builders,
factories, and Runnable APIs. Import its direct step and immutable host
configuration with:
from particula.gpu.kernels import coagulation_step_gpu
from particula.gpu.kernels.coagulation import CoagulationMechanismConfig
It accepts only distribution_type="particle_resolved". The identifiers are
brownian, charged_hard_sphere, sedimentation_sp2016, and
turbulent_shear_st1956; requested tuples normalize to Brownian, charged,
sedimentation, then turbulent order. Executable masks are singleton masks 1,
2, 4, 8; two-term masks 3, 5, 6, 9, 10, 12; and four-term
mask 15. Three-term masks 7, 11, 13, and 14 are deferred and fail
closed. Mask 7 rejects before particle metadata or enabled-term validation;
masks 11, 13, and 14 validate metadata and enabled terms before failing.
Every turbulent mask requires keyword-only positive finite
turbulent_dissipation and fluid_density: either scalars or same-device
wp.float64 arrays with shape (n_boxes,). Non-turbulent masks ignore these
inputs. Particle data, including same-device fp64 charge, supplied collision
pair/count outputs, and persistent RNG state are caller-owned Warp resources.
Accepted collisions mutate particle mass, concentration, and charge in place;
supplied outputs retain identity, persistent RNG advances in place and resets
only with initialize_rng=True, and omitted outputs/state are call-local. The
exact return tuple is (particles, collision_pairs, n_collisions); RNG state is
not returned.
Malformed configuration and unsupported distributions fail before particle
access. Same-device preflight validates caller state, but a later runtime
failure has no rollback guarantee. Transfers are explicit: there is no automatic
transfer, CPU fallback, or high-level Runnable integration. Warp CPU is the
installed-Warp baseline. CUDA is optional/additive, never mandatory, and guarded
suites skip cleanly when it is unavailable. This path provides no performance,
broad CPU/GPU parity, graph-capture, broad autodiff, DNS/general-turbulence
support, unsupported-mechanism, or unsupported-distribution claim. See the
canonical ownership contract
and evidence/command record.
Focused baseline commands:
pytest particula/gpu/kernels/tests/coagulation_validation_test.py -q -m "warp and gpu_parity" -Werror
pytest particula/gpu/kernels/tests/coagulation_stochastic_validation_test.py -q -m "warp and stochastic and not cuda" -Werror
pytest particula/tests/gpu_coagulation_docs_test.py -q -Werror
Optional/local CUDA evidence:
pytest particula/gpu/kernels/tests/coagulation_stochastic_validation_test.py -q -m "warp and cuda" -Werror
This optional/local command is additive and skips cleanly when CUDA is unavailable.
E7-F3 concrete selected-Brownian adapter
The concrete E7-F3 adapter is a separate selected route beside the
multi-mechanism direct-kernel contract above. It accepts an exact
BrownianCoagulationConfig marker only. The CPU reference route retains a
caller-owned Aerosol and exact Coagulation runnable. The resident-Warp
route retains caller-owned WarpParticleData. It supports Brownian
particle_resolved dispatch only. CPU and Warp stochastic trajectories have
no seed-by-seed parity claim.
Import the concrete-only marker, state carriers, and adapters from their concrete module:
from particula.execution.adapters.coagulation import (
BrownianCoagulationConfig,
CPUCoagulationExecutionAdapter,
CPUCoagulationExecutionState,
CPUCoagulationState,
WarpBrownianCoagulationExecutionAdapter,
WarpBrownianCoagulationExecutionState,
WarpBrownianCoagulationState,
)
Neither particula.execution nor the top-level particula package exports
these concrete names. The exact marker fails closed before selected dispatch
for charged, sedimentation, turbulent shear, combined, or unknown mechanisms,
non-particle_resolved requests, and marker subclasses. These are
selection-owned capability errors; native direct-kernel physical, device, and
schema errors retain their native behavior.
The Warp state accepts exactly one thermo form: provide both temperature and
pressure with environment=None, or provide temperature=None and
pressure=None with a WarpEnvironmentData. Optional volume is valid in
either form. Mixing an environment with either direct thermo input fails
adapter-owned form validation.
The caller retains the aerosol or resident particles, environment or direct
thermo/volume values, optional collision outputs, and required persistent
(n_boxes,) wp.uint32 RNG sidecar by identity. Supplied diagnostic outputs
have an adapter identity guarantee; omitted direct-kernel diagnostics remain
native call-local convenience results. Diagnostics do not imply a host
readback. Seed the sidecar on the first selected Warp dispatch with
initialize_rng=True; ordinary later dispatches reuse that identical sidecar
with initialize_rng=False. Only initialize_rng=True resets it, even when
rng_seed repeats.
Callers control conversion, restoration, device placement, and
wp.synchronize() before observing or resolving device state. The adapter
does not upload, restore, synchronize, select a fallback, reseed per step,
retry, create a session or scheduler, or roll back state after launch. E7-F4
resident-session lifecycle, E7-F5 scheduling, and E7-F8 checkpoint/restart
stream policy remain explicitly deferred.
For a runnable selected-Brownian introduction, see the
GPU coagulation example. It
keeps explicit conversion, synchronization, restoration, and caller-owned
collision/RNG sidecars; it is not a CPU fallback or a high-level Runnable
workflow. The direct-kernel evidence/command record above remains the complete
approved-mask evidence boundary.
Related Documentation
- Wall loss strategies: wall_loss_strategy_system.md
- Condensation strategies: condensation_strategy_system.md
- GPU direct example: gpu_coagulation_direct.py
- GPU roadmap: Data-Oriented Design and GPU Roadmap
FAQ
How do I choose between turbulent shear and turbulent DNS?
Use turbulent shear (TurbulentShearCoagulationStrategy) for Saffman–Turner regimes with moderate dissipation and when Reynolds lambda is not specified. Use turbulent DNS (TurbulentDNSCoagulationStrategy) when you have DNS-fit parameters (reynolds_lambda, relative_velocity) or want higher-Re effects.
Can I mix charged and turbulent effects?
Yes. Build charged and turbulent strategies with the same distribution_type, then sum them via CombineCoagulationStrategy (or factory strategy_type="combine") to add their kernels.
How does particle-resolved stepping differ from discrete bins?
Particle-resolved mode bins a kernel radius grid, draws collision pairs stochastically, and calls collide_pairs; discrete/continuous modes apply gain/loss rates directly to concentrations.