use crate::sparse::CsrMatrix;
#[derive(Debug, Clone)]
pub struct DirichletBc {
pub node: usize,
pub dof: usize,
pub value: f64,
}
impl DirichletBc {
pub fn new(node: usize, dof: usize, value: f64) -> Self {
Self { node, dof, value }
}
pub fn global_dof(&self) -> usize {
self.node * 3 + self.dof
}
}
#[derive(Debug, Clone)]
pub struct NeumannBc {
pub node: usize,
pub force: [f64; 3],
}
impl NeumannBc {
pub fn new(node: usize, force: [f64; 3]) -> Self {
Self { node, force }
}
}
#[derive(Debug, Clone)]
pub struct DirichletBC {
pub node_id: usize,
pub dof: usize,
pub value: f64,
}
impl DirichletBC {
pub fn new(node_id: usize, dof: usize, value: f64) -> Self {
Self {
node_id,
dof,
value,
}
}
}
#[derive(Debug, Clone)]
pub struct NeumannBC {
pub node_id: usize,
pub dof: usize,
pub force: f64,
}
impl NeumannBC {
pub fn new(node_id: usize, dof: usize, force: f64) -> Self {
Self {
node_id,
dof,
force,
}
}
}
#[derive(Debug, Clone)]
pub struct RobinBC {
pub node_id: usize,
pub dof: usize,
pub stiffness: f64,
pub value: f64,
}
impl RobinBC {
pub fn new(node_id: usize, dof: usize, stiffness: f64, value: f64) -> Self {
Self {
node_id,
dof,
stiffness,
value,
}
}
pub fn global_dof(&self, dofs_per_node: usize) -> usize {
self.node_id * dofs_per_node + self.dof
}
}
#[derive(Debug, Clone)]
pub struct RobinBCImproved {
pub node_id: usize,
pub dof: usize,
pub alpha: f64,
pub beta: f64,
pub g: f64,
pub damping: f64,
}
impl RobinBCImproved {
pub fn new(node_id: usize, dof: usize, alpha: f64, beta: f64, g: f64) -> Self {
Self {
node_id,
dof,
alpha,
beta,
g,
damping: 0.0,
}
}
pub fn with_damping(mut self, damping: f64) -> Self {
self.damping = damping;
self
}
pub fn global_dof(&self, dofs_per_node: usize) -> usize {
self.node_id * dofs_per_node + self.dof
}
pub fn apply_dense(&self, k: &mut [f64], f: &mut [f64], n_dofs: usize, dofs_per_node: usize) {
let i = self.global_dof(dofs_per_node);
assert!(i < n_dofs, "Robin DOF {i} out of range {n_dofs}");
k[i * n_dofs + i] += self.alpha;
f[i] += self.g;
}
pub fn apply_damping(&self, c_diag: &mut [f64], dofs_per_node: usize) {
let i = self.global_dof(dofs_per_node);
if i < c_diag.len() {
c_diag[i] += self.damping;
}
}
}
#[derive(Debug, Clone)]
pub struct PeriodicBC {
pub master_node: usize,
pub slave_node: usize,
pub dof: usize,
pub offset: f64,
}
impl PeriodicBC {
pub fn new(master_node: usize, slave_node: usize, dof: usize) -> Self {
Self {
master_node,
slave_node,
dof,
offset: 0.0,
}
}
pub fn with_offset(master_node: usize, slave_node: usize, dof: usize, offset: f64) -> Self {
Self {
master_node,
slave_node,
dof,
offset,
}
}
pub fn master_global_dof(&self) -> usize {
self.master_node * 3 + self.dof
}
pub fn slave_global_dof(&self) -> usize {
self.slave_node * 3 + self.dof
}
}
pub fn apply_periodic_dense_penalty(
k: &mut [f64],
f: &mut [f64],
n_dofs: usize,
bcs: &[PeriodicBC],
penalty: f64,
) {
for bc in bcs {
let m = bc.master_global_dof();
let s = bc.slave_global_dof();
assert!(m < n_dofs && s < n_dofs);
k[s * n_dofs + s] += penalty;
k[m * n_dofs + m] += penalty;
k[s * n_dofs + m] -= penalty;
k[m * n_dofs + s] -= penalty;
f[s] += penalty * bc.offset;
f[m] -= penalty * bc.offset;
}
}
pub fn apply_periodic_dense_elimination(
k: &mut [f64],
f: &mut [f64],
n_dofs: usize,
bcs: &[PeriodicBC],
) {
for bc in bcs {
let m = bc.master_global_dof();
let s = bc.slave_global_dof();
assert!(m < n_dofs && s < n_dofs);
for j in 0..n_dofs {
if j != s {
k[m * n_dofs + j] += k[s * n_dofs + j];
k[j * n_dofs + m] += k[j * n_dofs + s];
}
}
k[m * n_dofs + m] += k[s * n_dofs + s];
f[m] += f[s];
for j in 0..n_dofs {
k[s * n_dofs + j] = 0.0;
k[j * n_dofs + s] = 0.0;
}
k[s * n_dofs + s] = 1.0;
k[s * n_dofs + m] = -1.0;
f[s] = bc.offset;
}
}
#[derive(Debug, Clone)]
pub struct SymmetryBC {
pub nodes: Vec<usize>,
pub constrained_dof: usize,
}
impl SymmetryBC {
pub fn new(nodes: Vec<usize>, constrained_dof: usize) -> Self {
assert!(constrained_dof < 3, "DOF must be 0, 1, or 2");
Self {
nodes,
constrained_dof,
}
}
pub fn to_dirichlet_bcs(&self) -> Vec<DirichletBC> {
self.nodes
.iter()
.map(|&node| DirichletBC::new(node, self.constrained_dof, 0.0))
.collect()
}
pub fn apply_dense(&self, k: &mut [f64], f: &mut [f64], n_dofs: usize) {
let bcs = self.to_dirichlet_bcs();
apply_dirichlet_dense(k, f, n_dofs, &bcs);
}
}
#[derive(Debug, Clone)]
pub enum TimeFunction {
Constant(f64),
Ramp {
ramp_time: f64,
},
Sinusoidal {
frequency: f64,
},
Step {
step_time: f64,
},
Triangular {
period: f64,
},
ExponentialDecay {
decay_rate: f64,
},
}
impl TimeFunction {
pub fn evaluate(&self, t: f64) -> f64 {
match self {
TimeFunction::Constant(c) => *c,
TimeFunction::Ramp { ramp_time } => {
if *ramp_time <= 0.0 {
1.0
} else {
(t / ramp_time).clamp(0.0, 1.0)
}
}
TimeFunction::Sinusoidal { frequency } => {
(2.0 * std::f64::consts::PI * frequency * t).sin()
}
TimeFunction::Step { step_time } => {
if t >= *step_time {
1.0
} else {
0.0
}
}
TimeFunction::Triangular { period } => {
if *period <= 0.0 {
return 0.0;
}
let phase = (t / period) % 1.0;
if phase < 0.5 {
4.0 * phase - 1.0
} else {
3.0 - 4.0 * phase
}
}
TimeFunction::ExponentialDecay { decay_rate } => (-decay_rate * t).exp(),
}
}
}
#[derive(Debug, Clone)]
pub struct TimeDependentBC {
pub node_id: usize,
pub dof: usize,
pub amplitude: f64,
pub time_function: TimeFunction,
}
impl TimeDependentBC {
pub fn new(node_id: usize, dof: usize, amplitude: f64, time_function: TimeFunction) -> Self {
Self {
node_id,
dof,
amplitude,
time_function,
}
}
pub fn value_at(&self, t: f64) -> f64 {
self.amplitude * self.time_function.evaluate(t)
}
pub fn to_dirichlet_bc(&self, t: f64) -> DirichletBC {
DirichletBC::new(self.node_id, self.dof, self.value_at(t))
}
}
pub fn apply_time_dependent_dense(
k: &mut [f64],
f: &mut [f64],
n_dofs: usize,
bcs: &[TimeDependentBC],
t: f64,
) {
let dirichlet_bcs: Vec<DirichletBC> = bcs.iter().map(|bc| bc.to_dirichlet_bc(t)).collect();
apply_dirichlet_dense(k, f, n_dofs, &dirichlet_bcs);
}
pub fn apply_time_dependent_sparse(
k: &mut CsrMatrix,
f: &mut [f64],
bcs: &[TimeDependentBC],
t: f64,
) {
let dirichlet_bcs: Vec<DirichletBc> = bcs
.iter()
.map(|bc| DirichletBc::new(bc.node_id, bc.dof, bc.value_at(t)))
.collect();
apply_dirichlet(k, f, &dirichlet_bcs);
}
#[derive(Debug, Clone)]
pub struct ContactBC {
pub node_pairs: Vec<(usize, usize)>,
pub gap: f64,
pub penalty: f64,
}
impl ContactBC {
pub fn new(node_pairs: Vec<(usize, usize)>, gap: f64, penalty: f64) -> Self {
Self {
node_pairs,
gap,
penalty,
}
}
}
#[derive(Debug, Clone)]
pub struct MixedBC {
pub node_id: usize,
pub dirichlet_dofs: Vec<(usize, f64)>,
pub neumann_dofs: Vec<(usize, f64)>,
}
impl MixedBC {
pub fn new(
node_id: usize,
dirichlet_dofs: Vec<(usize, f64)>,
neumann_dofs: Vec<(usize, f64)>,
) -> Self {
Self {
node_id,
dirichlet_dofs,
neumann_dofs,
}
}
pub fn roller(node_id: usize, fixed_dof: usize, fixed_value: f64) -> Self {
let dirichlet_dofs = vec![(fixed_dof, fixed_value)];
Self {
node_id,
dirichlet_dofs,
neumann_dofs: Vec::new(),
}
}
pub fn pin_2d(node_id: usize, free_dof: usize) -> Self {
let mut dirichlet_dofs = Vec::new();
for d in 0..3 {
if d != free_dof {
dirichlet_dofs.push((d, 0.0));
}
}
Self {
node_id,
dirichlet_dofs,
neumann_dofs: Vec::new(),
}
}
pub fn to_separate_bcs(&self) -> (Vec<DirichletBC>, Vec<NeumannBC>) {
let dirichlet: Vec<DirichletBC> = self
.dirichlet_dofs
.iter()
.map(|&(dof, val)| DirichletBC::new(self.node_id, dof, val))
.collect();
let neumann: Vec<NeumannBC> = self
.neumann_dofs
.iter()
.map(|&(dof, force)| NeumannBC::new(self.node_id, dof, force))
.collect();
(dirichlet, neumann)
}
pub fn apply_dense(&self, k: &mut [f64], f: &mut [f64], n_dofs: usize) {
let (dirichlet, neumann) = self.to_separate_bcs();
apply_neumann_scalar(f, n_dofs, &neumann);
apply_dirichlet_dense(k, f, n_dofs, &dirichlet);
}
}
pub fn apply_dirichlet_dense_penalty(
k: &mut [f64],
f: &mut [f64],
n_dofs: usize,
bcs: &[DirichletBC],
penalty: f64,
) {
for bc in bcs {
let i = bc.node_id * 3 + bc.dof; assert!(i < n_dofs, "BC DOF {i} out of range {n_dofs}");
k[i * n_dofs + i] += penalty;
f[i] += penalty * bc.value;
}
}
pub fn apply_dirichlet_dense(k: &mut [f64], f: &mut [f64], n_dofs: usize, bcs: &[DirichletBC]) {
for bc in bcs {
let dof = bc.node_id * 3 + bc.dof;
assert!(dof < n_dofs);
if bc.value.abs() > 0.0 {
for j in 0..n_dofs {
if j != dof {
f[j] -= k[j * n_dofs + dof] * bc.value;
}
}
}
for j in 0..n_dofs {
k[dof * n_dofs + j] = 0.0;
k[j * n_dofs + dof] = 0.0;
}
k[dof * n_dofs + dof] = 1.0;
f[dof] = bc.value;
}
}
pub fn apply_neumann_scalar(f: &mut [f64], n_dofs: usize, bcs: &[NeumannBC]) {
for bc in bcs {
let i = bc.node_id * 3 + bc.dof;
assert!(i < n_dofs, "Neumann DOF {i} out of range");
f[i] += bc.force;
}
}
pub fn apply_robin(k: &mut [f64], f: &mut [f64], n_dofs: usize, bcs: &[RobinBC]) {
for bc in bcs {
let i = bc.global_dof(3);
assert!(i < n_dofs, "Robin DOF {i} out of range");
k[i * n_dofs + i] += bc.stiffness;
f[i] += bc.stiffness * bc.value;
}
}
pub fn apply_robin_improved(
k: &mut [f64],
f: &mut [f64],
n_dofs: usize,
bcs: &[RobinBCImproved],
dofs_per_node: usize,
) {
for bc in bcs {
bc.apply_dense(k, f, n_dofs, dofs_per_node);
}
}
pub fn apply_dirichlet_sparse_penalty(
k: &mut CsrMatrix,
f: &mut [f64],
bcs: &[DirichletBc],
penalty: f64,
) {
for bc in bcs {
let dof = bc.global_dof();
assert!(dof < k.nrows, "BC DOF {dof} out of range");
let start = k.row_ptr[dof];
let end = k.row_ptr[dof + 1];
for idx in start..end {
if k.col_indices[idx] == dof {
k.values[idx] += penalty;
break;
}
}
f[dof] += penalty * bc.value;
}
}
pub fn apply_robin_sparse(k: &mut CsrMatrix, f: &mut [f64], bcs: &[RobinBC], dofs_per_node: usize) {
for bc in bcs {
let i = bc.global_dof(dofs_per_node);
assert!(i < k.nrows, "Robin DOF {i} out of range");
let start = k.row_ptr[i];
let end = k.row_ptr[i + 1];
for idx in start..end {
if k.col_indices[idx] == i {
k.values[idx] += bc.stiffness;
break;
}
}
f[i] += bc.stiffness * bc.value;
}
}
pub fn apply_neumann_sparse(f: &mut [f64], bcs: &[NeumannBC]) {
for bc in bcs {
let i = bc.node_id * 3 + bc.dof;
if i < f.len() {
f[i] += bc.force;
}
}
}
#[derive(Debug, Default)]
pub struct BoundarySet {
dirichlet: Vec<DirichletBC>,
neumann: Vec<NeumannBC>,
robin: Vec<RobinBC>,
periodic: Vec<PeriodicBC>,
symmetry: Vec<SymmetryBC>,
time_dependent: Vec<TimeDependentBC>,
mixed: Vec<MixedBC>,
}
impl BoundarySet {
pub fn new() -> Self {
Self::default()
}
pub fn add_dirichlet(&mut self, bc: DirichletBC) {
self.dirichlet.push(bc);
}
pub fn add_neumann(&mut self, bc: NeumannBC) {
self.neumann.push(bc);
}
pub fn add_robin(&mut self, bc: RobinBC) {
self.robin.push(bc);
}
pub fn add_periodic(&mut self, bc: PeriodicBC) {
self.periodic.push(bc);
}
pub fn add_symmetry(&mut self, bc: SymmetryBC) {
self.symmetry.push(bc);
}
pub fn add_time_dependent(&mut self, bc: TimeDependentBC) {
self.time_dependent.push(bc);
}
pub fn add_mixed(&mut self, bc: MixedBC) {
self.mixed.push(bc);
}
pub fn count(&self) -> usize {
self.dirichlet.len()
+ self.neumann.len()
+ self.robin.len()
+ self.periodic.len()
+ self.symmetry.iter().map(|s| s.nodes.len()).sum::<usize>()
+ self.time_dependent.len()
+ self.mixed.len()
}
pub fn apply_all(&self, k: &mut [f64], f: &mut [f64], n_dofs: usize, penalty: f64) {
for bc in &self.mixed {
let (_, neumann) = bc.to_separate_bcs();
apply_neumann_scalar(f, n_dofs, &neumann);
}
apply_neumann_scalar(f, n_dofs, &self.neumann);
apply_robin(k, f, n_dofs, &self.robin);
if !self.periodic.is_empty() {
apply_periodic_dense_penalty(k, f, n_dofs, &self.periodic, penalty);
}
for sym in &self.symmetry {
let sym_bcs = sym.to_dirichlet_bcs();
apply_dirichlet_dense_penalty(k, f, n_dofs, &sym_bcs, penalty);
}
for bc in &self.mixed {
let (dirichlet, _) = bc.to_separate_bcs();
apply_dirichlet_dense_penalty(k, f, n_dofs, &dirichlet, penalty);
}
apply_dirichlet_dense_penalty(k, f, n_dofs, &self.dirichlet, penalty);
}
pub fn apply_all_at_time(
&self,
k: &mut [f64],
f: &mut [f64],
n_dofs: usize,
penalty: f64,
t: f64,
) {
self.apply_all(k, f, n_dofs, penalty);
if !self.time_dependent.is_empty() {
let td_bcs: Vec<DirichletBC> = self
.time_dependent
.iter()
.map(|bc| bc.to_dirichlet_bc(t))
.collect();
apply_dirichlet_dense_penalty(k, f, n_dofs, &td_bcs, penalty);
}
}
}
pub fn apply_dirichlet(k: &mut CsrMatrix, f: &mut [f64], bcs: &[DirichletBc]) {
for bc in bcs {
let dof = bc.global_dof();
assert!(dof < k.nrows, "BC DOF {dof} out of range");
if bc.value.abs() > 0.0 {
for (row, f_row) in f.iter_mut().enumerate().take(k.nrows) {
if row == dof {
continue;
}
let rs = k.row_ptr[row];
let re = k.row_ptr[row + 1];
for idx in rs..re {
if k.col_indices[idx] == dof {
*f_row -= k.values[idx] * bc.value;
}
}
}
}
let start = k.row_ptr[dof];
let end = k.row_ptr[dof + 1];
for idx in start..end {
if k.col_indices[idx] == dof {
k.values[idx] = 1.0;
} else {
k.values[idx] = 0.0;
}
}
for row in 0..k.nrows {
if row == dof {
continue;
}
let rs = k.row_ptr[row];
let re = k.row_ptr[row + 1];
for idx in rs..re {
if k.col_indices[idx] == dof {
k.values[idx] = 0.0;
}
}
}
f[dof] = bc.value;
}
}
pub fn apply_neumann(f: &mut [f64], bcs: &[NeumannBc]) {
for bc in bcs {
let base = bc.node * 3;
f[base] += bc.force[0];
f[base + 1] += bc.force[1];
f[base + 2] += bc.force[2];
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_dirichlet_bc_zeros_row_col() {
let triplets: Vec<(usize, usize, f64)> = vec![
(0, 0, 10.0),
(0, 1, 1.0),
(0, 2, 2.0),
(1, 0, 1.0),
(1, 1, 20.0),
(1, 2, 3.0),
(2, 0, 2.0),
(2, 1, 3.0),
(2, 2, 30.0),
];
let mut k = CsrMatrix::from_triplets(3, 3, &triplets);
let mut f = vec![100.0, 200.0, 300.0];
let bcs = vec![DirichletBc {
node: 0,
dof: 1,
value: 0.0,
}];
apply_dirichlet(&mut k, &mut f, &bcs);
assert_eq!(k.get(1, 0), 0.0);
assert!((k.get(1, 1) - 1.0).abs() < 1e-12);
assert_eq!(k.get(1, 2), 0.0);
assert_eq!(k.get(0, 1), 0.0);
assert_eq!(k.get(2, 1), 0.0);
}
#[test]
fn test_dirichlet_bc_applied() {
let triplets: Vec<(usize, usize, f64)> =
vec![(0, 0, 8.0), (0, 1, 3.0), (1, 0, 3.0), (1, 1, 5.0)];
let mut k = CsrMatrix::from_triplets(2, 2, &triplets);
let mut f = vec![16.0, 10.0];
let bcs = vec![DirichletBc {
node: 0,
dof: 0,
value: 0.0,
}];
apply_dirichlet(&mut k, &mut f, &bcs);
assert!((k.get(0, 0) - 1.0).abs() < 1e-12, "diagonal should be 1");
assert!(
k.get(0, 1).abs() < 1e-12,
"off-diagonal in row 0 should be 0"
);
assert!(
k.get(1, 0).abs() < 1e-12,
"off-diagonal in col 0 should be 0"
);
assert!(
(f[0] - 0.0).abs() < 1e-12,
"f[0] should equal prescribed value"
);
}
#[test]
fn test_neumann_bc() {
let mut f = vec![0.0; 9]; let bcs = vec![NeumannBc::new(2, [0.0, -1000.0, 0.0])];
apply_neumann(&mut f, &bcs);
assert_eq!(f[6], 0.0);
assert_eq!(f[7], -1000.0);
assert_eq!(f[8], 0.0);
}
#[test]
fn dense_dirichlet_elimination_zeros_row_col() {
let mut k = vec![4.0, 2.0, 2.0, 3.0];
let mut f = vec![8.0, 6.0];
let bcs = vec![DirichletBC::new(0, 0, 0.0)];
apply_dirichlet_dense(&mut k, &mut f, 2, &bcs);
assert!((k[0] - 1.0).abs() < 1e-12);
assert!(k[1].abs() < 1e-12);
assert!(k[2].abs() < 1e-12);
assert!((f[0]).abs() < 1e-12);
}
#[test]
fn dense_dirichlet_nonzero_value_adjusts_rhs() {
let mut k = vec![4.0, 2.0, 2.0, 3.0];
let mut f = vec![0.0, 0.0];
let bcs = vec![DirichletBC::new(0, 0, 2.0)];
apply_dirichlet_dense(&mut k, &mut f, 2, &bcs);
assert!((f[1] - (-4.0)).abs() < 1e-12, "f[1]={}", f[1]);
assert!((f[0] - 2.0).abs() < 1e-12, "f[0]={}", f[0]);
}
#[test]
fn neumann_scalar_adds_force() {
let mut f = vec![0.0; 6]; let bcs = vec![NeumannBC::new(1, 1, -500.0)]; apply_neumann_scalar(&mut f, 6, &bcs);
assert_eq!(f[4], -500.0); }
#[test]
fn robin_bc_adds_stiffness_to_diagonal() {
let mut k = vec![1.0, 0.0, 0.0, 1.0]; let mut f = vec![0.0, 0.0];
let bcs = vec![RobinBC::new(0, 0, 100.0, 0.5)];
apply_robin(&mut k, &mut f, 2, &bcs);
assert!((k[0] - 101.0).abs() < 1e-12);
assert!((f[0] - 50.0).abs() < 1e-12);
}
#[test]
fn boundary_set_apply_all_combined() {
let mut set = BoundarySet::new();
set.add_neumann(NeumannBC::new(0, 2, 10.0)); set.add_robin(RobinBC::new(0, 0, 50.0, 1.0)); set.add_dirichlet(DirichletBC::new(0, 1, 0.0));
let n = 6; let mut k = vec![0.0; n * n];
for i in 0..n {
k[i * n + i] = 1.0;
}
let mut f = vec![0.0; n];
set.apply_all(&mut k, &mut f, n, 1e12);
assert!((f[2] - 10.0).abs() < 1e-9, "f[2]={}", f[2]);
assert!((k[0] - 51.0).abs() < 1e-9, "K[0,0]={}", k[0]);
assert!(k[n + 1] > 1e11, "K[1,1]={}", k[n + 1]);
}
#[test]
fn robin_improved_applies_alpha_and_g() {
let bc = RobinBCImproved::new(0, 0, 200.0, 1.0, 50.0);
let n = 3;
let mut k = vec![0.0; n * n];
for i in 0..n {
k[i * n + i] = 1.0;
}
let mut f = vec![0.0; n];
bc.apply_dense(&mut k, &mut f, n, 3);
assert!((k[0] - 201.0).abs() < 1e-12, "K[0,0]={}", k[0]);
assert!((f[0] - 50.0).abs() < 1e-12, "f[0]={}", f[0]);
}
#[test]
fn robin_improved_damping() {
let bc = RobinBCImproved::new(0, 1, 100.0, 1.0, 0.0).with_damping(5.0);
let mut c_diag = vec![0.0; 6];
bc.apply_damping(&mut c_diag, 3);
assert!((c_diag[1] - 5.0).abs() < 1e-12);
}
#[test]
fn apply_robin_improved_batch() {
let bcs = vec![
RobinBCImproved::new(0, 0, 10.0, 1.0, 5.0),
RobinBCImproved::new(0, 1, 20.0, 1.0, 3.0),
];
let n = 6;
let mut k = vec![0.0; n * n];
let mut f = vec![0.0; n];
apply_robin_improved(&mut k, &mut f, n, &bcs, 3);
assert!((k[0] - 10.0).abs() < 1e-12);
assert!((k[n + 1] - 20.0).abs() < 1e-12);
assert!((f[0] - 5.0).abs() < 1e-12);
assert!((f[1] - 3.0).abs() < 1e-12);
}
#[test]
fn periodic_bc_penalty_ties_dofs() {
let n = 6;
let mut k = vec![0.0; n * n];
for i in 0..n {
k[i * n + i] = 10.0;
}
let mut f = vec![0.0; n];
let bcs = vec![PeriodicBC::new(0, 1, 0)]; let penalty = 1e6;
apply_periodic_dense_penalty(&mut k, &mut f, n, &bcs, penalty);
assert!((k[0] - (10.0 + penalty)).abs() < 1e-6);
assert!((k[3 * n + 3] - (10.0 + penalty)).abs() < 1e-6);
assert!((k[3] - (-penalty)).abs() < 1e-6);
assert!((k[3 * n] - (-penalty)).abs() < 1e-6);
}
#[test]
fn periodic_bc_with_offset() {
let n = 6;
let mut k = vec![0.0; n * n];
for i in 0..n {
k[i * n + i] = 10.0;
}
let mut f = vec![0.0; n];
let bcs = vec![PeriodicBC::with_offset(0, 1, 0, 2.0)];
let penalty = 1e6;
apply_periodic_dense_penalty(&mut k, &mut f, n, &bcs, penalty);
assert!((f[3] - penalty * 2.0).abs() < 1e-6);
assert!((f[0] - (-penalty * 2.0)).abs() < 1e-6);
}
#[test]
fn periodic_bc_elimination() {
let n = 6;
let mut k = vec![0.0; n * n];
for i in 0..n {
k[i * n + i] = 10.0;
}
let mut f = vec![1.0; n];
let bcs = vec![PeriodicBC::new(0, 1, 0)];
apply_periodic_dense_elimination(&mut k, &mut f, n, &bcs);
assert!((k[3 * n + 3] - 1.0).abs() < 1e-12);
assert!((k[3 * n] - (-1.0)).abs() < 1e-12);
assert!((f[3]).abs() < 1e-12); }
#[test]
fn symmetry_bc_fixes_normal_dof() {
let sym = SymmetryBC::new(vec![0, 1], 1); let bcs = sym.to_dirichlet_bcs();
assert_eq!(bcs.len(), 2);
assert_eq!(bcs[0].node_id, 0);
assert_eq!(bcs[0].dof, 1);
assert_eq!(bcs[0].value, 0.0);
assert_eq!(bcs[1].node_id, 1);
}
#[test]
fn symmetry_bc_apply_dense() {
let sym = SymmetryBC::new(vec![0], 0); let n = 3;
let mut k = vec![5.0, 1.0, 2.0, 1.0, 5.0, 1.0, 2.0, 1.0, 5.0];
let mut f = vec![10.0, 20.0, 30.0];
sym.apply_dense(&mut k, &mut f, n);
assert!((k[0] - 1.0).abs() < 1e-12);
assert!(k[1].abs() < 1e-12);
assert!(k[2].abs() < 1e-12);
assert!((f[0]).abs() < 1e-12);
}
#[test]
fn time_function_constant() {
let tf = TimeFunction::Constant(3.125);
assert!((tf.evaluate(0.0) - 3.125).abs() < 1e-12);
assert!((tf.evaluate(100.0) - 3.125).abs() < 1e-12);
}
#[test]
fn time_function_ramp() {
let tf = TimeFunction::Ramp { ramp_time: 2.0 };
assert!((tf.evaluate(0.0)).abs() < 1e-12);
assert!((tf.evaluate(1.0) - 0.5).abs() < 1e-12);
assert!((tf.evaluate(2.0) - 1.0).abs() < 1e-12);
assert!((tf.evaluate(5.0) - 1.0).abs() < 1e-12); }
#[test]
fn time_function_sinusoidal() {
let tf = TimeFunction::Sinusoidal { frequency: 1.0 };
assert!(tf.evaluate(0.0).abs() < 1e-12);
assert!((tf.evaluate(0.25) - 1.0).abs() < 1e-12); }
#[test]
fn time_function_step() {
let tf = TimeFunction::Step { step_time: 1.0 };
assert!((tf.evaluate(0.5)).abs() < 1e-12);
assert!((tf.evaluate(1.0) - 1.0).abs() < 1e-12);
assert!((tf.evaluate(2.0) - 1.0).abs() < 1e-12);
}
#[test]
fn time_function_triangular() {
let tf = TimeFunction::Triangular { period: 1.0 };
assert!((tf.evaluate(0.0) - (-1.0)).abs() < 1e-12);
assert!(tf.evaluate(0.25).abs() < 1e-12);
assert!((tf.evaluate(0.5) - 1.0).abs() < 1e-12);
}
#[test]
fn time_function_exponential_decay() {
let tf = TimeFunction::ExponentialDecay { decay_rate: 1.0 };
assert!((tf.evaluate(0.0) - 1.0).abs() < 1e-12);
assert!((tf.evaluate(1.0) - (-1.0_f64).exp()).abs() < 1e-12);
}
#[test]
fn time_dependent_bc_value_at() {
let bc = TimeDependentBC::new(0, 0, 5.0, TimeFunction::Ramp { ramp_time: 1.0 });
assert!((bc.value_at(0.0)).abs() < 1e-12);
assert!((bc.value_at(0.5) - 2.5).abs() < 1e-12);
assert!((bc.value_at(1.0) - 5.0).abs() < 1e-12);
}
#[test]
fn time_dependent_bc_to_dirichlet() {
let bc = TimeDependentBC::new(2, 1, 10.0, TimeFunction::Constant(1.0));
let dbc = bc.to_dirichlet_bc(0.0);
assert_eq!(dbc.node_id, 2);
assert_eq!(dbc.dof, 1);
assert!((dbc.value - 10.0).abs() < 1e-12);
}
#[test]
fn apply_time_dependent_dense_test() {
let bcs = vec![TimeDependentBC::new(
0,
0,
1.0,
TimeFunction::Step { step_time: 0.5 },
)];
let n = 3;
let mut k = vec![5.0, 1.0, 2.0, 1.0, 5.0, 1.0, 2.0, 1.0, 5.0];
let mut f = vec![10.0, 20.0, 30.0];
apply_time_dependent_dense(&mut k, &mut f, n, &bcs, 0.0);
assert!((f[0]).abs() < 1e-12);
let mut k2 = vec![5.0, 1.0, 2.0, 1.0, 5.0, 1.0, 2.0, 1.0, 5.0];
let mut f2 = vec![10.0, 20.0, 30.0];
apply_time_dependent_dense(&mut k2, &mut f2, n, &bcs, 1.0);
assert!((f2[0] - 1.0).abs() < 1e-12);
}
#[test]
fn mixed_bc_roller() {
let bc = MixedBC::roller(0, 1, 0.0); let (dirichlet, neumann) = bc.to_separate_bcs();
assert_eq!(dirichlet.len(), 1);
assert_eq!(dirichlet[0].dof, 1);
assert!(neumann.is_empty());
}
#[test]
fn mixed_bc_pin_2d() {
let bc = MixedBC::pin_2d(0, 2); let (dirichlet, _) = bc.to_separate_bcs();
assert_eq!(dirichlet.len(), 2);
let dofs: Vec<usize> = dirichlet.iter().map(|d| d.dof).collect();
assert!(dofs.contains(&0));
assert!(dofs.contains(&1));
assert!(!dofs.contains(&2));
}
#[test]
fn mixed_bc_apply_dense_test() {
let bc = MixedBC::new(
0,
vec![(0, 0.0)], vec![(1, -100.0)], );
let n = 3;
let mut k = vec![5.0, 1.0, 2.0, 1.0, 5.0, 1.0, 2.0, 1.0, 5.0];
let mut f = vec![0.0; n];
bc.apply_dense(&mut k, &mut f, n);
assert!((f[1] - (-100.0)).abs() < 1e-12);
assert!((k[0] - 1.0).abs() < 1e-12);
assert!((f[0]).abs() < 1e-12);
}
#[test]
fn dirichlet_sparse_penalty_adds_to_diagonal() {
let triplets: Vec<(usize, usize, f64)> =
vec![(0, 0, 10.0), (0, 1, 1.0), (1, 0, 1.0), (1, 1, 20.0)];
let mut k = CsrMatrix::from_triplets(2, 2, &triplets);
let mut f = vec![0.0; 2];
let bcs = vec![DirichletBc::new(0, 0, 3.0)];
let penalty = 1e10;
apply_dirichlet_sparse_penalty(&mut k, &mut f, &bcs, penalty);
assert!((k.get(0, 0) - (10.0 + penalty)).abs() < 1.0);
assert!((f[0] - penalty * 3.0).abs() < 1.0);
assert!((k.get(0, 1) - 1.0).abs() < 1e-12);
}
#[test]
fn robin_sparse_test() {
let triplets: Vec<(usize, usize, f64)> = vec![(0, 0, 10.0), (1, 1, 20.0), (2, 2, 30.0)];
let mut k = CsrMatrix::from_triplets(3, 3, &triplets);
let mut f = vec![0.0; 3];
let bcs = vec![RobinBC::new(0, 0, 50.0, 2.0)];
apply_robin_sparse(&mut k, &mut f, &bcs, 3);
assert!((k.get(0, 0) - 60.0).abs() < 1e-12);
assert!((f[0] - 100.0).abs() < 1e-12);
}
#[test]
fn neumann_sparse_adds_force() {
let mut f = vec![0.0; 6];
let bcs = vec![NeumannBC::new(1, 2, 42.0)];
apply_neumann_sparse(&mut f, &bcs);
assert!((f[5] - 42.0).abs() < 1e-12);
}
#[test]
fn boundary_set_count() {
let mut set = BoundarySet::new();
assert_eq!(set.count(), 0);
set.add_dirichlet(DirichletBC::new(0, 0, 0.0));
set.add_neumann(NeumannBC::new(0, 1, 1.0));
set.add_robin(RobinBC::new(0, 2, 10.0, 0.0));
set.add_periodic(PeriodicBC::new(0, 1, 0));
set.add_symmetry(SymmetryBC::new(vec![0, 1, 2], 1));
set.add_time_dependent(TimeDependentBC::new(0, 0, 1.0, TimeFunction::Constant(1.0)));
set.add_mixed(MixedBC::roller(0, 0, 0.0));
assert_eq!(set.count(), 9); }
#[test]
fn boundary_set_apply_all_at_time() {
let mut set = BoundarySet::new();
set.add_time_dependent(TimeDependentBC::new(
0,
0,
5.0,
TimeFunction::Ramp { ramp_time: 1.0 },
));
let n = 3;
let mut k = vec![0.0; n * n];
for i in 0..n {
k[i * n + i] = 1.0;
}
let mut f = vec![0.0; n];
set.apply_all_at_time(&mut k, &mut f, n, 1e12, 0.5);
assert!(
f[0] > 1e11,
"f[0] should be large due to penalty, got {}",
f[0]
);
}
#[test]
fn boundary_set_with_mixed_and_periodic() {
let mut set = BoundarySet::new();
set.add_mixed(MixedBC::new(0, vec![(0, 0.0)], vec![(1, 50.0)]));
set.add_periodic(PeriodicBC::new(0, 1, 2));
let n = 6;
let mut k = vec![0.0; n * n];
for i in 0..n {
k[i * n + i] = 1.0;
}
let mut f = vec![0.0; n];
let penalty = 1e6;
set.apply_all(&mut k, &mut f, n, penalty);
assert!((f[1] - 50.0).abs() < 1e-6, "f[1]={}", f[1]);
}
}