peclet-dem 0.4.0
Performance-portable XPBD Discrete Element Method (Kokkos + ArborX)
Loading...
Searching...
No Matches
dem_bindings.cpp
Go to the documentation of this file.
1
10#include <nanobind/nanobind.h>
11#include <nanobind/ndarray.h>
12#include <nanobind/stl/optional.h>
13#include <nanobind/stl/pair.h>
14#include <nanobind/stl/string.h>
15#include <nanobind/stl/tuple.h>
16#include <nanobind/stl/vector.h>
17
18#include <cstdint>
19#include <Kokkos_Core.hpp>
20#include <optional>
21#include <tuple>
22#include <vector>
23
24#ifdef PECLET_DEM_MPI
25#include <mpi.h>
26#endif
27
28#include "peclet/core/python/ndarray_interop.hpp"
29#include "sim.hpp"
30
31namespace nb = nanobind;
33
34// A contiguous numpy array -> flat C-order host vector (the bridge handles host copy / device
35// wrap).
36static std::vector<float> to_vec(nb::ndarray<float, nb::c_contig> a) {
37 return peclet::core::python::ndarray_to_vector<float>(nb::ndarray<>(a));
38}
39
40// A flat C-order vector -> (N,cols) numpy array, moved into the array's backing store (no extra
41// copy).
42static nb::ndarray<nb::numpy, float> rows(std::vector<float>&& v, int cols) {
43 const std::size_t n = v.size() / static_cast<std::size_t>(cols);
44 return peclet::core::python::vector_to_ndarray(std::move(v), {n, (std::size_t)cols},
45 {(std::int64_t)cols, 1});
46}
47static nb::ndarray<nb::numpy, float> flat(std::vector<float>&& v) {
48 const std::size_t n = v.size();
49 return peclet::core::python::vector_to_ndarray(std::move(v), {n}, {1});
50}
51
52NB_MODULE(_dem, m) {
53 m.attr("__doc__") = "DEM-GPU (Kokkos + ArborX): portable XPBD granular dynamics";
54
55 if (!Kokkos::is_initialized())
56 Kokkos::initialize();
57 // Teardown order matters on CUDA: releaseAll() drops every live Simulation's Views FIRST (so none
58 // outlive finalize -> no "deallocated after Kokkos::finalize"), THEN Kokkos::finalize() runs from
59 // a Python atexit hook while the CUDA driver is still up (so no cudaErrorCudartUnloading). Doing
60 // only one of the two aborts on CUDA. Returned arrays are backed by host std::vectors (no device
61 // Views).
62 auto shutdown = []() {
64 if (Kokkos::is_initialized() && !Kokkos::is_finalized())
65 Kokkos::finalize();
66 };
67 m.def("finalize", shutdown,
68 "Release all live Simulations and finalize Kokkos (deterministic teardown; also run at "
69 "exit).");
70 nb::module_::import_("atexit").attr("register")(nb::cpp_function(shutdown));
71 m.attr("execution_space") = nb::str(Kokkos::DefaultExecutionSpace::name());
72
73 nb::class_<Simulation>(m, "Simulation")
74 .def(nb::init<int>(), nb::arg("capacity"))
75 .def("set_sphere_shape", &Simulation::setSphereShape, nb::arg("radius"),
76 "Use a uniform sphere of the given radius for all particles.")
77 .def("initialize_shape", &Simulation::initializeShape, nb::arg("shape_type"),
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.")
80 // CUDA-API alias: initialize(shape_type, radius, height, thickness).
81 .def("initialize", &Simulation::initializeShape, nb::arg("shape_type"),
82 nb::arg("radius") = 0.5f, nb::arg("height") = 2.0f, nb::arg("thickness") = 0.0f,
83 "CUDA-API alias for initialize_shape.")
84 // Import a general particle as a grid SDF + surface point shell (all particles share it).
85 // grid: flat [nx*ny*nz] signed-distance samples, x-fastest (idx = x + y*nx + z*nx*ny), at
86 // nodes origin + (x,y,z)*spacing (negative inside). shell: (M,3) surface points. inv_inertia:
87 // unit-mass principal-frame diagonal inverse inertia. bounding_radius: canonical enclosing
88 // radius. See peclet.dem.particle_builder for the SDF -> (grid, shell, inertia) helper.
89 .def(
90 "set_sdf_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) {
96 to_vec(grid), nx, ny, nz,
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)},
99 to_vec(shell),
100 peclet::dem::F3{std::get<0>(inv_inertia), std::get<1>(inv_inertia),
101 std::get<2>(inv_inertia)},
102 bounding_radius);
103 },
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.")
108 .def("set_domain", &Simulation::setDomain, nb::arg("lx"), nb::arg("ly"), nb::arg("lz"),
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.")
111 // CUDA-API overload: set_domain(min, max) tuples (arbitrary origin); keeps current
112 // periodicity.
113 .def(
114 "set_domain",
115 [](Simulation& s, std::tuple<float, float, float> mn,
116 std::tuple<float, float, float> mx) {
117 s.setDomainMinMax(peclet::dem::F3{std::get<0>(mn), std::get<1>(mn), std::get<2>(mn)},
118 peclet::dem::F3{std::get<0>(mx), std::get<1>(mx), std::get<2>(mx)});
119 },
120 nb::arg("min"), nb::arg("max"),
121 "Set the domain by (min, max) corner tuples (arbitrary origin); keeps current "
122 "periodicity.")
123 .def("enable_periodicity", &Simulation::enablePeriodicity, nb::arg("x"), nb::arg("y"),
124 nb::arg("z"), "Enable periodic boundaries per axis (x, y, z).")
125 .def("get_domain_min", &Simulation::getDomainMin,
126 "Return the domain minimum corner (x, y, z).")
127 .def("get_domain_max", &Simulation::getDomainMax,
128 "Return the domain maximum corner (x, y, z).")
129 .def("set_gravity", &Simulation::setGravity,
130 "Set the gravitational acceleration vector (gx, gy, gz).")
131 .def("set_thermostat", &Simulation::setThermostat, nb::arg("temperature"), nb::arg("tau"),
132 nb::arg("kB") = 1.0f,
133 "Enable a Berendsen-style velocity thermostat (target temperature, coupling time tau).")
134 .def("set_solver_iterations", &Simulation::setSolverIterations, nb::arg("pos"),
135 nb::arg("vel"), "Set the XPBD position- and velocity-solve iteration counts.")
136 .def("set_hertz_material", &Simulation::setHertzMaterial, nb::arg("mat"), nb::arg("youngs"),
137 nb::arg("poisson"),
138 "Per-material Young's modulus and Poisson ratio for the soft-sphere Hertz-Mindlin "
139 "engine (material ids as in set_material_ids).")
140 .def("step_hertz", &Simulation::stepHertz, nb::arg("dt"), nb::arg("substeps") = 1,
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.")
145 .def("set_stabilization", &Simulation::setStabilization, nb::arg("enabled"),
146 "Enable/disable the stabilization pass of the staged velocity solve (default True). "
147 "Boolean form of set_stabilization_mode: True = 'onesided', False = 'off'.")
148 .def("set_stabilization_mode", &Simulation::setStabilizationMode, nb::arg("mode"),
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).")
155 .def("get_rest_orphan_stats", &Simulation::restOrphanStats,
156 "Poisson-restitution diagnostics: (sum, max, n_bodies>0) of the per-body orphaned "
157 "event budget (physical impulse units).")
158 .def("get_rest_bank_stats", &Simulation::restBankStats,
159 "Poisson-restitution diagnostics: (sum, max, n_pairs>0) of the per-pair owed "
160 "separation impulse committed last substep (physical impulse units).")
161 .def("set_restitution_model", &Simulation::setRestitutionModel, nb::arg("model"),
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.")
167 .def("set_velocity_use_gs", &Simulation::setVelocityUseGS, nb::arg("use_gs"),
168 "Select the single-GPU restitution solve: True (default) = colored Gauss–Seidel "
169 "(correct multi-contact dissipation), False = count-averaged Jacobi (legacy).")
170 .def("set_global_scale", &Simulation::setGlobalScale,
171 "Set a global length scale applied to all particles.")
172 .def("set_dt", &Simulation::setDt, "Set the time step dt.")
173 .def("set_material_params", &Simulation::setMaterialParams, nb::arg("restitution_normal"),
174 nb::arg("restitution_tangent") = 0.0f, nb::arg("friction") = 0.0f,
175 "Set normal/tangential restitution and the Coulomb friction coefficient.")
176 .def("set_material_ids", &Simulation::setMaterialIds, nb::arg("ids"),
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.")
179 .def("set_pair_material", &Simulation::setPairMaterial, nb::arg("a"), nb::arg("b"),
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.")
183 .def("set_wall_material_id", &Simulation::setWallMaterialId, nb::arg("wid"), nb::arg("mat"),
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.")
186 .def("add_plane", &Simulation::addPlane, "Add a boundary wall plane (px,py,pz, nx,ny,nz).")
187 // CUDA-API overload: add_plane(point, normal) as 3-sequences.
188 .def(
189 "add_plane",
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));
193 },
194 nb::arg("point"), nb::arg("normal"),
195 "Add a boundary wall plane from a point and a normal (3-sequences).")
196 // Static world-space SDF wall/container (drum barrel, hopper, vibrating tray). grid: flat
197 // [nx*ny*nz] signed distance, x-fastest (idx = x + y*nx + z*nx*ny), at world nodes
198 // origin+(x,y,z)*spacing — POSITIVE in the void where grains live, NEGATIVE in the solid
199 // wall. restitution/friction are the binary particle–wall material. Returns the wall index
200 // (for set_wall_velocity). See peclet.dem.build_wall_sdf for the SDF -> (grid, origin,
201 // spacing) helper.
202 .def(
203 "add_sdf_wall",
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) {
207 return s.addSdfWall(
208 to_vec(grid), nx, ny, nz,
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);
212 },
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, "
216 "positive "
217 "in the void), world origin/spacing, and the binary particle–wall restitution & "
218 "friction. "
219 "Returns the wall index.")
220 // Rigid-body surface-velocity field of a wall: v(x) = linVel + angVel × (x − center).
221 // Rotating drum: set angVel about the axis point `center`. Vibrating wall: drive linVel each
222 // step.
223 .def(
224 "set_wall_velocity",
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)},
229 peclet::dem::F3{std::get<0>(ang), std::get<1>(ang), std::get<2>(ang)},
230 peclet::dem::F3{std::get<0>(center), std::get<1>(center), std::get<2>(center)});
231 },
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 "
236 "by "
237 "grains in contact even though the geometry is static). Cheap; call every step for a "
238 "vibrating wall.")
239 // Accepts (N,3) or (N,4) like CUDA set_positions; column 3 (if present) is inv_mass (w==0
240 // -> 1.0).
241 .def(
242 "set_positions",
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);
249 if (hasMass)
250 im.resize(n);
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];
255 if (hasMass) {
256 float w = p[k * i + 3];
257 im[i] = (w == 0.0f) ? 1.0f : w;
258 }
259 }
260 s.setPositions(xyz);
261 if (hasMass)
262 s.setInvMass(im);
263 } else {
264 s.setPositions(to_vec(a)); // flat [n*3] fallback
265 }
266 },
267 "Set particle positions from an (N,3) array, or (N,4) where column 3 is inverse mass.")
268 .def(
269 "set_velocities",
270 [](Simulation& s, nb::ndarray<float, nb::c_contig> a) { s.setVelocities(to_vec(a)); },
271 "Set particle velocities from an (N,3) array.")
272 .def(
273 "set_external_forces",
274 [](Simulation& s, nb::ndarray<float, nb::c_contig> a) { s.setExternalForces(to_vec(a)); },
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.")
277 .def("clear_external_forces", &Simulation::clearExternalForces,
278 "Zero all per-particle external forces.")
279 .def(
280 "set_quaternions",
281 [](Simulation& s, nb::ndarray<float, nb::c_contig> a) { s.setQuaternions(to_vec(a)); },
282 "Set particle orientation quaternions from an (N,4) array.")
283 .def(
284 "set_angular_velocities",
285 [](Simulation& s, nb::ndarray<float, nb::c_contig> a) {
287 },
288 "Set particle angular velocities from an (N,3) array.")
289 .def(
290 "set_inv_inertia",
291 [](Simulation& s, nb::ndarray<float, nb::c_contig> a) { s.setInvInertia(to_vec(a)); },
292 "Set per-particle inverse inertia from an (N,3) array.")
293 .def(
294 "set_inv_mass",
295 [](Simulation& s, nb::ndarray<float, nb::c_contig> a) { s.setInvMass(to_vec(a)); },
296 "Set per-particle inverse mass (0 => fixed/immovable).")
297 .def("get_angular_velocities",
298 [](const Simulation& s) { return rows(s.getAngularVelocities(), 3); })
299 .def("get_inv_inertia", [](const Simulation& s) { return rows(s.getInvInertia(), 3); })
300 .def("set_scales_uniform", &Simulation::setScalesUniform,
301 "Set a single uniform scale for all particles.")
302 .def(
303 "set_scales",
304 [](Simulation& s, nb::ndarray<float, nb::c_contig> a) { s.setScales(to_vec(a)); },
305 "Set per-particle scales from an array.")
306 .def("set_growth_params", &Simulation::setGrowthParams, nb::arg("rate"),
307 nb::arg("new_factor") = -1.0f, "Set the particle growth rate and target size factor.")
308 .def("get_growth_factor", &Simulation::growthFactor,
309 "Return the current particle growth factor.")
310 .def("get_growth_rate", &Simulation::getGrowthRate, "Return the particle growth rate.")
311 .def("get_masses", [](const Simulation& s) { return flat(s.getMasses()); })
312 .def(
313 "get_positions", [](const Simulation& s) { return rows(s.getPositions(), 3); },
314 "Return particle positions as an (N,3) numpy array.")
315 .def(
316 "get_velocities", [](const Simulation& s) { return rows(s.getVelocities(), 3); },
317 "Return particle velocities as an (N,3) numpy array.")
318 // Zero-copy device export (H2): the returned (N,3) array REFERENCES the device particle Views
319 // — a NumPy view on a host backend, a DLPack/__cuda_array_interface__ array (consume with
320 // cupy.from_dlpack / torch.from_dlpack) on CUDA/HIP — so a GPU-resident analysis chain never
321 // pays the device->host copy. The array keeps the (ref-counted) View alive.
322 .def(
323 "get_positions_view",
324 [](const Simulation& s) {
325 return peclet::core::python::view_to_ndarray(Kokkos::subview(
326 s.positionsView(), Kokkos::make_pair(0, s.numParticles()), Kokkos::ALL));
327 },
328 "Zero-copy (N,3) device array of positions (NumPy view on host, DLPack/CuPy on GPU).")
329 .def(
330 "get_velocities_view",
331 [](const Simulation& s) {
332 return peclet::core::python::view_to_ndarray(Kokkos::subview(
333 s.velocitiesView(), Kokkos::make_pair(0, s.numParticles()), Kokkos::ALL));
334 },
335 "Zero-copy (N,3) device array of velocities (NumPy view on host, DLPack/CuPy on GPU).")
336 .def(
337 "get_external_forces_view",
338 [](const Simulation& s) {
339 return peclet::core::python::view_to_ndarray(Kokkos::subview(
340 s.externalForcesView(), Kokkos::make_pair(0, s.numParticles()), Kokkos::ALL));
341 },
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.")
344 .def(
345 "get_inv_mass_view",
346 [](const Simulation& s) {
347 return peclet::core::python::view_to_ndarray(
348 Kokkos::subview(s.invMassView(), Kokkos::make_pair(0, s.numParticles())));
349 },
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.")
352 .def(
353 "get_quaternions", [](const Simulation& s) { return rows(s.getQuaternions(), 4); },
354 "Return particle orientation quaternions as an (N,4) numpy array.")
355 .def(
356 "get_scales", [](const Simulation& s) { return flat(s.getScales()); },
357 "Return per-particle scales as a numpy array.")
358 .def("step", &Simulation::step, nb::arg("dt") = 0.0f,
359 "Advance the simulation one step (dt=0 uses the configured time step).")
360 .def(
361 "get_sdf_grid",
362 [](Simulation& s, std::tuple<int, int, int> res) {
363 auto [rx, ry, rz] = res;
364 // C-order (rx,ry,rz) float array, matching the prior py::array_t<float>({rx,ry,rz},
365 // ...).
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});
369 },
370 nb::arg("resolution"),
371 "Reconstruct a packed-bed SDF on a (rx,ry,rz) grid (the get_sdf_grid pipeline for CFD).")
372 .def("write_vtp", &Simulation::writeVtp, nb::arg("filename"),
373 "Write particle state to a VTP file (ParaView/Ovito).")
374 .def("num_particles", &Simulation::numParticles, "Return the number of particles.")
375 .def("num_contacts", &Simulation::numContacts, "Return the number of broad-phase contacts.")
376 .def("num_manifolds", &Simulation::numManifolds, "Return the number of contact manifolds.")
377 .def(
378 "set_sleeping", &Simulation::setSleeping, nb::arg("enabled"),
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).")
384 .def("num_asleep", &Simulation::numAsleep, "Number of currently-sleeping real bodies.")
385 .def("set_verlet_skin", &Simulation::setVerletSkin, nb::arg("skin_frac"),
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 "
388 "grain radius).")
389 .def("debug_coloring_conflicts", &Simulation::debugColoringConflicts,
390 "TEST-ONLY: (velocity, position) colouring-invariant violations in the last substep; "
391 "a valid colouring returns (0, 0).")
392 .def("max_overlap", &Simulation::maxOverlap, "Return the maximum particle-particle overlap.")
393 // CUDA-API parity: overlap measurement + LAMMPS/SDF export + profiling.
394 .def("get_num_contacts", &Simulation::numContacts) // CUDA-API alias
395 .def("get_num_manifolds", &Simulation::numManifolds) // CUDA-API alias
396 .def("get_max_overlap", &Simulation::maxOverlap) // CUDA-API alias
397 .def("compute_overlaps", &Simulation::computeOverlaps, "Recompute particle overlaps.")
398 .def("export_lammps", &Simulation::exportLammps, nb::arg("filename"), nb::arg("step"),
399 "Export particle state to a LAMMPS dump file.")
400 .def(
401 "export_sdf",
402 [](Simulation& s, const std::string& filename, std::tuple<int, int, int> res) {
403 auto [rx, ry, rz] = res;
404 s.exportSdf(filename, rx, ry, rz);
405 },
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.")
408 .def(
409 "get_profiling_info",
410 [](Simulation& s) {
411 nb::dict d;
412 d["num_particles"] = s.numParticles();
413 d["num_contacts"] = s.numContacts();
414 d["num_manifolds"] = s.numManifolds();
415 d["max_overlap"] = s.maxOverlap();
416 return d;
417 },
418 "Return a dict of particle/contact/manifold counts and the max overlap.")
419#ifdef PECLET_DEM_MPI
420 // Gated MPI step (mirrors the CUDA dem MPI binding); built only with -DDEM_MPI.
421 .def(
422 "init_mpi",
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) {
426 int inited = 0;
427 MPI_Initialized(&inited);
428 if (!inited) {
429 int argc = 0;
430 char** argv = nullptr;
431 MPI_Init(&argc, &argv);
432 }
433 s.initMpi(origin, size, gsize, periodic, MPI_COMM_WORLD);
434 },
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 "
437 "step.")
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 "
443 "skin "
444 "(0 = rebuild the halo topology every substep; >0 = reuse it until a particle moves > "
445 "skin).")
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 "
455 "owned count.")
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.")
467#endif
468 ;
469
470 // CUDA-API parity: module-level export_lammps(filename, step, pos, vel, quats, radii, box_min,
471 // box_max, pbc).
472 m.def(
473 "export_lammps",
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;
481 if (box_min) {
482 bmin[0] = std::get<0>(*box_min);
483 bmin[1] = std::get<1>(*box_min);
484 bmin[2] = std::get<2>(*box_min);
485 pmn = bmin;
486 }
487 if (box_max) {
488 bmax[0] = std::get<0>(*box_max);
489 bmax[1] = std::get<1>(*box_max);
490 bmax[2] = std::get<2>(*box_max);
491 pmx = bmax;
492 }
493 peclet::dem::writeLammpsDump(filename, step, to_vec(pos), to_vec(vel), to_vec(quats),
494 to_vec(radii), pmn, pmx, pbc_enabled);
495 },
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, "
500 "box, pbc).");
501}
Host-facing facade with std::vector setters/getters (binding-agnostic).
Definition sim.hpp:407
void setQuaternions(const std::vector< float > &q)
Definition sim.hpp:956
std::vector< float > getMasses() const
Definition sim.hpp:1013
void setSleeping(bool enabled, float threshold_scale=2.0f, int consecutive=64, float wake_scale=40.0f)
Island sleeping / freezing (single-GPU statics, default ON; PECLET_DEM_SLEEP=0 disables).
Definition sim.hpp:698
void setThermostat(float temperature, float tau, float kB)
Definition sim.hpp:633
std::tuple< float, float, float > getDomainMax() const
Definition sim.hpp:629
std::vector< float > getInvInertia() const
Definition sim.hpp:994
std::pair< int, int > debugColoringConflicts()
Definition sim.hpp:1172
int numAsleep()
Number of currently-sleeping real bodies (diagnostics / tests).
Definition sim.hpp:718
std::vector< float > getVelocities() const
Definition sim.hpp:1026
static void releaseAll()
Definition sim.hpp:454
void setPairMaterial(int a, int b, float restitution, float friction)
Set the symmetric pair material (restitution, friction) for material ids (a, b).
Definition sim.hpp:771
std::vector< float > getScales() const
Definition sim.hpp:1034
void enablePeriodicity(bool x, bool y, bool z)
Definition sim.hpp:621
std::tuple< double, float, int > restOrphanStats()
Orphan-account diagnostics: (sum, max, count>0) of the per-body orphaned budget.
Definition sim.hpp:1234
void setVelocityUseGS(bool useGS)
Definition sim.hpp:644
void setSphereShape(float radius)
Definition sim.hpp:463
void setStabilization(bool enabled)
Enable/disable the stabilization pass (default on).
Definition sim.hpp:649
std::vector< float > getQuaternions() const
Definition sim.hpp:1030
void setWallVelocity(int wallIndex, F3 linVel, F3 angVel, F3 center)
Definition sim.hpp:857
void exportSdf(const std::string &filename, int rx, int ry, int rz)
Definition sim.hpp:1062
void writeVtp(const std::string &filename) const
Definition sim.hpp:1240
void setVerletSkin(float skin_frac)
Verlet-cached impulse broadphase (single-GPU, non-periodic; default OFF).
Definition sim.hpp:713
int numParticles() const
Definition sim.hpp:1160
void setGlobalScale(float s)
Definition sim.hpp:746
void addPlane(float px, float py, float pz, float nx, float ny, float nz)
Definition sim.hpp:801
void setVelocities(const std::vector< float > &v)
Definition sim.hpp:931
float growthFactor() const
Definition sim.hpp:1009
std::tuple< float, float, float > getDomainMin() const
Definition sim.hpp:626
int addSdfWall(const std::vector< float > &grid, int nx, int ny, int nz, F3 origin, F3 spacing, float restitution, float friction)
Definition sim.hpp:818
void setPositions(const std::vector< float > &xyz)
Definition sim.hpp:867
void setInvInertia(const std::vector< float > &ii)
Definition sim.hpp:975
const Vf & invMassView() const
Definition sim.hpp:953
void setDt(float dt)
Definition sim.hpp:750
void setAngularVelocities(const std::vector< float > &w)
Definition sim.hpp:966
const V3 & externalForcesView() const
Definition sim.hpp:952
void setHertzMaterial(int mat, float youngs, float poisson)
Per-material Young's modulus + Poisson ratio for the Hertz-Mindlin engine (material ids as in setMate...
Definition sim.hpp:728
void clearExternalForces()
Definition sim.hpp:951
std::vector< float > getAngularVelocities() const
Definition sim.hpp:990
void setScalesUniform(float s)
Definition sim.hpp:915
std::vector< float > getSdfGrid(int rx, int ry, int rz)
Definition sim.hpp:1154
void step(float dt)
Definition sim.hpp:1040
void setDomain(float lx, float ly, float lz, bool px, bool py, bool pz)
Definition sim.hpp:607
void setSolverIterations(int pos, int vel)
Definition sim.hpp:638
void initializeShape(int shape_type, float radius, float height, float thickness)
Definition sim.hpp:469
const V3 & velocitiesView() const
Definition sim.hpp:1165
void setExternalForces(const std::vector< float > &f)
Definition sim.hpp:942
std::vector< float > getPositions() const
Definition sim.hpp:1022
std::tuple< double, float, int > restBankStats()
Poisson-restitution diagnostics: (sum, max, count>0) of the committed per-pair owed separation impuls...
Definition sim.hpp:1230
void setMaterialParams(float restitution_normal, float restitution_tangent, float friction)
Definition sim.hpp:754
void exportLammps(const std::string &filename, int step) const
Definition sim.hpp:1050
float getGrowthRate() const
Definition sim.hpp:1010
void setDomainMinMax(F3 mn, F3 mx)
Definition sim.hpp:612
void setMaterialIds(const std::vector< int > &ids)
Per-particle material ids (0..kMaxMaterials-1); pair (e, mu) values come from setPairMaterial.
Definition sim.hpp:762
const V3 & positionsView() const
Definition sim.hpp:1164
void setInvMass(const std::vector< float > &im)
Definition sim.hpp:984
void setSdfShape(const std::vector< float > &grid, int nx, int ny, int nz, F3 origin, F3 spacing, const std::vector< float > &shellFlat, F3 invInertia, float boundingRadius)
Definition sim.hpp:555
void setScales(const std::vector< float > &s)
Definition sim.hpp:924
void setGrowthParams(float rate, float new_factor)
Definition sim.hpp:1000
void stepHertz(float dt, int substeps, float skin_frac)
Advance substeps explicit soft-sphere Hertz-Mindlin steps of size dt (device-side loop; the (e,...
Definition sim.hpp:742
void setGravity(float gx, float gy, float gz)
Definition sim.hpp:632
void setWallMaterialId(int wid, int mat)
Give an SDF wall a material id so particle-wall (e, mu) also resolves via the pair table.
Definition sim.hpp:793
void setStabilizationMode(const std::string &mode)
Select the stabilization pass of the staged velocity solve: "off" (pure symmetric PGS),...
Definition sim.hpp:656
void setRestitutionModel(const std::string &model)
Restitution model of the PGS velocity solve: "newton" (default; per-substep restitution on the pre-so...
Definition sim.hpp:677
static std::vector< float > to_vec(nb::ndarray< float, nb::c_contig > a)
NB_MODULE(_dem, m)
static nb::ndarray< nb::numpy, float > rows(std::vector< float > &&v, int cols)
static nb::ndarray< nb::numpy, float > flat(std::vector< float > &&v)
void writeLammpsDump(const std::string &filename, int step, const std::vector< float > &pos, const std::vector< float > &vel, const std::vector< float > &quat, const std::vector< float > &radii, const float *boxMin, const float *boxMax, bool pbcEnabled)
Definition io.hpp:23
dem — portable (Kokkos) Simulation facade: the dem flip's host-facing driver.