import importlib.util, os, subprocess, sys
_local = os.environ.get("PECLET_LOCAL_BUILD")
if _local:
for p in _local.split(os.pathsep):
sys.path.insert(0, p)
elif importlib.util.find_spec("peclet") is None:
subprocess.run([sys.executable, "-m", "pip", "install", "-q", "peclet"], check=True)Confinement at finite Re: the wall correction that inertia screens away
A sphere on the axis of a square duct feels 30–40 % more drag than in open fluid when the flow creeps — and only about 10 % more at Re = 1.5, because inertia screens the long-range Stokes disturbance the walls reflect. Measured here with the exact static twin of a settling experiment, plus the geometry trap that hid it for two campaigns.
GPU example — the frozen page reads correctly without a solver.
What you’ll learn
The Faxén/Ladenburg wall correction says a sphere of diameter \(d\) on the axis of a square duct of side \(W = 6.67\,d\) drags about 39 % more than in open fluid — in creeping flow, where the disturbance decays as \(1/r\) and the walls reflect it. At finite Reynolds number the far field is Oseen-screened beyond \(\ell \sim \nu/U = d/\mathrm{Re}\); with the wall at \(3.3\,d\) and \(\mathrm{Re} = 1.5\) that is five screening lengths away, and the correction collapses toward a few percent. That collapse is what makes the classic settling experiment of ten Cate et al. (2002) read 0.947 of the unbounded terminal velocity instead of 0.72.
This page measures the duct’s confinement penalty — its drag over the unbounded Abraham value at the same Reynolds number — directly, with no moving geometry at all: the sphere is fixed, the duct walls translate with the plug (set_instance_motion on the duct instance — a wall-velocity datum on a \(y\)-invariant body, not a rebuild), and the fluid starts as a uniform stream (set_velocity). That is the exact sphere-frame twin of a sphere falling through a tank at rest: plug flow far away, zero wall shear, quiescent lab state. The Reynolds number is set by the viscosity alone, so creeping and finite-Re share one grid and one geometry.
A container wall is naturally built as slab minus cavity. In a periodic box the scene evaluates the union of an instance’s periodic images — right for a body straddling a face, and silent disaster for a slab wider than the box: its images overlap, and a neighbouring image’s slab reaches back into the cavity. A 0.7 L slab minus a 53-cell cavity gives a 38-cell duct whatever the cavity size. Built that way, the settling benchmark ran 30 % narrow and its “creeping-valued” confinement — measured, cross-checked, and blamed on the advection operator — was the geometry. set_solid_from_scene now detects it exactly (it re-samples the primary image whenever an instance spans more than the box and counts the cells the images changed), warns, and exposes periodic_image_overlap_cells(). The rule: slab half-extent = half the box + wall thickness, never more. The last section shows the detector firing.
import time
import numpy as np
import matplotlib.pyplot as plt
from peclet import flow as sdflow
from peclet.core import geom
plt.rcParams.update({"figure.dpi": 130, "font.size": 9, "axes.axisbelow": True,
"figure.facecolor": "white", "savefig.bbox": "tight"})
U = 0.02
KI_I, KI_R = 2, 17
OFF, WALL = 0.3, 4.3
def abraham_cd(re):
return 24.0 / 9.06 ** 2 * (9.06 / np.sqrt(re) + 1.0) ** 2
def faxen_square_duct(lam):
"""Creeping-flow drag factor for a sphere on the axis of a square duct, lam = d / W
(Faxen's series, first two terms; ~1.39 at lam = 0.15)."""
a = lam # a/(W/2) = d/W
return 1.0 / (1.0 - 1.903 * a + 1.968 * a ** 3)Kokkos::OpenMP::initialize WARNING: OMP_PROC_BIND environment variable not set
In general, for best performance with OpenMP 4.0 or better set OMP_PROC_BIND=spread and OMP_PLACES=threads
For best performance with OpenMP 3.1 set OMP_PROC_BIND=true
For unit testing set OMP_PROC_BIND=false
The static twin
def run(DH, Re, duct, steps, slab_half=None):
NU = U * DH / Re
DT = 3.2 if Re > 0.5 else 8.0
L = int(np.ceil((100 / 15 * DH + 2 * WALL) / 8) * 8) # the ten Cate cross-section, W = 6.67 d
WX = (L - 100 / 15 * DH) / 2
NY = int(np.ceil(12 * DH / 8) * 8) # periodic streamwise length 12 d
b = geom.SceneBuilder()
sph = b.add_leaf("sphere", [DH / 2])
if duct:
half = (L / 2 + 1.0) if slab_half is None else slab_half
slab = b.add_leaf("box", [half, NY * 2.0, half])
cav = b.add_leaf("box", [(L - 2 * WX) / 2, NY * 2.0, (L - 2 * WX) / 2])
duc = b.add_difference(slab, cav)
ni, nr, _, _ = b.encode()
nI = 2 if duct else 1
ii = np.zeros((nI, KI_I), dtype=np.int32); ir = np.zeros((nI, KI_R))
x0 = 0.5 * L + OFF
ii[0] = (sph, -1); ir[0, 0:3] = (x0, 0.5 * NY + OFF, x0); ir[0, 6] = 1.0; ir[0, 7] = 1.0
if duct:
ii[1] = (duc, -1); ir[1, 0:3] = (x0, 0.5 * NY, x0); ir[1, 6] = 1.0; ir[1, 7] = 1.0
s = sdflow.Solver(L, NY, L)
s.set_rho(1.0); s.set_mu(NU); s.set_dt(DT); s.set_advection(True)
s.set_velocity_solver_params(60); s.set_pressure_solver_params(20)
s.set_pressure_multigrid(True, levels=4)
s.set_scene(np.asarray(ni, np.int32), np.asarray(nr, float), ii.ravel(), ir.ravel(),
periodic=True)
if duct:
s.set_instance_motion(1, lin_vel=[0.0, U, 0.0]) # walls move WITH the plug: zero shear
s.set_solid_from_scene(True)
overlap = s.periodic_image_overlap_cells()
s.set_velocity(1, np.full((L, NY, L), U, dtype=np.float64, order="F"))
vfl = ((L - 2 * WX) ** 2 * NY if duct else L * L * NY) - np.pi / 6 * DH ** 3
fest = abraham_cd(Re) * 0.5 * U ** 2 * np.pi * (DH / 2) ** 2 * (1.4 if duct else 1.05)
s.set_body_force(0.0, fest / vfl, 0.0) # hold the mean against the drag
nfl = int((np.abs(np.asarray(s.get_v())) > 0).sum())
F = []; t0 = time.time()
for k in range(steps):
s.step()
F.append(float(np.asarray(s.hydro_force_torque_reaction())[0][0][1]))
F = np.array(F)
u = float(np.asarray(s.get_v()).sum()) / vfl # fluid-mean relative speed
f = F[steps * 3 // 4:].mean()
cd = f / (0.5 * u ** 2 * np.pi * (DH / 2) ** 2)
return dict(F=F, u=u, re=u * DH / NU, cd=cd, cd_abr=cd / abraham_cd(u * DH / NU),
nfl=nfl, overlap=overlap, wall=time.time() - t0, L=L, NY=NY, cavity=L - 2 * WX)DH = 8.0
RES = [0.015, 1.5, 30.0]
STEPS = {0.015: 400, 1.5: 800, 30.0: 800}
rows = {}
print(" Re | Cd/Abraham periodic duct | duct penalty | Faxen creeping | time")
for Re in RES:
p = run(DH, Re, False, STEPS[Re])
d = run(DH, Re, True, STEPS[Re])
K = d["cd"] / p["cd"]
rows[Re] = (p, d, K)
print(" %5.3f | %.4f %.4f | %+5.1f %% | %+5.1f %% | %3.0f s"
% (Re, p["cd_abr"], d["cd_abr"], 100 * (d["cd_abr"] - 1), 100 * (faxen_square_duct(DH / d["cavity"]) - 1),
p["wall"] + d["wall"])) Re | Cd/Abraham periodic duct | duct penalty | Faxen creeping | time
0.015 | 1.1582 1.3036 | +30.4 % | +38.7 % | 96 s
1.500 | 0.9885 1.0834 | +8.3 % | +38.7 % | 204 s
30.000 | 1.0358 1.0647 | +6.5 % | +38.7 % | 173 s
Code
fig, (a1, a2) = plt.subplots(1, 2, figsize=(7.4, 3.0))
re = np.array(RES)
a1.semilogx(re, [rows[r][1]["cd_abr"] for r in RES], "o-", color="#4c72b0", label="duct, W = 6.67 d")
a1.semilogx(re, [rows[r][0]["cd_abr"] for r in RES], "s--", color="#8c8c8c", label="periodic box (images at 8 d)")
a1.axhline(1.0, color="k", lw=0.6); a1.set_ylabel("$C_d$ / Abraham"); a1.set_xlabel("Re")
a1.legend(fontsize=8, frameon=False); a1.grid(alpha=0.3)
a2.semilogx(re, [100 * (rows[r][1]["cd_abr"] - 1) for r in RES], "o-", color="#c44e52", label="duct penalty")
a2.axhline(100 * (faxen_square_duct(DH / rows[1.5][1]["cavity"]) - 1), color="#c44e52", ls=":", lw=0.9, label="Faxén, creeping")
a2.set_ylabel("confinement penalty [%]"); a2.set_xlabel("Re"); a2.legend(fontsize=8, frameon=False); a2.grid(alpha=0.3)
plt.show()
The trap, demonstrated
DH = 8.0
p = rows[1.5][0]
good = rows[1.5][1]
bad = run(DH, 1.5, True, 800, slab_half=0.7 * good["L"]) # the slab as it was first written
print(" duct built with slab half-extent | cavity cells | overlap cells flagged | Cd/Abraham")
print(" L/2 + wall (correct) | %7d | %6d | %.4f"
% (good["nfl"], good["overlap"], good["cd_abr"]))
print(" 0.7 L (wider than the box) | %7d | %6d | %.4f"
% (bad["nfl"], bad["overlap"], bad["cd_abr"]))peclet.flow set_solid_from_scene WARNING: an instance is wider than the periodic box, and the UNION of its periodic images changes the solid at 131040 cells on this rank. If it is a container wall (slab minus cavity), keep the slab's half-extent at half the box plus the wall thickness -- not more -- or the images refill the cavity. periodic_image_overlap_cells() returns this count.
duct built with slab half-extent | cavity cells | overlap cells flagged | Cd/Abraham
L/2 + wall (correct) | 269400 | 0 | 1.0834
0.7 L (wider than the box) | 138360 | 131040 | 2.5037
Results
| claim | measured | reference |
|---|---|---|
| duct \(C_d\) / Abraham, creeping | 1.304 | Faxén square duct ≈ 1.39 (+ streamwise images) |
| duct \(C_d\) / Abraham, Re 1.5 | 1.083 | ≈ 1.05–1.15 (screened; the experiment’s 0.947 ⇒ 1.06) |
| duct \(C_d\) / Abraham, Re 30 | 1.065 | ≈ 1 |
| duct penalty creeping → Re 1.5 → Re 30 | +30% / +8% / +6% | falls as inertia screens; Faxén creeping +39% |
| oversized slab: cavity cells / flagged | 138360 / 131040 vs correct 269400 / 0 | detector fires only on the trap |
| oversized slab: duct \(C_d\) / Abraham at Re 1.5 | 2.50 | the number the trap produced, read for two campaigns as physics |
The confinement penalty relative to open fluid falls from 30% at creeping to 8% at Re 1.5 — the screening the experiment relies on, resolved on an \(8\)-cell sphere. The absolute creeping value sits a few percent below Faxén for the same reason the periodic reference sits a few percent below its own image correction: the \(d/h = 8\) cut-cell drag is a few percent low at this resolution (see the Galilean pair), and the streamwise periodicity of the duct adds a little that Faxén’s infinite duct does not have. The fall of the penalty with Reynolds number is what the confinement physics owns, and it behaves; the ratio to the periodic box is not a clean metric because that box’s own images screen with Re as well.
Adapt this yourself
- Climb the ladder. \(d/h = 12\) and \(16\) tighten the absolute values; \(K\) should barely move.
- Off-axis. Shift the sphere toward a wall — the lift and the asymmetric wake at Re 30 are well documented (Zeng, Balachandar & Fischer 2005) and the same script reaches them.
- Close the tank. Add floor and ceiling (a box difference in \(y\) too) and you have the settling geometry’s static twin at any height; compare with the settling sphere page’s deceleration onset.
Reproduce this
PECLET_LOCAL_BUILD=/path/to/suite/flow/build_l3_cuda:/path/to/suite/core/python/build_geom \
quarto render examples/confined-drag-screening/index.qmd --execute