53 m.attr(
"__doc__") =
"DEM-GPU (Kokkos + ArborX): portable XPBD granular dynamics";
55 if (!Kokkos::is_initialized())
62 auto shutdown = []() {
64 if (Kokkos::is_initialized() && !Kokkos::is_finalized())
67 m.def(
"finalize", shutdown,
68 "Release all live Simulations and finalize Kokkos (deterministic teardown; also run at "
70 nb::module_::import_(
"atexit").attr(
"register")(nb::cpp_function(shutdown));
71 m.attr(
"execution_space") = nb::str(Kokkos::DefaultExecutionSpace::name());
73 nb::class_<Simulation>(m,
"Simulation")
74 .def(nb::init<int>(), nb::arg(
"capacity"))
76 "Use a uniform sphere of the given radius for all particles.")
78 nb::arg(
"radius"), nb::arg(
"height") = 0.0f, nb::arg(
"thickness") = 0.0f,
79 "Select the particle shape (sphere/cylinder/ring/...) and its dimensions.")
82 nb::arg(
"radius") = 0.5f, nb::arg(
"height") = 2.0f, nb::arg(
"thickness") = 0.0f,
83 "CUDA-API alias for initialize_shape.")
91 [](
Simulation& s, nb::ndarray<float, nb::c_contig> grid,
int nx,
int ny,
int nz,
92 std::tuple<float, float, float> origin, std::tuple<float, float, float> spacing,
93 nb::ndarray<float, nb::c_contig> shell, std::tuple<float, float, float> inv_inertia,
94 float bounding_radius) {
97 peclet::dem::F3{std::get<0>(origin), std::get<1>(origin), std::get<2>(origin)},
98 peclet::dem::F3{std::get<0>(spacing), std::get<1>(spacing), std::get<2>(spacing)},
101 std::get<2>(inv_inertia)},
104 nb::arg(
"grid"), nb::arg(
"nx"), nb::arg(
"ny"), nb::arg(
"nz"), nb::arg(
"origin"),
105 nb::arg(
"spacing"), nb::arg(
"shell"), nb::arg(
"inv_inertia"), nb::arg(
"bounding_radius"),
106 "Import a general particle: grid SDF (flat nx*ny*nz, x-fastest), surface point shell "
107 "(M,3), unit-mass principal diagonal inverse inertia, and canonical bounding radius.")
109 nb::arg(
"px") =
true, nb::arg(
"py") =
true, nb::arg(
"pz") =
false,
110 "Set the box size (lx,ly,lz) and per-axis periodicity.")
115 [](
Simulation& s, std::tuple<float, float, float> mn,
116 std::tuple<float, float, float> mx) {
120 nb::arg(
"min"), nb::arg(
"max"),
121 "Set the domain by (min, max) corner tuples (arbitrary origin); keeps current "
124 nb::arg(
"z"),
"Enable periodic boundaries per axis (x, y, z).")
126 "Return the domain minimum corner (x, y, z).")
128 "Return the domain maximum corner (x, y, z).")
130 "Set the gravitational acceleration vector (gx, gy, gz).")
132 nb::arg(
"kB") = 1.0f,
133 "Enable a Berendsen-style velocity thermostat (target temperature, coupling time tau).")
135 nb::arg(
"vel"),
"Set the XPBD position- and velocity-solve iteration counts.")
138 "Per-material Young's modulus and Poisson ratio for the soft-sphere Hertz-Mindlin "
139 "engine (material ids as in set_material_ids).")
141 nb::arg(
"skin_frac") = 0.3f,
142 "Advance explicit soft-sphere Hertz-Mindlin steps (spheres, SDF walls, non-periodic): "
143 "viscoelastic Hertz normal force + Mindlin shear-history spring, Coulomb-clamped; "
144 "(e, mu) from the pair-material tables, stiffness from set_hertz_material.")
146 "Enable/disable the stabilization pass of the staged velocity solve (default True). "
147 "Boolean form of set_stabilization_mode: True = 'onesided', False = 'off'.")
149 "Select the stabilization pass of the staged velocity solve: 'off' (pure symmetric "
150 "PGS), 'onesided' (default: held-lower-side grounded impulses -- arrests any collapse "
151 "but is a momentum sink), 'multilevel' (GraphMG contact-graph aggregation: coarse "
152 "inelastic solves at super-body masses -- momentum-conserving transport "
153 "acceleration), 'escalate' (extra symmetric sweeps up to 256; diagnostic/fallback), "
154 "'ordered' (level-ordered symmetric sweeps; measurement mode).")
156 "Poisson-restitution diagnostics: (sum, max, n_bodies>0) of the per-body orphaned "
157 "event budget (physical impulse units).")
159 "Poisson-restitution diagnostics: (sum, max, n_pairs>0) of the per-pair owed "
160 "separation impulse committed last substep (physical impulse units).")
162 "Restitution model of the PGS velocity solve: 'newton' (default; per-substep "
163 "restitution on the pre-solve approach) or 'poisson' (event-level: each pair banks its "
164 "kinetic compression impulse and releases e x the bank as a budget-capped "
165 "separation-velocity target during unloading -- restores the multi-substep-impact "
166 "rebound per-substep Newton cannot return). PECLET_DEM_REST_MODEL overrides.")
168 "Select the single-GPU restitution solve: True (default) = colored Gauss–Seidel "
169 "(correct multi-contact dissipation), False = count-averaged Jacobi (legacy).")
171 "Set a global length scale applied to all particles.")
174 nb::arg(
"restitution_tangent") = 0.0f, nb::arg(
"friction") = 0.0f,
175 "Set normal/tangential restitution and the Coulomb friction coefficient.")
177 "Per-particle material ids (0..7). Pair (e, mu) values come from set_pair_material; "
178 "without any set_pair_material call the global material applies everywhere.")
180 nb::arg(
"restitution"), nb::arg(
"friction"),
181 "Symmetric pair material (restitution, friction) for material ids (a, b). The first "
182 "call seeds every pair from the current global material.")
184 "Give an SDF wall a material id so particle-wall (e, mu) resolves via the pair table "
185 "instead of the wall's binary material.")
190 [](
Simulation& s, std::tuple<float, float, float> p, std::tuple<float, float, float> n) {
191 s.
addPlane(std::get<0>(p), std::get<1>(p), std::get<2>(p), std::get<0>(n),
192 std::get<1>(n), std::get<2>(n));
194 nb::arg(
"point"), nb::arg(
"normal"),
195 "Add a boundary wall plane from a point and a normal (3-sequences).")
204 [](
Simulation& s, nb::ndarray<float, nb::c_contig> grid,
int nx,
int ny,
int nz,
205 std::tuple<float, float, float> origin, std::tuple<float, float, float> spacing,
206 float restitution,
float friction) {
209 peclet::dem::F3{std::get<0>(origin), std::get<1>(origin), std::get<2>(origin)},
210 peclet::dem::F3{std::get<0>(spacing), std::get<1>(spacing), std::get<2>(spacing)},
211 restitution, friction);
213 nb::arg(
"grid"), nb::arg(
"nx"), nb::arg(
"ny"), nb::arg(
"nz"), nb::arg(
"origin"),
214 nb::arg(
"spacing"), nb::arg(
"restitution") = 0.0f, nb::arg(
"friction") = 0.0f,
215 "Add a static world-space SDF wall/container: flat grid SDF (nx*ny*nz, x-fastest, "
217 "in the void), world origin/spacing, and the binary particle–wall restitution & "
219 "Returns the wall index.")
225 [](
Simulation& s,
int wall_index, std::tuple<float, float, float> lin,
226 std::tuple<float, float, float> ang, std::tuple<float, float, float> center) {
228 wall_index,
peclet::dem::F3{std::get<0>(lin), std::get<1>(lin), std::get<2>(lin)},
230 peclet::dem::F3{std::get<0>(center), std::get<1>(center), std::get<2>(center)});
232 nb::arg(
"wall_index"), nb::arg(
"lin_vel") = std::make_tuple(0.0f, 0.0f, 0.0f),
233 nb::arg(
"ang_vel") = std::make_tuple(0.0f, 0.0f, 0.0f),
234 nb::arg(
"center") = std::make_tuple(0.0f, 0.0f, 0.0f),
235 "Set a wall's rigid-body surface velocity v(x) = lin_vel + ang_vel × (x − center) (felt "
237 "grains in contact even though the geometry is static). Cheap; call every step for a "
243 [](
Simulation& s, nb::ndarray<float, nb::c_contig> a) {
244 if (a.ndim() == 2 && (a.shape(1) == 3 || a.shape(1) == 4)) {
245 const int n = (int)a.shape(0), k = (int)a.shape(1);
246 const float* p =
static_cast<const float*
>(a.data());
247 std::vector<float> xyz((
size_t)n * 3), im;
248 const bool hasMass = (k == 4);
251 for (
int i = 0; i < n; ++i) {
252 xyz[3 * i] = p[k * i];
253 xyz[3 * i + 1] = p[k * i + 1];
254 xyz[3 * i + 2] = p[k * i + 2];
256 float w = p[k * i + 3];
257 im[i] = (w == 0.0f) ? 1.0f : w;
267 "Set particle positions from an (N,3) array, or (N,4) where column 3 is inverse mass.")
271 "Set particle velocities from an (N,3) array.")
273 "set_external_forces",
275 "Set the per-particle external FORCE (e.g. fluid drag) from an (N,3) array. Applied each "
276 "step as dv = F*invMass*dt; persists until re-set or cleared.")
278 "Zero all per-particle external forces.")
282 "Set particle orientation quaternions from an (N,4) array.")
284 "set_angular_velocities",
285 [](
Simulation& s, nb::ndarray<float, nb::c_contig> a) {
288 "Set particle angular velocities from an (N,3) array.")
292 "Set per-particle inverse inertia from an (N,3) array.")
296 "Set per-particle inverse mass (0 => fixed/immovable).")
297 .def(
"get_angular_velocities",
301 "Set a single uniform scale for all particles.")
305 "Set per-particle scales from an array.")
307 nb::arg(
"new_factor") = -1.0f,
"Set the particle growth rate and target size factor.")
309 "Return the current particle growth factor.")
314 "Return particle positions as an (N,3) numpy array.")
317 "Return particle velocities as an (N,3) numpy array.")
323 "get_positions_view",
325 return peclet::core::python::view_to_ndarray(Kokkos::subview(
328 "Zero-copy (N,3) device array of positions (NumPy view on host, DLPack/CuPy on GPU).")
330 "get_velocities_view",
332 return peclet::core::python::view_to_ndarray(Kokkos::subview(
335 "Zero-copy (N,3) device array of velocities (NumPy view on host, DLPack/CuPy on GPU).")
337 "get_external_forces_view",
339 return peclet::core::python::view_to_ndarray(Kokkos::subview(
342 "Zero-copy (N,3) device array of the per-particle external force (NumPy view on host, "
343 "DLPack/CuPy on GPU) — write fluid drag here directly to avoid a host round-trip.")
347 return peclet::core::python::view_to_ndarray(
350 "Zero-copy (N,) device array of per-particle inverse mass (NumPy view on host, "
351 "DLPack/CuPy on GPU) — read-only use; needed for stiff-safe drag integration.")
354 "Return particle orientation quaternions as an (N,4) numpy array.")
357 "Return per-particle scales as a numpy array.")
359 "Advance the simulation one step (dt=0 uses the configured time step).")
362 [](
Simulation& s, std::tuple<int, int, int> res) {
363 auto [rx, ry, rz] = res;
366 return peclet::core::python::vector_to_ndarray(
367 s.
getSdfGrid(rx, ry, rz), {(std::size_t)rx, (std::size_t)ry, (std::size_t)rz},
368 {(std::int64_t)ry * rz, (std::int64_t)rz, 1});
370 nb::arg(
"resolution"),
371 "Reconstruct a packed-bed SDF on a (rx,ry,rz) grid (the get_sdf_grid pipeline for CFD).")
373 "Write particle state to a VTP file (ParaView/Ovito).")
379 nb::arg(
"threshold_scale") = 2.0f, nb::arg(
"consecutive") = 64,
380 nb::arg(
"wake_scale") = 40.0f,
381 "Enable island sleeping (single-GPU statics, default OFF): freeze grounded bodies whose "
382 "motion stays below threshold_scale x the resting floor for `consecutive` substeps; wake "
383 "only above wake_scale x that floor (hysteresis vs residual jitter).")
386 "Enable the Verlet-cached impulse broadphase (single-GPU, non-periodic, default OFF): "
387 "skip the ArborX rebuild while nothing moved more than skin/2 (skin = skin_frac x max "
390 "TEST-ONLY: (velocity, position) colouring-invariant violations in the last substep; "
391 "a valid colouring returns (0, 0).")
399 "Export particle state to a LAMMPS dump file.")
402 [](
Simulation& s,
const std::string& filename, std::tuple<int, int, int> res) {
403 auto [rx, ry, rz] = res;
406 nb::arg(
"filename"), nb::arg(
"resolution"),
407 "Reconstruct and write the packed-bed SDF on a (rx,ry,rz) grid to a VTI file.")
409 "get_profiling_info",
418 "Return a dict of particle/contact/manifold counts and the max overlap.")
423 [](
Simulation& s, std::tuple<double, double, double> origin,
424 std::tuple<double, double, double> size, std::tuple<long, long, long> gsize,
425 std::tuple<bool, bool, bool> periodic) {
427 MPI_Initialized(&inited);
430 char** argv =
nullptr;
431 MPI_Init(&argc, &argv);
433 s.initMpi(origin, size, gsize, periodic, MPI_COMM_WORLD);
435 nb::arg(
"origin"), nb::arg(
"size"), nb::arg(
"gsize"), nb::arg(
"periodic"),
436 "Set up the ORB block decomposition + transport-core particle halo for the distributed "
438 .def(
"enable_mpi_step", &Simulation::enableMpiStep, nb::arg(
"rcut"),
439 nb::arg(
"sync_every") = 1, nb::arg(
"forward_rotation") =
true,
440 nb::arg(
"rebalance_every") = 0, nb::arg(
"verlet_skin") = 0.0,
441 "Enable the distributed step: ghost cutoff rcut, sync cadence, rotation forwarding, the "
442 "load-rebalance interval in steps (0 = fixed decomposition), and the Verlet ghost-reuse "
444 "(0 = rebuild the halo topology every substep; >0 = reuse it until a particle moves > "
446 .def(
"step_mpi", &Simulation::stepMpi, nb::arg(
"nsteps") = 1,
447 "Advance the distributed (MPI) simulation by nsteps with halo exchange.")
448 .def(
"step_hertz_mpi", &Simulation::stepHertzMpi, nb::arg(
"dt"), nb::arg(
"substeps") = 1,
449 nb::arg(
"skin_frac") = 0.3f,
450 "Advance `substeps` distributed explicit Hertz-Mindlin (force-based) steps of size dt "
451 "— the MPI counterpart of step_hertz on the init_mpi/enable_mpi_step decomposition. "
452 "rebalance_every counts CALLS of this method; migration carries the Mindlin history.")
453 .def(
"rebalance", &Simulation::rebalance,
454 "Re-decompose by particle count and migrate ownership now; returns this rank's new "
456 .def(
"migrate_to_weights", &Simulation::migrateToWeights, nb::arg(
"weights"),
457 "Co-rebalance: migrate ownership onto the weighted ORB of per-cell weights (global "
458 "x-fastest, matching the ORB grid) -- the SAME partition the coupled flow solver "
459 "redistributes onto from the same weight field. Returns this rank's new owned count.")
460 .def(
"rank", &Simulation::rank,
"Return this rank's MPI index.")
461 .def(
"num_ghost", &Simulation::numGhost,
"Return the number of ghost particles on this rank.")
462 .def(
"mpi_rebuilds", &Simulation::mpiRebuilds,
463 "Cumulative halo topology-rebuild count (Verlet-skin path); pair with mpi_gathers() for "
464 "the ghost-reuse ratio.")
465 .def(
"mpi_gathers", &Simulation::mpiGathers,
466 "Cumulative ghost gather() count across distributed steps.")
474 [](
const std::string& filename,
int step, nb::ndarray<float, nb::c_contig> pos,
475 nb::ndarray<float, nb::c_contig> vel, nb::ndarray<float, nb::c_contig> quats,
476 nb::ndarray<float, nb::c_contig> radii,
477 std::optional<std::tuple<float, float, float>> box_min,
478 std::optional<std::tuple<float, float, float>> box_max,
bool pbc_enabled) {
479 float bmin[3], bmax[3];
480 const float *pmn =
nullptr, *pmx =
nullptr;
482 bmin[0] = std::get<0>(*box_min);
483 bmin[1] = std::get<1>(*box_min);
484 bmin[2] = std::get<2>(*box_min);
488 bmax[0] = std::get<0>(*box_max);
489 bmax[1] = std::get<1>(*box_max);
490 bmax[2] = std::get<2>(*box_max);
494 to_vec(radii), pmn, pmx, pbc_enabled);
496 nb::arg(
"filename"), nb::arg(
"step"), nb::arg(
"pos"), nb::arg(
"vel"), nb::arg(
"quats"),
497 nb::arg(
"radii"), nb::arg(
"box_min") = std::nullopt, nb::arg(
"box_max") = std::nullopt,
498 nb::arg(
"pbc_enabled") =
false,
499 "Module-level LAMMPS dump writer from raw arrays (filename, step, pos, vel, quats, radii, "