use crate::OdeSystem;
use numra_core::Scalar;
#[derive(Clone, Debug)]
pub struct DaeIndexInfo {
pub structural_index: usize,
pub n_hidden_constraints: usize,
pub differentiation_schedule: Vec<(usize, usize)>,
pub assignment: Vec<Option<usize>>,
pub n_diff: usize,
pub n_alg: usize,
}
#[derive(Clone, Debug)]
pub struct DaeStructure {
pub n_diff: usize,
pub n_alg: usize,
pub n_diff_eqs: usize,
pub n_alg_eqs: usize,
pub incidence: Vec<Vec<usize>>,
}
impl DaeStructure {
pub fn n_vars(&self) -> usize {
self.n_diff + self.n_alg
}
pub fn n_eqs(&self) -> usize {
self.n_diff_eqs + self.n_alg_eqs
}
}
pub fn analyze_dae_index(structure: &DaeStructure) -> DaeIndexInfo {
let n_eqs = structure.n_eqs();
let _n_vars = structure.n_vars();
let n_diff = structure.n_diff;
let n_alg = structure.n_alg;
let n_diff_eqs = structure.n_diff_eqs;
let n_alg_eqs = structure.n_alg_eqs;
if n_alg_eqs == 0 {
return DaeIndexInfo {
structural_index: 0,
n_hidden_constraints: 0,
differentiation_schedule: Vec::new(),
assignment: vec![None; n_eqs],
n_diff,
n_alg,
};
}
let alg_var_start = n_diff;
let mut alg_incidence: Vec<Vec<usize>> = Vec::new();
for alg_eq_idx in 0..n_alg_eqs {
let eq_idx = n_diff_eqs + alg_eq_idx;
let vars = if eq_idx < structure.incidence.len() {
&structure.incidence[eq_idx]
} else {
continue;
};
let alg_vars: Vec<usize> = vars
.iter()
.filter(|&&v| v >= alg_var_start && v < alg_var_start + n_alg)
.map(|&v| v - alg_var_start)
.collect();
alg_incidence.push(alg_vars);
}
let mut eq_to_var: Vec<Option<usize>> = vec![None; n_alg_eqs];
let mut var_to_eq: Vec<Option<usize>> = vec![None; n_alg];
for eq in 0..n_alg_eqs {
let mut visited = vec![false; n_alg];
augmenting_path_restricted(
eq,
&alg_incidence,
&mut eq_to_var,
&mut var_to_eq,
&mut visited,
);
}
let mut diff_schedule: Vec<(usize, usize)> = Vec::new();
let mut max_differentiations = 0usize;
for alg_eq_idx in 0..n_alg_eqs {
if eq_to_var[alg_eq_idx].is_none() {
diff_schedule.push((n_diff_eqs + alg_eq_idx, 1));
max_differentiations = max_differentiations.max(1);
}
}
if !diff_schedule.is_empty() {
let n_new_eqs = diff_schedule.len();
let mut aug_alg_incidence: Vec<Vec<usize>> = alg_incidence.clone();
let mut n_aug_alg = n_alg;
for &(orig_eq_idx, _) in &diff_schedule {
let orig_vars = if orig_eq_idx < structure.incidence.len() {
&structure.incidence[orig_eq_idx]
} else {
continue;
};
let mut new_alg_vars: Vec<usize> = Vec::new();
for &v in orig_vars {
if v < n_diff {
new_alg_vars.push(n_aug_alg);
n_aug_alg += 1;
}
}
let orig_alg: Vec<usize> = orig_vars
.iter()
.filter(|&&v| v >= alg_var_start && v < alg_var_start + n_alg)
.map(|&v| v - alg_var_start)
.collect();
let mut combined = orig_alg;
combined.extend(new_alg_vars);
aug_alg_incidence.push(combined);
}
let total_alg_eqs = n_alg_eqs + n_new_eqs;
let mut eq_to_var2: Vec<Option<usize>> = vec![None; total_alg_eqs];
let mut var_to_eq2: Vec<Option<usize>> = vec![None; n_aug_alg];
for eq in 0..total_alg_eqs {
let mut visited = vec![false; n_aug_alg];
augmenting_path_restricted(
eq,
&aug_alg_incidence,
&mut eq_to_var2,
&mut var_to_eq2,
&mut visited,
);
}
let mut second_round_unmatched = 0;
for new_eq in n_alg_eqs..total_alg_eqs {
if eq_to_var2[new_eq].is_none() {
second_round_unmatched += 1;
}
}
if second_round_unmatched > 0 {
for entry in &mut diff_schedule {
entry.1 += 1; }
max_differentiations += 1;
}
}
let structural_index = if diff_schedule.is_empty() {
1 } else {
max_differentiations + 1 };
let n_hidden = diff_schedule.iter().map(|&(_, n)| n).sum::<usize>();
let mut assignment: Vec<Option<usize>> = vec![None; n_eqs];
for i in 0..n_diff_eqs.min(n_diff) {
assignment[i] = Some(i);
}
for (alg_eq_idx, &matched_var) in eq_to_var.iter().enumerate() {
if let Some(v) = matched_var {
assignment[n_diff_eqs + alg_eq_idx] = Some(alg_var_start + v);
}
}
DaeIndexInfo {
structural_index,
n_hidden_constraints: n_hidden,
differentiation_schedule: diff_schedule,
assignment,
n_diff,
n_alg,
}
}
fn augmenting_path_restricted(
eq: usize,
incidence: &[Vec<usize>],
eq_to_var: &mut [Option<usize>],
var_to_eq: &mut [Option<usize>],
visited: &mut [bool],
) -> bool {
if eq >= incidence.len() {
return false;
}
for &var in &incidence[eq] {
if var >= visited.len() || visited[var] {
continue;
}
visited[var] = true;
let can_reassign = match var_to_eq[var] {
None => true,
Some(other_eq) => {
augmenting_path_restricted(other_eq, incidence, eq_to_var, var_to_eq, visited)
}
};
if can_reassign {
eq_to_var[eq] = Some(var);
var_to_eq[var] = Some(eq);
return true;
}
}
false
}
pub fn detect_structure<S, Sys>(system: &Sys, t0: S, y0: &[S]) -> DaeStructure
where
S: Scalar,
Sys: OdeSystem<S>,
{
let n = system.dim();
let alg_indices = system.algebraic_indices();
let n_alg = alg_indices.len();
let n_diff = n - n_alg;
let is_algebraic: Vec<bool> = (0..n).map(|i| alg_indices.contains(&i)).collect();
let diff_eq_indices: Vec<usize> = (0..n).filter(|i| !is_algebraic[*i]).collect();
let alg_eq_indices: Vec<usize> = (0..n).filter(|i| is_algebraic[*i]).collect();
let n_diff_eqs = diff_eq_indices.len();
let n_alg_eqs = alg_eq_indices.len();
let h_factor = S::EPSILON.sqrt();
let mut f0 = vec![S::ZERO; n];
system.rhs(t0, y0, &mut f0);
let mut incidence = vec![Vec::new(); n_diff_eqs + n_alg_eqs];
let mut y_pert = y0.to_vec();
for j in 0..n {
let yj_save = y_pert[j];
let h = h_factor * (S::ONE + yj_save.abs());
y_pert[j] = yj_save + h;
let mut f1 = vec![S::ZERO; n];
system.rhs(t0, &y_pert, &mut f1);
y_pert[j] = yj_save;
let threshold = S::from_f64(1e-12);
for (local_idx, &eq_idx) in diff_eq_indices.iter().enumerate() {
let diff = (f1[eq_idx] - f0[eq_idx]).abs();
if diff > threshold * h {
incidence[local_idx].push(j);
}
}
for (local_idx, &eq_idx) in alg_eq_indices.iter().enumerate() {
let diff = (f1[eq_idx] - f0[eq_idx]).abs();
if diff > threshold * h {
incidence[n_diff_eqs + local_idx].push(j);
}
}
}
DaeStructure {
n_diff,
n_alg,
n_diff_eqs,
n_alg_eqs,
incidence,
}
}
type RhsFn<S> = Box<dyn Fn(S, &[S], &mut [S]) + Send + Sync>;
type MassFn<S> = Box<dyn Fn(&mut [S]) + Send + Sync>;
impl<S: Scalar> core::fmt::Debug for ReducedDaeSystem<S> {
fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result {
f.debug_struct("ReducedDaeSystem")
.field("n_orig", &self.n_orig)
.field("n_diff", &self.n_diff)
.field("aug_dim", &self.aug_dim)
.field("n_new_vars", &self.n_new_vars)
.field("info", &self.info)
.finish()
}
}
pub struct ReducedDaeSystem<S: Scalar> {
n_orig: usize,
n_diff: usize,
alg_eq_indices: Vec<usize>,
diff_eq_indices: Vec<usize>,
rhs_fn: RhsFn<S>,
mass_fn: MassFn<S>,
info: DaeIndexInfo,
aug_dim: usize,
n_new_vars: usize,
fd_eps: S,
}
impl<S: Scalar> ReducedDaeSystem<S> {
pub fn info(&self) -> &DaeIndexInfo {
&self.info
}
pub fn n_diff(&self) -> usize {
self.n_diff
}
pub fn augmented_dim(&self) -> usize {
self.aug_dim
}
pub fn original_dim(&self) -> usize {
self.n_orig
}
pub fn extract_original(&self, y_aug: &[S]) -> Vec<S> {
y_aug[..self.n_orig].to_vec()
}
pub fn augment_initial_conditions(&self, t0: S, y0: &[S]) -> Vec<S> {
assert_eq!(y0.len(), self.n_orig, "y0 must have original dimension");
let mut y_aug = vec![S::ZERO; self.aug_dim];
y_aug[..self.n_orig].copy_from_slice(y0);
let mut f0 = vec![S::ZERO; self.n_orig];
(self.rhs_fn)(t0, y0, &mut f0);
for (k, &alg_eq) in self.alg_eq_indices.iter().enumerate() {
if self.n_orig + k < self.aug_dim {
y_aug[self.n_orig + k] = f0[alg_eq];
}
}
y_aug
}
fn differentiate_constraint(&self, t: S, y: &[S], eq_idx: usize, dydt_diff: &[S]) -> S {
let n = self.n_orig;
let eps = self.fd_eps;
let mut f0 = vec![S::ZERO; n];
(self.rhs_fn)(t, y, &mut f0);
let g0 = f0[eq_idx];
let mut f_tp = vec![S::ZERO; n];
let ht = eps * (S::ONE + t.abs());
(self.rhs_fn)(t + ht, y, &mut f_tp);
let dgdt = (f_tp[eq_idx] - g0) / ht;
let mut dgdy_dot = S::ZERO;
let mut y_pert = y.to_vec();
for j in 0..n {
let yj_save = y_pert[j];
let h = eps * (S::ONE + yj_save.abs());
y_pert[j] = yj_save + h;
let mut f1 = vec![S::ZERO; n];
(self.rhs_fn)(t, &y_pert, &mut f1);
y_pert[j] = yj_save;
let dgdyj = (f1[eq_idx] - g0) / h;
dgdy_dot = dgdy_dot + dgdyj * dydt_diff[j];
}
dgdt + dgdy_dot
}
}
impl<S: Scalar> OdeSystem<S> for ReducedDaeSystem<S> {
fn dim(&self) -> usize {
self.aug_dim
}
fn rhs(&self, t: S, y: &[S], dydt: &mut [S]) {
let n = self.n_orig;
let y_orig = &y[..n];
let mut f_orig = vec![S::ZERO; n];
(self.rhs_fn)(t, y_orig, &mut f_orig);
for &i in &self.diff_eq_indices {
dydt[i] = f_orig[i];
}
for &i in &self.alg_eq_indices {
dydt[i] = f_orig[i];
}
for (k, &(eq_idx, n_diffs)) in self.info.differentiation_schedule.iter().enumerate() {
if k >= self.n_new_vars {
break;
}
let new_var_idx = n + k;
if new_var_idx < self.aug_dim && n_diffs >= 1 {
let dg = self.differentiate_constraint(t, y_orig, eq_idx, &f_orig);
dydt[new_var_idx] = dg;
}
}
}
fn has_mass_matrix(&self) -> bool {
true
}
fn mass_matrix(&self, mass: &mut [S]) {
let aug = self.aug_dim;
for i in 0..aug * aug {
mass[i] = S::ZERO;
}
let n = self.n_orig;
let mut orig_mass = vec![S::ZERO; n * n];
(self.mass_fn)(&mut orig_mass);
for i in 0..n {
for j in 0..n {
mass[i * aug + j] = orig_mass[i * n + j];
}
}
}
fn is_singular_mass(&self) -> bool {
true
}
fn algebraic_indices(&self) -> Vec<usize> {
let mut indices = Vec::new();
for &i in &self.alg_eq_indices {
indices.push(i);
}
for k in 0..self.n_new_vars {
indices.push(self.n_orig + k);
}
indices
}
}
pub fn reduce_index<S, F, M>(
rhs_fn: F,
mass_fn: M,
alg_indices: &[usize],
n: usize,
t0: S,
y0: &[S],
) -> Result<ReducedDaeSystem<S>, String>
where
S: Scalar,
F: Fn(S, &[S], &mut [S]) + Send + Sync + 'static,
M: Fn(&mut [S]) + Send + Sync + 'static,
{
let n_alg = alg_indices.len();
let n_diff = n - n_alg;
let is_algebraic: Vec<bool> = (0..n).map(|i| alg_indices.contains(&i)).collect();
let diff_eq_indices: Vec<usize> = (0..n).filter(|i| !is_algebraic[*i]).collect();
let alg_eq_indices: Vec<usize> = alg_indices.to_vec();
let structure = detect_structure_from_fn(&rhs_fn, n, &diff_eq_indices, &alg_eq_indices, t0, y0);
let info = analyze_dae_index(&structure);
if info.structural_index <= 1 {
return Err("System is already index-1 or index-0; no reduction needed".to_string());
}
let n_new_vars: usize = info
.differentiation_schedule
.iter()
.map(|&(_, nd)| nd)
.sum();
let aug_dim = n + n_new_vars;
Ok(ReducedDaeSystem {
n_orig: n,
n_diff,
alg_eq_indices,
diff_eq_indices,
rhs_fn: Box::new(rhs_fn),
mass_fn: Box::new(mass_fn),
info,
aug_dim,
n_new_vars,
fd_eps: S::EPSILON.sqrt(),
})
}
fn detect_structure_from_fn<S, F>(
rhs_fn: &F,
n: usize,
diff_eq_indices: &[usize],
alg_eq_indices: &[usize],
t0: S,
y0: &[S],
) -> DaeStructure
where
S: Scalar,
F: Fn(S, &[S], &mut [S]),
{
let n_diff_eqs = diff_eq_indices.len();
let n_alg_eqs = alg_eq_indices.len();
let n_diff = n - n_alg_eqs;
let n_alg = n_alg_eqs;
let h_factor = S::EPSILON.sqrt();
let mut f0 = vec![S::ZERO; n];
rhs_fn(t0, y0, &mut f0);
let mut incidence = vec![Vec::new(); n_diff_eqs + n_alg_eqs];
let mut y_pert = y0.to_vec();
for j in 0..n {
let yj_save = y_pert[j];
let h = h_factor * (S::ONE + yj_save.abs());
y_pert[j] = yj_save + h;
let mut f1 = vec![S::ZERO; n];
rhs_fn(t0, &y_pert, &mut f1);
y_pert[j] = yj_save;
let threshold = S::from_f64(1e-12);
for (local_idx, &eq_idx) in diff_eq_indices.iter().enumerate() {
let diff = (f1[eq_idx] - f0[eq_idx]).abs();
if diff > threshold * h {
incidence[local_idx].push(j);
}
}
for (local_idx, &eq_idx) in alg_eq_indices.iter().enumerate() {
let diff = (f1[eq_idx] - f0[eq_idx]).abs();
if diff > threshold * h {
incidence[n_diff_eqs + local_idx].push(j);
}
}
}
DaeStructure {
n_diff,
n_alg,
n_diff_eqs,
n_alg_eqs,
incidence,
}
}
pub fn reduce_dae_problem<S, F, M>(
problem: &DaeProblem<S, F, M>,
t0: S,
y0: &[S],
) -> Result<(DaeIndexInfo, ReducedDaeSystem<S>), String>
where
S: Scalar,
F: Fn(S, &[S], &mut [S]) + Clone + Send + Sync + 'static,
M: Fn(&mut [S]) + Clone + Send + Sync + 'static,
{
let structure = detect_structure(problem, t0, y0);
let info = analyze_dae_index(&structure);
if info.structural_index <= 1 {
return Err(format!(
"System is index-{}, no reduction needed",
info.structural_index
));
}
let rhs_fn = problem.f.clone();
let mass_fn = problem.mass.clone();
let alg_indices = problem.alg_indices.clone();
let n = problem.dim();
let n_new_vars: usize = info
.differentiation_schedule
.iter()
.map(|&(_, nd)| nd)
.sum();
let aug_dim = n + n_new_vars;
let is_algebraic: Vec<bool> = (0..n).map(|i| alg_indices.contains(&i)).collect();
let diff_eq_indices: Vec<usize> = (0..n).filter(|i| !is_algebraic[*i]).collect();
let reduced = ReducedDaeSystem {
n_orig: n,
n_diff: n - alg_indices.len(),
alg_eq_indices: alg_indices,
diff_eq_indices,
rhs_fn: Box::new(rhs_fn),
mass_fn: Box::new(mass_fn),
info: info.clone(),
aug_dim,
n_new_vars,
fd_eps: S::EPSILON.sqrt(),
};
Ok((info, reduced))
}
pub fn analyze_system<S, Sys>(system: &Sys, t0: S, y0: &[S]) -> DaeIndexInfo
where
S: Scalar,
Sys: OdeSystem<S>,
{
let structure = detect_structure(system, t0, y0);
analyze_dae_index(&structure)
}
use crate::DaeProblem;
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_pure_ode_index_0() {
let structure = DaeStructure {
n_diff: 2,
n_alg: 0,
n_diff_eqs: 2,
n_alg_eqs: 0,
incidence: vec![
vec![0, 1], vec![0, 1], ],
};
let info = analyze_dae_index(&structure);
assert_eq!(info.structural_index, 0);
assert_eq!(info.n_hidden_constraints, 0);
assert!(info.differentiation_schedule.is_empty());
}
#[test]
fn test_index_1_dae() {
let structure = DaeStructure {
n_diff: 1,
n_alg: 1,
n_diff_eqs: 1,
n_alg_eqs: 1,
incidence: vec![
vec![0, 1], vec![0, 1], ],
};
let info = analyze_dae_index(&structure);
assert_eq!(info.structural_index, 1);
assert_eq!(info.n_hidden_constraints, 0);
}
#[test]
fn test_index_2_dae() {
let structure = DaeStructure {
n_diff: 2,
n_alg: 1,
n_diff_eqs: 2,
n_alg_eqs: 1,
incidence: vec![
vec![0, 1], vec![0, 2], vec![0], ],
};
let info = analyze_dae_index(&structure);
assert!(
info.structural_index >= 2,
"Expected index >= 2, got {}",
info.structural_index
);
assert!(info.n_hidden_constraints >= 1);
assert!(!info.differentiation_schedule.is_empty());
}
#[test]
fn test_detect_structure_index1() {
let dae = DaeProblem::new(
|_t, y: &[f64], dydt: &mut [f64]| {
dydt[0] = -y[0];
dydt[1] = y[1] - y[0] * y[0];
},
|mass: &mut [f64]| {
mass[0] = 1.0;
mass[1] = 0.0;
mass[2] = 0.0;
mass[3] = 0.0;
},
0.0,
1.0,
vec![2.0, 4.0],
vec![1],
);
let structure = detect_structure(&dae, 0.0, &[2.0, 4.0]);
assert_eq!(structure.n_diff, 1);
assert_eq!(structure.n_alg, 1);
assert_eq!(structure.n_diff_eqs, 1);
assert_eq!(structure.n_alg_eqs, 1);
let info = analyze_dae_index(&structure);
assert_eq!(info.structural_index, 1);
}
#[test]
fn test_detect_structure_index2() {
let dae = DaeProblem::new(
|_t, y: &[f64], dydt: &mut [f64]| {
let x = y[0];
let v = y[1];
let lam = y[2];
dydt[0] = v; dydt[1] = -lam * x; dydt[2] = x * x - 1.0; },
|mass: &mut [f64]| {
for i in 0..9 {
mass[i] = 0.0;
}
mass[0] = 1.0; mass[4] = 1.0; },
0.0,
1.0,
vec![1.0, 0.0, 0.0],
vec![2],
);
let structure = detect_structure(&dae, 0.0, &[1.0, 0.0, 0.0]);
let info = analyze_dae_index(&structure);
assert!(
info.structural_index >= 2,
"Expected index >= 2, got {}. Schedule: {:?}",
info.structural_index,
info.differentiation_schedule
);
}
#[test]
fn test_reduce_index_creates_augmented_system() {
let n = 3;
let alg_indices = vec![2usize];
let result = reduce_index(
|_t, y: &[f64], dydt: &mut [f64]| {
let x = y[0];
let v = y[1];
let lam = y[2];
dydt[0] = v;
dydt[1] = -lam * x;
dydt[2] = x * x - 1.0;
},
|mass: &mut [f64]| {
for i in 0..9 {
mass[i] = 0.0;
}
mass[0] = 1.0;
mass[4] = 1.0;
},
&alg_indices,
n,
0.0_f64,
&[1.0, 0.0, 0.0],
);
assert!(
result.is_ok(),
"Reduction should succeed: {:?}",
result.err()
);
let reduced = result.unwrap();
assert!(
reduced.augmented_dim() > n,
"Augmented dim {} should be > original dim {}",
reduced.augmented_dim(),
n
);
assert!(reduced.is_singular_mass());
let y0_aug = reduced.augment_initial_conditions(0.0, &[1.0, 0.0, 0.0]);
assert_eq!(y0_aug.len(), reduced.augmented_dim());
assert!((y0_aug[0] - 1.0).abs() < 1e-10);
assert!((y0_aug[1] - 0.0).abs() < 1e-10);
assert!((y0_aug[2] - 0.0).abs() < 1e-10);
}
#[test]
fn test_reduced_system_rhs_evaluates() {
let n = 3;
let alg_indices = vec![2usize];
let reduced = reduce_index(
|_t, y: &[f64], dydt: &mut [f64]| {
let x = y[0];
let v = y[1];
let lam = y[2];
dydt[0] = v;
dydt[1] = -lam * x;
dydt[2] = x * x - 1.0;
},
|mass: &mut [f64]| {
for i in 0..9 {
mass[i] = 0.0;
}
mass[0] = 1.0;
mass[4] = 1.0;
},
&alg_indices,
n,
0.0_f64,
&[1.0, 0.0, 0.0],
)
.unwrap();
let aug_dim = reduced.augmented_dim();
let y0_aug = reduced.augment_initial_conditions(0.0, &[1.0, 0.0, 0.0]);
let mut dydt = vec![0.0; aug_dim];
reduced.rhs(0.0, &y0_aug, &mut dydt);
assert!(
(dydt[0] - 0.0).abs() < 1e-10,
"x' should be 0, got {}",
dydt[0]
);
assert!(
(dydt[1] - 0.0).abs() < 1e-10,
"v' should be 0, got {}",
dydt[1]
);
assert!(
(dydt[2] - 0.0).abs() < 1e-8,
"constraint should be ~0, got {}",
dydt[2]
);
}
#[test]
fn test_reduced_system_mass_matrix() {
let n = 3;
let alg_indices = vec![2usize];
let reduced = reduce_index(
|_t, y: &[f64], dydt: &mut [f64]| {
dydt[0] = y[1];
dydt[1] = -y[2] * y[0];
dydt[2] = y[0] * y[0] - 1.0;
},
|mass: &mut [f64]| {
for i in 0..9 {
mass[i] = 0.0;
}
mass[0] = 1.0;
mass[4] = 1.0;
},
&alg_indices,
n,
0.0_f64,
&[1.0, 0.0, 0.0],
)
.unwrap();
let aug_dim = reduced.augmented_dim();
let mut mass = vec![0.0; aug_dim * aug_dim];
reduced.mass_matrix(&mut mass);
assert!((mass[0 * aug_dim + 0] - 1.0).abs() < 1e-10); assert!((mass[1 * aug_dim + 1] - 1.0).abs() < 1e-10); assert!((mass[2 * aug_dim + 2] - 0.0).abs() < 1e-10);
for k in n..aug_dim {
for j in 0..aug_dim {
assert!(
(mass[k * aug_dim + j] - 0.0).abs() < 1e-10,
"New row {} should be all zero, but M[{},{}] = {}",
k,
k,
j,
mass[k * aug_dim + j]
);
}
}
}
#[test]
fn test_already_index1_returns_err() {
let result = reduce_index(
|_t, y: &[f64], dydt: &mut [f64]| {
dydt[0] = -y[0];
dydt[1] = y[1] - y[0] * y[0];
},
|mass: &mut [f64]| {
mass[0] = 1.0;
mass[1] = 0.0;
mass[2] = 0.0;
mass[3] = 0.0;
},
&[1],
2,
0.0_f64,
&[2.0, 4.0],
);
assert!(result.is_err());
assert!(result.unwrap_err().contains("index-1"));
}
#[test]
fn test_analyze_system_convenience() {
let dae = DaeProblem::new(
|_t, y: &[f64], dydt: &mut [f64]| {
dydt[0] = -y[0];
dydt[1] = y[1] - y[0] * y[0];
},
|mass: &mut [f64]| {
mass[0] = 1.0;
mass[1] = 0.0;
mass[2] = 0.0;
mass[3] = 0.0;
},
0.0,
1.0,
vec![2.0, 4.0],
vec![1],
);
let info = analyze_system(&dae, 0.0, &[2.0, 4.0]);
assert_eq!(info.structural_index, 1);
}
#[test]
fn test_extract_original() {
let n = 3;
let reduced = reduce_index(
|_t, y: &[f64], dydt: &mut [f64]| {
dydt[0] = y[1];
dydt[1] = -y[2] * y[0];
dydt[2] = y[0] * y[0] - 1.0;
},
|mass: &mut [f64]| {
for i in 0..9 {
mass[i] = 0.0;
}
mass[0] = 1.0;
mass[4] = 1.0;
},
&[2],
n,
0.0_f64,
&[1.0, 0.0, 0.0],
)
.unwrap();
let y_aug = vec![1.0, 2.0, 3.0, 4.0, 5.0]; let y_orig = reduced.extract_original(&y_aug);
assert_eq!(y_orig.len(), 3);
assert!((y_orig[0] - 1.0).abs() < 1e-10);
assert!((y_orig[1] - 2.0).abs() < 1e-10);
assert!((y_orig[2] - 3.0).abs() < 1e-10);
}
#[test]
fn test_dae_structure_helpers() {
let structure = DaeStructure {
n_diff: 2,
n_alg: 1,
n_diff_eqs: 2,
n_alg_eqs: 1,
incidence: vec![vec![0, 1], vec![0, 2], vec![0]],
};
assert_eq!(structure.n_vars(), 3);
assert_eq!(structure.n_eqs(), 3);
}
#[test]
fn test_pantelides_empty_incidence() {
let structure = DaeStructure {
n_diff: 1,
n_alg: 1,
n_diff_eqs: 1,
n_alg_eqs: 1,
incidence: vec![
vec![0], vec![], ],
};
let info = analyze_dae_index(&structure);
assert!(info.structural_index >= 1);
}
#[test]
fn test_multiple_algebraic_equations() {
let structure = DaeStructure {
n_diff: 1,
n_alg: 2,
n_diff_eqs: 1,
n_alg_eqs: 2,
incidence: vec![
vec![0], vec![0, 1], vec![0, 2], ],
};
let info = analyze_dae_index(&structure);
assert_eq!(info.structural_index, 1);
assert_eq!(info.n_hidden_constraints, 0);
}
}