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:
@@ -0,0 +1,452 @@
|
||||
//! ZX-diagram: nodes (Z/X spiders, H-boxes) and wires.
|
||||
//!
|
||||
//! The ZX-calculus is a graphical language for quantum computing.
|
||||
//! Spiders are the fundamental building blocks:
|
||||
//! - Z-spider (green): |0⟩^⊗n + e^{iα}|1⟩^⊗n
|
||||
//! - X-spider (red): |+⟩^⊗n + e^{iα}|−⟩^⊗n
|
||||
//! - H-box: Hadamard gate
|
||||
|
||||
use std::collections::HashMap;
|
||||
use std::f64::consts::PI;
|
||||
|
||||
use serde::{Deserialize, Serialize};
|
||||
|
||||
/// Identifier for a node in the ZX diagram.
|
||||
pub type NodeId = usize;
|
||||
/// Identifier for a wire (edge) in the ZX diagram.
|
||||
pub type WireId = usize;
|
||||
|
||||
/// Node type in the ZX-calculus.
|
||||
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
|
||||
pub enum ZXNode {
|
||||
/// Z-spider (green dot) with phase α (radians).
|
||||
Z { phase: f64 },
|
||||
/// X-spider (red dot) with phase α (radians).
|
||||
X { phase: f64 },
|
||||
/// Hadamard box (yellow square).
|
||||
H,
|
||||
/// Input boundary (numbered wire from left).
|
||||
Input(usize),
|
||||
/// Output boundary (numbered wire from right).
|
||||
Output(usize),
|
||||
}
|
||||
|
||||
impl ZXNode {
|
||||
/// Phase of the spider, 0 for H/boundary nodes.
|
||||
#[must_use]
|
||||
pub fn phase(&self) -> f64 {
|
||||
match self {
|
||||
Self::Z { phase } | Self::X { phase } => *phase,
|
||||
_ => 0.0,
|
||||
}
|
||||
}
|
||||
|
||||
/// True if this is a Z-spider.
|
||||
#[must_use]
|
||||
pub fn is_z(&self) -> bool {
|
||||
matches!(self, Self::Z { .. })
|
||||
}
|
||||
/// True if this is an X-spider.
|
||||
#[must_use]
|
||||
pub fn is_x(&self) -> bool {
|
||||
matches!(self, Self::X { .. })
|
||||
}
|
||||
/// True if this is an H-box.
|
||||
#[must_use]
|
||||
pub fn is_h(&self) -> bool {
|
||||
matches!(self, Self::H)
|
||||
}
|
||||
/// True if this is a boundary (input or output).
|
||||
#[must_use]
|
||||
pub fn is_boundary(&self) -> bool {
|
||||
matches!(self, Self::Input(_) | Self::Output(_))
|
||||
}
|
||||
|
||||
/// True if this is a zero-phase spider (identity-like).
|
||||
#[must_use]
|
||||
pub fn is_zero_phase(&self) -> bool {
|
||||
match self {
|
||||
Self::Z { phase } | Self::X { phase } => {
|
||||
phase.abs() < 1e-12 || (phase - 2.0 * PI).abs() < 1e-12
|
||||
}
|
||||
_ => false,
|
||||
}
|
||||
}
|
||||
|
||||
/// Same colour as another spider?
|
||||
#[must_use]
|
||||
pub fn same_colour(&self, other: &Self) -> bool {
|
||||
matches!(
|
||||
(self, other),
|
||||
(Self::Z { .. }, Self::Z { .. }) | (Self::X { .. }, Self::X { .. })
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
impl std::fmt::Display for ZXNode {
|
||||
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
|
||||
match self {
|
||||
Self::Z { phase } => write!(f, "Z({phase:.3})"),
|
||||
Self::X { phase } => write!(f, "X({phase:.3})"),
|
||||
Self::H => write!(f, "H"),
|
||||
Self::Input(i) => write!(f, "In({i})"),
|
||||
Self::Output(i) => write!(f, "Out({i})"),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Wire type: regular (Clifford) or Hadamard-decorated.
|
||||
#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
|
||||
pub enum WireType {
|
||||
/// Standard wire.
|
||||
Regular,
|
||||
/// Hadamard wire (equivalent to inserting an H-box).
|
||||
Hadamard,
|
||||
}
|
||||
|
||||
/// A ZX-diagram: undirected hypergraph of spiders and wires.
|
||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||
pub struct ZXDiagram {
|
||||
/// Node storage: node_id → ZXNode.
|
||||
pub nodes: HashMap<NodeId, ZXNode>,
|
||||
/// Wire storage: wire_id → (node_a, node_b, wire_type).
|
||||
pub wires: HashMap<WireId, (NodeId, NodeId, WireType)>,
|
||||
/// Ordered input node ids.
|
||||
pub inputs: Vec<NodeId>,
|
||||
/// Ordered output node ids.
|
||||
pub outputs: Vec<NodeId>,
|
||||
next_node: NodeId,
|
||||
next_wire: WireId,
|
||||
}
|
||||
|
||||
impl ZXDiagram {
|
||||
// ── Construction ──────────────────────────────────────────────
|
||||
|
||||
/// Empty diagram.
|
||||
#[must_use]
|
||||
pub fn new() -> Self {
|
||||
Self {
|
||||
nodes: HashMap::new(),
|
||||
wires: HashMap::new(),
|
||||
inputs: Vec::new(),
|
||||
outputs: Vec::new(),
|
||||
next_node: 0,
|
||||
next_wire: 0,
|
||||
}
|
||||
}
|
||||
|
||||
/// Add a node, returning its id.
|
||||
pub fn add_node(&mut self, node: ZXNode) -> NodeId {
|
||||
let id = self.next_node;
|
||||
self.next_node += 1;
|
||||
self.nodes.insert(id, node);
|
||||
id
|
||||
}
|
||||
|
||||
/// Add an input boundary, returning its node id.
|
||||
pub fn add_input(&mut self) -> NodeId {
|
||||
let idx = self.inputs.len();
|
||||
let id = self.add_node(ZXNode::Input(idx));
|
||||
self.inputs.push(id);
|
||||
id
|
||||
}
|
||||
|
||||
/// Add an output boundary, returning its node id.
|
||||
pub fn add_output(&mut self) -> NodeId {
|
||||
let idx = self.outputs.len();
|
||||
let id = self.add_node(ZXNode::Output(idx));
|
||||
self.outputs.push(id);
|
||||
id
|
||||
}
|
||||
|
||||
/// Add a wire between two nodes.
|
||||
pub fn add_wire(&mut self, a: NodeId, b: NodeId) -> WireId {
|
||||
let id = self.next_wire;
|
||||
self.next_wire += 1;
|
||||
self.wires.insert(id, (a, b, WireType::Regular));
|
||||
id
|
||||
}
|
||||
|
||||
/// Add a Hadamard-decorated wire.
|
||||
pub fn add_h_wire(&mut self, a: NodeId, b: NodeId) -> WireId {
|
||||
let id = self.next_wire;
|
||||
self.next_wire += 1;
|
||||
self.wires.insert(id, (a, b, WireType::Hadamard));
|
||||
id
|
||||
}
|
||||
|
||||
// ── Accessors ─────────────────────────────────────────────────
|
||||
|
||||
/// Neighbours of a node (connected node ids).
|
||||
#[must_use]
|
||||
pub fn neighbours(&self, node: NodeId) -> Vec<(NodeId, WireId, WireType)> {
|
||||
self.wires
|
||||
.iter()
|
||||
.filter_map(|(&wid, &(a, b, wt))| {
|
||||
if a == node {
|
||||
Some((b, wid, wt))
|
||||
} else if b == node {
|
||||
Some((a, wid, wt))
|
||||
} else {
|
||||
None
|
||||
}
|
||||
})
|
||||
.collect()
|
||||
}
|
||||
|
||||
/// Degree of a node.
|
||||
#[must_use]
|
||||
pub fn degree(&self, node: NodeId) -> usize {
|
||||
self.neighbours(node).len()
|
||||
}
|
||||
|
||||
/// Number of nodes.
|
||||
#[must_use]
|
||||
pub fn node_count(&self) -> usize {
|
||||
self.nodes.len()
|
||||
}
|
||||
|
||||
/// Number of wires.
|
||||
#[must_use]
|
||||
pub fn wire_count(&self) -> usize {
|
||||
self.wires.len()
|
||||
}
|
||||
|
||||
/// Number of input/output qubits.
|
||||
#[must_use]
|
||||
pub fn qubit_count(&self) -> usize {
|
||||
self.inputs.len()
|
||||
}
|
||||
|
||||
// ── Diagram mutation ──────────────────────────────────────────
|
||||
|
||||
/// Remove a node and all its wires.
|
||||
pub fn remove_node(&mut self, id: NodeId) {
|
||||
self.nodes.remove(&id);
|
||||
self.wires.retain(|_, &mut (a, b, _)| a != id && b != id);
|
||||
self.inputs.retain(|&i| i != id);
|
||||
self.outputs.retain(|&o| o != id);
|
||||
}
|
||||
|
||||
/// Remove a wire by id.
|
||||
pub fn remove_wire(&mut self, wid: WireId) {
|
||||
self.wires.remove(&wid);
|
||||
}
|
||||
|
||||
/// Update the phase of a spider node.
|
||||
pub fn set_phase(&mut self, id: NodeId, phase: f64) {
|
||||
if let Some(ZXNode::Z { phase: p } | ZXNode::X { phase: p }) = self.nodes.get_mut(&id) {
|
||||
*p = phase;
|
||||
}
|
||||
}
|
||||
|
||||
// ── Rewrite helpers ───────────────────────────────────────────
|
||||
|
||||
/// True if nodes `a` and `b` are directly connected.
|
||||
#[must_use]
|
||||
pub fn connected(&self, a: NodeId, b: NodeId) -> bool {
|
||||
self.wires
|
||||
.values()
|
||||
.any(|&(x, y, _)| (x == a && y == b) || (x == b && y == a))
|
||||
}
|
||||
|
||||
/// Find the wire connecting `a` and `b`, if any.
|
||||
#[must_use]
|
||||
pub fn wire_between(&self, a: NodeId, b: NodeId) -> Option<WireId> {
|
||||
self.wires.iter().find_map(|(&wid, &(x, y, _))| {
|
||||
if (x == a && y == b) || (x == b && y == a) {
|
||||
Some(wid)
|
||||
} else {
|
||||
None
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
/// Merge two same-colour adjacent spiders (spider fusion rule).
|
||||
/// Returns the id of the merged spider (reuses `a`), removes `b`.
|
||||
pub fn fuse_spiders(&mut self, a: NodeId, b: NodeId) -> Option<NodeId> {
|
||||
let node_a = self.nodes.get(&a)?.clone();
|
||||
let node_b = self.nodes.get(&b)?.clone();
|
||||
if !node_a.same_colour(&node_b) {
|
||||
return None;
|
||||
}
|
||||
|
||||
// New phase = sum of phases (mod 2π)
|
||||
let new_phase = (node_a.phase() + node_b.phase()).rem_euclid(2.0 * PI);
|
||||
self.set_phase(a, new_phase);
|
||||
|
||||
// Find wire between a and b and remove it
|
||||
if let Some(wid) = self.wire_between(a, b) {
|
||||
self.remove_wire(wid);
|
||||
}
|
||||
|
||||
// Redirect all wires from b to a (except the one we removed)
|
||||
let b_wires: Vec<(WireId, NodeId, WireType)> = self
|
||||
.wires
|
||||
.iter()
|
||||
.filter_map(|(&wid, &(x, y, wt))| {
|
||||
if x == b {
|
||||
Some((wid, y, wt))
|
||||
} else if y == b {
|
||||
Some((wid, x, wt))
|
||||
} else {
|
||||
None
|
||||
}
|
||||
})
|
||||
.collect();
|
||||
|
||||
for (wid, other, wt) in b_wires {
|
||||
self.wires.remove(&wid);
|
||||
if other != a {
|
||||
self.wires.insert(wid, (a, other, wt));
|
||||
}
|
||||
}
|
||||
|
||||
self.remove_node(b);
|
||||
Some(a)
|
||||
}
|
||||
}
|
||||
|
||||
impl Default for ZXDiagram {
|
||||
fn default() -> Self {
|
||||
Self::new()
|
||||
}
|
||||
}
|
||||
|
||||
/// Build the ZX representation of a Hadamard gate (1 qubit).
|
||||
/// H = Z(0) —H— Z(0) with Hadamard wire.
|
||||
#[must_use]
|
||||
pub fn hadamard_diagram() -> ZXDiagram {
|
||||
let mut d = ZXDiagram::new();
|
||||
let inp = d.add_input();
|
||||
let z = d.add_node(ZXNode::Z { phase: 0.0 });
|
||||
let out = d.add_output();
|
||||
d.add_wire(inp, z);
|
||||
d.add_h_wire(z, out);
|
||||
d
|
||||
}
|
||||
|
||||
/// Build the ZX representation of a CNOT gate (2 qubits).
|
||||
#[must_use]
|
||||
pub fn cnot_diagram() -> ZXDiagram {
|
||||
let mut d = ZXDiagram::new();
|
||||
let in0 = d.add_input();
|
||||
let in1 = d.add_input();
|
||||
let z = d.add_node(ZXNode::Z { phase: 0.0 }); // control
|
||||
let x = d.add_node(ZXNode::X { phase: 0.0 }); // target
|
||||
let out0 = d.add_output();
|
||||
let out1 = d.add_output();
|
||||
d.add_wire(in0, z);
|
||||
d.add_wire(z, out0);
|
||||
d.add_wire(in1, x);
|
||||
d.add_wire(x, out1);
|
||||
d.add_wire(z, x); // entangling wire
|
||||
d
|
||||
}
|
||||
|
||||
/// Build the ZX representation of a T gate.
|
||||
/// T = Z(π/4)
|
||||
#[must_use]
|
||||
pub fn t_gate_diagram() -> ZXDiagram {
|
||||
let mut d = ZXDiagram::new();
|
||||
let inp = d.add_input();
|
||||
let z = d.add_node(ZXNode::Z { phase: PI / 4.0 });
|
||||
let out = d.add_output();
|
||||
d.add_wire(inp, z);
|
||||
d.add_wire(z, out);
|
||||
d
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
|
||||
#[test]
|
||||
fn cnot_zx_representation() {
|
||||
let d = cnot_diagram();
|
||||
assert_eq!(d.qubit_count(), 2);
|
||||
assert_eq!(d.inputs.len(), 2);
|
||||
assert_eq!(d.outputs.len(), 2);
|
||||
// Z (control) and X (target) nodes plus 4 boundary nodes = 6
|
||||
assert_eq!(d.node_count(), 6);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn hadamard_zx_representation() {
|
||||
let d = hadamard_diagram();
|
||||
assert_eq!(d.qubit_count(), 1);
|
||||
assert_eq!(d.wire_count(), 2); // regular + hadamard wire
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn t_gate_zx_representation() {
|
||||
let d = t_gate_diagram();
|
||||
// Should have a Z(π/4) node
|
||||
let has_t = d
|
||||
.nodes
|
||||
.values()
|
||||
.any(|n| matches!(n, ZXNode::Z { phase } if (phase - PI/4.0).abs() < 1e-9));
|
||||
assert!(has_t, "T gate should contain Z(π/4) node");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn spider_fusion_z() {
|
||||
let mut d = ZXDiagram::new();
|
||||
let a = d.add_node(ZXNode::Z { phase: PI / 4.0 });
|
||||
let b = d.add_node(ZXNode::Z { phase: PI / 4.0 });
|
||||
d.add_wire(a, b);
|
||||
let merged = d.fuse_spiders(a, b);
|
||||
assert!(merged.is_some());
|
||||
// Fused phase = π/4 + π/4 = π/2
|
||||
let new_phase = d.nodes[&a].phase();
|
||||
assert!(
|
||||
(new_phase - PI / 2.0).abs() < 1e-9,
|
||||
"fused phase = {new_phase}"
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn spider_fusion_x() {
|
||||
let mut d = ZXDiagram::new();
|
||||
let a = d.add_node(ZXNode::X { phase: PI / 2.0 });
|
||||
let b = d.add_node(ZXNode::X { phase: PI / 2.0 });
|
||||
d.add_wire(a, b);
|
||||
d.fuse_spiders(a, b);
|
||||
let new_phase = d.nodes[&a].phase();
|
||||
assert!((new_phase - PI).abs() < 1e-9);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn spider_fusion_different_colour_fails() {
|
||||
let mut d = ZXDiagram::new();
|
||||
let a = d.add_node(ZXNode::Z { phase: 0.0 });
|
||||
let b = d.add_node(ZXNode::X { phase: 0.0 });
|
||||
d.add_wire(a, b);
|
||||
assert!(d.fuse_spiders(a, b).is_none());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn identity_removal_candidate() {
|
||||
// A zero-phase Z-spider with exactly 2 legs is an identity wire
|
||||
let mut d = ZXDiagram::new();
|
||||
let inp = d.add_input();
|
||||
let z = d.add_node(ZXNode::Z { phase: 0.0 });
|
||||
let out = d.add_output();
|
||||
d.add_wire(inp, z);
|
||||
d.add_wire(z, out);
|
||||
assert_eq!(d.degree(z), 2);
|
||||
assert!(d.nodes[&z].is_zero_phase());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn node_degree() {
|
||||
let d = cnot_diagram();
|
||||
// The Z (control) node connects to: in0, out0, X = degree 3
|
||||
let z_id = d
|
||||
.nodes
|
||||
.iter()
|
||||
.find_map(|(&id, n)| if n.is_z() { Some(id) } else { None })
|
||||
.unwrap();
|
||||
assert_eq!(d.degree(z_id), 3);
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user