2"""A/B of the COLLOCATED advecting velocity: the projected divergence-free MAC face field
3`uf/vf/wf` (the shipped scheme) against the un-projected cell->face average 1/2(u_i+u_j) (the
4phase-2 form, kept as `diagnostics.set_uf_advection(False)`).
6Why this exists: `doc/flow_colocated_plan.md` §1 step 3 prescribes the projected field ("these u_f
7become the advecting velocities for the next step's advection"), `colocated_advection.hpp` said the
8swap was pending, and the FOU operator's conservative row-sum identity needs a divergence-free
9advecting field (../docs/decisions/flow.md:977). The swap landed 2026-09-21; this script is the
10evidence behind it and the instrument for re-judging it.
12Four measurements, each printed for BOTH choices:
14 div -- max |div_h(advecting field)| at the end of a Taylor-Green run. This is the structural
15 claim, not a tuning number: the projection makes `uf` solenoidal to solver tolerance,
16 the cell->face average of the *corrected cell* field is not (the residual is intrinsic
17 to cell-centred velocity placement -- it is why the coupling is an APPROXIMATE
18 projection). Measured here in Python on the same 7-point stencil `divergOpen` uses.
19 tg -- 2-D Taylor-Green in a triply-periodic box: an EXACT Navier-Stokes solution (the
20 nonlinear term is balanced by the pressure gradient), so the L2 error and its observed
21 order are absolute, not relative to a reference run.
22 shift -- the same vortex Galilean-shifted by a uniform mean velocity, no body force, periodic:
23 the continuum answer is a mean momentum that is exactly constant, so any drift is the
24 discrete advection failing to conserve.
25 bed -- the production regime the dt^2 scaling of the two choices' difference points at
26 (../../amr/docs/amr_flow_uniform_parity.md §3a): steady large-dt driving through a
27 cut-cell sphere bed, advection on, reported as <u_x> (proportional to permeability at
28 fixed forcing) with the STAGGERED solver's answer on the same geometry as the
29 reference -- the staggered stored velocity IS the projected face velocity, so it has
30 no A/B to make and is the closest thing to a known answer here.
32Run (host-openmp). Use PYTHONPATH, not PECLET_FLOW_BUILD: the suite venv carries an INSTALLED
33`peclet.flow`, and `_bootstrap`'s first rule (deliberately) keeps whatever is already importable,
34so PECLET_FLOW_BUILD is ignored there and the study would silently measure the installed wheel.
35Every run prints which module it resolved -- read that line before the numbers.
37 OMP_NUM_THREADS=8 OMP_PROC_BIND=false PYTHONPATH=$PWD/build_uf \\
38 python tests/study/uf_advection_ab.py [div|tg|shift|bed|all]
45sys.path.insert(0, os.path.join(os.path.dirname(os.path.abspath(__file__)),
"..",
"..",
"scripts"))
46from _bootstrap
import ensure_flow
50NU, DT, RHO = 0.05, 0.5, 1.0
55 """Taylor-Green at decay amplitude `amp`, optionally Galilean-shifted in x by `shift`."""
58 X, Y = np.meshgrid(ix, ix, indexing=
"ij")
59 u2 = U0 * amp * np.sin(k * X) * np.cos(k * Y) + shift
60 v2 = -U0 * amp * np.cos(k * X) * np.sin(k * Y)
61 u = np.repeat(u2[:, :,
None], nz, axis=2)
62 v = np.repeat(v2[:, :,
None], nz, axis=2)
63 w = np.zeros((N, N, nz))
64 return np.asfortranarray(u), np.asfortranarray(v), np.asfortranarray(w)
68 """The eight-sphere periodic bed of tests/regression/state_hash.py (same geometry, so the
69 numbers here and the byte gate's talk about the same scene)."""
71 cs = np.array([0.25 * N, 0.75 * N])
72 xs = np.arange(N) + 0.5
73 X, Y, Z = np.meshgrid(xs, xs, xs, indexing=
"ij")
74 best = np.full((N, N, N), 1e30)
78 dx = X - sx; dx -= N * np.round(dx / N)
79 dy = Y - sy; dy -= N * np.round(dy / N)
80 dz = Z - sz; dz -= N * np.round(dz / N)
81 best = np.minimum(best, np.sqrt(dx * dx + dy * dy + dz * dz) - R)
82 return np.asfortranarray(best)
87 """div_h of a MAC face field in flow's LOW-face convention (entry i = the -axis face of cell
88 i), on a fully periodic all-fluid block: (uf(i+1) - uf(i)) + ... , spacing 1."""
89 return ((np.roll(uf, -1, 0) - uf) + (np.roll(vf, -1, 1) - vf) + (np.roll(wf, -1, 2) - wf))
93 """max |div_h| of the field the advection operators actually read, on a periodic all-fluid box.
95 use_uf -- the solver's own projected face field (`get_uf`/`get_vf`/`get_wf`, already the low-face
97 else -- the cell->face average `cadv::adv_vel` forms from the cell field: the LOW face of
98 cell i is 1/2(u(i-1) + u(i)).
101 f = [np.asarray(g())
for g
in (s.get_uf, s.get_vf, s.get_wf)]
103 cell = [np.asarray(s.get_u()), np.asarray(s.get_v()), np.asarray(s.get_w())]
104 f = [0.5 * (np.roll(cell[c], 1, c) + cell[c])
for c
in range(3)]
109def run_tg(cls, N, use_uf, nz=4, U0=1.0, steps=40, shift=0.0, dt=DT):
114 s.set_advection(
True)
115 if use_uf
is not None:
116 s.diagnostics.set_uf_advection(use_uf)
117 s.diagnostics.set_velocity_solver_params(80)
118 s.set_solid(np.asfortranarray(np.full((N, N, nz), 1e3)), cutcell_pressure=
True)
119 u0, v0, w0 =
tg_fields(N, nz, U0, 1.0, shift)
120 s.set_state(u0, v0, w0)
121 for _
in range(steps):
123 return s, (u0, v0, w0)
127 """L2 error against the exact decayed (and shifted) solution, normalized by the initial norm."""
129 amp = np.exp(-2.0 * NU * k * k * dt * steps)
130 ue, ve, _ =
tg_fields(N, nz, U0, amp, shift)
131 uu, vv = np.asarray(s.get_u()), np.asarray(s.get_v())
132 num = np.sqrt(np.mean((uu - ue) ** 2 + (vv - ve) ** 2))
133 den = np.sqrt(np.mean(np.asarray(u0[0]) ** 2 + np.asarray(u0[1]) ** 2))
134 return float(num / den)
137def run_bed(cls, N, use_uf, steps, dt, mu=0.02, f0=1e-3):
138 """Steady large-dt driving through the cut-cell bed, advection ON and implicit (stable at a
139 dt far above the CFL limit -- the regime the two choices differ in)."""
144 s.set_body_force((f0, 0.0, 0.0))
145 s.set_advection(
True)
146 s.set_implicit_advection(
True)
147 if use_uf
is not None:
148 s.diagnostics.set_uf_advection(use_uf)
149 s.diagnostics.set_velocity_solver_params(80)
150 s.set_pressure_multigrid(
True, 3)
151 s.set_pressure_pcg(
True, 400, 1e-10)
153 for _
in range(steps):
157 ux = np.asarray(s.get_u())
158 return float(np.mean(np.where(fluid, ux, 0.0))), float(s.max_open_divergence())
163 print(
"=== div: max |div_h(advecting field)| after 40 Taylor-Green steps (periodic, all fluid) ===")
164 print(f
"{'N':>5} {'projected uf':>14} {'1/2(u_i+u_j)':>14} {'ratio':>9}")
166 s, _ =
run_tg(flow.SolverColocated, N,
True)
169 print(f
"{N:5d} {d_uf:14.3e} {d_av:14.3e} {d_av / max(d_uf, 1e-300):9.1e}")
173 print(
"=== tg: 2-D Taylor-Green, L2 error vs the EXACT solution (nu=0.05, dt=0.5, 40 steps) ===")
174 print(f
"{'N':>5} {'advecting velocity':>20} {'L2 error':>12} {'order':>7} {'maxdiv(uf)':>12}")
177 for use_uf, name
in ((
True,
"projected uf"), (
False,
"1/2(u_i+u_j)")):
178 s, u0 =
run_tg(flow.SolverColocated, N, use_uf, steps=steps)
179 l2 =
tg_error(s, u0, N, 4, 1.0, steps, 0.0, DT)
182 order = f
"{np.log2(prev[name] / l2):7.2f}"
184 print(f
"{N:5d} {name:>20} {l2:12.4e} {order:>7} {s.max_open_divergence():12.2e}")
193 print(f
"=== shift: Galilean-shifted vortex (mean u = {shift}), momentum drift over {steps} steps (dt={dt}) ===")
194 print(f
"{'N':>5} {'advecting velocity':>20} {'|<u>-<u>_0|':>13} {'|<v>-<v>_0|':>13}")
196 for use_uf, name
in ((
True,
"projected uf"), (
False,
"1/2(u_i+u_j)")):
197 s, u0 =
run_tg(flow.SolverColocated, N, use_uf, steps=steps, shift=shift, dt=dt)
198 du = abs(float(np.mean(np.asarray(s.get_u()))) - float(np.mean(np.asarray(u0[0]))))
199 dv = abs(float(np.mean(np.asarray(s.get_v()))) - float(np.mean(np.asarray(u0[1]))))
200 print(f
"{N:5d} {name:>20} {du:13.3e} {dv:13.3e}")
204 print(f
"=== bed: cut-cell sphere bed, advection ON + implicit, dt={dt}, {steps} steps ===")
205 print(f
"{'solver':>28} {'<u_x>_fluid':>13} {'max_open_div':>13}")
206 for cls, use_uf, name
in (
207 (flow.Solver,
None,
"staggered (reference)"),
208 (flow.SolverColocated,
True,
"collocated, projected uf"),
209 (flow.SolverColocated,
False,
"collocated, 1/2(u_i+u_j)"),
211 ux, div =
run_bed(cls, N, use_uf, steps, dt)
212 print(f
"{name:>28} {ux:13.6e} {div:13.2e}")
216 what = sys.argv[1]
if len(sys.argv) > 1
else "all"
217 if what
in (
"div",
"all"):
219 if what
in (
"tg",
"all"):
221 if what
in (
"shift",
"all"):
223 if what
in (
"bed",
"all"):
227if __name__ ==
"__main__":
run_tg(cls, N, use_uf, nz=4, U0=1.0, steps=40, shift=0.0, dt=DT)
div_of_advecting(s, use_uf)
report_shift(Ns=(32, 64), steps=120, shift=0.5, dt=0.1)
report_tg(Ns=(32, 64, 128), steps=40)
tg_fields(N, nz, U0, amp, shift=0.0)
run_bed(cls, N, use_uf, steps, dt, mu=0.02, f0=1e-3)
div_periodic_faces(uf, vf, wf)
report_bed(N=32, steps=200, dt=20.0)
tg_error(s, u0, N, nz, U0, steps, shift, dt)
packing_sdf(N, rfrac=0.18)