Skip to content

Native aerosol construction and replacement

This guide describes native construction used by the supported migrated examples. Native CPU processes and runnables consume these held containers; see dynamics migration for configuration and single-box limits. The access walkthrough demonstrates executable replacement and rejection checks. Use the v0.3.0 development checkout, not an assumed older published package. See also container foundations.

Assemble three containers

import particula as par

particles = par.particles.create_resolved_particle_data(
    masses=[[0.5e-18, 1.5e-18], [2e-18, 3e-18]],
    density=[1000.0, 1200.0],
    concentration=[8.0, 0.5],  # represented counts, including fractional weights
    volume=0.25,  # physical m^3
)
gas = (
    par.gas.GasDataBuilder()
    .set_names(["water", "inert", "organic"])
    .set_molar_mass([0.018, 0.028, 0.1])
    .set_partitioning([True, False, True])
    .set_concentration([1e-8, 7e-8, 3e-8])  # kg/m^3, in full gas order
    .build()
)
environment = par.gas.EnvironmentData(
    temperature=[298.15], pressure=[101325.0],
    saturation_ratio=[[0.7, 7.0, 1.3]],
)
aerosol = par.Aerosol(
    particles=particles, gas=gas, environment=environment,
)
assert aerosol.particles is particles

# Equivalent fluent construction; repeated builds share supplied containers.
builder = (par.AerosolBuilder().set_particles(particles)
           .set_gas(gas).set_environment(environment))
second = builder.build()
assert second.gas is gas

The constructor is keyword-only. All three builder inputs are required; no containers or physics configuration are synthesized. Builder setters retain references, and build() validates through the constructor. Optional *_units builder-protocol arguments are ignored with a log warning because these are already constructed containers, not quantities to convert.

Admission first checks container types, array structure, compatible box counts, unique/nonblank gas names, and full gas/environment width; it then calls ParticleData.validate_representation() for the explicit kind, float64 schema, finite/nonnegative particle values, material density, volume and PDF grid. All-false gas masks and unequal particle/gas species counts are valid. No gas lane is dropped. Chemical mapping and gas/environment ordering provenance stay process-owned: unnamed same-width permutations cannot be detected here.

Replace state without partial publication

# Compatible individual replacements retain candidate identity.
aerosol.particles = particles.copy()
aerosol.gas = gas.copy()
aerosol.environment = environment.copy()

# All three arguments are required, including for a coordinated layout change.
aerosol.replace_data(
    particles=particles, gas=gas, environment=environment,
)

Each operation validates the complete proposed triple before publishing any references, including same-object assignments. Invalid candidates leave both old and candidate state unchanged. Coordinated replacement can change box or gas/environment widths when individual replacements would be incompatible. It returns None; it selects caller-supplied state and does not rescale it to conserve an old inventory. Validation atomicity is not a threading guarantee.

Reads return the actual held objects. Raw arrays remain writable, so direct mutations are immediately visible and require subsequent validation before interpretation. Callers request detached data explicitly through container copy() methods. Derived particle population properties compute fresh values. Replacing CPU state does not refresh/rebind prepared or resident GPU state; its owners retain their original bindings and must explicitly recreate or reprepare them through their existing APIs.

Initialize particles without facades

All four functions are exported from particula.particles and return owned, writable float64 arrays. They use the existing ParticleDataBuilder for final admission. Existing native builder ownership and storage units are unchanged.

Function Inputs and storage
create_particle_data_from_radii Single material, positive radii and scalar/per-node density weights; explicit discrete or continuous_pdf; mass = 4πρr³/3
create_lognormal_binned_particle_data Existing lognormal PDF/PMF generators; explicit kind; scalar/per-node charge
create_resolved_particle_data Scalar or (N,) single-species mass, (N,S) or (B,N,S) mass; explicit density, optional counts, charge and physical volume
create_lognormal_resolved_particle_data Existing sampled mixture, single material, one count per sampled slot

