flow 1.3.0
Kokkos cut-cell IBM incompressible Navier-Stokes solver + pnm pore extraction
Loading...
Searching...
No Matches
koren_branch.py
Go to the documentation of this file.
1#!/usr/bin/env python
2"""Q-N (WO-9, ruling D-WO9-1): where does G9 koren at R_o/h 32 lose bitwise agreement between two
3runs (two backends, or one OpenMP build at two thread counts), and why?
4
5 OMP_NUM_THREADS=1 PYTHONPATH=build_dev python koren_branch.py run a.npz [nsteps]
6 OMP_NUM_THREADS=4 PYTHONPATH=build_dev python koren_branch.py run b.npz [nsteps]
7 python koren_branch.py analyse a.npz b.npz
8
9`run` drives the G9 gate case unchanged (tests/python/test_scalar_cutcell_gates.py g9_case) and
10records c after every step. `analyse` finds the first step whose difference jumps, and for every
11face evaluates, on each run's c^n, which branch of sadv::koren (src/staggered_advection.hpp) the
12explicit flux takes: |den| < 1e-10 (r = 0), both |num|, |den| < 1e-10 (r = 1), or the ratio. A
13koren face is one the kernel's ยง6.2 guard admits (upwind-upwind, upwind and downwind all unknowns).
14"""
15import math
16import os
17import sys
18
19import numpy as np
20
21HERE = os.path.dirname(os.path.abspath(__file__))
22sys.path.insert(0, os.path.join(HERE, "..", "..", "python"))
23
24
25def run(path, nsteps):
26 import test_scalar_cutcell_gates as g
27 cs, made, its, res = [], [], [], []
28
29 class Rec(g.pf.Solver):
30 def __init__(self, *a, **k):
31 super().__init__(*a, **k)
32 made.append(self)
33
34 def advance_scalars(self, *a, **k):
35 r = super().advance_scalars(*a, **k)
36 cs.append(np.array(self.get_field("c")))
37 cen = self.diagnostics.scalar_census("c")
38 its.append(int(cen["krylov_iterations"]))
39 res.append(float(cen["krylov_residual"]))
40 return r
41
42 g.pf.Solver = Rec
43 Rh = 32
44 g.g9_case(Rh, g.g9_offsets(Rh)[0], os.environ.get("SCHEME", "koren"), 0.5, nsteps=nsteps)
45 s = made[-1]
46 h = g.R_O / Rh
47 fx, fy = g.annulus_fluxes(s, h)
48 geo = s.diagnostics.scalar_geometry("c")
49 np.savez_compressed(path, c=np.stack(cs), unk=np.asarray(geo["unknown"]),
50 kappa=np.asarray(geo["kappa"]), fx=fx, fy=fy, its=np.asarray(its),
51 res=np.asarray(res))
52 print(f"wrote {path}: {len(cs)} steps")
53
54
55def branch(c, f, a):
56 """Per low face of every cell along axis a (periodic): the koren branch code on c^n for the
57 sign of f: 0 ratio, 1 |den| < 1e-10 (r = 0), 2 both < 1e-10 (r = 1); and the upwind-pair /
58 downwind unknown flags handled by the caller."""
59 cm1, cm2, cp1 = (np.roll(c, 1, axis=a), np.roll(c, 2, axis=a), np.roll(c, -1, axis=a))
60 pos = f > 0.0
61 num = np.where(pos, cm1 - cm2, c - cp1)
62 den = np.where(pos, c - cm1, cm1 - c)
63 small_d = np.abs(den) < 1e-10
64 small_n = np.abs(num) < 1e-10
65 code = np.where(small_d & small_n, 2, np.where(small_d, 1, 0))
66 return code, num, den
67
68
69def analyse(pa, pb):
70 A, B = np.load(pa), np.load(pb)
71 unk = A["unk"] > 0.5
72 cA, cB = A["c"], B["c"]
73 n = min(len(cA), len(cB))
74 d = np.array([np.max(np.abs(cA[k] - cB[k])[unk]) for k in range(n)])
75 cmax = float(np.max(np.abs(cA[0])))
76 jump = next((k for k in range(1, n) if d[k] > 1e3 * max(d[k - 1], 1e-30) and d[k] > 1e-14),
77 None)
78 print("per-step max|cA - cB| (absolute; max c ~ %.3g):" % cmax)
79 for k in range(n):
80 if k < 3 or (jump is not None and abs(k - jump) <= 2) or k == n - 1:
81 print(f" step {k + 1:4d}: {d[k]:.3e}")
82 if jump is None:
83 print("no jump found")
84 return
85 k = jump # step jump+1 consumed c^n = c after step `jump`
86 cnA, cnB = cA[k - 1], cB[k - 1]
87 loc = np.unravel_index(np.argmax(np.where(unk, np.abs(cA[k] - cB[k]), 0.0)), unk.shape)
88 print(f"jump at step {k + 1}: argmax cell {tuple(int(v) for v in loc)}, c^n there "
89 f"{cnA[loc]:.3e}, |dc^n| there {abs(cnA[loc] - cnB[loc]):.3e}; "
90 f"after: |dc| {abs(cA[k][loc] - cB[k][loc]):.3e}")
91 for a, f in ((0, A["fx"]), (1, A["fy"])):
92 u0, um1, um2, up1 = unk, np.roll(unk, 1, a), np.roll(unk, 2, a), np.roll(unk, -1, a)
93 koren = (f != 0.0) & u0 & um1 & np.where(f > 0.0, um2, up1)
94 bA, nA, dA = branch(cnA, f, a)
95 bB, nB, dB = branch(cnB, f, a)
96 diff = koren & (bA != bB)
97 for idx in zip(*np.nonzero(diff)):
98 dist = max(abs(int(idx[0]) - int(loc[0])), abs(int(idx[1]) - int(loc[1])))
99 print(f" axis {a} face {tuple(int(v) for v in idx)} (cheb. distance {dist} to the "
100 f"jump): branch A {bA[idx]} B {bB[idx]}; num {nA[idx]:.17e} / {nB[idx]:.17e}; "
101 f"den {dA[idx]:.17e} / {dB[idx]:.17e}; F/V {f[idx]:.3e}")
102 print(f"axis {a}: koren faces {int(koren.sum())}, branch differs on {int(diff.sum())}")
103 # the reach of the c^n differences near the threshold
104 near = unk & (np.abs(cnA) < 1e-8) & (np.abs(cnA) > 1e-12)
105 if np.any(near):
106 rel = np.abs(cnA - cnB)[near]
107 print(f"cells with 1e-12 < c^n < 1e-8: {int(near.sum())}, max |dc^n| {rel.max():.3e}")
108
109
110def onestep(src, k, out, rtol=None):
111 """Re-run step k+1 alone from run `src`'s c after step k, on a fresh solver (same setup as
112 g9_case), and store c after it: two thread counts from the SAME c^n isolate what the step
113 itself generates from what it amplifies."""
114 import test_scalar_cutcell_gates as g
115 Rh = 32
116 s, h = g.annulus_case(Rh, g.g9_offsets(Rh)[0], "koren")
117 geo = s.diagnostics.scalar_geometry("c")
118 unk = geo["unknown"] > 0.5
119 kap = geo["kappa"]
120 ax, ay = geo["aperture_x"], geo["aperture_y"]
121 full = unk & (kap >= 1.0)
122 for a, ap in ((0, ax), (1, ay), (2, geo["aperture_z"])):
123 full &= (ap >= 1.0) & (np.roll(ap, -1, axis=a) >= 1.0)
124 fx, fy = g.annulus_fluxes(s, h)
125 o = (np.maximum(np.roll(fx, -1, axis=0), 0) + np.maximum(-fx, 0) +
126 np.maximum(np.roll(fy, -1, axis=1), 0) + np.maximum(-fy, 0))
127 T = 2.0 * math.pi
128 nrev = int(math.ceil(T * float(np.max(o[full])) / 0.5))
129 s.set_dt(T / nrev)
130 g.set_annulus_velocity(s, fx, fy)
131 if rtol is not None:
132 s.set_scalar_tolerance("c", rtol)
133 s.set_field("c", np.asfortranarray(np.load(src)["c"][k - 1]))
134 s.advance_scalars()
135 cen = s.diagnostics.scalar_census("c")
136 np.savez_compressed(out, c=np.array(s.get_field("c")), its=cen["krylov_iterations"],
137 res=cen["krylov_residual"])
138 print(f"step {k + 1} from {src}: its {cen['krylov_iterations']} res "
139 f"{cen['krylov_residual']:.17e}")
140
141
142if __name__ == "__main__":
143 if sys.argv[1] == "run":
144 run(sys.argv[2], int(sys.argv[3]) if len(sys.argv) > 3 else 70)
145 elif sys.argv[1] == "onestep":
146 onestep(sys.argv[2], int(sys.argv[3]), sys.argv[4])
147 else:
148 analyse(sys.argv[2], sys.argv[3])
run(path, nsteps)
onestep(src, k, out, rtol=None)
analyse(pa, pb)
branch(c, f, a)