Particle Volumes and Adaptivity
Collocation particles carry no mass. To define a discrete conserved quantity, and to make inserting and merging particles exactly conservative, every particle gets a positive weight $V_i$ (stored in pg.core.volumes) with
\[\sum_i V_i = |\Omega|, \qquad \sum_i V_i\, f(x_i) \approx \int_\Omega f\,\mathrm dx .\]
No Voronoi tessellation is built or stored. $V_i = \int_\Omega \phi_i\,\mathrm dx$ for shape functions $\phi_i$ that form a partition of unity ($\sum_i \phi_i \equiv 1$), evaluated with a quadrature on the point cloud itself or on a throwaway lattice. Only the scalar $V_i$ is kept.
Volume estimators
| Estimator | Shape functions | Evaluation | Quadrature order | Cost |
|---|---|---|---|---|
ShepardVolumes(; sweeps = 1) | $\phi_i(x) = w(\lvert x - x_i\rvert) / \sum_k w(\lvert x - x_k\rvert)$ | particle-based, no lattice | 2 (quasi-uniform clouds) | one neighbour sweep per sweep |
MLSVolumes(P; samples_per_dx = 2) | MLS reproducing-kernel functions of degree $P$ | background lattice over $\Omega$ | $P + 1$ | lattice points × neighbours |
Shepard starts from the kernel-density volume $V_i \propto 1/S_i$, $S_i = w(0) + \sum_j w_{ij}$, applies sweeps partition-of-unity sweeps $V_i \leftarrow \sum_j (w_{ij}/S_j)\,V_j$ (which preserve $\sum V$), and renormalises to $|\Omega|$. On a uniform periodic cloud it reproduces $V_i = \Delta x^D$ exactly. It needs a kernel that is bounded at zero distance; with InverseWeightFunction the volumes fall back to $|\Omega|/N$ with a warning.
MLS evaluates, at every lattice point $y$, the shape functions $\phi_i(y) = w_i\, p_i^{\mathsf T} a$ with $\big(\sum_k w_k p_k p_k^{\mathsf T}\big) a = e_1$ and the full basis $p_k = (1, \text{monomials}(x_k - y))$. Because $\sum_i \phi_i(y)\, q(x_i) = q(y)$ for polynomials $q$ of degree $\le P$, the volumes integrate such polynomials exactly (up to the lattice's midpoint error). P = 0 is the lattice-integrated Shepard partition. Shape functions of degree $\ge 1$ can be negative; a volume that comes out negative is clamped before the final renormalisation.
pg = ParticleGrid(geom, dx, wf, M; volume_estimator = MLSVolumes(2)) # at construction
update_volumes!(pg, ShepardVolumes()) # any later time
total_mass(pg) # Σ V_i U_i
domain_volume(pg) # |Ω|domain_volume is analytic for the built-in rectangular and spherical domains; define a method for a custom geometry label.
Volumes on moving grids
After every step of a moving grid, the time stepper recomputes the volumes from the new geometry with its volume_estimator (default ShepardVolumes(); nothing leaves them untouched). Recomputation keeps the volumes consistent with the current cloud at the cost of one neighbour sweep, and never drifts. The alternative of integrating $\dot V_i = V_i\,(\nabla\cdot w)_i$ along with the state (the geometric conservation law) is smoother in time but accumulates error and needs periodic re-anchoring; it is not implemented.
The collocation scheme itself is not exactly conservative, with or without motion: $\sum_i V_i U_i$ changes by the scheme's truncation error per step. What the volumes buy is a well-defined discrete mass, so that topology changes can preserve it exactly.
Particle insertion and merging
On Lagrangian grids particles cluster in compressions and thin out in expansions. ParticleAdaptivity repairs that between steps:
ad = ParticleAdaptivity(Float64; max_gap = 1.6, min_gap = 0.5, clearance = 0.7,
samples_per_dx = 2, exclude_reduced_order = true)
report = adapt_particles!(pg, ad, _extract_order(interp)) # between time steps
report.inserted, report.removed, report.mass_residual # residual at round-offDetection
Lengths are in units of $\Delta x = \min_d \Delta x_d$.
- Insertion: a neighbour pair farther apart than
max_gapwhose midpoint has no particle withinclearancegets a new particle at the midpoint. The clearance test is what prevents insertions on a regular lattice, where second neighbours are $2\Delta x$ apart but the midpoint is occupied. - Merge: a neighbour pair closer than
min_gapis replaced by one particle at its midpoint. - At most one event per particle neighbourhood and per call. Events are kept $4R$ apart so their remaps do not interact.
- Shock band: with
exclude_reduced_order, no event happens within $4R$ of a particle whose MOOD order was lowered during the last step. Interpolation of any order is unreliable across a discontinuity, and a Lagrangian grid in a shock region is better left alone (or driven towards Eulerian motion by the mover).
The conservative remap
A topology change replaces the partition of unity $\{\phi_i^{\text{old}}\}$ by $\{\phi_i^{\text{new}}\}$. Each particle's volume and content change by the swept amounts
\[\delta V_i = \int \big(\phi_i^{\text{new}} - \phi_i^{\text{old}}\big)\,\mathrm dx, \qquad \delta m_i = \int \big(\phi_i^{\text{new}} - \phi_i^{\text{old}}\big)\,\tilde U\,\mathrm dx ,\]
where $\tilde U$ is the limited (bound-clipped) linear reconstruction of the old solution around the nearest old particle. New particles have $\phi^{\text{old}} = 0$, removed particles $\phi^{\text{new}} = 0$. Since both families sum to one pointwise, $\sum_i \delta V_i = 0$ and $\sum_i \delta m_i = 0$ for any quadrature: conservation is exact by construction, which is the particle analogue of swept-region remapping in ALE finite volumes. The integrals are evaluated on a lattice of spacing $\Delta x/\texttt{samples\_per\_dx}$ over the box of half-width $2R$ around the event, using only the old neighbour structure. The new state is $U_i = (V_i U_i + \delta m_i)/(V_i + \delta V_i)$.
Two corrections follow:
- Residual. The stored content of a removed particle, $V_r U_r$, is not exactly $\int \phi_r^{\text{old}} \tilde U$. The difference is spread over the affected particles in proportion to their new volumes, so $\sum V U$ is preserved to round-off.
- Repair. Each affected particle is clipped to the bounds of its old neighbourhood, and the clipped content is redistributed to affected particles that still have room. This enforces the discrete maximum principle while keeping the sum exact; if no room is left, the remainder is spread by volume and the bounds are violated by that remainder.
Then the particle arrays are compacted/extended, the neighbour lists are rebuilt, and new particles start at the scheme's maximum order. The remap is second-order accurate in smooth regions (linear reconstruction, averaged over the new shape function's support).
What the remap does not do
- It does not change particles outside the event's $3R$–$4R$ neighbourhood, so far-field solutions are untouched.
- It does not conserve across the time step itself; see above.
- It does not move particles for mesh quality. A repulsion or smoothing term belongs in the
GridMover, and keeps the particle set well-spaced before adaptivity has to act.
Reference
HyperCloudCore.MLSVolumes — Type
MLSVolumes(P; samples_per_dx = 2)V_i = ∫_Ω φ_i dx with MLS shape functions reproducing polynomials of degree P, integrated by a midpoint rule on a background lattice with samples_per_dx points per particle spacing (the lattice is not stored). P = 0 is the background-integrated Shepard partition.
HyperCloudCore.ShepardVolumes — Type
ShepardVolumes(; sweeps = 1)Kernel-density volumes V_i ∝ 1 / S_i, S_i = w(0) + Σ_j w_ij, followed by sweeps partition-of-unity smoothing sweeps V_i ← Σ_j (w_ij / S_j) V_j (which preserve Σ V exactly), and a final renormalisation to Σ V = |Ω|. Requires a kernel that is finite at zero distance (ExponentialWeightFunction; InverseWeightFunction is not suitable).
HyperCloudCore.domain_volume — Method
domain_volume(geom::GeometricDomain) -> TVolume of the physical region. Analytic for the built-in shapes; define a method for custom geometry labels (or integrate geom.is_interior on a lattice).
HyperCloudCore.total_mass — Method
total_mass(pg) -> State{M, T}Discrete conserved content Σ_i V_i U_i over all particles of the current cloud.
HyperCloudCore.ParticleAdaptivity — Type
ParticleAdaptivity(; max_gap = 1.6, min_gap = 0.5, clearance = 0.7,
samples_per_dx = 2, exclude_reduced_order = true, max_events = typemax(Int))Rules for keeping a moving point cloud well-spaced. Lengths are multiples of the particle spacing dx = minimum(pg.meta.dx).
- Insertion: a neighbour pair
(i, j)farther apart thanmax_gap·dxwhose midpoint has no particle withinclearance·dxgets a new particle at the midpoint. - Merge: a neighbour pair closer than
min_gap·dxis replaced by one particle at the midpoint. exclude_reduced_order: skip events near particles whose MOOD order was lowered in the last step (the shock band), where interpolation of any order is unreliable.samples_per_dx: resolution of the remap quadrature lattice.- At most one event per neighbourhood and per call; events are kept
4Rapart so their remaps do not interact. Call once between time steps.
HyperCloudCore.adapt_particles! — Method
adapt_particles!(pg, ad::ParticleAdaptivity, max_order::Int) -> (; inserted, removed, mass_residual)Detects holes and clusters, applies the conservative remap for every event on the old topology, changes the particle set, and rebuilds the neighbour lists. max_order is the scheme's maximum consistency order (new particles start there, and exclude_reduced_order compares against it). Requires valid neighbour lists and volumes (update_volumes!). mass_residual is the change of total_mass over the call, which should be at round-off level.
The event work (detection, remap, compaction) is serial and runs on the host. On a grid built with device_grid a kernel first checks whether any neighbour pair violates the spacing rules; only then is the grid copied to the host, adapted there, and copied back into the device grid in place (its arrays grow as needed), where the neighbour search runs. A well-spaced cloud costs one kernel and one reduction per call.