use lift_core::context::Context;
use lift_core::pass::{AnalysisCache, Pass, PassResult};
use lift_quantum::gates::QuantumGate;
use std::collections::HashSet;
#[derive(Debug)]
pub struct GateCancellation;
impl Pass for GateCancellation {
fn name(&self) -> &str {
"gate-cancellation"
}
fn run(&self, ctx: &mut Context, _cache: &mut AnalysisCache) -> PassResult {
let mut cancelled = 0usize;
let mut ops_to_remove: HashSet<lift_core::operations::OpKey> = HashSet::new();
let block_keys: Vec<_> = ctx.blocks.keys().collect();
for block_key in block_keys {
let block = match ctx.blocks.get(block_key) {
Some(b) => b,
None => continue,
};
let op_list: Vec<_> = block.ops.clone();
for i in 0..op_list.len() {
let op1_key = op_list[i];
if ops_to_remove.contains(&op1_key) {
continue;
}
for &op2_key in op_list.iter().skip(i + 1) {
if ops_to_remove.contains(&op2_key) {
continue;
}
let (gate1, gate2, same_qubit) = {
let op1 = match ctx.ops.get(op1_key) {
Some(o) => o,
None => continue,
};
let op2 = match ctx.ops.get(op2_key) {
Some(o) => o,
None => continue,
};
let name1 = ctx.strings.resolve(op1.name).to_string();
let name2 = ctx.strings.resolve(op2.name).to_string();
let g1 = match QuantumGate::from_name(&name1) {
Some(g) => g,
None => continue,
};
let g2 = match QuantumGate::from_name(&name2) {
Some(g) => g,
None => continue,
};
let same = if !op1.results.is_empty() && !op2.inputs.is_empty() {
op1.results.iter().any(|r| op2.inputs.contains(r))
} else {
false
};
(g1, g2, same)
};
if gate1 == gate2
&& gate1.is_self_inverse()
&& same_qubit
&& cancel_pair(ctx, op1_key, op2_key, &mut ops_to_remove)
{
cancelled += 1;
break;
}
let is_adjoint_pair = matches!(
(&gate1, &gate2),
(QuantumGate::S, QuantumGate::Sdg)
| (QuantumGate::Sdg, QuantumGate::S)
| (QuantumGate::T, QuantumGate::Tdg)
| (QuantumGate::Tdg, QuantumGate::T)
);
if is_adjoint_pair
&& same_qubit
&& cancel_pair(ctx, op1_key, op2_key, &mut ops_to_remove)
{
cancelled += 1;
break;
}
}
}
if !ops_to_remove.is_empty() {
if let Some(block) = ctx.blocks.get_mut(block_key) {
block.ops.retain(|op| !ops_to_remove.contains(op));
}
}
}
for op_key in &ops_to_remove {
if let Some(op) = ctx.ops.remove(*op_key) {
for result in &op.results {
ctx.values.remove(*result);
}
}
}
if cancelled > 0 {
tracing::info!("Gate cancellation: cancelled {} gate pairs", cancelled);
PassResult::Changed
} else {
PassResult::Unchanged
}
}
fn invalidates(&self) -> Vec<&str> {
vec!["analysis", "quantum_analysis"]
}
}
fn cancel_pair(
ctx: &mut Context,
op1_key: lift_core::operations::OpKey,
op2_key: lift_core::operations::OpKey,
ops_to_remove: &mut HashSet<lift_core::operations::OpKey>,
) -> bool {
let (op1_input, op2_result) = {
let op1 = match ctx.ops.get(op1_key) {
Some(o) => o,
None => return false,
};
let op2 = match ctx.ops.get(op2_key) {
Some(o) => o,
None => return false,
};
if op1.inputs.is_empty() || op2.results.is_empty() {
return false;
}
(op1.inputs[0], op2.results[0])
};
let op_keys_all: Vec<_> = ctx.ops.keys().collect();
for ok in op_keys_all {
if ok == op1_key || ok == op2_key {
continue;
}
if let Some(op) = ctx.ops.get_mut(ok) {
for input in &mut op.inputs {
if *input == op2_result {
*input = op1_input;
}
}
}
}
ops_to_remove.insert(op1_key);
ops_to_remove.insert(op2_key);
true
}
#[cfg(test)]
mod tests {
use super::*;
use lift_core::attributes::Attributes;
use lift_core::location::Location;
#[test]
fn test_non_consecutive_cancellation() {
let mut ctx = Context::new();
let qubit = ctx.make_qubit_type();
let block = ctx.create_block();
let q0 = ctx.create_block_arg(block, qubit);
let q2 = ctx.create_block_arg(block, qubit);
let (h1, h1_res) = ctx.create_op(
"quantum.h",
"quantum",
vec![q0],
vec![qubit],
Attributes::new(),
Location::unknown(),
);
ctx.add_op_to_block(block, h1);
let (x, _) = ctx.create_op(
"quantum.x",
"quantum",
vec![q2],
vec![qubit],
Attributes::new(),
Location::unknown(),
);
ctx.add_op_to_block(block, x);
let (h2, h2_res) = ctx.create_op(
"quantum.h",
"quantum",
vec![h1_res[0]],
vec![qubit],
Attributes::new(),
Location::unknown(),
);
ctx.add_op_to_block(block, h2);
let (cx, _) = ctx.create_op(
"quantum.cx",
"quantum",
vec![h2_res[0], q2],
vec![qubit, qubit],
Attributes::new(),
Location::unknown(),
);
ctx.add_op_to_block(block, cx);
let result = GateCancellation.run(&mut ctx, &mut AnalysisCache::new());
assert!(result.changed());
let h_count = ctx
.ops
.values()
.filter(|op| ctx.strings.resolve(op.name) == "quantum.h")
.count();
assert_eq!(h_count, 0);
let cx_op = ctx
.ops
.values()
.find(|op| ctx.strings.resolve(op.name) == "quantum.cx")
.unwrap();
assert!(cx_op.inputs.contains(&q0));
}
#[test]
fn test_no_cancel_when_intermediate_same_qubit() {
let mut ctx = Context::new();
let qubit = ctx.make_qubit_type();
let block = ctx.create_block();
let q0 = ctx.create_block_arg(block, qubit);
let (h1, h1_res) = ctx.create_op(
"quantum.h",
"quantum",
vec![q0],
vec![qubit],
Attributes::new(),
Location::unknown(),
);
ctx.add_op_to_block(block, h1);
let (x, x_res) = ctx.create_op(
"quantum.x",
"quantum",
vec![h1_res[0]],
vec![qubit],
Attributes::new(),
Location::unknown(),
);
ctx.add_op_to_block(block, x);
let (h2, _) = ctx.create_op(
"quantum.h",
"quantum",
vec![x_res[0]],
vec![qubit],
Attributes::new(),
Location::unknown(),
);
ctx.add_op_to_block(block, h2);
let result = GateCancellation.run(&mut ctx, &mut AnalysisCache::new());
assert_eq!(result, PassResult::Unchanged);
let h_count = ctx
.ops
.values()
.filter(|op| ctx.strings.resolve(op.name) == "quantum.h")
.count();
assert_eq!(h_count, 2);
}
}