Direct mass/species construction also remains available through ParticleDataBuilder; there is no need for a facade or strategy selection.

Distribution and volume rules

Kind Stored concentration Volume Bulk calculation
discrete Number per bin per m³ Exactly 1 m³ Sum weights or mass × weights
continuous_pdf Radius dn/dr, m⁻⁴ Exactly 1 m³ Trapezoids over explicit radius; integrate mass × PDF together
particle_resolved Represented counts Positive finite physical volume Sum counts or mass × counts, divided by volume for density

PDF nodes are positive, finite, strictly increasing metres with at least two nodes. Integration uses the supplied interval without extrapolation. Zero PDFs retain their grid. Empty discrete/resolved populations are supported; sampled presets require a positive integer sample count. Non-unit binned volumes reject. At initialization only, non-SI reference-volume conversion roundoff within four float64 ulps of one is canonicalized to exactly one; direct SI admission stays exact. Canonical SI units work without Pint. Other unit spellings and conversions use the existing optional Pint conversion utility.

For resolved inputs, concentration_units="count" is the default and does not rescale supplied weights. Explicit "1/m^3" or "1/cm^3" inputs multiply the converted density by each box's volume once. Omitting concentration creates unit counts and requires count units. For eight represented particles, densities at V=0.25, 1 and 4 m³ are 32, 8 and 2 m⁻³; their extensive particle mass is unchanged. Fractional weights are valid storage but do not imply support by every process.

Defaults, shapes and sampling

Binned defaults are logspace(-9, -4, 250) metre nodes, modal scale radii [100e-9, 1e-6] metres, GSD [1.2, 1.4], modal densities [1e10, 1e9] m⁻³, material density 1000 kg/m³, and zero charge. Supplied nodes/modes use radius_units; omitted defaults remain SI. GSD must exceed one, because the existing finite-width lognormal generator does not represent a delta function. The existing PDF and PMF algorithms retain their normalization on the supplied grid. Lognormal discrete PMFs require at least three positive, strictly increasing nodes because the retained converter extrapolates its last bin width from two intervals. Lognormal PDFs require two nodes. Direct radius initialization still accepts two discrete bins with caller-supplied weights. Binned kind is always explicit; legacy pdf/pmf aliases are not used.

Sampled defaults preserve 20,000 samples, volume 1 m³, and the same modal and material defaults. Modal densities control relative sampling weights only. Samples retain unit counts, so physical number density is sample count / volume, not the sum of the modal input densities. The existing NumPy/SciPy global random state is used; no new seed API or cross-backend replay guarantee is provided. Deterministic input validation happens before sampling. Modal densities must have a finite total: individually finite inputs such as [1e308, 1e308] reject with ValueError before the sampler runs or global random state changes.

Resolved density is per species; charge is per particle. Scalar and singleton forms broadcast. Redundant density population rows or charge species lanes may reduce only when every discarded value is identical to the retained value. Differing rows/lanes reject. Canonical (B,N) charge shape takes precedence; no values are discarded in that form. Radius helpers require a single material; use explicit species masses for mixtures. Inputs are never modified.

Explicit execution boundaries

Native discrete and resolved particle uploads preserve their tag and raw concentration values. Radius-PDF upload is unsupported; never strip metadata to bypass validation. Gas names remain externally owned, and native condensation and nucleation require explicit exact-carrier species bindings. Direct and resident examples show their concrete preparation APIs and synchronization.

A CPU aggregate replacement cannot rebind existing device arrays or a prepared resident request. Explicit restart uses a fresh same-device binding and schema-v5 canonical checkpoint bytes; CPU inspection is not continuation authority. There is no implicit transfer, fallback, migration or recovery. Batched storage does not broaden CPU execution beyond supported single-box process contracts.

Legacy facades and conversion bridges have been removed from this development tree. CPU-Warp population transfers and resident construction require explicit supported distribution metadata; untagged checkpoint records reject before restart setup. V0.3.0 final validation and review remain separate from release publication.