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 + "
111 "Set physical parameters (rho/mu/dt) and any domain BCs before the geometry / first "
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 "
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 ⇒ "
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).")
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 "
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): "
162 "in [0,1] scales the dissipative outflow term that prevents backflow divergence when "
164 "reverses at the outlet (e.g. a separated wake / BFS recirculation). Default 0.2; 0 = "
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.")
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).")
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_,
203 nb::arg(
"face"), nb::arg(
"profile"),
204 "Prescribe a per-position inlet velocity profile (Nb,Nc,3) over a face (sets it to "
207 "set_pressure_geometry",
208 [](S& s, nb::ndarray<double, nb::f_contig> sdf) { s.setPressureGeometry(
grid_in(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, "
213 "set_solid(sdf, cutcell_pressure=True) instead -- do NOT also call this (a second "
215 "setter overwrites the SDF and wipes the solid).")
218 [](S& s, nb::ndarray<double, nb::f_contig> sdf,
bool cutcell_pressure,
219 const std::string& ) {
220 s.setSolid(
grid_in(sdf), cutcell_pressure);
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 "
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.")
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) {
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).")
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 "
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 "
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 "
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 "
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).")
258 "get_oy", [](S& s) {
return field_out(s, s.getOpenness(1)); },
259 "TEMP: -y face openness per inner cell, (nx,ny,nz).")
261 "get_oz", [](S& s) {
return field_out(s, s.getOpenness(2)); },
262 "TEMP: -z face openness per inner cell, (nx,ny,nz).")
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: "
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: "
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: "
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.")
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.")
284 "field_names", [](S& s) {
return s.fieldNames(); },
285 "Names of all registered fields (velocity u/v/w, p, sdf, plus any added), sorted.")
287 "get_field", [](S& s,
const std::string& name) {
return field_out(s, s.getField(name)); },
289 "Return a registered field's inner region as a Fortran-order (nx,ny,nz) float64 array.")
292 [](S& s,
const std::string& name, nb::ndarray<double, nb::f_contig> a) {
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).")
300 [](S& s,
const std::string& name) {
return field3d_out(s, s.fieldView(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).")
305 "exchange_field", [](S& s,
const std::string& name) { s.exchangeField(name); },
307 "Fill a registered field's ghost cells (cross-rank + periodic under MPI; periodic "
310 "exchange_field_add", [](S& s,
const std::string& name) { s.exchangeFieldAdd(name); },
312 "Add-reduce halo: fold ghost-layer deposits back onto their owner (cross-rank + "
314 "The particle->grid deposition primitive for MPI CFD-DEM; single-rank non-periodic "
319 [](S& s,
const std::string& name,
double diffusivity,
int scheme,
int iters) {
320 s.addScalar(name, diffusivity, scheme, iters);
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 "
327 "is a registered field (get_field/set_field/field_view). Requires geometry "
328 "(set_solid/set_pressure_geometry) for the openness-weighted operators.")
331 [](S& s,
const std::string& name,
int face,
int type,
double value) {
332 s.setScalarBc(name, face, type, value);
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.")
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.")
341 "advance_scalars", [](S& s) { s.advanceScalars(); },
342 "Advance all registered scalars one dt with the current velocity (also done by step()).")
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")
356 throw std::runtime_error(
"set_property_model: unknown kind '" + kind +
"'");
357 s.setPropertyModel(target, k, in0, in1, params);
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().")
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).")
376 "update_properties", [](S& s) { s.updateProperties(); },
377 "Apply all registered property/force closures now (also done at the top of step()).")
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 "
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.")
385 "enable_drag", [](S& s) { s.enableDrag(); },
386 "Enable implicit (semi-implicit) linear drag for CFD-DEM: allocate the per-cell "
388 "field (added to the momentum diagonal so a -beta*(u-u_p) source is treated implicitly "
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.")
394 [](S& s,
const std::string& mode,
bool harmonic) {
395 s.setPropertyMode(mode ==
"variable", harmonic);
397 nb::arg(
"mode") =
"variable", nb::arg(
"harmonic") =
false,
398 "Enable variable-coefficient momentum (variable viscosity): mode 'variable' binds the "
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.")
405 "set_variable_rotational",
406 [](S& s,
const std::string& mode,
double chi) {
410 else if (mode ==
"full")
412 else if (mode ==
"off")
415 throw std::runtime_error(
"set_variable_rotational: mode must be min/full/off");
416 s.setVariableRotational(m, chi);
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).")
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]).")
438 "ghost_width", [](S& s) {
return s.ghostWidth(); },
439 "Ghost-layer width g of the velocity block (field_view returns an (n+2g) buffer).")
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 "
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.")
455 "set_pressure_underrelax", [](S& s,
double w) { s.setPressureUnderRelax(w); },
457 "Pressure under-relaxation factor omega_p in (0,1] for the incremental accumulation "
459 "§10.1); 1.0 = off (default). <1 damps the incremental predictor overshoot on stiff "
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 "
465 "to enforce div(eps u)=0 when the per-cell eps deposit's time-derivative is too jagged "
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 "
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).")
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).")
481 auto g = s.globalResolution();
482 return std::vector<int>{g[0], g[1], g[2]};
484 "Return the GLOBAL grid resolution [gnx, gny, gnz] (== local single-rank). For the "
485 "CFD-DEM co-decomposition weight field.")
489 auto o = s.blockOrigin();
490 return std::vector<int>{o[0], o[1], o[2]};
492 "This rank's inner-block origin in GLOBAL cells ([0,0,0] single-rank). Shift the "
494 "deposit origin by this so particles in global coordinates land in the local block.")
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
505 [](S& s,
int gnx,
int gny,
int gnz) {
507 s.initMpi(gnx, gny, gnz, MPI_COMM_WORLD);
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 "
512 "constructed with this rank's LOCAL block dims (from mpi_block). MPI_Init is called if "
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 "
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).")
527 MPI_Comm_rank(MPI_COMM_WORLD, &r);
530 "This rank's index in MPI_COMM_WORLD.")
536 MPI_Comm_size(MPI_COMM_WORLD, &n);
539 "The number of ranks in MPI_COMM_WORLD.")
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).")
546 "size", [](S&) {
return 1; },
"MPI size (1 in the single-rank Python module).")
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 "