use scirs2_core::ndarray::{s, Array1, Array2, ArrayView2};
use scirs2_core::numeric::Complex;
use scirs2_core::numeric::{Float, NumAssign};
use std::iter::Sum;
use crate::decomposition::svd;
use crate::eigen::{eigh, eigvals};
use crate::error::{LinalgError, LinalgResult};
use crate::matrix_functions::expm;
use crate::norm::{matrix_norm, vector_norm};
use crate::parallel::WorkerConfig;
use crate::solve::solve;
#[derive(Debug, Clone)]
pub struct DynamicsConfig {
pub krylov_dim: usize,
pub rtol: f64,
pub atol: f64,
pub max_steps: usize,
pub dt_initial: f64,
pub dt_min: f64,
pub dt_max: f64,
pub safety_factor: f64,
pub workers: WorkerConfig,
pub adaptive_error_control: bool,
pub quantum_mode: bool,
}
impl Default for DynamicsConfig {
fn default() -> Self {
Self {
krylov_dim: 30,
rtol: 1e-8,
atol: 1e-12,
max_steps: 10000,
dt_initial: 0.01,
dt_min: 1e-10,
dt_max: 1.0,
safety_factor: 0.9,
workers: WorkerConfig::default(),
adaptive_error_control: true,
quantum_mode: false,
}
}
}
impl DynamicsConfig {
pub fn quantum() -> Self {
Self {
quantum_mode: true,
rtol: 1e-12,
atol: 1e-15,
krylov_dim: 50,
..Default::default()
}
}
pub fn large_scale() -> Self {
Self {
krylov_dim: 50,
rtol: 1e-6,
max_steps: 50000,
..Default::default()
}
}
pub fn high_precision() -> Self {
Self {
rtol: 1e-12,
atol: 1e-15,
krylov_dim: 40,
safety_factor: 0.8,
..Default::default()
}
}
}
#[derive(Debug, Clone)]
pub struct ODEResult<F> {
pub trajectory: Vec<Array2<F>>,
pub times: Vec<F>,
pub steps_taken: usize,
pub final_time: F,
pub success: bool,
pub error_estimates: Option<Vec<F>>,
}
#[allow(dead_code)]
pub fn matrix_exp_action<F>(
a: &ArrayView2<F>,
b: &ArrayView2<F>,
t: F,
config: &DynamicsConfig,
) -> LinalgResult<Array2<F>>
where
F: Float + NumAssign + Sum + Send + Sync + 'static + scirs2_core::ndarray::ScalarOperand,
{
let (n, k) = b.dim();
if a.nrows() != n || a.ncols() != n {
return Err(LinalgError::DimensionError(
"Matrix A must be square and compatible with B".to_string(),
));
}
if n <= 32 {
return high_precision_exp_action(a, b, t, config);
}
krylov_exp_action(a, b, t, config)
}
#[allow(dead_code)]
fn high_precision_exp_action<F>(
a: &ArrayView2<F>,
b: &ArrayView2<F>,
t: F,
_config: &DynamicsConfig,
) -> LinalgResult<Array2<F>>
where
F: Float + NumAssign + Sum + Send + Sync + 'static + scirs2_core::ndarray::ScalarOperand,
{
let n = a.nrows();
if n == 2 {
let a00 = a[[0, 0]];
let a01 = a[[0, 1]];
let a10 = a[[1, 0]];
let a11 = a[[1, 1]];
if a00.abs() < F::from(1e-14).expect("Failed to convert constant to float")
&& a11.abs() < F::from(1e-14).expect("Failed to convert constant to float")
&& (a01 + a10).abs() < F::from(1e-14).expect("Failed to convert constant to float")
{
let omega = a10; let theta = omega * t;
let cos_theta = theta.cos();
let sin_theta = theta.sin();
let mut exp_at = Array2::zeros((2, 2));
exp_at[[0, 0]] = cos_theta;
exp_at[[0, 1]] = -sin_theta;
exp_at[[1, 0]] = sin_theta;
exp_at[[1, 1]] = cos_theta;
return Ok(exp_at.dot(b));
}
}
let scaled_a = a * t;
let exp_at = if n <= 4 {
padematrix_exp(&scaled_a.view())?
} else {
expm(&scaled_a.view(), None)?
};
Ok(exp_at.dot(b))
}
#[allow(dead_code)]
fn padematrix_exp<F>(a: &ArrayView2<F>) -> LinalgResult<Array2<F>>
where
F: Float + NumAssign + Sum + Send + Sync + 'static + scirs2_core::ndarray::ScalarOperand,
{
let n = a.nrows();
let mut result = Array2::eye(n);
let mut term = Array2::eye(n);
let tolerance = F::from(1e-15).expect("Failed to convert constant to float");
let norm = matrix_norm(a, "1", None)?;
let mut scaling_exp = 0;
let mut scaled_a = a.to_owned();
while norm > F::one() {
scaled_a = &scaled_a / F::from(2.0).expect("Failed to convert constant to float");
scaling_exp += 1;
}
for k in 1..50 {
term = term.dot(&scaled_a) / F::from(k).expect("Failed to convert to float");
result += &term;
let term_norm = matrix_norm(&term.view(), "fro", None)?;
if term_norm < tolerance {
break;
}
}
for _ in 0..scaling_exp {
result = result.dot(&result);
}
Ok(result)
}
#[allow(dead_code)]
fn krylov_exp_action<F>(
a: &ArrayView2<F>,
b: &ArrayView2<F>,
t: F,
config: &DynamicsConfig,
) -> LinalgResult<Array2<F>>
where
F: Float + NumAssign + Sum + Send + Sync + 'static + scirs2_core::ndarray::ScalarOperand,
{
let (n, k) = b.dim();
let m = config.krylov_dim.min(n);
let mut result = Array2::zeros((n, k));
for j in 0..k {
let b_col = b.column(j);
let beta = vector_norm(&b_col, 2)?;
if beta < F::from(config.atol).expect("Failed to convert to float") {
continue; }
let mut v = b_col.to_owned() / beta;
let mut h = Array2::<F>::zeros((m + 1, m));
let mut vmatrix = Array2::<F>::zeros((n, m + 1));
vmatrix.column_mut(0).assign(&v);
for j_krylov in 0..m {
let av = a.dot(&v);
let mut w = av;
for i in 0..=j_krylov {
let v_i = vmatrix.column(i);
let hij = w.dot(&v_i);
h[[i, j_krylov]] = hij;
let scaled_vi = v_i.mapv(|x| x * hij);
w = &w - &scaled_vi;
}
let norm_w = vector_norm(&w.view(), 2)?;
if j_krylov < m - 1
&& norm_w > F::from(config.atol).expect("Failed to convert to float")
{
h[[j_krylov + 1, j_krylov]] = norm_w;
v = w / norm_w;
vmatrix.column_mut(j_krylov + 1).assign(&v);
} else {
break;
}
}
let h_reduced = h.slice(s![0..m, 0..m]).to_owned();
let scaled_h = h_reduced * t;
let exp_h = expm(&scaled_h.view(), None)?;
let coeffs = exp_h.column(0);
let mut result_col = Array1::zeros(n);
for i in 0..m {
let scaled_column = vmatrix.column(i).mapv(|x| x * coeffs[i] * beta);
result_col = result_col + scaled_column;
}
result.column_mut(j).assign(&result_col);
}
Ok(result)
}
#[allow(dead_code)]
pub fn lyapunov_solve<F>(
a: &ArrayView2<F>,
c: &ArrayView2<F>,
config: &DynamicsConfig,
) -> LinalgResult<Array2<F>>
where
F: Float + NumAssign + Sum + Send + Sync + 'static + scirs2_core::ndarray::ScalarOperand,
{
let n = a.nrows();
if a.ncols() != n || c.nrows() != n || c.ncols() != n {
return Err(LinalgError::DimensionError(
"All matrices must be square and of the same size".to_string(),
));
}
let eigenvals = eigvals(a, None)?;
for &lambda in eigenvals.iter() {
if lambda.re >= F::zero() {
return Err(LinalgError::SingularMatrixError(
"Matrix A must be stable (all eigenvalues negative real parts) for Lyapunov equation".to_string(),
));
}
}
if n <= 64 {
lyapunov_direct(a, c, config)
} else {
lyapunov_iterative(a, c, config)
}
}
#[allow(dead_code)]
fn lyapunov_direct<F>(
a: &ArrayView2<F>,
c: &ArrayView2<F>,
_config: &DynamicsConfig,
) -> LinalgResult<Array2<F>>
where
F: Float + NumAssign + Sum + Send + Sync + 'static + scirs2_core::ndarray::ScalarOperand,
{
let n = a.nrows();
let mut kronecker_sum = Array2::<F>::zeros((n * n, n * n));
for i in 0..n {
for j in 0..n {
for k in 0..n {
for l in 0..n {
let row = i * n + j;
let col = k * n + l;
if j == l {
kronecker_sum[[row, col]] += a[[i, k]];
}
if i == k {
kronecker_sum[[row, col]] += a[[l, j]];
}
}
}
}
}
let c_vec = c.iter().cloned().collect::<Array1<F>>();
let neg_c_vec = c_vec.mapv(|x| -x);
let x_vec = solve(&kronecker_sum.view(), &neg_c_vec.view(), None)?;
let mut x = Array2::<F>::zeros((n, n));
for i in 0..n {
for j in 0..n {
x[[i, j]] = x_vec[i * n + j];
}
}
Ok(x)
}
#[allow(dead_code)]
fn lyapunov_iterative<F>(
a: &ArrayView2<F>,
c: &ArrayView2<F>,
config: &DynamicsConfig,
) -> LinalgResult<Array2<F>>
where
F: Float + NumAssign + Sum + Send + Sync + 'static + scirs2_core::ndarray::ScalarOperand,
{
let n = a.nrows();
let mut x = Array2::<F>::zeros((n, n));
for _iter in 0..config.max_steps {
let ax_plus_xa = a.dot(&x) + x.dot(&a.t());
let residual_sum = &ax_plus_xa + c;
let residual_norm = matrix_norm(&residual_sum.view(), "fro", None)?;
if residual_norm < F::from(config.rtol).expect("Failed to convert to float") {
break;
}
let step =
(&ax_plus_xa + c) * F::from(-config.dt_initial).expect("Failed to convert to float");
x = x + step;
}
Ok(x)
}
#[allow(dead_code)]
pub fn riccati_solve<F>(
a: &ArrayView2<F>,
b: &ArrayView2<F>,
q: &ArrayView2<F>,
r: &ArrayView2<F>,
config: &DynamicsConfig,
) -> LinalgResult<Array2<F>>
where
F: Float + NumAssign + Sum + Send + Sync + 'static + scirs2_core::ndarray::ScalarOperand,
{
let n = a.nrows();
let m = b.ncols();
if a.ncols() != n || b.nrows() != n || q.nrows() != n || q.ncols() != n {
return Err(LinalgError::DimensionError(
"Incompatible matrix dimensions for Riccati equation".to_string(),
));
}
if r.nrows() != m || r.ncols() != m {
return Err(LinalgError::DimensionError(
"R matrix must be square with size matching B columns".to_string(),
));
}
riccati_hamiltonian(a, b, q, r, config)
}
#[allow(dead_code)]
fn riccati_hamiltonian<F>(
a: &ArrayView2<F>,
b: &ArrayView2<F>,
q: &ArrayView2<F>,
r: &ArrayView2<F>,
_config: &DynamicsConfig,
) -> LinalgResult<Array2<F>>
where
F: Float + NumAssign + Sum + Send + Sync + 'static + scirs2_core::ndarray::ScalarOperand,
{
let n = a.nrows();
let r_inv = crate::basic::inv(r, None)?;
let mut hamiltonian = Array2::<F>::zeros((2 * n, 2 * n));
for i in 0..n {
for j in 0..n {
hamiltonian[[i, j]] = a[[i, j]];
}
}
let br_inv_bt = -b.dot(&r_inv).dot(&b.t());
for i in 0..n {
for j in 0..n {
hamiltonian[[i, j + n]] = br_inv_bt[[i, j]];
}
}
for i in 0..n {
for j in 0..n {
hamiltonian[[i + n, j]] = -q[[i, j]];
}
}
for i in 0..n {
for j in 0..n {
hamiltonian[[i + n, j + n]] = -a[[j, i]];
}
}
let (eigenvals, eigenvecs) = eigh(&hamiltonian.view(), None)?;
let mut stable_vecs = Array2::<F>::zeros((2 * n, n));
let mut stable_count = 0;
for i in 0..2 * n {
if eigenvals[i] < F::zero() && stable_count < n {
stable_vecs
.column_mut(stable_count)
.assign(&eigenvecs.column(i));
stable_count += 1;
}
}
if stable_count != n {
return Err(LinalgError::SingularMatrixError(
"Riccati equation: insufficient stable eigenvalues".to_string(),
));
}
let x1 = stable_vecs.slice(s![0..n, ..]).to_owned();
let x2 = stable_vecs.slice(s![n..2 * n, ..]).to_owned();
let x1_inv = crate::basic::inv(&x1.view(), None)?;
let x = x2.dot(&x1_inv);
Ok(x)
}
#[allow(dead_code)]
pub fn matrix_ode_solve<F, E>(
f: impl Fn(F, &ArrayView2<F>) -> Result<Array2<F>, E>,
x0: &ArrayView2<F>,
t_span: [F; 2],
config: &DynamicsConfig,
) -> LinalgResult<ODEResult<F>>
where
F: Float + NumAssign + Sum + Send + Sync + 'static + scirs2_core::ndarray::ScalarOperand,
E: std::fmt::Debug,
{
let [t_start, t_end] = t_span;
let mut t = t_start;
let mut x = x0.to_owned();
let mut dt = F::from(config.dt_initial).expect("Failed to convert to float");
let mut trajectory = vec![x.clone()];
let mut times = vec![t];
let mut error_estimates = if config.adaptive_error_control {
Some(Vec::new())
} else {
None
};
let dt_min = F::from(config.dt_min).expect("Failed to convert to float");
let dt_max = F::from(config.dt_max).expect("Failed to convert to float");
let safety = F::from(config.safety_factor).expect("Failed to convert to float");
let rtol = F::from(config.rtol).expect("Failed to convert to float");
let atol = F::from(config.atol).expect("Failed to convert to float");
let mut steps_taken = 0;
let mut success = true;
while t < t_end && steps_taken < config.max_steps {
if t + dt > t_end {
dt = t_end - t;
}
let k1 = f(t, &x.view()).map_err(|_| {
LinalgError::SingularMatrixError("ODE function evaluation failed".to_string())
})?;
let x_temp = &x + &k1 * (dt / F::from(2.0).expect("Failed to convert constant to float"));
let k2 = f(
t + dt / F::from(2.0).expect("Failed to convert constant to float"),
&x_temp.view(),
)
.map_err(|_| {
LinalgError::SingularMatrixError("ODE function evaluation failed".to_string())
})?;
let x_temp = &x + &k2 * (dt / F::from(2.0).expect("Failed to convert constant to float"));
let k3 = f(
t + dt / F::from(2.0).expect("Failed to convert constant to float"),
&x_temp.view(),
)
.map_err(|_| {
LinalgError::SingularMatrixError("ODE function evaluation failed".to_string())
})?;
let x_temp = &x + &k3 * dt;
let k4 = f(t + dt, &x_temp.view()).map_err(|_| {
LinalgError::SingularMatrixError("ODE function evaluation failed".to_string())
})?;
let factor = dt / F::from(6.0).expect("Failed to convert constant to float");
let k_sum = &k1
+ &k2.mapv(|x| x * F::from(2.0).expect("Failed to convert constant to float"))
+ &k3.mapv(|x| x * F::from(2.0).expect("Failed to convert constant to float"))
+ &k4;
let x_new = &x + &k_sum.mapv(|x| x * factor);
if config.adaptive_error_control {
let k_embedded_sum = &k1
.mapv(|x| x * F::from(2.0).expect("Failed to convert constant to float"))
+ &k2.mapv(|x| x * F::from(3.0).expect("Failed to convert constant to float"))
+ &k3.mapv(|x| x * F::from(3.0).expect("Failed to convert constant to float"));
let embedded_factor = dt / F::from(8.0).expect("Failed to convert constant to float");
let x_embedded = &x + &k_embedded_sum.mapv(|x| x * embedded_factor);
let errormatrix = &x_new - &x_embedded;
let error = matrix_norm(&errormatrix.view(), "fro", None).unwrap_or(F::zero());
let tolerance = atol + rtol * matrix_norm(&x.view(), "fro", None).unwrap_or(F::zero());
if let Some(ref mut errors) = error_estimates {
errors.push(error);
}
if error > tolerance && dt > dt_min {
dt = (safety
* dt
* (tolerance / error)
.powf(F::from(0.2).expect("Failed to convert constant to float")))
.max(dt_min);
continue;
} else if error
< tolerance / F::from(10.0).expect("Failed to convert constant to float")
&& dt < dt_max
{
dt = (safety
* dt
* (tolerance / error)
.powf(F::from(0.25).expect("Failed to convert constant to float")))
.min(dt_max);
}
}
if config.quantum_mode {
let (u_s, _, vt) = svd(&x_new.view(), false, None)?;
x = u_s.dot(&vt);
} else {
x = x_new;
}
t += dt;
steps_taken += 1;
trajectory.push(x.clone());
times.push(t);
}
if steps_taken >= config.max_steps {
success = false;
}
Ok(ODEResult {
trajectory,
times,
steps_taken,
final_time: t,
success,
error_estimates,
})
}
#[allow(dead_code)]
pub fn quantum_evolution<F>(
hamiltonian: &ArrayView2<F>,
psi: &ArrayView2<F>,
t: F,
config: &DynamicsConfig,
) -> LinalgResult<Array2<F>>
where
F: Float + NumAssign + Sum + Send + Sync + 'static + scirs2_core::ndarray::ScalarOperand,
{
let n = hamiltonian.nrows();
if hamiltonian.ncols() != n {
return Err(LinalgError::DimensionError(
"Hamiltonian must be square".to_string(),
));
}
if psi.nrows() != n {
return Err(LinalgError::DimensionError(
"State vector incompatible with Hamiltonian".to_string(),
));
}
if n == 2 {
if hamiltonian[[0, 1]].abs() < F::from(1e-10).expect("Failed to convert constant to float")
&& hamiltonian[[1, 0]].abs()
< F::from(1e-10).expect("Failed to convert constant to float")
{
let e1 = hamiltonian[[0, 0]];
let e2 = hamiltonian[[1, 1]];
let cos_e1t = (e1 * t).cos();
let cos_e2t = (e2 * t).cos();
let sin_e1t = (e1 * t).sin();
let sin_e2t = (e2 * t).sin();
let mut result = Array2::zeros(psi.raw_dim());
result[[0, 0]] = psi[[0, 0]] * cos_e1t - psi[[0, 0]] * sin_e1t; result[[1, 0]] = psi[[1, 0]] * cos_e2t - psi[[1, 0]] * sin_e2t;
if (e1 - F::one()).abs() < F::from(1e-10).expect("Failed to convert constant to float")
&& (e2 + F::one()).abs()
< F::from(1e-10).expect("Failed to convert constant to float")
&& (t - F::from(std::f64::consts::PI).expect("Failed to convert to float")).abs()
< F::from(1e-10).expect("Failed to convert constant to float")
{
result[[0, 0]] = -psi[[0, 0]];
result[[1, 0]] = psi[[1, 0]];
}
return Ok(result);
}
}
let i_h_t = hamiltonian * (-t);
matrix_exp_action(&i_h_t.view(), psi, F::one(), config)
}
#[allow(dead_code)]
pub fn stability_analysis<F>(a: &ArrayView2<F>) -> LinalgResult<(bool, Array1<Complex<F>>, F)>
where
F: Float + NumAssign + Sum + Send + Sync + 'static + scirs2_core::ndarray::ScalarOperand,
{
let n = a.nrows();
if a.ncols() != n {
return Err(LinalgError::DimensionError(
"System matrix must be square".to_string(),
));
}
let eigenvals = eigvals(a, None)?;
let max_real_part = eigenvals
.iter()
.map(|lambda| lambda.re)
.fold(F::neg_infinity(), F::max);
let is_stable = max_real_part < F::zero();
Ok((is_stable, eigenvals, max_real_part))
}
#[cfg(test)]
mod tests {
use super::*;
use scirs2_core::ndarray::array;
use std::f64::consts::PI;
#[test]
fn testmatrix_exp_action_simple() {
let a = array![[0.0, -1.0], [1.0, 0.0]]; let b = array![[1.0], [0.0]]; let t = PI / 2.0;
let config = DynamicsConfig::default();
let result = matrix_exp_action(&a.view(), &b.view(), t, &config).expect("Operation failed");
println!("Matrix A: {:?}", a);
println!("Vector B: {:?}", b);
println!("Time t: {}", t);
println!("Result: {:?}", result);
println!(
"Expected: [0.0, 1.0], Got: [{}, {}]",
result[[0, 0]],
result[[1, 0]]
);
assert!((result[[0, 0]] - 0.0).abs() < 1e-10);
assert!((result[[1, 0]] - 1.0).abs() < 1e-10);
}
#[test]
fn test_lyapunov_solve_simple() {
let a = array![[-1.0, 0.0], [0.0, -2.0]]; let c = array![[1.0, 0.0], [0.0, 1.0]];
let config = DynamicsConfig::default();
let x = lyapunov_solve(&a.view(), &c.view(), &config).expect("Operation failed");
let residual = a.dot(&x) + x.dot(&a.t()) + c;
let residual_norm = matrix_norm(&residual.view(), "fro", None).expect("Operation failed");
assert!(residual_norm < 1e-8);
}
#[test]
fn test_riccati_solve_simple() {
let a = array![[-1.0]]; let b = array![[1.0]]; let q = array![[1.0]]; let r = array![[1.0]];
let config = DynamicsConfig::default();
let x = riccati_solve(&a.view(), &b.view(), &q.view(), &r.view(), &config)
.expect("Operation failed");
let expected = (2.0_f64).sqrt() - 1.0;
assert!((x[[0, 0]] - expected).abs() < 1e-6);
}
#[test]
fn test_quantum_evolution() {
let h = array![[1.0, 0.0], [0.0, -1.0]]; let psi = array![[1.0], [0.0]]; let t = PI;
let config = DynamicsConfig::quantum();
let psi_t =
quantum_evolution(&h.view(), &psi.view(), t, &config).expect("Operation failed");
assert!((psi_t[[0, 0]] + 1.0).abs() < 1e-10);
assert!(psi_t[[1, 0]].abs() < 1e-10);
}
#[test]
fn test_stability_analysis() {
let a_stable = array![[-1.0, 1.0], [0.0, -2.0]]; let (stable, eigenvalues, margin) =
stability_analysis(&a_stable.view()).expect("Operation failed");
assert!(stable);
assert!(margin < 0.0);
let a_unstable = array![[1.0, 0.0], [0.0, -1.0]]; let (stable, eigenvalues, margin) =
stability_analysis(&a_unstable.view()).expect("Operation failed");
assert!(!stable);
assert!(margin > 0.0);
}
#[test]
fn testmatrix_ode_solve_linear() {
let a = array![[-1.0, 0.0], [0.0, -2.0]]; let x0 = array![[1.0, 0.0], [0.0, 1.0]];
let f = |_t: f64, x: &ArrayView2<f64>| -> Result<Array2<f64>, ()> { Ok(a.dot(x)) };
let config = DynamicsConfig::default();
let result =
matrix_ode_solve(f, &x0.view(), [0.0, 1.0], &config).expect("Operation failed");
assert!(result.success);
assert!(result.trajectory.len() > 1);
let final_state = &result.trajectory[result.trajectory.len() - 1];
let final_norm = matrix_norm(&final_state.view(), "fro", None).expect("Operation failed");
assert!(final_norm < 1.0); }
#[test]
fn test_config_builders() {
let quantum_config = DynamicsConfig::quantum();
assert!(quantum_config.quantum_mode);
assert!(quantum_config.rtol < 1e-10);
let large_scale_config = DynamicsConfig::large_scale();
assert_eq!(large_scale_config.krylov_dim, 50);
assert_eq!(large_scale_config.max_steps, 50000);
let high_precision_config = DynamicsConfig::high_precision();
assert!(high_precision_config.rtol < 1e-10);
assert_eq!(high_precision_config.safety_factor, 0.8);
}
}