peclet.dem — Lagrangian DEM (XPBD + Hertz–Mindlin)¶
Discrete-element simulation with SDF point-shell collision, analytic SDF walls (static and moving), scene particles, and the distributed (MPI) step. The MPI methods are present only in an MPI-enabled build. Simulation is the public tier; developer instruments, ablations and GPU execution policies live on Simulation.diagnostics (Diagnostics below).
Note
Auto-generated from the installed module docstrings. Drive simulations from Python; the full C++ API is on each repo's Doxygen site.
peclet.dem¶
peclet.dem — Lagrangian Discrete Element Method (XPBD) particle packing.
A Kokkos + ArborX XPBD solver with SDF point-shell collision for dense particle packing. The compiled
backend (Serial / OpenMP / CUDA / HIP) is chosen at build time — peclet.dem.execution_space reports
which one this build has. The distributed (MPI) step is exposed only in an MPI-enabled build
(pip install . --config-settings=cmake.define.PECLET_DEM_MPI=ON).
- :class:
peclet.dem.Simulation— the packing simulation (initialize_shape, set_positions, step, ...). - :func:
peclet.dem.build_particle— build a general particle (grid SDF + surface point shell + mass properties) from an implicit-solid SDF, ready forSimulation.set_sdf_shape.
peclet is an implicit (PEP 420) namespace shared with the other peclet-* packages, so it has no
top-level __init__.py.
Simulation¶
One granular-dynamics simulation (XPBD impulse engine + explicit Hertz-Mindlin engine, the same particle set). Set up shapes -> domain -> positions -> materials, set_dt, then step(n); read out with the get_* arrays. Developer instruments live on .diagnostics.
| Method / property | Description |
|---|---|
add_analytic_wall |
add_analytic_wall(self, node_ints: ndarray[dtype=int32, order='C'], node_reals: ndarray[dtype=float32, order='C'], root: int, invert: bool, restitution: float = 0.0, friction: float = 0.0) -> int Add an ANALYTIC wall from a core shape tree in the flat node encoding (3 ints + 16 reals per node). Exact at every scale, with no voxel grid to replicate per rank. invert=False for a stirrer/obstacle (grains outside the solid); invert=True for a container, built from a SOLID body (a solid cylinder for a drum). The wall is positioned by the node TRANSFORM -- an identity transform sits at the origin. Returns the wall index. |
add_plane |
add_plane(self, point: collections.abc.Sequence[float], normal: collections.abc.Sequence[float]) -> None Add an infinite boundary wall plane through point, with normal pointing into the half-space the grains occupy (both 3-sequences). XPBD engine only; the Hertz engine takes SDF walls. |
add_scene_shape |
add_scene_shape(self, node_ints: ndarray[dtype=int32, order='C'], node_reals: ndarray[dtype=float32, order='C'], root: int, shell: ndarray[dtype=float32, order='C'], inv_inertia: collections.abc.Sequence[float], bounding_radius: float) -> int Register a COMPOSED analytic particle shape from core's flat node encoding (the arrays peclet.geom.SceneBuilder.encode() returns; CSG of the full leaf vocabulary): the collision field is the exact tree, evaluated in canonical body space. shell: (M,3) surface probe points (bake the tree and run the shell path -- the point-shell model still needs probes). inv_inertia: unit-mass principal diagonal inverse inertia; bounding_radius: canonical enclosing radius. THE CANONICAL FRAME MUST BE THE PRINCIPAL INERTIA FRAME (SceneBuilder.principal_frame emits exactly that); a non-principal tree runs the diagonal-inertia rotational update on the wrong frame, silently. Returns the shape id for set_shape_ids. |
add_sdf_shape |
add_sdf_shape(self, grid: ndarray[dtype=float32, shape=(, , *), order='F'], origin: collections.abc.Sequence[float], spacing: collections.abc.Sequence[float], shell: ndarray[dtype=float32, order='C'], inv_inertia: collections.abc.Sequence[float], bounding_radius: float) -> int Append a grid-SDF shape (arguments as set_sdf_shape) and return its index. set_sdf_shape stays the single-shape entry point (it RESETS the registry). |
add_sdf_wall |
add_sdf_wall(self, grid: ndarray[dtype=float32, shape=(, , *), order='F'], origin: collections.abc.Sequence[float], spacing: collections.abc.Sequence[float], restitution: float = 0.0, friction: float = 0.0) -> int Add a static world-space SDF wall/container (drum barrel, hopper, vibrating tray): grid is the signed distance on an (nx, ny, nz) array indexed [x, y, z] at world nodes origin + (x, y, z) * spacing -- POSITIVE in the void where grains live, NEGATIVE in the solid wall; restitution / friction are the binary particle-wall material. Returns the wall index (for set_wall_velocity). See peclet.dem.build_wall_sdf for the SDF -> (grid, origin, spacing) helper. |
add_shape |
add_shape(self, shape: str, radius: float, height: float = 0.0, thickness: float = 0.0) -> int Append an analytic shape ('sphere' |
capacity |
The particle capacity this was built with. |
clear_external_forces |
clear_external_forces(self) -> None Zero all per-particle external forces. |
clear_external_torques |
clear_external_torques(self) -> None Zero all per-particle external torques (and re-enable island sleeping). |
compute_overlaps |
compute_overlaps(self) -> float Measure and return the maximum pair interpenetration of the current committed state. |
diagnostics |
The developer tier (Diagnostics) of this simulation. |
dt |
The time step set by set_dt. |
enable_mpi_step |
enable_mpi_step(self, rcut: float = 0.0, sync_every: int = 1, forward_rotation: bool = True, rebalance_every: int = 0, verlet_skin: float = 0.0) -> None Enable the distributed step: ghost-band width rcut (a lower bound -- step_mpi widens it to the contact reach plus a drift slack and the prediction allowance, max(rcut, 2.1 x the global maximum radius + 0.25 x it + P), so the default 0 means exactly that), sync_every (the owner/ghost reconciliation interval; conservation is exact at any value -- a body updated from several places is solved through copies, mass-split for the projection-form phases and exclusively held by one rank per sync interval for the g = 0 one-shot restitution sweep -- larger sync_every means more lag, fewer messages), rotation forwarding, the load-rebalance interval in steps (0 = fixed decomposition; every step also votes, in the same reduction as the band, whether any body has drifted more than the slack outside its owner's block, and migrates it if so -- ownership is therefore not fixed across a run, and ghost selection uses the domain-clamped position on non-periodic axes), and the Verlet ghost-reuse skin (0 = rebuild the halo topology every substep; >0 = reuse it until a particle moves > skin, and P above is the skin instead of the predicted displacement). |
export_lammps |
export_lammps(self, filename: str, step: int) -> None Export particle state to a LAMMPS dump file. |
export_sdf |
export_sdf(self, filename: str, resolution: tuple[int, int, int]) -> None Reconstruct and write the packed-bed SDF on an (rx, ry, rz) grid to a VTI file. |
extent |
The domain's SIZE (Lx, Ly, Lz) — read-only, and note it is a size and not the far corner, which is origin + extent (suite/docs/NAMING.md 1.1). |
get_angular_velocities |
get_angular_velocities(self) -> numpy.ndarray[dtype=float32] Return particle angular velocities (body frame) as an (N, 3) numpy array. |
get_external_forces_view |
get_external_forces_view(self) -> numpy.ndarray[dtype=float32] Zero-copy (N,3) device array of the per-particle external force (NumPy view on host, DLPack/CuPy on GPU) — write fluid drag here directly to avoid a host round-trip. |
get_inv_inertia |
get_inv_inertia(self) -> numpy.ndarray[dtype=float32] Return the per-particle principal-frame diagonal inverse inertia as an (N, 3) array. |
get_inv_mass_view |
get_inv_mass_view(self) -> numpy.ndarray[dtype=float32] Zero-copy (N,) device array of per-particle inverse mass (NumPy view on host, DLPack/CuPy on GPU) — read-only use; needed for stiff-safe drag integration. |
get_masses |
get_masses(self) -> numpy.ndarray[dtype=float32] Return per-particle masses (1 / inverse mass; 0 for fixed bodies) as an (N,) array. |
get_positions |
get_positions(self) -> numpy.ndarray[dtype=float32] Return particle positions as an (N, 3) numpy array. |
get_positions_view |
get_positions_view(self) -> numpy.ndarray[dtype=float32] Zero-copy (N,3) device array of positions (NumPy view on host, DLPack/CuPy on GPU). |
get_quaternions |
get_quaternions(self) -> numpy.ndarray[dtype=float32] Return particle orientation quaternions (x, y, z, w) as an (N, 4) numpy array. |
get_scales |
get_scales(self) -> numpy.ndarray[dtype=float32] Return per-particle scales as an (N,) numpy array. |
get_sdf_grid |
get_sdf_grid(self, resolution: tuple[int, int, int]) -> numpy.ndarray[dtype=float32] Reconstruct the packed-bed SDF (negative inside solid) over the domain on an (rx, ry, rz) grid; returns an (rx, ry, rz) array indexed [x, y, z]. |
get_velocities |
get_velocities(self) -> numpy.ndarray[dtype=float32] Return particle velocities as an (N, 3) numpy array. |
get_velocities_view |
get_velocities_view(self) -> numpy.ndarray[dtype=float32] Zero-copy (N,3) device array of velocities (NumPy view on host, DLPack/CuPy on GPU). |
global_scale |
The global length scale. |
gravity |
The gravitational acceleration (gx, gy, gz). |
growth_factor |
The current particle growth factor (-1 = growth inactive). |
growth_rate |
The particle growth rate. |
incremental_coloring |
Whether incremental (warm-started) coloring is enabled. |
init_mpi |
init_mpi(self, origin: tuple[float, float, float], extent: tuple[float, float, float], cells: tuple[int, int, int], periodic: tuple[bool, bool, bool]) -> None Set up the ORB block decomposition + core particle halo for the distributed step on the global domain (origin lower corner, extent SIZE, cells per axis for the ORB grid, periodic per axis -- the suite's domain quartet). |
initialize_shape |
initialize_shape(self, shape: str, radius: float, height: float = 0.0, thickness: float = 0.0) -> None Select the single particle shape and its dimensions: 'sphere' (radius), 'hollow_cylinder' (radius, height, thickness) or 'box' (half-extent = radius). RESETS the shape registry to this one shape (use add_shape for a mixture) and records the unit-mass inverse inertia that set_positions applies to every particle -- so call it BEFORE set_positions. |
max_overlap |
Maximum pair interpenetration recorded by the position solver in the last step (its last-iteration residual, so it under-reports the committed overlap -- see docs/archive/packing_investigation.md). compute_overlaps() measures the committed state. |
migrate_to_weights |
migrate_to_weights(self, weights: collections.abc.Sequence[float], align: int = 1) -> int Co-rebalance: migrate particle ownership onto the weighted ORB of per-cell weights (global x-fastest over the init_mpi cell grid), so each rank owns the particles in a block of near-equal total weight -- the SAME partition the coupled flow solver redistributes onto from the same weight field. align (a power of two, default 1) puts every block boundary on a multiple of align cells; 1 is the plain weighted ORB. A coupled run passes the alignment flow's rebalance_by_weights returned, which it chose for its pressure multigrid; dem alone has no reason to align. Raises ValueError when align is not a power of two, does not divide the cell grid, or weights does not cover it. Returns this rank's new owned count. |
num_asleep |
Number of currently-sleeping real bodies. |
num_contacts |
Broad-phase candidate pairs found in the last step (ArborX BVH query). |
num_ghost |
The number of ghost particles on this rank. |
num_manifolds |
Narrow-phase contact manifolds (touching pairs) resolved in the last step. |
num_particles |
The number of particles. |
num_shapes |
Number of registered shapes. |
origin |
The domain's lower corner (x, y, z) — read-only; set it with set_domain. |
periodic |
Per-axis periodicity (x, y, z) — read-only; set it with set_periodic. |
rank |
This rank's MPI index. |
rebalance |
rebalance(self) -> int Re-decompose by particle count and migrate ownership now; returns this rank's new owned count. |
relax |
relax(self, n: int = 1) -> None Run n dynamics-free RELAXATION substeps: overlap removal only, no gravity and no velocity update -- the growth-packing protocol's settle move (dt = 0 inside; the stored dt is untouched and set_dt is not required). |
restitution_model |
'newton' or 'poisson'. |
set_angular_velocities |
set_angular_velocities(self, angular_velocities: ndarray[dtype=float32, order='C']) -> None Set particle angular velocities (body frame) from an (N, 3) array. |
set_domain |
set_domain(self, min: collections.abc.Sequence[float], max: collections.abc.Sequence[float]) -> None set_domain(self, extent: collections.abc.Sequence[float], origin: collections.abc.Sequence[float] = [0.0, 0.0, 0.0], periodic: collections.abc.Sequence[bool] = [True, True, False]) -> None Overloaded function. 1. set_domain(self, min: collections.abc.Sequence[float], max: collections.abc.Sequence[float]) -> None Set the domain by (min, max) corner tuples (arbitrary origin); keeps current periodicity. 2. set_domain(self, extent: collections.abc.Sequence[float], origin: collections.abc.Sequence[float] = [0.0, 0.0, 0.0], periodic: collections.abc.Sequence[bool] = [True, True, False]) -> None Set the domain the suite-canonical way: extent is the box SIZE (not the far corner), origin the lower corner, periodic the per-axis flags. Equivalent to set_domain(min=origin, max=origin+extent) plus set_periodic(*periodic). Either form resets the broad-phase skin to 0.1 x the global scale (as set_global_scale does). |
set_dt |
set_dt(self, dt: float) -> None Set the time step (> 0) every stepper uses: step, step_hertz, step_mpi, step_hertz_mpi. There is no default -- a step before set_dt raises. |
set_external_forces |
set_external_forces(self, forces: ndarray[dtype=float32, order='C']) -> None Set the per-particle external FORCE (e.g. fluid drag) from an (N, 3) array. Applied each step as dv = FinvMassdt; persists until re-set or cleared. |
set_external_torques |
set_external_torques(self, torques: ndarray[dtype=float32, order='C']) -> None Set the per-particle external TORQUE in the WORLD frame from an (N, 3) array (the resolved-CFD-DEM hydrodynamic torque, a magnetic couple, ...). Applied each step in the angular predictor as Euler's equation in the body frame, dw = invI(tau_body - w x I w)dt, alongside the gyroscopic term that is already there; persists until re-set or cleared. Only bodies with a finite inertia respond -- a torque on a body whose invInertia is zero is inert, exactly as the gyroscopic term is. Sleeping is disabled while a torque is set, as it is for external forces. |
set_global_scale |
set_global_scale(self, scale: float) -> None Set a global length scale applied to all particles (effective radius = scale x global_scale x base radius). Resets the broad-phase skin, as set_domain does -- call it BEFORE set_domain if you rely on the skin. |
set_gravity |
set_gravity(self, gravity: collections.abc.Sequence[float]) -> None Set the gravitational acceleration (gx, gy, gz) -- any sequence of three floats. The default is ZERO. |
set_growth_params |
set_growth_params(self, rate: float, new_factor: float = -1.0) -> None Set the particle growth rate (factor = exp(ratedt) per step, capped at 1) and, if new_factor > 0, the current growth factor (a negative value keeps it; 0.01 if growth was inactive). |
set_hertz_material |
set_hertz_material(self, mat: int, youngs: float, poisson: float) -> None Per-material Young's modulus and Poisson ratio for the soft-sphere Hertz-Mindlin engine (material ids as in set_material_ids). |
set_incremental_coloring |
set_incremental_coloring(self, enabled: bool) -> None Incremental (warm-started) graph coloring of the contact manifolds (default True): reuse last substep's colors and repair only the conflicts. This CHANGES RESULTS -- the coloring fixes the Gauss-Seidel sweep order -- so False reproduces the pre-incremental behaviour rather than merely running slower. |
set_inv_inertia |
set_inv_inertia(self, inv_inertia: ndarray[dtype=float32, order='C']) -> None Set the per-particle principal-frame diagonal inverse inertia from an (N, 3) array. |
set_inv_mass |
set_inv_mass(self, inv_mass: ndarray[dtype=float32, order='C']) -> None Set per-particle inverse mass, an (N,) array (0 = fixed body). |
set_material_ids |
set_material_ids(self, ids: collections.abc.Sequence[int]) -> None Per-particle material ids (0..7). Pair (e, mu) values come from set_pair_material; without any set_pair_material call the global material applies everywhere. |
set_material_params |
set_material_params(self, restitution_normal: float, restitution_tangent: float = 0.0, friction: float = 0.0) -> None Set the BODY-BODY normal/tangential restitution and Coulomb friction coefficient. The default friction is ZERO, and add_analytic_wall / add_sdf_wall set the particle-WALL material only -- so a bed more than a few layers deep run with the defaults behaves like a liquid: it transmits full hydrostatic pressure to the container and the position solve squeezes grains through the boundary. That failure is silent and looks like a solver-convergence bug (raising the position iterations and halving dt do not move it). Set a non-zero friction for any deep bed. |
set_pair_material |
set_pair_material(self, a: int, b: int, restitution: float, friction: float) -> None Symmetric pair material (restitution, friction) for material ids (a, b). The first call seeds every pair from the current global material. |
set_periodic |
set_periodic(self, x: bool, y: bool, z: bool) -> None Set periodic boundaries per axis (x, y, z); read back with the periodic property (suite/docs/NAMING.md 1.4). |
set_positions |
set_positions(self, positions: ndarray[dtype=float32, order='C']) -> None Set particle positions from an (N, 3) array, or (N, 4) whose column 3 is the INVERSE mass (0 is remapped to 1; use set_inv_mass for a fixed body). N may not exceed capacity. This (re)sizes the particle set and RESETS every particle's quaternion, scale, inverse mass, shape id (to 0), velocities and gid -- so every other per-particle setter goes AFTER it. |
set_quaternions |
set_quaternions(self, quaternions: ndarray[dtype=float32, order='C']) -> None Set particle orientation quaternions (x, y, z, w) from an (N, 4) array. |
set_restitution_model |
set_restitution_model(self, model: str) -> None Restitution model of the PGS velocity solve: 'newton' (default; per-substep restitution with Moreau's target, -e x the pre-solve normal velocity on every closed contact above the resting threshold, which cannot create kinetic energy for a uniform e) or 'poisson' (event-level: each pair banks its kinetic compression impulse and releases e x the bank as a budget-capped separation-velocity target during unloading -- restores the multi-substep-impact rebound per-substep restitution cannot return; it keeps Newton's per-substep targets, -e x the approach on approaching contacts only). |
set_scales |
set_scales(self, scales: ndarray[dtype=float32, order='C']) -> None Set per-particle scales from an (N,) array (the growth target). |
set_scales_uniform |
set_scales_uniform(self, scale: float) -> None Set a single uniform scale for all particles. |
set_sdf_shape |
set_sdf_shape(self, grid: ndarray[dtype=float32, shape=(, , *), order='F'], origin: collections.abc.Sequence[float], spacing: collections.abc.Sequence[float], shell: ndarray[dtype=float32, order='C'], inv_inertia: collections.abc.Sequence[float], bounding_radius: float) -> None Import a general particle: grid is the body-frame signed distance on an (nx, ny, nz) array indexed [x, y, z] (negative inside), sampled at nodes origin + (x, y, z) * spacing; shell the (M,3) surface point shell (EMPTY = sample the field's own zero level set); inv_inertia the unit-mass principal diagonal inverse inertia; bounding_radius the canonical enclosing radius. RESETS the registry to this one shape. See peclet.dem.build_particle for the SDF -> (grid, shell, inertia) helper. |
set_shape_ids |
set_shape_ids(self, ids: ndarray[dtype=int32, order='C']) -> None Per-particle shape index (one int per particle, each < num_shapes). Refreshes each particle's inverse inertia from its new shape. Call it AFTER set_positions: set_positions resets every particle to shape 0 (and sizes the particle set this call is checked against). |
set_shell_detection |
set_shell_detection(self, mode: str) -> None Narrow-phase shell detection of the XPBD engine (step, step_mpi, relax, compute_overlaps; step_hertz is unaffected). 'one_way' (default): only the lower-index body's shell points are tested against the other body's SDF, so a thin rim, wall or edge pressed into a flat face between those points can go unseen. 'two_way': shell pairs are also probed the other way round, and a sphere-shell pair always uses the exact sphere probe. Enable it for thin-walled or non-convex shells (tubes, rings), sharp edges and corners against faces, and large size ratios where the smaller body's features are finer than the larger body's shell spacing; not needed for spheres or smooth convex bodies of similar size with dense shells. Costs up to 2x the narrow phase and the contacts of shell pairs (docs/solver_details.md). CHANGES RESULTS for non-sphere bodies. |
set_sleeping |
set_sleeping(self, enabled: bool, threshold_scale: float = 2.0, consecutive: int = 64, wake_scale: float = 40.0, wake_on_lost_contact: bool = False, immovable_frac: float = 0.009999999776482582) -> None Island sleeping (single-GPU statics, default ON): freeze grounded bodies whose motion stays below threshold_scale x the resting floor for consecutive substeps; wake only above wake_scale x that floor (hysteresis vs residual jitter). wake_on_lost_contact additionally wakes a sleeper whose support disappeared; immovable_frac is a sleeper's effective inverse-mass fraction in the solve (0 = exactly immovable). Requires gravity and no external force; inert under MPI. |
set_solver_iterations |
set_solver_iterations(self, pos: int, vel: int) -> None Set the XPBD position- and velocity-solve iteration counts. vel defaults to 0 (no velocity solve, hence no restitution). |
set_stabilization |
set_stabilization(self, mode: str) -> None Stabilization pass of the staged velocity solve: 'off' (pure symmetric PGS -- exact ballistic response, but a deep static column mid-collapse cannot be arrested within the iteration budget), 'onesided' (default: held-lower-side grounded impulses -- arrests any collapse but is a momentum sink), 'multilevel' (GraphMG contact-graph aggregation: coarse inelastic solves at super-body masses -- momentum-conserving transport acceleration). |
set_thermostat |
set_thermostat(self, temperature: float, tau: float, kB: float = 1.0) -> None Enable a Berendsen-style velocity thermostat (target temperature, coupling time tau; tau = 0 disables). |
set_velocities |
set_velocities(self, velocities: ndarray[dtype=float32, order='C']) -> None Set particle velocities from an (N, 3) array. |
set_verlet_skin |
set_verlet_skin(self, skin_frac: float) -> None Enable the Verlet-cached impulse broadphase (single-GPU, non-periodic, default OFF, i.e. skin_frac = 0): skip the ArborX rebuild while nothing moved more than skin/2 (skin = skin_frac x max grain radius). |
set_wall_material_id |
set_wall_material_id(self, wid: int, mat: int) -> None Give an SDF wall a material id so particle-wall (e, mu) resolves via the pair table instead of the wall's binary material. |
set_wall_transform |
set_wall_transform(self, wall_index: int, translation: collections.abc.Sequence[float] = [0.0, 0.0, 0.0], quat: collections.abc.Sequence[float] = [0.0, 0.0, 0.0, 1.0]) -> None Place an ANALYTIC wall (add_analytic_wall) rigidly in the world: quat (x,y,z,w) then translation, composed onto the AUTHORED root transform, so calls are absolute and never compound. This moves the GEOMETRY -- a stirrer blade sweeps -- where set_wall_velocity only gives the static surface a velocity field (enough for an axisymmetric drum). Drive both together each step: integrate ang_vel into the quaternion here and pass the same ang_vel to set_wall_velocity. Grid-SDF walls have no tree and are refused. |
set_wall_velocity |
set_wall_velocity(self, wall_index: int, lin_vel: collections.abc.Sequence[float] = [0.0, 0.0, 0.0], ang_vel: collections.abc.Sequence[float] = [0.0, 0.0, 0.0], center: collections.abc.Sequence[float] = [0.0, 0.0, 0.0]) -> None Set a wall's rigid-body surface velocity v(x) = lin_vel + ang_vel × (x − center) (felt by grains in contact even though the geometry is static). Cheap; call every step for a vibrating wall. |
shell_detection |
The narrow-phase shell detection: 'one_way' or 'two_way'. |
sleeping |
Whether island sleeping is enabled. |
stabilization |
The stabilization mode: 'off', 'onesided', 'multilevel' (or a diagnostics mode). |
step |
step(self, n: int = 1) -> None Advance n XPBD substeps of the time step set by set_dt (raises if set_dt was never called). |
step_hertz |
step_hertz(self, substeps: int = 1, skin_frac: float = 0.30000001192092896) -> None Advance substeps explicit soft-sphere Hertz-Mindlin steps of the time step set by set_dt (spheres, SDF walls, non-periodic): viscoelastic Hertz normal force + Mindlin shear-history spring, Coulomb-clamped; (e, mu) from the pair-material tables, stiffness from set_hertz_material. skin_frac is the Verlet pair-list skin as a fraction of the radius. |
step_hertz_mpi |
step_hertz_mpi(self, substeps: int = 1, skin_frac: float = 0.30000001192092896) -> None Advance substeps distributed explicit Hertz-Mindlin (force-based) steps of the time step set by set_dt — the MPI counterpart of step_hertz on the init_mpi/enable_mpi_step decomposition. rebalance_every counts CALLS of this method; migration carries the Mindlin history. |
step_mpi |
step_mpi(self, n: int = 1) -> None Advance the distributed (MPI) simulation by n steps of the time step set by set_dt, with halo exchange. |
verlet_skin |
Broadphase-skin fraction of the max grain radius (0 = rebuild every step). |
write_vtp |
write_vtp(self, filename: str) -> None Write particle state to a VTP file (ParaView/Ovito). |
Diagnostics¶
Developer tier of a Simulation, reached as sim.diagnostics: instruments, ablations and execution-policy switches. Nothing here is needed to set up, run or read out a simulation; the setters that CHANGE RESULTS say so in their docstring.
| Method / property | Description |
|---|---|
coloring_conflicts |
coloring_conflicts(self) -> tuple[int, int] (velocity, position) graph-coloring-invariant violations in the last substep; a valid coloring returns (0, 0). Test hook for the incremental warm-start path. |
coloring_leftovers |
coloring_leftovers(self) -> tuple[int, int] (velocity, position) edges -- manifolds, position units -- the last substep's colourings left uncoloured (single-rank). Non-zero means a colouring exhausted its arbitration rounds or palette. |
cuda_graphs |
Whether CUDA-graph replay of the solver loops is enabled. |
fused_sweeps |
Fused-sweep policy: 'auto', 'on' or 'off'. |
mpi_gathers |
Cumulative ghost gather() count across distributed steps. |
mpi_rebuilds |
Cumulative halo topology-rebuild count (Verlet-skin path); with mpi_gathers this is the ghost-reuse ratio. |
multilevel_coloring_conflicts |
multilevel_coloring_conflicts(self) -> int Same-colour pairs at an aggregate of the last multilevel hierarchy, summed over its levels; a valid coarse colouring returns 0 (0 also when no hierarchy was built). |
profiling_info |
profiling_info(self) -> dict Dict of the particle / broad-phase pair / manifold counts and the last-step max overlap (the same four values the Simulation properties carry). |
rest_bank_stats |
rest_bank_stats(self) -> tuple[float, float, int] Poisson-restitution instrument: (sum, max, n_pairs>0) of the per-pair owed separation impulse committed last substep (physical impulse units). |
rest_orphan_stats |
rest_orphan_stats(self) -> tuple[float, float, int] Poisson-restitution instrument: (sum, max, n_bodies>0) of the per-body orphaned event budget (physical impulse units). |
set_cuda_graphs |
set_cuda_graphs(self, enabled: bool) -> None CUDA-graph replay of the solver's iteration loops (default True): capture collapses each iteration's launch storm into one replay. Results are bit-identical either way; inert on non-CUDA backends and on the distributed step. |
set_fused_sweeps |
set_fused_sweeps(self, mode: str) -> None Fused color sweeps (CUDA): a whole sweep -- and where eligible the whole adaptive iteration loop -- as ONE kernel behind software grid barriers. 'auto' (default) uses them exactly where graph replay is unavailable (the distributed step, or set_cuda_graphs(False)); 'on'/'off' force. Bit-identical results either way. |
set_stabilization |
set_stabilization(self, mode: str) -> None The full stabilization mode set: the three production modes of Simulation.set_stabilization plus the two measurement modes -- 'escalate' (extra symmetric sweeps up to 256; diagnostic/fallback) and 'ordered' (level-ordered symmetric sweeps; known insufficient for deep columns). |
set_velocity_solver |
set_velocity_solver(self, name: str) -> None A/B switch of the collision solves: 'gauss_seidel' (default; colored Gauss-Seidel, correct multi-contact dissipation) or 'jacobi' (mass-split Jacobi: every contact is its own copy, solved against masses m/count, true-mass deltas summed -- conservative, and at a fixed iteration budget more dissipative than Gauss-Seidel). CHANGES RESULTS. |
split_stats |
split_stats(self) -> dict Dict of the last substep's body copies (docs/contact_solve_framework.md §4.4): hub copy slots per phase, light hubs (hub mass < 10x its partners' mean), bodies solved through more than one slot per phase (hubs; periodic images in the single-rank position phase), and (0 until implemented) unfired split contacts and drift migrations; ml_hub_aggregated: split velocity vertices in a multi-member level-1 multilevel group (max over the last step call's substeps); vel_iters_used / pos_iters_used: iterations of the last substep's main velocity and position loops (-1: a device-side loop, whose count is read back only with the C++ diagnostics switch debugIterationCounters); orphan_clamps: clamp hits of the distributed Poisson orphan balance over the last step_mpi call (must stay 0). |
velocity_solver |
'gauss_seidel' or 'jacobi'. |
wall_sdf_at |
wall_sdf_at(self, wall_index: int, points: ndarray[dtype=float32, order='C']) -> numpy.ndarray[dtype=float32] The wall's signed distance at world points: (M,3) in -> (M,) out, POSITIVE in the void where the grains live -- exactly what the narrow phase reads, so it is the honest check of a set_wall_transform placement and the way to draw a stirrer. |
Module attributes¶
| Attribute | Value in this build |
|---|---|
execution_space |
'OpenMP' |
build_particle¶
Build a :class:`ParticleShape` from a signed-distance function.
Parameters
----------
f : callable
``f(points) -> distances``: ``points`` is ``(N, 3)``, returns ``(N,)`` signed distances,
**negative inside** the solid. Any implicit-modeling front-end that can evaluate on point
arrays works (hand-written NumPy CSG, `fogleman/sdf`, ...).
bounds : ((xlo, ylo, zlo), (xhi, yhi, zhi))
Axis-aligned box enclosing the solid, with a little slack so the zero level set is interior.
resolution : int or (nx, ny, nz)
Lattice resolution. 48–96 is a good range; higher = smoother shell/SDF but larger on-device
grid.
density : float
Material density; scales ``mass`` and ``inertia`` (not ``inv_inertia_unit``).
shell : {"vertices", "centroids"}
Place surface points at marching-cubes vertices or triangle centroids, then thin them to a
controlled density (see ``target_shell_points``).
target_shell_points : int
Approximate number of surface points to keep. The dense marching-cubes cloud is voxel-thinned
to about this many — a few hundred is a good collision-shell size (comparable to the analytic
shapes). Ignored if ``shell_spacing`` is given.
shell_spacing : float or None
Explicit thinning cell size (canonical units); overrides ``target_shell_points``. ``None``
derives it from the surface area and the target count.
align_principal : bool
Recentre on the COM and rotate onto the principal axes so the stored diagonal inertia is
exact. Strongly recommended (the solver inertia is diagonal). Requires ``f`` callable.
margin : float
Extra half-width (as a fraction of the box size) added around ``bounds`` when re-sampling in
the principal frame, so the rotated shape still fits.
Returns
-------
ParticleShape
build_wall_sdf¶
Sample a static container/wall SDF onto a regular world-space lattice.
Give it ``f(points) -> distance`` (``points`` is ``(N, 3)`` world coordinates) that is **positive
in the void where the grains live and negative inside the solid wall**, over an axis-aligned box
``bounds`` that spans the whole simulation domain (the grid must cover wherever a grain can reach).
Returns a :class:`WallSDF` ready for :meth:`WallSDF.add_to`.
Parameters
----------
f : callable
``f(points) -> distances``; positive in the void, negative in the wall.
bounds : ((xlo, ylo, zlo), (xhi, yhi, zhi))
Axis-aligned box covering the domain (typically the full ``set_domain`` box).
resolution : int or (nx, ny, nz)
Lattice resolution. 64–128 resolves a smooth curved wall well.
export_lammps¶
export_lammps(filename: str, step: int, pos: ndarray[dtype=float32, order='C'], vel: ndarray[dtype=float32, order='C'], quats: ndarray[dtype=float32, order='C'], radii: ndarray[dtype=float32, order='C'], box_min: collections.abc.Sequence[float] | None = None, box_max: collections.abc.Sequence[float] | None = None, periodic: bool = False) -> None
Module-level LAMMPS dump writer from raw arrays (filename, step, pos, vel, quats, radii, box corners, periodic flag).
finalize¶
finalize() -> None
Release every live object and zero-copy array of this module, then Kokkos::finalize() (deterministic teardown; also run automatically at interpreter exit). Idempotent. After it, the module's solver objects and *_view arrays must not be used.