flow 1.3.0
Kokkos cut-cell IBM incompressible Navier-Stokes solver + pnm pore extraction
Loading...
Searching...
No Matches
rotlet_gate.py
Go to the documentation of this file.
1"""Layer 3 rungs 2-3 gate: ROTLET. A single sphere spinning in Stokes flow.
2
3Exact exterior solution for a sphere of radius R spinning at omega in UNBOUNDED Stokes flow:
4 u(r) = (R/|r|)^3 * (omega x r)
5a pure rotational (couplet) field: zero net force, decaying as 1/|r|^2.
6
7The solver's box is PERIODIC, so the true solution is the lattice of images, not the unbounded
8rotlet. That difference is physics, not discretisation error, and it does not shrink under
9refinement -- so the error is measured against BOTH references and the lattice sum's own
10convergence (9^3 vs 13^3 images) is printed, so a reader can see whether the reference is
11trustworthy before trusting an order taken against it.
12
13SAMPLING. get_u/get_v/get_w return MAC FACE values -- u at (i-1/2,j,k), v at (i,j-1/2,k), w at
14(i,j,k-1/2). Comparing them against the analytic field sampled at CELL CENTRES injects an O(h)
15offset that caps the measured order at 1 however accurate the solver is. Each component is
16therefore compared at its own staggered point.
17
18Two shells: the near band [1.15R, 2R], a few cells off a curved cut wall, and a far band
19[2R, 3.5R]. Order is the claim, not the absolute.
20"""
21import os
22import sys
23import numpy as np
24from peclet import flow as sdflow
25
26RF = float(os.environ.get("ROTLET_RF", "0.10")) # R / N
27OMEGA = 0.02 # rad per unit time, about z
28MU, RHO, DT, STEPS = 0.1, 1.0, 200.0, 400
29NEAR = (1.15, 2.0)
30FAR = (2.0, 3.5)
31KN_R, KI_I, KI_R = 16, 2, 17
32
33
34def rotlet(px, py, pz, R, w):
35 """Unbounded rotlet, omega = (0,0,w)."""
36 r = np.sqrt(px*px + py*py + pz*pz)
37 amp = np.where(r > 0, (R / np.where(r > 0, r, 1.0))**3, 0.0)
38 return -amp * w * py, amp * w * px, np.zeros_like(px)
39
40
41def rotlet_lattice(px, py, pz, R, w, L, nimg):
42 """Rotlet summed over a (2*nimg+1)^3 block of periodic images."""
43 ux = np.zeros_like(px); uy = np.zeros_like(px); uz = np.zeros_like(px)
44 for ix in range(-nimg, nimg + 1):
45 for iy in range(-nimg, nimg + 1):
46 for iz in range(-nimg, nimg + 1):
47 a, b, c = rotlet(px - ix*L, py - iy*L, pz - iz*L, R, w)
48 ux += a; uy += b; uz += c
49 return ux, uy, uz
50
51
52def run(N, crossings=False):
53 R = RF * N
54 C = (0.5 * N, 0.5 * N, 0.5 * N)
55 node_ints = np.array([1, -1, -1], dtype=np.int32)
56 node_reals = np.zeros(KN_R); node_reals[0] = R
57 node_reals[14] = 1.0; node_reals[15] = 1.0
58 ii = np.zeros((1, KI_I), dtype=np.int32); ii[0] = (0, -1)
59 ir = np.zeros((1, KI_R)); ir[0, 0:3] = C; ir[0, 6] = 1.0; ir[0, 7] = 1.0
60
61 s = sdflow.Solver(N, N, N)
62 s.set_rho(RHO); s.set_mu(MU); s.set_dt(DT)
63 s.set_advection(False)
64 s.set_velocity_solver_params(120)
65 s.set_pressure_solver_params(30)
66 # MG depth 4, not 1. levels=1 leaves the "coarse" solve on the FULL grid, which on CUDA costs
67 # 920 ms/step at N=64 against 19.5 ms at depth 4 (measured). Depth changes the solver path, not
68 # the converged answer.
69 s.set_pressure_multigrid(True, levels=4)
70 s.set_scene(node_ints, node_reals, ii.ravel(), ir.ravel(), periodic=True)
71 s.set_instance_motion(0, lin_vel=[0.0, 0.0, 0.0], ang_vel=[0.0, 0.0, OMEGA])
72 if crossings:
73 s.set_exact_crossings_from_scene() # analytic wall crossings (Layer 2)
74 s.set_solid_from_scene(True)
75 prev = 0.0
76 for it in range(STEPS):
77 s.step()
78 m = float(np.abs(np.asarray(s.get_v())).max())
79 if it > 15 and abs(m - prev) < 1e-9 * (abs(m) + 1e-300):
80 break
81 prev = m
82 return (N, R, C, np.asarray(s.get_u()), np.asarray(s.get_v()), np.asarray(s.get_w()),
83 s.max_open_divergence(), s.wall_flux_imbalance(), it + 1)
84
85
86OFFS = ((-0.5, 0.0, 0.0), (0.0, -0.5, 0.0), (0.0, 0.0, -0.5))
87
88
89def err(res, shell, nimg=None):
90 """Relative L2 error over `shell`; lattice reference when nimg is given, unbounded otherwise."""
91 N, R, C, u, v, w, _, _, _ = res
92 g = np.arange(N).astype(float)
93 num = den = 0.0
94 npts = 0
95 for comp, fld in enumerate((u, v, w)):
96 off = OFFS[comp]
97 X, Y, Z = np.meshgrid(g + off[0], g + off[1], g + off[2], indexing="ij")
98 px = X - C[0]; px -= N * np.round(px / N)
99 py = Y - C[1]; py -= N * np.round(py / N)
100 pz = Z - C[2]; pz -= N * np.round(pz / N)
101 r = np.sqrt(px*px + py*py + pz*pz)
102 m = (r > shell[0] * R) & (r < shell[1] * R)
103 qx, qy, qz = px[m], py[m], pz[m]
104 # References are evaluated ONLY on the shell: the lattice sum is O(images x points), so
105 # summing over the whole N^3 grid costs minutes of numpy for a few percent of the cells.
106 ref = (rotlet_lattice(qx, qy, qz, R, OMEGA, float(N), nimg) if nimg is not None
107 else rotlet(qx, qy, qz, R, OMEGA))
108 num += ((fld[m] - ref[comp])**2).sum()
109 den += (ref[comp]**2).sum()
110 npts += int(m.sum())
111 return np.sqrt(num / max(den, 1e-300)), npts
112
113
115 Ns = [int(x) for x in (sys.argv[1:] or ["48", "96", "144"])]
116 nimg = int(os.environ.get("ROTLET_IMAGES", "4"))
117 crossings = os.environ.get("ROTLET_CROSSINGS", "0") == "1"
118 print("rotlet: R/N=%.3f omega=%g near shell r/R %s far shell %s images=%d^3 "
119 "exact crossings=%s" % (RF, OMEGA, NEAR, FAR, 2*nimg+1, crossings))
120 rows = []
121 for N in Ns:
122 res = run(N, crossings)
123 e_near, n_near = err(res, NEAR, nimg)
124 e_far, n_far = err(res, FAR, nimg)
125 e_unb, _ = err(res, NEAR)
126 e_ref2, _ = err(res, NEAR, nimg + 2) # is the lattice reference itself converged?
127 rows.append((N, e_near, e_far))
128 print(" N=%3d R=%.1f cells steps=%3d near/far probes %6d/%7d max|div|=%.2e"
129 % (N, RF * N, res[8], n_near, n_far, res[6]))
130 print(" L2 rel err near %.4e far %.4e (unbounded ref %.4e; reference moves "
131 "%.1e going to %d^3 images)"
132 % (e_near, e_far, e_unb, abs(e_near - e_ref2), 2*(nimg+2)+1))
133 print(" orders (against the %d^3-image lattice):" % (2*nimg+1))
134 for a, b in zip(rows, rows[1:]):
135 pn = np.log(a[1] / b[1]) / np.log(b[0] / a[0])
136 pf = np.log(a[2] / b[2]) / np.log(b[0] / a[0])
137 print(" N %3d -> %3d : near shell %.2f far shell %.2f" % (a[0], b[0], pn, pf))
138
139
141 """IS THE PLATEAU THE SOLVER OR THE REFERENCE?
142
143 Decisive experiment: hold R fixed IN CELLS (so the discretisation error is unchanged) and
144 change only the box, i.e. only R/L. The naive periodic-image correction to an unbounded rotlet
145 scales like (r/L)^3 at fixed r/R, so halving R/L must drop it ~8x; the discretisation error
146 cannot move at all. Whatever the near-shell error does is therefore attributable.
147 """
148 Rcells = float(os.environ.get("ROTLET_RCELLS", "7.2"))
149 global RF
150 print("reference probe: R fixed at %.1f cells; only the box (and so R/L) changes" % Rcells)
151 prev = None
152 for N in [int(x) for x in (sys.argv[1:] or ["72", "144"])]:
153 RF = Rcells / N
154 res = run(N, os.environ.get("ROTLET_CROSSINGS", "0") == "1")
155 e_near, n = err(res, NEAR, int(os.environ.get("ROTLET_IMAGES", "4")))
156 line = (" N=%3d R/L=%.4f R=%.1f cells near-shell L2 rel err %.4e"
157 % (N, Rcells / N, Rcells, e_near))
158 if prev is not None:
159 line += " (dropped %.1fx; the image correction predicts ~%.1fx)" % (
160 prev[1] / e_near, (prev[0] / (Rcells / N))**3 * 0 + (N / prev[2])**3)
161 print(line)
162 prev = (Rcells / N, e_near, N)
163
164
165if __name__ == "__main__":
166 if os.environ.get("ROTLET_MODE", "order") == "refprobe":
168 else:
rotlet_lattice(px, py, pz, R, w, L, nimg)
err(res, shell, nimg=None)
run(N, crossings=False)
rotlet(px, py, pz, R, w)