flow
Kokkos cut-cell IBM incompressible Navier-Stokes solver + pnm pore extraction
Loading...
Searching...
No Matches
flow_bindings.cpp
Go to the documentation of this file.
1
17#include <nanobind/nanobind.h>
18#include <nanobind/ndarray.h>
19#include <nanobind/stl/pair.h>
20#include <nanobind/stl/string.h>
21#include <nanobind/stl/vector.h>
22
23#include <array>
24#include <cstdint>
25#include <Kokkos_Core.hpp>
26#include <string>
27#include <vector>
28
29#ifdef PECLET_FLOW_MPI
30#include <mpi.h>
31
32#include "peclet/core/common/types.hpp"
33#include "peclet/core/decomp/block_decomposer.hpp"
34#endif
35
36#include "flow_ibm.hpp"
37#include "peclet/core/python/ndarray_interop.hpp"
38
39#ifdef PECLET_FLOW_MPI
40// Ensure MPI_Init has been called (mirrors the dem init_mpi idiom); safe to call repeatedly.
41static void ensure_mpi_init() {
42 int inited = 0;
43 MPI_Initialized(&inited);
44 if (!inited) {
45 int argc = 0;
46 char** argv = nullptr;
47 MPI_Init(&argc, &argv);
48 }
49}
50#endif
51
52namespace nb = nanobind;
53
54// A solver field (flat x-fastest, ghost-stripped) -> Fortran-order (nx,ny,nz) float64 NumPy array
55// for [x,y,z] indexing. The vector is moved into the array's backing store (no extra copy vs the
56// old to_xyz).
57template <class S>
58static nb::ndarray<nb::numpy, double> field_out(S& s, std::vector<double>&& v) {
59 const auto nx = static_cast<std::size_t>(s.nx());
60 const auto ny = static_cast<std::size_t>(s.ny());
61 const auto nz = static_cast<std::size_t>(s.nz());
62 return peclet::core::python::vector_to_ndarray(
63 std::move(v), {nx, ny, nz},
64 {1, static_cast<std::int64_t>(nx), static_cast<std::int64_t>(nx * ny)});
65}
66
67// A Fortran-order (nx,ny,nz) float64 array -> flat x-fastest host vector (F-contiguous data() is
68// already x-fastest). nanobind casts/copies the input to f_contig double if needed.
69static std::vector<double> grid_in(nb::ndarray<double, nb::f_contig> a) {
70 return peclet::core::python::ndarray_to_vector<double>(nb::ndarray<>(a));
71}
72
73// Zero-copy export of a registered field's padded device buffer as a Fortran-order 3-D array of the
74// full block shape (ex,ey,ez) = (nx+2G, ny+2G, nz+2G), x-fastest strides {1,ex,ex*ey}. Includes the
75// ghost band (the flat buffer is contiguous; a ghost-stripped view would not be). The capsule owns
76// a copy of the managed CCField, so the allocation outlives the array — host → NumPy referencing
77// the buffer, device → DLPack for CuPy/torch. Mirrors peclet::core::python::view_to_ndarray but
78// with an explicit 3-D reshape of the flat 1-D field.
79template <class S>
80static auto field3d_out(S& s, peclet::flow::CCField f) {
81 namespace pcp = peclet::core::python;
83 const auto bs = s.blockShape();
84 std::array<std::size_t, 3> shape{static_cast<std::size_t>(bs[0]), static_cast<std::size_t>(bs[1]),
85 static_cast<std::size_t>(bs[2])};
86 std::array<std::int64_t, 3> strides{1, static_cast<std::int64_t>(bs[0]),
87 static_cast<std::int64_t>(bs[0]) * bs[1]};
88 auto* held = new peclet::flow::CCField(f);
89 nb::capsule owner(held, [](void* p) noexcept { delete static_cast<peclet::flow::CCField*>(p); });
90 double* data = f.data();
91 if constexpr (pcp::is_host_space_v<Mem>) {
92 return nb::ndarray<nb::numpy, double>(data, 3, shape.data(), owner, strides.data(),
93 nb::dtype<double>(), nb::device::cpu::value, 0);
94 } else {
95 auto dev = pcp::dlpack_device<Mem>();
96 return nb::ndarray<double>(data, 3, shape.data(), owner, strides.data(), nb::dtype<double>(),
97 dev.first, dev.second);
98 }
99}
100
101// Register a solver class for the given GridLayout policy (Staggered -> "Solver", Colocated ->
102// "SolverColocated"). The Python API is identical across grids; only the velocity-unknown placement
103// and the advection control volume differ inside Solver<Grid>.
104template <class Grid>
105static void bind_solver(nb::module_& m, const char* name) {
107 nb::class_<S>(m, name)
108 .def(nb::init<int, int, int>(), nb::arg("nx"), nb::arg("ny"), nb::arg("nz"),
109 "Create a solver on an nx x ny x nz unit-spacing grid (x-fastest, I = x + y*nx + "
110 "z*nx*ny). "
111 "Set physical parameters (rho/mu/dt) and any domain BCs before the geometry / first "
112 "step.")
113 .def("set_rho", &S::setRho, nb::arg("rho"),
114 "Set fluid density rho (physical units). Set before geometry/first step.")
115 .def("set_mu", &S::setMu, nb::arg("mu"), "Set dynamic viscosity mu (physical units).")
116 .def("set_dt", &S::setDt, nb::arg("dt"),
117 "Set the time step dt; the momentum solve is scaled by 1/dt (well-conditioned at large "
118 "dt).")
119 .def("set_body_force", &S::setBodyForce, nb::arg("fx"), nb::arg("fy"), nb::arg("fz"),
120 "Set the body force per unit volume (fx, fy, fz) — e.g. a mean pressure gradient.")
121 .def("set_advection", &S::setAdvection, nb::arg("on"),
122 "Enable/disable explicit high-order momentum advection (default scheme SOU). Off ⇒ "
123 "Stokes.")
124 .def("set_advection_scheme", &S::setAdvectionScheme, nb::arg("scheme"),
125 "High-order advection scheme: 0 = second-order upwind (SOU, default), 1 = Koren TVD.")
126 .def("set_incremental_pressure", &S::setIncrementalPressure, nb::arg("on"),
127 "Toggle the rotational incremental-pressure projection.")
128 .def("set_pressure_warmstart", &S::setPressureWarmstart, nb::arg("on"),
129 "Seed each pressure solve from the previous step's phi (default off).")
130 .def(
131 "set_face_interp", &S::setFaceInterp, nb::arg("mode"),
132 "Collocated cut-cell projection treatment: 0 = plain averaging + central-difference "
133 "grad(P) (default), 1 = wall-aware cell->face map only (ablation), 2 = wall-aware map + "
134 "its transpose (face-centre; ablation), 3 = mode 2 at the open-face-centroid (FV "
135 "constraint, FD momentum), 4 = fully-FV (mode-3 projection + second-order wall "
136 "viscous-flux deferred correction on the momentum; targets 2nd-order drag). No effect on "
137 "the staggered solver.")
138 .def("set_fv_relax", &S::setFvRelax, nb::arg("w"),
139 "Mode-4 FV wall-flux defect-correction under-relaxation (1=full; <1 damps the stiff "
140 "explicit-lagged wall term). Steady state is independent of w.")
141 .def("set_velocity_streams", &S::setVelocityStreams, nb::arg("on"),
142 "Toggle overlapped per-component velocity solves.")
143 .def("set_implicit_advection", &S::setImplicitAdvection, nb::arg("on"),
144 "Use implicit-FOU advection with deferred-correction TVD.")
145 .def("set_outer_iterations", &S::setOuterIterations, nb::arg("n"),
146 "Set the number of Picard/outer iterations per step.")
147 .def("set_outer_tolerance", &S::setOuterTolerance, nb::arg("tol"),
148 "Set the outer (Picard) convergence tolerance.")
149 .def("last_outer_iterations", &S::lastOuterIterations,
150 "Return the outer-iteration count from the last step().")
151 .def("set_velocity_solver_params", &S::setVelocityIterations, nb::arg("iters"),
152 "Set the velocity (diffusion) smoother iteration count.")
153 .def("set_deferred_correction", &S::setDeferredCorrection, nb::arg("on"),
154 "Deferred-correction advection: True (default) = 2nd order (implicit FOU + explicit "
155 "high-order correction, the high-order scheme being SOU by default or Koren TVD via "
156 "set_advection_scheme); False = pure implicit FOU (1st-order upwind, more dissipative "
157 "but "
158 "unconditionally stable at sharp shear layers).")
159 .def("set_backflow_stabilization", &S::setBackflowStab, nb::arg("beta"),
160 "Outflow backflow-stabilization coefficient (Bazilevs 2009 / Esmaily-Moghadam 2011): "
161 "beta "
162 "in [0,1] scales the dissipative outflow term that prevents backflow divergence when "
163 "flow "
164 "reverses at the outlet (e.g. a separated wake / BFS recirculation). Default 0.2; 0 = "
165 "off. "
166 "Inert where the outlet is purely outgoing.")
167 .def("set_pressure_solver_params", &S::setPressureIterations, nb::arg("iters"),
168 "Set the pressure smoother iteration count.")
169 .def(
170 "set_pressure_multigrid", [](S& s, bool, int levels) { s.setPressureLevels(levels); },
171 nb::arg("on"), nb::arg("levels") = 4,
172 "Set the pressure multigrid depth (levels=1 => pure RB-GS, no coarse grid).")
173 .def("set_pressure_chebyshev", &S::setPressureChebyshev, nb::arg("on"),
174 nb::arg("max_iter") = 120, nb::arg("rtol") = 1e-9,
175 "Use the communication-light Chebyshev pressure accelerator (exclusive with PCG).")
176 .def("set_pressure_graph_amg", &S::setPressureGraphAmg, nb::arg("on"),
177 "Solve the pressure MG's coarsest level with an agglomerated mesh-agnostic algebraic "
178 "multigrid (core GraphAMG), decomposition-agnostic: with levels=1 this gives a "
179 "mesh-independent pressure solve that works under a WEIGHTED ORB (where the geometric "
180 "coarse levels can't cleanly coarsen). Applied at the next set_solid.")
181 .def("set_pressure_pcg", &S::setPressurePcg, nb::arg("on"), nb::arg("max_iter") = 200,
182 nb::arg("rtol") = 1e-8,
183 "Use the MG-PCG pressure accelerator (single-GPU default; exclusive with Chebyshev).")
184 .def("set_velocity_multigrid", &S::setVelocityMultigrid, nb::arg("on"), nb::arg("levels") = 4,
185 nb::arg("vcycles") = 8,
186 "Enable velocity (momentum) multigrid for the implicit diffusion solve.")
187 .def("last_pressure_iterations", &S::lastPressureIterations,
188 "Return the pressure-solver iteration count from the last step().")
189 .def("set_domain_bc", &S::setDomainBc, nb::arg("face"), nb::arg("type"), nb::arg("vx") = 0.0,
190 nb::arg("vy") = 0.0, nb::arg("vz") = 0.0,
191 "Set a per-face domain BC (face 0..5 = -x,+x,-y,+y,-z,+z; type 0 periodic/1 wall/2 "
192 "inflow/3 outflow).")
193 .def(
194 "set_domain_bc_profile",
195 [](S& s, int face, nb::ndarray<double, nb::c_contig> prof) {
196 if (prof.ndim() != 3 || prof.shape(2) != 3)
197 throw std::runtime_error("profile must be (Nb,Nc,3)");
198 const int nb_ = (int)prof.shape(0), nc = (int)prof.shape(1);
199 s.setDomainBcProfile(
200 face, peclet::core::python::ndarray_to_vector<double>(nb::ndarray<>(prof)), nb_,
201 nc);
202 },
203 nb::arg("face"), nb::arg("profile"),
204 "Prescribe a per-position inlet velocity profile (Nb,Nc,3) over a face (sets it to "
205 "inflow).")
206 .def(
207 "set_pressure_geometry",
208 [](S& s, nb::ndarray<double, nb::f_contig> sdf) { s.setPressureGeometry(grid_in(sdf)); },
209 nb::arg("sdf"),
210 "Set an all-fluid SDF for the cut-cell pressure operator without an immersed solid (the "
211 "channel/BFS domain-BC path). For a no-slip immersed BODY in an inflow/outflow domain, "
212 "call "
213 "set_solid(sdf, cutcell_pressure=True) instead -- do NOT also call this (a second "
214 "geometry "
215 "setter overwrites the SDF and wipes the solid).")
216 .def(
217 "set_solid",
218 [](S& s, nb::ndarray<double, nb::f_contig> sdf, bool cutcell_pressure,
219 const std::string& /*pressure_coarse*/) {
220 s.setSolid(grid_in(sdf), cutcell_pressure);
221 },
222 nb::arg("sdf"), nb::arg("cutcell_pressure") = false, nb::arg("pressure_coarse") = "const",
223 "Set the solid SDF as a Fortran-order (nx,ny,nz) float64 array (negative inside the "
224 "solid, positive in fluid). cutcell_pressure=True enables the open-face-weighted "
225 "cut-cell "
226 "pressure operator (proper no-slip); it composes with domain BCs, so this is the single "
227 "call for a no-slip immersed body in an inflow/outflow domain.")
228 .def(
229 "set_state",
230 [](S& s, nb::ndarray<double, nb::f_contig> u, nb::ndarray<double, nb::f_contig> v,
231 nb::ndarray<double, nb::f_contig> w) {
232 s.uploadVelocity(grid_in(u), grid_in(v), grid_in(w));
233 },
234 nb::arg("u"), nb::arg("v"), nb::arg("w"),
235 "Upload an initial velocity field (u,v,w each a Fortran-order (nx,ny,nz) float64 array).")
236 .def("step", &S::step,
237 "Advance the solver one time step (semi-implicit: diffusion + projection).")
238 .def(
239 "get_u", [](S& s) { return field_out(s, s.getVelocity(0)); },
240 "Return the x-velocity component as a Fortran-order (nx,ny,nz) float64 array (index "
241 "[x,y,z]).")
242 .def(
243 "get_v", [](S& s) { return field_out(s, s.getVelocity(1)); },
244 "Return the y-velocity component as a Fortran-order (nx,ny,nz) float64 array (index "
245 "[x,y,z]).")
246 .def(
247 "get_w", [](S& s) { return field_out(s, s.getVelocity(2)); },
248 "Return the z-velocity component as a Fortran-order (nx,ny,nz) float64 array (index "
249 "[x,y,z]).")
250 .def(
251 "get_p", [](S& s) { return field_out(s, s.getPressure()); },
252 "Return the physical pressure as a Fortran-order (nx,ny,nz) float64 array (index "
253 "[x,y,z]).")
254 .def(
255 "get_ox", [](S& s) { return field_out(s, s.getOpenness(0)); },
256 "TEMP: -x face openness (fluid area fraction) per inner cell, (nx,ny,nz).")
257 .def(
258 "get_oy", [](S& s) { return field_out(s, s.getOpenness(1)); },
259 "TEMP: -y face openness per inner cell, (nx,ny,nz).")
260 .def(
261 "get_oz", [](S& s) { return field_out(s, s.getOpenness(2)); },
262 "TEMP: -z face openness per inner cell, (nx,ny,nz).")
263 .def(
264 "get_uf", [](S& s) { return field_out(s, s.getFaceVelocity(0)); },
265 "Return the divergence-free FACE x-velocity (collocated: projected MAC field; staggered: "
266 "== get_u).")
267 .def(
268 "get_vf", [](S& s) { return field_out(s, s.getFaceVelocity(1)); },
269 "Return the divergence-free FACE y-velocity (collocated: projected MAC field; staggered: "
270 "== get_v).")
271 .def(
272 "get_wf", [](S& s) { return field_out(s, s.getFaceVelocity(2)); },
273 "Return the divergence-free FACE z-velocity (collocated: projected MAC field; staggered: "
274 "== get_w).")
275 // --- Named field registry (multiphysics field container) ---------------------------------
276 .def(
277 "add_field", [](S& s, const std::string& name) { s.addField(name); }, nb::arg("name"),
278 "Register a new zero-initialised cell-centred field on the grid (for transported scalars "
279 "or material properties). Idempotent.")
280 .def(
281 "has_field", [](S& s, const std::string& name) { return s.hasField(name); },
282 nb::arg("name"), "Whether a field of this name is registered.")
283 .def(
284 "field_names", [](S& s) { return s.fieldNames(); },
285 "Names of all registered fields (velocity u/v/w, p, sdf, plus any added), sorted.")
286 .def(
287 "get_field", [](S& s, const std::string& name) { return field_out(s, s.getField(name)); },
288 nb::arg("name"),
289 "Return a registered field's inner region as a Fortran-order (nx,ny,nz) float64 array.")
290 .def(
291 "set_field",
292 [](S& s, const std::string& name, nb::ndarray<double, nb::f_contig> a) {
293 s.setField(name, grid_in(a));
294 },
295 nb::arg("name"), nb::arg("array"),
296 "Write a Fortran-order (nx,ny,nz) float64 array into a registered field's inner region "
297 "(ghosts refilled on the next exchange_field/step).")
298 .def(
299 "field_view",
300 [](S& s, const std::string& name) { return field3d_out(s, s.fieldView(name)); },
301 nb::arg("name"),
302 "Zero-copy view of a registered field's full padded buffer as a Fortran-order "
303 "(nx+2g, ny+2g, nz+2g) array (g = ghost_width); host → NumPy, device → DLPack (CuPy).")
304 .def(
305 "exchange_field", [](S& s, const std::string& name) { s.exchangeField(name); },
306 nb::arg("name"),
307 "Fill a registered field's ghost cells (cross-rank + periodic under MPI; periodic "
308 "single-rank).")
309 .def(
310 "exchange_field_add", [](S& s, const std::string& name) { s.exchangeFieldAdd(name); },
311 nb::arg("name"),
312 "Add-reduce halo: fold ghost-layer deposits back onto their owner (cross-rank + "
313 "periodic). "
314 "The particle->grid deposition primitive for MPI CFD-DEM; single-rank non-periodic "
315 "no-op.")
316 // --- Scalar transport (advection-diffusion) ----------------------------------------------
317 .def(
318 "add_scalar",
319 [](S& s, const std::string& name, double diffusivity, int scheme, int iters) {
320 s.addScalar(name, diffusivity, scheme, iters);
321 },
322 nb::arg("name"), nb::arg("diffusivity") = 0.0, nb::arg("scheme") = 1,
323 nb::arg("iters") = 50,
324 "Register a transported scalar (temperature/concentration/…): constant diffusivity (grid "
325 "units), advection scheme 0=FOU/1=Koren TVD/2=SOU, and RB-GS diffusion sweeps. The "
326 "scalar "
327 "is a registered field (get_field/set_field/field_view). Requires geometry "
328 "(set_solid/set_pressure_geometry) for the openness-weighted operators.")
329 .def(
330 "set_scalar_bc",
331 [](S& s, const std::string& name, int face, int type, double value) {
332 s.setScalarBc(name, face, type, value);
333 },
334 nb::arg("name"), nb::arg("face"), nb::arg("type"), nb::arg("value") = 0.0,
335 "Scalar boundary condition on a domain face (0..5 = -x,+x,-y,+y,-z,+z): type 0 periodic, "
336 "1 Neumann zero-flux (adiabatic), 2 Dirichlet value. Single-rank.")
337 .def(
338 "has_scalar", [](S& s, const std::string& name) { return s.hasScalar(name); },
339 nb::arg("name"), "Whether a transported scalar of this name is registered.")
340 .def(
341 "advance_scalars", [](S& s) { s.advanceScalars(); },
342 "Advance all registered scalars one dt with the current velocity (also done by step()).")
343 // --- Property closures + Boussinesq body force -------------------------------------------
344 .def(
345 "set_property_model",
346 [](S& s, const std::string& target, const std::string& kind, const std::string& in0,
347 const std::vector<double>& params, const std::string& in1) {
349 if (kind == "linear")
351 else if (kind == "boussinesq")
353 else if (kind == "arrhenius")
355 else
356 throw std::runtime_error("set_property_model: unknown kind '" + kind + "'");
357 s.setPropertyModel(target, k, in0, in1, params);
358 },
359 nb::arg("target"), nb::arg("kind"), nb::arg("field"),
360 nb::arg("params") = std::vector<double>{}, nb::arg("field2") = std::string{},
361 "Register a device closure writing a property/body-force field from input field(s). "
362 "target: a registered field (a property 'mu'/'rho'/… or a body-force component "
363 "'force_x'/'force_y'/'force_z'). kind: 'linear' (params [p0,p1,p2]: "
364 "p0+p1*field+p2*field2), "
365 "'boussinesq' (params [rho0,g,beta,T0]: rho0*g*beta*(field-T0) buoyancy), 'arrhenius' "
366 "(params [mu_ref,B,Tref]: mu_ref*exp(B*(1/field-1/Tref))). Applied at the top of step().")
367 .def(
368 "set_property_table",
369 [](S& s, const std::string& target, const std::string& field,
370 const std::vector<double>& x,
371 const std::vector<double>& y) { s.setPropertyTable(target, field, x, y); },
372 nb::arg("target"), nb::arg("field"), nb::arg("x"), nb::arg("y"),
373 "Register a tabulated property: target = piecewise-linear interpolation of (x, y) at the "
374 "input field value (x ascending, clamped at the ends).")
375 .def(
376 "update_properties", [](S& s) { s.updateProperties(); },
377 "Apply all registered property/force closures now (also done at the top of step()).")
378 .def(
379 "enable_cell_force", [](S& s) { s.enableCellForce(); },
380 "Allocate + register the per-cell body-force fields force_x/force_y/force_z and route "
381 "them "
382 "into the momentum RHS, for an external writer (e.g. CFD-DEM drag feedback) to fill "
383 "directly via field_view('force_z'). They persist across steps until overwritten.")
384 .def(
385 "enable_drag", [](S& s) { s.enableDrag(); },
386 "Enable implicit (semi-implicit) linear drag for CFD-DEM: allocate the per-cell "
387 "'drag_beta' "
388 "field (added to the momentum diagonal so a -beta*(u-u_p) source is treated implicitly "
389 "-> "
390 "unconditionally stable for the stiff beta of a dense bed) plus force_x/y/z (which carry "
391 "beta*u_p, the RHS target). Fill 'drag_beta' and 'force_*' via field_view each step.")
392 .def(
393 "set_property_mode",
394 [](S& s, const std::string& mode, bool harmonic) {
395 s.setPropertyMode(mode == "variable", harmonic);
396 },
397 nb::arg("mode") = "variable", nb::arg("harmonic") = false,
398 "Enable variable-coefficient momentum (variable viscosity): mode 'variable' binds the "
399 "'mu' "
400 "field (get/set_field('mu')) into the diffusion operator; 'constant' reverts. harmonic = "
401 "harmonic face-viscosity mean (continuous shear stress across a jump) vs arithmetic. A "
402 "closure targeting 'mu' enables this automatically. The incremental-rotational pressure "
403 "scheme (large-dt / steady-Stokes) stays active — see set_variable_rotational.")
404 .def(
405 "set_variable_rotational",
406 [](S& s, const std::string& mode, double chi) {
407 int m = 0;
408 if (mode == "min")
409 m = 0;
410 else if (mode == "full")
411 m = 1;
412 else if (mode == "off")
413 m = 2;
414 else
415 throw std::runtime_error("set_variable_rotational: mode must be min/full/off");
416 s.setVariableRotational(m, chi);
417 },
418 nb::arg("mode") = "min", nb::arg("chi") = 1.0,
419 "Rotational-pressure term under variable viscosity (the constant-mu Timmermans term "
420 "-mu*div(u*) is only valid for homogeneous viscosity — Deteix & Yakoubi 2018). 'min' "
421 "(default): constant coefficient chi*mu_min — provably stable at any contrast, exact "
422 "fallback to the constant-mu scheme for uniform mu. 'full': pointwise chi*mu(i) — better "
423 "pressure consistency at MILD contrast only. 'off': plain incremental (no rotational "
424 "term). All modes keep the incremental predictor (large-dt / steady-Stokes capability).")
425 .def(
426 "set_density_mode",
427 [](S& s, const std::string& mode) { s.setDensityMode(mode == "variable"); },
428 nb::arg("mode") = "variable",
429 "Enable variable density (staggered solver only): binds the 'rho' field "
430 "(get/set_field('rho'), created seeded with set_rho's value if absent) into the momentum "
431 "time term, the advection weight, the per-cell body force (face-interpolated), and the "
432 "pressure projection (face coefficient openness*rho0/rho_f with the matching 1/rho_f "
433 "velocity correction; rho0 = set_rho's value, so a uniform field reduces exactly to the "
434 "constant solver). A closure targeting 'rho' (e.g. a linear mixture of a transported "
435 "phase fraction) enables this automatically. For gravity, register a closure "
436 "force_z = linear(rho, params=[0, -g]).")
437 .def(
438 "ghost_width", [](S& s) { return s.ghostWidth(); },
439 "Ghost-layer width g of the velocity block (field_view returns an (n+2g) buffer).")
440 .def(
441 "set_porous_continuity", [](S& s, bool on) { s.setPorousContinuity(on); },
442 nb::arg("on") = true,
443 "Enable the volume-averaged (porous) continuity for unresolved CFD-DEM (staggered only): "
444 "the projection enforces d(eps)/dt + div(eps u) = 0 instead of div(u)=0, so the fluid "
445 "velocity is NOT solenoidal where the void fraction changes (bubbling/expansion). Binds "
446 "the 'eps' field (void fraction from the particle deposition, created seeded to 1 if "
447 "absent; the coupling writes it each step BEFORE step()). eps=1 everywhere reduces "
448 "exactly "
449 "to div(u)=0. Pair with max_porous_residual() for the meaningful convergence check.")
450 .def("max_open_divergence", &S::maxOpenDivergence,
451 "Return the max cut-cell velocity-flux divergence max|div(open*u)|. With porous "
452 "continuity this is NOT ~0 -- it equals -d(eps)/dt (the bed expanding). Use "
453 "max_porous_residual() for the continuity residual.")
454 .def(
455 "set_pressure_underrelax", [](S& s, double w) { s.setPressureUnderRelax(w); },
456 nb::arg("omega"),
457 "Pressure under-relaxation factor omega_p in (0,1] for the incremental accumulation "
458 "(MFIX "
459 "§10.1); 1.0 = off (default). <1 damps the incremental predictor overshoot on stiff "
460 "porous+drag.")
461 .def(
462 "set_porous_deps_dt", [](S& s, bool on) { s.setPorousDepsDt(on); }, nb::arg("on"),
463 "Include (default True) or drop the d(eps)/dt source in the porous projection RHS. Drop "
464 "it "
465 "to enforce div(eps u)=0 when the per-cell eps deposit's time-derivative is too jagged "
466 "and "
467 "destabilizes the eps-weighted pressure solve.")
468 .def("sync_porous_prev", &S::syncPorousPrev,
469 "Reseed eps^n = eps^{n+1} (d(eps)/dt=0 this step) — call once after the first "
470 "void-fraction "
471 "deposition so step 0 has no spurious source.")
472 .def("max_porous_residual", &S::maxPorousResidual,
473 "Residual of the volume-averaged continuity max|div(open*eps*u) + d(eps)/dt| -- the "
474 "quantity the porous projection drives to zero. 0 unless set_porous_continuity(True).")
475 .def(
476 "get_resolution", [](S& s) { return std::vector<int>{s.nx(), s.ny(), s.nz()}; },
477 "Return the LOCAL grid resolution [nx, ny, nz] (this rank's block under MPI).")
478 .def(
479 "global_resolution",
480 [](S& s) {
481 auto g = s.globalResolution();
482 return std::vector<int>{g[0], g[1], g[2]};
483 },
484 "Return the GLOBAL grid resolution [gnx, gny, gnz] (== local single-rank). For the "
485 "CFD-DEM co-decomposition weight field.")
486 .def(
487 "block_origin",
488 [](S& s) {
489 auto o = s.blockOrigin();
490 return std::vector<int>{o[0], o[1], o[2]};
491 },
492 "This rank's inner-block origin in GLOBAL cells ([0,0,0] single-rank). Shift the "
493 "coupling "
494 "deposit origin by this so particles in global coordinates land in the local block.")
495 .def(
496 "get_spacing", [](S&) { return std::vector<double>{1.0, 1.0, 1.0}; },
497 "Return the grid spacing [dx, dy, dz] (always unit on this grid).")
498#ifdef PECLET_FLOW_MPI
499 // Distributed path (built with -DPECLET_FLOW_MPI): construct the Solver with this rank's
500 // LOCAL block dims (see the module-level mpi_block()), then init_mpi with the GLOBAL grid
501 // dims. step() then does the g=2 velocity-block halo exchange + the distributed cut-cell
502 // pressure MG. Bit-exact to single-rank.
503 .def(
504 "init_mpi",
505 [](S& s, int gnx, int gny, int gnz) {
506 ensure_mpi_init();
507 s.initMpi(gnx, gny, gnz, MPI_COMM_WORLD);
508 },
509 nb::arg("gnx"), nb::arg("gny"), nb::arg("gnz"),
510 "Wire the multi-rank step: pass the GLOBAL grid dims (gnx,gny,gnz). The Solver must have "
511 "been "
512 "constructed with this rank's LOCAL block dims (from mpi_block). MPI_Init is called if "
513 "needed.")
514 .def(
515 "rebalance_by_weights",
516 [](S& s, const std::vector<double>& w) { s.rebalanceByWeights(w); }, nb::arg("weights"),
517 "Dynamic load balancing: redistribute the solver's state onto the weighted ORB of "
518 "per-cell "
519 "weights (global x-fastest, gnx*gny*gnz). Pass fluid work + gamma*particle_count and the "
520 "coupled dem migrates onto the SAME partition from the same array. State-preserving "
521 "(bit-exact at np=1, reduction floor at np>1).")
522 .def(
523 "rank",
524 [](S&) {
525 ensure_mpi_init();
526 int r = 0;
527 MPI_Comm_rank(MPI_COMM_WORLD, &r);
528 return r;
529 },
530 "This rank's index in MPI_COMM_WORLD.")
531 .def(
532 "size",
533 [](S&) {
534 ensure_mpi_init();
535 int n = 1;
536 MPI_Comm_size(MPI_COMM_WORLD, &n);
537 return n;
538 },
539 "The number of ranks in MPI_COMM_WORLD.")
540#else
541 .def(
542 "rank", [](S&) { return 0; },
543 "MPI rank (always 0 in the single-rank Python module; the multi-rank path is the "
544 "tests/kokkos_mpi suite).")
545 .def(
546 "size", [](S&) { return 1; }, "MPI size (1 in the single-rank Python module).")
547#endif
548 .def(
549 "bcast_from_root", [](S&, nb::object v) { return v; }, nb::arg("value"),
550 "Broadcast a value from rank 0 (identity in the single-rank module; mirrors the MPI "
551 "API).");
552}
553
554NB_MODULE(_flow, m) {
555 m.attr("__doc__") =
556 "flow — Kokkos cut-cell IBM incompressible Navier-Stokes solver for porous media.\n\n"
557 "Two solver classes share an identical API (only the velocity-unknown placement differs):\n"
558 " Solver — staggered MAC grid (THE flow solver; permeability/drag accuracy "
559 "default)\n"
560 " SolverColocated — collocated / cell-centered velocities (ABC approximate projection)\n\n"
561 "Conventions: physical units throughout (density rho, viscosity mu, physical pressure p); "
562 "SDFs\n"
563 "are negative inside the solid; fields are Fortran-order (nx,ny,nz) float64 (x-fastest). "
564 "This is\n"
565 "the single-rank module — the multi-rank MPI path is exercised by the tests/kokkos_mpi "
566 "suite.\n\n"
567 "Kokkos is initialized at import and finalized via a Python atexit hook. Release every "
568 "Solver "
569 "before interpreter exit (it goes out of scope, or `del s; gc.collect()`) so no Kokkos View "
570 "outlives finalize.";
571 if (!Kokkos::is_initialized())
572 Kokkos::initialize();
573 // Register Kokkos::finalize via Python atexit. This is REQUIRED on CUDA: without it, Kokkos's
574 // internal device state is torn down by static destructors AFTER the CUDA runtime unloads,
575 // aborting with cudaErrorCudartUnloading at every exit. atexit runs the hook while the driver is
576 // still up. (Returned fields are backed by host std::vectors, not device Views, so they never
577 // block finalize; a live Solver still holding Views at exit must be released first — hence the
578 // docstring note.)
579 nb::module_::import_("atexit").attr("register")(nb::cpp_function([]() {
580 if (Kokkos::is_initialized() && !Kokkos::is_finalized())
581 Kokkos::finalize();
582 }));
583 // The active Kokkos backend ("OpenMP", "Cuda", "HIP"), chosen by the build's install prefix.
584 m.attr("execution_space") = nb::str(Kokkos::DefaultExecutionSpace::name());
585
586 // Staggered MAC grid (THE flow solver) + the collocated/cell-centered variant. Same Python API.
587 bind_solver<peclet::flow::Staggered>(m, "Solver");
588 bind_solver<peclet::flow::Colocated>(m, "SolverColocated");
589
590#ifdef PECLET_FLOW_MPI
591 // Module-level: this rank's ORB block of the global (gnx,gny,gnz) grid, matching the
592 // deterministic BlockDecomposer the Solver's initMpi re-derives internally (and the C++
593 // tests/kokkos_mpi template). Returns (origin=[ox,oy,oz], size=[lnx,lny,lnz]); slice the global
594 // SDF with these to build the local block, then Solver(*size) + init_mpi(gnx,gny,gnz). MPI_Init
595 // is called if needed.
596 m.def(
597 "mpi_block",
598 [](int gnx, int gny, int gnz) {
599 ensure_mpi_init();
600 int rank = 0, size = 1;
601 MPI_Comm_rank(MPI_COMM_WORLD, &rank);
602 MPI_Comm_size(MPI_COMM_WORLD, &size);
603 peclet::core::decomp::BlockDecomposer<3> dec(static_cast<std::size_t>(size),
604 peclet::core::IVec<3>{gnx, gny, gnz});
605 auto blk = dec.block(static_cast<std::size_t>(rank));
606 std::vector<int> origin{(int)blk.origin[0], (int)blk.origin[1], (int)blk.origin[2]};
607 std::vector<int> bsize{(int)blk.size[0], (int)blk.size[1], (int)blk.size[2]};
608 return std::make_pair(origin, bsize);
609 },
610 nb::arg("gnx"), nb::arg("gny"), nb::arg("gnz"),
611 "Return this MPI rank's ORB block of the global (gnx,gny,gnz) grid as (origin, size), each a "
612 "length-3 list [x,y,z]. Use it to slice the global SDF into this rank's local block for a "
613 "distributed Solver (see Solver.init_mpi). MPI_Init is called if needed.");
614
615 m.attr("has_mpi") = true;
616#else
617 m.attr("has_mpi") = false;
618#endif
619}
NB_MODULE(_flow, m)
static nb::ndarray< nb::numpy, double > field_out(S &s, std::vector< double > &&v)
static std::vector< double > grid_in(nb::ndarray< double, nb::f_contig > a)
static void bind_solver(nb::module_ &m, const char *name)
static auto field3d_out(S &s, peclet::flow::CCField f)
flow — host-facing Kokkos IBM Navier-Stokes solver (drop-in flow-style API).
CCExec::memory_space CCMem
Kokkos::View< double *, CCMem > CCField
Kokkos::DefaultExecutionSpace::memory_space Mem