use crate::error::{IntegrateError, IntegrateResult};
use crate::port_hamiltonian::system::{PortHamiltonianBuilder, PortHamiltonianSystem};
use scirs2_core::ndarray::{array, Array2};
fn canonical_j(n: usize) -> Array2<f64> {
let two_n = 2 * n;
let mut j = Array2::zeros((two_n, two_n));
for i in 0..n {
j[[i, n + i]] = 1.0;
j[[n + i, i]] = -1.0;
}
j
}
pub fn pendulum_ph(
mass: f64,
length: f64,
gravity: f64,
damping: f64,
) -> IntegrateResult<PortHamiltonianSystem> {
if mass <= 0.0 {
return Err(IntegrateError::ValueError("Pendulum mass must be positive".into()));
}
if length <= 0.0 {
return Err(IntegrateError::ValueError("Pendulum length must be positive".into()));
}
if damping < 0.0 {
return Err(IntegrateError::ValueError("Damping must be non-negative".into()));
}
let ml2 = mass * length * length;
let mgl = mass * gravity * length;
PortHamiltonianBuilder::new(2, 1)
.with_j(move |_x| Ok(array![[0.0, 1.0], [-1.0, 0.0]]))
.with_r(move |_x| Ok(array![[0.0, 0.0], [0.0, damping]]))
.with_hamiltonian(move |x| {
let q = x[0];
let p = x[1];
Ok(p * p / (2.0 * ml2) + mgl * (1.0 - q.cos()))
})
.with_grad_hamiltonian(move |x| {
let q = x[0];
let p = x[1];
Ok(scirs2_core::ndarray::array![mgl * q.sin(), p / ml2])
})
.with_b(array![[0.0], [1.0]])
.build()
}
pub fn mass_spring_damper_ph(
mass: f64,
spring_const: f64,
damping: f64,
) -> IntegrateResult<PortHamiltonianSystem> {
if mass <= 0.0 {
return Err(IntegrateError::ValueError("Mass must be positive".into()));
}
if spring_const < 0.0 {
return Err(IntegrateError::ValueError("Spring constant must be non-negative".into()));
}
if damping < 0.0 {
return Err(IntegrateError::ValueError("Damping must be non-negative".into()));
}
PortHamiltonianBuilder::new(2, 1)
.with_j(move |_x| Ok(array![[0.0, 1.0], [-1.0, 0.0]]))
.with_r(move |_x| Ok(array![[0.0, 0.0], [0.0, damping]]))
.with_hamiltonian(move |x| {
let q = x[0];
let p = x[1];
Ok(p * p / (2.0 * mass) + spring_const * q * q / 2.0)
})
.with_grad_hamiltonian(move |x| {
let q = x[0];
let p = x[1];
Ok(scirs2_core::ndarray::array![spring_const * q, p / mass])
})
.with_b(array![[0.0], [1.0]])
.build()
}
pub fn double_pendulum_ph(
mass: f64,
length: f64,
gravity: f64,
damping1: f64,
damping2: f64,
) -> IntegrateResult<PortHamiltonianSystem> {
if mass <= 0.0 {
return Err(IntegrateError::ValueError("Mass must be positive".into()));
}
if length <= 0.0 {
return Err(IntegrateError::ValueError("Length must be positive".into()));
}
let m = mass;
let l = length;
let g = gravity;
let ml2 = m * l * l;
PortHamiltonianBuilder::new(4, 2)
.with_j(|_x| {
let j = canonical_j(2);
Ok(j)
})
.with_r(move |_x| {
Ok(array![
[0.0, 0.0, 0.0, 0.0],
[0.0, 0.0, 0.0, 0.0],
[0.0, 0.0, damping1, 0.0],
[0.0, 0.0, 0.0, damping2]
])
})
.with_hamiltonian(move |x| {
let q1 = x[0];
let q2 = x[1];
let p1 = x[2];
let p2 = x[3];
let delta = q1 - q2;
let cos_delta = delta.cos();
let denom = 2.0 * ml2 * (2.0 - cos_delta * cos_delta);
if denom.abs() < 1e-30 {
return Err(IntegrateError::ComputationError(
"Double pendulum singularity (cos²(Δ)=2)".into(),
));
}
let t_kin = (p1 * p1 + 2.0 * p2 * p2 - 2.0 * p1 * p2 * cos_delta) / denom;
let v_pot = -m * g * l * (2.0 * q1.cos() + q2.cos());
Ok(t_kin + v_pot)
})
.with_b({
let mut b = Array2::zeros((4, 2));
b[[2, 0]] = 1.0;
b[[3, 1]] = 1.0;
b
})
.build()
}
pub fn rlc_circuit_ph(
inductance: f64,
capacitance: f64,
resistance: f64,
) -> IntegrateResult<PortHamiltonianSystem> {
if inductance <= 0.0 {
return Err(IntegrateError::ValueError("Inductance must be positive".into()));
}
if capacitance <= 0.0 {
return Err(IntegrateError::ValueError("Capacitance must be positive".into()));
}
if resistance < 0.0 {
return Err(IntegrateError::ValueError("Resistance must be non-negative".into()));
}
PortHamiltonianBuilder::new(2, 1)
.with_j(|_x| Ok(array![[0.0, -1.0], [1.0, 0.0]]))
.with_r(move |_x| Ok(array![[0.0, 0.0], [0.0, resistance]]))
.with_hamiltonian(move |x| {
let q_c = x[0]; let phi_l = x[1]; Ok(q_c * q_c / (2.0 * capacitance) + phi_l * phi_l / (2.0 * inductance))
})
.with_grad_hamiltonian(move |x| {
let q_c = x[0];
let phi_l = x[1];
Ok(scirs2_core::ndarray::array![q_c / capacitance, phi_l / inductance])
})
.with_b(array![[1.0], [0.0]])
.build()
}
pub fn coupled_oscillators_ph(
masses: &[f64],
spring_consts: &[f64],
damping: &[f64],
) -> IntegrateResult<PortHamiltonianSystem> {
let n = masses.len();
if spring_consts.len() != n - 1 {
return Err(IntegrateError::ValueError(format!(
"spring_consts must have length n-1={}, got {}",
n - 1,
spring_consts.len()
)));
}
if damping.len() != n {
return Err(IntegrateError::ValueError(format!(
"damping must have length n={n}, got {}",
damping.len()
)));
}
for (i, &m) in masses.iter().enumerate() {
if m <= 0.0 {
return Err(IntegrateError::ValueError(format!("Mass {i} must be positive")));
}
}
let masses_vec = masses.to_vec();
let k_vec = spring_consts.to_vec();
let d_vec = damping.to_vec();
let masses_clone = masses_vec.clone();
let k_clone = k_vec.clone();
let d_clone = d_vec.clone();
let mut b = Array2::zeros((2 * n, n));
for i in 0..n {
b[[n + i, i]] = 1.0;
}
PortHamiltonianBuilder::new(2 * n, n)
.with_j(move |_x| Ok(canonical_j(n)))
.with_r(move |_x| {
let mut r = Array2::zeros((2 * n, 2 * n));
for i in 0..n {
r[[n + i, n + i]] = d_clone[i];
}
Ok(r)
})
.with_hamiltonian(move |x| {
let mut h = 0.0_f64;
for i in 0..n {
let p_i = x[n + i];
h += p_i * p_i / (2.0 * masses_vec[i]);
}
for i in 0..n - 1 {
let dq = x[i + 1] - x[i];
h += k_vec[i] * dq * dq / 2.0;
}
Ok(h)
})
.with_grad_hamiltonian(move |x| {
let mut grad = vec![0.0_f64; 2 * n];
for i in 0..n {
if i > 0 {
grad[i] -= k_clone[i - 1] * (x[i - 1] - x[i]);
}
if i < n - 1 {
grad[i] += k_clone[i] * (x[i] - x[i + 1]);
}
}
for i in 0..n {
grad[n + i] = x[n + i] / masses_clone[i];
}
Ok(scirs2_core::ndarray::Array1::from_vec(grad))
})
.with_b(b)
.build()
}
#[cfg(test)]
mod tests {
use super::*;
use crate::port_hamiltonian::integrators::{AverageVectorField, DiscreteGradientGonzalez};
#[test]
fn test_pendulum_energy_conservation() {
let sys = pendulum_ph(1.0, 1.0, 9.81, 0.0).expect("Failed to create pendulum");
let x0 = vec![0.1_f64, 0.0];
let h0 = sys.hamiltonian(&x0).expect("Hamiltonian eval failed");
let integrator = DiscreteGradientGonzalez::new();
let result = integrator
.integrate(&sys, &x0, 0.0, 2.0, 0.05, None)
.expect("Integration failed");
let h_final = *result.energy.last().expect("No energy in result");
assert!(
(h_final - h0).abs() < 1e-10,
"Energy drift = {} (expected < 1e-10)",
(h_final - h0).abs()
);
}
#[test]
fn test_mass_spring_damper_energy_decay() {
let mass = 1.0_f64;
let k = 4.0_f64;
let c = 0.5_f64;
let sys = mass_spring_damper_ph(mass, k, c).expect("Failed to create MSD");
let x0 = vec![1.0_f64, 0.0]; let h0 = sys.hamiltonian(&x0).expect("Hamiltonian eval failed");
let integrator = AverageVectorField::new();
let result = integrator
.integrate(&sys, &x0, 0.0, 5.0, 0.05, None)
.expect("Integration failed");
let h_final = *result.energy.last().expect("No energy in result");
assert!(
h_final < h0,
"Energy should decrease: h0={h0}, h_final={h_final}"
);
assert!(
h_final >= 0.0,
"Energy must remain non-negative: h_final={h_final}"
);
}
#[test]
fn test_rlc_circuit_structure() {
let sys = rlc_circuit_ph(1e-3, 1e-6, 100.0).expect("Failed to create RLC");
assert!(
sys.validate_skew_symmetry(&[0.0, 0.0])
.expect("Validation failed"),
"J must be skew-symmetric"
);
assert!(
sys.validate_psd(&[0.0, 0.0]).expect("PSD check failed"),
"R must be PSD"
);
}
#[test]
fn test_rlc_energy_decay() {
let sys = rlc_circuit_ph(1.0, 1.0, 1.0).expect("Failed to create RLC");
let x0 = vec![1.0_f64, 0.0];
let h0 = sys.hamiltonian(&x0).expect("Hamiltonian eval failed");
let integrator = AverageVectorField::new();
let result = integrator
.integrate(&sys, &x0, 0.0, 5.0, 0.1, None)
.expect("Integration failed");
let h_final = *result.energy.last().expect("No energy in result");
assert!(h_final < h0, "RLC energy must decrease: h0={h0}, h_final={h_final}");
}
#[test]
fn test_pendulum_output() {
let sys = pendulum_ph(1.0, 1.0, 9.81, 0.0).expect("Failed to create pendulum");
let x = vec![0.5_f64, 2.0];
let y = sys.output(&x).expect("Output eval failed");
let p_over_ml2 = 2.0 / (1.0 * 1.0 * 1.0); assert!(
(y[0] - p_over_ml2).abs() < 1e-10,
"Output y[0] = {} != {}",
y[0],
p_over_ml2
);
}
#[test]
fn test_coupled_oscillators() {
let masses = vec![1.0_f64, 1.0, 1.0];
let springs = vec![1.0_f64, 1.0];
let damps = vec![0.0_f64, 0.0, 0.0];
let sys = coupled_oscillators_ph(&masses, &springs, &damps)
.expect("Failed to create coupled oscillators");
let x0 = vec![1.0, 0.0, 0.0, 0.0, 0.0, 0.0]; let h0 = sys.hamiltonian(&x0).expect("Hamiltonian eval failed");
assert!(h0 > 0.0, "Potential energy should be positive");
let integrator = AverageVectorField::new();
let result = integrator
.integrate(&sys, &x0, 0.0, 2.0, 0.05, None)
.expect("Integration failed");
let h_final = *result.energy.last().expect("No energy in result");
assert!(
(h_final - h0).abs() < 1e-8,
"Energy drift in coupled oscillators: {}",
(h_final - h0).abs()
);
}
}