use num_complex::Complex;
use crate::density::density::DensityMatrix;
use crate::error::{QuantumError, QuantumResult};
use crate::handle::LcgRng;
use crate::pauli::pauli_string::{PauliOp, PauliString};
use crate::statevec::state::StateVector;
use crate::trotter::lindblad::LindbladOp;
type Complex32 = Complex<f32>;
#[derive(Debug, Clone)]
pub struct TrajectoryConfig {
pub dt: f32,
pub n_steps: usize,
pub n_trajectories: usize,
}
impl TrajectoryConfig {
pub fn new(dt: f32, n_steps: usize, n_trajectories: usize) -> QuantumResult<Self> {
if dt.is_nan() || dt <= 0.0 {
return Err(QuantumError::InvalidParameter { name: "dt".into() });
}
if n_steps == 0 {
return Err(QuantumError::InvalidParameter {
name: "n_steps".into(),
});
}
if n_trajectories == 0 {
return Err(QuantumError::InvalidParameter {
name: "n_trajectories".into(),
});
}
Ok(Self {
dt,
n_steps,
n_trajectories,
})
}
}
#[derive(Debug, Clone)]
pub struct QuantumTrajectory {
n_qubits: usize,
dim: usize,
h_eff: Vec<Complex32>,
jump_mats: Vec<Vec<Complex32>>,
}
impl QuantumTrajectory {
pub fn new(
n_qubits: usize,
ham_terms: &[(f32, Vec<PauliOp>)],
lindblad_ops: &[LindbladOp],
) -> QuantumResult<Self> {
if n_qubits == 0 || n_qubits > 14 {
return Err(QuantumError::InvalidQubitCount { n: n_qubits });
}
let dim = 1usize << n_qubits;
let mut h_eff = build_pauli_sum_matrix(ham_terms, dim, n_qubits)?;
let mut jump_mats = Vec::with_capacity(lindblad_ops.len());
for lop in lindblad_ops {
if lop.ops.len() != n_qubits {
return Err(QuantumError::DimensionMismatch {
expected: n_qubits,
got: lop.ops.len(),
});
}
let gamma = lop.coeff.max(0.0);
let sqrt_gamma = gamma.sqrt();
let l_mat = pauli_string_matrix(&lop.ops, dim, n_qubits)?;
let c_mat: Vec<Complex32> = l_mat.iter().map(|z| z * sqrt_gamma).collect();
let l_dag = mat_adjoint(&l_mat, dim);
let ldagl = mat_mul(&l_dag, &l_mat, dim);
let half_i_gamma = Complex32::new(0.0, -0.5 * gamma);
for (h, g) in h_eff.iter_mut().zip(ldagl.iter()) {
*h += half_i_gamma * g;
}
jump_mats.push(c_mat);
}
Ok(Self {
n_qubits,
dim,
h_eff,
jump_mats,
})
}
#[must_use]
pub fn n_qubits(&self) -> usize {
self.n_qubits
}
#[must_use]
pub fn n_channels(&self) -> usize {
self.jump_mats.len()
}
pub fn step(
&self,
psi: &mut Vec<Complex32>,
dt: f32,
rng: &mut LcgRng,
) -> QuantumResult<Option<usize>> {
if psi.len() != self.dim {
return Err(QuantumError::DimensionMismatch {
expected: self.dim,
got: psi.len(),
});
}
let mut dp_k = vec![0.0_f32; self.jump_mats.len()];
let mut dp_total = 0.0_f32;
for (k, c) in self.jump_mats.iter().enumerate() {
let c_psi = mat_vec(c, psi, self.dim);
let norm_sq: f32 = c_psi.iter().map(|z| z.norm_sqr()).sum();
let w = dt * norm_sq;
dp_k[k] = w;
dp_total += w;
}
let r = rng.next_u32() as f32 / 2f32.powi(32);
if r >= dp_total {
let h_psi = mat_vec(&self.h_eff, psi, self.dim);
let factor = Complex32::new(0.0, -dt); for (p, hp) in psi.iter_mut().zip(h_psi.iter()) {
*p += factor * hp;
}
renormalize(psi);
Ok(None)
} else {
let target = r; let mut acc = 0.0_f32;
let mut chosen = self.jump_mats.len().saturating_sub(1);
for (k, w) in dp_k.iter().enumerate() {
acc += *w;
if target < acc {
chosen = k;
break;
}
}
let c_psi = mat_vec(&self.jump_mats[chosen], psi, self.dim);
*psi = c_psi;
renormalize(psi);
Ok(Some(chosen))
}
}
pub fn evolve_single(
&self,
initial: &StateVector,
cfg: &TrajectoryConfig,
rng: &mut LcgRng,
) -> QuantumResult<StateVector> {
if initial.n_qubits != self.n_qubits {
return Err(QuantumError::DimensionMismatch {
expected: self.n_qubits,
got: initial.n_qubits,
});
}
let mut psi = initial.amps.clone();
renormalize(&mut psi);
for _ in 0..cfg.n_steps {
self.step(&mut psi, cfg.dt, rng)?;
}
Ok(StateVector {
amps: psi,
n_qubits: self.n_qubits,
})
}
pub fn evolve_density(
&self,
initial: &StateVector,
cfg: &TrajectoryConfig,
rng: &mut LcgRng,
) -> QuantumResult<DensityMatrix> {
if initial.n_qubits != self.n_qubits {
return Err(QuantumError::DimensionMismatch {
expected: self.n_qubits,
got: initial.n_qubits,
});
}
let dim = self.dim;
let mut rho = vec![Complex32::new(0.0, 0.0); dim * dim];
let inv_n = 1.0 / cfg.n_trajectories as f32;
for _ in 0..cfg.n_trajectories {
let final_state = self.evolve_single(initial, cfg, rng)?;
for i in 0..dim {
let ai = final_state.amps[i];
for j in 0..dim {
rho[i * dim + j] += inv_n * (ai * final_state.amps[j].conj());
}
}
}
Ok(DensityMatrix { rho, dim })
}
}
fn renormalize(psi: &mut [Complex32]) {
let norm: f32 = psi.iter().map(|z| z.norm_sqr()).sum::<f32>().sqrt();
if norm > 1e-20 {
let inv = 1.0 / norm;
for z in psi.iter_mut() {
*z *= inv;
}
}
}
fn mat_vec(a: &[Complex32], x: &[Complex32], dim: usize) -> Vec<Complex32> {
let mut y = vec![Complex32::new(0.0, 0.0); dim];
for i in 0..dim {
let row = &a[i * dim..i * dim + dim];
let mut acc = Complex32::new(0.0, 0.0);
for (k, xk) in x.iter().enumerate() {
acc += row[k] * xk;
}
y[i] = acc;
}
y
}
fn mat_mul(a: &[Complex32], b: &[Complex32], dim: usize) -> Vec<Complex32> {
let mut c = vec![Complex32::new(0.0, 0.0); dim * dim];
for i in 0..dim {
for j in 0..dim {
let mut acc = Complex32::new(0.0, 0.0);
for k in 0..dim {
acc += a[i * dim + k] * b[k * dim + j];
}
c[i * dim + j] = acc;
}
}
c
}
fn mat_adjoint(a: &[Complex32], dim: usize) -> Vec<Complex32> {
let mut out = vec![Complex32::new(0.0, 0.0); dim * dim];
for i in 0..dim {
for j in 0..dim {
out[j * dim + i] = a[i * dim + j].conj();
}
}
out
}
fn build_pauli_sum_matrix(
terms: &[(f32, Vec<PauliOp>)],
dim: usize,
n_qubits: usize,
) -> QuantumResult<Vec<Complex32>> {
let mut mat = vec![Complex32::new(0.0, 0.0); dim * dim];
for (coeff, ops) in terms {
if ops.len() != n_qubits {
return Err(QuantumError::DimensionMismatch {
expected: n_qubits,
got: ops.len(),
});
}
let term = pauli_string_matrix(ops, dim, n_qubits)?;
for (m, t) in mat.iter_mut().zip(term.iter()) {
*m += coeff * t;
}
}
Ok(mat)
}
fn pauli_string_matrix(
ops: &[PauliOp],
dim: usize,
n_qubits: usize,
) -> QuantumResult<Vec<Complex32>> {
let mut mat = vec![Complex32::new(0.0, 0.0); dim * dim];
let ps = PauliString::new(1.0, ops.to_vec());
for col in 0..dim {
let mut e_col = vec![Complex32::new(0.0, 0.0); dim];
e_col[col] = Complex32::new(1.0, 0.0);
let sv = StateVector {
amps: e_col,
n_qubits,
};
let p_sv = ps.apply_to_state(&sv)?;
for row in 0..dim {
mat[row * dim + col] = p_sv.amps[row];
}
}
Ok(mat)
}
#[cfg(test)]
mod tests {
use super::*;
use crate::density::metrics::purity;
#[test]
fn config_validation() {
assert!(TrajectoryConfig::new(0.0, 10, 10).is_err());
assert!(TrajectoryConfig::new(0.01, 0, 10).is_err());
assert!(TrajectoryConfig::new(0.01, 10, 0).is_err());
assert!(TrajectoryConfig::new(0.01, 10, 10).is_ok());
}
#[test]
fn no_dissipation_is_unitary_norm_preserving() {
let traj = QuantumTrajectory::new(1, &[(1.0, vec![PauliOp::Z])], &[])
.expect("valid 1-qubit trajectory");
let init = StateVector::new_zero_state(1).expect("valid zero state");
let cfg = TrajectoryConfig::new(0.01, 50, 1).expect("valid config");
let mut rng = LcgRng::new(1);
let out = traj
.evolve_single(&init, &cfg, &mut rng)
.expect("evolution succeeds");
let norm: f32 = out.amps.iter().map(|z| z.norm_sqr()).sum();
assert!((norm - 1.0).abs() < 1e-4, "norm={norm}");
}
#[test]
fn amplitude_damping_jumps_relax_excited_to_ground() {
let traj = QuantumTrajectory::new(1, &[], &[LindbladOp::new(1.0, vec![PauliOp::Z])])
.expect("valid trajectory");
let inv_sqrt2 = std::f32::consts::FRAC_1_SQRT_2;
let plus = StateVector {
amps: vec![
Complex32::new(inv_sqrt2, 0.0),
Complex32::new(inv_sqrt2, 0.0),
],
n_qubits: 1,
};
let cfg = TrajectoryConfig::new(0.02, 60, 400).expect("valid config");
let mut rng = LcgRng::new(2024);
let rho = traj
.evolve_density(&plus, &cfg, &mut rng)
.expect("density evolution");
let tr = rho.trace();
assert!((tr.re - 1.0).abs() < 1e-3, "trace={}", tr.re);
let p = purity(&rho);
assert!(p < 0.97, "purity should drop under dephasing, got {p}");
}
#[test]
fn density_trace_preserved_with_hamiltonian_and_jump() {
let traj = QuantumTrajectory::new(
1,
&[(0.5, vec![PauliOp::X])],
&[LindbladOp::new(0.4, vec![PauliOp::Z])],
)
.expect("valid trajectory");
let init = StateVector::new_zero_state(1).expect("valid zero state");
let cfg = TrajectoryConfig::new(0.01, 80, 200).expect("valid config");
let mut rng = LcgRng::new(7);
let rho = traj
.evolve_density(&init, &cfg, &mut rng)
.expect("density evolution");
let tr = rho.trace();
assert!((tr.re - 1.0).abs() < 2e-3, "trace={}", tr.re);
assert!(tr.im.abs() < 1e-3, "trace imag={}", tr.im);
}
#[test]
fn two_qubit_dimension_checks() {
let traj =
QuantumTrajectory::new(2, &[(1.0, vec![PauliOp::Z, PauliOp::Z])], &[]).expect("valid");
let mut bad = vec![Complex32::new(1.0, 0.0); 2];
let mut rng = LcgRng::new(0);
assert!(traj.step(&mut bad, 0.01, &mut rng).is_err());
}
#[test]
fn trajectory_matches_rk4_master_equation() {
use crate::trotter::lindblad_rk4::LindbladRk4;
let h_coeff = 0.6_f32;
let gamma = 0.7_f32;
let ham = [(h_coeff, vec![PauliOp::X])];
let lops = [LindbladOp::new(gamma, vec![PauliOp::Z])];
let inv_sqrt2 = std::f32::consts::FRAC_1_SQRT_2;
let plus = StateVector {
amps: vec![
Complex32::new(inv_sqrt2, 0.0),
Complex32::new(inv_sqrt2, 0.0),
],
n_qubits: 1,
};
let rk4 = LindbladRk4::new(1, &ham, &lops).expect("rk4");
let mut dm_ref = DensityMatrix::from_pure_state(&plus);
let dt = 0.01_f32;
let n_steps = 50usize; rk4.evolve(&mut dm_ref, dt, n_steps).expect("rk4 evolve");
let traj = QuantumTrajectory::new(1, &ham, &lops).expect("traj");
let cfg = TrajectoryConfig::new(dt, n_steps, 4000).expect("cfg");
let mut rng = LcgRng::new(20240620);
let dm_mcwf = traj
.evolve_density(&plus, &cfg, &mut rng)
.expect("mcwf density");
for idx in 0..4 {
let dr = (dm_ref.rho[idx].re - dm_mcwf.rho[idx].re).abs();
let di = (dm_ref.rho[idx].im - dm_mcwf.rho[idx].im).abs();
assert!(
dr < 4e-2 && di < 4e-2,
"entry {idx}: ref={:?} mcwf={:?}",
dm_ref.rho[idx],
dm_mcwf.rho[idx]
);
}
}
}