28MU, RHO, DT, STEPS = 0.1, 1.0, 200.0, 400
31KN_R, KI_I, KI_R = 16, 2, 17
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)
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
52def run(N, crossings=False):
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
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)
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])
73 s.set_exact_crossings_from_scene()
74 s.set_solid_from_scene(
True)
76 for it
in range(STEPS):
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):
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)
86OFFS = ((-0.5, 0.0, 0.0), (0.0, -0.5, 0.0), (0.0, 0.0, -0.5))
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)
95 for comp, fld
in enumerate((u, v, w)):
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]
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()
111 return np.sqrt(num / max(den, 1e-300)), npts
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))
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)
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))
141 """IS THE PLATEAU THE SOLVER OR THE REFERENCE?
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.
148 Rcells = float(os.environ.get(
"ROTLET_RCELLS",
"7.2"))
150 print(
"reference probe: R fixed at %.1f cells; only the box (and so R/L) changes" % Rcells)
152 for N
in [int(x)
for x
in (sys.argv[1:]
or [
"72",
"144"])]:
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))
159 line +=
" (dropped %.1fx; the image correction predicts ~%.1fx)" % (
160 prev[1] / e_near, (prev[0] / (Rcells / N))**3 * 0 + (N / prev[2])**3)
162 prev = (Rcells / N, e_near, N)