flow 1.3.0
Kokkos cut-cell IBM incompressible Navier-Stokes solver + pnm pore extraction
Loading...
Searching...
No Matches
scalar_cutcell_backends.py
Go to the documentation of this file.
1#!/usr/bin/env python
2"""§11 G11 (WO-9): the cut-cell scalar on two Kokkos backends, CUDA against OpenMP.
3
4Not a ctest: it needs two build trees. Run the same rows on each tree, then compare:
5
6 OMP_NUM_THREADS=4 PYTHONPATH=build_dev python tests/python/scalar_cutcell_backends.py dump omp.npz
7 PYTHONPATH=build_cuda python tests/python/scalar_cutcell_backends.py dump gpu.npz
8 python tests/python/scalar_cutcell_backends.py compare omp.npz gpu.npz [omp_1thread.npz]
9
10The optional third dump (the reference build at another OMP_NUM_THREADS) prints the reference
11backend's own reproducibility floor beside the cross-backend difference (column 'floor').
12
13The rows are the gate cases of tests/python/test_scalar_cutcell_gates.py, called unchanged (their
14solver is captured by a recording subclass that logs every solve's BiCGStab count):
15
16 g1_<Rh> G1 Dirichlet sphere, steady (single phase), R/h 8 / 16 / 32
17 g6c1_<Rh> G6 case 1 (conjugate composite sphere, 30 BE steps), R/h 8 / 16
18 g9_<scheme>_<Rh>[_c09] G9 annulus advection (transient, explicit/implicit faces, small cells),
19 one revolution at bulk Courant 0.5 (0.9), fou / koren, R_o/h 16 / 32
20 gadva_<pe> G-adv(a) (steady advective, A2 surrogate), R/h 16, Pe_h 1 / 10
21 gadvb_<wall>_<pe> G-adv(b) closure (Neumann: mean-gradient mode, singular) and (b') Dirichlet,
22 32^3, Pe_h 1 / 10
23
24G11 (doc/scalar_ibm_design.md §11): geometry fields <= 1e-12 relative; probe rungs identical;
25solutions <= 1e-9 relative; iterations within +-1 per solve. "Relative" = the max-norm difference
26over the max-norm of the reference array (solutions: over the unknowns of the reference).
27The koren rows of G9 (ruling D-WO9-1, §11 G11 as restated): solution <= 1e-7 relative, and G9's
28own integral bounds (budget identity, conservation, positivity, finite) met on BOTH dumps. Their
29pointwise difference is reduction-order noise inside the Krylov tolerance that the koren update
30does not damp (doc/scalar_ibm_log.md, WO-9b Q-N); no backend tolerance holds it at 1e-9.
31Exit 0 pass, 1 fail, 77 skipped (no module).
32"""
33import os
34import sys
35import time
36
37import numpy as np
38
39HERE = os.path.dirname(os.path.abspath(__file__))
40
41GEOM_CELL = ("kappa", "aperture_x", "aperture_y", "aperture_z", "kappa_solid")
42EXACT_CELL = ("unknown", "solid_unknown")
43GEOM_FACET = ("centroid", "normal", "area")
44EXACT_FACET = ("instance", "probe_rung")
45SOL_FACET = ("flux", "wall_value")
46G9_STATS = ("ident", "cons", "min", "max", "cmax0", "finite")
47
48
49def g9_bounds(D, n):
50 """G9 (i)-(iv) on one dump's row n, as test_scalar_cutcell_gates.gate_g9 gates them (koren's
51 (iii) only at bulk Courant 1/2, ruling D-WO5-1; the rtol-1e-13 drift clause is not a G11 row)."""
52 s = {k: float(D[f"{n}/g9/{k}"]) for k in G9_STATS}
53 if "_fou_" in n:
54 pos = s["min"] >= -1e-12 * s["cmax0"]
55 elif n.endswith("_c09"):
56 pos = True
57 else:
58 pos = s["min"] >= -1e-3 * s["cmax0"] and s["max"] <= (1 + 1e-3) * s["cmax0"]
59 return s["ident"] <= 1e-13 and s["cons"] <= 1e-12 and pos and s["finite"] > 0.5
60
61
62def rows(g):
63 """name -> thunk; each thunk runs one gate case of the gates module `g`."""
64 r = {}
65 for Rh in (8, 16, 32):
66 r[f"g1_{Rh}"] = lambda Rh=Rh: g.g1_case(Rh, g.OFFSETS[0])
67 for Rh in (8, 16):
68 r[f"g6c1_{Rh}"] = lambda Rh=Rh: g.g6_case(Rh, g.OFFSETS[0], g.G6_CASES[0])
69 for Rh in (16, 32):
70 for sch in ("fou", "koren"):
71 for cr in (0.5, 0.9):
72 r[f"g9_{sch}_{Rh}" + ("" if cr == 0.5 else "_c09")] = (
73 lambda Rh=Rh, sch=sch, cr=cr: g.g9_case(Rh, g.g9_offsets(Rh)[0], sch, cr))
74 for pe in (1.0, 10.0):
75 r[f"gadva_{pe:g}"] = lambda pe=pe: g.gadv_a_case(16, pe)
76 for wall in ("neumann", "dirichlet"):
77 r[f"gadvb_{wall}_{pe:g}"] = lambda pe=pe, wall=wall: g.gadv_b_case(32, pe, wall)
78 return r
79
80
81def dump(path, only=None):
82 try:
83 import peclet.flow as pf
84 except ImportError:
85 print("SKIP: peclet.flow not importable")
86 sys.exit(77)
87 sys.path.insert(0, HERE)
88 import test_scalar_cutcell_gates as g
89
90 made, its = [], []
91
92 class Recording(pf.Solver):
93 """The gates' Solver, logging every cut-cell solve's BiCGStab count (scalar 'c')."""
94
95 def __init__(self, *a, **k):
96 super().__init__(*a, **k)
97 made.append(self)
98
99 def _log(self):
100 its.append(int(self.diagnostics.scalar_census("c")["krylov_iterations"]))
101
102 def solve_scalar_steady(self, *a, **k):
103 r = super().solve_scalar_steady(*a, **k)
104 self._log()
105 return r
106
107 def advance_scalars(self, *a, **k):
108 r = super().advance_scalars(*a, **k)
109 self._log()
110 return r
111
112 g.pf.Solver = Recording # the gates module builds every case through pf.Solver
113 out = {}
114 for name, run in rows(g).items():
115 if only and name not in only:
116 continue
117 made.clear()
118 its.clear()
119 t0 = time.time()
120 st = run()
121 dt = time.time() - t0
122 s = made[-1]
123 geo = s.diagnostics.scalar_geometry("c")
124 for k in GEOM_CELL + EXACT_CELL:
125 if k in geo:
126 out[f"{name}/geo/{k}"] = np.asarray(geo[k])
127 fac = s.diagnostics.scalar_facets("c")
128 for k in GEOM_FACET + EXACT_FACET + SOL_FACET:
129 out[f"{name}/fac/{k}"] = np.asarray(fac[k])
130 out[f"{name}/sol/c"] = np.asarray(s.get_field("c"))
131 if "solid_unknown" in geo and np.any(np.asarray(geo["solid_unknown"]) > 0.5):
132 out[f"{name}/sol/c_solid"] = np.asarray(s.get_field("c_solid"))
133 out[f"{name}/its"] = np.asarray(its, dtype=np.int64)
134 cen = s.diagnostics.scalar_census("c")
135 pr = cen["probe_rungs"]
136 out[f"{name}/rungs"] = np.asarray([*pr["fluid"], *pr.get("solid", ())], dtype=np.int64)
137 out[f"{name}/time"] = np.asarray(dt)
138 if name.startswith("g9_"): # G9's integral quantities (the koren rows' gate, D-WO9-1)
139 for k in G9_STATS:
140 out[f"{name}/g9/{k}"] = np.asarray(float(st[k]))
141 print(f"{name:20s} {dt:7.1f} s solves {len(its):4d} iterations {sum(its):6d} "
142 f"rungs {out[f'{name}/rungs'].tolist()}", flush=True)
143 np.savez_compressed(path, **out)
144 print(f"wrote {path} ({len(out)} arrays)")
145
146
147def rel(a, b, mask=None):
148 a, b = np.asarray(a, dtype=float), np.asarray(b, dtype=float)
149 if mask is not None:
150 a, b = a[mask], b[mask]
151 fin = np.isfinite(a)
152 if not np.array_equal(fin, np.isfinite(b)):
153 return float("inf")
154 a, b = a[fin], b[fin]
155 if a.size == 0:
156 return 0.0
157 sc = float(np.max(np.abs(a)))
158 d = float(np.max(np.abs(a - b)))
159 return d / sc if sc > 0.0 else d
160
161
162def compare(pa, pb, pc=None):
163 A, B = np.load(pa), np.load(pb)
164 C = np.load(pc) if pc else None
165 names = sorted({k.split("/")[0] for k in A.files})
166 bad = []
167 print(f"{'row':20s} {'geom':>9s} {'exact':>5s} {'solution':>9s} {'its diff':>8s} "
168 f"{'solves':>6s} {'t_ref':>7s} {'t_new':>7s}")
169 for n in names:
170 if f"{n}/its" not in B.files:
171 print(f"{n:20s} missing in {pb}")
172 bad.append(n)
173 continue
174 g = max([rel(A[k], B[k]) for k in A.files
175 if k.startswith(f"{n}/") and k.split("/")[-1] in GEOM_CELL + GEOM_FACET] or [0.0])
176 ex = all(np.array_equal(A[k], B[k]) for k in A.files
177 if k.startswith(f"{n}/") and k.split("/")[-1] in EXACT_CELL + EXACT_FACET)
178 ex = ex and np.array_equal(A[f"{n}/rungs"], B[f"{n}/rungs"])
179 unk = A[f"{n}/geo/unknown"] > 0.5
180 sol = rel(A[f"{n}/sol/c"], B[f"{n}/sol/c"], unk)
181 if f"{n}/sol/c_solid" in A.files:
182 sol = max(sol, rel(A[f"{n}/sol/c_solid"], B[f"{n}/sol/c_solid"],
183 A[f"{n}/geo/solid_unknown"] > 0.5))
184 sol = max(sol, max(rel(A[f"{n}/fac/{k}"], B[f"{n}/fac/{k}"]) for k in SOL_FACET))
185 floor = ""
186 if C is not None and f"{n}/sol/c" in C.files:
187 floor = f" floor {rel(A[f'{n}/sol/c'], C[f'{n}/sol/c'], unk):9.2e}"
188 ia, ib = A[f"{n}/its"], B[f"{n}/its"]
189 di = int(np.max(np.abs(ia - ib))) if ia.shape == ib.shape else 10**9
190 koren = n.startswith("g9_koren_")
191 ok = g <= 1e-12 and ex and sol <= (1e-7 if koren else 1e-9) and di <= 1
192 if n.startswith("g9_"):
193 if f"{n}/g9/ident" in A.files and f"{n}/g9/ident" in B.files:
194 ok = ok and g9_bounds(A, n) and g9_bounds(B, n)
195 elif koren:
196 ok = False # the restated koren row needs G9's integral quantities on both dumps
197 print(f"{n:20s} {g:9.2e} {'yes' if ex else 'NO':>5s} {sol:9.2e} {di:8d} {ia.size:6d} "
198 f"{float(A[f'{n}/time']):7.1f} {float(B[f'{n}/time']):7.1f} {'ok' if ok else 'FAIL'}{floor}")
199 if not ok:
200 bad.append(n)
201 print("G11:", "PASS" if not bad else f"FAIL {bad}")
202 return 0 if not bad else 1
203
204
205if __name__ == "__main__":
206 if len(sys.argv) >= 3 and sys.argv[1] == "dump":
207 dump(sys.argv[2], set(sys.argv[3:]) or None)
208 elif len(sys.argv) in (4, 5) and sys.argv[1] == "compare":
209 sys.exit(compare(*sys.argv[2:]))
210 else:
211 print(__doc__)
212 sys.exit(2)
static int run(int bc)