use crate::circuit::Qubit;
use crate::circuit::circuit_impl::Circuit;
use crate::circuit::circuit_to_matrix;
use crate::circuit::error::CircuitError;
use crate::circuit::symbolic_matrix::{SymbolicMatrix, circuit_to_symbolic_matrix};
use indexmap::IndexSet;
use ndarray::Array2;
use num_complex::Complex64;
use std::collections::HashMap;
use std::sync::{Arc, OnceLock};
#[derive(Debug, Clone)]
pub struct FrozenCircuit {
pub(crate) circuit: Circuit,
used_symbols: Arc<IndexSet<String>>,
symbolic_matrix_cache: Arc<OnceLock<Arc<SymbolicMatrix>>>,
storage_order_symbolic_matrix_cache: Arc<OnceLock<Arc<SymbolicMatrix>>>,
}
impl FrozenCircuit {
pub fn new(circuit: Circuit) -> Self {
let used_symbols = Arc::new(circuit.used_symbols());
Self {
circuit,
used_symbols,
symbolic_matrix_cache: Arc::new(OnceLock::new()),
storage_order_symbolic_matrix_cache: Arc::new(OnceLock::new()),
}
}
pub fn circuit(&self) -> &Circuit {
&self.circuit
}
pub fn used_symbols(&self) -> &IndexSet<String> {
&self.used_symbols
}
pub(crate) fn qubits_in_numeric_order(&self) -> Vec<Qubit> {
let mut qubits = self.circuit.qubits();
qubits.sort_unstable();
qubits
}
pub fn symbolic_matrix(&self) -> Result<Arc<SymbolicMatrix>, CircuitError> {
if let Some(matrix) = self.symbolic_matrix_cache.get() {
return Ok(matrix.clone());
}
let order: Vec<usize> = self
.qubits_in_numeric_order()
.iter()
.map(|q| q.index())
.collect();
let matrix = Arc::new(circuit_to_symbolic_matrix(&self.circuit, Some(&order))?);
let _ = self.symbolic_matrix_cache.set(matrix.clone());
Ok(self.symbolic_matrix_cache.get().cloned().unwrap_or(matrix))
}
fn symbolic_matrix_in_storage_order(&self) -> Result<Arc<SymbolicMatrix>, CircuitError> {
if let Some(matrix) = self.storage_order_symbolic_matrix_cache.get() {
return Ok(matrix.clone());
}
let order: Vec<usize> = self.circuit.qubits().iter().map(|q| q.index()).collect();
let matrix = Arc::new(circuit_to_symbolic_matrix(&self.circuit, Some(&order))?);
let _ = self.storage_order_symbolic_matrix_cache.set(matrix.clone());
Ok(self
.storage_order_symbolic_matrix_cache
.get()
.cloned()
.unwrap_or(matrix))
}
}
impl PartialEq for FrozenCircuit {
fn eq(&self, other: &Self) -> bool {
self.circuit == other.circuit && self.used_symbols == other.used_symbols
}
}
#[derive(Debug, Clone)]
pub struct CircuitGate {
pub name: Arc<String>,
pub(crate) num_qubits: usize,
pub(crate) num_params: usize,
pub(crate) signature_params: IndexSet<String>,
pub(crate) circuit: Arc<FrozenCircuit>,
}
impl PartialEq for CircuitGate {
fn eq(&self, other: &Self) -> bool {
self.name == other.name
&& self.num_qubits == other.num_qubits
&& self.num_params == other.num_params
&& self
.signature_params
.iter()
.eq(other.signature_params.iter())
&& self.circuit.circuit() == other.circuit.circuit()
}
}
impl CircuitGate {
pub fn new(name: impl Into<String>, circuit: FrozenCircuit) -> Result<Self, CircuitError> {
let num_qubits = circuit.circuit.qubits().len();
let signature_params = circuit.used_symbols().clone();
let num_params = signature_params.len();
Ok(Self {
name: Arc::new(name.into()),
num_qubits,
num_params,
signature_params,
circuit: Arc::new(circuit),
})
}
pub fn with_signature(
name: impl Into<String>,
circuit: FrozenCircuit,
params: impl IntoIterator<Item = String>,
) -> Result<Self, CircuitError> {
let mut signature_params = IndexSet::new();
for param in params {
if !signature_params.insert(param.clone()) {
return Err(CircuitError::InvalidOperation(format!(
"duplicate circuit gate parameter '{param}'"
)));
}
}
for symbol in circuit.used_symbols() {
if !signature_params.contains(symbol) {
return Err(CircuitError::InvalidOperation(format!(
"circuit gate implementation references undeclared parameter '{symbol}'"
)));
}
}
let num_qubits = circuit.circuit.qubits().len();
let num_params = signature_params.len();
Ok(Self {
name: Arc::new(name.into()),
num_qubits,
num_params,
signature_params,
circuit: Arc::new(circuit),
})
}
pub fn signature_params(&self) -> &IndexSet<String> {
&self.signature_params
}
pub fn used_symbols(&self) -> &IndexSet<String> {
self.circuit.used_symbols()
}
pub fn symbols(&self) -> IndexSet<String> {
self.used_symbols().clone()
}
pub fn num_qubits(&self) -> usize {
self.num_qubits
}
pub fn num_params(&self) -> usize {
self.num_params
}
pub(crate) fn matrix_for_params(
&self,
params: &[f64],
) -> Result<Array2<Complex64>, CircuitError> {
if params.len() != self.signature_params.len() {
return Err(CircuitError::ParameterCountMismatch {
expected: self.signature_params.len(),
actual: params.len(),
});
}
for (index, &value) in params.iter().enumerate() {
if !value.is_finite() {
return Err(CircuitError::InvalidParameterValue(index, value));
}
}
let bindings: HashMap<&str, f64> = self
.signature_params
.iter()
.map(String::as_str)
.zip(params.iter().copied())
.collect();
let assigned = self
.circuit
.circuit
.assign_parameters(&Some(bindings))
.map_err(|_| CircuitError::SymbolicParameterError)?;
let order: Vec<usize> = assigned.qubits().iter().rev().map(|q| q.index()).collect();
circuit_to_matrix(&assigned, Some(&order))
}
pub fn circuit(&self) -> Arc<FrozenCircuit> {
self.circuit.clone()
}
pub fn symbolic_matrix(&self) -> Result<Arc<SymbolicMatrix>, CircuitError> {
self.circuit.symbolic_matrix_in_storage_order()
}
pub fn name(&self) -> &str {
&self.name
}
pub fn inverse(&self) -> Result<Self, CircuitError> {
let inverted_circuit = self.circuit.circuit.inverse()?;
let frozen_inverted = FrozenCircuit::new(inverted_circuit);
CircuitGate::with_signature(
format!("{}_dg", self.name),
frozen_inverted,
self.signature_params.iter().cloned(),
)
}
}
#[cfg(test)]
mod tests {
use super::*;
use crate::circuit::{Circuit, Parameter, Qubit};
#[test]
fn numeric_matrix_binds_declared_signature_and_global_phase_without_mutation() {
let mut circuit = Circuit::from_qubits(vec![Qubit::new(7)]).unwrap();
let theta = Parameter::symbol("theta");
let phi = Parameter::symbol("phi");
circuit.rx(Qubit::new(7), theta.clone()).unwrap();
circuit.rz(Qubit::new(7), phi.clone()).unwrap();
circuit.set_global_phase(theta + phi / Parameter::from(2.0));
let gate = CircuitGate::with_signature(
"declared",
FrozenCircuit::new(circuit),
["unused".to_string(), "phi".to_string(), "theta".to_string()],
)
.unwrap();
let original = gate.clone();
for (theta, phi) in [(0.37_f64, -0.21_f64), (-0.61, 0.42), (0.37, -0.21)] {
let matrix = gate.matrix_for_params(&[12.0, phi, theta]).unwrap();
for row in 0..2 {
let phase = theta + phi / 2.0 + if row == 0 { -phi / 2.0 } else { phi / 2.0 };
for col in 0..2 {
let rx = if row == col {
Complex64::new((theta / 2.0).cos(), 0.0)
} else {
Complex64::new(0.0, -(theta / 2.0).sin())
};
assert!(
(matrix[[row, col]] - Complex64::from_polar(1.0, phase) * rx).norm()
< 1e-12
);
}
}
assert_eq!(gate, original);
assert_eq!(gate.used_symbols().len(), 2);
}
}
#[test]
fn numeric_matrix_rejects_invalid_arguments_and_nonunitary_definitions() {
let gate = CircuitGate::with_signature(
"unused",
FrozenCircuit::new(Circuit::new(1)),
["unused".to_string()],
)
.unwrap();
for params in [vec![], vec![0.1, 0.2]] {
assert!(matches!(
gate.matrix_for_params(¶ms),
Err(CircuitError::ParameterCountMismatch { expected: 1, .. })
));
}
for value in [f64::NAN, f64::INFINITY, f64::NEG_INFINITY] {
assert!(matches!(
gate.matrix_for_params(&[value]),
Err(CircuitError::InvalidParameterValue(0, _))
));
}
for directive in [
crate::circuit::Directive::Measure,
crate::circuit::Directive::Reset,
] {
let mut body = Circuit::new(1);
body.append(
crate::circuit::Instruction::Directive(directive),
[Qubit::new(0)],
[],
None,
)
.unwrap();
let gate = CircuitGate::new("nonunitary", FrozenCircuit::new(body)).unwrap();
assert!(matches!(
gate.matrix_for_params(&[]),
Err(CircuitError::NoMatrixRepresentation)
));
}
}
#[test]
fn signature_params_are_distinct_from_used_symbols() {
let mut circuit = Circuit::new(1);
circuit
.rx(Qubit::new(0), Parameter::symbol("used"))
.unwrap();
let gate = CircuitGate::with_signature(
"declared",
FrozenCircuit::new(circuit),
["unused".to_string(), "used".to_string()],
)
.unwrap();
assert_eq!(gate.num_params(), 2);
assert_eq!(
gate.signature_params().iter().cloned().collect::<Vec<_>>(),
vec!["unused".to_string(), "used".to_string()]
);
assert!(gate.symbols().contains("used"));
assert!(!gate.symbols().contains("unused"));
assert_eq!(gate.used_symbols(), &gate.symbols());
}
#[test]
fn explicit_signature_ignores_interned_but_unreferenced_symbols() {
let mut circuit = Circuit::new(1);
circuit.add_parameter(Parameter::symbol("stale"));
circuit
.rx(Qubit::new(0), Parameter::symbol("used"))
.unwrap();
let gate = CircuitGate::with_signature(
"declared",
FrozenCircuit::new(circuit),
["declared_unused".to_string(), "used".to_string()],
)
.unwrap();
assert_eq!(
gate.signature_params()
.iter()
.map(String::as_str)
.collect::<Vec<_>>(),
vec!["declared_unused", "used"]
);
assert_eq!(
gate.used_symbols()
.iter()
.map(String::as_str)
.collect::<Vec<_>>(),
vec!["used"]
);
}
#[test]
fn explicit_signature_still_rejects_actually_used_undeclared_symbols() {
let mut circuit = Circuit::new(1);
circuit
.rx(Qubit::new(0), Parameter::symbol("used"))
.unwrap();
let error = CircuitGate::with_signature(
"missing",
FrozenCircuit::new(circuit),
["different".to_string()],
)
.unwrap_err();
assert!(
matches!(error, CircuitError::InvalidOperation(message) if message.contains("used"))
);
}
#[test]
fn inverse_preserves_explicit_signature_and_live_dependencies() {
let mut circuit = Circuit::new(1);
circuit.add_parameter(Parameter::symbol("stale"));
circuit
.rx(Qubit::new(0), Parameter::symbol("used"))
.unwrap();
let gate = CircuitGate::with_signature(
"forward",
FrozenCircuit::new(circuit),
["declared_unused".to_string(), "used".to_string()],
)
.unwrap();
let inverse = gate.inverse().unwrap();
assert_eq!(inverse.signature_params(), gate.signature_params());
assert_eq!(inverse.used_symbols(), gate.used_symbols());
}
#[test]
fn frozen_circuits_compare_structurally_ignoring_matrix_cache() {
let make_frozen = || {
let mut circuit = Circuit::new(1);
circuit.h(Qubit::new(0)).unwrap();
FrozenCircuit::new(circuit)
};
let a = make_frozen();
let b = make_frozen();
assert_eq!(a, b);
a.symbolic_matrix().unwrap();
assert_eq!(a, b);
let mut other = Circuit::new(1);
other.x(Qubit::new(0)).unwrap();
assert_ne!(a, FrozenCircuit::new(other));
}
}