Merge pull request 'test(symclaw-skill): cover handlers_advanced via JSON API' (#10) from ci-doctor/coverage-20260518-201834 into master

Reviewed-on: #10
This commit is contained in:
redclawsystems
2026-05-19 04:39:49 +00:00
commit f4b75db2ee
291 changed files with 130230 additions and 0 deletions
+261
View File
@@ -0,0 +1,261 @@
//! VQE (Variational Quantum Eigensolver) tools.
//!
//! Provides:
//! - UCCSD ansatz parameter-shift gradient formula
//! - Hartree-Fock reference state (symbolic)
//! - Parameter-shift rule for quantum gradients
use serde::{Deserialize, Serialize};
use std::collections::HashMap;
/// A parameterized quantum ansatz: list of (gate_type, qubit_indices, parameter_name).
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct Ansatz {
/// Layers: each is a list of (gate, qubits, param_name).
pub layers: Vec<AnsatzLayer>,
/// Current parameter values.
pub params: HashMap<String, f64>,
/// Number of qubits.
pub n_qubits: usize,
}
/// A single ansatz layer.
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct AnsatzLayer {
pub gates: Vec<AnsatzGate>,
}
/// A parameterized gate in the ansatz.
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct AnsatzGate {
pub gate_type: String,
pub qubits: Vec<usize>,
pub param_name: Option<String>,
}
impl Ansatz {
/// Create an empty ansatz for n qubits.
#[must_use]
pub fn new(n_qubits: usize) -> Self {
Self {
layers: Vec::new(),
n_qubits,
params: HashMap::new(),
}
}
/// Add a Rz(θ) rotation layer.
pub fn add_rz_layer(&mut self, qubit: usize, param_name: &str, init_val: f64) -> &mut Self {
self.layers.push(AnsatzLayer {
gates: vec![AnsatzGate {
gate_type: "Rz".into(),
qubits: vec![qubit],
param_name: Some(param_name.into()),
}],
});
self.params.insert(param_name.into(), init_val);
self
}
/// Add a CNOT entangling layer.
pub fn add_cnot_layer(&mut self, ctrl: usize, tgt: usize) -> &mut Self {
self.layers.push(AnsatzLayer {
gates: vec![AnsatzGate {
gate_type: "CNOT".into(),
qubits: vec![ctrl, tgt],
param_name: None,
}],
});
self
}
/// Number of variational parameters.
#[must_use]
pub fn n_params(&self) -> usize {
self.params.len()
}
/// List parameter names.
#[must_use]
pub fn param_names(&self) -> Vec<&str> {
self.params.keys().map(String::as_str).collect()
}
}
/// Parameter-shift gradient rule.
///
/// For a gate G(θ) = exp(-iθ/2 P) where P is a Pauli:
/// ∂⟨H⟩/∂θ = [⟨H⟩(θ + π/2) - ⟨H⟩(θ - π/2)] / 2
///
/// This function returns `(θ_plus, θ_minus, coefficient)` for a parameter at `θ`.
#[must_use]
pub fn parameter_shift_gradient_rule(theta: f64) -> (f64, f64, f64) {
let shift = std::f64::consts::PI / 2.0;
(theta + shift, theta - shift, 0.5)
}
/// Compute the numerical gradient of an expectation value using parameter-shift rule.
///
/// `expectation_fn` takes a parameter map and returns ⟨H⟩.
pub fn parameter_shift_gradient<F>(
params: &HashMap<String, f64>,
param_name: &str,
expectation_fn: &F,
) -> f64
where
F: Fn(&HashMap<String, f64>) -> f64,
{
let theta = *params.get(param_name).unwrap_or(&0.0);
let (tp, tm, coeff) = parameter_shift_gradient_rule(theta);
let mut params_plus = params.clone();
params_plus.insert(param_name.into(), tp);
let mut params_minus = params.clone();
params_minus.insert(param_name.into(), tm);
coeff * (expectation_fn(&params_plus) - expectation_fn(&params_minus))
}
/// Hartree-Fock reference state description.
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct HFReference {
/// Number of electrons.
pub n_electrons: usize,
/// Number of spin-orbitals.
pub n_orbitals: usize,
/// Occupied orbital indices (0-indexed).
pub occupied: Vec<usize>,
/// Virtual orbital indices.
pub virtual_orbs: Vec<usize>,
}
impl HFReference {
/// Create HF reference for `n_electrons` in `n_orbitals` spin-orbitals.
#[must_use]
pub fn new(n_electrons: usize, n_orbitals: usize) -> Self {
assert!(n_electrons <= n_orbitals);
let occupied: Vec<usize> = (0..n_electrons).collect();
let virtual_orbs: Vec<usize> = (n_electrons..n_orbitals).collect();
Self {
n_electrons,
n_orbitals,
occupied,
virtual_orbs,
}
}
/// Number of occupied orbitals.
#[must_use]
pub fn n_occ(&self) -> usize {
self.occupied.len()
}
/// Number of virtual orbitals.
#[must_use]
pub fn n_virt(&self) -> usize {
self.virtual_orbs.len()
}
/// UCCSD single excitation operators: (i→a) for i in occ, a in virt.
#[must_use]
pub fn single_excitations(&self) -> Vec<(usize, usize)> {
let mut excitations = Vec::new();
for &i in &self.occupied {
for &a in &self.virtual_orbs {
excitations.push((i, a));
}
}
excitations
}
/// UCCSD double excitation operators: (i,j→a,b) for i<j in occ, a<b in virt.
#[must_use]
pub fn double_excitations(&self) -> Vec<(usize, usize, usize, usize)> {
let mut excitations = Vec::new();
for (ki, &i) in self.occupied.iter().enumerate() {
for &j in &self.occupied[ki + 1..] {
for (ka, &a) in self.virtual_orbs.iter().enumerate() {
for &b in &self.virtual_orbs[ka + 1..] {
excitations.push((i, j, a, b));
}
}
}
}
excitations
}
/// Total UCCSD parameters: singles + doubles.
#[must_use]
pub fn n_uccsd_params(&self) -> usize {
self.single_excitations().len() + self.double_excitations().len()
}
}
#[cfg(test)]
mod tests {
use super::*;
use std::f64::consts::PI;
#[test]
fn parameter_shift_gradient_rule() {
let theta = 0.5;
let (tp, tm, coeff) = super::parameter_shift_gradient_rule(theta);
assert!((tp - (theta + PI / 2.0)).abs() < 1e-12);
assert!((tm - (theta - PI / 2.0)).abs() < 1e-12);
assert!((coeff - 0.5).abs() < 1e-12);
}
#[test]
fn parameter_shift_numerical_gradient() {
// f(θ) = cos(θ), ∂f/∂θ = -sin(θ)
let theta = 0.7;
let mut params = HashMap::new();
params.insert("theta".into(), theta);
let grad = parameter_shift_gradient(&params, "theta", &|p| p["theta"].cos());
let expected = -theta.sin();
assert!(
(grad - expected).abs() < 1e-6,
"gradient {grad} ≠ expected {expected}"
);
}
#[test]
fn hf_reference_state() {
let hf = HFReference::new(2, 4);
assert_eq!(hf.n_occ(), 2);
assert_eq!(hf.n_virt(), 2);
assert_eq!(hf.occupied, vec![0, 1]);
assert_eq!(hf.virtual_orbs, vec![2, 3]);
}
#[test]
fn uccsd_ansatz_singles() {
// 2 occ × 2 virt = 4 singles
let hf = HFReference::new(2, 4);
assert_eq!(hf.single_excitations().len(), 4);
}
#[test]
fn uccsd_ansatz_doubles() {
// C(2,2) × C(2,2) = 1 double excitation for 2 occ, 2 virt
let hf = HFReference::new(2, 4);
assert_eq!(hf.double_excitations().len(), 1);
}
#[test]
fn uccsd_parameterised() {
let hf = HFReference::new(2, 4);
let n = hf.n_uccsd_params();
assert_eq!(n, 5, "2e/4o: 4 singles + 1 double = 5 UCCSD params");
}
#[test]
fn ansatz_construction() {
let mut ansatz = Ansatz::new(2);
ansatz.add_rz_layer(0, "theta0", 0.0);
ansatz.add_cnot_layer(0, 1);
ansatz.add_rz_layer(1, "theta1", 0.0);
assert_eq!(ansatz.n_params(), 2);
assert_eq!(ansatz.layers.len(), 3);
}
}