use crate::error::{IntegrateError, IntegrateResult};
use scirs2_core::ndarray::{Array1, Array2};
#[derive(Debug, Clone)]
pub struct CMatrix {
pub re: Array2<f64>,
pub im: Array2<f64>,
}
impl CMatrix {
pub fn zeros(n: usize) -> Self {
Self {
re: Array2::zeros((n, n)),
im: Array2::zeros((n, n)),
}
}
pub fn eye(n: usize) -> Self {
Self {
re: Array2::eye(n),
im: Array2::zeros((n, n)),
}
}
pub fn from_parts(re: Array2<f64>, im: Array2<f64>) -> Self {
assert_eq!(re.shape(), im.shape(), "re and im must have the same shape");
Self { re, im }
}
pub fn n(&self) -> usize {
self.re.nrows()
}
pub fn adjoint(&self) -> Self {
let n = self.n();
let mut re = Array2::zeros((n, n));
let mut im = Array2::zeros((n, n));
for i in 0..n {
for j in 0..n {
re[[i, j]] = self.re[[j, i]];
im[[i, j]] = -self.im[[j, i]];
}
}
Self { re, im }
}
pub fn mul(&self, other: &Self) -> Self {
let n = self.n();
let mut re = Array2::zeros((n, n));
let mut im = Array2::zeros((n, n));
for i in 0..n {
for j in 0..n {
let mut r = 0.0_f64;
let mut c = 0.0_f64;
for k in 0..n {
r += self.re[[i, k]] * other.re[[k, j]] - self.im[[i, k]] * other.im[[k, j]];
c += self.re[[i, k]] * other.im[[k, j]] + self.im[[i, k]] * other.re[[k, j]];
}
re[[i, j]] = r;
im[[i, j]] = c;
}
}
Self { re, im }
}
pub fn add(&self, other: &Self) -> Self {
Self {
re: &self.re + &other.re,
im: &self.im + &other.im,
}
}
pub fn sub(&self, other: &Self) -> Self {
Self {
re: &self.re - &other.re,
im: &self.im - &other.im,
}
}
pub fn scale(&self, s: f64) -> Self {
Self {
re: &self.re * s,
im: &self.im * s,
}
}
pub fn commutator(&self, rho: &Self) -> Self {
self.mul(rho).sub(&rho.mul(self))
}
pub fn anticommutator(&self, b: &Self) -> Self {
self.mul(b).add(&b.mul(self))
}
pub fn trace_norm(&self) -> f64 {
let re_sq: f64 = self.re.iter().map(|x| x * x).sum();
let im_sq: f64 = self.im.iter().map(|x| x * x).sum();
(re_sq + im_sq).sqrt()
}
pub fn trace(&self) -> (f64, f64) {
let n = self.n();
let re = (0..n).map(|i| self.re[[i, i]]).sum();
let im = (0..n).map(|i| self.im[[i, i]]).sum();
(re, im)
}
pub fn to_vec(&self) -> Array1<f64> {
let n = self.n();
let mut v = Array1::zeros(2 * n * n);
for i in 0..n {
for j in 0..n {
v[i * n + j] = self.re[[i, j]];
v[n * n + i * n + j] = self.im[[i, j]];
}
}
v
}
pub fn from_vec(v: &Array1<f64>, n: usize) -> Self {
let mut re = Array2::zeros((n, n));
let mut im = Array2::zeros((n, n));
for i in 0..n {
for j in 0..n {
re[[i, j]] = v[i * n + j];
im[[i, j]] = v[n * n + i * n + j];
}
}
Self { re, im }
}
pub fn purity(&self) -> f64 {
let rho2 = self.mul(self);
rho2.trace().0
}
}
#[non_exhaustive]
#[derive(Debug, Clone)]
pub enum LindbladMethod {
EulerMaruyama,
RungeKutta4,
}
#[derive(Debug, Clone)]
pub struct LindbladConfig {
pub hbar: f64,
pub t_span: [f64; 2],
pub n_steps: usize,
pub method: LindbladMethod,
}
impl Default for LindbladConfig {
fn default() -> Self {
Self {
hbar: 1.0,
t_span: [0.0, 1.0],
n_steps: 100,
method: LindbladMethod::RungeKutta4,
}
}
}
pub struct LindbladSystem {
pub hamiltonian: CMatrix,
pub lindblad_ops: Vec<(f64, CMatrix)>,
}
pub struct LindbladResult {
pub times: Array1<f64>,
pub density_matrices: Vec<CMatrix>,
pub purity: Array1<f64>,
pub trace: Array1<f64>,
}
fn lindblad_rhs(system: &LindbladSystem, rho: &CMatrix, hbar: f64) -> CMatrix {
let n = rho.n();
let comm = system.hamiltonian.commutator(rho); let coherent = CMatrix {
re: &comm.im / hbar,
im: -&comm.re / hbar,
};
let mut dissipative = CMatrix::zeros(n);
for (gamma, l) in &system.lindblad_ops {
let l_dag = l.adjoint();
let l_dag_l = l_dag.mul(l);
let lrho = l.mul(rho);
let lrho_ldag = lrho.mul(&l_dag);
let anti = l_dag_l.anticommutator(rho).scale(0.5);
let term = lrho_ldag.sub(&anti).scale(*gamma);
dissipative = dissipative.add(&term);
}
coherent.add(&dissipative)
}
pub fn lindblad_evolve(
system: &LindbladSystem,
rho0: &CMatrix,
config: &LindbladConfig,
) -> IntegrateResult<LindbladResult> {
if config.n_steps == 0 {
return Err(IntegrateError::InvalidInput(
"n_steps must be > 0".to_string(),
));
}
let n = rho0.n();
if n == 0 {
return Err(IntegrateError::InvalidInput(
"density matrix must be at least 1×1".to_string(),
));
}
if system.hamiltonian.n() != n {
return Err(IntegrateError::DimensionMismatch(format!(
"Hamiltonian dimension {} ≠ rho dimension {}",
system.hamiltonian.n(),
n
)));
}
for (k, (_, lk)) in system.lindblad_ops.iter().enumerate() {
if lk.n() != n {
return Err(IntegrateError::DimensionMismatch(format!(
"Lindblad operator {} has dimension {} ≠ rho dimension {}",
k,
lk.n(),
n
)));
}
}
let (tr_re, _tr_im) = rho0.trace();
if (tr_re - 1.0).abs() > 1e-4 {
return Err(IntegrateError::InvalidInput(format!(
"Initial density matrix trace must be 1, got {tr_re:.6}"
)));
}
let t_start = config.t_span[0];
let t_end = config.t_span[1];
let n_steps = config.n_steps;
let h = (t_end - t_start) / n_steps as f64;
let mut times = Array1::zeros(n_steps + 1);
let mut density_matrices = Vec::with_capacity(n_steps + 1);
let mut purity_arr = Array1::zeros(n_steps + 1);
let mut trace_arr = Array1::zeros(n_steps + 1);
times[0] = t_start;
purity_arr[0] = rho0.purity();
trace_arr[0] = rho0.trace().0;
let mut rho = rho0.clone();
density_matrices.push(rho.clone());
for step in 0..n_steps {
let t = t_start + step as f64 * h;
match config.method {
LindbladMethod::EulerMaruyama => {
let drho = lindblad_rhs(system, &rho, config.hbar);
rho = rho.add(&drho.scale(h));
}
LindbladMethod::RungeKutta4 => {
let k1 = lindblad_rhs(system, &rho, config.hbar);
let rho2 = rho.add(&k1.scale(h / 2.0));
let k2 = lindblad_rhs(system, &rho2, config.hbar);
let rho3 = rho.add(&k2.scale(h / 2.0));
let k3 = lindblad_rhs(system, &rho3, config.hbar);
let rho4 = rho.add(&k3.scale(h));
let k4 = lindblad_rhs(system, &rho4, config.hbar);
let update = k1
.add(&k2.scale(2.0))
.add(&k3.scale(2.0))
.add(&k4)
.scale(h / 6.0);
rho = rho.add(&update);
}
#[allow(unreachable_patterns)]
_ => {
return Err(IntegrateError::NotImplementedError(
"Unknown LindbladMethod variant".to_string(),
));
}
}
times[step + 1] = t + h;
purity_arr[step + 1] = rho.purity();
trace_arr[step + 1] = rho.trace().0;
density_matrices.push(rho.clone());
}
Ok(LindbladResult {
times,
density_matrices,
purity: purity_arr,
trace: trace_arr,
})
}
pub fn steady_state(system: &LindbladSystem) -> IntegrateResult<CMatrix> {
let n = system.hamiltonian.n();
let rho0 = CMatrix {
re: Array2::eye(n) / n as f64,
im: Array2::zeros((n, n)),
};
let config = LindbladConfig {
hbar: 1.0,
t_span: [0.0, 50.0],
n_steps: 5000,
method: LindbladMethod::RungeKutta4,
};
let result = lindblad_evolve(system, &rho0, &config)?;
let np = result.purity.len();
if np < 2 {
return Ok(result.density_matrices[0].clone());
}
let purity_last = result.purity[np - 1];
let purity_prev = result.purity[np - 100.min(np - 1)];
if (purity_last - purity_prev).abs() > 1e-4 {
let config2 = LindbladConfig {
t_span: [0.0, 200.0],
n_steps: 20_000,
..config
};
let result2 = lindblad_evolve(system, &rho0, &config2)?;
let n2 = result2.density_matrices.len();
return Ok(result2.density_matrices[n2 - 1].clone());
}
let nd = result.density_matrices.len();
Ok(result.density_matrices[nd - 1].clone())
}
#[cfg(test)]
mod tests {
use super::*;
use scirs2_core::ndarray::Array2;
fn make_rho0_ground(n: usize) -> CMatrix {
let mut re = Array2::<f64>::zeros((n, n));
re[[0, 0]] = 1.0;
CMatrix::from_parts(re, Array2::zeros((n, n)))
}
fn make_rho0_excited(n: usize) -> CMatrix {
let mut re = Array2::<f64>::zeros((n, n));
re[[n - 1, n - 1]] = 1.0;
CMatrix::from_parts(re, Array2::zeros((n, n)))
}
fn make_equal_superposition(n: usize) -> CMatrix {
CMatrix {
re: Array2::eye(n) / n as f64,
im: Array2::zeros((n, n)),
}
}
#[test]
fn test_cmatrix_adjoint_involution() {
let re = Array2::from_shape_vec((2, 2), vec![1.0, 2.0, 3.0, 4.0]).unwrap();
let im = Array2::from_shape_vec((2, 2), vec![0.5, -0.5, 1.0, -1.0]).unwrap();
let a = CMatrix::from_parts(re, im);
let a_dag_dag = a.adjoint().adjoint();
let diff = a.sub(&a_dag_dag);
assert!(diff.trace_norm() < 1e-12, "adjoint involution failed");
}
#[test]
fn test_cmatrix_commutator_self_zero() {
let re = Array2::eye(2);
let im = Array2::zeros((2, 2));
let a = CMatrix::from_parts(re, im);
let comm = a.commutator(&a);
assert!(comm.trace_norm() < 1e-12, "[A,A] should be zero");
}
#[test]
fn test_cmatrix_trace_eye() {
let n = 3;
let eye = CMatrix::eye(n);
let (tr, _) = eye.trace();
assert!((tr - n as f64).abs() < 1e-14);
}
#[test]
fn test_cmatrix_to_from_vec_roundtrip() {
let n = 2;
let re = Array2::from_shape_vec((n, n), vec![1.0, 2.0, 3.0, 4.0]).unwrap();
let im = Array2::from_shape_vec((n, n), vec![5.0, 6.0, 7.0, 8.0]).unwrap();
let m = CMatrix::from_parts(re, im);
let v = m.to_vec();
let m2 = CMatrix::from_vec(&v, n);
let diff = m.sub(&m2);
assert!(diff.trace_norm() < 1e-14);
}
#[test]
fn test_zero_h_no_lindblad_constant_rho() {
let n = 2;
let system = LindbladSystem {
hamiltonian: CMatrix::zeros(n),
lindblad_ops: vec![],
};
let rho0 = make_equal_superposition(n);
let config = LindbladConfig {
t_span: [0.0, 2.0],
n_steps: 200,
..Default::default()
};
let result = lindblad_evolve(&system, &rho0, &config).unwrap();
let rho_final = &result.density_matrices[result.density_matrices.len() - 1];
let diff = rho0.sub(rho_final);
assert!(
diff.trace_norm() < 1e-10,
"rho should not change with H=0, no Lindblad"
);
}
#[test]
fn test_trace_preserved() {
let n = 2;
let mut l_re = Array2::<f64>::zeros((n, n));
l_re[[0, 1]] = 1.0; let l = CMatrix::from_parts(l_re, Array2::zeros((n, n)));
let system = LindbladSystem {
hamiltonian: CMatrix::zeros(n),
lindblad_ops: vec![(0.5, l)],
};
let rho0 = make_rho0_excited(n);
let config = LindbladConfig {
t_span: [0.0, 3.0],
n_steps: 300,
..Default::default()
};
let result = lindblad_evolve(&system, &rho0, &config).unwrap();
for tr in result.trace.iter() {
assert!((tr - 1.0).abs() < 1e-5, "Trace deviated from 1: {tr}");
}
}
#[test]
fn test_purity_leq_one() {
let n = 2;
let mut l_re = Array2::<f64>::zeros((n, n));
l_re[[0, 1]] = 1.0;
let l = CMatrix::from_parts(l_re, Array2::zeros((n, n)));
let system = LindbladSystem {
hamiltonian: CMatrix::zeros(n),
lindblad_ops: vec![(1.0, l)],
};
let rho0 = make_rho0_excited(n);
let config = LindbladConfig {
t_span: [0.0, 2.0],
n_steps: 200,
..Default::default()
};
let result = lindblad_evolve(&system, &rho0, &config).unwrap();
for (i, p) in result.purity.iter().enumerate() {
assert!(*p <= 1.0 + 1e-6, "Purity > 1 at step {i}: {p}");
}
}
#[test]
fn test_unitary_evolution_purity_one() {
let n = 2;
let mut h_re = Array2::<f64>::zeros((n, n));
h_re[[0, 0]] = 1.0;
h_re[[1, 1]] = -1.0;
let h = CMatrix::from_parts(h_re, Array2::zeros((n, n)));
let system = LindbladSystem {
hamiltonian: h,
lindblad_ops: vec![],
};
let rho0 = make_rho0_ground(n); let config = LindbladConfig {
t_span: [0.0, 1.0],
n_steps: 1000,
..Default::default()
};
let result = lindblad_evolve(&system, &rho0, &config).unwrap();
for (i, p) in result.purity.iter().enumerate() {
assert!(
(p - 1.0).abs() < 1e-4,
"Purity deviated from 1 at step {i}: {p}"
);
}
}
#[test]
fn test_two_level_decay_to_ground() {
let n = 2;
let mut l_re = Array2::<f64>::zeros((n, n));
l_re[[0, 1]] = 1.0; let l = CMatrix::from_parts(l_re, Array2::zeros((n, n)));
let system = LindbladSystem {
hamiltonian: CMatrix::zeros(n),
lindblad_ops: vec![(2.0, l)],
};
let rho0 = make_rho0_excited(n);
let config = LindbladConfig {
t_span: [0.0, 5.0],
n_steps: 1000,
..Default::default()
};
let result = lindblad_evolve(&system, &rho0, &config).unwrap();
let rho_final = &result.density_matrices[result.density_matrices.len() - 1];
assert!(
rho_final.re[[0, 0]] > 0.99,
"Ground state population = {}; expected > 0.99",
rho_final.re[[0, 0]]
);
}
#[test]
fn test_dephasing_kills_offdiag() {
let n = 2;
let mut l_re = Array2::<f64>::zeros((n, n));
l_re[[0, 0]] = 1.0;
l_re[[1, 1]] = -1.0;
let l = CMatrix::from_parts(l_re, Array2::zeros((n, n)));
let system = LindbladSystem {
hamiltonian: CMatrix::zeros(n),
lindblad_ops: vec![(1.0, l)],
};
let rho0 = CMatrix::from_parts(
Array2::from_shape_vec((2, 2), vec![0.5, 0.5, 0.5, 0.5]).unwrap(),
Array2::zeros((2, 2)),
);
let config = LindbladConfig {
t_span: [0.0, 5.0],
n_steps: 500,
..Default::default()
};
let result = lindblad_evolve(&system, &rho0, &config).unwrap();
let rho_final = &result.density_matrices[result.density_matrices.len() - 1];
assert!(
rho_final.re[[0, 1]].abs() < 0.01,
"Off-diagonal not decayed: {}",
rho_final.re[[0, 1]]
);
}
#[test]
fn test_steady_state_decay_is_ground() {
let n = 2;
let mut l_re = Array2::<f64>::zeros((n, n));
l_re[[0, 1]] = 1.0;
let l = CMatrix::from_parts(l_re, Array2::zeros((n, n)));
let system = LindbladSystem {
hamiltonian: CMatrix::zeros(n),
lindblad_ops: vec![(2.0, l)],
};
let ss = steady_state(&system).unwrap();
assert!(
ss.re[[0, 0]] > 0.99,
"Steady-state ground pop = {}",
ss.re[[0, 0]]
);
}
#[test]
fn test_invalid_trace_rho0() {
let n = 2;
let system = LindbladSystem {
hamiltonian: CMatrix::zeros(n),
lindblad_ops: vec![],
};
let rho0 = CMatrix::from_parts(Array2::eye(n), Array2::zeros((n, n)));
let config = LindbladConfig::default();
let result = lindblad_evolve(&system, &rho0, &config);
assert!(result.is_err(), "Should error on trace ≠ 1");
}
#[test]
fn test_invalid_n_steps_zero() {
let n = 2;
let system = LindbladSystem {
hamiltonian: CMatrix::zeros(n),
lindblad_ops: vec![],
};
let rho0 = make_rho0_ground(n);
let config = LindbladConfig {
n_steps: 0,
..Default::default()
};
let result = lindblad_evolve(&system, &rho0, &config);
assert!(result.is_err(), "Should error on n_steps=0");
}
#[test]
fn test_identity_rho_stays_identity_over_n() {
let n = 3;
let system = LindbladSystem {
hamiltonian: CMatrix::zeros(n),
lindblad_ops: vec![],
};
let rho0 = make_equal_superposition(n);
let config = LindbladConfig {
t_span: [0.0, 1.0],
n_steps: 100,
..Default::default()
};
let result = lindblad_evolve(&system, &rho0, &config).unwrap();
let rho_final = &result.density_matrices[result.density_matrices.len() - 1];
let diff = rho0.sub(rho_final);
assert!(diff.trace_norm() < 1e-10, "I/n state drifted");
}
#[test]
fn test_dimension_mismatch_error() {
let n = 2;
let system = LindbladSystem {
hamiltonian: CMatrix::zeros(n + 1), lindblad_ops: vec![],
};
let rho0 = make_rho0_ground(n);
let config = LindbladConfig::default();
let result = lindblad_evolve(&system, &rho0, &config);
assert!(result.is_err(), "Should error on dimension mismatch");
}
#[test]
fn test_euler_method_trace_preserved() {
let n = 2;
let mut l_re = Array2::<f64>::zeros((n, n));
l_re[[0, 1]] = 1.0;
let l = CMatrix::from_parts(l_re, Array2::zeros((n, n)));
let system = LindbladSystem {
hamiltonian: CMatrix::zeros(n),
lindblad_ops: vec![(0.5, l)],
};
let rho0 = make_rho0_excited(n);
let config = LindbladConfig {
t_span: [0.0, 0.5],
n_steps: 5000, method: LindbladMethod::EulerMaruyama,
..Default::default()
};
let result = lindblad_evolve(&system, &rho0, &config).unwrap();
let tr_final = result.trace[result.trace.len() - 1];
assert!(
(tr_final - 1.0).abs() < 1e-3,
"Euler trace deviated: {tr_final}"
);
}
}