flow 1.3.0
Kokkos cut-cell IBM incompressible Navier-Stokes solver + pnm pore extraction
Loading...
Searching...
No Matches
test_scalar_cutcell_operator.cpp
Go to the documentation of this file.
1// Cut-cell scalar operator and solve (doc/scalar_ibm_design.md §1.4, §1.6, §4, §5.1; WO-3),
2// single rank. Structural properties of the assembled operator, independent of the accuracy gates
3// (tests/python/test_scalar_cutcell_gates.py):
4//
5// guard — ruling D-WO3-4: a face with a snapped aperture > 0 between an unknown and an
6// interior neighbour always has an unknown neighbour, so the band guard toward a
7// non-unknown cell is a no-op away from the domain boundary;
8// symmetry — the 7-point part is symmetric: AE(i) == AW(i + 1) (and N/S, T/B) BITWISE;
9// rows — identity rows are AC = 1, bands 0, rhs 0; steady Neumann rows sum to 0;
10// constants — Dirichlet walls at g = 1, steady: c == 1 (the probe weights sum to 1); transient,
11// insulating walls: a constant stays constant (§1.6.2);
12// singular — steady, insulating walls + flux + source: the incompatibility is reported, the
13// projected problem converges and the gauge keeps sum kappa V c (§5.1);
14// budget — transient and steady, with Robin walls, a source and Dirichlet domain faces: the
15// identity of §1.6.1 closes to round-off;
16// refusals — a scalar periodic face against a flow wall, and the reverse.
17#include <cmath>
18#include <cstdio>
19#include <Kokkos_Core.hpp>
20#include <stdexcept>
21#include <vector>
22
23#include "flow_ibm.hpp"
24
25namespace {
26int failures = 0;
27#define CHECK(cond) \
28 do { \
29 if (!(cond)) { \
30 std::fprintf(stderr, "CHECK failed: %s\n at %s:%d\n", #cond, __FILE__, __LINE__); \
31 ++failures; \
32 } \
33 } while (0)
34
36constexpr int G = IbmSolver::G;
37constexpr int N = 24;
38
39std::vector<double> sphereSdf(double cx, double cy, double cz, double R, bool solidInside) {
40 std::vector<double> s((std::size_t)N * N * N);
41 for (int z = 0; z < N; ++z)
42 for (int y = 0; y < N; ++y)
43 for (int x = 0; x < N; ++x) {
44 const double r = std::sqrt((x - cx) * (x - cx) + (y - cy) * (y - cy) + (z - cz) * (z - cz));
45 s[(std::size_t)x + (std::size_t)y * N + (std::size_t)z * N * N] =
46 solidInside ? r - R : R - r;
47 }
48 return s;
49}
50
51using HV = Kokkos::View<double*, Kokkos::HostSpace>;
52HV host(const peclet::flow::CCField& f) {
53 return Kokkos::create_mirror_view_and_copy(Kokkos::HostSpace(), f);
54}
55
56// A solver with the off-centre sphere; `walls` puts no-slip walls on +-x (else all periodic).
57void setup(IbmSolver& s, bool walls) {
58 s.setRho(1.0);
59 s.setMu(1.0);
60 s.setDt(0.8);
61 if (walls) {
62 s.setDomainBc(0, 1, 0, 0, 0);
63 s.setDomainBc(1, 1, 0, 0, 0);
64 }
65 s.setSolid(sphereSdf(11.3, 12.6, 11.9, 6.4, true), false);
66}
67
68void testStructure() {
69 IbmSolver s(N, N, N);
70 setup(s, false);
71 s.addScalar("c", 1.3, 1, 50, true);
72 s.solveScalarSteady("c"); // insulating walls, no source: b = 0, assembled, nothing to solve
73 const auto& gm = s.scalarCutGeometry();
74 auto unk = host(gm.unknown);
75 const HV sa[3] = {host(gm.sax), host(gm.say), host(gm.saz)};
76 auto& sc = s.scalarField("c");
77 auto AC = host(sc.AC), b = host(sc.b);
78 const HV lo[3] = {host(sc.AW), host(sc.AS), host(sc.AB)};
79 const HV hi[3] = {host(sc.AE), host(sc.AN), host(sc.AT)};
80 const int ex = N + 2 * G;
81 const long st[3] = {1, ex, (long)ex * ex};
82 long guarded = 0, asym = 0, badId = 0, badSum = 0, nUnk = 0;
83 double maxRow = 0.0;
84 for (int z = G; z < N + G; ++z)
85 for (int y = G; y < N + G; ++y)
86 for (int x = G; x < N + G; ++x) {
87 const long i = (long)x + (long)y * st[1] + (long)z * st[2];
88 if (!(unk(i) > 0.5)) {
89 bool id = AC(i) == 1.0 && b(i) == 0.0;
90 for (int a = 0; a < 3; ++a)
91 id = id && lo[a](i) == 0.0 && hi[a](i) == 0.0;
92 badId += id ? 0 : 1;
93 continue;
94 }
95 ++nUnk;
96 double row = AC(i);
97 for (int a = 0; a < 3; ++a) {
98 row += lo[a](i) + hi[a](i);
99 // the periodic box: every neighbour is an interior (or periodic-image) cell
100 if (sa[a](i) > 0.0 && !(unk(i - st[a]) > 0.5))
101 ++guarded;
102 if (sa[a](i + st[a]) > 0.0 && !(unk(i + st[a]) > 0.5))
103 ++guarded;
104 const long j = i + st[a];
105 const int c[3] = {x, y, z};
106 if (c[a] + 1 < N + G && unk(j) > 0.5 && hi[a](i) != lo[a](j))
107 ++asym;
108 }
109 maxRow = std::fmax(maxRow, std::fabs(row) / AC(i));
110 if (std::fabs(row) > 1e-14 * AC(i))
111 ++badSum;
112 }
113 std::printf(
114 "structure: %ld unknowns, guard fires on %ld interior faces, %ld asymmetric pairs, "
115 "%ld bad identity rows, max |row sum|/AC %.1e\n",
116 nUnk, guarded, asym, badId, maxRow);
117 CHECK(nUnk > 0);
118 CHECK(guarded == 0);
119 CHECK(asym == 0);
120 CHECK(badId == 0);
121 CHECK(badSum == 0);
122}
123
124double maxDevUnknown(IbmSolver& s, double v) {
125 auto unk = host(s.scalarCutGeometry().unknown);
126 auto c = host(s.scalarField("c").c);
127 const int ex = N + 2 * G;
128 double m = 0.0;
129 for (int z = G; z < N + G; ++z)
130 for (int y = G; y < N + G; ++y)
131 for (int x = G; x < N + G; ++x) {
132 const long i = (long)x + (long)y * ex + (long)z * ex * ex;
133 if (unk(i) > 0.5)
134 m = std::fmax(m, std::fabs(c(i) - v));
135 }
136 return m;
137}
138
139void testConstants() {
140 { // Dirichlet walls at g = 1, steady: A 1 = b exactly up to round-off
141 IbmSolver s(N, N, N);
142 setup(s, false);
143 s.addScalar("c", 0.9, 1, 50, true);
144 s.setScalarWall("c", 1, 1.0, 0.0, -1);
145 s.setScalarTolerance("c", 1e-12);
146 s.setScalarMaxIterations("c", 3000);
147 s.solveScalarSteady("c");
148 const double d = maxDevUnknown(s, 1.0);
149 std::printf("constants: steady Dirichlet g = 1 -> max|c - 1| = %.2e (%d iterations)\n", d,
150 s.scalarField("c").cut->iterations);
151 CHECK(d <= 1e-9);
152 CHECK(s.scalarField("c").cut->converged);
153 }
154 { // transient, insulating walls: c == 5 stays
155 IbmSolver s(N, N, N);
156 setup(s, false);
157 s.addScalar("c", 0.9, 1, 50, true);
158 std::vector<double> five((std::size_t)N * N * N, 5.0);
159 s.setField("c", five);
160 for (int k = 0; k < 3; ++k)
161 s.advanceScalars();
162 const double d = maxDevUnknown(s, 5.0);
163 std::printf("constants: transient insulating, c = 5 -> max|c - 5| = %.2e\n", d);
164 CHECK(d <= 1e-12);
165 }
166}
167
168void testSingular() {
169 IbmSolver s(N, N, N);
170 setup(s, false);
171 s.addScalar("c", 0.9, 1, 50, true);
172 s.setScalarWall("c", 0, 0.1, 0.0, -1); // a flux into the fluid
173 s.setScalarSource("c", 0.05); // and a source: incompatible in a periodic box
174 s.setScalarMaxIterations("c", 3000);
175 s.solveScalarSteady("c");
176 const auto& st = *s.scalarField("c").cut;
177 auto unk = host(s.scalarCutGeometry().unknown);
178 auto kap = host(s.scalarCutGeometry().kappa);
179 auto c = host(s.scalarField("c").c);
180 double kc = 0.0, kac = 0.0;
181 const int ex = N + 2 * G;
182 for (int z = G; z < N + G; ++z)
183 for (int y = G; y < N + G; ++y)
184 for (int x = G; x < N + G; ++x) {
185 const long i = (long)x + (long)y * ex + (long)z * ex * ex;
186 if (unk(i) > 0.5) {
187 kc += kap(i) * c(i);
188 kac += kap(i) * std::fabs(c(i));
189 }
190 }
191 std::printf(
192 "singular: incompatibility %.3e, %d iterations, converged %d, residual %.2e, "
193 "sum kappa c = %.2e (sum kappa |c| = %.2e)\n",
194 st.incompatibility, st.iterations, (int)st.converged, st.residual, kc, kac);
195 CHECK(st.singular);
196 CHECK(st.incompatibility > 0.1);
197 CHECK(st.converged);
198 CHECK(std::fabs(kc) <= 1e-10 * kac);
199}
200
201void testBudget() {
202 for (int steady = 0; steady < 2; ++steady) {
203 IbmSolver s(N, N, N);
204 setup(s, true);
205 s.addScalar("c", 1.1, 1, 50, true);
206 s.setScalarBc("c", 0, 2, 0.25);
207 s.setScalarBc("c", 1, 2, 1.5);
208 s.setScalarWall("c", 2, 2.0, 0.5, -1); // robin: k = 0.5, g = 2
209 s.setScalarSource("c", 0.3);
210 std::vector<double> init((std::size_t)N * N * N);
211 for (std::size_t k = 0; k < init.size(); ++k)
212 init[k] = 0.5 + 0.25 * std::sin(0.37 * (double)k);
213 s.setField("c", init);
214 if (steady)
215 s.solveScalarSteady("c");
216 else
217 for (int k = 0; k < 2; ++k)
218 s.advanceScalars();
219 const auto b = s.scalarBudget("c");
220 // ruling D-WO6-3 (the D-WO5c-1 convention): the identity relative to the GROSS budget, the
221 // sum of the terms' absolute values (the source is uniform: |source_in|), bound 1e-11 — it is
222 // reduction-order round-off and must not depend on the thread count. Cell units: the rates
223 // are internal, and a transient budget is in mass units (dt times the rates).
224 const double dtB = steady ? 1.0 : s.scalarField("c").cut->dt;
225 const double scale =
226 std::fabs(b.dMass) +
227 dtB * ((std::fabs(b.wallIn) + std::fabs(b.boundaryIn)) + std::fabs(b.sourceIn));
228 std::printf(
229 "budget (%s): d_mass %.6e wall %.6e boundary %.6e source %.6e defect %.2e "
230 "identity %.2e (rel %.1e)\n",
231 steady ? "steady" : "transient", b.dMass, b.wallIn, b.boundaryIn, b.sourceIn, b.defect,
232 b.identityError, std::fabs(b.identityError) / scale);
233 CHECK(std::fabs(b.identityError) <= 1e-11 * scale);
234 CHECK(std::fabs(b.wallIn) > 0.0 && std::fabs(b.boundaryIn) > 0.0 && b.sourceIn > 0.0);
235 // the per-body wall flux is the budget's wall term
236 const auto wf = s.scalarWallFlux("c");
237 CHECK(wf.size() == 1 && std::fabs(wf[0] - b.wallIn) <= 1e-13 * std::fabs(b.wallIn));
238 }
239}
240
241void testRefusals() {
242 { // scalar Dirichlet face against a periodic flow face
243 IbmSolver s(N, N, N);
244 setup(s, false);
245 s.addScalar("c", 1.0, 1, 50, true);
246 s.setScalarBc("c", 2, 2, 1.0);
247 bool threw = false;
248 try {
249 s.advanceScalars();
250 } catch (const std::runtime_error&) {
251 threw = true;
252 }
253 CHECK(threw);
254 }
255 { // flow wall on -x, scalar left periodic
256 IbmSolver s(N, N, N);
257 setup(s, true);
258 s.addScalar("c", 1.0, 1, 50, true);
259 bool threw = false;
260 try {
261 s.solveScalarSteady("c");
262 } catch (const std::runtime_error&) {
263 threw = true;
264 }
265 CHECK(threw);
266 }
267}
268} // namespace
269
270int main(int argc, char** argv) {
271 Kokkos::initialize(argc, argv);
272 {
273 testStructure();
274 testConstants();
275 testSingular();
276 testBudget();
277 testRefusals();
278 }
279 Kokkos::finalize();
280 std::printf(failures ? "%d failure(s)\n" : "OK\n", failures);
281 return failures ? 1 : 0;
282}
static constexpr int G
Definition flow_ibm.hpp:80
int main()
Definition dbg_scene.cpp:13
flow — host-facing Kokkos IBM Navier-Stokes solver (drop-in flow-style API).
x
Definition cmp.py:24
y
Definition cmp.py:24
Kokkos::View< double *, CCMem > CCField
Solver< Staggered > IbmSolver
steady(s, probe, maxit=3000, tol=1e-9)
static constexpr int N
static std::vector< double > sphereSdf()
static constexpr int G
static constexpr double AC
#define CHECK(cond)