flow 1.3.0
Kokkos cut-cell IBM incompressible Navier-Stokes solver + pnm pore extraction
Loading...
Searching...
No Matches
mg_dense_precond.cpp
Go to the documentation of this file.
1// WO-M — DENSE ASSEMBLY OF THE MULTIGRID V-CYCLE PRECONDITIONER.
2//
3// Why this exists. WO-H closed the wall-bounded MG-PCG stall and, in doing so, measured a
4// *residual* failure it could not fix: on a high-contrast coefficient the V-cycle preconditioner M
5// goes INDEFINITE (a negative LDL pivot of sym(M) from density ratio ~1e3 walled and ~1e4 even
6// fully periodic), which no choice of the CG beta survives. WO-H attributed that to the arithmetic
7// coarsening of the face coefficient (`coarsenOpenAvg`) — VOF_PLAN's S3 item. But every one of
8// those measurements was taken on a hierarchy whose operator is stored in FLOAT
9// (`mac_cutcell_mg.hpp` MReal), and float rounding breaks the singular row-sum identity A*1 = 0 at
10// ~eps_f32 per row; under three decades of coefficient contrast that defect is ~1e-4 relative to
11// the small couplings. So a float-perturbed near-singular operator is a fully sufficient
12// ALTERNATIVE explanation for the same negative pivot, and nothing in the record separates the two.
13//
14// This probe separates them. It assembles M column by column exactly as CG builds it — one
15// mean-removed unit basis vector in, one symmetric V-cycle, the result out — on the SAME code path
16// the CG drivers use, and writes the dense matrix to disk. Building this file twice, with and
17// without -DPECLET_FLOW_OPERATOR_DOUBLE, changes ONLY the operator storage precision. If the
18// negative pivot disappears in double, the S3 coefficient-coarsening theory loses its evidence; if
19// it survives, S3 stands as an independent mechanism.
20//
21// It also assembles the FINE operator A densely (same unit-vector trick) so the mechanism claim
22// itself — the per-row defect of A*1 = 0 — is a measured number rather than an argument.
23//
24// Analysis (skew, LDL pivots, spectrum of sym(M) on the mean-free subspace) is done by the sibling
25// mg_precond_analyze.py: LAPACK gives an unambiguous smallest eigenvalue, where an unpivoted LDL on
26// a matrix that is singular BY CONSTRUCTION (the constant mode) can report a sign-noise pivot at
27// 1e-12 that means nothing.
28//
29// Usage:
30// ./mg_dense_precond --n 8 --levels 3 --geom periodic|wallz --ratio 1e3 --out M.bin
31// ./mg_dense_precond --sweep --outdir <dir> (the whole geom x ratio battery)
32#include <cmath>
33#include <cstdint>
34#include <cstdio>
35#include <cstdlib>
36#include <cstring>
37#include <Kokkos_Core.hpp>
38#include <string>
39#include <vector>
40
41#include "mac_cutcell_mg.hpp"
42
43namespace {
44
49
50struct Config {
51 int n = 8;
52 int levels = 3;
53 int geom = 0; // 0 = fully periodic, 1 = walls +-z
54 double ratio = 1; // density contrast of a sharp mid-height z-slab (1 = uniform coefficient)
55};
56
57const char* geomName(int g) {
58 return g == 0 ? "periodic" : "wallz";
59}
60
61// Write a dense row-major n x n double matrix with a 3-int64 header {magic, rows, cols}.
62void writeMatrix(const std::string& path, const std::vector<double>& a, long n) {
63 FILE* f = std::fopen(path.c_str(), "wb");
64 if (!f) {
65 std::fprintf(stderr, "mg_dense_precond: cannot open %s for writing\n", path.c_str());
66 return;
67 }
68 const std::int64_t hdr[3] = {0x50434D47444E5331LL, n, n}; // "PCMGDNS1"
69 std::fwrite(hdr, sizeof(std::int64_t), 3, f);
70 std::fwrite(a.data(), sizeof(double), (size_t)n * (size_t)n, f);
71 std::fclose(f);
72}
73
74// One configuration: build the hierarchy, assemble A and M densely, report and write.
75void run(const Config& cfg, const std::string& outdir) {
76 const int nx = cfg.n, ny = cfg.n, nz = cfg.n;
77 const int G = CutcellMG::G;
78 const C3 ext{nx + 2 * G, ny + 2 * G, nz + 2 * G};
79 const long sy = ext.x, sz = (long)ext.x * ext.y;
80 const long next = (long)ext.x * ext.y * ext.z;
81 const long n = (long)nx * ny * nz;
82
83 CutcellMG mg;
84 mg.init(nx, ny, nz, cfg.levels);
85 int bc[6] = {0, 0, 0, 0, 0, 0};
86 if (cfg.geom == 1) {
87 bc[4] = 1; // -z wall
88 bc[5] = 1; // +z wall
89 }
90 mg.setBoundaryConditions(bc);
91
92 // Face coefficients c_f = open_f * rho0/rho_f, arithmetic face mean of rho (buildRhoCoeff), from
93 // a SHARP mid-height slab: rho = rho0 below, rho0*ratio above. Uniform openness 1 — the geometry
94 // is a box, so the ONLY thing varying is the coefficient contrast this probe is about.
95 std::vector<double> rho((size_t)next, 1.0);
96 for (int z = 0; z < ext.z; ++z) {
97 // periodic wrap of the ghost planes onto the inner slab pattern
98 int zi = ((z - G) % nz + nz) % nz;
99 const double r = (zi >= nz / 2) ? cfg.ratio : 1.0;
100 for (int y = 0; y < ext.y; ++y)
101 for (int x = 0; x < ext.x; ++x)
102 rho[(size_t)(x + (long)y * sy + (long)z * sz)] = r;
103 }
104 std::vector<double> hcx((size_t)next, 0.0), hcy((size_t)next, 0.0), hcz((size_t)next, 0.0);
105 for (int z = G; z < ext.z - G; ++z)
106 for (int y = G; y < ext.y - G; ++y)
107 for (int x = G; x < ext.x - G; ++x) {
108 const long i = x + (long)y * sy + (long)z * sz;
109 hcx[(size_t)i] = 1.0 / (0.5 * (rho[(size_t)i] + rho[(size_t)(i - 1)]));
110 hcy[(size_t)i] = 1.0 / (0.5 * (rho[(size_t)i] + rho[(size_t)(i - sy)]));
111 hcz[(size_t)i] = 1.0 / (0.5 * (rho[(size_t)i] + rho[(size_t)(i - sz)]));
112 }
113 CCField cx("cx", next), cy("cy", next), cz("cz", next);
114 auto hx = Kokkos::create_mirror_view(cx), hy = Kokkos::create_mirror_view(cy),
115 hz = Kokkos::create_mirror_view(cz);
116 for (long i = 0; i < next; ++i) {
117 hx(i) = hcx[(size_t)i];
118 hy(i) = hcy[(size_t)i];
119 hz(i) = hcz[(size_t)i];
120 }
121 Kokkos::deep_copy(cx, hx);
122 Kokkos::deep_copy(cy, hy);
123 Kokkos::deep_copy(cz, hz);
124 mg.setOpenness(CCConst(cx), CCConst(cy), CCConst(cz), 1.0, 1.0, 1.0);
125
126 CutcellMG::Level& l0 = mg.level(0);
127 CCField v("v", next), y("y", next);
128 // Put the V-cycle on the PRODUCTION smoothing schedule. pre_/post_/bottom_ are private and are
129 // written by the Krylov entry points, so one zero-iteration solvePCG with the solver's own
130 // (2, 2, 12) is the setter — IbmSolver::project calls solvePCG/solveFCG with exactly those.
131 {
132 CCField s0("s0", next), s1("s1", next), s2("s2", next), s3("s3", next), s4("s4", next);
133 mg.solvePCG(s0, s1, s2, s3, s4, y, /*maxit=*/0, 1e-8, 2, 2, 12);
134 }
135 auto hv = Kokkos::create_mirror_view(v), hy2 = Kokkos::create_mirror_view(y);
136
137 std::vector<long> idx((size_t)n); // inner ordinal -> extended linear index
138 for (int k = 0; k < nz; ++k)
139 for (int j = 0; j < ny; ++j)
140 for (int i = 0; i < nx; ++i)
141 idx[(size_t)(i + (long)j * nx + (long)k * nx * ny)] =
142 (i + G) + (long)(j + G) * sy + (long)(k + G) * sz;
143
144 std::vector<double> A((size_t)n * (size_t)n, 0.0), M((size_t)n * (size_t)n, 0.0);
145
146 // --- the fine operator A, column by column (matvecOverlap is what solvePCG calls) -------------
147 for (long c = 0; c < n; ++c) {
148 Kokkos::deep_copy(hv, 0.0);
149 hv(idx[(size_t)c]) = 1.0;
150 Kokkos::deep_copy(v, hv);
151 mg.matvecOverlap(l0, y, v);
152 Kokkos::deep_copy(hy2, y);
153 for (long r = 0; r < n; ++r)
154 A[(size_t)r * (size_t)n + (size_t)c] = hy2(idx[(size_t)r]);
155 }
156
157 // --- the preconditioner M, column by column ---------------------------------------------------
158 // Exactly solvePCG's `precond` lambda, preceded by the mean removal CG applies to the residual
159 // before every preconditioner application (removeMean(l0, r) in the CG loop).
160 for (long c = 0; c < n; ++c) {
161 Kokkos::deep_copy(hv, 0.0);
162 hv(idx[(size_t)c]) = 1.0;
163 Kokkos::deep_copy(v, hv);
164 mg.removeMean(l0, v);
165 Kokkos::deep_copy(l0.rhs, v);
166 Kokkos::deep_copy(l0.x, 0.0);
167 mg.vcycle(0, /*sym=*/true);
168 Kokkos::deep_copy(hy2, l0.x);
169 for (long r = 0; r < n; ++r)
170 M[(size_t)r * (size_t)n + (size_t)c] = hy2(idx[(size_t)r]);
171 }
172
173 // --- the mechanism number: the per-row defect of A*1 = 0 ---------------------------------------
174 // Every interior row of the singular Poisson operator sums to zero EXACTLY in exact arithmetic
175 // (the diagonal is minus the sum of the off-diagonals). Float storage breaks that at ~eps_f32
176 // relative to the LARGEST coefficient in the row; on a row that mixes a large and a tiny coupling
177 // the same absolute defect is ~eps_f32 * contrast relative to the TINY one. Report both norms.
178 double rowsumAbs = 0, rowsumRelMax = 0, rowsumRelMin = 0;
179 for (long r = 0; r < n; ++r) {
180 double s = 0, mx = 0, mn = 1e300;
181 for (long c = 0; c < n; ++c) {
182 const double a = A[(size_t)r * (size_t)n + (size_t)c];
183 s += a;
184 if (c != r && a != 0.0) {
185 mx = std::fmax(mx, std::fabs(a));
186 mn = std::fmin(mn, std::fabs(a));
187 }
188 }
189 if (mx == 0.0)
190 continue;
191 rowsumAbs = std::fmax(rowsumAbs, std::fabs(s));
192 rowsumRelMax = std::fmax(rowsumRelMax, std::fabs(s) / mx);
193 rowsumRelMin = std::fmax(rowsumRelMin, std::fabs(s) / mn);
194 }
195
196 // --- skew, printed here for a quick read (the spectrum is Python's job) ------------------------
197 double fs = 0, fa = 0;
198 for (long r = 0; r < n; ++r)
199 for (long c = 0; c < n; ++c) {
200 const double m = M[(size_t)r * (size_t)n + (size_t)c],
201 t = M[(size_t)c * (size_t)n + (size_t)r];
202 fa += m * m;
203 fs += (m - t) * (m - t);
204 }
205 const double skew = std::sqrt(fs) / std::sqrt(fa + 1e-300);
206
207#ifdef PECLET_FLOW_OPERATOR_DOUBLE
208 const char* prec = "double";
209#else
210 const char* prec = "float";
211#endif
212 std::printf(
213 "%-8s %-8s ratio %-9.3g skew %.4e A*1 defect: abs %.3e /max|a| %.3e /min|a| %.3e\n",
214 prec, geomName(cfg.geom), cfg.ratio, skew, rowsumAbs, rowsumRelMax, rowsumRelMin);
215 std::fflush(stdout);
216
217 if (!outdir.empty()) {
218 char tag[256];
219 std::snprintf(tag, sizeof(tag), "%s/M_%s_%s_n%d_L%d_r%.0e.bin", outdir.c_str(), prec,
220 geomName(cfg.geom), cfg.n, cfg.levels, cfg.ratio);
221 writeMatrix(tag, M, n);
222 std::snprintf(tag, sizeof(tag), "%s/A_%s_%s_n%d_L%d_r%.0e.bin", outdir.c_str(), prec,
223 geomName(cfg.geom), cfg.n, cfg.levels, cfg.ratio);
224 writeMatrix(tag, A, n);
225 }
226}
227
228} // namespace
229
230int main(int argc, char** argv) {
231 Config cfg;
232 std::string outdir;
233 bool sweep = false;
234 for (int i = 1; i < argc; ++i) {
235 const std::string a = argv[i];
236 auto next = [&]() { return (i + 1 < argc) ? argv[++i] : (char*)"0"; };
237 if (a == "--n")
238 cfg.n = std::atoi(next());
239 else if (a == "--levels")
240 cfg.levels = std::atoi(next());
241 else if (a == "--geom")
242 cfg.geom = std::strcmp(next(), "periodic") == 0 ? 0 : 1;
243 else if (a == "--ratio")
244 cfg.ratio = std::atof(next());
245 else if (a == "--outdir")
246 outdir = next();
247 else if (a == "--sweep")
248 sweep = true;
249 }
250 Kokkos::initialize(argc, argv);
251 {
252 if (sweep) {
253 const double ratios[] = {1.0, 1e2, 1e3, 1e4, 1e5, 1e6};
254 for (int g = 0; g < 2; ++g)
255 for (double r : ratios) {
256 Config c = cfg;
257 c.geom = g;
258 c.ratio = r;
259 run(c, outdir);
260 }
261 } else {
262 run(cfg, outdir);
263 }
264 }
265 Kokkos::finalize();
266 return 0;
267}
int main()
Definition dbg_scene.cpp:13
flow — portable (Kokkos) geometric multigrid for the cut-cell (variable-openness) pressure Poisson.
x
Definition cmp.py:24
y
Definition cmp.py:24
M
Definition conj.py:180
Kokkos::View< double *, CCMem > CCField
Kokkos::View< const double *, CCMem > CCConst
static constexpr int G
static int run(int bc)