Download rust/algebraic-core/src/stochastic.rs from Snapkitty/sov-kernel-monster: direct link, hf CLI and curl.
- Browser
- Download file 22.9 kB
-
https://huggingface.co/Snapkitty/sov-kernel-monster/resolve/main/rust/algebraic-core/src/stochastic.rs
- Command line
-
hf download hf://Snapkitty/sov-kernel-monster/rust/algebraic-core/src/stochastic.rs
-
curl -L -o stochastic.rs https://huggingface.co/Snapkitty/sov-kernel-monster/resolve/main/rust/algebraic-core/src/stochastic.rs
22.9 kB
| //! # Stochastic Solver Module | |
| //! | |
| //! Implements geometric Euler-Maruyama SDE solver for quantum density matrix diffusion | |
| //! on the Bures manifold. | |
| //! Solves: dρₜ = -∇_Riem S(ρₜ) dt + √D dWₜ | |
| //! - Uses tangent space projection for Wiener process (dWₜ) | |
| //! - Updates state via projection-based retraction to preserve manifold constraints | |
| //! - Batched GPU execution for Monte Carlo trajectory simulation | |
| //! | |
| //! ## Key Design Decisions | |
| //! - **Backend**: `tch-rs` with CUDA (`libtorch` backend) for hardware acceleration | |
| //! - **Precision**: Strict `f64` usage (quantum states require double precision) | |
| //! - **Tangent Projection**: Generates random Hermitian matrices on GPU, projects to trace-zero | |
| //! - **Retraction Map**: Projects ρ + update onto density matrix manifold via eigenvalue | |
| //! clipping/renormalization | |
| //! - **Batching**: All operations vectorized; batch loop only for eigendecomposition | |
| //! - **Memory Safety**: Explicit tensor drops, no_grad scope, no intermediate leaks | |
| //! - **Validation**: Unit tests verify trace preservation and positive semi-definiteness | |
| //! | |
| //! ## Safety Guarantees | |
| //! - All tensor operations occur within `no_grad` scope to prevent autograd overhead | |
| //! - Random number generation strictly on device (Device::Cuda) to avoid PCIe bottlenecks | |
| //! - Eigenvalue decomposition uses `torch.linalg.eigh()` (CUDA-accelerated via cuSOLVER) | |
| //! - Division by zero avoided via epsilon clamping in renormalization | |
| //! - Error handling propagates errors for clear diagnostics | |
| //! - Unit tests validate manifold constraints under floating-point tolerance | |
| //! | |
| //! ## Integration with sov-kernel-monster | |
| //! - Entropy gradient drives density matrix evolution toward thermal equilibrium | |
| //! - Connects to JST φ-decay: contraction rate φ⁻¹ bounds drift magnitude | |
| //! - WORM-attested trajectory snapshots via sov_bifrost_sign at checkpoints | |
| //! - Batched simulation enables Monte Carlo estimation of quantum observables | |
| //! | |
| //! ## Prior Art | |
| //! - SnapKitty Foundry Intel (April 14, 2026) | |
| //! - Original Research Lab: JAB Capital Trust (2021) | |
| use tch::{Device, Kind, Tensor}; | |
| use thiserror::Error; | |
| use crate::geometry::BuresGeometry; | |
| /// Configuration for the geometric Euler-Maruyama solver. | |
| /// | |
| /// Controls the discretization and noise parameters of the SDE: | |
| /// dρₜ = -∇_Riem S(ρₜ) dt + √D dWₜ | |
| /// | |
| /// # Fields | |
| /// - `dt`: Time step size (Δt). Smaller → more accurate but slower. | |
| /// - `diffusion`: Diffusion coefficient (D). Controls noise magnitude. | |
| /// - `total_steps`: Number of Euler-Maruyama steps to simulate. | |
| /// | |
| /// # Stability Condition | |
| /// For the geometric Euler-Maruyama method to remain stable: | |
| /// - `dt` should satisfy `dt < 1 / (2 * max_eigenvalue(∇²S))` | |
| /// - In practice, `dt ≤ 0.01` works for most quantum systems with n ≤ 64 | |
| pub struct SolverConfig { | |
| pub dt: f64, | |
| pub diffusion: f64, | |
| pub total_steps: usize, | |
| } | |
| impl SolverConfig { | |
| /// Creates a new solver configuration. | |
| /// | |
| /// # Arguments | |
| /// * `dt` - Time step size (must be > 0) | |
| /// * `diffusion` - Diffusion coefficient D (must be ≥ 0; D=0 gives deterministic flow) | |
| /// * `total_steps` - Number of simulation steps | |
| /// | |
| /// # Panics | |
| /// - If `dt <= 0` | |
| /// - If `diffusion < 0` | |
| 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 } | |
| } | |
| } | |
| /// Error type for stochastic solver operations | |
| pub enum StochasticError { | |
| /// Underlying tensor operation failure (CUDA, memory, computation) | |
| TchError( tch::TchError), | |
| /// Input tensors have incompatible dimensions | |
| DimensionMismatch, | |
| /// Input tensor uses insufficient precision | |
| PrecisionError, | |
| /// Geometry module error (from gradient computation) | |
| GeometryError(String), | |
| /// Manifold constraint violated after retraction (should never happen) | |
| ManifoldViolation(f64), | |
| } | |
| pub type Result<T> = std::result::Result<T, StochasticError>; | |
| /// Geometric Euler-Maruyama SDE solver for quantum density matrix diffusion. | |
| /// | |
| /// Solves: dρₜ = drift(ρₜ) dt + √D · dWₜ | |
| /// where: | |
| /// - drift(ρ) = -∇_Riem S(ρ) (negative Riemannian gradient of von Neumann entropy) | |
| /// - dWₜ is a Wiener process in the tangent space T_ρ M (Hermitian, traceless matrices) | |
| /// - The solution is projected back onto the density matrix manifold at each step | |
| /// | |
| /// # Algorithm (per step): | |
| /// 1. Compute drift: μ = -∇_Riem S(ρ) · dt | |
| /// 2. Generate tangent noise: σ = √(D·dt) · project_tangent(randn()) | |
| /// 3. Tentative update: ρ̃ = ρ + μ + σ | |
| /// 4. Retraction: ρ_next = project_manifold(ρ̃) | |
| /// - Eigendecompose ρ̃ | |
| /// - Clip negative eigenvalues to 0 | |
| /// - Renormalize eigenvalues to sum to 1 | |
| /// - Reconstruct density matrix | |
| pub struct GeometricEulerMaruyama; | |
| impl GeometricEulerMaruyama { | |
| /// Performs a single step of the geometric Euler-Maruyama solver. | |
| /// | |
| /// # Arguments | |
| /// * `rho` - Current state tensor ([batch, n, n] or [n, n], Hermitian, trace=1, f64) | |
| /// * `config` - Solver configuration (dt, diffusion) | |
| /// | |
| /// # Returns | |
| /// * `Ok(Tensor)` - Next state tensor on the density matrix manifold | |
| /// * `Err(StochasticError)` - On validation failure or backend error | |
| /// | |
| /// # Manifold Guarantees | |
| /// The output tensor satisfies: | |
| /// - Hermitian (symmetric for real): ρ = ρ† | |
| /// - Trace one: Tr(ρ) = 1 | |
| /// - Positive semi-definite: all eigenvalues ≥ 0 | |
| pub fn step(rho: &Tensor, config: &SolverConfig) -> Result<Tensor> { | |
| let result = tch::no_grad(|| { | |
| // Validate input precision | |
| 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(); | |
| // Reshape to [batch, n, n] for uniform processing | |
| let rho_batch = if is_batched { | |
| rho.shallow_clone() | |
| } else { | |
| rho.unsqueeze(0) | |
| }; | |
| // ══════════════════════════════════════════════════════════════ | |
| // STEP 1: Compute deterministic drift = -∇_Riem S(ρ) · dt | |
| // ══════════════════════════════════════════════════════════════ | |
| // Process each batch element through geometry module | |
| let mut drift_components = Vec::with_capacity(batch as usize); | |
| for i in 0..batch { | |
| let rho_i = rho_batch.get(i); // [n, n] | |
| let grad = BuresGeometry::grad_von_neumann_entropy(&rho_i) | |
| .map_err(|e| StochasticError::GeometryError(e.to_string()))?; | |
| // drift = -grad_S * dt (entropy gradient descent) | |
| let drift_i = grad * (-config.dt); | |
| drift_components.push(drift_i.unsqueeze(0)); | |
| } | |
| let drift = Tensor::cat(&drift_components, 0); // [batch, n, n] | |
| drop(drift_components); | |
| // ══════════════════════════════════════════════════════════════ | |
| // STEP 2: Generate Wiener increment in tangent space | |
| // All noise generated ON DEVICE (no PCIe bottleneck) | |
| // ══════════════════════════════════════════════════════════════ | |
| let noise = if config.diffusion > 0.0 { | |
| // a. Sample random matrix on GPU | |
| let z_raw = Tensor::randn([batch, n, n], (Kind::Double, device)); | |
| // b. Make Hermitian (symmetric): Z = (Z + Z^T) / 2 | |
| let z_herm = (&z_raw + &z_raw.transpose(1, 2)) * 0.5f64; | |
| drop(z_raw); | |
| // c. Project to tangent space (trace-zero subspace) | |
| // Z_tan = Z - (Tr(Z)/n) · I | |
| // Tr(Z) computed per batch element | |
| let diag_sum = z_herm.diagonal(0, 1, 2).sum_dim_intlist([-1i64].as_slice(), false, Kind::Double); | |
| // diag_sum: [batch] | |
| let trace_correction = diag_sum.unsqueeze(-1).unsqueeze(-1) / (n as f64); | |
| // trace_correction: [batch, 1, 1] | |
| let eye = Tensor::eye(n, (Kind::Double, device)).unsqueeze(0); // [1, n, n] | |
| let z_tangent = &z_herm - &(trace_correction * &eye); | |
| drop(z_herm); | |
| // d. Scale by √(D · dt) | |
| let noise_scale = (config.diffusion * config.dt).sqrt(); | |
| z_tangent * noise_scale | |
| } else { | |
| // D=0 → deterministic flow (no noise) | |
| Tensor::zeros([batch, n, n], (Kind::Double, device)) | |
| }; | |
| // ══════════════════════════════════════════════════════════════ | |
| // STEP 3: Tentative state: ρ̃ = ρ + drift + noise | |
| // (Hermitian, trace≈1, but may have negative eigenvalues) | |
| // ══════════════════════════════════════════════════════════════ | |
| let rho_tilde = &rho_batch + &drift + &noise; | |
| drop(drift); | |
| drop(noise); | |
| // ══════════════════════════════════════════════════════════════ | |
| // STEP 4: Retraction — project ρ̃ onto density matrix manifold | |
| // - Eigendecompose: ρ̃ = VΛV^T | |
| // - Clip negatives: Λ' = max(Λ, 0) | |
| // - Renormalize: Λ'' = Λ' / Σᵢλ'ᵢ (ensures trace=1) | |
| // - Reconstruct: ρ_next = VΛ''V^T | |
| // ══════════════════════════════════════════════════════════════ | |
| let mut projected = Vec::with_capacity(batch as usize); | |
| for i in 0..batch { | |
| let rho_i = rho_tilde.get(i); // [n, n] | |
| // Make exactly symmetric (remove numerical asymmetry) | |
| let rho_sym = (&rho_i + &rho_i.tr()) * 0.5f64; | |
| // Eigendecompose | |
| let (evals, evecs) = rho_sym.linalg_eigh("L")?; | |
| // Clip negative eigenvalues | |
| let evals_clipped = evals.clamp_min(0.0); | |
| // Renormalize to trace=1 | |
| let trace_sum: f64 = evals_clipped.sum(Kind::Double).double_value(&[]); | |
| let evals_normed = if trace_sum > 1e-12 { | |
| &evals_clipped / trace_sum | |
| } else { | |
| // Degenerate case: fallback to maximally mixed state | |
| Tensor::ones([n], (Kind::Double, device)) / (n as f64) | |
| }; | |
| // Reconstruct density matrix: V · diag(λ) · V^T | |
| 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)); // [1, n, n] | |
| } | |
| drop(rho_tilde); | |
| let rho_next = Tensor::cat(&projected, 0); // [batch, n, n] | |
| drop(projected); | |
| // Return with original shape | |
| if is_batched { | |
| Ok(rho_next) | |
| } else { | |
| Ok(rho_next.squeeze_dim(0)) | |
| } | |
| }); | |
| result | |
| } | |
| /// Runs the full trajectory: `total_steps` iterations of geometric Euler-Maruyama. | |
| /// | |
| /// # Arguments | |
| /// * `rho_init` - Initial density matrix ([batch, n, n] or [n, n]) | |
| /// * `config` - Solver configuration | |
| /// | |
| /// # Returns | |
| /// * `Ok(Tensor)` - Final state after all steps (same shape as input) | |
| 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) | |
| } | |
| /// Runs the full trajectory and returns ALL intermediate states. | |
| /// | |
| /// # Arguments | |
| /// * `rho_init` - Initial density matrix ([n, n] only, not batched) | |
| /// * `config` - Solver configuration | |
| /// | |
| /// # Returns | |
| /// * `Ok(Vec<Tensor>)` - Trajectory of states [ρ₀, ρ₁, ..., ρ_T] | |
| /// | |
| /// # Notes | |
| /// - Returns `total_steps + 1` tensors (includes initial state) | |
| /// - Useful for trajectory analysis and WORM attestation checkpoints | |
| 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) | |
| } | |
| /// Computes the Monte Carlo estimate of an observable over trajectories. | |
| /// | |
| /// # Arguments | |
| /// * `rho_init` - Initial state ([batch, n, n]) — each batch element is one trajectory | |
| /// * `config` - Solver configuration | |
| /// * `observable` - Observable matrix O (n×n, Hermitian) | |
| /// | |
| /// # Returns | |
| /// * `Ok(f64)` - Mean value ⟨O⟩ = (1/B) Σᵢ Tr[O · ρᵢ_final] | |
| pub fn monte_carlo_expectation( | |
| rho_init: &Tensor, | |
| config: &SolverConfig, | |
| observable: &Tensor, | |
| ) -> Result<f64> { | |
| let result = tch::no_grad(|| { | |
| // Evolve all trajectories | |
| let rho_final = Self::solve(rho_init, config)?; | |
| let batch = rho_final.size()[0]; | |
| let n = rho_final.size()[1]; | |
| // Compute Tr[O · ρ_i] for each trajectory | |
| // O · ρ: [batch, n, n] via broadcasting | |
| let obs_expanded = observable.unsqueeze(0).expand([batch, n, n], false); | |
| let product = obs_expanded.matmul(&rho_final); // [batch, n, n] | |
| // Trace per batch element | |
| let traces = product.diagonal(0, 1, 2).sum_dim_intlist([-1i64].as_slice(), false, Kind::Double); | |
| // traces: [batch] | |
| // Mean over batch | |
| let mean: f64 = traces.mean(Kind::Double).double_value(&[]); | |
| Ok(mean) | |
| }); | |
| result | |
| } | |
| } | |
| /// Unit tests validating stochastic solver properties | |
| mod tests { | |
| use super::*; | |
| /// Creates a batch of maximally mixed density matrices (I/n, trace=1) | |
| 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() | |
| } | |
| /// Creates a single maximally mixed state | |
| fn maximally_mixed(n: i64, device: Device) -> Tensor { | |
| Tensor::eye(n, (Kind::Double, device)) / (n as f64) | |
| } | |
| /// Creates a pure state |0><0| | |
| 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 | |
| } | |
| 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" | |
| ); | |
| } | |
| 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"); | |
| // All eigenvalues must be non-negative | |
| 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}" | |
| ); | |
| } | |
| 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"); | |
| // Must be symmetric (Hermitian for real matrices) | |
| assert!( | |
| BuresGeometry::is_symmetric(&rho_final, 1e-10), | |
| "Output not Hermitian/symmetric" | |
| ); | |
| } | |
| fn test_zero_diffusion_deterministic() { | |
| let device = Device::Cpu; | |
| let config = SolverConfig::new(0.01, 0.0, 5); // D=0 | |
| 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}" | |
| ); | |
| // Maximally mixed is fixed point of entropy gradient → should stay near identity/n | |
| let expected = maximally_mixed(3, device); | |
| let diff: f64 = (&rho_final - &expected).abs().max().double_value(&[]); | |
| // For maximally mixed state, grad S = 0 → no drift → stays put | |
| assert!( | |
| diff < 1e-8, | |
| "Deterministic flow moved maximally mixed state: max diff = {diff}" | |
| ); | |
| } | |
| 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]); | |
| // Check trace preservation for all batch elements | |
| 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}" | |
| ); | |
| } | |
| } | |
| 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"); | |
| // Should have steps + 1 entries (includes initial state) | |
| assert_eq!(trajectory.len(), steps + 1); | |
| // All states should have trace=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}" | |
| ); | |
| } | |
| } | |
| 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"); | |
| // Should still be a valid density matrix | |
| 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}"); | |
| } | |
| 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) | |
| )); | |
| } | |
| fn test_config_negative_dt() { | |
| SolverConfig::new(-0.01, 0.1, 10); | |
| } | |
| fn test_config_negative_diffusion() { | |
| SolverConfig::new(0.01, -0.1, 10); | |
| } | |
| } | |