peclet-amr 0.2.0
Block-local-Morton AMR octree + collocated cut-cell Navier–Stokes solver (header-only C++20, Kokkos + MPI)
Loading...
Searching...
No Matches
cf_scheme.hpp
Go to the documentation of this file.
1// core — pluggable coarse/fine (2:1) interface schemes for the collocated AMR flow solver.
2//
3// The graded-accuracy limiter measured in docs/amr_collocated_projection.md (addendum): the
4// standard two-point C/F flux is 1st-order at level boundaries, and BOTH the aperture and the
5// ghost projection inherit it identically (+9% graded drag offset on the dilute sphere,
6// scheme-independent). This header makes the C/F treatment a pluggable SCHEME and provides the
7// Martin–Cartwright/Martin–Colella tangential-quadratic closure (CfScheme::quadratic — the same
8// coarseStar substitution AmrPoisson::applyLaplacianQuad uses) for every operator the STEADY
9// solution feels:
10//
11// D — the ½/½ face-average divergence constraint (buildCfDivDelta, vector → scalar)
12// G — the ABC cell gradient (−∇pⁿ predictor + cell correction) (buildCfGradDelta, scalar → 3)
13// ∇² — the momentum (velocity) diffusion at regular fluid rows (buildCfLapDelta, scalar →
14// scalar)
15//
16// Everything is the LINEAR substitution "coarse-side value → coarse*", where coarse* is the
17// scheme's interpolation of the coarse cell at the fine cell's tangential position. So each
18// operator's (quad − standard) difference is a precomputed CSR overlay over the level-boundary
19// rows: built ONCE on the host (shared verbatim by the host oracle and the device — parity by
20// construction), applied as one extra SpMV.
21//
22// STEADY-STATE PLACEMENT (the (1,2)-mixed philosophy, flow-validated): at the projection's fixed
23// point φ→0, so the pressure MATRIX C/F order does not move the steady solution — the matrix (and
24// the whole MG hierarchy / PCG / ghost BiCGStab) stays on the standard consistent operator, and
25// the scheme enters through (a) the RHS divergence, (b) the pressure gradients, (c) the momentum
26// operator (as a lagged deferred-correction RHS term, the same pattern as the implicit-FOU/SOU
27// split and Multigrid::solveQuad). The per-step operator mismatch converges through the
28// pseudo-transient stepping; the steady equations carry the 2nd-order C/F closure exactly.
29//
30// EXTENDING: add a value to CfScheme and a branch in cfAppendStencil — every operator delta picks
31// it up. A scheme is fully described by the linear stencil it substitutes for the coarse-side
32// value of one directed C/F sub-face.
33//
34// GATING — one predicate, per FACE, never per row (docs/amr_cf_flux_gate.md). The quadratic face
35// value applies on a 2:1 sub-face IFF BOTH incident cells are REGULAR FLUID:
36//
37// cfFace(i, j) := level(j) != level(i) && regular(i) && regular(j), regular = fluid && !cut
38//
39// and that ONE predicate drives D, the G substitution and uf alike, through the one emitter
40// cfAppendFaceValueDelta, so that D_std(Δuf) ≡ Δ_cfDiv holds entry by entry on every mesh. A face
41// flux is a single number shared by two cells, so a gate that is a property of a CELL cannot be
42// conservative: the old row gate let the regular cell book the correction in its constraint while
43// the cut cell did not and uf carried it for both — a mass source proportional to the velocity,
44// not to φ, so it did not decay at steady state. Withholding reverts the face to the standard
45// two-point value (the cf=0 scheme), which is an O(h) face value on the set where a level
46// boundary meets the wall — a curve, i.e. codimension 2, on the production graded meshes.
47// `AmrFlow::numCfCutFaces()` counts that set, so the cost is measured rather than silent.
48//
49// The per-tangential-axis robustness fallbacks INSIDE the stencil are unchanged and independent
50// of the above: the tangential coarse neighbours must exist, be same-level, be FLUID (the
51// caller's `fluidOk` — never lean on a decoupled/held solid value), and the tangential faces must
52// be sufficiently open (openness ≥ 0.5, matching applyLaplacianQuad's gate); one surviving side
53// gives the linear closure, neither gives the raw coarse value. Reading a CUT cell as a
54// tangential sample is fine — its velocity is a solved fluid value; the instability the row gate
55// was added for is about which ROW owns a constraint, not which cells it reads.
56#ifndef PECLET_AMR_CF_SCHEME_HPP
57#define PECLET_AMR_CF_SCHEME_HPP
58
59#include <array>
60#include <cstdint>
61#include <utility>
62#include <vector>
63
65#include "peclet/amr/common.hpp"
67#include "peclet/core/common/host_parallel.hpp"
68#include "peclet/core/common/types.hpp"
69
70namespace peclet::amr {
71
73enum class CfScheme : int {
74 standard = 0,
75 quadratic = 1,
76};
77
79struct CfCsr {
80 std::vector<Index> start;
81 std::vector<Index> slot;
82 std::vector<double> coef;
83};
84
86struct CfCompCsr {
87 std::vector<Index> start;
88 std::vector<Index> slot;
89 std::vector<double> coef;
90 std::vector<int8_t> comp;
91};
92
93namespace detail {
94
100template <unsigned Bits, class Ent, class FluidFn>
101inline void cfAppendStencil(const AmrPoisson<3, Bits>& ap, std::vector<Ent>& out, Index coarse,
102 Index fine, int axis, double scale, FluidFn&& fluidOk, CfScheme scheme,
103 const Ent& proto = Ent{}) {
105 return;
106 // DISTRIBUTED (2026-09-21): every geometric query here goes through `ap`, never through the
107 // block octree. `coarse`/`fine` may be GHOST slots (loOf/levelOf serve those transparently) and
108 // the tangential probes may leave the block (probeSlot routes them through the LeafHalo
109 // resolver). Single-rank both spellings are the same lookup on the same coordinate — block ==
110 // domain and probeSlot's periodic wrap is the one this function used to do by hand — so the
111 // built CSRs are bit-identical there.
112 const std::array<long, 3> bc = ap.loOf(coarse);
113 const std::array<long, 3> bf = ap.loOf(fine);
114 const unsigned Lc = ap.levelOf(coarse);
115 const double sc = static_cast<double>(Index(1) << Lc);
116 const double sf = static_cast<double>(Index(1) << ap.levelOf(fine));
117 auto push = [&](Index cell, double w) {
118 Ent e = proto;
119 e.cell = cell;
120 e.w = w;
121 out.push_back(e);
122 };
123 for (int tt = 0; tt < 3; ++tt) {
124 if (tt == axis)
125 continue;
126 // Phase 3: BOTH the tangential offset and the differencing width belong to the TANGENTIAL
127 // axis `tt` — they were one `H` when the cells were cubes (`docs/amr_anisotropic.md` §3).
128 const double H = ap.cellWidth(coarse, tt);
129 // Phase 3: the tangential offset is a length along axis `tt` (`docs/amr_anisotropic.md` §3).
130 const double dt =
131 ((static_cast<double>(bf[tt]) + 0.5 * sf) - (static_cast<double>(bc[tt]) + 0.5 * sc)) *
132 ap.h0()[tt];
133 // Tangential samples at the coarse cell's ± neighbours. Same-level neighbours contribute
134 // directly. A FINER neighbour (an island corner/edge: the region across the tangential face
135 // is refined — by 2:1 exactly one level finer) is sampled by the volume average of the 2^Dim
136 // leaves covering the coarse-size region: a 2nd-order sample of the region-center value, so
137 // the corner rows keep the quadratic closure instead of falling back to the raw coarse value
138 // (the old P5b-style skip, which left them locally 1st-order). A COARSER neighbour (or any
139 // non-fluid / irregular cover) still falls back to the skip (robust).
140 struct Samp {
141 Index cell[1 << 3];
142 double w[1 << 3];
143 int n = 0;
144 bool ok = false;
145 };
146 auto sampleTangential = [&](int dir) {
147 Samp sm;
148 const Index nb = ap.periodicNeighbor(coarse, tt, dir);
149 if (nb < 0)
150 return sm;
151 const unsigned Lnb = ap.levelOf(nb);
152 if (Lnb == Lc) {
153 if (!fluidOk(nb))
154 return sm;
155 sm.cell[0] = nb;
156 sm.w[0] = 1.0;
157 sm.n = 1;
158 sm.ok = true;
159 return sm;
160 }
161 if (Lnb + 1 != Lc)
162 return sm; // coarser neighbour: keep the fallback
163 // Finer neighbour: enumerate the 2^Dim children covering the coarse-size region. The child
164 // probes go through ap.probeSlot — NOT a hand-wrapped BlockOctree::find, which is what this
165 // did until 2026-09-21. probeSlot wraps in-block coords itself and hands out-of-block ones
166 // to the LeafHalo resolver, so a child owned by another rank resolves to its ghost slot.
167 // The hand wrap used the BLOCK period, which is the domain period only single-rank;
168 // multi-rank it folded a neighbour rank's child back into this block and `find` returned a
169 // real but unrelated leaf — either the guard below rejected it and the tangential side
170 // dropped silently to the raw coarse value, or it passed and the stencil read the wrong
171 // cell. Measured cost of that, np = 2 on tests/test_amr_distributed_cf_mpi: 131 % error.
172 const long scL = 1L << Lc, shL = scL >> 1;
173 std::array<long, 3> lo = bc;
174 lo[tt] += (dir > 0) ? scL : -scL;
175 for (int oct2 = 0; oct2 < (1 << 3); ++oct2) {
176 std::array<long, 3> q = lo;
177 for (int d = 0; d < 3; ++d)
178 if ((oct2 >> d) & 1)
179 q[d] += shL;
180 const auto [ch, Lch] = ap.probeSlot(q);
181 if (ch < 0 || Lch + 1 != Lc || !fluidOk(ch))
182 return Samp{}; // irregular cover or solid child: fall back
183 sm.cell[sm.n] = ch;
184 sm.w[sm.n] = 1.0 / static_cast<double>(1 << 3);
185 ++sm.n;
186 }
187 sm.ok = true;
188 return sm;
189 };
190 const Samp sp = sampleTangential(+1);
191 const Samp smi = sampleTangential(-1);
192 const bool okP = sp.ok && ap.faceOpenness(coarse, tt, +1) >= 0.5;
193 const bool okM = smi.ok && ap.faceOpenness(coarse, tt, -1) >= 0.5;
194 if (okP && okM) {
195 // Two-sided (the original path, bit-identical): tangential quadratic.
196 const double cUp = dt / (2.0 * H) + 0.5 * dt * dt / (H * H);
197 const double cUm = -dt / (2.0 * H) + 0.5 * dt * dt / (H * H);
198 const double cUc = -dt * dt / (H * H);
199 push(coarse, scale * cUc);
200 for (int k2 = 0; k2 < sp.n; ++k2)
201 push(sp.cell[k2], scale * cUp * sp.w[k2]);
202 for (int k2 = 0; k2 < smi.n; ++k2)
203 push(smi.cell[k2], scale * cUm * smi.w[k2]);
204 continue;
205 }
206 // Wall-aware one-sided fallback (mixed-level cut-band plan §6.1): when exactly one
207 // tangential side survives the gates (the other is solid / closed / irregular — the
208 // near-wall seam case), substitute the one-sided LINEAR interpolation dt·u' instead of
209 // falling all the way back to the raw coarse value: O(H²) tangential sample error vs the
210 // raw fallback's O(H) offset. Both sides gated ⇒ raw fallback (unchanged).
211 if (okP != okM) {
212 const Samp& s1 = okP ? sp : smi;
213 const double sgn = okP ? 1.0 : -1.0;
214 push(coarse, -scale * sgn * dt / H);
215 for (int k2 = 0; k2 < s1.n; ++k2)
216 push(s1.cell[k2], scale * sgn * (dt / H) * s1.w[k2]);
217 }
218 }
219}
220
221struct ScalarEnt {
223 double w = 0.0;
224};
225struct CompEnt {
227 double w = 0.0;
229};
230
268template <unsigned Bits, class Ent, class FluidFn>
269inline void cfAppendFaceValueDelta(const AmrPoisson<3, Bits>& ap, std::vector<Ent>& out,
270 Index coarse, Index fine, int axis, double dist, double scale,
271 FluidFn&& fluidOk, CfScheme scheme, const Ent& proto = Ent{}) {
272 const double H = ap.cellWidth(coarse, axis), h = ap.cellWidth(fine, axis);
273 const double wF = (0.5 * H) / dist, wC = (0.5 * h) / dist;
274 Ent eF = proto, eC = proto;
275 eF.cell = fine;
276 eF.w = scale * (wF - 0.5);
277 eC.cell = coarse;
278 eC.w = scale * (wC - 0.5);
279 out.push_back(eF);
280 out.push_back(eC);
282}
283
284template <class Ent, class Csr>
285inline void compactCsr(const std::vector<std::vector<Ent>>& per, Csr& out, Index n) {
286 out.start.assign(static_cast<std::size_t>(n) + 1, 0);
287 for (Index i = 0; i < n; ++i)
288 out.start[static_cast<std::size_t>(i) + 1] =
289 out.start[static_cast<std::size_t>(i)] +
290 static_cast<Index>(per[static_cast<std::size_t>(i)].size());
291 const Index nz = out.start[static_cast<std::size_t>(n)];
292 out.slot.resize(static_cast<std::size_t>(nz));
293 out.coef.resize(static_cast<std::size_t>(nz));
294 Index k = 0;
295 for (Index i = 0; i < n; ++i)
296 for (const auto& e : per[static_cast<std::size_t>(i)]) {
297 out.slot[static_cast<std::size_t>(k)] = e.cell;
298 out.coef[static_cast<std::size_t>(k)] = e.w;
299 ++k;
300 }
301}
302
303} // namespace detail
304
310template <unsigned Bits, class RowFn, class FluidFn>
312 double factor, RowFn&& rowOk, FluidFn&& fluidOk, CfScheme scheme) {
313 const Index n = t.numLeaves();
314 std::vector<std::vector<detail::ScalarEnt>> per(static_cast<std::size_t>(n));
315 // Host-parallel (setup-parallel plan rung 1): row i writes ONLY per[i] and reads only the frozen
316 // octree/openness — disjoint writes, so the staged rows are bitwise identical under any schedule
317 // and compactCsr below still concatenates them in leaf order.
318 hostParFor(n, [&](Index i) {
319 if (!rowOk(i))
320 return;
321 const unsigned Li = t.level(i);
322 const double invV = 1.0 / ap.cellVolume(i);
323 ap.forEachFaceNeighbor(i, [&](Index j, Real c, int axis, double a) {
324 const unsigned Lj = ap.levelOf(j); // ghost-safe (j may be a ghost slot)
325 if (Lj == Li)
326 return;
327 const Index coarse = (Lj > Li) ? j : i;
328 const Index fine = (Lj > Li) ? i : j;
329 const double scale = factor * invV * (a * c) * ((Lj > Li) ? 1.0 : -1.0);
330 detail::cfAppendStencil(ap, per[static_cast<std::size_t>(i)], coarse, fine, axis, scale,
331 fluidOk, scheme);
332 });
333 });
334 CfCsr csr;
336 return csr;
337}
338
349template <unsigned Bits, class RegularFn, class FluidFn>
352 const Index n = t.numLeaves();
353 std::vector<std::vector<detail::CompEnt>> per(static_cast<std::size_t>(n));
354 if (scheme != CfScheme::standard) {
355 hostParFor(n, [&](Index i) { // per[i]-disjoint (rung 1)
356 if (!regularOk(i))
357 return;
358 const unsigned Li = t.level(i);
359 const double invV = 1.0 / ap.cellVolume(i);
361 i, [&](Index j, int axis, int dir, double area, double dist, double alpha) {
362 const unsigned Lj = ap.levelOf(j); // ghost-safe
363 // cfFace(i, j): regularOk(i) holds by the early-out above.
364 if (Lj == Li || !regularOk(j))
365 return;
366 const Index coarse = (Lj > Li) ? j : i;
367 const Index fine = (Lj > Li) ? i : j;
368 const double scale = invV * alpha * area * static_cast<double>(dir);
370 proto.comp = static_cast<int8_t>(axis);
371 detail::cfAppendFaceValueDelta(ap, per[static_cast<std::size_t>(i)], coarse, fine, axis,
372 dist, scale, fluidOk, scheme, proto);
373 });
374 });
375 }
378 csr.comp.resize(csr.slot.size());
379 Index k = 0;
380 for (Index i = 0; i < n; ++i)
381 for (const auto& e : per[static_cast<std::size_t>(i)])
382 csr.comp[static_cast<std::size_t>(k++)] = e.comp;
383 return csr;
384}
385
396template <unsigned Bits, class RegularFn, class FluidFn>
397inline std::array<CfCsr, 3> buildCfGradDelta(const AmrPoisson<3, Bits>& ap,
400 const Index n = t.numLeaves();
401 std::array<std::vector<std::vector<detail::ScalarEnt>>, 3> per;
402 for (int a = 0; a < 3; ++a)
403 per[static_cast<std::size_t>(a)].resize(static_cast<std::size_t>(n));
404 if (scheme != CfScheme::standard) {
405 hostParFor(n, [&](Index i) { // per[axis][i]-disjoint (rung 1)
406 if (!regularOk(i))
407 return;
408 const unsigned Li = t.level(i);
409 // Per axis/side: open-face count, the side's (uniform) center-to-center distance, and
410 // whether the side has a level mismatch. slot [axis][side], side 0 = +, 1 = −.
411 int cnt[3][2] = {};
412 double sdist[3][2] = {};
413 bool cf[3][2] = {};
414 ap.forEachFaceFull(i, [&](Index j, int axis, int dir, double, double dist, double alpha) {
415 if (alpha <= 1e-12)
416 return;
417 const int s = (dir > 0) ? 0 : 1;
418 ++cnt[axis][s];
419 sdist[axis][s] = dist;
420 // D and G must be paired FACE BY FACE (docs/amr_cf_flux_gate.md §6.3, §8 point 3): a
421 // face whose value D takes as standard must be one whose gradient G takes as standard, or
422 // the constraint gains a component the gradient cannot see. So the side reweighting is
423 // triggered by a PASSING C/F face, not by any level mismatch.
424 if (ap.levelOf(j) != Li && regularOk(j)) // cfFace(i, j); regularOk(i) by the early-out
425 cf[axis][s] = true;
426 });
427 for (int a = 0; a < 3; ++a) {
428 if (!cf[a][0] && !cf[a][1])
429 continue; // no level mismatch on this axis: standard ½/½ is centered
430 if (cnt[a][0] == 0 || cnt[a][1] == 0)
431 continue; // one-sided (closed) axis: leave the standard treatment
432 const double dp = sdist[a][0], dm = sdist[a][1];
433 const double wp = dm / (dp + dm), wm = dp / (dp + dm);
434 auto& row = per[static_cast<std::size_t>(a)][static_cast<std::size_t>(i)];
435 ap.forEachFaceFull(i, [&](Index j, int axis, int dir, double, double dist, double alpha) {
436 if (axis != a || alpha <= 1e-12)
437 return;
438 const int s = (dir > 0) ? 0 : 1;
439 const double w = (s == 0) ? wp : wm;
440 const double inv = 1.0 / static_cast<double>(cnt[a][s]);
441 const double gsgn = (dir > 0) ? 1.0 : -1.0; // coefficient of f_j in g
442 // (a) side reweighting of the STANDARD face gradient: (w − ½)·g/cnt.
443 const double rw = (w - 0.5) * inv * gsgn / dist;
444 if (rw != 0.0) {
445 row.push_back({j, rw});
446 row.push_back({i, -rw});
447 }
448 // (b) coarse* substitution inside the C/F face gradients, at the NEW side weight.
449 const unsigned Lj = ap.levelOf(j); // ghost-safe
450 if (Lj == Li || !regularOk(j)) // cfFace(i, j)
451 return;
452 const Index coarse = (Lj > Li) ? j : i;
453 const Index fine = (Lj > Li) ? i : j;
454 const double ssgn = (Lj > Li) ? gsgn : -gsgn; // sign of the substituted value in g
455 detail::cfAppendStencil(ap, row, coarse, fine, a, w * inv * ssgn / dist, fluidOk, scheme);
456 });
457 }
458 });
459 }
460 std::array<CfCsr, 3> out;
461 for (int a = 0; a < 3; ++a)
462 detail::compactCsr(per[static_cast<std::size_t>(a)], out[static_cast<std::size_t>(a)], n);
463 return out;
464}
465
490
491template <unsigned Bits, class RegularFn, class FluidFn>
494 const Index n = t.numLeaves();
495 // Rung 1, two-pass. forEachFaceFull is CELL-MAJOR, so leaf i owns the contiguous slot range
496 // [slotBase[i], slotBase[i+1]): count the slots per leaf in parallel, then an INTEGER exclusive
497 // scan (exact under any combination order) reproduces today's serial slot numbering exactly.
498 std::vector<Index> slotCnt(static_cast<std::size_t>(n), 0);
499 hostParFor(n, [&](Index i) {
500 Index c = 0;
501 ap.forEachFaceFull(i, [&](Index, int, int, double, double, double) { ++c; });
502 slotCnt[static_cast<std::size_t>(i)] = c;
503 });
504 std::vector<Index> slotBase(static_cast<std::size_t>(n) + 1, 0);
505 hostParScan(n, [&](Index i, Index& acc, bool final) {
506 if (final)
507 slotBase[static_cast<std::size_t>(i)] = acc;
508 acc += slotCnt[static_cast<std::size_t>(i)];
509 });
510 const Index nSlots =
511 (n > 0) ? slotBase[static_cast<std::size_t>(n) - 1] + slotCnt[static_cast<std::size_t>(n) - 1]
512 : 0;
513 slotBase[static_cast<std::size_t>(n)] = nSlots;
514 CfUfDelta d;
515 d.vel.start.assign(static_cast<std::size_t>(nSlots) + 1, 0);
517 return d;
518 // Pass 1 (parallel): each leaf stages its own entries in its own slot order, and writes its
519 // slots' ROW WIDTHS into start[slot+1] (disjoint: one leaf owns each slot). The prefix sum is
520 // taken serially below, exactly as the single-pass version accumulated it.
521 std::vector<std::vector<detail::CompEnt>> velPer(static_cast<std::size_t>(n));
522 hostParFor(n, [&](Index i) {
523 const unsigned Li = t.level(i);
524 Index slot = slotBase[static_cast<std::size_t>(i)];
525 auto& vrow = velPer[static_cast<std::size_t>(i)];
526 ap.forEachFaceFull(i, [&](Index j, int axis, int, double, double dist, double) {
527 const unsigned Lj = ap.levelOf(j); // ghost-safe
528 if (Lj != Li && regularOk(i) && regularOk(j)) { // cfFace(i, j); regular ⇒ fluid
529 const Index coarse = (Lj > Li) ? j : i;
530 const Index fine = (Lj > Li) ? i : j;
531 std::vector<detail::CompEnt> ve;
533 proto.comp = static_cast<int8_t>(axis);
535 proto);
536 for (const auto& e : ve)
537 vrow.push_back(e);
538 d.vel.start[static_cast<std::size_t>(slot) + 1] = static_cast<Index>(ve.size());
539 }
540 ++slot;
541 });
542 });
543 // Pass 2 (serial, order-preserving): widths → offsets, then concatenate leaf-major. Identical to
544 // the single pass's append order (leaf-major, slot-major within a leaf, entry order within slot).
545 for (Index s = 0; s < nSlots; ++s)
546 d.vel.start[static_cast<std::size_t>(s) + 1] += d.vel.start[static_cast<std::size_t>(s)];
547 d.vel.slot.reserve(static_cast<std::size_t>(d.vel.start[static_cast<std::size_t>(nSlots)]));
548 d.vel.coef.reserve(static_cast<std::size_t>(d.vel.start[static_cast<std::size_t>(nSlots)]));
549 d.vel.comp.reserve(static_cast<std::size_t>(d.vel.start[static_cast<std::size_t>(nSlots)]));
550 for (Index i = 0; i < n; ++i)
551 for (const auto& e : velPer[static_cast<std::size_t>(i)]) {
552 d.vel.slot.push_back(e.cell);
553 d.vel.coef.push_back(e.w);
554 d.vel.comp.push_back(e.comp);
555 }
556 return d;
557}
558
569template <unsigned Bits, class RegularFn, class FluidFn>
572 const Index n = t.numLeaves();
573 // Per-leaf counts then a serial sum: disjoint writes (rung 1), and the total is an exact
574 // integer under any schedule. One face traversal, the same shape as the CSR builders beside it.
575 std::vector<Index> per(static_cast<std::size_t>(n), 0);
576 hostParFor(n, [&](Index i) {
577 if (!fluidOk(i))
578 return;
579 const unsigned Li = t.level(i);
580 Index c = 0;
581 ap.forEachFaceFull(i, [&](Index j, int, int, double, double, double) {
582 if (ap.levelOf(j) == Li || !fluidOk(j))
583 return;
584 if (regularOk(i) && regularOk(j))
585 return;
586 ++c;
587 });
588 per[static_cast<std::size_t>(i)] = c;
589 });
590 Index cnt = 0;
591 for (Index i = 0; i < n; ++i)
592 cnt += per[static_cast<std::size_t>(i)];
593 return cnt;
594}
595
596// ---- host applies (the oracle path; the device uses the same CSRs uploaded + SpMV kernels) ----
597
599inline void cfApplyHost(const CfCsr& c, const std::vector<double>& f, std::vector<double>& out) {
600 const Index n = static_cast<Index>(c.start.size()) - 1;
601 for (Index i = 0; i < n; ++i) {
602 double acc = 0.0;
603 for (Index k = c.start[static_cast<std::size_t>(i)];
604 k < c.start[static_cast<std::size_t>(i) + 1]; ++k)
605 acc += c.coef[static_cast<std::size_t>(k)] *
606 f[static_cast<std::size_t>(c.slot[static_cast<std::size_t>(k)])];
607 out[static_cast<std::size_t>(i)] += acc;
608 }
609}
610
612inline void cfApplyCompHost(const CfCompCsr& c, const std::array<std::vector<double>, 3>& u,
613 std::vector<double>& out) {
614 const Index n = static_cast<Index>(c.start.size()) - 1;
615 for (Index i = 0; i < n; ++i) {
616 double acc = 0.0;
617 for (Index k = c.start[static_cast<std::size_t>(i)];
618 k < c.start[static_cast<std::size_t>(i) + 1]; ++k)
619 acc += c.coef[static_cast<std::size_t>(k)] *
620 u[static_cast<std::size_t>(c.comp[static_cast<std::size_t>(k)])]
621 [static_cast<std::size_t>(c.slot[static_cast<std::size_t>(k)])];
622 out[static_cast<std::size_t>(i)] += acc;
623 }
624}
625
626// ---- device mirrors (Kokkos TUs only; include after a Kokkos-carrying header) ------------------
627#ifdef KOKKOS_INLINE_FUNCTION
628
641
642inline CfCsrDev uploadCfCsr(const CfCsr& h, const char* name) {
643 CfCsrDev d;
644 d.n = static_cast<Index>(h.start.size()) - 1;
645 d.nz = static_cast<Index>(h.slot.size());
646 if (d.nz == 0) {
647 d.n = 0;
648 return d;
649 }
650 d.start = toDevice(h.start, name);
651 d.slot = toDevice(h.slot, name);
652 d.coef = toDevice(h.coef, name);
653 return d;
654}
655inline CfCompCsrDev uploadCfCompCsr(const CfCompCsr& h, const char* name) {
656 CfCompCsrDev d;
657 d.n = static_cast<Index>(h.start.size()) - 1;
658 d.nz = static_cast<Index>(h.slot.size());
659 if (d.nz == 0) {
660 d.n = 0;
661 return d;
662 }
663 d.start = toDevice(h.start, name);
664 d.slot = toDevice(h.slot, name);
665 d.coef = toDevice(h.coef, name);
666 d.comp = toDevice(h.comp, name);
667 return d;
668}
669
672 if (c.n == 0)
673 return;
674 auto st = c.start;
675 auto sl = c.slot;
676 auto w = c.coef;
677 Kokkos::parallel_for(
678 "amr::cf_apply", c.n, KOKKOS_LAMBDA(const Index i) {
679 double acc = 0.0;
680 for (Index k = st(i); k < st(i + 1); ++k)
681 acc += w(k) * f(sl(k));
682 out(i) += acc;
683 });
684}
685
689 if (c.n == 0)
690 return;
691 auto st = c.start;
692 auto sl = c.slot;
693 auto w = c.coef;
694 auto cp = c.comp;
695 Kokkos::parallel_for(
696 "amr::cf_apply_comp", c.n, KOKKOS_LAMBDA(const Index i) {
697 double acc = 0.0;
698 for (Index k = st(i); k < st(i + 1); ++k) {
699 const int a = cp(k);
700 const double v = (a == 0) ? u0(sl(k)) : (a == 1) ? u1(sl(k)) : u2(sl(k));
701 acc += w(k) * v;
702 }
703 out(i) += acc;
704 });
705}
706
707#endif // KOKKOS_INLINE_FUNCTION
708
709} // namespace peclet::amr
710
711#endif // PECLET_AMR_CF_SCHEME_HPP
Cell-centered FV Poisson operator on one (periodic) block octree.
Definition poisson.hpp:43
unsigned levelOf(Index slot) const
Octree level of an extended slot (local leaf or declared ghost).
Definition poisson.hpp:98
Real cellWidth(Index i, int axis) const
Cell width along axis (Phase 3: per axis).
Definition poisson.hpp:255
void forEachFaceFull(Index i, Fn &&fn) const
Like forEachFaceNeighbor but exposes geometry for a consistent FV divergence/gradient: fn(neighbour,...
Definition poisson.hpp:329
const Vec< Dim > & h0() const
Definition poisson.hpp:587
void forEachFaceNeighbor(Index i, Fn &&fn) const
Visit each face neighbour of leaf i: fn(neighbourSlot, coeff, axis, alpha) where coeff = A_f / d_f (p...
Definition poisson.hpp:274
std::pair< Index, unsigned > probeSlot(const std::array< long, Dim > &p) const
Covering slot + level of a block-local probe (longs, possibly outside the block).
Definition poisson.hpp:120
double faceOpenness(Index i, int axis, int dir) const
Openness of leaf i's face on (axis,dir); 1 if no openness has been set.
Definition poisson.hpp:150
Index periodicNeighbor(Index i, int axis, int dir) const
Periodic face neighbour leaf (covering the cell just across the face).
Definition poisson.hpp:375
std::array< long, Dim > loOf(Index slot) const
Block-local lo corner of an extended slot as longs (ghosts may lie outside the block).
Definition poisson.hpp:103
Real cellVolume(Index i) const
Definition poisson.hpp:262
Per-block adaptive octree over block-local Morton codes.
void cfAppendStencil(const AmrPoisson< 3, Bits > &ap, std::vector< Ent > &out, Index coarse, Index fine, int axis, double scale, FluidFn &&fluidOk, CfScheme scheme, const Ent &proto=Ent{})
Append the scheme's (coarse* − coarse_raw) substitution stencil for ONE directed C/F sub-face to out,...
void cfAppendFaceValueDelta(const AmrPoisson< 3, Bits > &ap, std::vector< Ent > &out, Index coarse, Index fine, int axis, double dist, double scale, FluidFn &&fluidOk, CfScheme scheme, const Ent &proto=Ent{})
THE face-value delta for ONE directed 2:1 sub-face, ×scale — the single source of truth shared by the...
void compactCsr(const std::vector< std::vector< Ent > > &per, Csr &out, Index n)
Index countCfCutFaces(const AmrPoisson< 3, Bits > &ap, const BlockOctree< 3, Bits > &t, RegularFn &&regularOk, FluidFn &&fluidOk)
The C/F CENSUS (docs/amr_cf_flux_gate.md §6.5): how many OWNED forEachFaceFull slots (i,...
CfCsrDev uploadCfCsr(const CfCsr &h, const char *name)
CfCompCsr buildCfDivDelta(const AmrPoisson< 3, Bits > &ap, const BlockOctree< 3, Bits > &t, RegularFn &&regularOk, FluidFn &&fluidOk, CfScheme scheme)
(D_scheme − D_std) for the face-average divergence div_i = invV·Σ α·A·dir·(face value).
void cfApply(const CfCsrDev &c, View< const double > f, View< double > out)
out(i) += Σ coef·f(slot).
CfCompCsrDev uploadCfCompCsr(const CfCompCsr &h, const char *name)
CfUfDelta buildCfUfDelta(const AmrPoisson< 3, Bits > &ap, const BlockOctree< 3, Bits > &t, RegularFn &&regularOk, FluidFn &&fluidOk, CfScheme scheme)
void cfApplyCompHost(const CfCompCsr &c, const std::array< std::vector< double >, 3 > &u, std::vector< double > &out)
out(i) += Σ coef·u[comp](slot) (the divergence overlay).
CfCsr buildCfLapDelta(const AmrPoisson< 3, Bits > &ap, const BlockOctree< 3, Bits > &t, double factor, RowFn &&rowOk, FluidFn &&fluidOk, CfScheme scheme)
(∇²_scheme − ∇²_std) as a scalar CSR, ×factor (pass μ for the momentum deferred-correction RHS,...
void cfApplyHost(const CfCsr &c, const std::vector< double > &f, std::vector< double > &out)
out(i) += Σ coef·f(slot) (the scalar overlay: momentum ∇² delta, gradient delta per axis).
std::array< CfCsr, 3 > buildCfGradDelta(const AmrPoisson< 3, Bits > &ap, const BlockOctree< 3, Bits > &t, RegularFn &&regularOk, FluidFn &&fluidOk, CfScheme scheme)
(G_scheme − G_std) for the ABC cell gradient gradOf/grad3.
std::vector< std::array< double, Dim > > transferFieldGradients(const BlockOctree< Dim, Bits > &oldT, const std::vector< double > &oldF)
Per-old-leaf minmod prolongation gradients (per fine-coordinate unit) — transferField's stencil,...
Definition adapt.hpp:51
void cfApplyComp(const CfCompCsrDev &c, View< const double > u0, View< const double > u1, View< const double > u2, View< double > out)
out(i) += Σ coef·u[comp](slot).
CfScheme
Coarse/fine interface scheme for the collocated flow operators.
Definition cf_scheme.hpp:73
@ standard
raw coarse value (two-point flux; 1st-order at 2:1 faces)
@ quadratic
Martin–Cartwright tangential quadratic (2nd-order at 2:1 faces)
Component-tagged CSR overlay (the divergence delta): out(i) += Σ_k coef·u[comp](slot).
Definition cf_scheme.hpp:86
std::vector< Index > slot
Definition cf_scheme.hpp:88
std::vector< Index > start
Definition cf_scheme.hpp:87
std::vector< int8_t > comp
Definition cf_scheme.hpp:90
std::vector< double > coef
Definition cf_scheme.hpp:89
Device mirror of a CfCsr (empty when the scheme is standard / no C/F rows).
View< Index > start
View< double > coef
Scalar-input CSR overlay: out(i) += Σ_k coef·f(slot) over rows with C/F faces.
Definition cf_scheme.hpp:79
std::vector< double > coef
Definition cf_scheme.hpp:82
std::vector< Index > slot
Definition cf_scheme.hpp:81
std::vector< Index > start
size n+1
Definition cf_scheme.hpp:80
(uf_scheme − uf_std) for the div-free FACE field uf_k = ½(u_i+u_j) − (φ₊−φ₋)/d, one delta row per for...
CfCompCsr vel
reads the velocity components, rows = face slots
An assembled face-CSR: row offsets (size n+1), neighbour index + coefficient per face (size nFaces).
Definition csr.hpp:72