peclet-coupling 0.3.0__tar.gz → 0.4.0__tar.gz
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- {peclet_coupling-0.3.0 → peclet_coupling-0.4.0}/CMakeLists.txt +10 -1
- {peclet_coupling-0.3.0 → peclet_coupling-0.4.0}/PKG-INFO +1 -1
- {peclet_coupling-0.3.0 → peclet_coupling-0.4.0}/cmake/PecletDeps.cmake +1 -1
- {peclet_coupling-0.3.0 → peclet_coupling-0.4.0}/pyproject.toml +1 -1
- peclet_coupling-0.4.0/python/peclet_coupling/__init__.py +45 -0
- peclet_coupling-0.4.0/python/peclet_coupling/resolved.py +176 -0
- {peclet_coupling-0.3.0 → peclet_coupling-0.4.0}/src/coupling_bindings.cpp +10 -4
- peclet_coupling-0.3.0/python/peclet_coupling/__init__.py +0 -23
- {peclet_coupling-0.3.0 → peclet_coupling-0.4.0}/.github/workflows/release.yml +0 -0
- {peclet_coupling-0.3.0 → peclet_coupling-0.4.0}/.gitignore +0 -0
- {peclet_coupling-0.3.0 → peclet_coupling-0.4.0}/LICENSE +0 -0
- {peclet_coupling-0.3.0 → peclet_coupling-0.4.0}/README.md +0 -0
- {peclet_coupling-0.3.0 → peclet_coupling-0.4.0}/examples/fluidized_bed.py +0 -0
- {peclet_coupling-0.3.0 → peclet_coupling-0.4.0}/python/peclet_coupling/driver.py +0 -0
- {peclet_coupling-0.3.0 → peclet_coupling-0.4.0}/src/coupling_kernels.hpp +0 -0
- {peclet_coupling-0.3.0 → peclet_coupling-0.4.0}/src/drag.hpp +0 -0
- {peclet_coupling-0.3.0 → peclet_coupling-0.4.0}/tests/CMakeLists.txt +0 -0
- {peclet_coupling-0.3.0 → peclet_coupling-0.4.0}/tests/smoothing_ref_eps.npy +0 -0
- {peclet_coupling-0.3.0 → peclet_coupling-0.4.0}/tests/test_fixed_bed_ergun.py +0 -0
- {peclet_coupling-0.3.0 → peclet_coupling-0.4.0}/tests/test_fixed_bed_ergun_porous.py +0 -0
- {peclet_coupling-0.3.0 → peclet_coupling-0.4.0}/tests/test_mpi_fixed_bed_ergun.py +0 -0
- {peclet_coupling-0.3.0 → peclet_coupling-0.4.0}/tests/test_mpi_moving_suspension.py +0 -0
- {peclet_coupling-0.3.0 → peclet_coupling-0.4.0}/tests/test_mpi_smoothing.py +0 -0
- {peclet_coupling-0.3.0 → peclet_coupling-0.4.0}/tests/test_terminal_velocity.py +0 -0
|
@@ -32,10 +32,18 @@ configure_file(${CMAKE_CURRENT_SOURCE_DIR}/python/peclet_coupling/__init__.py
|
|
|
32
32
|
${CMAKE_CURRENT_BINARY_DIR}/peclet/coupling/__init__.py COPYONLY)
|
|
33
33
|
configure_file(${CMAKE_CURRENT_SOURCE_DIR}/python/peclet_coupling/driver.py
|
|
34
34
|
${CMAKE_CURRENT_BINARY_DIR}/peclet/coupling/driver.py COPYONLY)
|
|
35
|
+
# resolved.py is imported by __init__.py, so omitting it makes a dev build unimportable.
|
|
36
|
+
configure_file(${CMAKE_CURRENT_SOURCE_DIR}/python/peclet_coupling/resolved.py
|
|
37
|
+
${CMAKE_CURRENT_BINARY_DIR}/peclet/coupling/resolved.py COPYONLY)
|
|
35
38
|
target_include_directories(coupling PRIVATE src "${CORE_INC}")
|
|
36
39
|
target_link_libraries(coupling PRIVATE Kokkos::kokkos)
|
|
37
40
|
if(Kokkos_ENABLE_HIP)
|
|
38
41
|
target_link_options(coupling PRIVATE -Wl,--no-gc-sections)
|
|
42
|
+
# HIP/lld experiment 1 (docs/RELEASE.md §8): nanobind sets CXX_VISIBILITY_PRESET hidden, and under hipcc
|
|
43
|
+
# the host objects then reference the Kokkos SharedAllocationRecord / shared_ptr control-block vtables
|
|
44
|
+
# as hidden symbols that nothing defines ("undefined hidden symbol: vtable for ..."). Default visibility
|
|
45
|
+
# on the HIP path only; CUDA/OpenMP builds are untouched.
|
|
46
|
+
set_target_properties(coupling PROPERTIES CXX_VISIBILITY_PRESET default)
|
|
39
47
|
endif()
|
|
40
48
|
|
|
41
49
|
option(PECLET_COUPLING_TESTS "Register the Python CFD-DEM integration tests as ctests" OFF)
|
|
@@ -45,12 +53,13 @@ if(PECLET_COUPLING_TESTS)
|
|
|
45
53
|
endif()
|
|
46
54
|
|
|
47
55
|
# --- pip / scikit-build-core install rule --------------------------------------------------------
|
|
48
|
-
# Places the extension + the peclet/coupling package (__init__.py + driver.py) into the PEP-420
|
|
56
|
+
# Places the extension + the peclet/coupling package (__init__.py + driver.py + resolved.py) into the PEP-420
|
|
49
57
|
# peclet namespace. In an sdist build the core headers + nanobind come from PecletDeps (fetched at
|
|
50
58
|
# the pinned tag), so the source distribution is self-contained.
|
|
51
59
|
if(DEFINED SKBUILD)
|
|
52
60
|
install(TARGETS coupling LIBRARY DESTINATION peclet/coupling COMPONENT python)
|
|
53
61
|
install(FILES ${CMAKE_CURRENT_SOURCE_DIR}/python/peclet_coupling/__init__.py
|
|
54
62
|
${CMAKE_CURRENT_SOURCE_DIR}/python/peclet_coupling/driver.py
|
|
63
|
+
${CMAKE_CURRENT_SOURCE_DIR}/python/peclet_coupling/resolved.py
|
|
55
64
|
DESTINATION peclet/coupling COMPONENT python)
|
|
56
65
|
endif()
|
|
@@ -16,7 +16,7 @@ include(FetchContent)
|
|
|
16
16
|
|
|
17
17
|
set(PECLET_KOKKOS_TAG "5.1.1" CACHE STRING "Vendored Kokkos git tag")
|
|
18
18
|
set(PECLET_ARBORX_TAG "v2.1" CACHE STRING "Vendored ArborX git tag")
|
|
19
|
-
set(PECLET_TPX_TAG "v0.
|
|
19
|
+
set(PECLET_TPX_TAG "v0.6.0" CACHE STRING "Vendored core git tag (headers)")
|
|
20
20
|
set(PECLET_MORTON_TAG "v0.2.1" CACHE STRING "Vendored morton git tag (headers)")
|
|
21
21
|
option(PECLET_VENDOR_DEPS "Force FetchContent-build of Kokkos/ArborX/siblings (self-contained wheel)" OFF)
|
|
22
22
|
|
|
@@ -16,7 +16,7 @@ build-backend = "scikit_build_core.build"
|
|
|
16
16
|
|
|
17
17
|
[project]
|
|
18
18
|
name = "peclet-coupling"
|
|
19
|
-
version = "0.
|
|
19
|
+
version = "0.4.0"
|
|
20
20
|
description = "peclet.coupling — unresolved point-particle CFD-DEM coupling of peclet.flow + peclet.dem"
|
|
21
21
|
readme = "README.md"
|
|
22
22
|
requires-python = ">=3.10"
|
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
"""peclet.coupling — CFD-DEM coupling, unresolved and resolved.
|
|
2
|
+
|
|
3
|
+
Composes peclet.flow (Eulerian fluid) + peclet.dem (Lagrangian particles).
|
|
4
|
+
|
|
5
|
+
CfdDem is the UNRESOLVED point-particle driver: a grain is a point with a drag closure, and the
|
|
6
|
+
compute kernels (particle<->grid deposition, drag laws, momentum feedback) live in the _coupling
|
|
7
|
+
extension, running in place on the arrays the two solvers expose (zero-copy grid fields; particle
|
|
8
|
+
forces round-tripped through the dem host API).
|
|
9
|
+
|
|
10
|
+
ResolvedCfdDem is the RESOLVED driver (Layer 4 of suite/docs/ANALYTIC_SDF_GEOMETRY.md): each grain
|
|
11
|
+
IS an analytic SDF instance in the flow solver's scene, the fluid resolves its surface, and the
|
|
12
|
+
coupling is a surface-traction exchange with no drag correlation in it. Pure Python -- it needs no
|
|
13
|
+
compiled kernels of its own.
|
|
14
|
+
"""
|
|
15
|
+
# The compiled kernels are needed by the UNRESOLVED driver only. ResolvedCfdDem is pure Python
|
|
16
|
+
# composing peclet.flow + peclet.dem -- it has no C++ dependency at all -- so a missing extension
|
|
17
|
+
# must not make it unimportable. CfdDem re-imports _coupling in its own constructor, where the
|
|
18
|
+
# failure is still immediate and its message still points at the right thing.
|
|
19
|
+
try:
|
|
20
|
+
from . import _coupling # noqa: F401 (deposit_solid_volume, compute_void_fraction, ...)
|
|
21
|
+
except ImportError: # pragma: no cover
|
|
22
|
+
_coupling = None
|
|
23
|
+
from .driver import CfdDem # noqa: F401
|
|
24
|
+
from .resolved import ResolvedCfdDem # noqa: F401
|
|
25
|
+
|
|
26
|
+
DRAG_STOKES = 0
|
|
27
|
+
DRAG_SCHILLER_NAUMANN = 1
|
|
28
|
+
DRAG_ERGUN = 2
|
|
29
|
+
DRAG_DI_FELICE = 3
|
|
30
|
+
DRAG_WEN_YU = 4
|
|
31
|
+
DRAG_GIDASPOW = 5 # Ergun (dense) + Wen & Yu (dilute), switched at eps = 0.8
|
|
32
|
+
DRAG_BEETSTRA = 6 # Beetstra-van der Hoef-Kuipers (2007) DNS drag — the published "BVK2"
|
|
33
|
+
DRAG_TANG = 7 # Tang et al. (2015) DNS drag — what MFIX-Exa's "BVK2" option actually executes
|
|
34
|
+
|
|
35
|
+
# The installed distribution's metadata (pyproject.toml) is the single source of truth for the version;
|
|
36
|
+
# a build-tree import (PYTHONPATH=<build>) has no metadata and reports "0+unknown". This replaces a
|
|
37
|
+
# hand-maintained literal that had drifted behind pyproject.toml in every package at 0.6.0.
|
|
38
|
+
try:
|
|
39
|
+
from importlib.metadata import version as _dist_version
|
|
40
|
+
__version__ = _dist_version("peclet-coupling")
|
|
41
|
+
except Exception: # PackageNotFoundError (dev build), or a broken metadata install
|
|
42
|
+
__version__ = "0+unknown"
|
|
43
|
+
|
|
44
|
+
__all__ = ["CfdDem", "ResolvedCfdDem", "_coupling", "DRAG_STOKES", "DRAG_SCHILLER_NAUMANN", "DRAG_ERGUN",
|
|
45
|
+
"DRAG_DI_FELICE", "DRAG_WEN_YU", "DRAG_GIDASPOW", "DRAG_BEETSTRA", "DRAG_TANG"]
|
|
@@ -0,0 +1,176 @@
|
|
|
1
|
+
"""ResolvedCfdDem -- resolved (geometry-resolving) CFD-DEM.
|
|
2
|
+
|
|
3
|
+
Layer 4 of suite/docs/ANALYTIC_SDF_GEOMETRY.md. Where `CfdDem` treats a grain as a point with a
|
|
4
|
+
drag closure, this driver makes each grain an ANALYTIC SDF INSTANCE in the flow solver's scene: the
|
|
5
|
+
fluid resolves the actual surface, no-slip is enforced on the moving wall by the cut-cell IBM
|
|
6
|
+
(Layer 3 rung 2), the projection carries the wall's own volume flux (rung 3), and the coupling is a hydrodynamic-load exchange (rung L4-R2) with no drag correlation anywhere in it:
|
|
7
|
+
by default the DISCRETE REACTION (route (b) of the design note's OPEN FOR REVIEW 1) -- the momentum
|
|
8
|
+
the fluid actually lost to each grain, exactly conservative -- with the reconstructed traction
|
|
9
|
+
integral available as force_method="traction" for diagnostics.
|
|
10
|
+
|
|
11
|
+
Python-composed, like `CfdDem` and for the same reason: dem and flow stay separate method codes and
|
|
12
|
+
nothing links them in C++. Per coupling step:
|
|
13
|
+
|
|
14
|
+
1. pull dem state (positions / quaternions / velocities / angular velocities) and push it into
|
|
15
|
+
flow's scene -- instance transforms plus rung-2 rigid-body motion;
|
|
16
|
+
2. flow.rebuild_geometry() -- re-derive SDF, cut-cell overlay, apertures and pressure operator;
|
|
17
|
+
the velocity and pressure fields survive it;
|
|
18
|
+
3. flow.step();
|
|
19
|
+
4. flow.hydro_force_torque() -> per-grain force; add gravity/buoyancy; hand to dem;
|
|
20
|
+
5. dem sub-steps at the DEM timestep, holding the fluid force constant.
|
|
21
|
+
|
|
22
|
+
WEAK, EXPLICIT COUPLING (spec: L4-R3). The fluid force is lagged by one fluid step. That is the
|
|
23
|
+
documented v1; a strongly-coupled variant would iterate 3-5 inside the step.
|
|
24
|
+
|
|
25
|
+
FLOAT/DOUBLE BOUNDARY. dem carries float32 state, flow's scene is float64. The per-step instance
|
|
26
|
+
rebuild converts; "zero-copy" is not literal across that divide, and does not need to be -- the
|
|
27
|
+
instance array is a few hundred bytes per grain against a geometry rebuild measured in tens of ms.
|
|
28
|
+
|
|
29
|
+
UNITS. Everything is in flow's grid units: cell spacing 1, cell (i,j,k) centred at (i,j,k), so dem
|
|
30
|
+
positions and radii must be expressed in cells.
|
|
31
|
+
|
|
32
|
+
HYDRODYNAMIC TORQUE (validated 2026-08-31). flow's reaction torque now carries the
|
|
33
|
+
transposed-stress wall term (flow `16e91ec`) and is GATED: a spinning sphere reproduces the exact
|
|
34
|
+
Stokes torque 8*pi*mu*a^3*Omega to +3.5/+2.4/+2.2% (converging with the aperture first moment),
|
|
35
|
+
and THIS loop with apply_torque=True decays a freely spinning sphere at 1.039x the same box's
|
|
36
|
+
calibrated rotational drag (rotation_gate.py part B). Before that term the torque was a structural
|
|
37
|
+
-31% -- the missing traction mu*(n x Omega) integrates to zero in the FORCE, which is why no
|
|
38
|
+
force-based gate ever saw it.
|
|
39
|
+
|
|
40
|
+
apply_torque stays **off by default** for one remaining reason: dem assigns a DEFAULT inverse
|
|
41
|
+
inertia unrelated to the grain's size, so handing a torque to a grain whose inertia was never set
|
|
42
|
+
spins it up at an arbitrary rate (the settling gate diverged to 1e+09 that way). Set the physical
|
|
43
|
+
principal inertia FIRST -- (2/5) m R^2 for a sphere, or `scene_particle`'s `inv_inertia_unit` --
|
|
44
|
+
then enable. The torque is computed and reported through `torques()` either way.
|
|
45
|
+
|
|
46
|
+
"""
|
|
47
|
+
import numpy as np
|
|
48
|
+
|
|
49
|
+
|
|
50
|
+
class ResolvedCfdDem:
|
|
51
|
+
def __init__(self, flow, dem, *, radius, mu, rho_f, fluid_dt, dem_substeps=20,
|
|
52
|
+
periodic=True, gravity=(0.0, 0.0, 0.0), rho_p=None, move=True,
|
|
53
|
+
buoyancy=True, apply_torque=False,
|
|
54
|
+
force_method="reaction"):
|
|
55
|
+
self.flow = flow
|
|
56
|
+
self.dem = dem
|
|
57
|
+
self.mu = float(mu)
|
|
58
|
+
self.rho_f = float(rho_f)
|
|
59
|
+
self.fluid_dt = float(fluid_dt)
|
|
60
|
+
self.dem_substeps = int(dem_substeps)
|
|
61
|
+
self.dt_dem = self.fluid_dt / self.dem_substeps
|
|
62
|
+
self.gravity = np.asarray(gravity, dtype=np.float64)
|
|
63
|
+
self.move = bool(move)
|
|
64
|
+
self.buoyancy = bool(buoyancy)
|
|
65
|
+
self.radius = float(radius)
|
|
66
|
+
self.rho_p = float(rho_p) if rho_p is not None else None
|
|
67
|
+
self.periodic = bool(periodic)
|
|
68
|
+
# Hand the reaction TORQUE to dem as well as the force (dem R2, set_external_torques).
|
|
69
|
+
# The torque is VALIDATED (rotating-sphere + spin-decay gates, see the class docstring);
|
|
70
|
+
# off by default only because dem's default inverse inertia is not the grain's -- set a
|
|
71
|
+
# physical principal inertia first, then enable.
|
|
72
|
+
self.apply_torque = bool(apply_torque)
|
|
73
|
+
# "reaction" (default): the discrete-reaction force -- exactly conservative (the momentum
|
|
74
|
+
# the fluid lost IS the momentum the grain gains) and as accurate as the flow solution it
|
|
75
|
+
# sustains. "traction": the reconstructed surface integral, kept as a diagnostic; it
|
|
76
|
+
# under-reads the drag by a resolution-independent ~29% (measured), which in this loop
|
|
77
|
+
# shows up as a total-momentum leak. See suite/docs/ANALYTIC_SDF_GEOMETRY.md OPEN FOR
|
|
78
|
+
# REVIEW 1.
|
|
79
|
+
if force_method not in ("reaction", "traction"):
|
|
80
|
+
raise ValueError("force_method must be 'reaction' or 'traction'")
|
|
81
|
+
self.force_method = force_method
|
|
82
|
+
self.n = int(dem.num_particles())
|
|
83
|
+
self.last_force = np.zeros((self.n, 3))
|
|
84
|
+
self.last_torque = np.zeros((self.n, 3))
|
|
85
|
+
flow.set_dt(self.fluid_dt)
|
|
86
|
+
dem.set_dt(self.dt_dem) # kept in sync, though step(dt) below passes it explicitly
|
|
87
|
+
self._install_scene()
|
|
88
|
+
|
|
89
|
+
# --- L4-R1: the dem -> scene bridge --------------------------------------------------------
|
|
90
|
+
_KN_R, _KI_I, _KI_R = 16, 2, 17
|
|
91
|
+
|
|
92
|
+
def _install_scene(self):
|
|
93
|
+
"""One kSphere node, one instance per grain. Spheres first, per the spec; a shaped grain is
|
|
94
|
+
the same bridge with the grain's own node tree in place of the sphere leaf."""
|
|
95
|
+
node_ints = np.array([1, -1, -1], dtype=np.int32) # kSphere
|
|
96
|
+
node_reals = np.zeros(self._KN_R)
|
|
97
|
+
node_reals[0] = self.radius
|
|
98
|
+
node_reals[14] = 1.0 # quaternion w
|
|
99
|
+
node_reals[15] = 1.0 # scale
|
|
100
|
+
ii, ir = self._instance_arrays()
|
|
101
|
+
self.flow.set_scene(node_ints, node_reals, ii.ravel(), ir.ravel(), periodic=self.periodic)
|
|
102
|
+
self._push_motion()
|
|
103
|
+
self.flow.set_solid_from_scene(True)
|
|
104
|
+
|
|
105
|
+
def _instance_arrays(self):
|
|
106
|
+
pos = np.asarray(self.dem.get_positions(), dtype=np.float64)
|
|
107
|
+
quat = np.asarray(self.dem.get_quaternions(), dtype=np.float64)
|
|
108
|
+
ii = np.zeros((self.n, self._KI_I), dtype=np.int32)
|
|
109
|
+
ir = np.zeros((self.n, self._KI_R))
|
|
110
|
+
ii[:, 0] = 0 # shapeRoot
|
|
111
|
+
ii[:, 1] = -1 # materialId
|
|
112
|
+
ir[:, 0:3] = pos[:, 0:3]
|
|
113
|
+
ir[:, 3:7] = quat[:, 0:4] # (x,y,z,w)
|
|
114
|
+
ir[:, 7] = 1.0 # scale
|
|
115
|
+
return ii, ir
|
|
116
|
+
|
|
117
|
+
def _push_motion(self):
|
|
118
|
+
"""Instance transforms + rigid-body velocities, straight from dem state."""
|
|
119
|
+
pos = np.asarray(self.dem.get_positions(), dtype=np.float64)
|
|
120
|
+
quat = np.asarray(self.dem.get_quaternions(), dtype=np.float64)
|
|
121
|
+
vel = np.asarray(self.dem.get_velocities(), dtype=np.float64)
|
|
122
|
+
omg = np.asarray(self.dem.get_angular_velocities(), dtype=np.float64)
|
|
123
|
+
for i in range(self.n):
|
|
124
|
+
self.flow.set_instance_transform(i, pos[i, 0:3].tolist(), quat[i, 0:4].tolist())
|
|
125
|
+
self.flow.set_instance_motion(i, lin_vel=vel[i, 0:3].tolist(),
|
|
126
|
+
ang_vel=omg[i, 0:3].tolist())
|
|
127
|
+
|
|
128
|
+
# --- L4-R3: the motion loop ----------------------------------------------------------------
|
|
129
|
+
def step(self):
|
|
130
|
+
if self.move:
|
|
131
|
+
self._push_motion()
|
|
132
|
+
self.flow.rebuild_geometry()
|
|
133
|
+
self.flow.step()
|
|
134
|
+
if self.force_method == "reaction":
|
|
135
|
+
ft = np.asarray(self.flow.hydro_force_torque_reaction()) # (2, n, 3): F, tau
|
|
136
|
+
self.last_force = np.array(ft[0][: self.n], dtype=np.float64)
|
|
137
|
+
self.last_torque = np.array(ft[1][: self.n], dtype=np.float64)
|
|
138
|
+
self.last_force_pressure = None # the reaction has no pressure/viscous split
|
|
139
|
+
self.last_force_viscous = None
|
|
140
|
+
else:
|
|
141
|
+
ft = np.asarray(self.flow.hydro_force_torque()) # (4, n, 3): F, tau, F_p, F_visc
|
|
142
|
+
self.last_force = np.array(ft[0][: self.n], dtype=np.float64)
|
|
143
|
+
self.last_torque = np.array(ft[1][: self.n], dtype=np.float64)
|
|
144
|
+
self.last_force_pressure = np.array(ft[2][: self.n], dtype=np.float64)
|
|
145
|
+
self.last_force_viscous = np.array(ft[3][: self.n], dtype=np.float64)
|
|
146
|
+
if not self.move:
|
|
147
|
+
return
|
|
148
|
+
F = self.last_force.copy()
|
|
149
|
+
if self.buoyancy and self.rho_p is not None:
|
|
150
|
+
# The resolved traction already contains the hydrostatic part of the pressure field, so
|
|
151
|
+
# what is added here is only the BODY force on the grain itself. Net gravity on a grain
|
|
152
|
+
# of density rho_p displacing rho_f is (rho_p - rho_f) V g when the fluid carries the
|
|
153
|
+
# hydrostatic gradient; when it does not (the usual periodic set-up, no gravity in the
|
|
154
|
+
# fluid), the full rho_p V g applies. buoyancy=False selects the latter.
|
|
155
|
+
V = 4.0 / 3.0 * np.pi * self.radius**3
|
|
156
|
+
F += (self.rho_p - self.rho_f) * V * self.gravity
|
|
157
|
+
elif self.rho_p is not None:
|
|
158
|
+
V = 4.0 / 3.0 * np.pi * self.radius**3
|
|
159
|
+
F += self.rho_p * V * self.gravity
|
|
160
|
+
self.dem.set_external_forces(np.ascontiguousarray(F, dtype=np.float32))
|
|
161
|
+
if self.apply_torque:
|
|
162
|
+
# World-frame torque; dem rotates it into the body frame in the predictor. Held
|
|
163
|
+
# constant over the sub-steps, exactly like the force.
|
|
164
|
+
self.dem.set_external_torques(
|
|
165
|
+
np.ascontiguousarray(self.last_torque, dtype=np.float32))
|
|
166
|
+
for _ in range(self.dem_substeps):
|
|
167
|
+
# dt MUST be passed explicitly. dem's step(dt=0) is a dynamics-free relaxation step
|
|
168
|
+
# (overlap removal only), so step() with no argument advances nothing and the driver
|
|
169
|
+
# would run happily with a frozen particle.
|
|
170
|
+
self.dem.step(self.dt_dem)
|
|
171
|
+
|
|
172
|
+
def forces(self):
|
|
173
|
+
return self.last_force
|
|
174
|
+
|
|
175
|
+
def torques(self):
|
|
176
|
+
return self.last_torque
|
|
@@ -6,8 +6,12 @@
|
|
|
6
6
|
/// so a coupled run never links the two solvers in C++ — the Python CfdDem driver composes them.
|
|
7
7
|
/// Particle arrays are float32 (dem SoA precision), grid fields float64 (flow field precision); each
|
|
8
8
|
/// array is wrapped as an unmanaged Kokkos View over the SAME memory (zero-copy on host and, for
|
|
9
|
-
/// DLPack device arrays on a GPU build, on device).
|
|
10
|
-
///
|
|
9
|
+
/// DLPack device arrays on a GPU build, on device). The arrays are borrowed (owned by the caller),
|
|
10
|
+
/// so this module owns no Kokkos Views and its teardown registry stays empty; it still follows the
|
|
11
|
+
/// suite-wide pattern of peclet/core/python/kokkos_teardown.hpp (Kokkos initialised at import, one
|
|
12
|
+
/// atexit hook that releases and THEN finalizes -- required on CUDA, where a Kokkos left to its
|
|
13
|
+
/// static destructors aborts with cudaErrorCudartUnloading after the runtime unloads -- plus
|
|
14
|
+
/// `finalize()` and `execution_space`).
|
|
11
15
|
#include <nanobind/nanobind.h>
|
|
12
16
|
#include <nanobind/ndarray.h>
|
|
13
17
|
|
|
@@ -15,6 +19,7 @@
|
|
|
15
19
|
#include <stdexcept>
|
|
16
20
|
|
|
17
21
|
#include "coupling_kernels.hpp"
|
|
22
|
+
#include "peclet/core/python/kokkos_teardown.hpp"
|
|
18
23
|
#include "peclet/core/python/ndarray_interop.hpp"
|
|
19
24
|
|
|
20
25
|
namespace nb = nanobind;
|
|
@@ -64,8 +69,9 @@ GridMap gmap(double ox, double oy, double oz, double h, int ex, int ey, int ez,
|
|
|
64
69
|
|
|
65
70
|
NB_MODULE(_coupling, m) {
|
|
66
71
|
m.doc() = "CFD-DEM coupling kernels (deposition, drag, momentum feedback) on shared arrays.";
|
|
67
|
-
|
|
68
|
-
|
|
72
|
+
// Kokkos init + the release-then-finalize atexit hook + finalize() + execution_space (the
|
|
73
|
+
// suite-wide teardown pattern; see the file comment).
|
|
74
|
+
peclet::core::python::install(m);
|
|
69
75
|
|
|
70
76
|
m.def(
|
|
71
77
|
"deposit_solid_volume",
|
|
@@ -1,23 +0,0 @@
|
|
|
1
|
-
"""peclet.coupling — unresolved point-particle CFD-DEM coupling.
|
|
2
|
-
|
|
3
|
-
Composes peclet.flow (Eulerian fluid) + peclet.dem (Lagrangian particles) via the CfdDem driver.
|
|
4
|
-
The compute kernels (particle<->grid deposition, drag laws, momentum feedback) live in the _coupling
|
|
5
|
-
extension and run in place on the arrays the two solvers expose (zero-copy grid fields; particle
|
|
6
|
-
forces round-tripped through the dem host API).
|
|
7
|
-
"""
|
|
8
|
-
from . import _coupling # noqa: F401 (deposit_solid_volume, compute_void_fraction, compute_drag_feedback)
|
|
9
|
-
from .driver import CfdDem # noqa: F401
|
|
10
|
-
|
|
11
|
-
DRAG_STOKES = 0
|
|
12
|
-
DRAG_SCHILLER_NAUMANN = 1
|
|
13
|
-
DRAG_ERGUN = 2
|
|
14
|
-
DRAG_DI_FELICE = 3
|
|
15
|
-
DRAG_WEN_YU = 4
|
|
16
|
-
DRAG_GIDASPOW = 5 # Ergun (dense) + Wen & Yu (dilute), switched at eps = 0.8
|
|
17
|
-
DRAG_BEETSTRA = 6 # Beetstra-van der Hoef-Kuipers (2007) DNS drag — the published "BVK2"
|
|
18
|
-
DRAG_TANG = 7 # Tang et al. (2015) DNS drag — what MFIX-Exa's "BVK2" option actually executes
|
|
19
|
-
|
|
20
|
-
__version__ = "0.2.0"
|
|
21
|
-
|
|
22
|
-
__all__ = ["CfdDem", "_coupling", "DRAG_STOKES", "DRAG_SCHILLER_NAUMANN", "DRAG_ERGUN",
|
|
23
|
-
"DRAG_DI_FELICE", "DRAG_WEN_YU", "DRAG_GIDASPOW", "DRAG_BEETSTRA", "DRAG_TANG"]
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|