52 m.attr(
"__doc__") =
"DEM-GPU (Kokkos + ArborX): portable XPBD granular dynamics";
54 if (!Kokkos::is_initialized())
61 auto shutdown = []() {
63 if (Kokkos::is_initialized() && !Kokkos::is_finalized())
66 m.def(
"finalize", shutdown,
67 "Release all live Simulations and finalize Kokkos (deterministic teardown; also run at "
69 nb::module_::import_(
"atexit").attr(
"register")(nb::cpp_function(shutdown));
70 m.attr(
"execution_space") = nb::str(Kokkos::DefaultExecutionSpace::name());
72 nb::class_<Simulation>(m,
"Simulation")
73 .def(nb::init<int>(), nb::arg(
"capacity"))
75 "Use a uniform sphere of the given radius for all particles.")
77 nb::arg(
"radius"), nb::arg(
"height") = 0.0f, nb::arg(
"thickness") = 0.0f,
78 "Select the particle shape (sphere/cylinder/ring/...) and its dimensions.")
81 nb::arg(
"radius") = 0.5f, nb::arg(
"height") = 2.0f, nb::arg(
"thickness") = 0.0f,
82 "CUDA-API alias for initialize_shape.")
90 [](
Simulation& s, nb::ndarray<float, nb::c_contig> grid,
int nx,
int ny,
int nz,
91 std::tuple<float, float, float> origin, std::tuple<float, float, float> spacing,
92 nb::ndarray<float, nb::c_contig> shell, std::tuple<float, float, float> inv_inertia,
93 float bounding_radius) {
96 peclet::dem::F3{std::get<0>(origin), std::get<1>(origin), std::get<2>(origin)},
97 peclet::dem::F3{std::get<0>(spacing), std::get<1>(spacing), std::get<2>(spacing)},
100 std::get<2>(inv_inertia)},
103 nb::arg(
"grid"), nb::arg(
"nx"), nb::arg(
"ny"), nb::arg(
"nz"), nb::arg(
"origin"),
104 nb::arg(
"spacing"), nb::arg(
"shell"), nb::arg(
"inv_inertia"), nb::arg(
"bounding_radius"),
105 "Import a general particle: grid SDF (flat nx*ny*nz, x-fastest), surface point shell "
106 "(M,3), unit-mass principal diagonal inverse inertia, and canonical bounding radius.")
108 nb::arg(
"px") =
true, nb::arg(
"py") =
true, nb::arg(
"pz") =
false,
109 "Set the box size (lx,ly,lz) and per-axis periodicity.")
114 [](
Simulation& s, std::tuple<float, float, float> mn,
115 std::tuple<float, float, float> mx) {
119 nb::arg(
"min"), nb::arg(
"max"),
120 "Set the domain by (min, max) corner tuples (arbitrary origin); keeps current "
123 nb::arg(
"z"),
"Enable periodic boundaries per axis (x, y, z).")
125 "Return the domain minimum corner (x, y, z).")
127 "Return the domain maximum corner (x, y, z).")
129 "Set the gravitational acceleration vector (gx, gy, gz).")
131 nb::arg(
"kB") = 1.0f,
132 "Enable a Berendsen-style velocity thermostat (target temperature, coupling time tau).")
134 nb::arg(
"vel"),
"Set the XPBD position- and velocity-solve iteration counts.")
136 "Set a global length scale applied to all particles.")
139 nb::arg(
"restitution_tangent") = 0.0f, nb::arg(
"friction") = 0.0f,
140 "Set normal/tangential restitution and the Coulomb friction coefficient.")
145 [](
Simulation& s, std::tuple<float, float, float> p, std::tuple<float, float, float> n) {
146 s.
addPlane(std::get<0>(p), std::get<1>(p), std::get<2>(p), std::get<0>(n),
147 std::get<1>(n), std::get<2>(n));
149 nb::arg(
"point"), nb::arg(
"normal"),
150 "Add a boundary wall plane from a point and a normal (3-sequences).")
159 [](
Simulation& s, nb::ndarray<float, nb::c_contig> grid,
int nx,
int ny,
int nz,
160 std::tuple<float, float, float> origin, std::tuple<float, float, float> spacing,
161 float restitution,
float friction) {
164 peclet::dem::F3{std::get<0>(origin), std::get<1>(origin), std::get<2>(origin)},
165 peclet::dem::F3{std::get<0>(spacing), std::get<1>(spacing), std::get<2>(spacing)},
166 restitution, friction);
168 nb::arg(
"grid"), nb::arg(
"nx"), nb::arg(
"ny"), nb::arg(
"nz"), nb::arg(
"origin"),
169 nb::arg(
"spacing"), nb::arg(
"restitution") = 0.0f, nb::arg(
"friction") = 0.0f,
170 "Add a static world-space SDF wall/container: flat grid SDF (nx*ny*nz, x-fastest, positive "
171 "in the void), world origin/spacing, and the binary particle–wall restitution & friction. "
172 "Returns the wall index.")
177 [](
Simulation& s,
int wall_index, std::tuple<float, float, float> lin,
178 std::tuple<float, float, float> ang, std::tuple<float, float, float> center) {
183 peclet::dem::F3{std::get<0>(center), std::get<1>(center), std::get<2>(center)});
185 nb::arg(
"wall_index"), nb::arg(
"lin_vel") = std::make_tuple(0.0f, 0.0f, 0.0f),
186 nb::arg(
"ang_vel") = std::make_tuple(0.0f, 0.0f, 0.0f),
187 nb::arg(
"center") = std::make_tuple(0.0f, 0.0f, 0.0f),
188 "Set a wall's rigid-body surface velocity v(x) = lin_vel + ang_vel × (x − center) (felt by "
189 "grains in contact even though the geometry is static). Cheap; call every step for a "
195 [](
Simulation& s, nb::ndarray<float, nb::c_contig> a) {
196 if (a.ndim() == 2 && (a.shape(1) == 3 || a.shape(1) == 4)) {
197 const int n = (int)a.shape(0), k = (int)a.shape(1);
198 const float* p =
static_cast<const float*
>(a.data());
199 std::vector<float> xyz((
size_t)n * 3), im;
200 const bool hasMass = (k == 4);
203 for (
int i = 0; i < n; ++i) {
204 xyz[3 * i] = p[k * i];
205 xyz[3 * i + 1] = p[k * i + 1];
206 xyz[3 * i + 2] = p[k * i + 2];
208 float w = p[k * i + 3];
209 im[i] = (w == 0.0f) ? 1.0f : w;
219 "Set particle positions from an (N,3) array, or (N,4) where column 3 is inverse mass.")
223 "Set particle velocities from an (N,3) array.")
225 "set_external_forces",
227 "Set the per-particle external FORCE (e.g. fluid drag) from an (N,3) array. Applied each "
228 "step as dv = F*invMass*dt; persists until re-set or cleared.")
230 "Zero all per-particle external forces.")
234 "Set particle orientation quaternions from an (N,4) array.")
236 "set_angular_velocities",
237 [](
Simulation& s, nb::ndarray<float, nb::c_contig> a) {
240 "Set particle angular velocities from an (N,3) array.")
244 "Set per-particle inverse inertia from an (N,3) array.")
248 "Set per-particle inverse mass (0 => fixed/immovable).")
249 .def(
"get_angular_velocities",
253 "Set a single uniform scale for all particles.")
257 "Set per-particle scales from an array.")
259 nb::arg(
"new_factor") = -1.0f,
"Set the particle growth rate and target size factor.")
261 "Return the current particle growth factor.")
266 "Return particle positions as an (N,3) numpy array.")
269 "Return particle velocities as an (N,3) numpy array.")
275 "get_positions_view",
277 return peclet::core::python::view_to_ndarray(Kokkos::subview(
280 "Zero-copy (N,3) device array of positions (NumPy view on host, DLPack/CuPy on GPU).")
282 "get_velocities_view",
284 return peclet::core::python::view_to_ndarray(Kokkos::subview(
287 "Zero-copy (N,3) device array of velocities (NumPy view on host, DLPack/CuPy on GPU).")
289 "get_external_forces_view",
291 return peclet::core::python::view_to_ndarray(Kokkos::subview(
294 "Zero-copy (N,3) device array of the per-particle external force (NumPy view on host, "
295 "DLPack/CuPy on GPU) — write fluid drag here directly to avoid a host round-trip.")
298 "Return particle orientation quaternions as an (N,4) numpy array.")
301 "Return per-particle scales as a numpy array.")
303 "Advance the simulation one step (dt=0 uses the configured time step).")
306 [](
Simulation& s, std::tuple<int, int, int> res) {
307 auto [rx, ry, rz] = res;
310 return peclet::core::python::vector_to_ndarray(
311 s.
getSdfGrid(rx, ry, rz), {(std::size_t)rx, (std::size_t)ry, (std::size_t)rz},
312 {(std::int64_t)ry * rz, (std::int64_t)rz, 1});
314 nb::arg(
"resolution"),
315 "Reconstruct a packed-bed SDF on a (rx,ry,rz) grid (the get_sdf_grid pipeline for CFD).")
317 "Write particle state to a VTP file (ParaView/Ovito).")
328 "Export particle state to a LAMMPS dump file.")
331 [](
Simulation& s,
const std::string& filename, std::tuple<int, int, int> res) {
332 auto [rx, ry, rz] = res;
335 nb::arg(
"filename"), nb::arg(
"resolution"),
336 "Reconstruct and write the packed-bed SDF on a (rx,ry,rz) grid to a VTI file.")
338 "get_profiling_info",
347 "Return a dict of particle/contact/manifold counts and the max overlap.")
352 [](
Simulation& s, std::tuple<double, double, double> origin,
353 std::tuple<double, double, double> size, std::tuple<long, long, long> gsize,
354 std::tuple<bool, bool, bool> periodic) {
356 MPI_Initialized(&inited);
359 char** argv =
nullptr;
360 MPI_Init(&argc, &argv);
362 s.initMpi(origin, size, gsize, periodic, MPI_COMM_WORLD);
364 nb::arg(
"origin"), nb::arg(
"size"), nb::arg(
"gsize"), nb::arg(
"periodic"),
365 "Set up the ORB block decomposition + transport-core particle halo for the distributed "
367 .def(
"enable_mpi_step", &Simulation::enableMpiStep, nb::arg(
"rcut"),
368 nb::arg(
"sync_every") = 1, nb::arg(
"forward_rotation") =
true,
369 nb::arg(
"rebalance_every") = 0, nb::arg(
"verlet_skin") = 0.0,
370 "Enable the distributed step: ghost cutoff rcut, sync cadence, rotation forwarding, the "
371 "load-rebalance interval in steps (0 = fixed decomposition), and the Verlet ghost-reuse "
373 "(0 = rebuild the halo topology every substep; >0 = reuse it until a particle moves > "
375 .def(
"step_mpi", &Simulation::stepMpi, nb::arg(
"nsteps") = 1,
376 "Advance the distributed (MPI) simulation by nsteps with halo exchange.")
377 .def(
"rebalance", &Simulation::rebalance,
378 "Re-decompose by particle count and migrate ownership now; returns this rank's new "
380 .def(
"migrate_to_weights", &Simulation::migrateToWeights, nb::arg(
"weights"),
381 "Co-rebalance: migrate ownership onto the weighted ORB of per-cell weights (global "
382 "x-fastest, matching the ORB grid) -- the SAME partition the coupled flow solver "
383 "redistributes onto from the same weight field. Returns this rank's new owned count.")
384 .def(
"rank", &Simulation::rank,
"Return this rank's MPI index.")
385 .def(
"num_ghost", &Simulation::numGhost,
"Return the number of ghost particles on this rank.")
386 .def(
"mpi_rebuilds", &Simulation::mpiRebuilds,
387 "Cumulative halo topology-rebuild count (Verlet-skin path); pair with mpi_gathers() for "
388 "the ghost-reuse ratio.")
389 .def(
"mpi_gathers", &Simulation::mpiGathers,
390 "Cumulative ghost gather() count across distributed steps.")
398 [](
const std::string& filename,
int step, nb::ndarray<float, nb::c_contig> pos,
399 nb::ndarray<float, nb::c_contig> vel, nb::ndarray<float, nb::c_contig> quats,
400 nb::ndarray<float, nb::c_contig> radii,
401 std::optional<std::tuple<float, float, float>> box_min,
402 std::optional<std::tuple<float, float, float>> box_max,
bool pbc_enabled) {
403 float bmin[3], bmax[3];
404 const float *pmn =
nullptr, *pmx =
nullptr;
406 bmin[0] = std::get<0>(*box_min);
407 bmin[1] = std::get<1>(*box_min);
408 bmin[2] = std::get<2>(*box_min);
412 bmax[0] = std::get<0>(*box_max);
413 bmax[1] = std::get<1>(*box_max);
414 bmax[2] = std::get<2>(*box_max);
418 to_vec(radii), pmn, pmx, pbc_enabled);
420 nb::arg(
"filename"), nb::arg(
"step"), nb::arg(
"pos"), nb::arg(
"vel"), nb::arg(
"quats"),
421 nb::arg(
"radii"), nb::arg(
"box_min") = std::nullopt, nb::arg(
"box_max") = std::nullopt,
422 nb::arg(
"pbc_enabled") =
false,
423 "Module-level LAMMPS dump writer from raw arrays (filename, step, pos, vel, quats, radii, "