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.
Files changed (24) hide show
  1. {peclet_coupling-0.3.0 → peclet_coupling-0.4.0}/CMakeLists.txt +10 -1
  2. {peclet_coupling-0.3.0 → peclet_coupling-0.4.0}/PKG-INFO +1 -1
  3. {peclet_coupling-0.3.0 → peclet_coupling-0.4.0}/cmake/PecletDeps.cmake +1 -1
  4. {peclet_coupling-0.3.0 → peclet_coupling-0.4.0}/pyproject.toml +1 -1
  5. peclet_coupling-0.4.0/python/peclet_coupling/__init__.py +45 -0
  6. peclet_coupling-0.4.0/python/peclet_coupling/resolved.py +176 -0
  7. {peclet_coupling-0.3.0 → peclet_coupling-0.4.0}/src/coupling_bindings.cpp +10 -4
  8. peclet_coupling-0.3.0/python/peclet_coupling/__init__.py +0 -23
  9. {peclet_coupling-0.3.0 → peclet_coupling-0.4.0}/.github/workflows/release.yml +0 -0
  10. {peclet_coupling-0.3.0 → peclet_coupling-0.4.0}/.gitignore +0 -0
  11. {peclet_coupling-0.3.0 → peclet_coupling-0.4.0}/LICENSE +0 -0
  12. {peclet_coupling-0.3.0 → peclet_coupling-0.4.0}/README.md +0 -0
  13. {peclet_coupling-0.3.0 → peclet_coupling-0.4.0}/examples/fluidized_bed.py +0 -0
  14. {peclet_coupling-0.3.0 → peclet_coupling-0.4.0}/python/peclet_coupling/driver.py +0 -0
  15. {peclet_coupling-0.3.0 → peclet_coupling-0.4.0}/src/coupling_kernels.hpp +0 -0
  16. {peclet_coupling-0.3.0 → peclet_coupling-0.4.0}/src/drag.hpp +0 -0
  17. {peclet_coupling-0.3.0 → peclet_coupling-0.4.0}/tests/CMakeLists.txt +0 -0
  18. {peclet_coupling-0.3.0 → peclet_coupling-0.4.0}/tests/smoothing_ref_eps.npy +0 -0
  19. {peclet_coupling-0.3.0 → peclet_coupling-0.4.0}/tests/test_fixed_bed_ergun.py +0 -0
  20. {peclet_coupling-0.3.0 → peclet_coupling-0.4.0}/tests/test_fixed_bed_ergun_porous.py +0 -0
  21. {peclet_coupling-0.3.0 → peclet_coupling-0.4.0}/tests/test_mpi_fixed_bed_ergun.py +0 -0
  22. {peclet_coupling-0.3.0 → peclet_coupling-0.4.0}/tests/test_mpi_moving_suspension.py +0 -0
  23. {peclet_coupling-0.3.0 → peclet_coupling-0.4.0}/tests/test_mpi_smoothing.py +0 -0
  24. {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()
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: peclet-coupling
3
- Version: 0.3.0
3
+ Version: 0.4.0
4
4
  Summary: peclet.coupling — unresolved point-particle CFD-DEM coupling of peclet.flow + peclet.dem
5
5
  Author-Email: Frank Peters <e.a.j.f.peters@gmail.com>
6
6
  License-Expression: MIT
@@ -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.5.0" CACHE STRING "Vendored core git tag (headers)")
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.3.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). Kokkos is initialised at import; the arrays are
10
- /// borrowed (owned by the caller), so there is nothing to release at exit.
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
- if (!Kokkos::is_initialized())
68
- Kokkos::initialize();
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