|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| use tch::{Device, Kind, Tensor};
|
| use thiserror::Error;
|
| use crate::algebra::{AlgebraError, JordanTensor};
|
|
|
|
|
| #[derive(Debug, Error)]
|
| pub enum GeometryError {
|
|
|
| #[error("Tensor operation failed: {0}")]
|
| TchError(#[from] tch::TchError),
|
|
|
|
|
| #[error("Algebra error: {0}")]
|
| AlgebraError(#[from] AlgebraError),
|
|
|
|
|
| #[error("Invalid tensor dimensions: expected {0}x{0} square matrix")]
|
| DimensionMismatch(i64),
|
|
|
|
|
| #[error("Tensor must be f64 precision for quantum state stability")]
|
| PrecisionError,
|
|
|
|
|
| #[error("Input tensor must be Hermitian (within tolerance)")]
|
| NonHermitian,
|
|
|
|
|
| #[error("Input must be a valid density matrix (Hermitian, trace=1, PSD)")]
|
| NotDensityMatrix,
|
|
|
|
|
| #[error("Tangent vector must be traceless (trace=0)")]
|
| NotTraceless,
|
| }
|
|
|
| pub type Result<T> = std::result::Result<T, GeometryError>;
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| pub struct BuresGeometry;
|
|
|
| impl BuresGeometry {
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| pub fn solve_lyapunov(rho: &Tensor, c: &Tensor) -> Result<Tensor> {
|
| let result = tch::no_grad(|| {
|
|
|
| Self::validate_f64(rho)?;
|
| Self::validate_f64(c)?;
|
| Self::validate_square(rho)?;
|
| Self::validate_square(c)?;
|
| let n = rho.size()[0];
|
| if c.size()[0] != n || c.size()[1] != n {
|
| return Err(GeometryError::DimensionMismatch(n));
|
| }
|
|
|
|
|
|
|
| let (evals, evecs) = rho.linalg_eigh("L")?;
|
|
|
|
|
| let v_t = evecs.tr();
|
| let c_t = v_t.matmul(c).matmul(&evecs);
|
|
|
|
|
|
|
| let evals_col = evals.unsqueeze(1);
|
| let evals_row = evals.unsqueeze(0);
|
| let denom = (&evals_col + &evals_row).clamp_min(1e-12);
|
|
|
|
|
| let m_t = &c_t / &denom;
|
|
|
|
|
| let g = evecs.matmul(&m_t).matmul(&v_t);
|
|
|
| Ok(g)
|
| });
|
| result
|
| }
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| pub fn bures_metric_operator(rho: &Tensor, delta_rho: &Tensor) -> Result<Tensor> {
|
| let result = tch::no_grad(|| {
|
| Self::validate_f64(rho)?;
|
| Self::validate_f64(delta_rho)?;
|
| Self::validate_square(rho)?;
|
| Self::validate_square(delta_rho)?;
|
|
|
| let n = rho.size()[0];
|
| if delta_rho.size()[0] != n {
|
| return Err(GeometryError::DimensionMismatch(n));
|
| }
|
|
|
|
|
| let trace_val: f64 = delta_rho.trace().double_value(&[]);
|
| if trace_val.abs() > 1e-6 {
|
| return Err(GeometryError::NotTraceless);
|
| }
|
|
|
|
|
| let two_delta = delta_rho * 2.0f64;
|
| Self::solve_lyapunov(rho, &two_delta)
|
| });
|
| result
|
| }
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| pub fn bures_inner_product(rho: &Tensor, u: &Tensor, v: &Tensor) -> Result<f64> {
|
| let result = tch::no_grad(|| {
|
|
|
| let g_v = Self::bures_metric_operator(rho, v)?;
|
|
|
| let product = u.matmul(&g_v);
|
| let trace: f64 = product.trace().double_value(&[]);
|
| Ok(0.5 * trace)
|
| });
|
| result
|
| }
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| pub fn grad_von_neumann_entropy(rho: &Tensor) -> Result<Tensor> {
|
| let result = tch::no_grad(|| {
|
| Self::validate_f64(rho)?;
|
| Self::validate_square(rho)?;
|
|
|
| let n = rho.size()[0];
|
|
|
|
|
|
|
| let (evals, evecs) = rho.linalg_eigh("L")?;
|
|
|
|
|
|
|
| let log_evals = evals.clamp_min(1e-300).log();
|
|
|
|
|
| let log_diag = Tensor::diag_embed(&log_evals, 0, -2, -1);
|
| let log_rho = evecs.matmul(&log_diag).matmul(&evecs.tr());
|
|
|
|
|
|
|
| let jordan_result = rho.jordan_product(&log_rho)
|
| .map_err(|e| GeometryError::AlgebraError(e))?;
|
|
|
|
|
| let grad = jordan_result * (-4.0f64);
|
|
|
| Ok(grad)
|
| });
|
| result
|
| }
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| pub fn von_neumann_entropy(rho: &Tensor) -> Result<f64> {
|
| let result = tch::no_grad(|| {
|
| Self::validate_f64(rho)?;
|
| Self::validate_square(rho)?;
|
|
|
|
|
| let (evals, _) = rho.linalg_eigh("L")?;
|
|
|
|
|
| let safe_evals = evals.clamp_min(1e-300);
|
| let entropy_terms = &safe_evals * &safe_evals.log() * (-1.0f64);
|
|
|
|
|
| let mask = evals.gt(1e-15);
|
| let masked = entropy_terms * &mask;
|
|
|
| let entropy: f64 = masked.sum(Kind::Double).double_value(&[]);
|
| Ok(entropy)
|
| });
|
| result
|
| }
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| pub fn christoffel_symbols(rho: &Tensor) -> Result<Tensor> {
|
| let result = tch::no_grad(|| {
|
| Self::validate_f64(rho)?;
|
| Self::validate_square(rho)?;
|
| let n = rho.size()[0];
|
| let n_sq = n * n;
|
|
|
|
|
|
|
|
|
| Ok(Tensor::zeros([n_sq, n_sq, n_sq], (Kind::Double, rho.device())))
|
| });
|
| result
|
| }
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| pub fn bures_distance(rho: &Tensor, sigma: &Tensor) -> Result<f64> {
|
| let result = tch::no_grad(|| {
|
| Self::validate_f64(rho)?;
|
| Self::validate_f64(sigma)?;
|
| Self::validate_square(rho)?;
|
| Self::validate_square(sigma)?;
|
|
|
| let n = rho.size()[0];
|
| if sigma.size()[0] != n {
|
| return Err(GeometryError::DimensionMismatch(n));
|
| }
|
|
|
|
|
| let (evals_rho, evecs_rho) = rho.linalg_eigh("L")?;
|
| let sqrt_evals = evals_rho.clamp_min(0.0).sqrt();
|
| let sqrt_diag = Tensor::diag_embed(&sqrt_evals, 0, -2, -1);
|
| let sqrt_rho = evecs_rho.matmul(&sqrt_diag).matmul(&evecs_rho.tr());
|
|
|
|
|
| let inner = sqrt_rho.matmul(sigma).matmul(&sqrt_rho);
|
|
|
|
|
| let (evals_inner, _) = inner.linalg_eigh("L")?;
|
| let sqrt_evals_inner = evals_inner.clamp_min(0.0).sqrt();
|
|
|
|
|
| let trace_sqrt: f64 = sqrt_evals_inner.sum(Kind::Double).double_value(&[]);
|
|
|
|
|
| let distance_sq = 2.0 * (1.0 - trace_sqrt.min(1.0));
|
| Ok(distance_sq.max(0.0).sqrt())
|
| });
|
| result
|
| }
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| pub fn fidelity(rho: &Tensor, sigma: &Tensor) -> Result<f64> {
|
| let result = tch::no_grad(|| {
|
| Self::validate_f64(rho)?;
|
| Self::validate_f64(sigma)?;
|
| Self::validate_square(rho)?;
|
| Self::validate_square(sigma)?;
|
|
|
| let n = rho.size()[0];
|
| if sigma.size()[0] != n {
|
| return Err(GeometryError::DimensionMismatch(n));
|
| }
|
|
|
|
|
| let (evals_rho, evecs_rho) = rho.linalg_eigh("L")?;
|
| let sqrt_evals = evals_rho.clamp_min(0.0).sqrt();
|
| let sqrt_diag = Tensor::diag_embed(&sqrt_evals, 0, -2, -1);
|
| let sqrt_rho = evecs_rho.matmul(&sqrt_diag).matmul(&evecs_rho.tr());
|
|
|
|
|
| let inner = sqrt_rho.matmul(sigma).matmul(&sqrt_rho);
|
|
|
|
|
| let (evals_inner, _) = inner.linalg_eigh("L")?;
|
| let sqrt_inner = evals_inner.clamp_min(0.0).sqrt();
|
| let trace: f64 = sqrt_inner.sum(Kind::Double).double_value(&[]);
|
|
|
| Ok((trace * trace).min(1.0))
|
| });
|
| result
|
| }
|
|
|
|
|
|
|
| fn validate_f64(t: &Tensor) -> Result<()> {
|
| if t.kind() != Kind::Double {
|
| return Err(GeometryError::PrecisionError);
|
| }
|
| Ok(())
|
| }
|
|
|
| fn validate_square(t: &Tensor) -> Result<()> {
|
| let size = t.size();
|
| if size.len() != 2 || size[0] != size[1] {
|
| return Err(GeometryError::DimensionMismatch(size[0]));
|
| }
|
| Ok(())
|
| }
|
|
|
|
|
| pub fn is_symmetric(t: &Tensor, tol: f64) -> bool {
|
| let diff = t - &t.tr();
|
| let norm: f64 = diff.abs().sum(Kind::Double).double_value(&[]);
|
| norm < tol
|
| }
|
|
|
|
|
| pub fn is_density_matrix(t: &Tensor, tol: f64) -> bool {
|
| if !Self::is_symmetric(t, tol) {
|
| return false;
|
| }
|
| let trace: f64 = t.trace().double_value(&[]);
|
| if (trace - 1.0).abs() > tol {
|
| return false;
|
| }
|
|
|
| if let Ok((evals, _)) = t.linalg_eigh("L") {
|
| let min_eval: f64 = evals.min().double_value(&[]);
|
| min_eval > -tol
|
| } else {
|
| false
|
| }
|
| }
|
| }
|
|
|
|
|
| #[cfg(test)]
|
| mod tests {
|
| use super::*;
|
| use crate::algebra::jordan_product_cpu;
|
|
|
|
|
| fn rand_density_matrix_cpu(n: i64) -> Tensor {
|
|
|
| let raw = Tensor::randn([n, n], (Kind::Double, Device::Cpu));
|
| let symmetric = (&raw + &raw.tr()) * 0.5f64;
|
|
|
| let (evals, evecs) = symmetric.linalg_eigh("L").unwrap();
|
| let pos_evals = evals.abs().clamp_min(0.01);
|
|
|
| let trace: f64 = pos_evals.sum(Kind::Double).double_value(&[]);
|
| let normed_evals = &pos_evals / trace;
|
| let diag = Tensor::diag_embed(&normed_evals, 0, -2, -1);
|
| evecs.matmul(&diag).matmul(&evecs.tr())
|
| }
|
|
|
|
|
| fn rand_tangent_cpu(n: i64) -> Tensor {
|
| let raw = Tensor::randn([n, n], (Kind::Double, Device::Cpu));
|
| let symmetric = (&raw + &raw.tr()) * 0.5f64;
|
|
|
| let trace: f64 = symmetric.trace().double_value(&[]);
|
| let correction = Tensor::eye(n, (Kind::Double, Device::Cpu)) * (trace / n as f64);
|
| &symmetric - &correction
|
| }
|
|
|
| #[test]
|
| fn test_lyapunov_solves_equation() {
|
| let rho = rand_density_matrix_cpu(4);
|
| let delta = rand_tangent_cpu(4);
|
| let c = &delta * 2.0f64;
|
|
|
|
|
| let g = BuresGeometry::solve_lyapunov(&rho, &c).expect("Lyapunov solve failed");
|
|
|
|
|
| let reconstructed = rho.matmul(&g) + g.matmul(&rho);
|
| let diff: f64 = (&reconstructed - &c).abs().max().double_value(&[]);
|
| assert!(
|
| diff < 1e-8,
|
| "Lyapunov equation not satisfied: max residual = {diff}"
|
| );
|
| }
|
|
|
| #[test]
|
| fn test_bures_metric_symmetry() {
|
| let rho = rand_density_matrix_cpu(4);
|
| let u = rand_tangent_cpu(4);
|
| let v = rand_tangent_cpu(4);
|
|
|
|
|
| let g_uv = BuresGeometry::bures_inner_product(&rho, &u, &v)
|
| .expect("g(u,v) failed");
|
| let g_vu = BuresGeometry::bures_inner_product(&rho, &v, &u)
|
| .expect("g(v,u) failed");
|
|
|
| assert!(
|
| (g_uv - g_vu).abs() < 1e-8,
|
| "Metric not symmetric: g(u,v)={g_uv}, g(v,u)={g_vu}"
|
| );
|
| }
|
|
|
| #[test]
|
| fn test_bures_metric_positive_definite() {
|
| let rho = rand_density_matrix_cpu(4);
|
| let v = rand_tangent_cpu(4);
|
|
|
|
|
| let g_vv = BuresGeometry::bures_inner_product(&rho, &v, &v)
|
| .expect("g(v,v) failed");
|
|
|
| assert!(
|
| g_vv > 0.0,
|
| "Metric not positive-definite: g(v,v) = {g_vv}"
|
| );
|
| }
|
|
|
| #[test]
|
| fn test_bures_metric_bilinearity() {
|
| let rho = rand_density_matrix_cpu(3);
|
| let u = rand_tangent_cpu(3);
|
| let v = rand_tangent_cpu(3);
|
| let alpha = 2.5f64;
|
|
|
|
|
| let g_au_v = BuresGeometry::bures_inner_product(&rho, &(&u * alpha), &v)
|
| .expect("g(αu,v) failed");
|
| let a_g_uv = alpha * BuresGeometry::bures_inner_product(&rho, &u, &v)
|
| .expect("g(u,v) failed");
|
|
|
| assert!(
|
| (g_au_v - a_g_uv).abs() < 1e-8,
|
| "Metric not bilinear: g(αu,v)={g_au_v}, α·g(u,v)={a_g_uv}"
|
| );
|
| }
|
|
|
| #[test]
|
| fn test_grad_entropy_uses_jordan_product() {
|
| let rho = rand_density_matrix_cpu(3);
|
|
|
|
|
| let grad = BuresGeometry::grad_von_neumann_entropy(&rho)
|
| .expect("Gradient failed");
|
|
|
|
|
| let (evals, evecs) = rho.linalg_eigh("L").unwrap();
|
| let log_evals = evals.clamp_min(1e-300).log();
|
| let log_diag = Tensor::diag_embed(&log_evals, 0, -2, -1);
|
| let log_rho = evecs.matmul(&log_diag).matmul(&evecs.tr());
|
|
|
| let jordan = jordan_product_cpu(&rho, &log_rho).expect("Jordan product failed");
|
| let expected = jordan * (-4.0f64);
|
|
|
| let diff: f64 = (&grad - &expected).abs().max().double_value(&[]);
|
| assert!(
|
| diff < 1e-8,
|
| "Gradient doesn't match -4(ρ∘logρ): max diff = {diff}"
|
| );
|
| }
|
|
|
| #[test]
|
| fn test_von_neumann_entropy_pure_state() {
|
|
|
| let mut rho = Tensor::zeros([3, 3], (Kind::Double, Device::Cpu));
|
| let _ = rho.narrow(0, 0, 1).narrow(1, 0, 1).fill_(1.0);
|
|
|
| let entropy = BuresGeometry::von_neumann_entropy(&rho)
|
| .expect("Entropy failed");
|
|
|
| assert!(
|
| entropy.abs() < 1e-10,
|
| "Pure state entropy should be 0, got {entropy}"
|
| );
|
| }
|
|
|
| #[test]
|
| fn test_von_neumann_entropy_maximally_mixed() {
|
|
|
| let n = 4i64;
|
| let rho = Tensor::eye(n, (Kind::Double, Device::Cpu)) / (n as f64);
|
|
|
| let entropy = BuresGeometry::von_neumann_entropy(&rho)
|
| .expect("Entropy failed");
|
| let expected = (n as f64).ln();
|
|
|
| assert!(
|
| (entropy - expected).abs() < 1e-10,
|
| "Maximally mixed entropy should be ln({n})={expected}, got {entropy}"
|
| );
|
| }
|
|
|
| #[test]
|
| fn test_fidelity_same_state() {
|
| let rho = rand_density_matrix_cpu(4);
|
|
|
|
|
| let f = BuresGeometry::fidelity(&rho, &rho).expect("Fidelity failed");
|
| assert!(
|
| (f - 1.0).abs() < 1e-8,
|
| "Fidelity of state with itself should be 1, got {f}"
|
| );
|
| }
|
|
|
| #[test]
|
| fn test_bures_distance_same_state() {
|
| let rho = rand_density_matrix_cpu(4);
|
|
|
|
|
| let d = BuresGeometry::bures_distance(&rho, &rho).expect("Distance failed");
|
| assert!(
|
| d < 1e-6,
|
| "Distance of state to itself should be 0, got {d}"
|
| );
|
| }
|
|
|
| #[test]
|
| fn test_bures_distance_triangle_inequality() {
|
| let rho = rand_density_matrix_cpu(3);
|
| let sigma = rand_density_matrix_cpu(3);
|
| let tau = rand_density_matrix_cpu(3);
|
|
|
| let d_rs = BuresGeometry::bures_distance(&rho, &sigma).expect("d(ρ,σ) failed");
|
| let d_st = BuresGeometry::bures_distance(&sigma, &tau).expect("d(σ,τ) failed");
|
| let d_rt = BuresGeometry::bures_distance(&rho, &tau).expect("d(ρ,τ) failed");
|
|
|
|
|
| assert!(
|
| d_rt <= d_rs + d_st + 1e-8,
|
| "Triangle inequality violated: d(ρ,τ)={d_rt} > d(ρ,σ)+d(σ,τ)={}", d_rs + d_st
|
| );
|
| }
|
|
|
| #[test]
|
| fn test_precision_enforcement() {
|
| let rho = Tensor::eye(3, (Kind::Float, Device::Cpu));
|
| assert!(matches!(
|
| BuresGeometry::solve_lyapunov(&rho, &rho),
|
| Err(GeometryError::PrecisionError)
|
| ));
|
| }
|
|
|
| #[test]
|
| fn test_christoffel_symbols_placeholder() {
|
| let rho = rand_density_matrix_cpu(3);
|
| let gamma = BuresGeometry::christoffel_symbols(&rho).expect("Christoffel failed");
|
|
|
|
|
| let norm: f64 = gamma.abs().sum(Kind::Double).double_value(&[]);
|
| assert!(
|
| norm < 1e-15,
|
| "Christoffel placeholder should be zero, got norm={norm}"
|
| );
|
| }
|
|
|
| #[test]
|
| fn test_is_density_matrix() {
|
| let rho = rand_density_matrix_cpu(4);
|
| assert!(BuresGeometry::is_density_matrix(&rho, 1e-6));
|
|
|
|
|
| let bad = Tensor::eye(4, (Kind::Double, Device::Cpu));
|
| assert!(!BuresGeometry::is_density_matrix(&bad, 1e-6));
|
| }
|
| }
|
|
|