use crate::error::{IntegrateError, IntegrateResult};
use super::bspline::BSplineBasis;
fn gauss_solve(a: &mut Vec<Vec<f64>>, b: &mut Vec<f64>, n: usize) -> IntegrateResult<Vec<f64>> {
for col in 0..n {
let mut max_row = col;
let mut max_val = a[col][col].abs();
for row in (col + 1)..n {
let v = a[row][col].abs();
if v > max_val {
max_val = v;
max_row = row;
}
}
if max_val < 1e-300 {
return Err(IntegrateError::LinearSolveError(
"Near-singular stiffness matrix in IGA solve".to_string(),
));
}
a.swap(col, max_row);
b.swap(col, max_row);
let pivot = a[col][col];
for row in (col + 1)..n {
let factor = a[row][col] / pivot;
for k in col..n {
let sub = factor * a[col][k];
a[row][k] -= sub;
}
let sub_b = factor * b[col];
b[row] -= sub_b;
}
}
let mut x = vec![0.0_f64; n];
for i in (0..n).rev() {
let mut s = b[i];
for j in (i + 1)..n {
s -= a[i][j] * x[j];
}
x[i] = s / a[i][i];
}
Ok(x)
}
fn gauss_legendre(n: usize) -> (Vec<f64>, Vec<f64>) {
match n {
1 => (vec![0.0], vec![2.0]),
2 => (vec![-0.577_350_269_189_625_8, 0.577_350_269_189_625_8], vec![1.0, 1.0]),
3 => (
vec![-0.774_596_669_241_483_4, 0.0, 0.774_596_669_241_483_4],
vec![0.555_555_555_555_555_6, 0.888_888_888_888_888_9, 0.555_555_555_555_555_6],
),
4 => (
vec![
-0.861_136_311_594_052_6, -0.339_981_043_584_856_3,
0.339_981_043_584_856_3, 0.861_136_311_594_052_6,
],
vec![
0.347_854_845_137_453_8, 0.652_145_154_862_546_1,
0.652_145_154_862_546_1, 0.347_854_845_137_453_8,
],
),
_ => gauss_legendre(4),
}
}
#[derive(Debug, Clone)]
pub struct IGASolver1DConfig {
pub n_gauss: usize,
}
impl Default for IGASolver1DConfig {
fn default() -> Self {
Self { n_gauss: 4 }
}
}
#[derive(Debug, Clone)]
pub struct IGASolution1D {
pub coefficients: Vec<f64>,
pub basis: BSplineBasis,
}
impl IGASolution1D {
pub fn eval(&self, t: f64) -> f64 {
let (span, n_vals) = self.basis.eval_basis_functions(t);
let p = self.basis.degree;
let start = if span >= p { span - p } else { 0 };
let mut val = 0.0_f64;
for (k, &n_k) in n_vals.iter().enumerate() {
let idx = start + k;
if idx < self.coefficients.len() {
val += n_k * self.coefficients[idx];
}
}
val
}
pub fn eval_deriv(&self, t: f64) -> f64 {
let (span, dn_vals) = self.basis.eval_basis_derivatives(t);
let p = self.basis.degree;
let start = if span >= p { span - p } else { 0 };
let mut val = 0.0_f64;
for (k, &dn_k) in dn_vals.iter().enumerate() {
let idx = start + k;
if idx < self.coefficients.len() {
val += dn_k * self.coefficients[idx];
}
}
val
}
}
pub struct IGASolver1D {
basis: BSplineBasis,
cfg: IGASolver1DConfig,
}
impl IGASolver1D {
pub fn new(degree: usize, n_elements: usize, cfg: IGASolver1DConfig) -> IntegrateResult<Self> {
let basis = BSplineBasis::uniform_open(degree, n_elements + degree)?;
Ok(Self { basis, cfg })
}
pub fn from_basis(basis: BSplineBasis, cfg: IGASolver1DConfig) -> Self {
Self { basis, cfg }
}
fn assemble_stiffness<A>(&self, a_coeff: &A) -> Vec<Vec<f64>>
where
A: Fn(f64) -> f64,
{
let n = self.basis.n_basis;
let mut k_mat = vec![vec![0.0_f64; n]; n];
let knots = &self.basis.knots;
let p = self.basis.degree;
let (xi_ref, w_ref) = gauss_legendre(self.cfg.n_gauss);
for span_idx in p..=(n - 1) {
let ta = knots[span_idx];
let tb = knots[span_idx + 1];
if (tb - ta).abs() < 1e-15 {
continue; }
let half = (tb - ta) * 0.5;
let mid = (ta + tb) * 0.5;
for (&xi, &w) in xi_ref.iter().zip(w_ref.iter()) {
let t = mid + half * xi;
let jac = half;
let a_val = a_coeff(t);
let (_, dn_vals) = self.basis.eval_basis_derivatives(t);
let start = if span_idx >= p { span_idx - p } else { 0 };
for (ki, &dn_i) in dn_vals.iter().enumerate() {
let i = start + ki;
if i >= n { continue; }
for (kj, &dn_j) in dn_vals.iter().enumerate() {
let j = start + kj;
if j >= n { continue; }
k_mat[i][j] += a_val * dn_i * dn_j * w * jac;
}
}
}
}
k_mat
}
fn assemble_load<F>(&self, f_rhs: &F) -> Vec<f64>
where
F: Fn(f64) -> f64,
{
let n = self.basis.n_basis;
let mut f_vec = vec![0.0_f64; n];
let knots = &self.basis.knots;
let p = self.basis.degree;
let (xi_ref, w_ref) = gauss_legendre(self.cfg.n_gauss);
for span_idx in p..=(n - 1) {
let ta = knots[span_idx];
let tb = knots[span_idx + 1];
if (tb - ta).abs() < 1e-15 { continue; }
let half = (tb - ta) * 0.5;
let mid = (ta + tb) * 0.5;
for (&xi, &w) in xi_ref.iter().zip(w_ref.iter()) {
let t = mid + half * xi;
let jac = half;
let f_val = f_rhs(t);
let (_, n_vals) = self.basis.eval_basis_functions(t);
let start = if span_idx >= p { span_idx - p } else { 0 };
for (ki, &n_k) in n_vals.iter().enumerate() {
let i = start + ki;
if i < n {
f_vec[i] += f_val * n_k * w * jac;
}
}
}
}
f_vec
}
pub fn solve<A, F>(
&self,
a_coeff: &A,
f_rhs: &F,
u0: f64,
u1: f64,
) -> IntegrateResult<IGASolution1D>
where
A: Fn(f64) -> f64,
F: Fn(f64) -> f64,
{
let n = self.basis.n_basis;
if n < 2 {
return Err(IntegrateError::InvalidInput(
"Need at least 2 basis functions".to_string(),
));
}
let mut k_mat = self.assemble_stiffness(a_coeff);
let mut f_vec = self.assemble_load(f_rhs);
for i in 1..(n - 1) {
f_vec[i] -= k_mat[i][0] * u0 + k_mat[i][n - 1] * u1;
}
let n_free = n - 2;
if n_free == 0 {
let mut coeffs = vec![0.0_f64; n];
coeffs[0] = u0;
coeffs[n - 1] = u1;
return Ok(IGASolution1D {
coefficients: coeffs,
basis: self.basis.clone(),
});
}
let mut k_free = vec![vec![0.0_f64; n_free]; n_free];
let mut f_free = vec![0.0_f64; n_free];
for i in 0..n_free {
f_free[i] = f_vec[i + 1];
for j in 0..n_free {
k_free[i][j] = k_mat[i + 1][j + 1];
}
}
let u_free = gauss_solve(&mut k_free, &mut f_free, n_free)?;
let mut coeffs = vec![0.0_f64; n];
coeffs[0] = u0;
for i in 0..n_free {
coeffs[i + 1] = u_free[i];
}
coeffs[n - 1] = u1;
Ok(IGASolution1D {
coefficients: coeffs,
basis: self.basis.clone(),
})
}
}
#[derive(Debug, Clone)]
pub struct IGASolution2D {
pub coefficients: Vec<Vec<f64>>,
pub basis_u: BSplineBasis,
pub basis_v: BSplineBasis,
}
impl IGASolution2D {
pub fn eval(&self, u: f64, v: f64) -> f64 {
let (span_u, n_u) = self.basis_u.eval_basis_functions(u);
let (span_v, n_v) = self.basis_v.eval_basis_functions(v);
let pu = self.basis_u.degree;
let pv = self.basis_v.degree;
let start_u = if span_u >= pu { span_u - pu } else { 0 };
let start_v = if span_v >= pv { span_v - pv } else { 0 };
let mut val = 0.0_f64;
for (ki, &n_ui) in n_u.iter().enumerate() {
let i = start_u + ki;
if i >= self.coefficients.len() { continue; }
for (kj, &n_vj) in n_v.iter().enumerate() {
let j = start_v + kj;
if j < self.coefficients[i].len() {
val += n_ui * n_vj * self.coefficients[i][j];
}
}
}
val
}
}
pub struct IGASolver2D {
basis_u: BSplineBasis,
basis_v: BSplineBasis,
cfg: IGASolver1DConfig,
}
impl IGASolver2D {
pub fn new(
degree: usize,
n_elements_u: usize,
n_elements_v: usize,
cfg: IGASolver1DConfig,
) -> IntegrateResult<Self> {
let basis_u = BSplineBasis::uniform_open(degree, n_elements_u + degree)?;
let basis_v = BSplineBasis::uniform_open(degree, n_elements_v + degree)?;
Ok(Self { basis_u, basis_v, cfg })
}
pub fn solve<F>(&self, f_rhs: &F) -> IntegrateResult<IGASolution2D>
where
F: Fn(f64, f64) -> f64,
{
let nu = self.basis_u.n_basis;
let nv = self.basis_v.n_basis;
let n_total = nu * nv;
let knots_u = &self.basis_u.knots;
let knots_v = &self.basis_v.knots;
let pu = self.basis_u.degree;
let pv = self.basis_v.degree;
let (xi_ref, w_ref) = gauss_legendre(self.cfg.n_gauss);
let idx = |i: usize, j: usize| i * nv + j;
let is_boundary = |i: usize, j: usize| i == 0 || i == nu - 1 || j == 0 || j == nv - 1;
let mut k_global = vec![vec![0.0_f64; n_total]; n_total];
let mut f_global = vec![0.0_f64; n_total];
for span_i in pu..=(nu - 1) {
let ua = knots_u[span_i];
let ub = knots_u[span_i + 1];
if (ub - ua).abs() < 1e-15 { continue; }
let half_u = (ub - ua) * 0.5;
let mid_u = (ua + ub) * 0.5;
for span_j in pv..=(nv - 1) {
let va = knots_v[span_j];
let vb = knots_v[span_j + 1];
if (vb - va).abs() < 1e-15 { continue; }
let half_v = (vb - va) * 0.5;
let mid_v = (va + vb) * 0.5;
for (&xi_u, &w_u) in xi_ref.iter().zip(w_ref.iter()) {
let u_pt = mid_u + half_u * xi_u;
let (_, n_u) = self.basis_u.eval_basis_functions(u_pt);
let (_, dn_u) = self.basis_u.eval_basis_derivatives(u_pt);
let start_u = if span_i >= pu { span_i - pu } else { 0 };
for (&xi_v, &w_v) in xi_ref.iter().zip(w_ref.iter()) {
let v_pt = mid_v + half_v * xi_v;
let (_, n_v) = self.basis_v.eval_basis_functions(v_pt);
let (_, dn_v) = self.basis_v.eval_basis_derivatives(v_pt);
let start_v = if span_j >= pv { span_j - pv } else { 0 };
let jac = half_u * half_v * w_u * w_v;
let f_val = f_rhs(u_pt, v_pt);
for (ki, (&n_ui, &dn_ui)) in n_u.iter().zip(dn_u.iter()).enumerate() {
let i = start_u + ki;
if i >= nu { continue; }
for (kj, (&n_vj, &dn_vj)) in n_v.iter().zip(dn_v.iter()).enumerate() {
let j = start_v + kj;
if j >= nv { continue; }
let row = idx(i, j);
f_global[row] += f_val * n_ui * n_vj * jac;
for (ki2, (&n_ui2, &dn_ui2)) in n_u.iter().zip(dn_u.iter()).enumerate() {
let i2 = start_u + ki2;
if i2 >= nu { continue; }
for (kj2, (&n_vj2, &dn_vj2)) in n_v.iter().zip(dn_v.iter()).enumerate() {
let j2 = start_v + kj2;
if j2 >= nv { continue; }
let col = idx(i2, j2);
let k_val = (dn_ui * n_vj) * (dn_ui2 * n_vj2)
+ (n_ui * dn_vj) * (n_ui2 * dn_vj2);
k_global[row][col] += k_val * jac;
}
}
}
}
}
}
}
}
for i in 0..nu {
for j in 0..nv {
if is_boundary(i, j) {
let row = idx(i, j);
for k in 0..n_total {
k_global[row][k] = 0.0;
k_global[k][row] = 0.0;
}
k_global[row][row] = 1.0;
f_global[row] = 0.0;
}
}
}
let u_flat = gauss_solve(&mut k_global, &mut f_global, n_total)?;
let mut coeffs = vec![vec![0.0_f64; nv]; nu];
for i in 0..nu {
for j in 0..nv {
coeffs[i][j] = u_flat[idx(i, j)];
}
}
Ok(IGASolution2D {
coefficients: coeffs,
basis_u: self.basis_u.clone(),
basis_v: self.basis_v.clone(),
})
}
}
pub struct IGASolver;
impl IGASolver {
pub fn solver_1d(degree: usize, n_elements: usize) -> IntegrateResult<IGASolver1D> {
IGASolver1D::new(degree, n_elements, IGASolver1DConfig::default())
}
pub fn solver_2d(
degree: usize,
n_elements_u: usize,
n_elements_v: usize,
) -> IntegrateResult<IGASolver2D> {
IGASolver2D::new(degree, n_elements_u, n_elements_v, IGASolver1DConfig::default())
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_iga_1d_poisson_uniform() {
let solver = IGASolver1D::new(3, 8, IGASolver1DConfig { n_gauss: 4 })
.expect("IGA 1D solver creation");
let a = |_x: f64| 1.0_f64;
let f = |x: f64| std::f64::consts::PI * std::f64::consts::PI * (std::f64::consts::PI * x).sin();
let sol = solver.solve(&a, &f, 0.0, 0.0).expect("IGA 1D solve");
let test_pts = [0.25, 0.5, 0.75];
for &x in &test_pts {
let u_iga = sol.eval(x);
let u_exact = (std::f64::consts::PI * x).sin();
let err = (u_iga - u_exact).abs();
assert!(
err < 0.05,
"IGA 1D u({x}) = {u_iga:.6}, exact = {u_exact:.6}, err = {err:.2e}"
);
}
}
#[test]
fn test_iga_1d_variable_coeff() {
let solver = IGASolver1D::new(2, 4, IGASolver1DConfig { n_gauss: 3 })
.expect("IGA 1D creation");
let a = |_x: f64| 2.0_f64;
let f = |_x: f64| -2.0_f64;
let sol = solver.solve(&a, &f, 0.0, 1.0).expect("IGA 1D variable coeff solve");
for k in 0..5 {
let x = k as f64 * 0.2 + 0.1;
let u_iga = sol.eval(x);
let u_exact = x * (x + 1.0) / 2.0;
let err = (u_iga - u_exact).abs();
assert!(
err < 0.05,
"IGA 1D variable coeff u({x:.1}) = {u_iga:.6}, exact = {u_exact:.6}"
);
}
}
#[test]
fn test_iga_1d_solution_derivative() {
let solver = IGASolver1D::new(3, 6, IGASolver1DConfig::default())
.expect("IGA 1D creation");
let a = |_x: f64| 1.0_f64;
let pi = std::f64::consts::PI;
let f = |x: f64| pi * pi * (pi * x).sin();
let sol = solver.solve(&a, &f, 0.0, 0.0).expect("solve");
let x = 0.3;
let du_iga = sol.eval_deriv(x);
let du_exact = pi * (pi * x).cos();
let err = (du_iga - du_exact).abs();
assert!(err < 0.3, "Derivative error = {err:.4} (too large)");
}
}