core 1.5.0
Shared MPI block decomposition, asynchronous ghost-layer exchange, SDF geometry, load balancing (header-only C++20)
Loading...
Searching...
No Matches
quadrature.hpp
Go to the documentation of this file.
1// core — implicit quadrature over {phi > 0}: face apertures and cell volume fractions for
2// ANALYTIC geometry, the general replacement for the sphere-only closed form
3// (flow/scripts/exact_apertures_spheres.py).
4//
5// WHAT THIS IS. Given any device-callable field phi (a SceneQueryView, a single leaf, a lambda),
6// compute the fraction of an axis-aligned face or box on which phi > 0. The construction is
7// Saye's dimension reduction (SIAM J. Sci. Comput. 37(2), 2015) at its simplest useful level:
8//
9// - pick a HEIGHT DIRECTION in which the interface is (locally) a graph;
10// - integrate over the remaining directions with Gauss-Legendre;
11// - along each height line, locate the sign changes by bracketed bisection and sum the
12// lengths of the positive sub-intervals.
13//
14// The inner 1-D measure is exact to rounding, so all the error lives in the outer Gauss rule, and
15// its integrand -- the positive length as a function of the transverse coordinate -- is smooth
16// exactly where the interface is a single graph over the height axis.
17//
18// THE OUTER RULE IS SPLIT AT THE KINKS, and that is what makes the method worth having. The length
19// function has a kink wherever the interface crosses an EDGE of the face, and integrating straight
20// through those pins the error at ~1e-3 regardless of h: an aperture is a FRACTION, so a fixed kink
21// in a fixed normalized integrand costs a fixed amount and refining the grid does not help.
22// Locating the crossings on the two edge functions (two bracketed bisections each) and running the
23// Gauss rule piecewise removes the obstruction: measured on a sphere against the closed form, the
24// mean face error goes 8.5e-04 -> 3.9e-12 at N=16 and 5.4e-04 -> 3.9e-12 at N=64, against 3.8e-02
25// and 4.7e-02 for the sampled-SDF linear estimator the CFD otherwise uses. Pass breakpoints=false
26// to reproduce the unsplit behaviour.
27//
28// cellVolumeFraction does NOT do this: its outer rule is two-dimensional and the kinks are CURVES,
29// not points. Its accuracy is correspondingly weaker (~1e-6 relative on a sphere at 24^3) and more
30// nodes, not subdivision, is the lever there.
31//
32// *** NOT CERTIFIED. *** Unlike the candidate-grid pruning in scene_query.hpp, this carries NO
33// exactness guarantee, and the failure modes are structural, not rounding:
34//
35// 1. MULTIPLE CROSSINGS PER LINE are handled (every bracketed sign change is refined and the
36// positive sub-intervals accumulate), but only those a fixed initial subdivision RESOLVES. A
37// feature thinner than the sampling stride between two bracketing points is missed entirely.
38// 2. WHERE THE INTERFACE EXITS THE FACE the length function has a kink or a square-root corner,
39// and the outer Gauss rule drops from spectral to roughly second order. That is the generic
40// case for a sphere cutting a grid face, so second order is what the gate MEASURES -- do not
41// expect the spectral rate the smooth case would give.
42// 3. NON-GRAPH interfaces (a thin neck, two surfaces crossing the same line non-monotonically,
43// a corner of a CSG difference) violate the premise of the height reduction. The result stays
44// bounded in [0,1] and consistent, but the order claim is void.
45// 4. The height direction is chosen ONCE per face from the gradient at its centre. A face whose
46// interface changes orientation across it gets a poor choice and loses order.
47//
48// Consumers must therefore treat this as a high-accuracy ESTIMATOR, not as an exact aperture:
49// keep the sub-resolution floor the cut-cell pressure operator needs (alpha ~ 1e-12 rows destroy
50// its conditioning -- measured), and do not build a conservation argument on it.
51//
52// Host- and device-callable (PECLET_HD); phi is a template parameter so it inlines.
53#ifndef PECLET_CORE_GEOM_QUADRATURE_HPP
54#define PECLET_CORE_GEOM_QUADRATURE_HPP
55
56#include "peclet/core/common/portable.hpp" // PECLET_HD, Vec3, Quat
58
59namespace peclet::core::geom {
60
63namespace quad_detail {
64
65template <class Real>
66PECLET_HD void gaussLegendre01(int n, int i, Real& x, Real& w) {
67 // nodes/weights on [-1,1], mapped to [0,1] as x = (1+t)/2, w = wt/2
68 constexpr double T1[1] = {0.0};
69 constexpr double W1[1] = {2.0};
70 constexpr double T2[2] = {-0.5773502691896257, 0.5773502691896257};
71 constexpr double W2[2] = {1.0, 1.0};
72 constexpr double T3[3] = {-0.7745966692414834, 0.0, 0.7745966692414834};
73 constexpr double W3[3] = {0.5555555555555556, 0.8888888888888888, 0.5555555555555556};
74 constexpr double T4[4] = {-0.8611363115940526, -0.3399810435848563, 0.3399810435848563,
75 0.8611363115940526};
76 constexpr double W4[4] = {0.3478548451374538, 0.6521451548625461, 0.6521451548625461,
77 0.3478548451374538};
78 constexpr double T5[5] = {-0.9061798459386640, -0.5384693101056831, 0.0, 0.5384693101056831,
79 0.9061798459386640};
80 constexpr double W5[5] = {0.2369268850561891, 0.4786286704993665, 0.5688888888888889,
81 0.4786286704993665, 0.2369268850561891};
82 constexpr double T6[6] = {-0.9324695142031521, -0.6612093864662645, -0.2386191860831969,
83 0.2386191860831969, 0.6612093864662645, 0.9324695142031521};
84 constexpr double W6[6] = {0.1713244923791704, 0.3607615730481386, 0.4679139345726910,
85 0.4679139345726910, 0.3607615730481386, 0.1713244923791704};
86 constexpr double T8[8] = {-0.9602898564975363, -0.7966664774136267, -0.5255324099163290,
87 -0.1834346424956498, 0.1834346424956498, 0.5255324099163290,
88 0.7966664774136267, 0.9602898564975363};
89 constexpr double W8[8] = {0.1012285362903763, 0.2223810344533745, 0.3137066458778873,
90 0.3626837833783620, 0.3626837833783620, 0.3137066458778873,
91 0.2223810344533745, 0.1012285362903763};
92 const double* T = T4;
93 const double* W = W4;
94 int m = 4;
95 if (n <= 1) {
96 T = T1;
97 W = W1;
98 m = 1;
99 } else if (n == 2) {
100 T = T2;
101 W = W2;
102 m = 2;
103 } else if (n == 3) {
104 T = T3;
105 W = W3;
106 m = 3;
107 } else if (n == 4) {
108 T = T4;
109 W = W4;
110 m = 4;
111 } else if (n == 5) {
112 T = T5;
113 W = W5;
114 m = 5;
115 } else if (n == 6) {
116 T = T6;
117 W = W6;
118 m = 6;
119 } else {
120 T = T8;
121 W = W8;
122 m = 8;
123 }
124 const int k = i < 0 ? 0 : (i >= m ? m - 1 : i);
125 x = Real(0.5) * (Real(1) + Real(T[k]));
126 w = Real(0.5) * Real(W[k]);
127}
128
130 return n <= 1 ? 1 : (n == 2 ? 2 : (n == 3 ? 3 : (n == 4 ? 4 : (n == 5 ? 5 : (n == 6 ? 6 : 8)))));
131}
132
136template <class Real, class Phi>
138 const int NS = nseg < 2 ? 2 : (nseg > 64 ? 64 : nseg);
139 auto at = [&](Real s) { return phi(Vec3<Real>{p0.x + s * d.x, p0.y + s * d.y, p0.z + s * d.z}); };
140 auto root = [&](Real a, Real b, Real fa) {
141 for (int it = 0; it < 40; ++it) { // ~1 ulp of the segment parameter
142 const Real m = Real(0.5) * (a + b);
143 const Real fm = at(m);
144 if ((fm > Real(0)) == (fa > Real(0))) {
145 a = m;
146 fa = fm;
147 } else {
148 b = m;
149 }
150 }
151 return Real(0.5) * (a + b);
152 };
153 Real acc = 0;
154 Real sPrev = 0, fPrev = at(Real(0));
155 Real openAt = fPrev > Real(0) ? Real(0) : Real(-1); // start of the current positive run
156 for (int i = 1; i <= NS; ++i) {
157 const Real s = Real(i) / Real(NS);
158 const Real f = at(s);
159 if ((f > Real(0)) != (fPrev > Real(0))) {
160 const Real r = root(sPrev, s, fPrev);
161 if (fPrev > Real(0)) { // positive run ends here
162 acc += r - openAt;
163 openAt = Real(-1);
164 } else { // positive run starts here
165 openAt = r;
166 }
167 }
168 sPrev = s;
169 fPrev = f;
170 }
171 if (openAt >= Real(0))
172 acc += Real(1) - openAt;
173 return acc < Real(0) ? Real(0) : (acc > Real(1) ? Real(1) : acc);
174}
175
176} // namespace quad_detail
177
186template <class Real, class Phi>
188 int order = 4, int nseg = 8, bool breakpoints = true) {
189 const int a1 = (axis + 1) % 3, a2 = (axis + 2) % 3;
190 Real e1[3] = {0, 0, 0}, e2[3] = {0, 0, 0};
191 e1[a1] = hu;
192 e2[a2] = hv;
193 auto at = [&](Real u, Real v) {
194 return phi(Vec3<Real>{origin.x + u * e1[0] + v * e2[0], origin.y + u * e1[1] + v * e2[1],
195 origin.z + u * e1[2] + v * e2[2]});
196 };
197 // height direction: the in-face axis with the larger central difference
198 const Real h = Real(0.25);
199 const Real du = at(Real(0.5) + h, Real(0.5)) - at(Real(0.5) - h, Real(0.5));
200 const Real dv = at(Real(0.5), Real(0.5) + h) - at(Real(0.5), Real(0.5) - h);
201 const bool heightIsV = (dv < Real(0) ? -dv : dv) >= (du < Real(0) ? -du : du);
202
203 // Transverse coordinate t; the measured length L(t) along the height direction. L is smooth
204 // WHERE THE INTERFACE STAYS INSIDE THE FACE and has a KINK at every t where it crosses one of
205 // the two edges t -> (t, 0) and t -> (t, 1). Integrating straight through those kinks is what
206 // pins the error at ~1e-3 no matter how fine the grid: the aperture is a FRACTION, so a fixed
207 // kink in a fixed normalized integrand gives a fixed error, and refining h does not help --
208 // only more nodes do. Splitting the rule AT the kinks removes the obstruction entirely, and the
209 // breakpoints cost two bracketed bisections each on the same edge functions.
210 auto edge = [&](Real t, int which) {
211 return heightIsV ? at(t, which ? Real(1) : Real(0)) : at(which ? Real(1) : Real(0), t);
212 };
213 Real brk[10];
214 int nb = 0;
215 const int NS = nseg < 2 ? 2 : (nseg > 32 ? 32 : nseg);
216 for (int w = 0; w < 2 && breakpoints && nb < 8; ++w) {
217 Real tPrev = 0, fPrev = edge(Real(0), w);
218 for (int i = 1; i <= NS && nb < 8; ++i) {
219 const Real t = Real(i) / Real(NS);
220 const Real f = edge(t, w);
221 if ((f > Real(0)) != (fPrev > Real(0))) {
222 Real a = tPrev, b = t, fa = fPrev;
223 for (int it = 0; it < 40; ++it) {
224 const Real m = Real(0.5) * (a + b);
225 const Real fm = edge(m, w);
226 if ((fm > Real(0)) == (fa > Real(0))) {
227 a = m;
228 fa = fm;
229 } else {
230 b = m;
231 }
232 }
233 brk[nb++] = Real(0.5) * (a + b);
234 }
235 tPrev = t;
236 fPrev = f;
237 }
238 }
239 // sort the breakpoints (at most 8; insertion sort is the right tool at this size)
240 for (int i = 1; i < nb; ++i) {
241 const Real key = brk[i];
242 int j = i - 1;
243 while (j >= 0 && brk[j] > key) {
244 brk[j + 1] = brk[j];
245 --j;
246 }
247 brk[j + 1] = key;
248 }
249 brk[nb] = Real(1);
250
251 const int nq = quad_detail::gaussCount(order);
252 Real acc = 0;
253 Real lo = 0;
254 for (int seg = 0; seg <= nb; ++seg) {
255 const Real hi = brk[seg];
256 const Real len = hi - lo;
257 if (len > Real(0)) {
258 for (int i = 0; i < nq; ++i) {
259 Real t, w;
260 quad_detail::gaussLegendre01<Real>(order, i, t, w);
261 const Real tt = lo + t * len;
262 Vec3<Real> p0, d;
263 if (heightIsV) {
264 p0 = Vec3<Real>{origin.x + tt * e1[0], origin.y + tt * e1[1], origin.z + tt * e1[2]};
265 d = Vec3<Real>{e2[0], e2[1], e2[2]};
266 } else {
267 p0 = Vec3<Real>{origin.x + tt * e2[0], origin.y + tt * e2[1], origin.z + tt * e2[2]};
268 d = Vec3<Real>{e1[0], e1[1], e1[2]};
269 }
271 }
272 }
273 lo = hi;
274 }
275 return acc < Real(0) ? Real(0) : (acc > Real(1) ? Real(1) : acc);
276}
277
281template <class Real, class Phi>
283 int nseg = 8) {
284 auto at = [&](Real a, Real b, Real c) {
285 return phi(Vec3<Real>{origin.x + a * h.x, origin.y + b * h.y, origin.z + c * h.z});
286 };
287 const Real q = Real(0.25);
288 const Real gx = at(Real(0.5) + q, Real(0.5), Real(0.5)) - at(Real(0.5) - q, Real(0.5), Real(0.5));
289 const Real gy = at(Real(0.5), Real(0.5) + q, Real(0.5)) - at(Real(0.5), Real(0.5) - q, Real(0.5));
290 const Real gz = at(Real(0.5), Real(0.5), Real(0.5) + q) - at(Real(0.5), Real(0.5), Real(0.5) - q);
291 auto ab = [](Real v) { return v < Real(0) ? -v : v; };
292 int hi = 0;
293 if (ab(gy) >= ab(gx) && ab(gy) >= ab(gz))
294 hi = 1;
295 else if (ab(gz) >= ab(gx) && ab(gz) >= ab(gy))
296 hi = 2;
297 const int t1 = (hi + 1) % 3, t2 = (hi + 2) % 3;
298 const Real hh[3] = {h.x, h.y, h.z};
299
300 const int nq = quad_detail::gaussCount(order);
301 Real acc = 0;
302 for (int i = 0; i < nq; ++i) {
303 Real ti, wi;
304 quad_detail::gaussLegendre01<Real>(order, i, ti, wi);
305 for (int j = 0; j < nq; ++j) {
306 Real tj, wj;
307 quad_detail::gaussLegendre01<Real>(order, j, tj, wj);
308 Real o[3] = {origin.x, origin.y, origin.z};
309 o[t1] += ti * hh[t1];
310 o[t2] += tj * hh[t2];
311 Real d[3] = {0, 0, 0};
312 d[hi] = hh[hi];
313 acc += wi * wj *
315 Vec3<Real>{d[0], d[1], d[2]}, nseg);
316 }
317 }
318 return acc < Real(0) ? Real(0) : (acc > Real(1) ? Real(1) : acc);
319}
320
321} // namespace peclet::core::geom
322
323#endif // PECLET_CORE_GEOM_QUADRATURE_HPP
PECLET_HD void gaussLegendre01(int n, int i, Real &x, Real &w)
PECLET_HD int gaussCount(int n)
PECLET_HD Real positiveLength(const Phi &phi, Vec3< Real > p0, Vec3< Real > d, int nseg)
Length of {phi > 0} along the segment p0 + s*d, s in [0,1], by bracketed bisection.
PECLET_HD Real faceAperture(const Phi &phi, Vec3< Real > origin, int axis, Real hu, Real hv, int order=4, int nseg=8, bool breakpoints=true)
Fluid fraction ({phi > 0}) of the axis-aligned FACE with corner origin, normal axis,...
PECLET_HD Real cellVolumeFraction(const Phi &phi, Vec3< Real > origin, Vec3< Real > h, int order=4, int nseg=8)
Fluid fraction ({phi > 0}) of the axis-aligned BOX [origin, origin+h].
Kokkos::View< T *, MemSpace > View
1D device array.
Definition view.hpp:26
double Real
Default host floating type. Device kernels may use float; conversions happen at the boundary.
Definition types.hpp:18
#define PECLET_HD
Definition portable.hpp:35