|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| use tch::{Device, Kind, Tensor};
|
| use thiserror::Error;
|
| use crate::geometry::BuresGeometry;
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| #[derive(Debug, Clone, Copy)]
|
| pub struct SolverConfig {
|
| pub dt: f64,
|
| pub diffusion: f64,
|
| pub total_steps: usize,
|
| }
|
|
|
| impl SolverConfig {
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| pub fn new(dt: f64, diffusion: f64, total_steps: usize) -> Self {
|
| assert!(dt > 0.0, "dt must be positive, got {dt}");
|
| assert!(diffusion >= 0.0, "diffusion must be non-negative, got {diffusion}");
|
| Self { dt, diffusion, total_steps }
|
| }
|
| }
|
|
|
|
|
| #[derive(Debug, Error)]
|
| pub enum StochasticError {
|
|
|
| #[error("Tensor operation failed: {0}")]
|
| TchError(#[from] tch::TchError),
|
|
|
|
|
| #[error("Invalid tensor dimensions: expected [batch, n, n] square matrices")]
|
| DimensionMismatch,
|
|
|
|
|
| #[error("Tensor must be f64 precision for quantum state stability")]
|
| PrecisionError,
|
|
|
|
|
| #[error("Geometry error: {0}")]
|
| GeometryError(String),
|
|
|
|
|
| #[error("Manifold constraint violated: trace={0}, expected 1.0")]
|
| ManifoldViolation(f64),
|
| }
|
|
|
| pub type Result<T> = std::result::Result<T, StochasticError>;
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| pub struct GeometricEulerMaruyama;
|
|
|
| impl GeometricEulerMaruyama {
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| pub fn step(rho: &Tensor, config: &SolverConfig) -> Result<Tensor> {
|
| let result = tch::no_grad(|| {
|
|
|
| if rho.kind() != Kind::Double {
|
| return Err(StochasticError::PrecisionError);
|
| }
|
|
|
| let size = rho.size();
|
| let (is_batched, batch, n) = match size.len() {
|
| 2 => {
|
| if size[0] != size[1] {
|
| return Err(StochasticError::DimensionMismatch);
|
| }
|
| (false, 1i64, size[0])
|
| }
|
| 3 => {
|
| if size[1] != size[2] {
|
| return Err(StochasticError::DimensionMismatch);
|
| }
|
| (true, size[0], size[1])
|
| }
|
| _ => return Err(StochasticError::DimensionMismatch),
|
| };
|
|
|
| let device = rho.device();
|
|
|
|
|
| let rho_batch = if is_batched {
|
| rho.shallow_clone()
|
| } else {
|
| rho.unsqueeze(0)
|
| };
|
|
|
|
|
|
|
|
|
|
|
| let mut drift_components = Vec::with_capacity(batch as usize);
|
| for i in 0..batch {
|
| let rho_i = rho_batch.get(i);
|
| let grad = BuresGeometry::grad_von_neumann_entropy(&rho_i)
|
| .map_err(|e| StochasticError::GeometryError(e.to_string()))?;
|
|
|
| let drift_i = grad * (-config.dt);
|
| drift_components.push(drift_i.unsqueeze(0));
|
| }
|
| let drift = Tensor::cat(&drift_components, 0);
|
| drop(drift_components);
|
|
|
|
|
|
|
|
|
|
|
| let noise = if config.diffusion > 0.0 {
|
|
|
| let z_raw = Tensor::randn([batch, n, n], (Kind::Double, device));
|
|
|
|
|
| let z_herm = (&z_raw + &z_raw.transpose(1, 2)) * 0.5f64;
|
| drop(z_raw);
|
|
|
|
|
|
|
|
|
| let diag_sum = z_herm.diagonal(0, 1, 2).sum_dim_intlist([-1i64].as_slice(), false, Kind::Double);
|
|
|
| let trace_correction = diag_sum.unsqueeze(-1).unsqueeze(-1) / (n as f64);
|
|
|
| let eye = Tensor::eye(n, (Kind::Double, device)).unsqueeze(0);
|
| let z_tangent = &z_herm - &(trace_correction * &eye);
|
| drop(z_herm);
|
|
|
|
|
| let noise_scale = (config.diffusion * config.dt).sqrt();
|
| z_tangent * noise_scale
|
| } else {
|
|
|
| Tensor::zeros([batch, n, n], (Kind::Double, device))
|
| };
|
|
|
|
|
|
|
|
|
|
|
| let rho_tilde = &rho_batch + &drift + &noise;
|
| drop(drift);
|
| drop(noise);
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| let mut projected = Vec::with_capacity(batch as usize);
|
| for i in 0..batch {
|
| let rho_i = rho_tilde.get(i);
|
|
|
|
|
| let rho_sym = (&rho_i + &rho_i.tr()) * 0.5f64;
|
|
|
|
|
| let (evals, evecs) = rho_sym.linalg_eigh("L")?;
|
|
|
|
|
| let evals_clipped = evals.clamp_min(0.0);
|
|
|
|
|
| let trace_sum: f64 = evals_clipped.sum(Kind::Double).double_value(&[]);
|
| let evals_normed = if trace_sum > 1e-12 {
|
| &evals_clipped / trace_sum
|
| } else {
|
|
|
| Tensor::ones([n], (Kind::Double, device)) / (n as f64)
|
| };
|
|
|
|
|
| let diag_matrix = Tensor::diag_embed(&evals_normed, 0, -2, -1);
|
| let rho_proj = evecs.matmul(&diag_matrix).matmul(&evecs.tr());
|
|
|
| projected.push(rho_proj.unsqueeze(0));
|
| }
|
| drop(rho_tilde);
|
|
|
| let rho_next = Tensor::cat(&projected, 0);
|
| drop(projected);
|
|
|
|
|
| if is_batched {
|
| Ok(rho_next)
|
| } else {
|
| Ok(rho_next.squeeze_dim(0))
|
| }
|
| });
|
| result
|
| }
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| pub fn solve(rho_init: &Tensor, config: &SolverConfig) -> Result<Tensor> {
|
| let mut rho = rho_init.shallow_clone();
|
| for _step in 0..config.total_steps {
|
| rho = Self::step(&rho, config)?;
|
| }
|
| Ok(rho)
|
| }
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| pub fn solve_trajectory(rho_init: &Tensor, config: &SolverConfig) -> Result<Vec<Tensor>> {
|
| let mut trajectory = Vec::with_capacity(config.total_steps + 1);
|
| trajectory.push(rho_init.shallow_clone());
|
|
|
| let mut rho = rho_init.shallow_clone();
|
| for _step in 0..config.total_steps {
|
| rho = Self::step(&rho, config)?;
|
| trajectory.push(rho.shallow_clone());
|
| }
|
| Ok(trajectory)
|
| }
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| pub fn monte_carlo_expectation(
|
| rho_init: &Tensor,
|
| config: &SolverConfig,
|
| observable: &Tensor,
|
| ) -> Result<f64> {
|
| let result = tch::no_grad(|| {
|
|
|
| let rho_final = Self::solve(rho_init, config)?;
|
|
|
| let batch = rho_final.size()[0];
|
| let n = rho_final.size()[1];
|
|
|
|
|
|
|
| let obs_expanded = observable.unsqueeze(0).expand([batch, n, n], false);
|
| let product = obs_expanded.matmul(&rho_final);
|
|
|
|
|
| let traces = product.diagonal(0, 1, 2).sum_dim_intlist([-1i64].as_slice(), false, Kind::Double);
|
|
|
|
|
|
|
| let mean: f64 = traces.mean(Kind::Double).double_value(&[]);
|
| Ok(mean)
|
| });
|
| result
|
| }
|
| }
|
|
|
|
|
| #[cfg(test)]
|
| mod tests {
|
| use super::*;
|
|
|
|
|
| fn maximally_mixed_batch(batch: i64, n: i64, device: Device) -> Tensor {
|
| let eye = Tensor::eye(n, (Kind::Double, device)) / (n as f64);
|
| eye.unsqueeze(0).expand([batch, n, n], false).contiguous()
|
| }
|
|
|
|
|
| fn maximally_mixed(n: i64, device: Device) -> Tensor {
|
| Tensor::eye(n, (Kind::Double, device)) / (n as f64)
|
| }
|
|
|
|
|
| fn pure_state(n: i64, device: Device) -> Tensor {
|
| let mut rho = Tensor::zeros([n, n], (Kind::Double, device));
|
| let _ = rho.narrow(0, 0, 1).narrow(1, 0, 1).fill_(1.0);
|
| rho
|
| }
|
|
|
| #[test]
|
| fn test_manifold_preservation_trace() {
|
| let device = Device::Cpu;
|
| let config = SolverConfig::new(0.01, 0.1, 10);
|
| let rho_init = maximally_mixed(3, device);
|
|
|
| let rho_final = GeometricEulerMaruyama::solve(&rho_init, &config)
|
| .expect("Solver failed");
|
|
|
| let trace: f64 = rho_final.trace().double_value(&[]);
|
| assert!(
|
| (trace - 1.0).abs() < 1e-8,
|
| "Trace not preserved: got {trace}, expected 1.0"
|
| );
|
| }
|
|
|
| #[test]
|
| fn test_manifold_preservation_psd() {
|
| let device = Device::Cpu;
|
| let config = SolverConfig::new(0.01, 0.05, 10);
|
| let rho_init = maximally_mixed(4, device);
|
|
|
| let rho_final = GeometricEulerMaruyama::solve(&rho_init, &config)
|
| .expect("Solver failed");
|
|
|
|
|
| let (evals, _) = rho_final.linalg_eigh("L").expect("Eigendecomp failed");
|
| let min_eval: f64 = evals.min().double_value(&[]);
|
| assert!(
|
| min_eval >= -1e-10,
|
| "PSD violated: min eigenvalue = {min_eval}"
|
| );
|
| }
|
|
|
| #[test]
|
| fn test_manifold_preservation_hermitian() {
|
| let device = Device::Cpu;
|
| let config = SolverConfig::new(0.01, 0.1, 5);
|
| let rho_init = maximally_mixed(3, device);
|
|
|
| let rho_final = GeometricEulerMaruyama::solve(&rho_init, &config)
|
| .expect("Solver failed");
|
|
|
|
|
| assert!(
|
| BuresGeometry::is_symmetric(&rho_final, 1e-10),
|
| "Output not Hermitian/symmetric"
|
| );
|
| }
|
|
|
| #[test]
|
| fn test_zero_diffusion_deterministic() {
|
| let device = Device::Cpu;
|
| let config = SolverConfig::new(0.01, 0.0, 5);
|
| let rho_init = maximally_mixed(3, device);
|
|
|
| let rho_final = GeometricEulerMaruyama::solve(&rho_init, &config)
|
| .expect("Solver failed");
|
|
|
| let trace: f64 = rho_final.trace().double_value(&[]);
|
| assert!(
|
| (trace - 1.0).abs() < 1e-10,
|
| "Deterministic flow trace violated: {trace}"
|
| );
|
|
|
|
|
| let expected = maximally_mixed(3, device);
|
| let diff: f64 = (&rho_final - &expected).abs().max().double_value(&[]);
|
|
|
| assert!(
|
| diff < 1e-8,
|
| "Deterministic flow moved maximally mixed state: max diff = {diff}"
|
| );
|
| }
|
|
|
| #[test]
|
| fn test_batched_execution() {
|
| let device = Device::Cpu;
|
| let config = SolverConfig::new(0.005, 0.05, 3);
|
| let batch_size = 16i64;
|
| let n = 3i64;
|
| let rho_init = maximally_mixed_batch(batch_size, n, device);
|
|
|
| let rho_final = GeometricEulerMaruyama::solve(&rho_init, &config)
|
| .expect("Batched solver failed");
|
|
|
| assert_eq!(rho_final.size(), &[batch_size, n, n]);
|
|
|
|
|
| for i in 0..batch_size {
|
| let trace: f64 = rho_final.get(i).trace().double_value(&[]);
|
| assert!(
|
| (trace - 1.0).abs() < 1e-8,
|
| "Batch element {i}: trace = {trace}"
|
| );
|
| }
|
| }
|
|
|
| #[test]
|
| fn test_trajectory_length() {
|
| let device = Device::Cpu;
|
| let steps = 7;
|
| let config = SolverConfig::new(0.01, 0.0, steps);
|
| let rho_init = maximally_mixed(2, device);
|
|
|
| let trajectory = GeometricEulerMaruyama::solve_trajectory(&rho_init, &config)
|
| .expect("Trajectory failed");
|
|
|
|
|
| assert_eq!(trajectory.len(), steps + 1);
|
|
|
|
|
| for (i, state) in trajectory.iter().enumerate() {
|
| let trace: f64 = state.trace().double_value(&[]);
|
| assert!(
|
| (trace - 1.0).abs() < 1e-8,
|
| "Trajectory step {i}: trace = {trace}"
|
| );
|
| }
|
| }
|
|
|
| #[test]
|
| fn test_pure_state_evolution() {
|
| let device = Device::Cpu;
|
| let config = SolverConfig::new(0.005, 0.01, 5);
|
| let rho_init = pure_state(3, device);
|
|
|
| let rho_final = GeometricEulerMaruyama::solve(&rho_init, &config)
|
| .expect("Pure state evolution failed");
|
|
|
|
|
| let trace: f64 = rho_final.trace().double_value(&[]);
|
| assert!((trace - 1.0).abs() < 1e-8, "Trace violated: {trace}");
|
|
|
| let (evals, _) = rho_final.linalg_eigh("L").unwrap();
|
| let min_eval: f64 = evals.min().double_value(&[]);
|
| assert!(min_eval >= -1e-10, "PSD violated: min eval = {min_eval}");
|
| }
|
|
|
| #[test]
|
| fn test_precision_enforcement() {
|
| let config = SolverConfig::new(0.01, 0.1, 1);
|
| let rho_f32 = Tensor::eye(3, (Kind::Float, Device::Cpu)) / 3.0f64;
|
|
|
| assert!(matches!(
|
| GeometricEulerMaruyama::step(&rho_f32, &config),
|
| Err(StochasticError::PrecisionError)
|
| ));
|
| }
|
|
|
| #[test]
|
| #[should_panic(expected = "dt must be positive")]
|
| fn test_config_negative_dt() {
|
| SolverConfig::new(-0.01, 0.1, 10);
|
| }
|
|
|
| #[test]
|
| #[should_panic(expected = "diffusion must be non-negative")]
|
| fn test_config_negative_diffusion() {
|
| SolverConfig::new(0.01, -0.1, 10);
|
| }
|
| }
|
|
|