use super::types::CurvedElement;
use crate::error::{IntegrateError, IntegrateResult};
pub fn lagrange_nodes_triangle(order: usize) -> IntegrateResult<Vec<[f64; 2]>> {
if order == 0 {
return Ok(vec![[1.0 / 3.0, 1.0 / 3.0]]);
}
let p = order as f64;
let mut nodes = Vec::new();
for j in 0..=order {
for i in 0..=(order - j) {
let xi = i as f64 / p;
let eta = j as f64 / p;
nodes.push([xi, eta]);
}
}
Ok(nodes)
}
pub fn lagrange_basis_triangle(xi: f64, eta: f64, order: usize) -> IntegrateResult<Vec<f64>> {
let nodes = lagrange_nodes_triangle(order)?;
let n = nodes.len();
if n == 1 {
return Ok(vec![1.0]);
}
let lam1 = 1.0 - xi - eta; let lam2 = xi; let lam3 = eta;
match order {
1 => {
Ok(vec![lam1, lam2, lam3])
}
2 => {
let phi0 = lam1 * (2.0 * lam1 - 1.0);
let phi1 = lam2 * (2.0 * lam2 - 1.0);
let phi2 = lam3 * (2.0 * lam3 - 1.0);
let phi3 = 4.0 * lam1 * lam2;
let phi4 = 4.0 * lam2 * lam3;
let phi5 = 4.0 * lam1 * lam3;
Ok(vec![phi0, phi1, phi2, phi3, phi4, phi5])
}
3 => {
let phi0 = lam1 * (3.0 * lam1 - 1.0) * (3.0 * lam1 - 2.0) / 2.0;
let phi1 = lam2 * (3.0 * lam2 - 1.0) * (3.0 * lam2 - 2.0) / 2.0;
let phi2 = lam3 * (3.0 * lam3 - 1.0) * (3.0 * lam3 - 2.0) / 2.0;
let phi3 = 9.0 / 2.0 * lam1 * lam2 * (3.0 * lam1 - 1.0);
let phi4 = 9.0 / 2.0 * lam1 * lam2 * (3.0 * lam2 - 1.0);
let phi5 = 9.0 / 2.0 * lam2 * lam3 * (3.0 * lam2 - 1.0);
let phi6 = 9.0 / 2.0 * lam2 * lam3 * (3.0 * lam3 - 1.0);
let phi7 = 9.0 / 2.0 * lam3 * lam1 * (3.0 * lam3 - 1.0);
let phi8 = 9.0 / 2.0 * lam3 * lam1 * (3.0 * lam1 - 1.0);
let phi9 = 27.0 * lam1 * lam2 * lam3;
Ok(vec![
phi0, phi1, phi2, phi3, phi4, phi5, phi6, phi7, phi8, phi9,
])
}
_ => Err(IntegrateError::NotImplementedError(format!(
"Lagrange basis on triangle implemented for order 0-3, got {}",
order
))),
}
}
pub fn lagrange_basis_triangle_deriv(
xi: f64,
eta: f64,
order: usize,
) -> IntegrateResult<(Vec<f64>, Vec<f64>)> {
let lam1 = 1.0 - xi - eta;
let lam2 = xi;
let lam3 = eta;
match order {
0 => Ok((vec![0.0], vec![0.0])),
1 => {
Ok((vec![-1.0, 1.0, 0.0], vec![-1.0, 0.0, 1.0]))
}
2 => {
let d0_dxi = -(4.0 * lam1 - 1.0);
let d0_deta = -(4.0 * lam1 - 1.0);
let d1_dxi = 4.0 * lam2 - 1.0;
let d1_deta = 0.0;
let d2_dxi = 0.0;
let d2_deta = 4.0 * lam3 - 1.0;
let d3_dxi = 4.0 * (-lam2 + lam1 * 1.0);
let d3_deta = -4.0 * lam2;
let d4_dxi = 4.0 * lam3;
let d4_deta = 4.0 * lam2;
let d5_dxi = -4.0 * lam3;
let d5_deta = 4.0 * (-lam3 + lam1);
Ok((
vec![d0_dxi, d1_dxi, d2_dxi, d3_dxi, d4_dxi, d5_dxi],
vec![d0_deta, d1_deta, d2_deta, d3_deta, d4_deta, d5_deta],
))
}
3 => {
let dl1_dxi = -1.0_f64;
let dl1_deta = -1.0_f64;
let dl2_dxi = 1.0_f64;
let dl2_deta = 0.0_f64;
let dl3_dxi = 0.0_f64;
let dl3_deta = 1.0_f64;
let corner_d = |l: f64, dl_dxi: f64, dl_deta: f64| -> (f64, f64) {
let factor = (27.0 * l * l - 18.0 * l + 2.0) / 2.0;
(dl_dxi * factor, dl_deta * factor)
};
let (d0_dxi, d0_deta) = corner_d(lam1, dl1_dxi, dl1_deta);
let (d1_dxi, d1_deta) = corner_d(lam2, dl2_dxi, dl2_deta);
let (d2_dxi, d2_deta) = corner_d(lam3, dl3_dxi, dl3_deta);
let d3_dxi = 4.5
* (dl1_dxi * lam2 * (3.0 * lam1 - 1.0)
+ lam1 * dl2_dxi * (3.0 * lam1 - 1.0)
+ lam1 * lam2 * 3.0 * dl1_dxi);
let d3_deta = 4.5
* (dl1_deta * lam2 * (3.0 * lam1 - 1.0)
+ lam1 * dl2_deta * (3.0 * lam1 - 1.0)
+ lam1 * lam2 * 3.0 * dl1_deta);
let d4_dxi = 4.5
* (dl1_dxi * lam2 * (3.0 * lam2 - 1.0)
+ lam1 * dl2_dxi * (3.0 * lam2 - 1.0)
+ lam1 * lam2 * 3.0 * dl2_dxi);
let d4_deta = 4.5
* (dl1_deta * lam2 * (3.0 * lam2 - 1.0)
+ lam1 * dl2_deta * (3.0 * lam2 - 1.0)
+ lam1 * lam2 * 3.0 * dl2_deta);
let d5_dxi = 4.5
* (dl2_dxi * lam3 * (3.0 * lam2 - 1.0)
+ lam2 * dl3_dxi * (3.0 * lam2 - 1.0)
+ lam2 * lam3 * 3.0 * dl2_dxi);
let d5_deta = 4.5
* (dl2_deta * lam3 * (3.0 * lam2 - 1.0)
+ lam2 * dl3_deta * (3.0 * lam2 - 1.0)
+ lam2 * lam3 * 3.0 * dl2_deta);
let d6_dxi = 4.5
* (dl2_dxi * lam3 * (3.0 * lam3 - 1.0)
+ lam2 * dl3_dxi * (3.0 * lam3 - 1.0)
+ lam2 * lam3 * 3.0 * dl3_dxi);
let d6_deta = 4.5
* (dl2_deta * lam3 * (3.0 * lam3 - 1.0)
+ lam2 * dl3_deta * (3.0 * lam3 - 1.0)
+ lam2 * lam3 * 3.0 * dl3_deta);
let d7_dxi = 4.5
* (dl3_dxi * lam1 * (3.0 * lam3 - 1.0)
+ lam3 * dl1_dxi * (3.0 * lam3 - 1.0)
+ lam3 * lam1 * 3.0 * dl3_dxi);
let d7_deta = 4.5
* (dl3_deta * lam1 * (3.0 * lam3 - 1.0)
+ lam3 * dl1_deta * (3.0 * lam3 - 1.0)
+ lam3 * lam1 * 3.0 * dl3_deta);
let d8_dxi = 4.5
* (dl3_dxi * lam1 * (3.0 * lam1 - 1.0)
+ lam3 * dl1_dxi * (3.0 * lam1 - 1.0)
+ lam3 * lam1 * 3.0 * dl1_dxi);
let d8_deta = 4.5
* (dl3_deta * lam1 * (3.0 * lam1 - 1.0)
+ lam3 * dl1_deta * (3.0 * lam1 - 1.0)
+ lam3 * lam1 * 3.0 * dl1_deta);
let d9_dxi =
27.0 * (dl1_dxi * lam2 * lam3 + lam1 * dl2_dxi * lam3 + lam1 * lam2 * dl3_dxi);
let d9_deta =
27.0 * (dl1_deta * lam2 * lam3 + lam1 * dl2_deta * lam3 + lam1 * lam2 * dl3_deta);
Ok((
vec![
d0_dxi, d1_dxi, d2_dxi, d3_dxi, d4_dxi, d5_dxi, d6_dxi, d7_dxi, d8_dxi, d9_dxi,
],
vec![
d0_deta, d1_deta, d2_deta, d3_deta, d4_deta, d5_deta, d6_deta, d7_deta,
d8_deta, d9_deta,
],
))
}
_ => Err(IntegrateError::NotImplementedError(format!(
"Lagrange derivatives on triangle for order 0-3, got {}",
order
))),
}
}
pub fn isoparametric_map(
ref_pt: [f64; 2],
nodes: &[[f64; 2]],
order: usize,
) -> IntegrateResult<[f64; 2]> {
let phi = lagrange_basis_triangle(ref_pt[0], ref_pt[1], order)?;
if phi.len() != nodes.len() {
return Err(IntegrateError::DimensionMismatch(format!(
"isoparametric_map: {} basis functions but {} nodes",
phi.len(),
nodes.len()
)));
}
let mut x = 0.0;
let mut y = 0.0;
for (k, &p) in phi.iter().enumerate() {
x += p * nodes[k][0];
y += p * nodes[k][1];
}
Ok([x, y])
}
pub fn jacobian(
ref_pt: [f64; 2],
nodes: &[[f64; 2]],
order: usize,
) -> IntegrateResult<[[f64; 2]; 2]> {
let (dphi_dxi, dphi_deta) = lagrange_basis_triangle_deriv(ref_pt[0], ref_pt[1], order)?;
if dphi_dxi.len() != nodes.len() {
return Err(IntegrateError::DimensionMismatch(format!(
"jacobian: {} derivatives but {} nodes",
dphi_dxi.len(),
nodes.len()
)));
}
let mut dx_dxi = 0.0;
let mut dx_deta = 0.0;
let mut dy_dxi = 0.0;
let mut dy_deta = 0.0;
for k in 0..nodes.len() {
dx_dxi += dphi_dxi[k] * nodes[k][0];
dx_deta += dphi_deta[k] * nodes[k][0];
dy_dxi += dphi_dxi[k] * nodes[k][1];
dy_deta += dphi_deta[k] * nodes[k][1];
}
Ok([[dx_dxi, dx_deta], [dy_dxi, dy_deta]])
}
pub fn det_jacobian(j: &[[f64; 2]; 2]) -> f64 {
j[0][0] * j[1][1] - j[0][1] * j[1][0]
}
pub fn inv_jacobian(j: &[[f64; 2]; 2]) -> IntegrateResult<[[f64; 2]; 2]> {
let det = det_jacobian(j);
if det.abs() < 1e-14 {
return Err(IntegrateError::ValueError(
"Jacobian is singular (det ≈ 0)".into(),
));
}
let inv_det = 1.0 / det;
Ok([
[j[1][1] * inv_det, -j[0][1] * inv_det],
[-j[1][0] * inv_det, j[0][0] * inv_det],
])
}
pub fn arc_boundary_map(
t: f64,
center: [f64; 2],
r: f64,
theta_start: f64,
theta_end: f64,
) -> [f64; 2] {
let theta = theta_start + t * (theta_end - theta_start);
[center[0] + r * theta.cos(), center[1] + r * theta.sin()]
}
pub fn blended_transfinite_interpolation(xi: f64, eta: f64, edges: &[[f64; 2]; 4]) -> [f64; 2] {
let s = (xi + 1.0) * 0.5; let t = (eta + 1.0) * 0.5;
let w00 = (1.0 - s) * (1.0 - t);
let w10 = s * (1.0 - t);
let w11 = s * t;
let w01 = (1.0 - s) * t;
[
w00 * edges[0][0] + w10 * edges[1][0] + w11 * edges[2][0] + w01 * edges[3][0],
w00 * edges[0][1] + w10 * edges[1][1] + w11 * edges[2][1] + w01 * edges[3][1],
]
}
fn gauss_triangle_dunavant(n: usize) -> IntegrateResult<(Vec<[f64; 2]>, Vec<f64>)> {
match n {
1 => {
Ok((vec![[1.0 / 3.0, 1.0 / 3.0]], vec![0.5]))
}
3 => {
let a = 1.0 / 6.0;
let b = 2.0 / 3.0;
let w = 1.0 / 6.0;
Ok((vec![[a, a], [b, a], [a, b]], vec![w, w, w]))
}
4 => {
let a1 = 1.0 / 3.0;
let a2 = 1.0 / 5.0;
let b2 = 3.0 / 5.0;
let w1 = -27.0 / 96.0;
let w2 = 25.0 / 96.0;
Ok((
vec![[a1, a1], [a2, a2], [b2, a2], [a2, b2]],
vec![w1, w2, w2, w2],
))
}
6 => {
let a1 = 0.445948490915965;
let b1 = 0.108103018168070;
let a2 = 0.091576213509771;
let b2 = 0.816847572980459;
let w1 = 0.111690794839005;
let w2 = 0.054975871827661;
Ok((
vec![[a1, a1], [b1, a1], [a1, b1], [a2, a2], [b2, a2], [a2, b2]],
vec![w1, w1, w1, w2, w2, w2],
))
}
_ => {
let n_1d = n.max(2);
let (gl_nodes, gl_weights) = gauss_legendre_1d(n_1d);
let mut pts = Vec::new();
let mut wts = Vec::new();
for (i, &r_ref) in gl_nodes.iter().enumerate() {
let r = (r_ref + 1.0) * 0.5;
let wr = gl_weights[i] * 0.5;
for (j, &s_ref) in gl_nodes.iter().enumerate() {
let s = (s_ref + 1.0) * 0.5;
let ws = gl_weights[j] * 0.5;
let xi_pt = r;
let eta_pt = (1.0 - r) * s;
let jac = (1.0 - r).max(0.0);
pts.push([xi_pt, eta_pt]);
wts.push(wr * ws * jac);
}
}
Ok((pts, wts))
}
}
}
pub fn gauss_legendre_1d(n: usize) -> (Vec<f64>, Vec<f64>) {
match n {
1 => (vec![0.0], vec![2.0]),
2 => (
vec![-0.5773502691896257, 0.5773502691896257],
vec![1.0, 1.0],
),
3 => (
vec![-0.7745966692414834, 0.0, 0.7745966692414834],
vec![
0.5555555555555556,
0.888_888_888_888_889,
0.5555555555555556,
],
),
4 => (
vec![
-0.8611363115940526,
-0.3399810435848563,
0.3399810435848563,
0.8611363115940526,
],
vec![
0.3478548451374538,
0.6521451548625461,
0.6521451548625461,
0.3478548451374538,
],
),
5 => (
vec![
-0.906_179_845_938_664,
-0.5384693101056831,
0.0,
0.5384693101056831,
0.906_179_845_938_664,
],
vec![
0.2369268850561891,
0.4786286704993665,
0.5688888888888889,
0.4786286704993665,
0.2369268850561891,
],
),
6 => (
vec![
-0.932_469_514_203_152,
-0.6612093864662645,
-0.2386191860831969,
0.2386191860831969,
0.6612093864662645,
0.932_469_514_203_152,
],
vec![
0.1713244923791704,
0.3607615730481386,
0.467_913_934_572_691,
0.467_913_934_572_691,
0.3607615730481386,
0.1713244923791704,
],
),
7 => (
vec![
-0.9491079123427585,
-0.7415311855993945,
-0.4058451513773972,
0.0,
0.4058451513773972,
0.7415311855993945,
0.9491079123427585,
],
vec![
0.1294849661688697,
0.2797053914892767,
0.3818300505051189,
0.4179591836734694,
0.3818300505051189,
0.2797053914892767,
0.1294849661688697,
],
),
_ => {
gauss_legendre_newton(n)
}
}
}
fn gauss_legendre_newton(n: usize) -> (Vec<f64>, Vec<f64>) {
let mut nodes = vec![0.0_f64; n];
let mut weights = vec![0.0_f64; n];
let nf = n as f64;
let n_half = n.div_ceil(2);
for k in 0..n_half {
let theta = std::f64::consts::PI * (k as f64 + 0.75) / (nf + 0.5);
let mut x = -theta.cos();
for _ in 0..100 {
let mut p0 = 1.0_f64;
let mut p1 = x;
for j in 2..=n {
let jf = j as f64;
let p2 = ((2.0 * jf - 1.0) * x * p1 - (jf - 1.0) * p0) / jf;
p0 = p1;
p1 = p2;
}
let pn = p1;
let pn1 = p0;
let dpn = if (x * x - 1.0).abs() > 1e-12 {
nf * (x * pn - pn1) / (x * x - 1.0)
} else {
let s = if x > 0.0 {
1.0
} else {
(-1_f64).powi(n as i32 + 1)
};
s * nf * (nf + 1.0) * 0.5
};
if dpn.abs() < 1e-300 {
break;
}
let dx = -pn / dpn;
x += dx;
if dx.abs() < 1e-15 * x.abs().max(1.0) {
break;
}
}
let mut p0 = 1.0_f64;
let mut p1 = x;
for j in 2..=n {
let jf = j as f64;
let p2 = ((2.0 * jf - 1.0) * x * p1 - (jf - 1.0) * p0) / jf;
p0 = p1;
p1 = p2;
}
let pn = p1;
let pn1 = p0;
let dpn = if (x * x - 1.0).abs() > 1e-12 {
nf * (x * pn - pn1) / (x * x - 1.0)
} else {
let s = if x > 0.0 {
1.0
} else {
(-1_f64).powi(n as i32 + 1)
};
s * nf * (nf + 1.0) * 0.5
};
let w = 2.0 / ((1.0 - x * x) * dpn * dpn);
let mirror = n - 1 - k;
nodes[k] = -x;
nodes[mirror] = x;
weights[k] = w;
weights[mirror] = w;
}
(nodes, weights)
}
pub fn curved_quad_points_weights(
element: &CurvedElement,
n: usize,
) -> IntegrateResult<(Vec<[f64; 2]>, Vec<f64>)> {
let order = match element.vertices.len() {
1 => 0,
3 => 1,
6 => 2,
10 => 3,
_ => {
return Err(IntegrateError::ValueError(format!(
"curved_quad_points_weights: unexpected vertex count {}",
element.vertices.len()
)))
}
};
let (ref_pts, ref_wts) = gauss_triangle_dunavant(n)?;
let mut phys_pts = Vec::with_capacity(ref_pts.len());
let mut phys_wts = Vec::with_capacity(ref_pts.len());
for (i, &rp) in ref_pts.iter().enumerate() {
let phys = isoparametric_map(rp, &element.vertices, order)?;
let j = jacobian(rp, &element.vertices, order)?;
let det = det_jacobian(&j).abs();
phys_pts.push(phys);
phys_wts.push(ref_wts[i] * det);
}
Ok((phys_pts, phys_wts))
}
#[cfg(test)]
mod tests {
use super::*;
use std::f64::consts::PI;
#[test]
fn test_isoparametric_map_linear() {
let v0 = [0.0, 0.0];
let v1 = [2.0, 0.0];
let v2 = [0.0, 3.0];
let nodes = vec![v0, v1, v2];
let p0 = isoparametric_map([0.0, 0.0], &nodes, 1).unwrap();
assert!((p0[0] - v0[0]).abs() < 1e-12 && (p0[1] - v0[1]).abs() < 1e-12);
let p1 = isoparametric_map([1.0, 0.0], &nodes, 1).unwrap();
assert!((p1[0] - v1[0]).abs() < 1e-12 && (p1[1] - v1[1]).abs() < 1e-12);
let p2 = isoparametric_map([0.0, 1.0], &nodes, 1).unwrap();
assert!((p2[0] - v2[0]).abs() < 1e-12 && (p2[1] - v2[1]).abs() < 1e-12);
}
#[test]
fn test_jacobian_constant() {
let v0 = [0.0, 0.0];
let v1 = [1.0, 0.0];
let v2 = [0.0, 1.0];
let nodes = vec![v0, v1, v2];
let j0 = jacobian([0.0, 0.0], &nodes, 1).unwrap();
let j1 = jacobian([0.5, 0.0], &nodes, 1).unwrap();
let j2 = jacobian([0.25, 0.25], &nodes, 1).unwrap();
for (i, row) in j0.iter().enumerate() {
for (k, &val) in row.iter().enumerate() {
assert!((val - j1[i][k]).abs() < 1e-12);
assert!((val - j2[i][k]).abs() < 1e-12);
}
}
}
#[test]
fn test_det_jacobian_positive() {
let v0 = [0.0, 0.0];
let v1 = [1.0, 0.0];
let v2 = [0.0, 1.0];
let nodes = vec![v0, v1, v2];
let j = jacobian([1.0 / 3.0, 1.0 / 3.0], &nodes, 1).unwrap();
let det = det_jacobian(&j);
assert!(
det > 0.0,
"Jacobian determinant should be positive for CCW orientation"
);
}
#[test]
fn test_arc_boundary_parameterization() {
let center = [0.0, 0.0];
let r = 2.0;
for i in 0..10 {
let t = i as f64 / 9.0;
let pt = arc_boundary_map(t, center, r, 0.0, PI / 2.0);
let dist = (pt[0] * pt[0] + pt[1] * pt[1]).sqrt();
assert!((dist - r).abs() < 1e-12, "Point should lie on circle");
}
}
#[test]
fn test_blended_transfinite_corners() {
let edges: [[f64; 2]; 4] = [[0.0, 0.0], [1.0, 0.0], [1.0, 1.0], [0.0, 1.0]];
let p00 = blended_transfinite_interpolation(-1.0, -1.0, &edges);
assert!((p00[0] - edges[0][0]).abs() < 1e-12);
assert!((p00[1] - edges[0][1]).abs() < 1e-12);
let p10 = blended_transfinite_interpolation(1.0, -1.0, &edges);
assert!((p10[0] - edges[1][0]).abs() < 1e-12);
assert!((p10[1] - edges[1][1]).abs() < 1e-12);
let p11 = blended_transfinite_interpolation(1.0, 1.0, &edges);
assert!((p11[0] - edges[2][0]).abs() < 1e-12);
assert!((p11[1] - edges[2][1]).abs() < 1e-12);
let p01 = blended_transfinite_interpolation(-1.0, 1.0, &edges);
assert!((p01[0] - edges[3][0]).abs() < 1e-12);
assert!((p01[1] - edges[3][1]).abs() < 1e-12);
}
#[test]
fn test_curved_quad_points_count() {
let element = CurvedElement::linear_triangle([0.0, 0.0], [1.0, 0.0], [0.0, 1.0]);
let (pts, wts) = curved_quad_points_weights(&element, 3).unwrap();
assert_eq!(pts.len(), 3, "3-point rule should yield 3 points");
assert_eq!(wts.len(), 3);
}
}