//! Every quadrature rule must integrate 1 to the measure of its reference //! domain. //! //! This is the cheapest possible invariant on a quadrature rule, it holds for //! every order and every element family, and it is decisive: a rule whose //! weights do not sum correctly scales every integral computed with it by a //! constant factor. Nothing downstream can detect that — the stiffness matrix //! stays symmetric, the mass matrix stays positive definite, rigid-body modes //! stay exact. Only the total is wrong. //! //! Two rules failed it when it was first written: `triangle(3)` summed to 0.25 //! against a reference area of 0.5, and `tetrahedron(3)` to 1/36 against a //! reference volume of 1/6. Both carried a spurious division by the reference //! measure applied to weights that were already tabulated for it. use rtx_fea::elements::quadrature::QuadratureRule; fn weight_sum(rule: &QuadratureRule) -> f64 { rule.points.iter().map(|p| p.weight).sum() } fn check(name: &str, rule: &QuadratureRule, expected: f64) { assert!( !rule.points.is_empty(), "{name}: rule has no points — integration loops over it would silently \ produce zero rather than fail" ); let sum = weight_sum(rule); assert!( (sum - expected).abs() < 1e-12, "{name}: weights sum to {sum}, expected the reference measure {expected} \ (ratio {:.6})", sum / expected, ); } /// The 1-D reference domain is [-1, 1], measure 2. #[test] fn line_rules_integrate_the_reference_length() { for order in 1..=5 { check( &format!("line({order})"), &QuadratureRule::line(order).unwrap(), 2.0, ); } } /// The reference triangle has area 1/2. #[test] fn triangle_rules_integrate_the_reference_area() { for order in 1..=3 { check( &format!("triangle({order})"), &QuadratureRule::triangle(order).unwrap(), 0.5, ); } } /// The reference quadrilateral is [-1, 1]^2, area 4. #[test] fn quadrilateral_rules_integrate_the_reference_area() { for order in 1..=5 { check( &format!("quadrilateral({order})"), &QuadratureRule::quadrilateral(order).unwrap(), 4.0, ); } } /// The reference tetrahedron has volume 1/6. #[test] fn tetrahedron_rules_integrate_the_reference_volume() { for order in 1..=3 { check( &format!("tetrahedron({order})"), &QuadratureRule::tetrahedron(order).unwrap(), 1.0 / 6.0, ); } } /// The reference hexahedron is [-1, 1]^3, volume 8. #[test] fn hexahedron_rules_integrate_the_reference_volume() { for order in 1..=5 { check( &format!("hexahedron({order})"), &QuadratureRule::hexahedron(order).unwrap(), 8.0, ); } } /// Gauss-Legendre of `n` points integrates polynomials of degree `2n - 1` /// exactly, which the weight sum alone does not check. #[test] fn gauss_legendre_is_exact_to_its_nominal_degree() { for n in 1..=5usize { let rule = QuadratureRule::gauss_legendre_1d(n).unwrap(); let degree = 2 * n - 1; // Integral of x^d over [-1, 1] is 0 for odd d, 2/(d+1) for even d. for d in 0..=degree { let numeric: f64 = rule .points .iter() .map(|p| p.weight * p.coords.xi().powi(d as i32)) .sum(); let exact = if d % 2 == 1 { 0.0 } else { 2.0 / (d as f64 + 1.0) }; assert!( (numeric - exact).abs() < 1e-12, "gauss_legendre_1d({n}) integrating x^{d}: got {numeric}, exact {exact}" ); } } }