flow 1.3.0
Kokkos cut-cell IBM incompressible Navier-Stokes solver + pnm pore extraction
Loading...
Searching...
No Matches
vof_orb_weights.py
Go to the documentation of this file.
1#!/usr/bin/env python3
2"""WO-V9 item 4 — the INTERFACE-WEIGHTED ORB.
3
4A two-phase step's cost is not uniform over the grid: the PLIC reconstruction, the geometric
5fluxes and the whole curvature cascade are paid only where the colour is mixed, while the pressure
6and momentum solves are paid everywhere. A plain (cell-count) ORB therefore hands one rank the
7whole interface and another rank none of it. This measures the imbalance that costs, and what
8re-weighting the ORB by
9
10 w(cell) = 1 + W * [the cell is interfacial]
11
12through the existing `rebalance_by_weights` buys. `W` is the ratio of interfacial to bulk work
13per cell; it is swept.
14
15Scene: a 64-marker bubble swarm (the WO-W12 swarm of `tests/study/vof_blocks_ns.py`'s `swarm`
16gate, here as a single global colour field so the GRID decomposition is what is being measured),
17surface tension on, ratio 100.
18
19 mpirun -np 4 python tests/study/vof_orb_weights.py --steps 20 --w 8
20"""
21import argparse
22import math
23import os
24import sys
25import time
26
27import numpy as np
28
29import peclet.flow as flow
30
31HERE = os.path.dirname(os.path.abspath(__file__))
32sys.path.insert(0, HERE)
33import vof_profile as vp # noqa: E402
34
35EPS = 1e-8
36
37
38def swarm_colour(n, seeds, sub=4):
39 """C = 1 inside the markers (the dispersed phase), sub^3-subsampled fractions."""
40 q = (np.arange(sub) + 0.5) / sub
41 C = np.zeros((n, n, n))
42 for (cx, cy, cz, r) in seeds:
43 i0, i1 = max(0, int(cx - r - 2)), min(n, int(cx + r + 3))
44 j0, j1 = max(0, int(cy - r - 2)), min(n, int(cy + r + 3))
45 k0, k1 = max(0, int(cz - r - 2)), min(n, int(cz + r + 3))
46 px = (np.arange(i0, i1)[:, None] + q[None, :]).ravel()
47 py = (np.arange(j0, j1)[:, None] + q[None, :]).ravel()
48 pz = (np.arange(k0, k1)[:, None] + q[None, :]).ravel()
49 d = ((px[:, None, None] - cx) ** 2 + (py[None, :, None] - cy) ** 2
50 + (pz[None, None, :] - cz) ** 2) < r * r
51 blk = d.reshape(i1 - i0, sub, j1 - j0, sub, k1 - k0, sub).mean(axis=(1, 3, 5))
52 C[i0:i1, j0:j1, k0:k1] = np.maximum(C[i0:i1, j0:j1, k0:k1], blk)
53 return np.asfortranarray(C)
54
55
56def seeds_64(n, layout="lattice"):
57 """`lattice` is WO-W12's swarm -- 4x4x4, uniformly filling the box. It is exactly the
58 configuration a CELL-COUNT ORB already balances perfectly, so it cannot show the lever; it is
59 kept as the control. `plume` is the configuration the lever is for: the same 64 markers
60 packed into the bottom third of the column, i.e. a rising swarm before it has spread."""
61 out, k = [], 0
62 if layout == "lattice":
63 for i in range(4):
64 for j in range(4):
65 for m in range(4):
66 r = 2.0 + 3.5 * ((k % 8) / 7.0)
67 out.append(((i + 0.5) * n / 4, (j + 0.5) * n / 4, (m + 0.5) * n / 4, r))
68 k += 1
69 return out
70 rng = np.random.default_rng(3)
71 tries = 0
72 while len(out) < 64 and tries < 200000:
73 tries += 1
74 r = 2.0 + 2.0 * ((len(out) % 8) / 7.0)
75 p = (rng.uniform(r + 1, n - r - 1), rng.uniform(r + 1, n - r - 1),
76 rng.uniform(r + 1, 0.5 * n - r - 1))
77 if all((p[0] - q[0]) ** 2 + (p[1] - q[1]) ** 2 + (p[2] - q[2]) ** 2
78 > (r + q[3] + 1.5) ** 2 for q in out):
79 out.append((p[0], p[1], p[2], r))
80 return out
81
82
83def build(n, Cglob, ratio=100.0):
84 o, sz = flow.mpi_block(n, n, n)
85 s = flow.Solver(*sz)
86 rho_g, rho_l = 1.0, ratio # C = 1 is the DISPERSED phase here
87 s.set_rho(rho_l)
88 s.set_mu(0.05)
89 s.init_mpi(n, n, n)
90 s.set_pressure_geometry(np.full(tuple(sz), 10.0, order="F"))
91 s.set_pressure_chebyshev(True, 600, 1e-12)
92 s.enable_vof()
93 s.set_vof(np.asfortranarray(
94 Cglob[o[0]:o[0] + sz[0], o[1]:o[1] + sz[1], o[2]:o[2] + sz[2]]))
95 s.set_property_model("rho", "linear", "C", [rho_l, rho_g - rho_l])
96 s.set_property_model("mu", "linear", "C", [0.05, 0.05 / ratio - 0.05])
97 s.set_surface_tension(1.0)
98 s.set_property_model("force_z", "linear", "C", [-1e-4 * rho_l, -1e-4 * (rho_g - rho_l)])
99 s.diagnostics.set_vof_curvature_worklist(os.environ.get("V9_CURV_WL", "1") == "1")
100 s.diagnostics.set_vof_worklist(os.environ.get("V9_ADV_WL", "1") == "1")
101 s.set_dt(0.4 * s.capillary_dt())
102 return s, o, sz
103
104
106 C = np.asarray(s.get_vof())
107 return int(((C > EPS) & (C < 1.0 - EPS)).sum())
108
109
110def gather_ints(s, v):
111 """max/mean over ranks of an integer per-rank load, using the solver's own allreduce-free
112 route: mpi4py if present, else a plain allgather through it."""
113 from mpi4py import MPI
114 return MPI.COMM_WORLD.allgather(int(v))
115
116
117def timed(s, steps, cap):
118 pick = vp.dt_from_limits(0.4)
119 return vp.profile(s, steps, pick, cap, "")
120
121
122def main():
123 ap = argparse.ArgumentParser()
124 ap.add_argument("--n", type=int, default=64)
125 ap.add_argument("--steps", type=int, default=20)
126 ap.add_argument("--warm", type=int, default=5)
127 ap.add_argument("--w", type=float, default=8.0, help="interfacial work multiplier")
128 ap.add_argument("--layout", choices=("lattice", "plume"), default="plume")
129 a = ap.parse_args()
130 from mpi4py import MPI
131 comm = MPI.COMM_WORLD
132 rank, size = comm.Get_rank(), comm.Get_size()
133 n = a.n
134 Cglob = swarm_colour(n, seeds_64(n, a.layout))
135 s, o, sz = build(n, Cglob)
136 pick = vp.dt_from_limits(0.4)
137 for i in range(a.warm):
138 pick(s, i)
139 s.step()
140
141 def report(tag):
142 loads = gather_ints(s, mixed_count(s))
143 cells = gather_ints(s, int(np.prod(np.asarray(s.get_vof()).shape)))
144 tm = timed(s, a.steps, 600)
145 if rank == 0:
146 mx, mean = max(loads), sum(loads) / len(loads)
147 cmx, cmean = max(cells), sum(cells) / len(cells)
148 print(f"\n--- {tag} (np = {size}, {n}^3, {a.layout}) ---")
149 print(f" mixed cells per rank : {loads}")
150 print(f" imbalance (mixed) : {mx/mean:.4f} (max {mx}, mean {mean:.1f})")
151 print(f" cells per rank : {cells}")
152 print(f" imbalance (cells) : {cmx/cmean:.4f}")
153 print(f" ms/step : {tm['ms_per_step']:.3f} "
154 f"pressure {tm['press_max']}/600"
155 + (" CAPPED" if tm["capped"] else ""))
156 k = max(tm["steps"], 1)
157 print(f" VoF stage : {1e3*(tm['vof_advect']+tm['curvature']+tm['csf'])/k:.3f}"
158 f" ms/step projection {1e3*tm['projection']/k:.3f} "
159 f"momentum {1e3*tm['momentum_solve']/k:.3f}")
160 return tm
161
162 t_before = report("plain ORB (cell-count weights)")
163
164 # The weight field: the CURRENT interface, gathered from every rank's own colour so the weights
165 # describe the state being rebalanced (not the initial condition).
166 Cl = np.asarray(s.get_vof())
167 mixed = ((Cl > EPS) & (Cl < 1.0 - EPS)).astype(np.float64)
168 Wglob = np.zeros((n, n, n), order="F")
169 Wl = np.zeros((n, n, n), order="F")
170 Wl[o[0]:o[0] + sz[0], o[1]:o[1] + sz[1], o[2]:o[2] + sz[2]] = mixed
171 comm.Allreduce(Wl, Wglob, op=MPI.SUM)
172 w = (1.0 + a.w * Wglob).ravel(order="F")
173 if rank == 0:
174 print(f"\n weights: 1 + {a.w} * [mixed]; {int(Wglob.sum())} interfacial cells of "
175 f"{n**3} ({100*Wglob.sum()/n**3:.2f} %)")
176 s.diagnostics.rebalance_by_weights(list(w))
177 t_after = report(f"interface-weighted ORB (W = {a.w})")
178 if rank == 0:
179 b, af = t_before["ms_per_step"], t_after["ms_per_step"]
180 print(f"\n ms/step {b:.3f} -> {af:.3f} ({100*(af/b-1):+.2f} %)")
181
182
183if __name__ == "__main__":
184 main()
timed(s, steps, cap)
swarm_colour(n, seeds, sub=4)
seeds_64(n, layout="lattice")
static double mean(const std::vector< double > &a)