DNDSR 0.1.0.dev1+gcd065ad
Distributed Numeric Data Structure for CFV
Loading...
Searching...
No Matches
test_Quadrature.cpp
Go to the documentation of this file.
1/**
2 * @file test_Quadrature.cpp
3 * @brief Comprehensive doctest-based unit tests for numerical quadrature accuracy.
4 *
5 * Tests cover:
6 * 1. Polynomial integration accuracy (0th to 6th degree)
7 * - For each parametric space: Line, Tri, Quad, Tet, Hex, Prism, Pyramid
8 * - Tests exactness up to the theoretical order of each quadrature scheme
9 * 2. Complete Taylor basis integration (degree <= 3)
10 * - Uses BaseFunction utilities for systematic testing
11 * - All monomials: 1, x, y, z, x², xy, y², xz, yz, z², x³, x²y, xy², y³, ...
12 * 3. Weight sum verification (should equal parametric space volume)
13 * 4. Integration scheme selection correctness
14 *
15 * Theoretical exactness:
16 * - Gauss-Legendre on [-1,1]^d: exact for polynomials up to 2n-1
17 * - Hammer rules on simplex: exactness varies by rule
18 * - Product rules: exactness determined by component rules
19 */
20
21#define DOCTEST_CONFIG_IMPLEMENT_WITH_MAIN
22#include "doctest.h"
23
24#include "Geom/Quadrature.hpp"
25#include "Geom/Elements.hpp"
26#include "Geom/BaseFunction.hpp"
27
28#include <cmath>
29#include <array>
30#include <iostream>
31
32using namespace DNDS::Geom;
33using namespace DNDS::Geom::Elem;
34
35// ===================================================================
36// Helper functions for polynomial integration
37// ===================================================================
38
39/**
40 * @brief Get the list of Taylor basis monomial exponents up to given order
41 *
42 * Uses BaseFunction's diffOperatorOrderList to get exponents (nx, ny, nz)
43 * for each polynomial basis function.
44 *
45 * @param dim Dimension (2 or 3)
46 * @param maxOrder Maximum polynomial order (0-3)
47 * @return Vector of {nx, ny, nz} exponent triplets
48 */
49std::vector<std::array<int, 3>> GetTaylorBasisMonomials(int dim, int maxOrder)
50{
51 std::vector<std::array<int, 3>> result;
52
53 int nDof = Base::PolynomialNDOF<2, 3>(); // Get max 2D DOF
54 if (dim == 3)
55 nDof = Base::PolynomialNDOF<3, 3>(); // Get max 3D DOF
56
57 // Determine how many DOFs we actually need based on maxOrder
58 int nDofNeeded = 1;
59 switch (maxOrder)
60 {
61 case 0: nDofNeeded = 1; break;
62 case 1: nDofNeeded = dim == 2 ? 3 : 4; break;
63 case 2: nDofNeeded = dim == 2 ? 6 : 10; break;
64 case 3: nDofNeeded = dim == 2 ? 10 : 20; break;
65 }
66
67 nDof = std::min(nDof, nDofNeeded);
68
69 for (int i = 0; i < nDof; i++)
70 {
71 if (dim == 2)
72 result.push_back(Base::diffOperatorOrderList2D[i]);
73 else
74 result.push_back(Base::diffOperatorOrderList[i]);
75 }
76
77 return result;
78}
79
80/**
81 * @brief Evaluate a monomial x^a * y^b * z^c at point p
82 */
83t_real EvalMonomial(const tPoint& p, int a, int b = 0, int c = 0)
84{
85 t_real result = 1.0;
86 if (a > 0) result *= std::pow(p(0), a);
87 if (b > 0) result *= std::pow(p(1), b);
88 if (c > 0) result *= std::pow(p(2), c);
89 return result;
90}
91
92/**
93 * @brief Compute exact integral of monomial over parametric reference element
94 *
95 * Reference domains:
96 * - Line: [-1, 1] integral = 2/(a+1) for x^a (if a even)
97 * - Quad: [-1, 1]^2 integral = product of 1D integrals
98 * - Hex: [-1, 1]^3 integral = product of 1D integrals
99 * - Tri: {xi>=0, et>=0, xi+et<=1} integral = a!*b!/(a+b+2)!
100 * - Tet: {xi>=0, et>=0, zt>=0, xi+et+zt<=1}
101 * - Prism: Triangle x [-1,1]
102 * - Pyramid: Special domain
103 */
104t_real ExactMonomialIntegral(ParamSpace ps, int a, int b = 0, int c = 0)
105{
106 switch (ps)
107 {
108 case LineSpace:
109 {
110 // Integral of x^a from -1 to 1
111 if (a % 2 == 1) return 0.0; // Odd powers integrate to 0
112 return 2.0 / (a + 1.0);
113 }
114 case QuadSpace:
115 {
116 // Product of 1D integrals
117 t_real Ix = (a % 2 == 1) ? 0.0 : 2.0 / (a + 1.0);
118 t_real Iy = (b % 2 == 1) ? 0.0 : 2.0 / (b + 1.0);
119 return Ix * Iy;
120 }
121 case HexSpace:
122 {
123 t_real Ix = (a % 2 == 1) ? 0.0 : 2.0 / (a + 1.0);
124 t_real Iy = (b % 2 == 1) ? 0.0 : 2.0 / (b + 1.0);
125 t_real Iz = (c % 2 == 1) ? 0.0 : 2.0 / (c + 1.0);
126 return Ix * Iy * Iz;
127 }
128 case TriSpace:
129 {
130 // Over reference triangle with vertices at (0,0), (1,0), (0,1)
131 // The Hammer quadrature rules use a different reference triangle:
132 // area coordinates with xi + et + zt = 1, xi >= 0, et >= 0, zt >= 0
133 // where zt = 1 - xi - et
134 //
135 // For the Hammer rules, the reference triangle is:
136 // Vertices: (0,0), (1,0), (0,1)
137 // Integral of xi^a * et^b over this triangle:
138 // = B(a+1, b+1) / 2 = Gamma(a+1)*Gamma(b+1) / (2*Gamma(a+b+2))
139 // = a! * b! / (2 * (a+b+1)!)
140 // Wait, that's not right either. Let me reconsider.
141 //
142 // For standard reference triangle {(xi, et) : xi>=0, et>=0, xi+et<=1}:
143 // int_0^1 int_0^{1-xi} xi^a * et^b det dxi
144 // = Beta(a+1, b+1) / (a+b+2) ? No...
145 //
146 // Actually: int_0^1 xi^a * (1-xi)^{b+1} / (b+1) dxi
147 // = B(a+1, b+2) / (b+1) = Gamma(a+1)*Gamma(b+2) / ((b+1)*Gamma(a+b+3))
148 // = a! * (b+1)! / ((b+1) * (a+b+2)!)
149 // = a! * b! / (a+b+2)!
150 auto factorial = [](int n) -> t_real {
151 t_real result = 1.0;
152 for (int i = 2; i <= n; i++) result *= i;
153 return result;
154 };
155 return factorial(a) * factorial(b) / factorial(a + b + 2);
156 }
157 case TetSpace:
158 {
159 // Over reference tet: int_0^1 int_0^{1-xi} int_0^{1-xi-et} xi^a et^b zt^c dzt det dxi
160 // = a! * b! * c! / (a + b + c + 3)!
161 auto factorial = [](int n) -> t_real {
162 t_real result = 1.0;
163 for (int i = 2; i <= n; i++) result *= i;
164 return result;
165 };
166 return factorial(a) * factorial(b) * factorial(c) / factorial(a + b + c + 3);
167 }
168 case PrismSpace:
169 {
170 // Product of triangle integral (xi, et) and line integral (zt)
171 auto factorial = [](int n) -> t_real {
172 t_real result = 1.0;
173 for (int i = 2; i <= n; i++) result *= i;
174 return result;
175 };
176 t_real I_tri = factorial(a) * factorial(b) / factorial(a + b + 2);
177 t_real I_line = (c % 2 == 1) ? 0.0 : 2.0 / (c + 1.0);
178 return I_tri * I_line;
179 }
180 case PyramidSpace:
181 {
182 // Pyramid reference: base [-1,1]^2 at z=0, apex at z=1
183 // Parametric coordinates transformed
184 // Approximate exact values for low-order monomials
185 // For pyramid: xi, et in [-1,1], zt in [0,1] with scaling
186 // Jacobian factor: (1 - zt)^2
187
188 // This is more complex - use numerical reference for verification
189 // For low degrees, we can compute analytically
190
191 // Pure z powers: int_0^1 zt^c * (1-zt)^2 * 4 dz (area at height z is 4*(1-z)^2)
192 // = 4 * int_0^1 zt^c * (1 - 2zt + zt^2) dz
193 // = 4 * [1/(c+1) - 2/(c+2) + 1/(c+3)]
194 if (a == 0 && b == 0)
195 {
196 return 4.0 * (1.0/(c+1) - 2.0/(c+2) + 1.0/(c+3));
197 }
198
199 // Pure x or y: odd powers give 0
200 if ((a % 2 == 1 && b == 0 && c == 0) || (b % 2 == 1 && a == 0 && c == 0))
201 return 0.0;
202
203 // For x^2: int_0^1 (1-z)^2/3 * (1-z)^2 * 4 dz with appropriate scaling
204 // = (4/3) * int_0^1 (1-z)^4 dz = (4/3) * 1/5 = 4/15
205 if (a == 2 && b == 0 && c == 0)
206 return 4.0 / 15.0;
207 if (a == 0 && b == 2 && c == 0)
208 return 4.0 / 15.0;
209
210 // Constant
211 if (a == 0 && b == 0 && c == 0)
212 return 4.0 / 3.0; // Volume of pyramid
213
214 // For more complex cases, return NaN to skip test
215 return std::nan("");
216 }
217 default:
218 return std::nan("");
219 }
220}
221
222/**
223 * @brief Numerically integrate a monomial using quadrature
224 */
225t_real QuadratureMonomialIntegral(ParamSpace ps, int int_order, int a, int b = 0, int c = 0)
226{
227 t_index scheme = GetQuadratureScheme(ps, int_order);
228 if (scheme == 0) return std::nan("");
229
230 t_real sum = 0.0;
231 for (t_index iG = 0; iG < scheme; iG++)
232 {
233 tPoint pParam{0, 0, 0};
234 t_real w;
235 GetQuadraturePoint(ps, scheme, iG, pParam, w);
236 sum += w * EvalMonomial(pParam, a, b, c);
237 }
238 return sum;
239}
240
241/**
242 * @brief Get parametric space from element type for testing
243 */
245{
246 Element e{t};
247 return e.GetParamSpace();
248}
249
250// ===================================================================
251// Test cases
252// ===================================================================
253
254TEST_CASE("Quadrature: scheme constants are valid")
255{
256 // Verify that all scheme constants are positive
257 CHECK(INT_SCHEME_Line_1 > 0);
258 CHECK(INT_SCHEME_Line_2 > 0);
259 CHECK(INT_SCHEME_Line_3 > 0);
260 CHECK(INT_SCHEME_Line_4 > 0);
261
262 CHECK(INT_SCHEME_Quad_1 > 0);
263 CHECK(INT_SCHEME_Quad_4 > 0);
264 CHECK(INT_SCHEME_Quad_9 > 0);
265 CHECK(INT_SCHEME_Quad_16 > 0);
266
267 CHECK(INT_SCHEME_Tri_1 > 0);
268 CHECK(INT_SCHEME_Tri_3 > 0);
269 CHECK(INT_SCHEME_Tri_6 > 0);
270 CHECK(INT_SCHEME_Tri_7 > 0);
271 CHECK(INT_SCHEME_Tri_12 > 0);
272
273 CHECK(INT_SCHEME_Tet_1 > 0);
274 CHECK(INT_SCHEME_Tet_4 > 0);
275 CHECK(INT_SCHEME_Tet_8 > 0);
276 CHECK(INT_SCHEME_Tet_14 > 0);
277 CHECK(INT_SCHEME_Tet_24 > 0);
278
279 CHECK(INT_SCHEME_Hex_1 > 0);
280 CHECK(INT_SCHEME_Hex_8 > 0);
281 CHECK(INT_SCHEME_Hex_27 > 0);
282 CHECK(INT_SCHEME_Hex_64 > 0);
283
284 CHECK(INT_SCHEME_Prism_1 > 0);
285 CHECK(INT_SCHEME_Prism_6 > 0);
286 CHECK(INT_SCHEME_Prism_18 > 0);
287 CHECK(INT_SCHEME_Prism_21 > 0);
288 CHECK(INT_SCHEME_Prism_48 > 0);
289
290 CHECK(INT_SCHEME_Pyramid_1 > 0);
291 CHECK(INT_SCHEME_Pyramid_8 > 0);
292 CHECK(INT_SCHEME_Pyramid_27 > 0);
293 CHECK(INT_SCHEME_Pyramid_64 > 0);
294}
295
296TEST_CASE("Quadrature: weight sums equal reference element volumes")
297{
298 constexpr t_real tol = 1e-12;
299
300 // Line [-1,1]: volume = 2
301 for (int order = 0; order <= 6; order++)
302 {
304 if (scheme == 0) continue;
305
306 t_real sum = 0.0;
307 for (t_index iG = 0; iG < scheme; iG++)
308 {
309 tPoint p;
310 t_real w;
311 GetQuadraturePoint(LineSpace, scheme, iG, p, w);
312 sum += w;
313 }
314 CHECK(sum == doctest::Approx(2.0).epsilon(tol));
315 }
316
317 // Quad [-1,1]^2: volume = 4
318 for (int order = 0; order <= 6; order++)
319 {
321 if (scheme == 0) continue;
322
323 t_real sum = 0.0;
324 for (t_index iG = 0; iG < scheme; iG++)
325 {
326 tPoint p;
327 t_real w;
328 GetQuadraturePoint(QuadSpace, scheme, iG, p, w);
329 sum += w;
330 }
331 CHECK(sum == doctest::Approx(4.0).epsilon(tol));
332 }
333
334 // Hex [-1,1]^3: volume = 8
335 for (int order = 0; order <= 6; order++)
336 {
338 if (scheme == 0) continue;
339
340 t_real sum = 0.0;
341 for (t_index iG = 0; iG < scheme; iG++)
342 {
343 tPoint p;
344 t_real w;
345 GetQuadraturePoint(HexSpace, scheme, iG, p, w);
346 sum += w;
347 }
348 CHECK(sum == doctest::Approx(8.0).epsilon(tol));
349 }
350
351 // Triangle: volume = 1/2
352 for (int order = 0; order <= 6; order++)
353 {
355 if (scheme == 0) continue;
356
357 t_real sum = 0.0;
358 for (t_index iG = 0; iG < scheme; iG++)
359 {
360 tPoint p;
361 t_real w;
362 GetQuadraturePoint(TriSpace, scheme, iG, p, w);
363 sum += w;
364 }
365 CHECK(sum == doctest::Approx(0.5).epsilon(tol));
366 }
367
368 // Tetrahedron: volume = 1/6
369 for (int order = 0; order <= 6; order++)
370 {
372 if (scheme == 0) continue;
373
374 t_real sum = 0.0;
375 for (t_index iG = 0; iG < scheme; iG++)
376 {
377 tPoint p;
378 t_real w;
379 GetQuadraturePoint(TetSpace, scheme, iG, p, w);
380 sum += w;
381 }
382 CHECK(sum == doctest::Approx(1.0/6.0).epsilon(tol));
383 }
384
385 // Prism: volume = 1/2 * 2 = 1
386 for (int order = 0; order <= 6; order++)
387 {
389 if (scheme == 0) continue;
390
391 t_real sum = 0.0;
392 for (t_index iG = 0; iG < scheme; iG++)
393 {
394 tPoint p;
395 t_real w;
396 GetQuadraturePoint(PrismSpace, scheme, iG, p, w);
397 sum += w;
398 }
399 CHECK(sum == doctest::Approx(1.0).epsilon(tol));
400 }
401
402 // Pyramid: volume = 4/3
403 for (int order = 0; order <= 6; order++)
404 {
406 if (scheme == 0) continue;
407
408 t_real sum = 0.0;
409 for (t_index iG = 0; iG < scheme; iG++)
410 {
411 tPoint p;
412 t_real w;
413 GetQuadraturePoint(PyramidSpace, scheme, iG, p, w);
414 sum += w;
415 }
416 CHECK(sum == doctest::Approx(4.0/3.0).epsilon(tol));
417 }
418}
419
420TEST_CASE("Quadrature: Line integrates polynomials up to expected order")
421{
422 constexpr t_real tol = 1e-12;
423
424 // Gauss-Legendre with n points is exact for polynomials up to degree 2n-1
425 // Line_1 (1 point): exact up to degree 1
426 // Line_2 (2 points): exact up to degree 3
427 // Line_3 (3 points): exact up to degree 5
428 // Line_4 (4 points): exact up to degree 7 (we only test up to 6)
429
430 struct TestCase { int order; int maxExactDegree; };
431 TestCase tests[] = {{1, 1}, {3, 3}, {5, 5}, {6, 7}};
432
433 for (const auto& tc : tests)
434 {
435 CAPTURE(tc.order);
436 CAPTURE(tc.maxExactDegree);
437
438 for (int deg = 0; deg <= std::min(tc.maxExactDegree, 6); deg++)
439 {
440 CAPTURE(deg);
442 t_real numeric = QuadratureMonomialIntegral(LineSpace, tc.order, deg);
443
444 CHECK(numeric == doctest::Approx(exact).epsilon(tol));
445 }
446 }
447}
448
449TEST_CASE("Quadrature: Quad integrates polynomials up to expected order")
450{
451 constexpr t_real tol = 1e-12;
452
453 // Product of 1D Gauss-Legendre: exact for degree 2n-1 in each direction
454 // Quad_1 (1x1): exact up to degree 1 in each direction (total degree 2)
455 // Quad_4 (2x2): exact up to degree 3 in each direction (total degree 6)
456 // Quad_9 (3x3): exact up to degree 5 in each direction (total degree 10)
457
458 struct TestCase { int order; int maxDegreePerDir; };
459 TestCase tests[] = {{1, 1}, {3, 3}, {5, 5}, {6, 5}};
460
461 for (const auto& tc : tests)
462 {
463 CAPTURE(tc.order);
464 int maxDeg = std::min(tc.maxDegreePerDir, 3); // Limit test scope
465
466 for (int a = 0; a <= maxDeg; a++)
467 {
468 for (int b = 0; b <= maxDeg; b++)
469 {
470 if (a + b > tc.maxDegreePerDir * 2) continue; // Skip high total degree
471
472 CAPTURE(a);
473 CAPTURE(b);
475 t_real numeric = QuadratureMonomialIntegral(QuadSpace, tc.order, a, b);
476
477 CHECK(numeric == doctest::Approx(exact).epsilon(tol));
478 }
479 }
480 }
481}
482
483TEST_CASE("Quadrature: Hex integrates polynomials up to expected order")
484{
485 constexpr t_real tol = 1e-12;
486
487 // Product of 1D Gauss-Legendre in 3D
488 struct TestCase { int order; int maxDegreePerDir; };
489 TestCase tests[] = {{1, 1}, {3, 3}};
490
491 for (const auto& tc : tests)
492 {
493 CAPTURE(tc.order);
494 int maxDeg = std::min(tc.maxDegreePerDir, 2); // Limit test scope
495
496 for (int a = 0; a <= maxDeg; a++)
497 {
498 for (int b = 0; b <= maxDeg; b++)
499 {
500 for (int c = 0; c <= maxDeg; c++)
501 {
502 CAPTURE(a);
503 CAPTURE(b);
504 CAPTURE(c);
505 t_real exact = ExactMonomialIntegral(HexSpace, a, b, c);
506 t_real numeric = QuadratureMonomialIntegral(HexSpace, tc.order, a, b, c);
507
508 CHECK(numeric == doctest::Approx(exact).epsilon(tol));
509 }
510 }
511 }
512 }
513}
514
515TEST_CASE("Quadrature: Triangle integrates polynomials up to expected order")
516{
517 constexpr t_real tol = 1e-10;
518
519 // Hammer rules have different exactness properties
520 // Tri_1: degree 1
521 // Tri_3: degree 2
522 // Tri_6: degree 4
523 // Tri_7: degree 5
524 // Tri_12: degree 6
525
526 // First, verify the coordinate system by checking the first few quadrature points
527 {
528 tPoint p;
529 t_real w;
530
531 // Tri_1: single point at centroid
532 GetQuadraturePoint(TriSpace, INT_SCHEME_Tri_1, 0, p, w);
533 CHECK(w == doctest::Approx(0.5).epsilon(tol)); // Area of reference triangle
534
535 // Tri_3: 3 points
536 t_real sum_w = 0;
537 for (int i = 0; i < 3; i++) {
538 GetQuadraturePoint(TriSpace, INT_SCHEME_Tri_3, i, p, w);
539 sum_w += w;
540 }
541 CHECK(sum_w == doctest::Approx(0.5).epsilon(tol));
542 }
543
544 struct TestCase { int order; int maxTotalDegree; };
545 TestCase tests[] = {{1, 1}, {2, 2}, {4, 4}, {5, 5}, {6, 6}};
546
547 for (const auto& tc : tests)
548 {
549 CAPTURE(tc.order);
550 CAPTURE(tc.maxTotalDegree);
551
552 for (int a = 0; a <= tc.maxTotalDegree; a++)
553 {
554 for (int b = 0; b <= tc.maxTotalDegree - a; b++)
555 {
556 CAPTURE(a);
557 CAPTURE(b);
559 t_real numeric = QuadratureMonomialIntegral(TriSpace, tc.order, a, b);
560
561 if (std::isnan(exact) || std::isnan(numeric))
562 continue;
563
564 CHECK(numeric == doctest::Approx(exact).epsilon(tol));
565 }
566 }
567 }
568}
569
570TEST_CASE("Quadrature: Tetrahedron integrates polynomials up to expected order")
571{
572 constexpr t_real tol = 1e-10;
573
574 // Hammer tet rules
575 // Tet_1: degree 1
576 // Tet_4: degree 2
577 // Tet_8: degree 3
578 // Tet_14: degree 5
579 // Tet_24: degree 6
580
581 struct TestCase { int order; int maxTotalDegree; };
582 TestCase tests[] = {{1, 1}, {2, 2}, {3, 3}, {5, 5}, {6, 6}};
583
584 for (const auto& tc : tests)
585 {
586 CAPTURE(tc.order);
587 CAPTURE(tc.maxTotalDegree);
588
589 for (int a = 0; a <= tc.maxTotalDegree; a++)
590 {
591 for (int b = 0; b <= tc.maxTotalDegree - a; b++)
592 {
593 for (int c = 0; c <= tc.maxTotalDegree - a - b; c++)
594 {
595 CAPTURE(a);
596 CAPTURE(b);
597 CAPTURE(c);
598 t_real exact = ExactMonomialIntegral(TetSpace, a, b, c);
599 t_real numeric = QuadratureMonomialIntegral(TetSpace, tc.order, a, b, c);
600
601 if (std::isnan(exact) || std::isnan(numeric))
602 continue;
603
604 CHECK(numeric == doctest::Approx(exact).epsilon(tol));
605 }
606 }
607 }
608 }
609}
610
611TEST_CASE("Quadrature: Prism integrates polynomials up to expected order")
612{
613 constexpr t_real tol = 1e-10;
614
615 // Prism = Triangle x Line
616 // Exactness determined by component rules
617 struct TestCase { int order; int maxTriDegree; int maxLineDegree; };
618 TestCase tests[] = {{1, 1, 1}, {2, 2, 3}};
619
620 for (const auto& tc : tests)
621 {
622 CAPTURE(tc.order);
623
624 for (int a = 0; a <= tc.maxTriDegree; a++)
625 {
626 for (int b = 0; b <= tc.maxTriDegree - a; b++)
627 {
628 for (int c = 0; c <= tc.maxLineDegree && c <= 3; c += 2) // Even powers only for non-zero
629 {
630 CAPTURE(a);
631 CAPTURE(b);
632 CAPTURE(c);
634 t_real numeric = QuadratureMonomialIntegral(PrismSpace, tc.order, a, b, c);
635
636 if (std::isnan(exact) || std::isnan(numeric))
637 continue;
638
639 CHECK(numeric == doctest::Approx(exact).epsilon(tol));
640 }
641 }
642 }
643 }
644}
645
646TEST_CASE("Quadrature: Pyramid integrates constant and low-degree polynomials")
647{
648 constexpr t_real tol = 1e-10;
649
650 // Pyramid uses product of Gauss-Legendre and Gauss-Jacobi
651 // Pyramid_1 (O1), Pyramid_8 (O3), Pyramid_27 (O5), Pyramid_64 (O7)
652
653 struct TestCase { int order; int maxExactDegree; };
654 TestCase tests[] = {{1, 1}, {3, 3}, {5, 5}, {6, 5}};
655
656 for (const auto& tc : tests)
657 {
658 CAPTURE(tc.order);
659 CAPTURE(tc.maxExactDegree);
660
661 // Test constant (degree 0)
662 {
663 t_real exact = ExactMonomialIntegral(PyramidSpace, 0, 0, 0);
664 t_real numeric = QuadratureMonomialIntegral(PyramidSpace, tc.order, 0, 0, 0);
665 CHECK(numeric == doctest::Approx(exact).epsilon(tol));
666 }
667
668 // Test odd powers x, y (degree 1) - should integrate to 0
669 if (tc.maxExactDegree >= 1)
670 {
671 t_real numeric_x = QuadratureMonomialIntegral(PyramidSpace, tc.order, 1, 0, 0);
672 CHECK(numeric_x == doctest::Approx(0.0).epsilon(tol));
673
674 t_real numeric_y = QuadratureMonomialIntegral(PyramidSpace, tc.order, 0, 1, 0);
675 CHECK(numeric_y == doctest::Approx(0.0).epsilon(tol));
676 }
677
678 // Test z (degree 1)
679 if (tc.maxExactDegree >= 1)
680 {
681 t_real exact = ExactMonomialIntegral(PyramidSpace, 0, 0, 1);
682 if (!std::isnan(exact))
683 {
684 t_real numeric = QuadratureMonomialIntegral(PyramidSpace, tc.order, 0, 0, 1);
685 CHECK(numeric == doctest::Approx(exact).epsilon(tol));
686 }
687 }
688
689 // Test x^2, y^2 (degree 2) - only for O3 and higher
690 if (tc.maxExactDegree >= 2)
691 {
692 t_real exact_x2 = ExactMonomialIntegral(PyramidSpace, 2, 0, 0);
693 if (!std::isnan(exact_x2))
694 {
695 t_real numeric = QuadratureMonomialIntegral(PyramidSpace, tc.order, 2, 0, 0);
696 CHECK(numeric == doctest::Approx(exact_x2).epsilon(tol));
697 }
698
699 t_real exact_y2 = ExactMonomialIntegral(PyramidSpace, 0, 2, 0);
700 if (!std::isnan(exact_y2))
701 {
702 t_real numeric = QuadratureMonomialIntegral(PyramidSpace, tc.order, 0, 2, 0);
703 CHECK(numeric == doctest::Approx(exact_y2).epsilon(tol));
704 }
705 }
706
707 // Test z^2 (degree 2) - only for O3 and higher
708 if (tc.maxExactDegree >= 2)
709 {
710 t_real exact = ExactMonomialIntegral(PyramidSpace, 0, 0, 2);
711 if (!std::isnan(exact))
712 {
713 t_real numeric = QuadratureMonomialIntegral(PyramidSpace, tc.order, 0, 0, 2);
714 CHECK(numeric == doctest::Approx(exact).epsilon(tol));
715 }
716 }
717 }
718}
719
720TEST_CASE("Quadrature: Quadrature class Integration method works")
721{
722 constexpr t_real tol = 1e-12;
723
724 // Test using the Quadrature class interface
725 Element elem{Quad4};
726 Quadrature quad(elem, 3); // Order 3 for Quad
727
728 // Integrate constant 1.0
729 {
730 t_real sum = 0.0;
731 quad.Integration(sum, [](t_real& acc, int iG, const tPoint& p, const tD01Nj& D01Nj) {
732 acc = 1.0;
733 });
734 CHECK(sum == doctest::Approx(4.0).epsilon(tol)); // Volume of [-1,1]^2
735 }
736
737 // Integrate x^2
738 {
739 t_real sum = 0.0;
740 quad.Integration(sum, [](t_real& acc, int iG, const tPoint& p, const tD01Nj& D01Nj) {
741 acc = p(0) * p(0);
742 });
743 CHECK(sum == doctest::Approx(4.0/3.0).epsilon(tol)); // Integral of x^2 over [-1,1]^2
744 }
745
746 // Integrate x^2 * y^2
747 {
748 t_real sum = 0.0;
749 quad.Integration(sum, [](t_real& acc, int iG, const tPoint& p, const tD01Nj& D01Nj) {
750 acc = p(0) * p(0) * p(1) * p(1);
751 });
752 CHECK(sum == doctest::Approx(4.0/9.0).epsilon(tol)); // (4/3) * (4/3) / 4 = 4/9
753 }
754}
755
756TEST_CASE("Quadrature: GetQuadraturePointInfo returns correct data")
757{
758 Element elem{Tri3};
759 Quadrature quad(elem, 2); // Order 2 for Tri
760
761 CHECK(quad.GetNumPoints() == 3); // Tri_3 has 3 points
762
763 t_real totalWeight = 0.0;
764 for (t_index iG = 0; iG < quad.GetNumPoints(); iG++)
765 {
766 auto [pParam, w] = quad.GetQuadraturePointInfo(iG);
767 totalWeight += w;
768
769 // Verify weight matches GetWeight
770 CHECK(w == quad.GetWeight(iG));
771
772 // For triangle, all points should have barycentric coordinates summing to <= 1
773 t_real coordSum = pParam(0) + pParam(1);
774 CHECK(coordSum <= 1.0 + 1e-12);
775 CHECK(pParam(0) >= -1e-12);
776 CHECK(pParam(1) >= -1e-12);
777 }
778
779 // Total weight should equal triangle area
780 CHECK(totalWeight == doctest::Approx(0.5).epsilon(1e-12));
781}
782
783TEST_CASE("Quadrature: IntegrationSimple overloads work correctly")
784{
785 constexpr t_real tol = 1e-12;
786
787 Element elem{Line2};
788 Quadrature quad(elem, 3);
789
790 // Test 2-argument version: f(acc, iG)
791 {
792 t_real sum = 0.0;
793 quad.IntegrationSimple(sum, [](t_real& acc, int iG) {
794 acc = 1.0;
795 });
796 CHECK(sum == doctest::Approx(2.0).epsilon(tol));
797 }
798
799 // Test 3-argument version: f(acc, iG, w)
800 {
801 t_real sum = 0.0;
802 quad.IntegrationSimple(sum, [](t_real& acc, int iG, t_real w) {
803 acc = w; // Just accumulate the weight directly
804 });
805 CHECK(sum == doctest::Approx(2.0).epsilon(tol));
806 }
807}
808
809// ===================================================================
810// Part C: Complete Taylor Basis Integration Tests (Degree <= 3)
811// ===================================================================
812// These tests use BaseFunction utilities to systematically test ALL
813// polynomial basis functions up to degree 3, not just monomials.
814
815TEST_CASE("Quadrature: Taylor basis integration on 2D elements (degree <= 3)")
816{
817 constexpr t_real tol = 1e-10;
818
819 // Parametric spaces for 2D elements
820 // Each config specifies: ps, dim, and pairs of (integration_order, max_exact_degree)
821 struct TestConfig {
823 int dim;
825 std::vector<std::pair<int, int>> testOrdersWithDegree; // (intOrder, maxExactDegree)
826 };
827
829 // Triangle Hammer rules: Tri_1 (O1), Tri_3 (O2), Tri_6 (O4), Tri_7 (O5), Tri_12 (O6)
830 {TriSpace, 2, 3, {{1, 1}, {2, 2}, {4, 4}, {5, 5}, {6, 6}}},
831 // Quad Gauss-Legendre: Quad_1 (O1), Quad_4 (O3), Quad_9 (O5), Quad_16 (O7)
832 {QuadSpace, 2, 3, {{1, 1}, {3, 3}, {5, 5}, {6, 5}}}, // O6 uses O5 rule
833 };
834
835 for (const auto& cfg : configs)
836 {
837 CAPTURE(cfg.ps);
838
839 // Get all Taylor basis monomials up to degree 3
840 auto monomials = GetTaylorBasisMonomials(cfg.dim, cfg.maxOrder);
841
842 for (const auto& [intOrder, maxExactDegree] : cfg.testOrdersWithDegree)
843 {
844 CAPTURE(intOrder);
845 CAPTURE(maxExactDegree);
846
847 t_index scheme = GetQuadratureScheme(cfg.ps, intOrder);
848 if (scheme == 0) continue;
849
850 for (size_t iMono = 0; iMono < monomials.size(); iMono++)
851 {
852 const auto& exp = monomials[iMono];
853 int a = exp[0], b = exp[1], c = exp[2];
854 int totalDegree = a + b + c;
855
856 // Skip monomials that exceed the quadrature rule's exactness
857 if (totalDegree > maxExactDegree)
858 continue;
859
860 CAPTURE(a);
861 CAPTURE(b);
862 CAPTURE(c);
863 CAPTURE(totalDegree);
864
865 // Compute exact integral
866 t_real exact = ExactMonomialIntegral(cfg.ps, a, b, c);
867 if (std::isnan(exact)) continue;
868
869 // Compute numerical integral
870 t_real numeric = QuadratureMonomialIntegral(cfg.ps, intOrder, a, b, c);
871 if (std::isnan(numeric)) continue;
872
873 CHECK(numeric == doctest::Approx(exact).epsilon(tol));
874 }
875 }
876 }
877}
878
879TEST_CASE("Quadrature: Taylor basis integration on 3D elements (degree <= 3)")
880{
881 constexpr t_real tol = 1e-10;
882
883 // Parametric spaces for 3D elements
884 struct TestConfig {
886 int dim;
887 int maxOrder;
888 std::vector<std::pair<int, int>> testOrdersWithDegree; // (intOrder, maxExactDegree)
889 };
890
891 TestConfig configs[] = {
892 // Tet Hammer rules: Tet_1 (O1), Tet_4 (O2), Tet_8 (O3), Tet_14 (O5), Tet_24 (O6)
893 {TetSpace, 3, 3, {{1, 1}, {2, 2}, {3, 3}, {5, 5}, {6, 6}}},
894 // Hex Gauss-Legendre: Hex_1 (O1), Hex_8 (O3), Hex_27 (O5), Hex_64 (O7)
895 {HexSpace, 3, 3, {{1, 1}, {3, 3}, {5, 5}, {6, 5}}},
896 // Prism = Tri x Line: limited by component rules
897 {PrismSpace, 3, 3, {{1, 1}, {2, 2}, {4, 4}, {5, 5}, {6, 5}}},
898 };
899
900 for (const auto& cfg : configs)
901 {
902 CAPTURE(cfg.ps);
903
904 // Get all Taylor basis monomials up to degree 3
905 auto monomials = GetTaylorBasisMonomials(cfg.dim, cfg.maxOrder);
906
907 for (const auto& [intOrder, maxExactDegree] : cfg.testOrdersWithDegree)
908 {
909 CAPTURE(intOrder);
910 CAPTURE(maxExactDegree);
911
912 t_index scheme = GetQuadratureScheme(cfg.ps, intOrder);
913 if (scheme == 0) continue;
914
915 for (size_t iMono = 0; iMono < monomials.size(); iMono++)
916 {
917 const auto& exp = monomials[iMono];
918 int a = exp[0], b = exp[1], c = exp[2];
919 int totalDegree = a + b + c;
920
921 // Skip monomials that exceed the quadrature rule's exactness
922 if (totalDegree > maxExactDegree)
923 continue;
924
925 CAPTURE(a);
926 CAPTURE(b);
927 CAPTURE(c);
928 CAPTURE(totalDegree);
929
930 // Compute exact integral
931 t_real exact = ExactMonomialIntegral(cfg.ps, a, b, c);
932 if (std::isnan(exact)) continue;
933
934 // Compute numerical integral
935 t_real numeric = QuadratureMonomialIntegral(cfg.ps, intOrder, a, b, c);
936 if (std::isnan(numeric)) continue;
937
938 CHECK(numeric == doctest::Approx(exact).epsilon(tol));
939 }
940 }
941 }
942}
943
944TEST_CASE("Quadrature: Taylor basis integration on 1D elements (degree <= 3)")
945{
946 constexpr t_real tol = 1e-12;
947
948 // Get Taylor basis monomials for 1D (just powers of x)
949 std::vector<std::array<int, 3>> monomials;
950 for (int deg = 0; deg <= 3; deg++)
951 monomials.push_back({deg, 0, 0});
952
953 // Line Gauss-Legendre: Line_1 (O1), Line_2 (O3), Line_3 (O5), Line_4 (O7)
954 struct TestCase { int order; int maxExactDegree; };
955 TestCase tests[] = {{1, 1}, {3, 3}, {5, 5}, {6, 5}}; // O6 uses O5 rule
956
957 for (const auto& tc : tests)
958 {
959 CAPTURE(tc.order);
960 CAPTURE(tc.maxExactDegree);
961
962 for (const auto& exp : monomials)
963 {
964 int a = exp[0];
965 int totalDegree = a;
966
967 // Skip monomials that exceed the quadrature rule's exactness
968 if (totalDegree > tc.maxExactDegree)
969 continue;
970
971 CAPTURE(a);
972
974 t_real numeric = QuadratureMonomialIntegral(LineSpace, tc.order, a);
975
976 CHECK(numeric == doctest::Approx(exact).epsilon(tol));
977 }
978 }
979}
980
981TEST_CASE("Quadrature: Pyramid Taylor basis (degree <= 2)")
982{
983 constexpr t_real tol = 1e-10;
984
985 // Pyramid has more complex geometry, test up to degree 2
986 // Pyramid_1 (O1), Pyramid_8 (O3), Pyramid_27 (O5), Pyramid_64 (O7)
987 auto monomials = GetTaylorBasisMonomials(3, 2); // Up to degree 2
988
989 struct TestCase { int order; int maxExactDegree; };
990 TestCase tests[] = {{1, 1}, {3, 3}, {5, 5}, {6, 5}};
991
992 for (const auto& tc : tests)
993 {
994 CAPTURE(tc.order);
995 CAPTURE(tc.maxExactDegree);
996
997 t_index scheme = GetQuadratureScheme(PyramidSpace, tc.order);
998 if (scheme == 0) continue;
999
1000 for (const auto& exp : monomials)
1001 {
1002 int a = exp[0], b = exp[1], c = exp[2];
1003 int totalDegree = a + b + c;
1004
1005 // Skip monomials that exceed the quadrature rule's exactness
1006 if (totalDegree > tc.maxExactDegree)
1007 continue;
1008
1009 CAPTURE(a);
1010 CAPTURE(b);
1011 CAPTURE(c);
1012
1014 if (std::isnan(exact)) continue;
1015
1016 t_real numeric = QuadratureMonomialIntegral(PyramidSpace, tc.order, a, b, c);
1017 if (std::isnan(numeric)) continue;
1018
1019 CHECK(numeric == doctest::Approx(exact).epsilon(tol));
1020 }
1021 }
1022}
1023
1024TEST_CASE("Quadrature: FPolynomialFill consistency check")
1025{
1026 // Verify that our monomial evaluations match FPolynomialFill results
1027 // for the 0th derivative (function values)
1028
1029 tPoint p{0.3, 0.4, 0.5};
1030
1031 // Test 2D polynomials
1032 {
1033 Eigen::Matrix<t_real, 1, 10> polyVals;
1034 Base::FPolynomialFill2D(polyVals, p(0), p(1), p(2), 1.0, 1.0, 1.0, 1, 10);
1035
1036 // Compare with manual monomial evaluation
1038 for (size_t i = 0; i < monomials.size() && i < 10; i++)
1039 {
1040 const auto& exp = monomials[i];
1041 t_real expected = EvalMonomial(p, exp[0], exp[1], exp[2]);
1042 CHECK(polyVals(0, i) == doctest::Approx(expected).epsilon(1e-12));
1043 }
1044 }
1045
1046 // Test 3D polynomials
1047 {
1048 Eigen::Matrix<t_real, 1, 20> polyVals;
1049 Base::FPolynomialFill3D(polyVals, p(0), p(1), p(2), 1.0, 1.0, 1.0, 1, 20);
1050
1052 for (size_t i = 0; i < monomials.size() && i < 20; i++)
1053 {
1054 const auto& exp = monomials[i];
1055 t_real expected = EvalMonomial(p, exp[0], exp[1], exp[2]);
1056 CHECK(polyVals(0, i) == doctest::Approx(expected).epsilon(1e-12));
1057 }
1058 }
1059}
void FPolynomialFill3D(TDIBJ &T, real x, real y, real z, real lx, real ly, real lz, int rows, int cols)
void FPolynomialFill2D(TDIBJ &T, real x, real y, real z, real lx, real ly, real lz, int rows, int cols)
constexpr t_index GetQuadratureScheme(ParamSpace ps, int int_order)
void GetQuadraturePoint(ParamSpace ps, t_index scheme, int iG, TPoint &pParam, t_real &w)
Eigen::Matrix< t_real, 4, Eigen::Dynamic > tD01Nj
Definition Elements.hpp:183
int32_t t_index
Definition Geometric.hpp:6
Eigen::Vector3d tPoint
Definition Geometric.hpp:9
double t_real
Definition Geometric.hpp:8
DNDS_DEVICE_CALLABLE constexpr ParamSpace GetParamSpace() const
Definition Elements.hpp:192
DNDS_DEVICE_CALLABLE std::enable_if_t< std::is_invocable_v< TFunc, TAcc &, int > > IntegrationSimple(TAcc &buf, TFunc &&f)
Simple integration without shape function access.
DNDS_DEVICE_CALLABLE t_index GetNumPoints() const
DNDS_DEVICE_CALLABLE auto GetQuadraturePointInfo(int iG) const
DNDS_DEVICE_CALLABLE real GetWeight(int iG) const
DNDS_DEVICE_CALLABLE void Integration(TAcc &buf, TFunc &&f)
General integration method with shape function access.
std::vector< std::pair< int, int > > testOrdersWithDegree
tVec b(NCells)
CHECK(result.size()==3)
real expected
auto result
double order
Definition test_ODE.cpp:257
t_real QuadratureMonomialIntegral(ParamSpace ps, int int_order, int a, int b=0, int c=0)
Numerically integrate a monomial using quadrature.
t_real ExactMonomialIntegral(ParamSpace ps, int a, int b=0, int c=0)
Compute exact integral of monomial over parametric reference element.
TestConfig configs[]
t_real EvalMonomial(const tPoint &p, int a, int b=0, int c=0)
Evaluate a monomial x^a * y^b * z^c at point p.
std::vector< std::array< int, 3 > > GetTaylorBasisMonomials(int dim, int maxOrder)
Get the list of Taylor basis monomial exponents up to given order.
TestCase tests[]
ParamSpace GetParamSpaceFromElement(ElemType t)
Get parametric space from element type for testing.
std::vector< std::array< int, 3 > > monomials
TEST_CASE("3D: VFV P2 HQM error < P1 on sinCos3D")
Eigen::Vector3d n(1.0, 0.0, 0.0)