From 13213d529c5a24478b5816e395b52092118034e0 Mon Sep 17 00:00:00 2001 From: Luca Mondada Date: Wed, 27 Sep 2023 15:58:25 +0200 Subject: [PATCH 1/4] feat: Add CircuitMut trait --- src/circuit.rs | 144 +++++++++++++++++++++++++++++++++++++++++++++++-- 1 file changed, 141 insertions(+), 3 deletions(-) diff --git a/src/circuit.rs b/src/circuit.rs index 8f9e9a22..c675c814 100644 --- a/src/circuit.rs +++ b/src/circuit.rs @@ -9,10 +9,17 @@ pub use hash::CircuitHash; use hugr::HugrView; +use derive_more::From; +use hugr::hugr::hugrmut::HugrMut; +use hugr::hugr::{NodeType, PortIndex}; +use hugr::ops::dataflow::IOTrait; pub use hugr::ops::OpType; +use hugr::ops::{Input, Output, DFG}; use hugr::types::FunctionType; pub use hugr::types::{EdgeKind, Signature, Type, TypeRow}; pub use hugr::{Node, Port, Wire}; +use itertools::Itertools; +use thiserror::Error; use self::units::{filter, FilteredUnits, Units}; @@ -120,18 +127,132 @@ pub trait Circuit: HugrView { } } +/// A circuit object that can be mutated. +pub trait CircuitMut: Circuit + HugrMut { + /// Remove an empty wire from the circuit. + /// + /// Pass the index of the wire at the input port. + /// + /// This will return an error if the wire is not empty or if a HugrError + /// occurs. + fn remove_empty_wire(&mut self, input_port: usize) -> Result<(), CircuitMutError> { + let inp = self.input(); + let input_port = Port::new_outgoing(input_port); + let (out, output_port) = self + .linked_ports(inp, input_port) + .exactly_one() + .ok() + .expect("invalid circuit"); + if out != self.output() { + return Err(CircuitMutError::DeleteNonEmptyWire(input_port.index())); + } + self.disconnect(inp, input_port)?; + + // Shift ports at input + shift_ports(self, inp, input_port, self.num_outputs(inp))?; + // Shift ports at output + shift_ports(self, out, output_port, self.num_inputs(out))?; + // Update input node, output node and root signatures. + update_type_signature(self, input_port.index(), output_port.index()); + Ok(()) + } +} + +/// Errors that can occur when mutating a circuit. +#[derive(Debug, Clone, Error, PartialEq, Eq, From)] +pub enum CircuitMutError { + /// A Hugr error occurred. + #[error("Hugr error: {0:?}")] + HugrError(hugr::hugr::HugrError), + /// The wire to be deleted is not empty. + #[error("Wire {0} cannot be deleted: not empty")] + DeleteNonEmptyWire(usize), +} + +/// Shift ports in range (free_port + 1 .. max_ind) by -1. +fn shift_ports( + circ: &mut C, + node: Node, + mut free_port: Port, + max_ind: usize, +) -> Result { + let dir = free_port.direction(); + let port_range = (free_port.index() + 1..max_ind).map(|p| Port::new(dir, p)); + dbg!(&port_range); + for port in port_range { + if let Some(connected_to) = circ + .linked_ports(node, port) + .at_most_one() + .ok() + .expect("invalid circuit") + { + circ.disconnect(node, port)?; + circ.connect(node, free_port, connected_to.0, connected_to.1)?; + } + free_port = port; + } + Ok(free_port) +} + +// Update the signature of circ when removing the in_index-th input wire and +// the out_index-th output wire. +fn update_type_signature( + circ: &mut C, + in_index: usize, + out_index: usize, +) { + let inp = circ.input(); + // Update input node + let inp_types: TypeRow = { + let OpType::Input(Input { types }) = circ.get_optype(inp).clone() else { + panic!("invalid circuit") + }; + let mut types = types.into_owned(); + types.remove(in_index); + types.into() + }; + let new_inp_op = Input::new(inp_types.clone()); + let inp_exts = circ.get_nodetype(inp).input_extensions().cloned(); + circ.replace_op(inp, NodeType::new(new_inp_op, inp_exts)); + + // Update output node + let out = circ.output(); + let out_types: TypeRow = { + let OpType::Output(Output { types }) = circ.get_optype(out).clone() else { + panic!("invalid circuit") + }; + let mut types = types.into_owned(); + types.remove(out_index); + types.into() + }; + let new_out_op = Input::new(out_types.clone()); + let inp_exts = circ.get_nodetype(out).input_extensions().cloned(); + circ.replace_op(out, NodeType::new(new_out_op, inp_exts)); + + // Update root + let OpType::DFG(DFG { mut signature, .. }) = circ.get_optype(circ.root()).clone() else { + panic!("invalid circuit") + }; + signature.input = inp_types; + signature.output = out_types; + let new_dfg_op = DFG { signature }; + let inp_exts = circ.get_nodetype(circ.root()).input_extensions().cloned(); + circ.replace_op(circ.root(), NodeType::new(new_dfg_op, inp_exts)); +} + impl Circuit for T where T: HugrView {} +impl CircuitMut for T where T: Circuit + HugrMut {} #[cfg(test)] mod tests { use hugr::Hugr; - use crate::{circuit::Circuit, json::load_tk1_json_str}; + use super::*; + use crate::{json::load_tk1_json_str, utils::build_simple_circuit, T2Op}; fn test_circuit() -> Hugr { load_tk1_json_str( - r#"{ - "phase": "0", + r#"{ "phase": "0", "bits": [["c", [0]]], "qubits": [["q", [0]], ["q", [1]]], "commands": [ @@ -160,4 +281,21 @@ mod tests { assert_eq!(circ.linear_units().count(), 3); assert_eq!(circ.qubits().count(), 2); } + + #[test] + fn remove_qubit() { + let mut circ = build_simple_circuit(2, |circ| { + circ.append(T2Op::X, [0])?; + Ok(()) + }) + .unwrap(); + + assert_eq!(circ.qubit_count(), 2); + assert!(circ.remove_empty_wire(1).is_ok()); + assert_eq!(circ.qubit_count(), 1); + assert_eq!( + circ.remove_empty_wire(0).unwrap_err(), + CircuitMutError::DeleteNonEmptyWire(0) + ); + } } From 22bf6a695cc9fee7ea2920870fb4b2c3e0f83027 Mon Sep 17 00:00:00 2001 From: Luca Mondada Date: Wed, 27 Sep 2023 16:55:16 +0200 Subject: [PATCH 2/4] Support copyable wires --- src/circuit.rs | 90 ++++++++++++++++++++++++++++++++++++-------------- 1 file changed, 65 insertions(+), 25 deletions(-) diff --git a/src/circuit.rs b/src/circuit.rs index c675c814..e7ceb19f 100644 --- a/src/circuit.rs +++ b/src/circuit.rs @@ -131,29 +131,42 @@ pub trait Circuit: HugrView { pub trait CircuitMut: Circuit + HugrMut { /// Remove an empty wire from the circuit. /// - /// Pass the index of the wire at the input port. + /// The wire to be removed is identified by the index of the outgoing port + /// at the circuit input node. + /// + /// This will change the circuit signature and will shift all ports after + /// the removed wire by -1. If the wire is connected to the output node, + /// this will also change the signature output and shift the ports after + /// the removed wire by -1. /// /// This will return an error if the wire is not empty or if a HugrError /// occurs. fn remove_empty_wire(&mut self, input_port: usize) -> Result<(), CircuitMutError> { let inp = self.input(); + if input_port >= self.num_outputs(inp) { + return Err(CircuitMutError::InvalidPortOffset(input_port)); + } let input_port = Port::new_outgoing(input_port); - let (out, output_port) = self + let link = self .linked_ports(inp, input_port) - .exactly_one() + .at_most_one() .ok() .expect("invalid circuit"); - if out != self.output() { + if link.is_some() && link.unwrap().0 != self.output() { return Err(CircuitMutError::DeleteNonEmptyWire(input_port.index())); } - self.disconnect(inp, input_port)?; + if link.is_some() { + self.disconnect(inp, input_port)?; + } // Shift ports at input shift_ports(self, inp, input_port, self.num_outputs(inp))?; // Shift ports at output - shift_ports(self, out, output_port, self.num_inputs(out))?; - // Update input node, output node and root signatures. - update_type_signature(self, input_port.index(), output_port.index()); + if let Some((out, output_port)) = link { + shift_ports(self, out, output_port, self.num_inputs(out))?; + } + // Update input node, output node (if necessary) and root signatures. + update_signature(self, input_port.index(), link.map(|(_, p)| p.index())); Ok(()) } } @@ -165,8 +178,13 @@ pub enum CircuitMutError { #[error("Hugr error: {0:?}")] HugrError(hugr::hugr::HugrError), /// The wire to be deleted is not empty. + #[from(ignore)] #[error("Wire {0} cannot be deleted: not empty")] DeleteNonEmptyWire(usize), + /// The wire does not exist. + #[from(ignore)] + #[error("Wire {0} does not exist")] + InvalidPortOffset(usize), } /// Shift ports in range (free_port + 1 .. max_ind) by -1. @@ -178,7 +196,6 @@ fn shift_ports( ) -> Result { let dir = free_port.direction(); let port_range = (free_port.index() + 1..max_ind).map(|p| Port::new(dir, p)); - dbg!(&port_range); for port in port_range { if let Some(connected_to) = circ .linked_ports(node, port) @@ -196,10 +213,10 @@ fn shift_ports( // Update the signature of circ when removing the in_index-th input wire and // the out_index-th output wire. -fn update_type_signature( +fn update_signature( circ: &mut C, in_index: usize, - out_index: usize, + out_index: Option, ) { let inp = circ.input(); // Update input node @@ -215,26 +232,31 @@ fn update_type_signature( let inp_exts = circ.get_nodetype(inp).input_extensions().cloned(); circ.replace_op(inp, NodeType::new(new_inp_op, inp_exts)); - // Update output node - let out = circ.output(); - let out_types: TypeRow = { - let OpType::Output(Output { types }) = circ.get_optype(out).clone() else { - panic!("invalid circuit") + // Update output node if necessary. + let out_types = out_index.map(|out_index| { + let out = circ.output(); + let out_types: TypeRow = { + let OpType::Output(Output { types }) = circ.get_optype(out).clone() else { + panic!("invalid circuit") + }; + let mut types = types.into_owned(); + types.remove(out_index); + types.into() }; - let mut types = types.into_owned(); - types.remove(out_index); - types.into() - }; - let new_out_op = Input::new(out_types.clone()); - let inp_exts = circ.get_nodetype(out).input_extensions().cloned(); - circ.replace_op(out, NodeType::new(new_out_op, inp_exts)); + let new_out_op = Input::new(out_types.clone()); + let inp_exts = circ.get_nodetype(out).input_extensions().cloned(); + circ.replace_op(out, NodeType::new(new_out_op, inp_exts)); + out_types + }); // Update root let OpType::DFG(DFG { mut signature, .. }) = circ.get_optype(circ.root()).clone() else { panic!("invalid circuit") }; signature.input = inp_types; - signature.output = out_types; + if let Some(out_types) = out_types { + signature.output = out_types; + } let new_dfg_op = DFG { signature }; let inp_exts = circ.get_nodetype(circ.root()).input_extensions().cloned(); circ.replace_op(circ.root(), NodeType::new(new_dfg_op, inp_exts)); @@ -245,7 +267,11 @@ impl CircuitMut for T where T: Circuit + HugrMut {} #[cfg(test)] mod tests { - use hugr::Hugr; + use hugr::{ + builder::{DFGBuilder, DataflowHugr}, + extension::{prelude::BOOL_T, PRELUDE_REGISTRY}, + Hugr, + }; use super::*; use crate::{json::load_tk1_json_str, utils::build_simple_circuit, T2Op}; @@ -298,4 +324,18 @@ mod tests { CircuitMutError::DeleteNonEmptyWire(0) ); } + + #[test] + fn remove_bit() { + let h = DFGBuilder::new(FunctionType::new(vec![BOOL_T], vec![])).unwrap(); + let mut circ = h.finish_hugr_with_outputs([], &PRELUDE_REGISTRY).unwrap(); + + assert_eq!(circ.units().count(), 1); + assert!(circ.remove_empty_wire(0).is_ok()); + assert_eq!(circ.units().count(), 0); + assert_eq!( + circ.remove_empty_wire(2).unwrap_err(), + CircuitMutError::InvalidPortOffset(2) + ); + } } From 2abeaf745c2d371129a8b28352bc0bd9d497d10a Mon Sep 17 00:00:00 2001 From: Luca Mondada Date: Wed, 27 Sep 2023 17:58:56 +0200 Subject: [PATCH 3/4] Bug fixes --- src/circuit.rs | 27 +++++++++++++++++---------- 1 file changed, 17 insertions(+), 10 deletions(-) diff --git a/src/circuit.rs b/src/circuit.rs index e7ceb19f..89769741 100644 --- a/src/circuit.rs +++ b/src/circuit.rs @@ -19,6 +19,7 @@ use hugr::types::FunctionType; pub use hugr::types::{EdgeKind, Signature, Type, TypeRow}; pub use hugr::{Node, Port, Wire}; use itertools::Itertools; +use portgraph::Direction; use thiserror::Error; use self::units::{filter, FilteredUnits, Units}; @@ -150,8 +151,7 @@ pub trait CircuitMut: Circuit + HugrMut { let link = self .linked_ports(inp, input_port) .at_most_one() - .ok() - .expect("invalid circuit"); + .map_err(|_| CircuitMutError::DeleteNonEmptyWire(input_port.index()))?; if link.is_some() && link.unwrap().0 != self.output() { return Err(CircuitMutError::DeleteNonEmptyWire(input_port.index())); } @@ -167,6 +167,11 @@ pub trait CircuitMut: Circuit + HugrMut { } // Update input node, output node (if necessary) and root signatures. update_signature(self, input_port.index(), link.map(|(_, p)| p.index())); + // Resize ports at input/output node + self.set_num_ports(inp, 0, self.num_outputs(inp) - 1); + if let Some((out, _)) = link { + self.set_num_ports(out, self.num_inputs(out) - 1, 0); + } Ok(()) } } @@ -197,14 +202,16 @@ fn shift_ports( let dir = free_port.direction(); let port_range = (free_port.index() + 1..max_ind).map(|p| Port::new(dir, p)); for port in port_range { - if let Some(connected_to) = circ - .linked_ports(node, port) - .at_most_one() - .ok() - .expect("invalid circuit") - { + let links = circ.linked_ports(node, port).collect_vec(); + if !links.is_empty() { circ.disconnect(node, port)?; - circ.connect(node, free_port, connected_to.0, connected_to.1)?; + } + for (other_n, other_p) in links { + // TODO: simplify when CQCL-DEV/hugr#565 is resolved + match dir { + Direction::Incoming => circ.connect(other_n, other_p, node, free_port), + Direction::Outgoing => circ.connect(node, free_port, other_n, other_p), + }?; } free_port = port; } @@ -243,7 +250,7 @@ fn update_signature( types.remove(out_index); types.into() }; - let new_out_op = Input::new(out_types.clone()); + let new_out_op = Output::new(out_types.clone()); let inp_exts = circ.get_nodetype(out).input_extensions().cloned(); circ.replace_op(out, NodeType::new(new_out_op, inp_exts)); out_types From adfce6db80844164166d0a6c8e6b70d69f944015 Mon Sep 17 00:00:00 2001 From: Luca Mondada Date: Fri, 29 Sep 2023 12:19:43 +0200 Subject: [PATCH 4/4] Replace trait with generic fn --- src/circuit.rs | 98 +++++++++++++++++++++++++------------------------- 1 file changed, 49 insertions(+), 49 deletions(-) diff --git a/src/circuit.rs b/src/circuit.rs index 89769741..5bf0d1ff 100644 --- a/src/circuit.rs +++ b/src/circuit.rs @@ -128,52 +128,53 @@ pub trait Circuit: HugrView { } } -/// A circuit object that can be mutated. -pub trait CircuitMut: Circuit + HugrMut { - /// Remove an empty wire from the circuit. - /// - /// The wire to be removed is identified by the index of the outgoing port - /// at the circuit input node. - /// - /// This will change the circuit signature and will shift all ports after - /// the removed wire by -1. If the wire is connected to the output node, - /// this will also change the signature output and shift the ports after - /// the removed wire by -1. - /// - /// This will return an error if the wire is not empty or if a HugrError - /// occurs. - fn remove_empty_wire(&mut self, input_port: usize) -> Result<(), CircuitMutError> { - let inp = self.input(); - if input_port >= self.num_outputs(inp) { - return Err(CircuitMutError::InvalidPortOffset(input_port)); - } - let input_port = Port::new_outgoing(input_port); - let link = self - .linked_ports(inp, input_port) - .at_most_one() - .map_err(|_| CircuitMutError::DeleteNonEmptyWire(input_port.index()))?; - if link.is_some() && link.unwrap().0 != self.output() { - return Err(CircuitMutError::DeleteNonEmptyWire(input_port.index())); - } - if link.is_some() { - self.disconnect(inp, input_port)?; - } +/// Remove an empty wire in a dataflow HUGR. +/// +/// The wire to be removed is identified by the index of the outgoing port +/// at the circuit input node. +/// +/// This will change the circuit signature and will shift all ports after +/// the removed wire by -1. If the wire is connected to the output node, +/// this will also change the signature output and shift the ports after +/// the removed wire by -1. +/// +/// This will return an error if the wire is not empty or if a HugrError +/// occurs. +#[allow(dead_code)] +pub(crate) fn remove_empty_wire( + circ: &mut impl HugrMut, + input_port: usize, +) -> Result<(), CircuitMutError> { + let [inp, out] = circ.get_io(circ.root()).expect("no IO nodes found at root"); + if input_port >= circ.num_outputs(inp) { + return Err(CircuitMutError::InvalidPortOffset(input_port)); + } + let input_port = Port::new_outgoing(input_port); + let link = circ + .linked_ports(inp, input_port) + .at_most_one() + .map_err(|_| CircuitMutError::DeleteNonEmptyWire(input_port.index()))?; + if link.is_some() && link.unwrap().0 != out { + return Err(CircuitMutError::DeleteNonEmptyWire(input_port.index())); + } + if link.is_some() { + circ.disconnect(inp, input_port)?; + } - // Shift ports at input - shift_ports(self, inp, input_port, self.num_outputs(inp))?; - // Shift ports at output - if let Some((out, output_port)) = link { - shift_ports(self, out, output_port, self.num_inputs(out))?; - } - // Update input node, output node (if necessary) and root signatures. - update_signature(self, input_port.index(), link.map(|(_, p)| p.index())); - // Resize ports at input/output node - self.set_num_ports(inp, 0, self.num_outputs(inp) - 1); - if let Some((out, _)) = link { - self.set_num_ports(out, self.num_inputs(out) - 1, 0); - } - Ok(()) + // Shift ports at input + shift_ports(circ, inp, input_port, circ.num_outputs(inp))?; + // Shift ports at output + if let Some((out, output_port)) = link { + shift_ports(circ, out, output_port, circ.num_inputs(out))?; + } + // Update input node, output node (if necessary) and root signatures. + update_signature(circ, input_port.index(), link.map(|(_, p)| p.index())); + // Resize ports at input/output node + circ.set_num_ports(inp, 0, circ.num_outputs(inp) - 1); + if let Some((out, _)) = link { + circ.set_num_ports(out, circ.num_inputs(out) - 1, 0); } + Ok(()) } /// Errors that can occur when mutating a circuit. @@ -270,7 +271,6 @@ fn update_signature( } impl Circuit for T where T: HugrView {} -impl CircuitMut for T where T: Circuit + HugrMut {} #[cfg(test)] mod tests { @@ -324,10 +324,10 @@ mod tests { .unwrap(); assert_eq!(circ.qubit_count(), 2); - assert!(circ.remove_empty_wire(1).is_ok()); + assert!(remove_empty_wire(&mut circ, 1).is_ok()); assert_eq!(circ.qubit_count(), 1); assert_eq!( - circ.remove_empty_wire(0).unwrap_err(), + remove_empty_wire(&mut circ, 0).unwrap_err(), CircuitMutError::DeleteNonEmptyWire(0) ); } @@ -338,10 +338,10 @@ mod tests { let mut circ = h.finish_hugr_with_outputs([], &PRELUDE_REGISTRY).unwrap(); assert_eq!(circ.units().count(), 1); - assert!(circ.remove_empty_wire(0).is_ok()); + assert!(remove_empty_wire(&mut circ, 0).is_ok()); assert_eq!(circ.units().count(), 0); assert_eq!( - circ.remove_empty_wire(2).unwrap_err(), + remove_empty_wire(&mut circ, 2).unwrap_err(), CircuitMutError::InvalidPortOffset(2) ); }