Download rust/algebraic-core/src/backend_contract.rs from Snapkitty/sov-kernel-monster: direct link, hf CLI and curl.
- Browser
- Download file 21.8 kB
-
https://huggingface.co/Snapkitty/sov-kernel-monster/resolve/main/rust/algebraic-core/src/backend_contract.rs
- Command line
-
hf download hf://Snapkitty/sov-kernel-monster/rust/algebraic-core/src/backend_contract.rs
-
curl -L -o backend_contract.rs https://huggingface.co/Snapkitty/sov-kernel-monster/resolve/main/rust/algebraic-core/src/backend_contract.rs
21.8 kB
| //! # Quantum Backend Contract | |
| //! | |
| //! Defines the B = (Q, Γ, Λ, Π, Ξ, Θ) contract that binds quantum execution | |
| //! to a known physical device state. This ensures reproducibility and enables | |
| //! WORM-attested execution receipts that reference calibration_hash. | |
| //! | |
| //! ## Backend Contract Definition | |
| //! - **Q**: Physical qubits (indices 0..n_qubits) | |
| //! - **Γ**: Coupling graph (connectivity between qubits) | |
| //! - **Λ**: Native gates (operations available on this device) | |
| //! - **Π**: Pulse definitions (gate calibrations) | |
| //! - **Ξ**: Calibration data (noise parameters, timing, T1/T2) | |
| //! - **Θ**: Timing constraints (gate durations, measurement window) | |
| //! | |
| //! ## Invariants | |
| //! - Backend must be valid before use (validate_backend()) | |
| //! - Calibration hash must be deterministic and reproducible | |
| //! - All timing must be physically meaningful (positive) | |
| //! - Native gates must be realizable on topology | |
| use std::collections::{BTreeMap, HashMap}; | |
| use std::fmt; | |
| use sha2::{Sha256, Digest}; | |
| use serde::{Serialize, Deserialize}; | |
| use thiserror::Error; | |
| /// Error type for backend contract operations | |
| pub enum BackendError { | |
| InvalidQubit(usize), | |
| NotConnected(usize, usize), | |
| UnsupportedGate(String), | |
| InvalidTiming { field: String, value: f64 }, | |
| NonSquareConnectivity, | |
| MissingCalibration(usize), | |
| EmptyBackend, | |
| SerializationError(String), | |
| } | |
| pub type Result<T> = std::result::Result<T, BackendError>; | |
| /// Native gate types available on quantum backends | |
| pub enum NativeGate { | |
| X, // Pauli X (π rotation around X) | |
| Y, // Pauli Y | |
| Z, // Pauli Z | |
| H, // Hadamard | |
| S, // S gate (π/2 phase) | |
| T, // T gate (π/4 phase) | |
| Rx, // Rotation around X (parametric) | |
| Ry, // Rotation around Y (parametric) | |
| Rz, // Rotation around Z (parametric) | |
| CX, // CNOT (two-qubit) | |
| CZ, // Controlled-Z (two-qubit) | |
| SWAP, // SWAP (two-qubit) | |
| } | |
| impl fmt::Display for NativeGate { | |
| fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { | |
| match self { | |
| NativeGate::X => write!(f, "X"), | |
| NativeGate::Y => write!(f, "Y"), | |
| NativeGate::Z => write!(f, "Z"), | |
| NativeGate::H => write!(f, "H"), | |
| NativeGate::S => write!(f, "S"), | |
| NativeGate::T => write!(f, "T"), | |
| NativeGate::Rx => write!(f, "Rx"), | |
| NativeGate::Ry => write!(f, "Ry"), | |
| NativeGate::Rz => write!(f, "Rz"), | |
| NativeGate::CX => write!(f, "CX"), | |
| NativeGate::CZ => write!(f, "CZ"), | |
| NativeGate::SWAP => write!(f, "SWAP"), | |
| } | |
| } | |
| } | |
| /// Pulse definition: calibrated waveform for a gate on specific qubits | |
| pub struct PulseDefinition { | |
| pub gate: NativeGate, | |
| pub target_qubits: Vec<usize>, // Empty for global, 1 for single-qubit, 2 for two-qubit | |
| pub duration: f64, // in nanoseconds | |
| pub amplitude: f64, // pulse amplitude (0..1) | |
| pub frequency: f64, // drive frequency in GHz | |
| pub phase: f64, // initial phase in radians | |
| } | |
| impl PulseDefinition { | |
| /// Create a new pulse definition | |
| pub fn new( | |
| gate: NativeGate, | |
| target_qubits: Vec<usize>, | |
| duration: f64, | |
| amplitude: f64, | |
| frequency: f64, | |
| phase: f64, | |
| ) -> Result<Self> { | |
| if duration <= 0.0 { | |
| return Err(BackendError::InvalidTiming { | |
| field: "duration".to_string(), | |
| value: duration, | |
| }); | |
| } | |
| if amplitude < 0.0 || amplitude > 1.0 { | |
| return Err(BackendError::InvalidTiming { | |
| field: "amplitude".to_string(), | |
| value: amplitude, | |
| }); | |
| } | |
| if frequency < 0.0 { | |
| return Err(BackendError::InvalidTiming { | |
| field: "frequency".to_string(), | |
| value: frequency, | |
| }); | |
| } | |
| Ok(PulseDefinition { | |
| gate, | |
| target_qubits, | |
| duration, | |
| amplitude, | |
| frequency, | |
| phase, | |
| }) | |
| } | |
| } | |
| /// Coupling graph: adjacency matrix defining qubit connectivity | |
| pub struct CouplingGraph { | |
| connectivity: Vec<Vec<bool>>, // [i][j] = true iff qubits i and j are coupled | |
| } | |
| impl CouplingGraph { | |
| /// Create a new coupling graph from adjacency matrix | |
| pub fn new(connectivity: Vec<Vec<bool>>) -> Result<Self> { | |
| if connectivity.is_empty() { | |
| return Err(BackendError::EmptyBackend); | |
| } | |
| let n = connectivity.len(); | |
| for row in &connectivity { | |
| if row.len() != n { | |
| return Err(BackendError::NonSquareConnectivity); | |
| } | |
| } | |
| Ok(CouplingGraph { connectivity }) | |
| } | |
| /// Check if two qubits are connected | |
| pub fn are_connected(&self, q1: usize, q2: usize) -> Result<bool> { | |
| let n = self.connectivity.len(); | |
| if q1 >= n || q2 >= n { | |
| return Err(BackendError::InvalidQubit( | |
| if q1 >= n { q1 } else { q2 }, | |
| )); | |
| } | |
| Ok(self.connectivity[q1][q2]) | |
| } | |
| /// Get all neighbors of a qubit | |
| pub fn neighbors(&self, qubit: usize) -> Result<Vec<usize>> { | |
| let n = self.connectivity.len(); | |
| if qubit >= n { | |
| return Err(BackendError::InvalidQubit(qubit)); | |
| } | |
| Ok(self | |
| .connectivity[qubit] | |
| .iter() | |
| .enumerate() | |
| .filter(|(_, &connected)| connected) | |
| .map(|(i, _)| i) | |
| .collect()) | |
| } | |
| /// Compute distance between qubits (shortest path) | |
| pub fn distance(&self, q1: usize, q2: usize) -> Result<usize> { | |
| let n = self.connectivity.len(); | |
| if q1 >= n || q2 >= n { | |
| return Err(BackendError::InvalidQubit( | |
| if q1 >= n { q1 } else { q2 }, | |
| )); | |
| } | |
| if q1 == q2 { | |
| return Ok(0); | |
| } | |
| // BFS to find shortest path | |
| let mut visited = vec![false; n]; | |
| let mut queue = std::collections::VecDeque::new(); | |
| queue.push_back((q1, 0)); | |
| visited[q1] = true; | |
| while let Some((current, dist)) = queue.pop_front() { | |
| for neighbor in self.neighbors(current)? { | |
| if neighbor == q2 { | |
| return Ok(dist + 1); | |
| } | |
| if !visited[neighbor] { | |
| visited[neighbor] = true; | |
| queue.push_back((neighbor, dist + 1)); | |
| } | |
| } | |
| } | |
| // Not connected | |
| Ok(usize::MAX) | |
| } | |
| pub fn num_qubits(&self) -> usize { | |
| self.connectivity.len() | |
| } | |
| } | |
| /// Per-qubit calibration snapshot | |
| pub struct QubitCalibration { | |
| pub qubit: usize, | |
| pub frequency: f64, // in GHz | |
| pub t1: f64, // energy decay time in microseconds | |
| pub t2: f64, // dephasing time in microseconds | |
| pub single_qubit_error: f64, // 1-qubit gate error (0..1) | |
| pub two_qubit_error: f64, // 2-qubit gate error (0..1) | |
| pub readout_error_0_to_1: f64, // P(measure 1 | state 0) | |
| pub readout_error_1_to_0: f64, // P(measure 0 | state 1) | |
| } | |
| impl QubitCalibration { | |
| /// Create a new qubit calibration | |
| pub fn new( | |
| qubit: usize, | |
| frequency: f64, | |
| t1: f64, | |
| t2: f64, | |
| single_qubit_error: f64, | |
| two_qubit_error: f64, | |
| readout_error_0_to_1: f64, | |
| readout_error_1_to_0: f64, | |
| ) -> Result<Self> { | |
| if t1 <= 0.0 { | |
| return Err(BackendError::InvalidTiming { | |
| field: "t1".to_string(), | |
| value: t1, | |
| }); | |
| } | |
| if t2 <= 0.0 { | |
| return Err(BackendError::InvalidTiming { | |
| field: "t2".to_string(), | |
| value: t2, | |
| }); | |
| } | |
| if t2 > t1 { | |
| // Physical constraint: dephasing faster than decay | |
| return Err(BackendError::InvalidTiming { | |
| field: "t2_exceeds_t1".to_string(), | |
| value: t2 - t1, | |
| }); | |
| } | |
| if single_qubit_error < 0.0 || single_qubit_error > 1.0 { | |
| return Err(BackendError::InvalidTiming { | |
| field: "single_qubit_error".to_string(), | |
| value: single_qubit_error, | |
| }); | |
| } | |
| if two_qubit_error < 0.0 || two_qubit_error > 1.0 { | |
| return Err(BackendError::InvalidTiming { | |
| field: "two_qubit_error".to_string(), | |
| value: two_qubit_error, | |
| }); | |
| } | |
| if readout_error_0_to_1 < 0.0 || readout_error_0_to_1 > 1.0 { | |
| return Err(BackendError::InvalidTiming { | |
| field: "readout_error_0_to_1".to_string(), | |
| value: readout_error_0_to_1, | |
| }); | |
| } | |
| if readout_error_1_to_0 < 0.0 || readout_error_1_to_0 > 1.0 { | |
| return Err(BackendError::InvalidTiming { | |
| field: "readout_error_1_to_0".to_string(), | |
| value: readout_error_1_to_0, | |
| }); | |
| } | |
| Ok(QubitCalibration { | |
| qubit, | |
| frequency, | |
| t1, | |
| t2, | |
| single_qubit_error, | |
| two_qubit_error, | |
| readout_error_0_to_1, | |
| readout_error_1_to_0, | |
| }) | |
| } | |
| } | |
| /// Calibration snapshot with timestamp and hash for WORM binding | |
| pub struct CalibrationSnapshot { | |
| pub device_id: String, | |
| pub timestamp: u64, // Unix timestamp in seconds | |
| pub qubit_calibrations: BTreeMap<usize, QubitCalibration>, | |
| pub gate_calibrations: BTreeMap<String, PulseDefinition>, // key: "GATE_q0_q1" format | |
| pub calibration_hash: String, // Blake3 hash of calibration data | |
| } | |
| impl CalibrationSnapshot { | |
| /// Create a new calibration snapshot and compute hash | |
| pub fn new( | |
| device_id: String, | |
| timestamp: u64, | |
| qubit_calibrations: BTreeMap<usize, QubitCalibration>, | |
| gate_calibrations: BTreeMap<String, PulseDefinition>, | |
| ) -> Result<Self> { | |
| let mut snapshot = CalibrationSnapshot { | |
| device_id, | |
| timestamp, | |
| qubit_calibrations, | |
| gate_calibrations, | |
| calibration_hash: String::new(), | |
| }; | |
| snapshot.compute_hash()?; | |
| Ok(snapshot) | |
| } | |
| /// Compute deterministic hash of calibration data | |
| fn compute_hash(&mut self) -> Result<Self> { | |
| let serialized = serde_json::to_string(&(&self.device_id, self.timestamp, &self.qubit_calibrations, &self.gate_calibrations)) | |
| .map_err(|e| BackendError::SerializationError(e.to_string()))?; | |
| let mut hasher = Sha256::new(); | |
| hasher.update(serialized.as_bytes()); | |
| let hash = hasher.finalize(); | |
| self.calibration_hash = format!("{:x}", hash); | |
| Ok(self.clone()) | |
| } | |
| /// Verify that calibration is complete for given qubits | |
| pub fn verify_qubits(&self, qubits: &[usize]) -> Result<()> { | |
| for &q in qubits { | |
| if !self.qubit_calibrations.contains_key(&q) { | |
| return Err(BackendError::MissingCalibration(q)); | |
| } | |
| } | |
| Ok(()) | |
| } | |
| } | |
| /// Timing constraints for the backend | |
| pub struct TimingConstraints { | |
| pub gate_duration_min: f64, // minimum gate duration in ns | |
| pub gate_duration_max: f64, // maximum gate duration in ns | |
| pub measurement_duration: f64, // measurement window in ns | |
| pub reset_duration: f64, // reset time in ns | |
| pub coherence_time_limit: f64, // max coherence time before decoherence dominates (ns) | |
| } | |
| impl TimingConstraints { | |
| /// Create new timing constraints | |
| pub fn new( | |
| gate_duration_min: f64, | |
| gate_duration_max: f64, | |
| measurement_duration: f64, | |
| reset_duration: f64, | |
| coherence_time_limit: f64, | |
| ) -> Result<Self> { | |
| if gate_duration_min <= 0.0 || gate_duration_max <= 0.0 { | |
| return Err(BackendError::InvalidTiming { | |
| field: "gate_duration".to_string(), | |
| value: gate_duration_min.min(gate_duration_max), | |
| }); | |
| } | |
| if gate_duration_min > gate_duration_max { | |
| return Err(BackendError::InvalidTiming { | |
| field: "gate_duration_min_exceeds_max".to_string(), | |
| value: gate_duration_min - gate_duration_max, | |
| }); | |
| } | |
| if measurement_duration <= 0.0 { | |
| return Err(BackendError::InvalidTiming { | |
| field: "measurement_duration".to_string(), | |
| value: measurement_duration, | |
| }); | |
| } | |
| if reset_duration <= 0.0 { | |
| return Err(BackendError::InvalidTiming { | |
| field: "reset_duration".to_string(), | |
| value: reset_duration, | |
| }); | |
| } | |
| Ok(TimingConstraints { | |
| gate_duration_min, | |
| gate_duration_max, | |
| measurement_duration, | |
| reset_duration, | |
| coherence_time_limit, | |
| }) | |
| } | |
| } | |
| /// Full quantum backend contract: B = (Q, Γ, Λ, Π, Ξ, Θ) | |
| pub struct QuantumBackend { | |
| pub num_qubits: usize, | |
| pub coupling_graph: CouplingGraph, | |
| pub native_gates: Vec<NativeGate>, | |
| pub pulse_definitions: HashMap<String, PulseDefinition>, | |
| pub calibration: CalibrationSnapshot, | |
| pub timing_constraints: TimingConstraints, | |
| pub backend_hash: String, // Blake3 hash of entire backend contract | |
| } | |
| impl QuantumBackend { | |
| /// Create a new quantum backend with full contract | |
| pub fn new( | |
| num_qubits: usize, | |
| coupling_graph: CouplingGraph, | |
| native_gates: Vec<NativeGate>, | |
| pulse_definitions: HashMap<String, PulseDefinition>, | |
| calibration: CalibrationSnapshot, | |
| timing_constraints: TimingConstraints, | |
| ) -> Result<Self> { | |
| let mut backend = QuantumBackend { | |
| num_qubits, | |
| coupling_graph, | |
| native_gates, | |
| pulse_definitions, | |
| calibration, | |
| timing_constraints, | |
| backend_hash: String::new(), | |
| }; | |
| backend.validate()?; | |
| backend.compute_hash()?; | |
| Ok(backend) | |
| } | |
| /// Validate backend contract invariants | |
| pub fn validate(&self) -> Result<()> { | |
| // 1. Coupling graph must have correct size | |
| if self.coupling_graph.num_qubits() != self.num_qubits { | |
| return Err(BackendError::InvalidQubit(self.coupling_graph.num_qubits())); | |
| } | |
| // 2. All calibrations must match qubits | |
| self.calibration.verify_qubits( | |
| &(0..self.num_qubits).collect::<Vec<_>>(), | |
| )?; | |
| // 3. Native gates must be non-empty | |
| if self.native_gates.is_empty() { | |
| return Err(BackendError::InvalidTiming { | |
| field: "native_gates".to_string(), | |
| value: 0.0, | |
| }); | |
| } | |
| // 4. Pulse definitions must respect timing constraints | |
| for (_, pulse) in &self.pulse_definitions { | |
| if pulse.duration < self.timing_constraints.gate_duration_min | |
| || pulse.duration > self.timing_constraints.gate_duration_max | |
| { | |
| return Err(BackendError::InvalidTiming { | |
| field: "pulse_duration".to_string(), | |
| value: pulse.duration, | |
| }); | |
| } | |
| } | |
| Ok(()) | |
| } | |
| /// Compute deterministic hash of backend contract | |
| fn compute_hash(&mut self) -> Result<()> { | |
| let serialized = serde_json::to_string(&( | |
| self.num_qubits, | |
| &self.native_gates, | |
| &self.calibration.calibration_hash, | |
| &self.timing_constraints, | |
| )) | |
| .map_err(|e| BackendError::SerializationError(e.to_string()))?; | |
| let mut hasher = Sha256::new(); | |
| hasher.update(serialized.as_bytes()); | |
| let hash = hasher.finalize(); | |
| self.backend_hash = format!("{:x}", hash); | |
| Ok(()) | |
| } | |
| /// Check if a gate is native on this backend | |
| pub fn supports_gate(&self, gate: &NativeGate) -> bool { | |
| self.native_gates.contains(gate) | |
| } | |
| /// Verify two-qubit gate is supported on this topology | |
| pub fn can_apply_two_qubit_gate(&self, q1: usize, q2: usize) -> Result<bool> { | |
| self.coupling_graph.are_connected(q1, q2) | |
| } | |
| /// Get calibration for specific qubit | |
| pub fn get_qubit_calibration(&self, qubit: usize) -> Result<&QubitCalibration> { | |
| self.calibration | |
| .qubit_calibrations | |
| .get(&qubit) | |
| .ok_or(BackendError::MissingCalibration(qubit)) | |
| } | |
| } | |
| mod tests { | |
| use super::*; | |
| fn make_linear_coupling(n: usize) -> CouplingGraph { | |
| let mut connectivity = vec![vec![false; n]; n]; | |
| for i in 0..n - 1 { | |
| connectivity[i][i + 1] = true; | |
| connectivity[i + 1][i] = true; | |
| } | |
| connectivity[i][i] = true; // self-loops | |
| for i in 0..n { | |
| connectivity[i][i] = true; | |
| } | |
| CouplingGraph::new(connectivity).unwrap() | |
| } | |
| fn make_fully_connected(n: usize) -> CouplingGraph { | |
| let connectivity = vec![vec![true; n]; n]; | |
| CouplingGraph::new(connectivity).unwrap() | |
| } | |
| fn test_coupling_graph_neighbors() { | |
| let graph = make_linear_coupling(5); | |
| let neighbors = graph.neighbors(2).unwrap(); | |
| assert!(neighbors.contains(&1)); | |
| assert!(neighbors.contains(&3)); | |
| assert!(neighbors.contains(&2)); // self-loop | |
| assert!(!neighbors.contains(&0)); | |
| } | |
| fn test_coupling_graph_distance() { | |
| let graph = make_linear_coupling(5); | |
| assert_eq!(graph.distance(0, 0).unwrap(), 0); | |
| assert_eq!(graph.distance(0, 1).unwrap(), 1); | |
| assert_eq!(graph.distance(0, 4).unwrap(), 4); | |
| } | |
| fn test_qubit_calibration_valid() { | |
| let cal = QubitCalibration::new(0, 5.0, 100.0, 50.0, 0.001, 0.01, 0.02, 0.01) | |
| .expect("Valid calibration"); | |
| assert_eq!(cal.qubit, 0); | |
| assert_eq!(cal.t1, 100.0); | |
| } | |
| fn test_qubit_calibration_t2_exceeds_t1() { | |
| let result = QubitCalibration::new(0, 5.0, 100.0, 150.0, 0.001, 0.01, 0.02, 0.01); | |
| assert!(result.is_err()); | |
| } | |
| fn test_timing_constraints_valid() { | |
| let timing = TimingConstraints::new(10.0, 100.0, 200.0, 500.0, 10000.0) | |
| .expect("Valid timing"); | |
| assert_eq!(timing.gate_duration_min, 10.0); | |
| } | |
| fn test_calibration_snapshot_hash() { | |
| let mut cals = BTreeMap::new(); | |
| cals.insert( | |
| 0, | |
| QubitCalibration::new(0, 5.0, 100.0, 50.0, 0.001, 0.01, 0.02, 0.01).unwrap(), | |
| ); | |
| let snapshot1 = | |
| CalibrationSnapshot::new("ibm-fake".to_string(), 1000, cals.clone(), BTreeMap::new()) | |
| .expect("Valid snapshot"); | |
| let snapshot2 = | |
| CalibrationSnapshot::new("ibm-fake".to_string(), 1000, cals, BTreeMap::new()) | |
| .expect("Valid snapshot"); | |
| // Same inputs → same hash | |
| assert_eq!(snapshot1.calibration_hash, snapshot2.calibration_hash); | |
| } | |
| fn test_backend_contract_valid() { | |
| let graph = make_fully_connected(3); | |
| let native_gates = vec![NativeGate::H, NativeGate::CX]; | |
| let pulse_defs = HashMap::new(); | |
| let mut cals = BTreeMap::new(); | |
| for i in 0..3 { | |
| cals.insert( | |
| i, | |
| QubitCalibration::new(i, 5.0 + i as f64, 100.0, 50.0, 0.001, 0.01, 0.02, 0.01) | |
| .unwrap(), | |
| ); | |
| } | |
| let calibration = CalibrationSnapshot::new("test".to_string(), 0, cals, BTreeMap::new()) | |
| .expect("Valid calibration"); | |
| let timing = TimingConstraints::new(10.0, 100.0, 200.0, 500.0, 10000.0).unwrap(); | |
| let backend = QuantumBackend::new(3, graph, native_gates, pulse_defs, calibration, timing) | |
| .expect("Valid backend"); | |
| assert_eq!(backend.num_qubits, 3); | |
| assert!(backend.supports_gate(&NativeGate::H)); | |
| assert!(backend.can_apply_two_qubit_gate(0, 1).unwrap()); | |
| } | |
| fn test_backend_validates_mismatched_qubits() { | |
| let graph = make_fully_connected(3); | |
| let native_gates = vec![NativeGate::H]; | |
| let pulse_defs = HashMap::new(); | |
| let mut cals = BTreeMap::new(); | |
| cals.insert( | |
| 0, | |
| QubitCalibration::new(0, 5.0, 100.0, 50.0, 0.001, 0.01, 0.02, 0.01).unwrap(), | |
| ); | |
| // Missing qubits 1 and 2! | |
| let calibration = CalibrationSnapshot::new("test".to_string(), 0, cals, BTreeMap::new()) | |
| .expect("Valid calibration"); | |
| let timing = TimingConstraints::new(10.0, 100.0, 200.0, 500.0, 10000.0).unwrap(); | |
| let result = QuantumBackend::new(3, graph, native_gates, pulse_defs, calibration, timing); | |
| assert!(result.is_err()); | |
| } | |
| } | |