From a8cddcd9f30bcb1d9a7d676d221093cf674c8a37 Mon Sep 17 00:00:00 2001 From: Raynel Sanchez <87539502+raynelfss@users.noreply.github.com> Date: Wed, 5 Jun 2024 19:59:27 -0400 Subject: [PATCH 01/45] Initial: Add equivalence to `qiskit._accelerate.circuit` --- Cargo.lock | 1 + crates/circuit/Cargo.toml | 1 + crates/circuit/src/equivalence.rs | 474 ++++++++++++++++++++++++++++++ crates/circuit/src/lib.rs | 1 + 4 files changed, 477 insertions(+) create mode 100644 crates/circuit/src/equivalence.rs diff --git a/Cargo.lock b/Cargo.lock index cf8e4f365df9..b6ec645603ee 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -1197,6 +1197,7 @@ version = "1.2.0" dependencies = [ "hashbrown 0.14.5", "pyo3", + "rustworkx-core", ] [[package]] diff --git a/crates/circuit/Cargo.toml b/crates/circuit/Cargo.toml index 6ec38392cc38..6ebf9694afc1 100644 --- a/crates/circuit/Cargo.toml +++ b/crates/circuit/Cargo.toml @@ -12,3 +12,4 @@ doctest = false [dependencies] hashbrown.workspace = true pyo3.workspace = true +rustworkx-core = "0.14.2" diff --git a/crates/circuit/src/equivalence.rs b/crates/circuit/src/equivalence.rs new file mode 100644 index 000000000000..124934f889d6 --- /dev/null +++ b/crates/circuit/src/equivalence.rs @@ -0,0 +1,474 @@ +// This code is part of Qiskit. +// +// (C) Copyright IBM 2024 +// +// This code is licensed under the Apache License, Version 2.0. You may +// obtain a copy of this license in the LICENSE.txt file in the root directory +// of this source tree or at http://www.apache.org/licenses/LICENSE-2.0. +// +// Any modifications or derivative works of this code must retain this +// copyright notice, and modified files need to carry a notice indicating +// that they have been altered from the originals. + +use std::{error::Error, fmt::Display}; + +use exceptions::CircuitError; +use hashbrown::{HashMap, HashSet}; +use pyo3::prelude::*; +use rustworkx_core::petgraph::{ + graph::{DiGraph, EdgeIndex, NodeIndex}, + visit::EdgeRef, +}; + + + +mod exceptions { + use pyo3::import_exception_bound; + import_exception_bound! {qiskit.exceptions, CircuitError} +} +// Custom Structs + +#[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord, Hash)] +pub struct Key { + pub name: String, + pub num_qubits: usize, +} + +#[derive(Debug, Clone)] +pub struct Equivalence { + pub params: Vec, + pub circuit: CircuitRep, +} + +// Temporary interpretation of Param +#[derive(Debug, Clone, FromPyObject)] +pub enum Param { + Float(f64), + ParameterExpression(PyObject), +} + +impl Param { + fn compare(one: &PyObject, other: &PyObject) -> bool { + Python::with_gil(|py| -> PyResult { + let other_bound = other.bind(py); + other_bound.eq(one) + }) + .unwrap() + } +} + +impl PartialEq for Param { + fn eq(&self, other: &Self) -> bool { + match (self, other) { + (Param::Float(s), Param::Float(other)) => s == other, + (Param::Float(_), Param::ParameterExpression(_)) => false, + (Param::ParameterExpression(_), Param::Float(_)) => false, + (Param::ParameterExpression(s), Param::ParameterExpression(other)) => { + Self::compare(s, other) + } + } + } +} + +#[derive(Debug, Clone)] +pub struct NodeData { + key: Key, + equivs: Vec, +} + +#[derive(Debug, Clone)] +pub struct EdgeData { + pub index: u32, + pub num_gates: usize, + pub rule: Equivalence, + pub source: Key, +} + +/// Temporary interpretation of Gate +#[derive(Debug, Clone)] +pub struct GateRep { + object: PyObject, + pub num_qubits: Option, + pub num_clbits: Option, + pub name: Option, + pub label: Option, + pub params: Vec, +} + +impl FromPyObject<'_> for GateRep { + fn extract(ob: &'_ PyAny) -> PyResult { + let num_qubits = match ob.getattr("num_qubits") { + Ok(num_qubits) => num_qubits.extract::().ok(), + Err(_) => None, + }; + let num_clbits = match ob.getattr("num_clbits") { + Ok(num_clbits) => num_clbits.extract::().ok(), + Err(_) => None, + }; + let name = match ob.getattr("name") { + Ok(name) => name.extract::().ok(), + Err(_) => None, + }; + let label = match ob.getattr("label") { + Ok(label) => label.extract::().ok(), + Err(_) => None, + }; + let params = match ob.getattr("params") { + Ok(params) => params.extract::>().ok(), + Err(_) => Some(vec![]), + } + .unwrap_or_default(); + Ok(Self { + object: ob.into(), + num_qubits, + num_clbits, + name, + label, + params, + }) + } +} + +/// Temporary interpretation of Gate +#[derive(Debug, Clone)] +pub struct CircuitRep { + object: PyObject, + pub num_qubits: Option, + pub num_clbits: Option, + pub label: Option, + pub params: Vec, + pub data: Vec, +} + +impl FromPyObject<'_> for CircuitRep { + fn extract(ob: &'_ PyAny) -> PyResult { + let num_qubits = match ob.getattr("num_qubits") { + Ok(num_qubits) => num_qubits.extract::().ok(), + Err(_) => None, + }; + let num_clbits = match ob.getattr("num_clbits") { + Ok(num_clbits) => num_clbits.extract::().ok(), + Err(_) => None, + }; + let label = match ob.getattr("label") { + Ok(label) => label.extract::().ok(), + Err(_) => None, + }; + let params = match ob.getattr("params") { + Ok(params) => params.extract::>().ok(), + Err(_) => Some(vec![]), + } + .unwrap_or_default(); + let data = match ob.getattr("data") { + Ok(data) => data.extract::>().ok(), + Err(_) => Some(vec![]), + } + .unwrap_or_default(); + Ok(Self { + object: ob.into(), + num_qubits, + num_clbits, + label, + params, + data, + }) + } +} + +// Custom Types +type GraphType = DiGraph; +type KTIType = HashMap; + +#[pyclass] +#[derive(Debug, Clone)] +pub struct EquivalenceLibrary { + graph: GraphType, + key_to_node_index: KTIType, + rule_id: usize, +} + +#[pymethods] +impl EquivalenceLibrary { + /// Create a new equivalence library. + /// + /// Args: + /// base (Optional[EquivalenceLibrary]): Base equivalence library to + /// be referenced if an entry is not found in this library. + #[new] + fn new(base: Option<&EquivalenceLibrary>) -> Self { + if let Some(base) = base { + Self { + graph: base.graph.clone(), + key_to_node_index: base.key_to_node_index.clone(), + rule_id: base.rule_id, + } + } else { + Self { + graph: GraphType::new(), + key_to_node_index: KTIType::new(), + rule_id: 0_usize, + } + } + } + + // TODO: Add a way of returning a graph + + /// Add a new equivalence to the library. Future queries for the Gate + /// will include the given circuit, in addition to all existing equivalences + /// (including those from base). + /// + /// Parameterized Gates (those including `qiskit.circuit.Parameters` in their + /// `Gate.params`) can be marked equivalent to parameterized circuits, + /// provided the parameters match. + /// + /// Args: + /// gate (Gate): A Gate instance. + /// equivalent_circuit (QuantumCircuit): A circuit equivalently + /// implementing the given Gate. + fn add_equivalence(&mut self, gate: GateRep, equivalent_circuit: CircuitRep) -> PyResult<()> { + match self.add_equiv(gate, equivalent_circuit) { + Ok(_) => Ok(()), + Err(e) => Err(CircuitError::new_err(e.message)), + } + } + + /// Check if a library contains any decompositions for gate. + + /// Args: + /// gate (Gate): A Gate instance. + + /// Returns: + /// Bool: True if gate has a known decomposition in the library. + /// False otherwise. + pub fn has_entry(&self, gate: GateRep) -> bool { + let key = Key { + name: gate.name.unwrap(), + num_qubits: gate.num_qubits.unwrap_or_default(), + }; + self.key_to_node_index.contains_key(&key) + } + + /// Set the equivalence record for a Gate. Future queries for the Gate + /// will return only the circuits provided. + /// + /// Parameterized Gates (those including `qiskit.circuit.Parameters` in their + /// `Gate.params`) can be marked equivalent to parameterized circuits, + /// provided the parameters match. + /// + /// Args: + /// gate (Gate): A Gate instance. + /// entry (List['QuantumCircuit']) : A list of QuantumCircuits, each + /// equivalently implementing the given Gate. + fn set_entry(&mut self, gate: GateRep, entry: Vec) -> PyResult<()> { + match self.set_entry_native(&gate, &entry) { + Ok(_) => Ok(()), + Err(e) => Err(CircuitError::new_err(e.message)), + } + } + + /// Gets the set of QuantumCircuits circuits from the library which + /// equivalently implement the given Gate. + /// + /// Parameterized circuits will have their parameters replaced with the + /// corresponding entries from Gate.params. + /// + /// Args: + /// gate (Gate) - Gate: A Gate instance. + /// + /// Returns: + /// List[QuantumCircuit]: A list of equivalent QuantumCircuits. If empty, + /// library contains no known decompositions of Gate. + /// + /// Returned circuits will be ordered according to their insertion in + /// the library, from earliest to latest, from top to base. The + /// ordering of the StandardEquivalenceLibrary will not generally be + /// consistent across Qiskit versions. + pub fn get_entry(&self, _gate: GateRep) { + todo!() + } +} + +// Rust native methods +impl EquivalenceLibrary { + /// Create a new node if key not found + fn set_default_node(&mut self, key: Key) -> NodeIndex { + *self + .key_to_node_index + .entry(key.to_owned()) + .or_insert(self.graph.add_node(NodeData { + key, + equivs: vec![], + })) + } + + /// Rust native equivalent to `EquivalenceLibrary.add_equivalence()` + /// + /// Add a new equivalence to the library. Future queries for the Gate + /// will include the given circuit, in addition to all existing equivalences + /// (including those from base). + /// + /// Parameterized Gates (those including `qiskit.circuit.Parameters` in their + /// `Gate.params`) can be marked equivalent to parameterized circuits, + /// provided the parameters match. + /// + /// Args: + /// gate (Gate): A Gate instance. + /// equivalent_circuit (QuantumCircuit): A circuit equivalently + /// implementing the given Gate. + pub fn add_equiv( + &mut self, + gate: GateRep, + equivalent_circuit: CircuitRep, + ) -> Result<(), EquivalenceError> { + raise_if_shape_mismatch(&gate, &equivalent_circuit)?; + raise_if_param_mismatch(&gate.params, &equivalent_circuit.params)?; + + let key: Key = Key { + name: gate.name.unwrap(), + num_qubits: gate.num_qubits.unwrap(), + }; + let equiv = Equivalence { + params: gate.params, + circuit: equivalent_circuit.to_owned(), + }; + + let target = self.set_default_node(key); + let node = self.graph.node_weight_mut(target).unwrap(); + node.equivs.push(equiv.to_owned()); + + let sources: HashSet = + HashSet::from_iter(equivalent_circuit.data.iter().map(|inst| Key { + name: inst.name.to_owned().unwrap(), + num_qubits: inst.num_qubits.unwrap_or_default(), + })); + let edges = Vec::from_iter(sources.iter().map(|key| { + ( + self.set_default_node(key.to_owned()), + target, + EdgeData { + index: self.rule_id as u32, + num_gates: sources.len(), + rule: equiv.to_owned(), + source: key.to_owned(), + }, + ) + })); + for edge in edges { + self.graph.add_edge(edge.0, edge.1, edge.2); + } + self.rule_id += 1; + Ok(()) + } + + /// Rust native equivalent to `EquivalenceLibrary.set_entry()` + /// Set the equivalence record for a Gate. Future queries for the Gate + /// will return only the circuits provided. + /// + /// Parameterized Gates (those including `qiskit.circuit.Parameters` in their + /// `Gate.params`) can be marked equivalent to parameterized circuits, + /// provided the parameters match. + /// + /// Args: + /// gate (Gate): A Gate instance. + /// entry (List['QuantumCircuit']) : A list of QuantumCircuits, each + /// equivalently implementing the given Gate. + pub fn set_entry_native( + &mut self, + gate: &GateRep, + entry: &Vec, + ) -> Result<(), EquivalenceError> { + for equiv in entry { + raise_if_shape_mismatch(gate, equiv)?; + raise_if_param_mismatch(&gate.params, &equiv.params)?; + } + + let key = Key { + name: gate.name.to_owned().unwrap(), + num_qubits: gate.num_qubits.unwrap_or_default(), + }; + let node_index = self.set_default_node(key); + + let graph_ind = &mut self.graph.node_weight_mut(node_index).unwrap(); + graph_ind.equivs.clear(); + + let edges: Vec = self + .graph + .edges_directed(node_index, rustworkx_core::petgraph::Direction::Incoming) + .map(|x| x.id()) + .collect(); + for edge in edges { + self.graph.remove_edge(edge); + } + for equiv in entry { + self.add_equiv(gate.to_owned(), equiv.to_owned())? + } + Ok(()) + } + + /// Get all the equivalences for the given key + fn get_equivalences(&self, key: &Key) -> Vec { + if let Some(key_in) = self.key_to_node_index.get(key) { + self.graph[*key_in].equivs.to_owned() + } else { + vec![] + } + } +} + +fn raise_if_param_mismatch( + gate_params: &[Param], + circuit_parameters: &[Param], +) -> Result<(), EquivalenceError> { + let gate_parameters: Vec<&Param> = gate_params + .iter() + .filter(|x| matches!(x, Param::ParameterExpression(_))) + .collect(); + if circuit_parameters + .iter() + .any(|x| gate_parameters.contains(&x)) + { + return Err(EquivalenceError::new_err(format!( + "Cannot add equivalence between circuit and gate \ + of different parameters. Gate params: {:?}. \ + Circuit params: {:?}.", + gate_parameters, circuit_parameters + ))); + } + Ok(()) +} + +fn raise_if_shape_mismatch(gate: &GateRep, circuit: &CircuitRep) -> Result<(), EquivalenceError> { + if gate.num_qubits != circuit.num_qubits || gate.num_clbits != circuit.num_clbits { + return Err(EquivalenceError::new_err(format!( + "Cannot add equivalence between circuit and gate \ + of different shapes. Gate: {} qubits and {} clbits. \ + Circuit: {} qubits and {} clbits.", + gate.num_qubits.unwrap_or_default(), + gate.num_clbits.unwrap_or_default(), + circuit.num_qubits.unwrap_or_default(), + circuit.num_clbits.unwrap_or_default() + ))); + } + Ok(()) +} + +// Errors + +#[derive(Debug, Clone)] +pub struct EquivalenceError { + pub message: String, +} + +impl EquivalenceError { + pub fn new_err(message: String) -> Self { + Self { message } + } +} + +impl Error for EquivalenceError {} + +impl Display for EquivalenceError { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + write!(f, "{}", self.message) + } +} diff --git a/crates/circuit/src/lib.rs b/crates/circuit/src/lib.rs index c186c4243e93..fc8b2c686251 100644 --- a/crates/circuit/src/lib.rs +++ b/crates/circuit/src/lib.rs @@ -13,6 +13,7 @@ pub mod circuit_data; pub mod circuit_instruction; pub mod dag_node; +pub mod equivalence; pub mod intern_context; use pyo3::prelude::*; From f53b89c8a7e9d7e66103123013960348295d4f0f Mon Sep 17 00:00:00 2001 From: Raynel Sanchez <87539502+raynelfss@users.noreply.github.com> Date: Mon, 10 Jun 2024 15:52:32 +0200 Subject: [PATCH 02/45] Add: `build_basis_graph` method --- crates/circuit/src/equivalence.rs | 111 +++++++++++++++++++++++++----- 1 file changed, 95 insertions(+), 16 deletions(-) diff --git a/crates/circuit/src/equivalence.rs b/crates/circuit/src/equivalence.rs index 124934f889d6..c0295e122784 100644 --- a/crates/circuit/src/equivalence.rs +++ b/crates/circuit/src/equivalence.rs @@ -10,18 +10,19 @@ // copyright notice, and modified files need to carry a notice indicating // that they have been altered from the originals. -use std::{error::Error, fmt::Display}; +use std::{ + error::Error, + fmt::{Display}, +}; use exceptions::CircuitError; use hashbrown::{HashMap, HashSet}; -use pyo3::prelude::*; +use pyo3::{prelude::*}; use rustworkx_core::petgraph::{ graph::{DiGraph, EdgeIndex, NodeIndex}, visit::EdgeRef, }; - - mod exceptions { use pyo3::import_exception_bound; import_exception_bound! {qiskit.exceptions, CircuitError} @@ -34,6 +35,16 @@ pub struct Key { pub num_qubits: usize, } +impl Display for Key { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + write!( + f, + "Key(name=\"{}\" num_qubits=\"{}\"", + self.name, self.num_qubits + ) + } +} + #[derive(Debug, Clone)] pub struct Equivalence { pub params: Vec, @@ -117,7 +128,7 @@ impl FromPyObject<'_> for GateRep { Ok(params) => params.extract::>().ok(), Err(_) => Some(vec![]), } - .unwrap_or_default(); + .unwrap(); Ok(Self { object: ob.into(), num_qubits, @@ -137,7 +148,7 @@ pub struct CircuitRep { pub num_clbits: Option, pub label: Option, pub params: Vec, - pub data: Vec, + pub data: Vec, } impl FromPyObject<'_> for CircuitRep { @@ -158,12 +169,12 @@ impl FromPyObject<'_> for CircuitRep { Ok(params) => params.extract::>().ok(), Err(_) => Some(vec![]), } - .unwrap_or_default(); + .unwrap(); let data = match ob.getattr("data") { - Ok(data) => data.extract::>().ok(), + Ok(data) => data.extract::>().ok(), Err(_) => Some(vec![]), } - .unwrap_or_default(); + .unwrap(); Ok(Self { object: ob.into(), num_qubits, @@ -175,6 +186,18 @@ impl FromPyObject<'_> for CircuitRep { } } +#[derive(Debug, Clone)] +pub struct CircuitInstructionRep { + operation: GateRep, +} + +impl FromPyObject<'_> for CircuitInstructionRep { + fn extract(ob: &'_ PyAny) -> PyResult { + let operation = ob.getattr("operation")?.extract::()?; + Ok(Self { operation }) + } +} + // Custom Types type GraphType = DiGraph; type KTIType = HashMap; @@ -268,17 +291,17 @@ impl EquivalenceLibrary { /// Gets the set of QuantumCircuits circuits from the library which /// equivalently implement the given Gate. - /// + /// /// Parameterized circuits will have their parameters replaced with the /// corresponding entries from Gate.params. - /// + /// /// Args: /// gate (Gate) - Gate: A Gate instance. - /// + /// /// Returns: /// List[QuantumCircuit]: A list of equivalent QuantumCircuits. If empty, /// library contains no known decompositions of Gate. - /// + /// /// Returned circuits will be ordered according to their insertion in /// the library, from earliest to latest, from top to base. The /// ordering of the StandardEquivalenceLibrary will not generally be @@ -338,8 +361,8 @@ impl EquivalenceLibrary { let sources: HashSet = HashSet::from_iter(equivalent_circuit.data.iter().map(|inst| Key { - name: inst.name.to_owned().unwrap(), - num_qubits: inst.num_qubits.unwrap_or_default(), + name: inst.operation.name.to_owned().unwrap_or_default(), + num_qubits: inst.operation.num_qubits.unwrap_or_default(), })); let edges = Vec::from_iter(sources.iter().map(|key| { ( @@ -383,7 +406,7 @@ impl EquivalenceLibrary { } let key = Key { - name: gate.name.to_owned().unwrap(), + name: gate.name.to_owned().unwrap_or_default(), num_qubits: gate.num_qubits.unwrap_or_default(), }; let node_index = self.set_default_node(key); @@ -413,6 +436,62 @@ impl EquivalenceLibrary { vec![] } } + + fn build_basis_graph(&self) -> DiGraph> { + let mut graph: DiGraph> = DiGraph::new(); + + let mut node_map: HashMap = HashMap::new(); + + for key in self.key_to_node_index.keys() { + let (name, num_qubits) = (key.name.to_owned(), key.num_qubits); + let equivalences = self.get_equivalences(key); + let basis: String = format!("{{{name}/{num_qubits}}}"); + for equivalence in equivalences { + let decomp_basis: HashSet = HashSet::from_iter( + equivalence + .circuit + .data + .iter() + .map(|x| (x.operation.name.to_owned(), x.operation.num_qubits)) + .map(|(name, num_qubits)| { + format!( + "{}/{}", + name.unwrap_or_default(), + num_qubits.unwrap_or_default() + ) + }), + ); + let decomp_string = format!( + "{{{}}}", + decomp_basis + .iter() + .cloned() + .collect::>() + .join(", ") + ); + if !node_map.contains_key(&basis) { + let basis_node = + graph.add_node(format!("basis: {:?}, label: {}", basis, decomp_string)); + node_map.insert(basis.to_owned(), basis_node); + } + if node_map.contains_key(&decomp_string) { + let decomp_basis_node = graph.add_node(format!( + "basis: {:?}, label: {}", + decomp_string, decomp_string + )); + node_map.insert(decomp_string.to_owned(), decomp_basis_node); + } + let label = format!("{:?}\n{:?}", equivalence.params, equivalence.circuit); + let map: HashMap<&str, String> = HashMap::from_iter([ + ("label", label), + ("fontname", "Courier".to_owned()), + ("fontsize", 8.to_string()), + ]); + graph.add_edge(node_map[&basis], node_map[&decomp_string], map); + } + } + graph + } } fn raise_if_param_mismatch( From a2156943dfd5ef2f9d676761d105e5f8b35c6b55 Mon Sep 17 00:00:00 2001 From: Raynel Sanchez <87539502+raynelfss@users.noreply.github.com> Date: Tue, 11 Jun 2024 16:09:51 +0200 Subject: [PATCH 03/45] Add: `EquivalencyLibrary` to `qiskit._accelerate.circuit` - Add `get_entry` method to obtain an entry from binding to a `QuantumCircuit`. - Add `rebind_equiv` to bind parameters to `QuantumCircuit` --- crates/circuit/src/equivalence.rs | 150 +++++++++++++++++++++++++++--- crates/circuit/src/lib.rs | 1 + 2 files changed, 140 insertions(+), 11 deletions(-) diff --git a/crates/circuit/src/equivalence.rs b/crates/circuit/src/equivalence.rs index c0295e122784..a49a0065b920 100644 --- a/crates/circuit/src/equivalence.rs +++ b/crates/circuit/src/equivalence.rs @@ -10,14 +10,12 @@ // copyright notice, and modified files need to carry a notice indicating // that they have been altered from the originals. -use std::{ - error::Error, - fmt::{Display}, -}; +use std::{error::Error, fmt::Display}; use exceptions::CircuitError; use hashbrown::{HashMap, HashSet}; -use pyo3::{prelude::*}; +use pyo3::sync::GILOnceCell; +use pyo3::{prelude::*, types::IntoPyDict}; use rustworkx_core::petgraph::{ graph::{DiGraph, EdgeIndex, NodeIndex}, visit::EdgeRef, @@ -25,10 +23,52 @@ use rustworkx_core::petgraph::{ mod exceptions { use pyo3::import_exception_bound; - import_exception_bound! {qiskit.exceptions, CircuitError} + import_exception_bound! {qiskit.circuit.exceptions, CircuitError} +} + +/// Helper wrapper around `GILOnceCell` instances that are just intended to store a Python object +/// that is lazily imported. +pub struct ImportOnceCell { + module: &'static str, + object: &'static str, + cell: GILOnceCell>, } + +impl ImportOnceCell { + const fn new(module: &'static str, object: &'static str) -> Self { + Self { + module, + object, + cell: GILOnceCell::new(), + } + } + + /// Get the underlying GIL-independent reference to the contained object, importing if + /// required. + #[inline] + pub fn get(&self, py: Python) -> &Py { + self.cell.get_or_init(py, || { + py.import_bound(self.module) + .unwrap() + .getattr(self.object) + .unwrap() + .unbind() + }) + } + + /// Get a GIL-bound reference to the contained object, importing if required. + #[inline] + pub fn get_bound<'py>(&self, py: Python<'py>) -> &Bound<'py, PyAny> { + self.get(py).bind(py) + } +} + // Custom Structs +pub static PARAMETER_EXPRESSION: ImportOnceCell = + ImportOnceCell::new("qiskit.circuit.parameterexpression", "ParameterExpression"); +pub static QUANTUM_CIRCUIT: ImportOnceCell = + ImportOnceCell::new("qiskit.circuit.quantumcircuit", "QuantumCircuit"); #[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord, Hash)] pub struct Key { pub name: String, @@ -51,11 +91,47 @@ pub struct Equivalence { pub circuit: CircuitRep, } -// Temporary interpretation of Param -#[derive(Debug, Clone, FromPyObject)] +#[derive(Clone, Debug)] pub enum Param { - Float(f64), ParameterExpression(PyObject), + Float(f64), + Obj(PyObject), +} + +impl<'py> FromPyObject<'py> for Param { + fn extract_bound(b: &Bound<'py, PyAny>) -> Result { + Ok( + if b.is_instance(PARAMETER_EXPRESSION.get_bound(b.py()))? + || b.is_instance(QUANTUM_CIRCUIT.get_bound(b.py()))? + { + Param::ParameterExpression(b.clone().unbind()) + } else if let Ok(val) = b.extract::() { + Param::Float(val) + } else { + Param::Obj(b.clone().unbind()) + }, + ) + } +} + +impl IntoPy for Param { + fn into_py(self, py: Python) -> PyObject { + match &self { + Self::Float(val) => val.to_object(py), + Self::ParameterExpression(val) => val.clone_ref(py), + Self::Obj(val) => val.clone_ref(py), + } + } +} + +impl ToPyObject for Param { + fn to_object(&self, py: Python) -> PyObject { + match self { + Self::Float(val) => val.to_object(py), + Self::ParameterExpression(val) => val.clone_ref(py), + Self::Obj(val) => val.clone_ref(py), + } + } } impl Param { @@ -77,6 +153,11 @@ impl PartialEq for Param { (Param::ParameterExpression(s), Param::ParameterExpression(other)) => { Self::compare(s, other) } + (Param::ParameterExpression(_), Param::Obj(_)) => false, + (Param::Float(_), Param::Obj(_)) => false, + (Param::Obj(_), Param::ParameterExpression(_)) => false, + (Param::Obj(_), Param::Float(_)) => false, + (Param::Obj(one), Param::Obj(other)) => Self::compare(one, other), } } } @@ -186,6 +267,12 @@ impl FromPyObject<'_> for CircuitRep { } } +impl IntoPy for CircuitRep { + fn into_py(self, _py: Python<'_>) -> PyObject { + self.object + } +} + #[derive(Debug, Clone)] pub struct CircuitInstructionRep { operation: GateRep, @@ -306,8 +393,17 @@ impl EquivalenceLibrary { /// the library, from earliest to latest, from top to base. The /// ordering of the StandardEquivalenceLibrary will not generally be /// consistent across Qiskit versions. - pub fn get_entry(&self, _gate: GateRep) { - todo!() + pub fn get_entry(&self, py: Python<'_>, gate: GateRep) -> Vec { + let key = Key { + name: gate.name.unwrap_or_default(), + num_qubits: gate.num_qubits.unwrap_or_default(), + }; + let query_params = gate.params; + + self.get_equivalences(&key) + .into_iter() + .filter_map(|equivalence| rebind_equiv(py, equivalence, &query_params).ok()) + .collect() } } @@ -531,6 +627,32 @@ fn raise_if_shape_mismatch(gate: &GateRep, circuit: &CircuitRep) -> Result<(), E Ok(()) } +fn rebind_equiv( + py: Python<'_>, + equiv: Equivalence, + query_params: &[Param], +) -> PyResult { + let (equiv_params, equiv_circuit) = (equiv.params, equiv.circuit); + let param_map: Vec<(Param, Param)> = equiv_params + .into_iter() + .filter_map(|param| { + if matches!(param, Param::Obj(_)) { + Some(param) + } else { + None + } + }) + .zip(query_params.iter().cloned()) + .collect(); + let dict = param_map.as_slice().into_py_dict_bound(py); + let kwargs = [("inplace", false), ("flat_input", true)].into_py_dict_bound(py); + let equiv = + equiv_circuit + .object + .call_method_bound(py, "assign_parameters", (dict,), Some(&kwargs))?; + equiv.extract::(py) +} + // Errors #[derive(Debug, Clone)] @@ -551,3 +673,9 @@ impl Display for EquivalenceError { write!(f, "{}", self.message) } } + +#[pymodule] +pub fn equivalence(_py: Python, m: &Bound<'_, PyModule>) -> PyResult<()> { + m.add_class::()?; + Ok(()) +} diff --git a/crates/circuit/src/lib.rs b/crates/circuit/src/lib.rs index fc8b2c686251..55787fd7f058 100644 --- a/crates/circuit/src/lib.rs +++ b/crates/circuit/src/lib.rs @@ -32,6 +32,7 @@ pub enum SliceOrInt<'a> { #[pymodule] pub fn circuit(m: Bound) -> PyResult<()> { m.add_class::()?; + m.add_class::()?; m.add_class::()?; m.add_class::()?; m.add_class::()?; From 7d0f4deb6c69a99711ce153b562a476ea2a6bc76 Mon Sep 17 00:00:00 2001 From: Raynel Sanchez <87539502+raynelfss@users.noreply.github.com> Date: Wed, 12 Jun 2024 11:07:46 +0200 Subject: [PATCH 04/45] Add: PyDiGraph converter for `equivalence.rs` --- crates/circuit/src/equivalence.rs | 155 ++++++++++++++++++------ test/python/circuit/test_equivalence.py | 3 +- 2 files changed, 118 insertions(+), 40 deletions(-) diff --git a/crates/circuit/src/equivalence.rs b/crates/circuit/src/equivalence.rs index a49a0065b920..c8fabbcdb2af 100644 --- a/crates/circuit/src/equivalence.rs +++ b/crates/circuit/src/equivalence.rs @@ -63,15 +63,20 @@ impl ImportOnceCell { } } -// Custom Structs - pub static PARAMETER_EXPRESSION: ImportOnceCell = ImportOnceCell::new("qiskit.circuit.parameterexpression", "ParameterExpression"); pub static QUANTUM_CIRCUIT: ImportOnceCell = ImportOnceCell::new("qiskit.circuit.quantumcircuit", "QuantumCircuit"); +pub static PYDIGRAPH: ImportOnceCell = ImportOnceCell::new("rustworkx", "PyDiGraph"); + +// Custom Structs + +#[pyclass(sequence)] #[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord, Hash)] pub struct Key { + #[pyo3(get)] pub name: String, + #[pyo3(get)] pub num_qubits: usize, } @@ -85,12 +90,39 @@ impl Display for Key { } } +#[pyclass(sequence)] #[derive(Debug, Clone)] pub struct Equivalence { + #[pyo3(get)] pub params: Vec, + #[pyo3(get)] pub circuit: CircuitRep, } +#[pyclass(sequence)] +#[derive(Debug, Clone)] +pub struct NodeData { + #[pyo3(get)] + key: Key, + #[pyo3(get)] + equivs: Vec, +} + +#[pyclass(sequence)] +#[derive(Debug, Clone)] +pub struct EdgeData { + #[pyo3(get)] + pub index: u32, + #[pyo3(get)] + pub num_gates: usize, + #[pyo3(get)] + pub rule: Equivalence, + #[pyo3(get)] + pub source: Key, +} + +// REPRESENTATIONS of non rust objects +// Temporary definition of Parameter #[derive(Clone, Debug)] pub enum Param { ParameterExpression(PyObject), @@ -162,20 +194,6 @@ impl PartialEq for Param { } } -#[derive(Debug, Clone)] -pub struct NodeData { - key: Key, - equivs: Vec, -} - -#[derive(Debug, Clone)] -pub struct EdgeData { - pub index: u32, - pub num_gates: usize, - pub rule: Equivalence, - pub source: Key, -} - /// Temporary interpretation of Gate #[derive(Debug, Clone)] pub struct GateRep { @@ -220,8 +238,13 @@ impl FromPyObject<'_> for GateRep { }) } } +impl IntoPy for GateRep { + fn into_py(self, _py: Python<'_>) -> PyObject { + self.object + } +} -/// Temporary interpretation of Gate +/// Temporary interpretation of QuantumCircuit #[derive(Debug, Clone)] pub struct CircuitRep { object: PyObject, @@ -273,6 +296,7 @@ impl IntoPy for CircuitRep { } } +// Temporary Representation of CircuitInstruction #[derive(Debug, Clone)] pub struct CircuitInstructionRep { operation: GateRep, @@ -292,9 +316,10 @@ type KTIType = HashMap; #[pyclass] #[derive(Debug, Clone)] pub struct EquivalenceLibrary { - graph: GraphType, + _graph: GraphType, key_to_node_index: KTIType, rule_id: usize, + graph: Option, } #[pymethods] @@ -308,15 +333,17 @@ impl EquivalenceLibrary { fn new(base: Option<&EquivalenceLibrary>) -> Self { if let Some(base) = base { Self { - graph: base.graph.clone(), + _graph: base._graph.clone(), key_to_node_index: base.key_to_node_index.clone(), rule_id: base.rule_id, + graph: None, } } else { Self { - graph: GraphType::new(), + _graph: GraphType::new(), key_to_node_index: KTIType::new(), rule_id: 0_usize, + graph: None, } } } @@ -405,19 +432,39 @@ impl EquivalenceLibrary { .filter_map(|equivalence| rebind_equiv(py, equivalence, &query_params).ok()) .collect() } + + #[getter] + fn get_graph(&mut self, py: Python<'_>) -> PyResult { + if let Some(graph) = &self.graph { + Ok(graph.to_owned()) + } else { + self.graph = Some(to_pygraph(py, &self._graph)?); + Ok(self.graph.to_object(py)) + } + } } // Rust native methods impl EquivalenceLibrary { /// Create a new node if key not found fn set_default_node(&mut self, key: Key) -> NodeIndex { - *self - .key_to_node_index - .entry(key.to_owned()) - .or_insert(self.graph.add_node(NodeData { - key, + if let Some(value) = self.key_to_node_index.get(&key) { + *value + } else { + let node = self._graph.add_node(NodeData { + key: key.to_owned(), equivs: vec![], - })) + }); + self.key_to_node_index.insert(key, node); + node + } + // *self + // .key_to_node_index + // .entry(key.to_owned()) + // .or_insert(self._graph.add_node(NodeData { + // key, + // equivs: vec![], + // })) } /// Rust native equivalent to `EquivalenceLibrary.add_equivalence()` @@ -452,30 +499,31 @@ impl EquivalenceLibrary { }; let target = self.set_default_node(key); - let node = self.graph.node_weight_mut(target).unwrap(); - node.equivs.push(equiv.to_owned()); - + if let Some(node) = self._graph.node_weight_mut(target) { + node.equivs.push(equiv.to_owned()); + } let sources: HashSet = HashSet::from_iter(equivalent_circuit.data.iter().map(|inst| Key { name: inst.operation.name.to_owned().unwrap_or_default(), num_qubits: inst.operation.num_qubits.unwrap_or_default(), })); - let edges = Vec::from_iter(sources.iter().map(|key| { + let edges = Vec::from_iter(sources.iter().map(|source| { ( - self.set_default_node(key.to_owned()), + self.set_default_node(source.to_owned()), target, EdgeData { index: self.rule_id as u32, num_gates: sources.len(), rule: equiv.to_owned(), - source: key.to_owned(), + source: source.to_owned(), }, ) })); for edge in edges { - self.graph.add_edge(edge.0, edge.1, edge.2); + self._graph.add_edge(edge.0, edge.1, edge.2); } self.rule_id += 1; + self.graph = None; Ok(()) } @@ -507,27 +555,29 @@ impl EquivalenceLibrary { }; let node_index = self.set_default_node(key); - let graph_ind = &mut self.graph.node_weight_mut(node_index).unwrap(); - graph_ind.equivs.clear(); + if let Some(graph_ind) = self._graph.node_weight_mut(node_index) { + graph_ind.equivs.clear(); + } let edges: Vec = self - .graph + ._graph .edges_directed(node_index, rustworkx_core::petgraph::Direction::Incoming) .map(|x| x.id()) .collect(); for edge in edges { - self.graph.remove_edge(edge); + self._graph.remove_edge(edge); } for equiv in entry { self.add_equiv(gate.to_owned(), equiv.to_owned())? } + self.graph = None; Ok(()) } /// Get all the equivalences for the given key fn get_equivalences(&self, key: &Key) -> Vec { if let Some(key_in) = self.key_to_node_index.get(key) { - self.graph[*key_in].equivs.to_owned() + self._graph[*key_in].equivs.to_owned() } else { vec![] } @@ -674,8 +724,37 @@ impl Display for EquivalenceError { } } +fn to_pygraph(py: Python<'_>, pet_graph: &DiGraph) -> PyResult +where + N: IntoPy + Clone, + E: IntoPy + Clone, +{ + let graph = PYDIGRAPH.get_bound(py).call0()?; + let node_weights = pet_graph.node_weights(); + for node in node_weights { + graph.call_method1("add_node", (node.to_owned(),))?; + } + let edge_weights = pet_graph.edge_indices().map(|edge| { + ( + pet_graph.edge_endpoints(edge).unwrap(), + pet_graph.edge_weight(edge).unwrap(), + ) + }); + for ((source, target), weight) in edge_weights { + graph.call_method1( + "add_edge", + (source.index(), target.index(), weight.to_owned()), + )?; + } + Ok(graph.unbind()) +} + #[pymodule] pub fn equivalence(_py: Python, m: &Bound<'_, PyModule>) -> PyResult<()> { m.add_class::()?; + m.add_class::()?; + m.add_class::()?; + m.add_class::()?; + m.add_class::()?; Ok(()) } diff --git a/test/python/circuit/test_equivalence.py b/test/python/circuit/test_equivalence.py index 286941313ae9..f4836e122f72 100644 --- a/test/python/circuit/test_equivalence.py +++ b/test/python/circuit/test_equivalence.py @@ -22,9 +22,8 @@ from qiskit.circuit.library import U2Gate from qiskit.circuit.exceptions import CircuitError from qiskit.converters import circuit_to_instruction, circuit_to_gate -from qiskit.circuit import EquivalenceLibrary +from qiskit._accelerate.circuit import EquivalenceLibrary, Key, Equivalence, NodeData, EdgeData from qiskit.utils import optionals -from qiskit.circuit.equivalence import Key, Equivalence, NodeData, EdgeData from test import QiskitTestCase # pylint: disable=wrong-import-order from ..visualization.visualization import QiskitVisualizationTestCase, path_to_diagram_reference From f73581e8acc8915d83f44dcf8e2b53dd8a5b61af Mon Sep 17 00:00:00 2001 From: Raynel Sanchez <87539502+raynelfss@users.noreply.github.com> Date: Wed, 12 Jun 2024 15:00:10 +0200 Subject: [PATCH 05/45] Add: Extend original equivalence with rust representation --- crates/circuit/src/equivalence.rs | 159 +++++++++++------- crates/circuit/src/lib.rs | 4 + qiskit/circuit/equivalence.py | 215 +----------------------- test/python/circuit/test_equivalence.py | 2 +- 4 files changed, 114 insertions(+), 266 deletions(-) diff --git a/crates/circuit/src/equivalence.rs b/crates/circuit/src/equivalence.rs index c8fabbcdb2af..b1fbb28227d5 100644 --- a/crates/circuit/src/equivalence.rs +++ b/crates/circuit/src/equivalence.rs @@ -10,6 +10,8 @@ // copyright notice, and modified files need to carry a notice indicating // that they have been altered from the originals. +use std::collections::hash_map::DefaultHasher; +use std::hash::{Hash, Hasher}; use std::{error::Error, fmt::Display}; use exceptions::CircuitError; @@ -80,11 +82,33 @@ pub struct Key { pub num_qubits: usize, } +#[pymethods] +impl Key { + #[new] + fn new(name: String, num_qubits: usize) -> Self { + Self { name, num_qubits } + } + + fn __eq__(&self, other: Self) -> bool { + self.eq(&other) + } + + fn __hash__(&self) -> u64 { + let mut hasher = DefaultHasher::new(); + (self.name.to_string(), self.num_qubits).hash(&mut hasher); + hasher.finish() + } + + fn __repr__(slf: PyRef<'_, Self>) -> String { + slf.to_string() + } +} + impl Display for Key { fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { write!( f, - "Key(name=\"{}\" num_qubits=\"{}\"", + "Key(name=\'{}\', num_qubits={})", self.name, self.num_qubits ) } @@ -99,6 +123,28 @@ pub struct Equivalence { pub circuit: CircuitRep, } +#[pymethods] +impl Equivalence { + #[new] + fn new(params: Vec, circuit: CircuitRep) -> Self { + Self { circuit, params } + } + + fn __repr__(&self) -> String { + self.to_string() + } +} + +impl Display for Equivalence { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + write!( + f, + "Equivalence(params={:?}, circuits={:?})", + self.params, self.circuit + ) + } +} + #[pyclass(sequence)] #[derive(Debug, Clone)] pub struct NodeData { @@ -108,6 +154,24 @@ pub struct NodeData { equivs: Vec, } +#[pymethods] +impl NodeData { + #[new] + fn new(key: Key, equivs: Vec) -> Self { + Self { key, equivs } + } + + fn __repr__(&self) -> String { + self.to_string() + } +} + +impl Display for NodeData { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + write!(f, "NodeData(key={}, equivs={:#?})", self.key, self.equivs) + } +} + #[pyclass(sequence)] #[derive(Debug, Clone)] pub struct EdgeData { @@ -121,6 +185,33 @@ pub struct EdgeData { pub source: Key, } +#[pymethods] +impl EdgeData { + #[new] + fn new(index: u32, num_gates: usize, rule: Equivalence, source: Key) -> Self { + Self { + index, + num_gates, + rule, + source, + } + } + + fn __repr__(&self) -> String { + self.to_string() + } +} + +impl Display for EdgeData { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + write!( + f, + "EdgeData(index={}, num_gates={}, rule={}, source={})", + self.index, self.num_gates, self.rule, self.source + ) + } +} + // REPRESENTATIONS of non rust objects // Temporary definition of Parameter #[derive(Clone, Debug)] @@ -313,7 +404,7 @@ impl FromPyObject<'_> for CircuitInstructionRep { type GraphType = DiGraph; type KTIType = HashMap; -#[pyclass] +#[pyclass(subclass, name = "BaseEquivalenceLibrary")] #[derive(Debug, Clone)] pub struct EquivalenceLibrary { _graph: GraphType, @@ -442,6 +533,14 @@ impl EquivalenceLibrary { Ok(self.graph.to_object(py)) } } + + fn keys(&self) -> HashSet { + self.key_to_node_index.keys().cloned().collect() + } + + fn node_index(&self, key: Key) -> usize { + self.key_to_node_index[&key].index() + } } // Rust native methods @@ -582,62 +681,6 @@ impl EquivalenceLibrary { vec![] } } - - fn build_basis_graph(&self) -> DiGraph> { - let mut graph: DiGraph> = DiGraph::new(); - - let mut node_map: HashMap = HashMap::new(); - - for key in self.key_to_node_index.keys() { - let (name, num_qubits) = (key.name.to_owned(), key.num_qubits); - let equivalences = self.get_equivalences(key); - let basis: String = format!("{{{name}/{num_qubits}}}"); - for equivalence in equivalences { - let decomp_basis: HashSet = HashSet::from_iter( - equivalence - .circuit - .data - .iter() - .map(|x| (x.operation.name.to_owned(), x.operation.num_qubits)) - .map(|(name, num_qubits)| { - format!( - "{}/{}", - name.unwrap_or_default(), - num_qubits.unwrap_or_default() - ) - }), - ); - let decomp_string = format!( - "{{{}}}", - decomp_basis - .iter() - .cloned() - .collect::>() - .join(", ") - ); - if !node_map.contains_key(&basis) { - let basis_node = - graph.add_node(format!("basis: {:?}, label: {}", basis, decomp_string)); - node_map.insert(basis.to_owned(), basis_node); - } - if node_map.contains_key(&decomp_string) { - let decomp_basis_node = graph.add_node(format!( - "basis: {:?}, label: {}", - decomp_string, decomp_string - )); - node_map.insert(decomp_string.to_owned(), decomp_basis_node); - } - let label = format!("{:?}\n{:?}", equivalence.params, equivalence.circuit); - let map: HashMap<&str, String> = HashMap::from_iter([ - ("label", label), - ("fontname", "Courier".to_owned()), - ("fontsize", 8.to_string()), - ]); - graph.add_edge(node_map[&basis], node_map[&decomp_string], map); - } - } - graph - } } fn raise_if_param_mismatch( diff --git a/crates/circuit/src/lib.rs b/crates/circuit/src/lib.rs index 68e39746d6c4..2a4bc594d1c3 100644 --- a/crates/circuit/src/lib.rs +++ b/crates/circuit/src/lib.rs @@ -66,6 +66,10 @@ impl From for BitType { pub fn circuit(m: Bound) -> PyResult<()> { m.add_class::()?; m.add_class::()?; + m.add_class::()?; + m.add_class::()?; + m.add_class::()?; + m.add_class::()?; m.add_class::()?; m.add_class::()?; m.add_class::()?; diff --git a/qiskit/circuit/equivalence.py b/qiskit/circuit/equivalence.py index 45921c3f2293..28ce5a4b92dd 100644 --- a/qiskit/circuit/equivalence.py +++ b/qiskit/circuit/equivalence.py @@ -18,184 +18,18 @@ from rustworkx.visualization import graphviz_draw import rustworkx as rx + from qiskit.exceptions import InvalidFileError from .exceptions import CircuitError from .parameter import Parameter from .parameterexpression import ParameterExpression - -Key = namedtuple("Key", ["name", "num_qubits"]) -Equivalence = namedtuple("Equivalence", ["params", "circuit"]) # Ordered to match Gate.params -NodeData = namedtuple("NodeData", ["key", "equivs"]) -EdgeData = namedtuple("EdgeData", ["index", "num_gates", "rule", "source"]) +from qiskit._accelerate.circuit import BaseEquivalenceLibrary, Key, Equivalence, NodeData, EdgeData -class EquivalenceLibrary: +class EquivalenceLibrary(BaseEquivalenceLibrary): """A library providing a one-way mapping of Gates to their equivalent implementations as QuantumCircuits.""" - def __init__(self, *, base=None): - """Create a new equivalence library. - - Args: - base (Optional[EquivalenceLibrary]): Base equivalence library to - be referenced if an entry is not found in this library. - """ - self._base = base - - if base is None: - self._graph = rx.PyDiGraph() - self._key_to_node_index = {} - # Some unique identifier for rules. - self._rule_id = 0 - else: - self._graph = base._graph.copy() - self._key_to_node_index = copy.deepcopy(base._key_to_node_index) - self._rule_id = base._rule_id - - @property - def graph(self) -> rx.PyDiGraph: - """Return graph representing the equivalence library data. - - This property should be treated as read-only as it provides - a reference to the internal state of the :class:`~.EquivalenceLibrary` object. - If the graph returned by this property is mutated it could corrupt the - the contents of the object. If you need to modify the output ``PyDiGraph`` - be sure to make a copy prior to any modification. - - Returns: - PyDiGraph: A graph object with equivalence data in each node. - """ - return self._graph - - def _set_default_node(self, key): - """Create a new node if key not found""" - if key not in self._key_to_node_index: - self._key_to_node_index[key] = self._graph.add_node(NodeData(key=key, equivs=[])) - return self._key_to_node_index[key] - - def add_equivalence(self, gate, equivalent_circuit): - """Add a new equivalence to the library. Future queries for the Gate - will include the given circuit, in addition to all existing equivalences - (including those from base). - - Parameterized Gates (those including `qiskit.circuit.Parameters` in their - `Gate.params`) can be marked equivalent to parameterized circuits, - provided the parameters match. - - Args: - gate (Gate): A Gate instance. - equivalent_circuit (QuantumCircuit): A circuit equivalently - implementing the given Gate. - """ - - _raise_if_shape_mismatch(gate, equivalent_circuit) - _raise_if_param_mismatch(gate.params, equivalent_circuit.parameters) - - key = Key(name=gate.name, num_qubits=gate.num_qubits) - equiv = Equivalence(params=gate.params.copy(), circuit=equivalent_circuit.copy()) - - target = self._set_default_node(key) - self._graph[target].equivs.append(equiv) - - sources = { - Key(name=instruction.operation.name, num_qubits=len(instruction.qubits)) - for instruction in equivalent_circuit - } - edges = [ - ( - self._set_default_node(source), - target, - EdgeData(index=self._rule_id, num_gates=len(sources), rule=equiv, source=source), - ) - for source in sources - ] - self._graph.add_edges_from(edges) - self._rule_id += 1 - - def has_entry(self, gate): - """Check if a library contains any decompositions for gate. - - Args: - gate (Gate): A Gate instance. - - Returns: - Bool: True if gate has a known decomposition in the library. - False otherwise. - """ - key = Key(name=gate.name, num_qubits=gate.num_qubits) - - return key in self._key_to_node_index - - def set_entry(self, gate, entry): - """Set the equivalence record for a Gate. Future queries for the Gate - will return only the circuits provided. - - Parameterized Gates (those including `qiskit.circuit.Parameters` in their - `Gate.params`) can be marked equivalent to parameterized circuits, - provided the parameters match. - - Args: - gate (Gate): A Gate instance. - entry (List['QuantumCircuit']) : A list of QuantumCircuits, each - equivalently implementing the given Gate. - """ - for equiv in entry: - _raise_if_shape_mismatch(gate, equiv) - _raise_if_param_mismatch(gate.params, equiv.parameters) - - node_index = self._set_default_node(Key(name=gate.name, num_qubits=gate.num_qubits)) - # Remove previous equivalences of this node, leaving in place any later equivalences that - # were added that use `gate`. - self._graph[node_index].equivs.clear() - for parent, child, _ in self._graph.in_edges(node_index): - # `child` should always be ourselves, but there might be parallel edges. - self._graph.remove_edge(parent, child) - for equivalence in entry: - self.add_equivalence(gate, equivalence) - - def get_entry(self, gate): - """Gets the set of QuantumCircuits circuits from the library which - equivalently implement the given Gate. - - Parameterized circuits will have their parameters replaced with the - corresponding entries from Gate.params. - - Args: - gate (Gate) - Gate: A Gate instance. - - Returns: - List[QuantumCircuit]: A list of equivalent QuantumCircuits. If empty, - library contains no known decompositions of Gate. - - Returned circuits will be ordered according to their insertion in - the library, from earliest to latest, from top to base. The - ordering of the StandardEquivalenceLibrary will not generally be - consistent across Qiskit versions. - """ - key = Key(name=gate.name, num_qubits=gate.num_qubits) - query_params = gate.params - - return [_rebind_equiv(equiv, query_params) for equiv in self._get_equivalences(key)] - - def keys(self): - """Return list of keys to key to node index map. - - Returns: - List: Keys to the key to node index map. - """ - return self._key_to_node_index.keys() - - def node_index(self, key): - """Return node index for a given key. - - Args: - key (Key): Key to an equivalence. - - Returns: - Int: Index to the node in the graph for the given key. - """ - return self._key_to_node_index[key] - def draw(self, filename=None): """Draws the equivalence relations available in the library. @@ -227,12 +61,13 @@ def _build_basis_graph(self): graph = rx.PyDiGraph() node_map = {} - for key in self._key_to_node_index: - name, num_qubits = key + for key in super().keys(): + name, num_qubits = key.name, key.num_qubits equivalences = self._get_equivalences(key) basis = frozenset([f"{name}/{num_qubits}"]) - for params, decomp in equivalences: + for equivalence in equivalences: + params, decomp = equivalence.params, equivalence.circuit decomp_basis = frozenset( f"{name}/{num_qubits}" for name, num_qubits in { @@ -260,38 +95,4 @@ def _build_basis_graph(self): def _get_equivalences(self, key): """Get all the equivalences for the given key""" - return ( - self._graph[self._key_to_node_index[key]].equivs - if key in self._key_to_node_index - else [] - ) - - -def _raise_if_param_mismatch(gate_params, circuit_parameters): - gate_parameters = [p for p in gate_params if isinstance(p, ParameterExpression)] - - if set(gate_parameters) != circuit_parameters: - raise CircuitError( - "Cannot add equivalence between circuit and gate " - "of different parameters. Gate params: {}. " - "Circuit params: {}.".format(gate_parameters, circuit_parameters) - ) - - -def _raise_if_shape_mismatch(gate, circuit): - if gate.num_qubits != circuit.num_qubits or gate.num_clbits != circuit.num_clbits: - raise CircuitError( - "Cannot add equivalence between circuit and gate " - "of different shapes. Gate: {} qubits and {} clbits. " - "Circuit: {} qubits and {} clbits.".format( - gate.num_qubits, gate.num_clbits, circuit.num_qubits, circuit.num_clbits - ) - ) - - -def _rebind_equiv(equiv, query_params): - equiv_params, equiv_circuit = equiv - param_map = {x: y for x, y in zip(equiv_params, query_params) if isinstance(x, Parameter)} - equiv = equiv_circuit.assign_parameters(param_map, inplace=False, flat_input=True) - - return equiv + return self.graph[self.node_index(key)].equivs if key in self.keys() else [] diff --git a/test/python/circuit/test_equivalence.py b/test/python/circuit/test_equivalence.py index f4836e122f72..acdcf9e9685c 100644 --- a/test/python/circuit/test_equivalence.py +++ b/test/python/circuit/test_equivalence.py @@ -22,7 +22,7 @@ from qiskit.circuit.library import U2Gate from qiskit.circuit.exceptions import CircuitError from qiskit.converters import circuit_to_instruction, circuit_to_gate -from qiskit._accelerate.circuit import EquivalenceLibrary, Key, Equivalence, NodeData, EdgeData +from qiskit.circuit.equivalence import EquivalenceLibrary, Key, Equivalence, NodeData, EdgeData from qiskit.utils import optionals from test import QiskitTestCase # pylint: disable=wrong-import-order From 57e0ef8886fe8ea11feac73aad8610e44cd9d68c Mon Sep 17 00:00:00 2001 From: Raynel Sanchez <87539502+raynelfss@users.noreply.github.com> Date: Thu, 13 Jun 2024 03:51:38 +0200 Subject: [PATCH 06/45] Fix: Correct circuit parameter extraction --- crates/circuit/src/equivalence.rs | 87 ++++++++++++++++++++----------- 1 file changed, 56 insertions(+), 31 deletions(-) diff --git a/crates/circuit/src/equivalence.rs b/crates/circuit/src/equivalence.rs index b1fbb28227d5..314de290589e 100644 --- a/crates/circuit/src/equivalence.rs +++ b/crates/circuit/src/equivalence.rs @@ -216,7 +216,7 @@ impl Display for EdgeData { // Temporary definition of Parameter #[derive(Clone, Debug)] pub enum Param { - ParameterExpression(PyObject), + ParameterExpression(ParamExpRep), Float(f64), Obj(PyObject), } @@ -227,7 +227,11 @@ impl<'py> FromPyObject<'py> for Param { if b.is_instance(PARAMETER_EXPRESSION.get_bound(b.py()))? || b.is_instance(QUANTUM_CIRCUIT.get_bound(b.py()))? { - Param::ParameterExpression(b.clone().unbind()) + let id = b.getattr("_uuid")?.getattr("hex")?.extract::()?; + Param::ParameterExpression(ParamExpRep { + id, + object: b.clone().unbind(), + }) } else if let Ok(val) = b.extract::() { Param::Float(val) } else { @@ -241,7 +245,7 @@ impl IntoPy for Param { fn into_py(self, py: Python) -> PyObject { match &self { Self::Float(val) => val.to_object(py), - Self::ParameterExpression(val) => val.clone_ref(py), + Self::ParameterExpression(val) => val.object.clone_ref(py), Self::Obj(val) => val.clone_ref(py), } } @@ -251,7 +255,7 @@ impl ToPyObject for Param { fn to_object(&self, py: Python) -> PyObject { match self { Self::Float(val) => val.to_object(py), - Self::ParameterExpression(val) => val.clone_ref(py), + Self::ParameterExpression(val) => val.object.clone_ref(py), Self::Obj(val) => val.clone_ref(py), } } @@ -263,7 +267,7 @@ impl Param { let other_bound = other.bind(py); other_bound.eq(one) }) - .unwrap() + .unwrap_or_default() } } @@ -273,9 +277,7 @@ impl PartialEq for Param { (Param::Float(s), Param::Float(other)) => s == other, (Param::Float(_), Param::ParameterExpression(_)) => false, (Param::ParameterExpression(_), Param::Float(_)) => false, - (Param::ParameterExpression(s), Param::ParameterExpression(other)) => { - Self::compare(s, other) - } + (Param::ParameterExpression(s), Param::ParameterExpression(other)) => s.id == other.id, (Param::ParameterExpression(_), Param::Obj(_)) => false, (Param::Float(_), Param::Obj(_)) => false, (Param::Obj(_), Param::ParameterExpression(_)) => false, @@ -285,6 +287,13 @@ impl PartialEq for Param { } } +/// ParamExpIdentifier +#[derive(Debug, Clone)] +pub struct ParamExpRep { + id: String, + object: PyObject, +} + /// Temporary interpretation of Gate #[derive(Debug, Clone)] pub struct GateRep { @@ -318,7 +327,7 @@ impl FromPyObject<'_> for GateRep { Ok(params) => params.extract::>().ok(), Err(_) => Some(vec![]), } - .unwrap(); + .unwrap_or_default(); Ok(Self { object: ob.into(), num_qubits, @@ -360,16 +369,16 @@ impl FromPyObject<'_> for CircuitRep { Ok(label) => label.extract::().ok(), Err(_) => None, }; - let params = match ob.getattr("params") { - Ok(params) => params.extract::>().ok(), - Err(_) => Some(vec![]), - } - .unwrap(); + let params = ob + .getattr("parameters")? + .getattr("data")? + .extract::>() + .unwrap_or_default(); let data = match ob.getattr("data") { Ok(data) => data.extract::>().ok(), Err(_) => Some(vec![]), } - .unwrap(); + .unwrap_or_default(); Ok(Self { object: ob.into(), num_qubits, @@ -391,12 +400,14 @@ impl IntoPy for CircuitRep { #[derive(Debug, Clone)] pub struct CircuitInstructionRep { operation: GateRep, + qubits: Vec, } impl FromPyObject<'_> for CircuitInstructionRep { fn extract(ob: &'_ PyAny) -> PyResult { + let qubits = ob.getattr("qubits")?.extract::>()?; let operation = ob.getattr("operation")?.extract::()?; - Ok(Self { operation }) + Ok(Self { operation, qubits }) } } @@ -470,7 +481,7 @@ impl EquivalenceLibrary { /// False otherwise. pub fn has_entry(&self, gate: GateRep) -> bool { let key = Key { - name: gate.name.unwrap(), + name: gate.name.unwrap_or_default(), num_qubits: gate.num_qubits.unwrap_or_default(), }; self.key_to_node_index.contains_key(&key) @@ -589,8 +600,8 @@ impl EquivalenceLibrary { raise_if_param_mismatch(&gate.params, &equivalent_circuit.params)?; let key: Key = Key { - name: gate.name.unwrap(), - num_qubits: gate.num_qubits.unwrap(), + name: gate.name.unwrap_or_default(), + num_qubits: gate.num_qubits.unwrap_or_default(), }; let equiv = Equivalence { params: gate.params, @@ -604,7 +615,7 @@ impl EquivalenceLibrary { let sources: HashSet = HashSet::from_iter(equivalent_circuit.data.iter().map(|inst| Key { name: inst.operation.name.to_owned().unwrap_or_default(), - num_qubits: inst.operation.num_qubits.unwrap_or_default(), + num_qubits: inst.qubits.len(), })); let edges = Vec::from_iter(sources.iter().map(|source| { ( @@ -687,19 +698,28 @@ fn raise_if_param_mismatch( gate_params: &[Param], circuit_parameters: &[Param], ) -> Result<(), EquivalenceError> { - let gate_parameters: Vec<&Param> = gate_params + let uid_gate_parameters: HashSet = gate_params .iter() - .filter(|x| matches!(x, Param::ParameterExpression(_))) + .filter_map(|x| match x { + Param::ParameterExpression(param) => Some(param.id.to_string()), + Param::Float(_) => None, + Param::Obj(_) => None, + }) .collect(); - if circuit_parameters + let uid_circuit_parameters: HashSet = circuit_parameters .iter() - .any(|x| gate_parameters.contains(&x)) - { + .filter_map(|x| match x { + Param::ParameterExpression(param) => Some(param.id.to_string()), + Param::Float(_) => None, + Param::Obj(_) => None, + }) + .collect(); + if uid_gate_parameters != uid_circuit_parameters { return Err(EquivalenceError::new_err(format!( "Cannot add equivalence between circuit and gate \ - of different parameters. Gate params: {:?}. \ - Circuit params: {:?}.", - gate_parameters, circuit_parameters + of different parameters. Gate params: {:#?}. \ + Circuit params: {:#?}.", + gate_params, circuit_parameters ))); } Ok(()) @@ -729,7 +749,12 @@ fn rebind_equiv( let param_map: Vec<(Param, Param)> = equiv_params .into_iter() .filter_map(|param| { - if matches!(param, Param::Obj(_)) { + println!( + "{:#?}: is expr: {}", + param, + matches!(param, Param::ParameterExpression(_)) + ); + if matches!(param, Param::ParameterExpression(_)) { Some(param) } else { None @@ -739,11 +764,11 @@ fn rebind_equiv( .collect(); let dict = param_map.as_slice().into_py_dict_bound(py); let kwargs = [("inplace", false), ("flat_input", true)].into_py_dict_bound(py); - let equiv = + let new_equiv = equiv_circuit .object .call_method_bound(py, "assign_parameters", (dict,), Some(&kwargs))?; - equiv.extract::(py) + new_equiv.extract::(py) } // Errors From 11611e293c78155f3771f0b7ab865c3d2cfe309a Mon Sep 17 00:00:00 2001 From: Raynel Sanchez <87539502+raynelfss@users.noreply.github.com> Date: Thu, 13 Jun 2024 14:41:12 +0200 Subject: [PATCH 07/45] Add: Stable infrastructure for EquivalenceLibrary - TODO: Make elements pickleable. --- Cargo.lock | 1 + crates/circuit/Cargo.toml | 1 + crates/circuit/src/equivalence.rs | 159 ++++++++++++++++-- crates/circuit/src/lib.rs | 8 +- qiskit/__init__.py | 1 + qiskit/circuit/equivalence.py | 8 +- .../passes/basis/basis_translator.py | 11 +- 7 files changed, 162 insertions(+), 27 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index b6ec645603ee..6e2fd814155a 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -1196,6 +1196,7 @@ name = "qiskit-circuit" version = "1.2.0" dependencies = [ "hashbrown 0.14.5", + "itertools 0.13.0", "pyo3", "rustworkx-core", ] diff --git a/crates/circuit/Cargo.toml b/crates/circuit/Cargo.toml index 6ebf9694afc1..bafd60fbaaec 100644 --- a/crates/circuit/Cargo.toml +++ b/crates/circuit/Cargo.toml @@ -11,5 +11,6 @@ doctest = false [dependencies] hashbrown.workspace = true +itertools = "0.13.0" pyo3.workspace = true rustworkx-core = "0.14.2" diff --git a/crates/circuit/src/equivalence.rs b/crates/circuit/src/equivalence.rs index 314de290589e..303a7da06b27 100644 --- a/crates/circuit/src/equivalence.rs +++ b/crates/circuit/src/equivalence.rs @@ -10,6 +10,7 @@ // copyright notice, and modified files need to carry a notice indicating // that they have been altered from the originals. +use itertools::Itertools; use std::collections::hash_map::DefaultHasher; use std::hash::{Hash, Hasher}; use std::{error::Error, fmt::Display}; @@ -17,7 +18,9 @@ use std::{error::Error, fmt::Display}; use exceptions::CircuitError; use hashbrown::{HashMap, HashSet}; use pyo3::sync::GILOnceCell; +use pyo3::types::PyDict; use pyo3::{prelude::*, types::IntoPyDict}; + use rustworkx_core::petgraph::{ graph::{DiGraph, EdgeIndex, NodeIndex}, visit::EdgeRef, @@ -73,7 +76,7 @@ pub static PYDIGRAPH: ImportOnceCell = ImportOnceCell::new("rustworkx", "PyDiGra // Custom Structs -#[pyclass(sequence)] +#[pyclass(sequence, module = "qiskit._accelerate.circuit.equivalence")] #[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord, Hash)] pub struct Key { #[pyo3(get)] @@ -85,6 +88,7 @@ pub struct Key { #[pymethods] impl Key { #[new] + #[pyo3(signature = (name, num_qubits))] fn new(name: String, num_qubits: usize) -> Self { Self { name, num_qubits } } @@ -102,6 +106,15 @@ impl Key { fn __repr__(slf: PyRef<'_, Self>) -> String { slf.to_string() } + + fn __getstate__(slf: PyRef) -> (String, usize) { + (slf.name.to_owned(), slf.num_qubits) + } + + fn __setstate__(mut slf: PyRefMut, state: (String, usize)) { + slf.name = state.0; + slf.num_qubits = state.1; + } } impl Display for Key { @@ -114,8 +127,8 @@ impl Display for Key { } } -#[pyclass(sequence)] -#[derive(Debug, Clone)] +#[pyclass(sequence, module = "qiskit._accelerate.circuit.equivalence")] +#[derive(Debug, Clone, PartialEq)] pub struct Equivalence { #[pyo3(get)] pub params: Vec, @@ -126,6 +139,7 @@ pub struct Equivalence { #[pymethods] impl Equivalence { #[new] + #[pyo3(signature = (params, circuit))] fn new(params: Vec, circuit: CircuitRep) -> Self { Self { circuit, params } } @@ -133,6 +147,19 @@ impl Equivalence { fn __repr__(&self) -> String { self.to_string() } + + fn __eq__(&self, other: Self) -> bool { + self.eq(&other) + } + + fn __getstate__(slf: PyRef) -> (Vec, CircuitRep) { + (slf.params.to_owned(), slf.circuit.to_owned()) + } + + fn __setstate__(mut slf: PyRefMut, state: (Vec, CircuitRep)) { + slf.params = state.0; + slf.circuit = state.1; + } } impl Display for Equivalence { @@ -145,8 +172,8 @@ impl Display for Equivalence { } } -#[pyclass(sequence)] -#[derive(Debug, Clone)] +#[pyclass(sequence, module = "qiskit._accelerate.circuit.equivalence")] +#[derive(Debug, Clone, PartialEq)] pub struct NodeData { #[pyo3(get)] key: Key, @@ -157,6 +184,7 @@ pub struct NodeData { #[pymethods] impl NodeData { #[new] + #[pyo3(signature = (key, equivs))] fn new(key: Key, equivs: Vec) -> Self { Self { key, equivs } } @@ -164,19 +192,37 @@ impl NodeData { fn __repr__(&self) -> String { self.to_string() } + + fn __eq__(&self, other: Self) -> bool { + self.eq(&other) + } + + fn __getstate__(slf: PyRef) -> (Key, Vec) { + (slf.key.to_owned(), slf.equivs.to_owned()) + } + + fn __setstate__(mut slf: PyRefMut, state: (Key, Vec)) { + slf.key = state.0; + slf.equivs = state.1; + } } impl Display for NodeData { fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { - write!(f, "NodeData(key={}, equivs={:#?})", self.key, self.equivs) + write!( + f, + "NodeData(key={}, equivs=[{}])", + self.key, + self.equivs.iter().format(", ") + ) } } -#[pyclass(sequence)] -#[derive(Debug, Clone)] +#[pyclass(sequence, module = "qiskit._accelerate.circuit.equivalence")] +#[derive(Debug, Clone, PartialEq)] pub struct EdgeData { #[pyo3(get)] - pub index: u32, + pub index: usize, #[pyo3(get)] pub num_gates: usize, #[pyo3(get)] @@ -188,7 +234,8 @@ pub struct EdgeData { #[pymethods] impl EdgeData { #[new] - fn new(index: u32, num_gates: usize, rule: Equivalence, source: Key) -> Self { + #[pyo3(signature = (index, num_gates, rule, source))] + fn new(index: usize, num_gates: usize, rule: Equivalence, source: Key) -> Self { Self { index, num_gates, @@ -200,6 +247,26 @@ impl EdgeData { fn __repr__(&self) -> String { self.to_string() } + + fn __eq__(&self, other: Self) -> bool { + self.eq(&other) + } + + fn __getstate__(slf: PyRef) -> (usize, usize, Equivalence, Key) { + ( + slf.index, + slf.num_gates, + slf.rule.to_owned(), + slf.source.to_owned(), + ) + } + + fn __setstate__(mut slf: PyRefMut, state: (usize, usize, Equivalence, Key)) { + slf.index = state.0; + slf.num_gates = state.1; + slf.rule = state.2; + slf.source = state.3; + } } impl Display for EdgeData { @@ -390,6 +457,12 @@ impl FromPyObject<'_> for CircuitRep { } } +impl PartialEq for CircuitRep { + fn eq(&self, other: &Self) -> bool { + self.object.is(&other.object) + } +} + impl IntoPy for CircuitRep { fn into_py(self, _py: Python<'_>) -> PyObject { self.object @@ -415,7 +488,11 @@ impl FromPyObject<'_> for CircuitInstructionRep { type GraphType = DiGraph; type KTIType = HashMap; -#[pyclass(subclass, name = "BaseEquivalenceLibrary")] +#[pyclass( + subclass, + name = "BaseEquivalenceLibrary", + module = "qiskit._accelerate.circuit.equivalence" +)] #[derive(Debug, Clone)] pub struct EquivalenceLibrary { _graph: GraphType, @@ -552,6 +629,59 @@ impl EquivalenceLibrary { fn node_index(&self, key: Key) -> usize { self.key_to_node_index[&key].index() } + + fn __getstate__(slf: PyRef) -> PyResult> { + let ret = PyDict::new_bound(slf.py()); + ret.set_item("rule_id", slf.rule_id)?; + let key_to_usize_node: HashMap<(String, usize), usize> = HashMap::from_iter( + slf.key_to_node_index + .iter() + .map(|(key, val)| ((key.name.to_string(), key.num_qubits), val.index())), + ); + ret.set_item("key_to_node_index", key_to_usize_node.into_py(slf.py()))?; + let graph_nodes: Vec = slf._graph.node_weights().cloned().collect(); + ret.set_item("graph_nodes", graph_nodes.into_py(slf.py()))?; + let graph_edges: Vec<(usize, usize, EdgeData)> = slf + ._graph + .edge_indices() + .map(|edge_id| { + ( + slf._graph.edge_endpoints(edge_id).unwrap(), + slf._graph.edge_weight(edge_id).unwrap(), + ) + }) + .map(|((source, target), weight)| (source.index(), target.index(), weight.to_owned())) + .collect_vec(); + ret.set_item("graph_edges", graph_edges.into_py(slf.py()))?; + Ok(ret) + } + + fn __setstate__(mut slf: PyRefMut, state: &Bound<'_, PyDict>) -> PyResult<()> { + slf.rule_id = state.get_item("rule_id")?.unwrap().extract()?; + slf.key_to_node_index = state + .get_item("key_to_node_index")? + .unwrap() + .extract::>()? + .into_iter() + .map(|(key, val)| (key, NodeIndex::new(val))) + .collect(); + let graph_nodes: Vec = state.get_item("graph_nodes")?.unwrap().extract()?; + let graph_edges: Vec<(usize, usize, EdgeData)> = + state.get_item("graph_edges")?.unwrap().extract()?; + slf._graph = GraphType::new(); + for node_weight in graph_nodes { + slf._graph.add_node(node_weight); + } + for (source_node, target_node, edge_weight) in graph_edges { + slf._graph.add_edge( + NodeIndex::new(source_node), + NodeIndex::new(target_node), + edge_weight, + ); + } + slf.graph = None; + Ok(()) + } } // Rust native methods @@ -622,7 +752,7 @@ impl EquivalenceLibrary { self.set_default_node(source.to_owned()), target, EdgeData { - index: self.rule_id as u32, + index: self.rule_id, num_gates: sources.len(), rule: equiv.to_owned(), source: source.to_owned(), @@ -749,11 +879,6 @@ fn rebind_equiv( let param_map: Vec<(Param, Param)> = equiv_params .into_iter() .filter_map(|param| { - println!( - "{:#?}: is expr: {}", - param, - matches!(param, Param::ParameterExpression(_)) - ); if matches!(param, Param::ParameterExpression(_)) { Some(param) } else { diff --git a/crates/circuit/src/lib.rs b/crates/circuit/src/lib.rs index 2a4bc594d1c3..f11b3c78b389 100644 --- a/crates/circuit/src/lib.rs +++ b/crates/circuit/src/lib.rs @@ -19,8 +19,8 @@ mod bit_data; mod interner; mod packed_instruction; -use pyo3::prelude::*; use pyo3::types::PySlice; +use pyo3::{prelude::*, wrap_pymodule}; /// A private enumeration type used to extract arguments to pymethod /// that may be either an index or a slice @@ -64,12 +64,8 @@ impl From for BitType { #[pymodule] pub fn circuit(m: Bound) -> PyResult<()> { + m.add_wrapped(wrap_pymodule!(equivalence::equivalence))?; m.add_class::()?; - m.add_class::()?; - m.add_class::()?; - m.add_class::()?; - m.add_class::()?; - m.add_class::()?; m.add_class::()?; m.add_class::()?; m.add_class::()?; diff --git a/qiskit/__init__.py b/qiskit/__init__.py index fce544333478..4326da36f7a0 100644 --- a/qiskit/__init__.py +++ b/qiskit/__init__.py @@ -60,6 +60,7 @@ # We manually define them on import so people can directly import qiskit._accelerate.* submodules # and not have to rely on attribute access. No action needed for top-level extension packages. sys.modules["qiskit._accelerate.circuit"] = _accelerate.circuit +sys.modules["qiskit._accelerate.circuit.equivalence"] = _accelerate.circuit.equivalence sys.modules["qiskit._accelerate.convert_2q_block_matrix"] = _accelerate.convert_2q_block_matrix sys.modules["qiskit._accelerate.dense_layout"] = _accelerate.dense_layout sys.modules["qiskit._accelerate.error_map"] = _accelerate.error_map diff --git a/qiskit/circuit/equivalence.py b/qiskit/circuit/equivalence.py index 28ce5a4b92dd..418ab231e66d 100644 --- a/qiskit/circuit/equivalence.py +++ b/qiskit/circuit/equivalence.py @@ -23,7 +23,13 @@ from .exceptions import CircuitError from .parameter import Parameter from .parameterexpression import ParameterExpression -from qiskit._accelerate.circuit import BaseEquivalenceLibrary, Key, Equivalence, NodeData, EdgeData +from qiskit._accelerate.circuit.equivalence import ( + BaseEquivalenceLibrary, + Key, + Equivalence, + NodeData, + EdgeData, +) class EquivalenceLibrary(BaseEquivalenceLibrary): diff --git a/qiskit/transpiler/passes/basis/basis_translator.py b/qiskit/transpiler/passes/basis/basis_translator.py index bcac1f5d4faf..4df901f90408 100644 --- a/qiskit/transpiler/passes/basis/basis_translator.py +++ b/qiskit/transpiler/passes/basis/basis_translator.py @@ -32,7 +32,7 @@ ) from qiskit.dagcircuit import DAGCircuit from qiskit.converters import circuit_to_dag, dag_to_circuit -from qiskit.circuit.equivalence import Key, NodeData +from qiskit.circuit.equivalence import Key, NodeData, Equivalence from qiskit.transpiler.basepasses import TransformationPass from qiskit.transpiler.exceptions import TranspilerError @@ -541,7 +541,7 @@ def _basis_search(equiv_lib, source_basis, target_basis): logger.debug("Begining basis search from %s to %s.", source_basis, target_basis) source_basis = { - (gate_name, gate_num_qubits) + Key(gate_name, gate_num_qubits) for gate_name, gate_num_qubits in source_basis if gate_name not in target_basis } @@ -559,7 +559,12 @@ def _basis_search(equiv_lib, source_basis, target_basis): # we add a dummy node and connect it with gates in the target basis. # we'll start the search from this dummy node. - dummy = graph.add_node(NodeData(key="key", equivs=[("dummy starting node", 0)])) + dummy = graph.add_node( + NodeData( + key=Key("key", 0), + equivs=[Equivalence([], QuantumCircuit(0, name="dummy starting node"))], + ) + ) try: graph.add_edges_from_no_data( From 1bf0316cf7560deb3965ffecf3fd0e60b208b7bc Mon Sep 17 00:00:00 2001 From: Raynel Sanchez <87539502+raynelfss@users.noreply.github.com> Date: Fri, 14 Jun 2024 23:01:20 +0200 Subject: [PATCH 08/45] Add: Default methods to equivalence data structures. --- crates/circuit/src/equivalence.rs | 55 +++++++++++++++++++++++++------ 1 file changed, 45 insertions(+), 10 deletions(-) diff --git a/crates/circuit/src/equivalence.rs b/crates/circuit/src/equivalence.rs index 303a7da06b27..68b945535765 100644 --- a/crates/circuit/src/equivalence.rs +++ b/crates/circuit/src/equivalence.rs @@ -88,7 +88,7 @@ pub struct Key { #[pymethods] impl Key { #[new] - #[pyo3(signature = (name, num_qubits))] + #[pyo3(signature = (name="".to_string(), num_qubits=0))] fn new(name: String, num_qubits: usize) -> Self { Self { name, num_qubits } } @@ -127,8 +127,17 @@ impl Display for Key { } } +impl Default for Key { + fn default() -> Self { + Self { + name: "".to_string(), + num_qubits: 0, + } + } +} + #[pyclass(sequence, module = "qiskit._accelerate.circuit.equivalence")] -#[derive(Debug, Clone, PartialEq)] +#[derive(Debug, Clone, PartialEq, Default)] pub struct Equivalence { #[pyo3(get)] pub params: Vec, @@ -139,7 +148,7 @@ pub struct Equivalence { #[pymethods] impl Equivalence { #[new] - #[pyo3(signature = (params, circuit))] + #[pyo3(signature = (params=vec![], circuit=CircuitRep::default()))] fn new(params: Vec, circuit: CircuitRep) -> Self { Self { circuit, params } } @@ -166,14 +175,14 @@ impl Display for Equivalence { fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { write!( f, - "Equivalence(params={:?}, circuits={:?})", + "Equivalence(params={:?}, circuit={})", self.params, self.circuit ) } } #[pyclass(sequence, module = "qiskit._accelerate.circuit.equivalence")] -#[derive(Debug, Clone, PartialEq)] +#[derive(Debug, Clone, PartialEq, Default)] pub struct NodeData { #[pyo3(get)] key: Key, @@ -184,7 +193,7 @@ pub struct NodeData { #[pymethods] impl NodeData { #[new] - #[pyo3(signature = (key, equivs))] + #[pyo3(signature = (key=Key::default(), equivs=vec![]))] fn new(key: Key, equivs: Vec) -> Self { Self { key, equivs } } @@ -219,7 +228,7 @@ impl Display for NodeData { } #[pyclass(sequence, module = "qiskit._accelerate.circuit.equivalence")] -#[derive(Debug, Clone, PartialEq)] +#[derive(Debug, Clone, PartialEq, Default)] pub struct EdgeData { #[pyo3(get)] pub index: usize, @@ -234,7 +243,7 @@ pub struct EdgeData { #[pymethods] impl EdgeData { #[new] - #[pyo3(signature = (index, num_gates, rule, source))] + #[pyo3(signature = (index=0, num_gates=0, rule=Equivalence::default(), source=Key::default()))] fn new(index: usize, num_gates: usize, rule: Equivalence, source: Key) -> Self { Self { index, @@ -457,6 +466,19 @@ impl FromPyObject<'_> for CircuitRep { } } +impl Display for CircuitRep { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + let py_rep_str = Python::with_gil(|py| -> PyResult { + match self.object.call_method0(py, "__repr__") { + Ok(str_obj) => str_obj.extract::(py), + Err(_) => Ok("None".to_owned()), + } + }) + .unwrap(); + write!(f, "{}", py_rep_str) + } +} + impl PartialEq for CircuitRep { fn eq(&self, other: &Self) -> bool { self.object.is(&other.object) @@ -469,6 +491,19 @@ impl IntoPy for CircuitRep { } } +impl Default for CircuitRep { + fn default() -> Self { + Self { + object: Python::with_gil(|py| py.None()), + num_qubits: None, + num_clbits: None, + label: None, + params: vec![], + data: vec![], + } + } +} + // Temporary Representation of CircuitInstruction #[derive(Debug, Clone)] pub struct CircuitInstructionRep { @@ -661,9 +696,9 @@ impl EquivalenceLibrary { slf.key_to_node_index = state .get_item("key_to_node_index")? .unwrap() - .extract::>()? + .extract::>()? .into_iter() - .map(|(key, val)| (key, NodeIndex::new(val))) + .map(|((name, num_qubits), val)| (Key::new(name, num_qubits), NodeIndex::new(val))) .collect(); let graph_nodes: Vec = state.get_item("graph_nodes")?.unwrap().extract()?; let graph_edges: Vec<(usize, usize, EdgeData)> = From ab57e3e2a68ac2a7532d6b73e0511f9501ed8f67 Mon Sep 17 00:00:00 2001 From: Raynel Sanchez <87539502+raynelfss@users.noreply.github.com> Date: Sat, 15 Jun 2024 00:17:54 +0200 Subject: [PATCH 09/45] Fix: Adapt to new Gate Structure --- crates/circuit/src/circuit_instruction.rs | 13 +- crates/circuit/src/equivalence.rs | 345 +++++++--------------- crates/circuit/src/imports.rs | 2 +- crates/circuit/src/operations.rs | 23 ++ 4 files changed, 132 insertions(+), 251 deletions(-) diff --git a/crates/circuit/src/circuit_instruction.rs b/crates/circuit/src/circuit_instruction.rs index 2bb90367082d..122a3cd47ce6 100644 --- a/crates/circuit/src/circuit_instruction.rs +++ b/crates/circuit/src/circuit_instruction.rs @@ -659,10 +659,7 @@ impl CircuitInstruction { /// Take a reference to a `CircuitInstruction` and convert the operation /// inside that to a python side object. -pub(crate) fn operation_type_to_py( - py: Python, - circuit_inst: &CircuitInstruction, -) -> PyResult { +pub fn operation_type_to_py(py: Python, circuit_inst: &CircuitInstruction) -> PyResult { let (label, duration, unit, condition) = match &circuit_inst.extra_attrs { None => (None, None, None, None), Some(extra_attrs) => ( @@ -688,7 +685,7 @@ pub(crate) fn operation_type_to_py( /// a Python side full-fat Qiskit operation as a PyObject. This is typically /// used by accessor functions that need to return an operation to Qiskit, such /// as accesing `CircuitInstruction.operation`. -pub(crate) fn operation_type_and_data_to_py( +pub fn operation_type_and_data_to_py( py: Python, operation: &OperationType, params: &[Param], @@ -727,8 +724,8 @@ pub(crate) fn operation_type_and_data_to_py( /// A container struct that contains the output from the Python object to /// conversion to construct a CircuitInstruction object -#[derive(Debug)] -pub(crate) struct OperationTypeConstruct { +#[derive(Debug, Clone)] +pub struct OperationTypeConstruct { pub operation: OperationType, pub params: SmallVec<[Param; 3]>, pub label: Option, @@ -740,7 +737,7 @@ pub(crate) struct OperationTypeConstruct { /// Convert an inbound Python object for a Qiskit operation and build a rust /// representation of that operation. This will map it to appropriate variant /// of operation type based on class -pub(crate) fn convert_py_to_operation_type( +pub fn convert_py_to_operation_type( py: Python, py_op: PyObject, ) -> PyResult { diff --git a/crates/circuit/src/equivalence.rs b/crates/circuit/src/equivalence.rs index 68b945535765..a22feb512bfe 100644 --- a/crates/circuit/src/equivalence.rs +++ b/crates/circuit/src/equivalence.rs @@ -11,13 +11,13 @@ // that they have been altered from the originals. use itertools::Itertools; +use smallvec::{smallvec, SmallVec}; use std::collections::hash_map::DefaultHasher; use std::hash::{Hash, Hasher}; use std::{error::Error, fmt::Display}; use exceptions::CircuitError; use hashbrown::{HashMap, HashSet}; -use pyo3::sync::GILOnceCell; use pyo3::types::PyDict; use pyo3::{prelude::*, types::IntoPyDict}; @@ -26,52 +26,18 @@ use rustworkx_core::petgraph::{ visit::EdgeRef, }; +use crate::circuit_instruction::{ + convert_py_to_operation_type, CircuitInstruction as CircuitInstructionBase, + OperationTypeConstruct, +}; +use crate::imports::ImportOnceCell; +use crate::operations::Param; +use crate::operations::{Operation, OperationType}; + mod exceptions { use pyo3::import_exception_bound; import_exception_bound! {qiskit.circuit.exceptions, CircuitError} } - -/// Helper wrapper around `GILOnceCell` instances that are just intended to store a Python object -/// that is lazily imported. -pub struct ImportOnceCell { - module: &'static str, - object: &'static str, - cell: GILOnceCell>, -} - -impl ImportOnceCell { - const fn new(module: &'static str, object: &'static str) -> Self { - Self { - module, - object, - cell: GILOnceCell::new(), - } - } - - /// Get the underlying GIL-independent reference to the contained object, importing if - /// required. - #[inline] - pub fn get(&self, py: Python) -> &Py { - self.cell.get_or_init(py, || { - py.import_bound(self.module) - .unwrap() - .getattr(self.object) - .unwrap() - .unbind() - }) - } - - /// Get a GIL-bound reference to the contained object, importing if required. - #[inline] - pub fn get_bound<'py>(&self, py: Python<'py>) -> &Bound<'py, PyAny> { - self.get(py).bind(py) - } -} - -pub static PARAMETER_EXPRESSION: ImportOnceCell = - ImportOnceCell::new("qiskit.circuit.parameterexpression", "ParameterExpression"); -pub static QUANTUM_CIRCUIT: ImportOnceCell = - ImportOnceCell::new("qiskit.circuit.quantumcircuit", "QuantumCircuit"); pub static PYDIGRAPH: ImportOnceCell = ImportOnceCell::new("rustworkx", "PyDiGraph"); // Custom Structs @@ -82,14 +48,14 @@ pub struct Key { #[pyo3(get)] pub name: String, #[pyo3(get)] - pub num_qubits: usize, + pub num_qubits: u32, } #[pymethods] impl Key { #[new] #[pyo3(signature = (name="".to_string(), num_qubits=0))] - fn new(name: String, num_qubits: usize) -> Self { + fn new(name: String, num_qubits: u32) -> Self { Self { name, num_qubits } } @@ -107,11 +73,11 @@ impl Key { slf.to_string() } - fn __getstate__(slf: PyRef) -> (String, usize) { + fn __getstate__(slf: PyRef) -> (String, u32) { (slf.name.to_owned(), slf.num_qubits) } - fn __setstate__(mut slf: PyRefMut, state: (String, usize)) { + fn __setstate__(mut slf: PyRefMut, state: (String, u32)) { slf.name = state.0; slf.num_qubits = state.1; } @@ -140,7 +106,7 @@ impl Default for Key { #[derive(Debug, Clone, PartialEq, Default)] pub struct Equivalence { #[pyo3(get)] - pub params: Vec, + pub params: SmallVec<[Param; 3]>, #[pyo3(get)] pub circuit: CircuitRep, } @@ -148,8 +114,8 @@ pub struct Equivalence { #[pymethods] impl Equivalence { #[new] - #[pyo3(signature = (params=vec![], circuit=CircuitRep::default()))] - fn new(params: Vec, circuit: CircuitRep) -> Self { + #[pyo3(signature = (params=smallvec![], circuit=CircuitRep::default()))] + fn new(params: SmallVec<[Param; 3]>, circuit: CircuitRep) -> Self { Self { circuit, params } } @@ -161,11 +127,11 @@ impl Equivalence { self.eq(&other) } - fn __getstate__(slf: PyRef) -> (Vec, CircuitRep) { + fn __getstate__(slf: PyRef) -> (SmallVec<[Param; 3]>, CircuitRep) { (slf.params.to_owned(), slf.circuit.to_owned()) } - fn __setstate__(mut slf: PyRefMut, state: (Vec, CircuitRep)) { + fn __setstate__(mut slf: PyRefMut, state: (SmallVec<[Param; 3]>, CircuitRep)) { slf.params = state.0; slf.circuit = state.1; } @@ -288,135 +254,40 @@ impl Display for EdgeData { } } -// REPRESENTATIONS of non rust objects -// Temporary definition of Parameter -#[derive(Clone, Debug)] -pub enum Param { - ParameterExpression(ParamExpRep), - Float(f64), - Obj(PyObject), -} - -impl<'py> FromPyObject<'py> for Param { - fn extract_bound(b: &Bound<'py, PyAny>) -> Result { - Ok( - if b.is_instance(PARAMETER_EXPRESSION.get_bound(b.py()))? - || b.is_instance(QUANTUM_CIRCUIT.get_bound(b.py()))? - { - let id = b.getattr("_uuid")?.getattr("hex")?.extract::()?; - Param::ParameterExpression(ParamExpRep { - id, - object: b.clone().unbind(), - }) - } else if let Ok(val) = b.extract::() { - Param::Float(val) - } else { - Param::Obj(b.clone().unbind()) - }, - ) - } -} - -impl IntoPy for Param { - fn into_py(self, py: Python) -> PyObject { - match &self { - Self::Float(val) => val.to_object(py), - Self::ParameterExpression(val) => val.object.clone_ref(py), - Self::Obj(val) => val.clone_ref(py), - } - } +// Enum to extract circuit instructions more broadly +#[derive(Debug, Clone)] +pub enum CircuitInstruction { + Instruction(CircuitInstructionBase), + OperationTypeConstruct(OperationTypeConstruct), } -impl ToPyObject for Param { - fn to_object(&self, py: Python) -> PyObject { +impl CircuitInstruction { + #[inline] + pub fn operation(&self) -> &OperationType { match self { - Self::Float(val) => val.to_object(py), - Self::ParameterExpression(val) => val.object.clone_ref(py), - Self::Obj(val) => val.clone_ref(py), + CircuitInstruction::Instruction(instruction) => &instruction.operation, + CircuitInstruction::OperationTypeConstruct(operation) => &operation.operation, } } -} -impl Param { - fn compare(one: &PyObject, other: &PyObject) -> bool { - Python::with_gil(|py| -> PyResult { - let other_bound = other.bind(py); - other_bound.eq(one) - }) - .unwrap_or_default() - } -} - -impl PartialEq for Param { - fn eq(&self, other: &Self) -> bool { - match (self, other) { - (Param::Float(s), Param::Float(other)) => s == other, - (Param::Float(_), Param::ParameterExpression(_)) => false, - (Param::ParameterExpression(_), Param::Float(_)) => false, - (Param::ParameterExpression(s), Param::ParameterExpression(other)) => s.id == other.id, - (Param::ParameterExpression(_), Param::Obj(_)) => false, - (Param::Float(_), Param::Obj(_)) => false, - (Param::Obj(_), Param::ParameterExpression(_)) => false, - (Param::Obj(_), Param::Float(_)) => false, - (Param::Obj(one), Param::Obj(other)) => Self::compare(one, other), + #[inline] + pub fn params(&self) -> &[Param] { + match self { + CircuitInstruction::Instruction(instruction) => &instruction.params, + CircuitInstruction::OperationTypeConstruct(operation) => &operation.params, } } } -/// ParamExpIdentifier -#[derive(Debug, Clone)] -pub struct ParamExpRep { - id: String, - object: PyObject, -} - -/// Temporary interpretation of Gate -#[derive(Debug, Clone)] -pub struct GateRep { - object: PyObject, - pub num_qubits: Option, - pub num_clbits: Option, - pub name: Option, - pub label: Option, - pub params: Vec, -} - -impl FromPyObject<'_> for GateRep { - fn extract(ob: &'_ PyAny) -> PyResult { - let num_qubits = match ob.getattr("num_qubits") { - Ok(num_qubits) => num_qubits.extract::().ok(), - Err(_) => None, - }; - let num_clbits = match ob.getattr("num_clbits") { - Ok(num_clbits) => num_clbits.extract::().ok(), - Err(_) => None, - }; - let name = match ob.getattr("name") { - Ok(name) => name.extract::().ok(), - Err(_) => None, - }; - let label = match ob.getattr("label") { - Ok(label) => label.extract::().ok(), - Err(_) => None, - }; - let params = match ob.getattr("params") { - Ok(params) => params.extract::>().ok(), - Err(_) => Some(vec![]), +impl<'py> FromPyObject<'py> for CircuitInstruction { + fn extract(ob: &'py PyAny) -> PyResult { + match ob.extract::() { + Ok(inst) => Ok(Self::Instruction(inst)), + Err(_) => Ok(Self::OperationTypeConstruct(convert_py_to_operation_type( + ob.py(), + ob.into(), + )?)), } - .unwrap_or_default(); - Ok(Self { - object: ob.into(), - num_qubits, - num_clbits, - name, - label, - params, - }) - } -} -impl IntoPy for GateRep { - fn into_py(self, _py: Python<'_>) -> PyObject { - self.object } } @@ -424,23 +295,25 @@ impl IntoPy for GateRep { #[derive(Debug, Clone)] pub struct CircuitRep { object: PyObject, - pub num_qubits: Option, - pub num_clbits: Option, + pub num_qubits: u32, + pub num_clbits: u32, pub label: Option, - pub params: Vec, - pub data: Vec, + pub params: SmallVec<[Param; 3]>, + pub data: Vec, } impl FromPyObject<'_> for CircuitRep { fn extract(ob: &'_ PyAny) -> PyResult { let num_qubits = match ob.getattr("num_qubits") { - Ok(num_qubits) => num_qubits.extract::().ok(), + Ok(num_qubits) => num_qubits.extract::().ok(), Err(_) => None, - }; + } + .unwrap_or_default(); let num_clbits = match ob.getattr("num_clbits") { - Ok(num_clbits) => num_clbits.extract::().ok(), + Ok(num_clbits) => num_clbits.extract::().ok(), Err(_) => None, - }; + } + .unwrap_or_default(); let label = match ob.getattr("label") { Ok(label) => label.extract::().ok(), Err(_) => None, @@ -448,10 +321,10 @@ impl FromPyObject<'_> for CircuitRep { let params = ob .getattr("parameters")? .getattr("data")? - .extract::>() + .extract::>() .unwrap_or_default(); let data = match ob.getattr("data") { - Ok(data) => data.extract::>().ok(), + Ok(data) => data.extract::>().ok(), Err(_) => Some(vec![]), } .unwrap_or_default(); @@ -495,30 +368,15 @@ impl Default for CircuitRep { fn default() -> Self { Self { object: Python::with_gil(|py| py.None()), - num_qubits: None, - num_clbits: None, + num_qubits: 0, + num_clbits: 0, label: None, - params: vec![], + params: smallvec![], data: vec![], } } } -// Temporary Representation of CircuitInstruction -#[derive(Debug, Clone)] -pub struct CircuitInstructionRep { - operation: GateRep, - qubits: Vec, -} - -impl FromPyObject<'_> for CircuitInstructionRep { - fn extract(ob: &'_ PyAny) -> PyResult { - let qubits = ob.getattr("qubits")?.extract::>()?; - let operation = ob.getattr("operation")?.extract::()?; - Ok(Self { operation, qubits }) - } -} - // Custom Types type GraphType = DiGraph; type KTIType = HashMap; @@ -576,7 +434,11 @@ impl EquivalenceLibrary { /// gate (Gate): A Gate instance. /// equivalent_circuit (QuantumCircuit): A circuit equivalently /// implementing the given Gate. - fn add_equivalence(&mut self, gate: GateRep, equivalent_circuit: CircuitRep) -> PyResult<()> { + fn add_equivalence( + &mut self, + gate: CircuitInstruction, + equivalent_circuit: CircuitRep, + ) -> PyResult<()> { match self.add_equiv(gate, equivalent_circuit) { Ok(_) => Ok(()), Err(e) => Err(CircuitError::new_err(e.message)), @@ -591,10 +453,10 @@ impl EquivalenceLibrary { /// Returns: /// Bool: True if gate has a known decomposition in the library. /// False otherwise. - pub fn has_entry(&self, gate: GateRep) -> bool { + pub fn has_entry(&self, gate: CircuitInstruction) -> bool { let key = Key { - name: gate.name.unwrap_or_default(), - num_qubits: gate.num_qubits.unwrap_or_default(), + name: gate.operation().name().to_string(), + num_qubits: gate.operation().num_qubits(), }; self.key_to_node_index.contains_key(&key) } @@ -610,7 +472,7 @@ impl EquivalenceLibrary { /// gate (Gate): A Gate instance. /// entry (List['QuantumCircuit']) : A list of QuantumCircuits, each /// equivalently implementing the given Gate. - fn set_entry(&mut self, gate: GateRep, entry: Vec) -> PyResult<()> { + fn set_entry(&mut self, gate: CircuitInstruction, entry: Vec) -> PyResult<()> { match self.set_entry_native(&gate, &entry) { Ok(_) => Ok(()), Err(e) => Err(CircuitError::new_err(e.message)), @@ -634,16 +496,16 @@ impl EquivalenceLibrary { /// the library, from earliest to latest, from top to base. The /// ordering of the StandardEquivalenceLibrary will not generally be /// consistent across Qiskit versions. - pub fn get_entry(&self, py: Python<'_>, gate: GateRep) -> Vec { + pub fn get_entry(&self, py: Python<'_>, gate: CircuitInstruction) -> Vec { let key = Key { - name: gate.name.unwrap_or_default(), - num_qubits: gate.num_qubits.unwrap_or_default(), + name: gate.operation().name().to_string(), + num_qubits: gate.operation().num_qubits(), }; - let query_params = gate.params; + let query_params = gate.params(); self.get_equivalences(&key) .into_iter() - .filter_map(|equivalence| rebind_equiv(py, equivalence, &query_params).ok()) + .filter_map(|equivalence| rebind_equiv(py, equivalence, query_params).ok()) .collect() } @@ -668,7 +530,7 @@ impl EquivalenceLibrary { fn __getstate__(slf: PyRef) -> PyResult> { let ret = PyDict::new_bound(slf.py()); ret.set_item("rule_id", slf.rule_id)?; - let key_to_usize_node: HashMap<(String, usize), usize> = HashMap::from_iter( + let key_to_usize_node: HashMap<(String, u32), usize> = HashMap::from_iter( slf.key_to_node_index .iter() .map(|(key, val)| ((key.name.to_string(), key.num_qubits), val.index())), @@ -696,7 +558,7 @@ impl EquivalenceLibrary { slf.key_to_node_index = state .get_item("key_to_node_index")? .unwrap() - .extract::>()? + .extract::>()? .into_iter() .map(|((name, num_qubits), val)| (Key::new(name, num_qubits), NodeIndex::new(val))) .collect(); @@ -758,18 +620,18 @@ impl EquivalenceLibrary { /// implementing the given Gate. pub fn add_equiv( &mut self, - gate: GateRep, + gate: CircuitInstruction, equivalent_circuit: CircuitRep, ) -> Result<(), EquivalenceError> { raise_if_shape_mismatch(&gate, &equivalent_circuit)?; - raise_if_param_mismatch(&gate.params, &equivalent_circuit.params)?; + raise_if_param_mismatch(gate.params(), &equivalent_circuit.params)?; let key: Key = Key { - name: gate.name.unwrap_or_default(), - num_qubits: gate.num_qubits.unwrap_or_default(), + name: gate.operation().name().to_string(), + num_qubits: gate.operation().num_qubits(), }; let equiv = Equivalence { - params: gate.params, + params: gate.params().into(), circuit: equivalent_circuit.to_owned(), }; @@ -779,8 +641,8 @@ impl EquivalenceLibrary { } let sources: HashSet = HashSet::from_iter(equivalent_circuit.data.iter().map(|inst| Key { - name: inst.operation.name.to_owned().unwrap_or_default(), - num_qubits: inst.qubits.len(), + name: inst.operation().name().to_owned(), + num_qubits: inst.operation().num_qubits(), })); let edges = Vec::from_iter(sources.iter().map(|source| { ( @@ -816,17 +678,17 @@ impl EquivalenceLibrary { /// equivalently implementing the given Gate. pub fn set_entry_native( &mut self, - gate: &GateRep, + gate: &CircuitInstruction, entry: &Vec, ) -> Result<(), EquivalenceError> { for equiv in entry { raise_if_shape_mismatch(gate, equiv)?; - raise_if_param_mismatch(&gate.params, &equiv.params)?; + raise_if_param_mismatch(gate.params(), &equiv.params)?; } let key = Key { - name: gate.name.to_owned().unwrap_or_default(), - num_qubits: gate.num_qubits.unwrap_or_default(), + name: gate.operation().name().to_owned(), + num_qubits: gate.operation().num_qubits(), }; let node_index = self.set_default_node(key); @@ -863,23 +725,17 @@ fn raise_if_param_mismatch( gate_params: &[Param], circuit_parameters: &[Param], ) -> Result<(), EquivalenceError> { - let uid_gate_parameters: HashSet = gate_params + let gate_params = gate_params .iter() - .filter_map(|x| match x { - Param::ParameterExpression(param) => Some(param.id.to_string()), - Param::Float(_) => None, - Param::Obj(_) => None, - }) - .collect(); - let uid_circuit_parameters: HashSet = circuit_parameters + .filter(|param| matches!(param, Param::ParameterExpression(_))) + .collect_vec(); + let circuit_parameters = circuit_parameters .iter() - .filter_map(|x| match x { - Param::ParameterExpression(param) => Some(param.id.to_string()), - Param::Float(_) => None, - Param::Obj(_) => None, - }) - .collect(); - if uid_gate_parameters != uid_circuit_parameters { + .filter(|param| matches!(param, Param::ParameterExpression(_))) + .collect_vec(); + if gate_params.len() == circuit_parameters.len() + && gate_params.iter().any(|x| !circuit_parameters.contains(x)) + { return Err(EquivalenceError::new_err(format!( "Cannot add equivalence between circuit and gate \ of different parameters. Gate params: {:#?}. \ @@ -890,16 +746,21 @@ fn raise_if_param_mismatch( Ok(()) } -fn raise_if_shape_mismatch(gate: &GateRep, circuit: &CircuitRep) -> Result<(), EquivalenceError> { - if gate.num_qubits != circuit.num_qubits || gate.num_clbits != circuit.num_clbits { +fn raise_if_shape_mismatch( + gate: &CircuitInstruction, + circuit: &CircuitRep, +) -> Result<(), EquivalenceError> { + if gate.operation().num_qubits() != circuit.num_qubits + || gate.operation().num_clbits() != circuit.num_clbits + { return Err(EquivalenceError::new_err(format!( "Cannot add equivalence between circuit and gate \ of different shapes. Gate: {} qubits and {} clbits. \ Circuit: {} qubits and {} clbits.", - gate.num_qubits.unwrap_or_default(), - gate.num_clbits.unwrap_or_default(), - circuit.num_qubits.unwrap_or_default(), - circuit.num_clbits.unwrap_or_default() + gate.operation().num_qubits(), + gate.operation().num_clbits(), + circuit.num_qubits, + circuit.num_clbits ))); } Ok(()) diff --git a/crates/circuit/src/imports.rs b/crates/circuit/src/imports.rs index 050f7f2e053c..a759a3d7b5ae 100644 --- a/crates/circuit/src/imports.rs +++ b/crates/circuit/src/imports.rs @@ -28,7 +28,7 @@ pub struct ImportOnceCell { } impl ImportOnceCell { - const fn new(module: &'static str, object: &'static str) -> Self { + pub const fn new(module: &'static str, object: &'static str) -> Self { Self { module, object, diff --git a/crates/circuit/src/operations.rs b/crates/circuit/src/operations.rs index ead1b8ee1ebb..eaed5402f24d 100644 --- a/crates/circuit/src/operations.rs +++ b/crates/circuit/src/operations.rs @@ -173,6 +173,29 @@ impl ToPyObject for Param { } } +impl Param { + fn compare(one: &PyObject, other: &PyObject) -> bool { + Python::with_gil(|py| -> PyResult { + let other_bound = other.bind(py); + Ok(other_bound.eq(one)? || other_bound.is(one)) + }) + .unwrap_or_default() + } +} + +impl PartialEq for Param { + fn eq(&self, other: &Self) -> bool { + match (self, other) { + (Param::Float(s), Param::Float(other)) => s == other, + (Param::ParameterExpression(one), Param::ParameterExpression(other)) => { + Self::compare(one, other) + } + (Param::Obj(one), Param::Obj(other)) => Self::compare(one, other), + _ => false, + } + } +} + #[derive(Clone, Debug, Copy, Eq, PartialEq, Hash)] #[pyclass(module = "qiskit._accelerate.circuit")] pub enum StandardGate { From 0e822b506ea2c7962e25f06aff40cae529976c15 Mon Sep 17 00:00:00 2001 From: Raynel Sanchez <87539502+raynelfss@users.noreply.github.com> Date: Sat, 15 Jun 2024 22:27:24 +0200 Subject: [PATCH 10/45] Fix: Erroneous display of `Parameters` --- crates/circuit/src/equivalence.rs | 11 ++++++++--- crates/circuit/src/operations.rs | 14 ++++++++++++++ 2 files changed, 22 insertions(+), 3 deletions(-) diff --git a/crates/circuit/src/equivalence.rs b/crates/circuit/src/equivalence.rs index a22feb512bfe..9d357b547c5c 100644 --- a/crates/circuit/src/equivalence.rs +++ b/crates/circuit/src/equivalence.rs @@ -141,8 +141,9 @@ impl Display for Equivalence { fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { write!( f, - "Equivalence(params={:?}, circuit={})", - self.params, self.circuit + "Equivalence(params=[{}], circuit={})", + self.params.iter().format(", "), + self.circuit ) } } @@ -354,7 +355,11 @@ impl Display for CircuitRep { impl PartialEq for CircuitRep { fn eq(&self, other: &Self) -> bool { - self.object.is(&other.object) + Python::with_gil(|py| -> PyResult { + let bound = other.object.bind(py); + bound.eq(&self.object) + }) + .unwrap_or_default() } } diff --git a/crates/circuit/src/operations.rs b/crates/circuit/src/operations.rs index eaed5402f24d..a1cec9c617bb 100644 --- a/crates/circuit/src/operations.rs +++ b/crates/circuit/src/operations.rs @@ -196,6 +196,20 @@ impl PartialEq for Param { } } +impl std::fmt::Display for Param { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + let display_name: String = Python::with_gil(|py| -> PyResult { + match self { + Param::ParameterExpression(obj) => obj.call_method0(py, "__repr__")?.extract(py), + Param::Float(float_param) => Ok(format!("Parameter({})", float_param)), + Param::Obj(obj) => obj.call_method0(py, "__repr__")?.extract(py), + } + }) + .unwrap_or("None".to_owned()); + write!(f, "{}", display_name) + } +} + #[derive(Clone, Debug, Copy, Eq, PartialEq, Hash)] #[pyclass(module = "qiskit._accelerate.circuit")] pub enum StandardGate { From 6a033c935893ee978e61a53860ed05a84df2674f Mon Sep 17 00:00:00 2001 From: Raynel Sanchez <87539502+raynelfss@users.noreply.github.com> Date: Mon, 17 Jun 2024 09:18:36 -0400 Subject: [PATCH 11/45] Format: Fix lint test --- qiskit/circuit/equivalence.py | 8 +------- 1 file changed, 1 insertion(+), 7 deletions(-) diff --git a/qiskit/circuit/equivalence.py b/qiskit/circuit/equivalence.py index 418ab231e66d..8730771f4feb 100644 --- a/qiskit/circuit/equivalence.py +++ b/qiskit/circuit/equivalence.py @@ -12,18 +12,12 @@ """Gate equivalence library.""" -import copy -from collections import namedtuple - from rustworkx.visualization import graphviz_draw import rustworkx as rx from qiskit.exceptions import InvalidFileError -from .exceptions import CircuitError -from .parameter import Parameter -from .parameterexpression import ParameterExpression -from qiskit._accelerate.circuit.equivalence import ( +from qiskit._accelerate.circuit.equivalence import ( # pylint: disable=unused-import BaseEquivalenceLibrary, Key, Equivalence, From b04cae2ed9a64175bfdd4d1cfeab5f1d320550d2 Mon Sep 17 00:00:00 2001 From: Raynel Sanchez <87539502+raynelfss@users.noreply.github.com> Date: Mon, 17 Jun 2024 18:06:12 -0400 Subject: [PATCH 12/45] Fix: Use EdgeReferences instead of edge_indices. - Remove stray comment. - Use match instead of matches!. --- crates/circuit/src/equivalence.rs | 22 ++++++---------------- 1 file changed, 6 insertions(+), 16 deletions(-) diff --git a/crates/circuit/src/equivalence.rs b/crates/circuit/src/equivalence.rs index 9d357b547c5c..14fddca6a3b2 100644 --- a/crates/circuit/src/equivalence.rs +++ b/crates/circuit/src/equivalence.rs @@ -600,13 +600,6 @@ impl EquivalenceLibrary { self.key_to_node_index.insert(key, node); node } - // *self - // .key_to_node_index - // .entry(key.to_owned()) - // .or_insert(self._graph.add_node(NodeData { - // key, - // equivs: vec![], - // })) } /// Rust native equivalent to `EquivalenceLibrary.add_equivalence()` @@ -779,12 +772,9 @@ fn rebind_equiv( let (equiv_params, equiv_circuit) = (equiv.params, equiv.circuit); let param_map: Vec<(Param, Param)> = equiv_params .into_iter() - .filter_map(|param| { - if matches!(param, Param::ParameterExpression(_)) { - Some(param) - } else { - None - } + .filter_map(|param| match param { + Param::ParameterExpression(_) => Some(param), + _ => None, }) .zip(query_params.iter().cloned()) .collect(); @@ -828,10 +818,10 @@ where for node in node_weights { graph.call_method1("add_node", (node.to_owned(),))?; } - let edge_weights = pet_graph.edge_indices().map(|edge| { + let edge_weights = pet_graph.edge_references().map(|edge| { ( - pet_graph.edge_endpoints(edge).unwrap(), - pet_graph.edge_weight(edge).unwrap(), + pet_graph.edge_endpoints(edge.id()).unwrap(), + pet_graph.edge_weight(edge.id()).unwrap(), ) }); for ((source, target), weight) in edge_weights { From 38436cdb5ed96b5f52a11dd557fafcaf7384bc39 Mon Sep 17 00:00:00 2001 From: Raynel Sanchez <87539502+raynelfss@users.noreply.github.com> Date: Fri, 21 Jun 2024 11:57:14 -0400 Subject: [PATCH 13/45] Fix: Use StableDiGraph for more stable indexing. - Remove required py argument for get_entry. - Reformat `to_pygraph` to use `add_nodes_from` and `add_edges_from`. - Other small fixes. --- crates/circuit/src/equivalence.rs | 86 +++++++++++++++---------------- 1 file changed, 43 insertions(+), 43 deletions(-) diff --git a/crates/circuit/src/equivalence.rs b/crates/circuit/src/equivalence.rs index 14fddca6a3b2..cad527f62cc4 100644 --- a/crates/circuit/src/equivalence.rs +++ b/crates/circuit/src/equivalence.rs @@ -11,6 +11,9 @@ // that they have been altered from the originals. use itertools::Itertools; +use rustworkx_core::petgraph::stable_graph::StableDiGraph; +use rustworkx_core::petgraph::visit::IntoEdgeReferences; + use smallvec::{smallvec, SmallVec}; use std::collections::hash_map::DefaultHasher; use std::hash::{Hash, Hasher}; @@ -22,7 +25,7 @@ use pyo3::types::PyDict; use pyo3::{prelude::*, types::IntoPyDict}; use rustworkx_core::petgraph::{ - graph::{DiGraph, EdgeIndex, NodeIndex}, + graph::{EdgeIndex, NodeIndex}, visit::EdgeRef, }; @@ -383,7 +386,7 @@ impl Default for CircuitRep { } // Custom Types -type GraphType = DiGraph; +type GraphType = StableDiGraph; type KTIType = HashMap; #[pyclass( @@ -501,7 +504,7 @@ impl EquivalenceLibrary { /// the library, from earliest to latest, from top to base. The /// ordering of the StandardEquivalenceLibrary will not generally be /// consistent across Qiskit versions. - pub fn get_entry(&self, py: Python<'_>, gate: CircuitInstruction) -> Vec { + pub fn get_entry(&self, gate: CircuitInstruction) -> Vec { let key = Key { name: gate.operation().name().to_string(), num_qubits: gate.operation().num_qubits(), @@ -510,7 +513,7 @@ impl EquivalenceLibrary { self.get_equivalences(&key) .into_iter() - .filter_map(|equivalence| rebind_equiv(py, equivalence, query_params).ok()) + .filter_map(|equivalence| rebind_equiv(equivalence, query_params)) .collect() } @@ -764,29 +767,29 @@ fn raise_if_shape_mismatch( Ok(()) } -fn rebind_equiv( - py: Python<'_>, - equiv: Equivalence, - query_params: &[Param], -) -> PyResult { - let (equiv_params, equiv_circuit) = (equiv.params, equiv.circuit); - let param_map: Vec<(Param, Param)> = equiv_params - .into_iter() - .filter_map(|param| match param { - Param::ParameterExpression(_) => Some(param), - _ => None, - }) - .zip(query_params.iter().cloned()) - .collect(); - let dict = param_map.as_slice().into_py_dict_bound(py); - let kwargs = [("inplace", false), ("flat_input", true)].into_py_dict_bound(py); - let new_equiv = - equiv_circuit - .object - .call_method_bound(py, "assign_parameters", (dict,), Some(&kwargs))?; - new_equiv.extract::(py) +fn rebind_equiv(equiv: Equivalence, query_params: &[Param]) -> Option { + Python::with_gil(|py| -> PyResult { + let (equiv_params, equiv_circuit) = (equiv.params, equiv.circuit); + let param_map: Vec<(Param, Param)> = equiv_params + .into_iter() + .filter_map(|param| match param { + Param::ParameterExpression(_) => Some(param), + _ => None, + }) + .zip(query_params.iter().cloned()) + .collect(); + let dict = param_map.as_slice().into_py_dict_bound(py); + let kwargs = [("inplace", false), ("flat_input", true)].into_py_dict_bound(py); + let new_equiv = equiv_circuit.object.call_method_bound( + py, + "assign_parameters", + (dict,), + Some(&kwargs), + )?; + new_equiv.extract::(py) + }) + .ok() } - // Errors #[derive(Debug, Clone)] @@ -808,28 +811,25 @@ impl Display for EquivalenceError { } } -fn to_pygraph(py: Python<'_>, pet_graph: &DiGraph) -> PyResult +fn to_pygraph(py: Python<'_>, pet_graph: &StableDiGraph) -> PyResult where N: IntoPy + Clone, E: IntoPy + Clone, { let graph = PYDIGRAPH.get_bound(py).call0()?; - let node_weights = pet_graph.node_weights(); - for node in node_weights { - graph.call_method1("add_node", (node.to_owned(),))?; - } - let edge_weights = pet_graph.edge_references().map(|edge| { - ( - pet_graph.edge_endpoints(edge.id()).unwrap(), - pet_graph.edge_weight(edge.id()).unwrap(), - ) - }); - for ((source, target), weight) in edge_weights { - graph.call_method1( - "add_edge", - (source.index(), target.index(), weight.to_owned()), - )?; - } + let node_weights: Vec = pet_graph.node_weights().cloned().collect(); + graph.call_method1("add_nodes_from", (node_weights,))?; + let edge_weights: Vec<(usize, usize, E)> = pet_graph + .edge_references() + .map(|edge| { + ( + edge.source().index(), + edge.target().index(), + edge.weight().to_owned(), + ) + }) + .collect(); + graph.call_method1("add_edges_from", (edge_weights,))?; Ok(graph.unbind()) } From e16e57bff4de30bc99b313b0cbd82570d68a4fd1 Mon Sep 17 00:00:00 2001 From: Raynel Sanchez <87539502+raynelfss@users.noreply.github.com> Date: Fri, 21 Jun 2024 15:50:05 -0400 Subject: [PATCH 14/45] Fix: Use `clone` instead of `to_owned` - Use `clone_ref` for the PyObject Graph instance. --- crates/circuit/src/equivalence.rs | 44 +++++++++++++++++-------------- 1 file changed, 24 insertions(+), 20 deletions(-) diff --git a/crates/circuit/src/equivalence.rs b/crates/circuit/src/equivalence.rs index 2492f55f142a..c243d64ac7d2 100644 --- a/crates/circuit/src/equivalence.rs +++ b/crates/circuit/src/equivalence.rs @@ -77,7 +77,7 @@ impl Key { } fn __getstate__(slf: PyRef) -> (String, u32) { - (slf.name.to_owned(), slf.num_qubits) + (slf.name.clone(), slf.num_qubits) } fn __setstate__(mut slf: PyRefMut, state: (String, u32)) { @@ -131,7 +131,7 @@ impl Equivalence { } fn __getstate__(slf: PyRef) -> (SmallVec<[Param; 3]>, CircuitRep) { - (slf.params.to_owned(), slf.circuit.to_owned()) + (slf.params.clone(), slf.circuit.clone()) } fn __setstate__(mut slf: PyRefMut, state: (SmallVec<[Param; 3]>, CircuitRep)) { @@ -177,7 +177,7 @@ impl NodeData { } fn __getstate__(slf: PyRef) -> (Key, Vec) { - (slf.key.to_owned(), slf.equivs.to_owned()) + (slf.key.clone(), slf.equivs.clone()) } fn __setstate__(mut slf: PyRefMut, state: (Key, Vec)) { @@ -235,8 +235,8 @@ impl EdgeData { ( slf.index, slf.num_gates, - slf.rule.to_owned(), - slf.source.to_owned(), + slf.rule.clone(), + slf.source.clone(), ) } @@ -348,7 +348,7 @@ impl Display for CircuitRep { let py_rep_str = Python::with_gil(|py| -> PyResult { match self.object.call_method0(py, "__repr__") { Ok(str_obj) => str_obj.extract::(py), - Err(_) => Ok("None".to_owned()), + Err(_) => Ok("None".to_string()), } }) .unwrap(); @@ -520,17 +520,21 @@ impl EquivalenceLibrary { #[getter] fn get_graph(&mut self, py: Python<'_>) -> PyResult { if let Some(graph) = &self.graph { - Ok(graph.to_owned()) + Ok(graph.clone_ref(py)) } else { self.graph = Some(to_pygraph(py, &self._graph)?); - Ok(self.graph.to_object(py)) + Ok(self + .graph + .as_ref() + .map(|graph| graph.clone_ref(py)) + .unwrap()) } } /// Get all the equivalences for the given key pub fn _get_equivalences(&self, key: &Key) -> Vec { if let Some(key_in) = self.key_to_node_index.get(key) { - self._graph[*key_in].equivs.to_owned() + self._graph[*key_in].equivs.clone() } else { vec![] } @@ -564,7 +568,7 @@ impl EquivalenceLibrary { slf._graph.edge_weight(edge_id).unwrap(), ) }) - .map(|((source, target), weight)| (source.index(), target.index(), weight.to_owned())) + .map(|((source, target), weight)| (source.index(), target.index(), weight.clone())) .collect_vec(); ret.set_item("graph_edges", graph_edges.into_py(slf.py()))?; Ok(ret) @@ -606,7 +610,7 @@ impl EquivalenceLibrary { *value } else { let node = self._graph.add_node(NodeData { - key: key.to_owned(), + key: key.clone(), equivs: vec![], }); self.key_to_node_index.insert(key, node); @@ -642,27 +646,27 @@ impl EquivalenceLibrary { }; let equiv = Equivalence { params: gate.params().into(), - circuit: equivalent_circuit.to_owned(), + circuit: equivalent_circuit.clone(), }; let target = self.set_default_node(key); if let Some(node) = self._graph.node_weight_mut(target) { - node.equivs.push(equiv.to_owned()); + node.equivs.push(equiv.clone()); } let sources: HashSet = HashSet::from_iter(equivalent_circuit.data.iter().map(|inst| Key { - name: inst.operation().name().to_owned(), + name: inst.operation().name().to_string(), num_qubits: inst.operation().num_qubits(), })); let edges = Vec::from_iter(sources.iter().map(|source| { ( - self.set_default_node(source.to_owned()), + self.set_default_node(source.clone()), target, EdgeData { index: self.rule_id, num_gates: sources.len(), - rule: equiv.to_owned(), - source: source.to_owned(), + rule: equiv.clone(), + source: source.clone(), }, ) })); @@ -697,7 +701,7 @@ impl EquivalenceLibrary { } let key = Key { - name: gate.operation().name().to_owned(), + name: gate.operation().name().to_string(), num_qubits: gate.operation().num_qubits(), }; let node_index = self.set_default_node(key); @@ -715,7 +719,7 @@ impl EquivalenceLibrary { self._graph.remove_edge(edge); } for equiv in entry { - self.add_equiv(gate.to_owned(), equiv.to_owned())? + self.add_equiv(gate.clone(), equiv.clone())? } self.graph = None; Ok(()) @@ -825,7 +829,7 @@ where ( edge.source().index(), edge.target().index(), - edge.weight().to_owned(), + edge.weight().clone(), ) }) .collect(); From fb1652c8a3305b26e1917661edd0a8869f51acd6 Mon Sep 17 00:00:00 2001 From: Raynel Sanchez <87539502+raynelfss@users.noreply.github.com> Date: Mon, 24 Jun 2024 17:07:27 -0400 Subject: [PATCH 15/45] Fix: Use `OperationTypeConstruct` instead of `CircuitInstruction` - Use `convert_py_to_operation_type` to correctly extract Gate instances into rust operational datatypes. - Add function `get_sources_from_circuit_rep` to not extract circuit data directly but only the necessary data. - Modify failing test due to different mapping. (!!) - Other tweaks and fixes. --- crates/circuit/src/equivalence.rs | 164 ++++++++---------- .../transpiler/test_basis_translator.py | 4 +- 2 files changed, 76 insertions(+), 92 deletions(-) diff --git a/crates/circuit/src/equivalence.rs b/crates/circuit/src/equivalence.rs index c243d64ac7d2..9c0d60aeb03b 100644 --- a/crates/circuit/src/equivalence.rs +++ b/crates/circuit/src/equivalence.rs @@ -11,6 +11,8 @@ // that they have been altered from the originals. use itertools::Itertools; + +use rustworkx_core::petgraph::csr::IndexType; use rustworkx_core::petgraph::stable_graph::StableDiGraph; use rustworkx_core::petgraph::visit::IntoEdgeReferences; @@ -21,7 +23,7 @@ use std::{error::Error, fmt::Display}; use exceptions::CircuitError; use hashbrown::{HashMap, HashSet}; -use pyo3::types::PyDict; +use pyo3::types::{PyDict, PyString}; use pyo3::{prelude::*, types::IntoPyDict}; use rustworkx_core::petgraph::{ @@ -29,10 +31,7 @@ use rustworkx_core::petgraph::{ visit::EdgeRef, }; -use crate::circuit_instruction::{ - convert_py_to_operation_type, CircuitInstruction as CircuitInstructionBase, - OperationTypeConstruct, -}; +use crate::circuit_instruction::convert_py_to_operation_type; use crate::imports::ImportOnceCell; use crate::operations::Param; use crate::operations::{Operation, OperationType}; @@ -260,38 +259,18 @@ impl Display for EdgeData { // Enum to extract circuit instructions more broadly #[derive(Debug, Clone)] -pub enum CircuitInstruction { - Instruction(CircuitInstructionBase), - OperationTypeConstruct(OperationTypeConstruct), -} - -impl CircuitInstruction { - #[inline] - pub fn operation(&self) -> &OperationType { - match self { - CircuitInstruction::Instruction(instruction) => &instruction.operation, - CircuitInstruction::OperationTypeConstruct(operation) => &operation.operation, - } - } - - #[inline] - pub fn params(&self) -> &[Param] { - match self { - CircuitInstruction::Instruction(instruction) => &instruction.params, - CircuitInstruction::OperationTypeConstruct(operation) => &operation.params, - } - } +pub struct GateOper { + operation: OperationType, + params: SmallVec<[Param; 3]>, } -impl<'py> FromPyObject<'py> for CircuitInstruction { +impl<'py> FromPyObject<'py> for GateOper { fn extract(ob: &'py PyAny) -> PyResult { - match ob.extract::() { - Ok(inst) => Ok(Self::Instruction(inst)), - Err(_) => Ok(Self::OperationTypeConstruct(convert_py_to_operation_type( - ob.py(), - ob.into(), - )?)), - } + let op_struct = convert_py_to_operation_type(ob.py(), ob.into())?; + Ok(Self { + operation: op_struct.operation, + params: op_struct.params, + }) } } @@ -303,7 +282,7 @@ pub struct CircuitRep { pub num_clbits: u32, pub label: Option, pub params: SmallVec<[Param; 3]>, - pub data: Vec, + // TODO: Have a valid implementation of CircuiData that's usable in Rust. } impl FromPyObject<'_> for CircuitRep { @@ -327,18 +306,12 @@ impl FromPyObject<'_> for CircuitRep { .getattr("data")? .extract::>() .unwrap_or_default(); - let data = match ob.getattr("data") { - Ok(data) => data.extract::>().ok(), - Err(_) => Some(vec![]), - } - .unwrap_or_default(); Ok(Self { object: ob.into(), num_qubits, num_clbits, label, params, - data, }) } } @@ -380,7 +353,6 @@ impl Default for CircuitRep { num_clbits: 0, label: None, params: smallvec![], - data: vec![], } } } @@ -442,11 +414,7 @@ impl EquivalenceLibrary { /// gate (Gate): A Gate instance. /// equivalent_circuit (QuantumCircuit): A circuit equivalently /// implementing the given Gate. - fn add_equivalence( - &mut self, - gate: CircuitInstruction, - equivalent_circuit: CircuitRep, - ) -> PyResult<()> { + fn add_equivalence(&mut self, gate: GateOper, equivalent_circuit: CircuitRep) -> PyResult<()> { match self.add_equiv(gate, equivalent_circuit) { Ok(_) => Ok(()), Err(e) => Err(CircuitError::new_err(e.message)), @@ -461,10 +429,10 @@ impl EquivalenceLibrary { /// Returns: /// Bool: True if gate has a known decomposition in the library. /// False otherwise. - pub fn has_entry(&self, gate: CircuitInstruction) -> bool { + pub fn has_entry(&self, gate: GateOper) -> bool { let key = Key { - name: gate.operation().name().to_string(), - num_qubits: gate.operation().num_qubits(), + name: gate.operation.name().to_string(), + num_qubits: gate.operation.num_qubits(), }; self.key_to_node_index.contains_key(&key) } @@ -480,7 +448,7 @@ impl EquivalenceLibrary { /// gate (Gate): A Gate instance. /// entry (List['QuantumCircuit']) : A list of QuantumCircuits, each /// equivalently implementing the given Gate. - fn set_entry(&mut self, gate: CircuitInstruction, entry: Vec) -> PyResult<()> { + fn set_entry(&mut self, gate: GateOper, entry: Vec) -> PyResult<()> { match self.set_entry_native(&gate, &entry) { Ok(_) => Ok(()), Err(e) => Err(CircuitError::new_err(e.message)), @@ -504,16 +472,16 @@ impl EquivalenceLibrary { /// the library, from earliest to latest, from top to base. The /// ordering of the StandardEquivalenceLibrary will not generally be /// consistent across Qiskit versions. - pub fn get_entry(&self, gate: CircuitInstruction) -> Vec { + pub fn get_entry(&self, gate: GateOper) -> Vec { let key = Key { - name: gate.operation().name().to_string(), - num_qubits: gate.operation().num_qubits(), + name: gate.operation.name().to_string(), + num_qubits: gate.operation.num_qubits(), }; - let query_params = gate.params(); + let query_params = gate.params; self._get_equivalences(&key) .into_iter() - .filter_map(|equivalence| rebind_equiv(equivalence, query_params)) + .filter_map(|equivalence| rebind_equiv(equivalence, &query_params)) .collect() } @@ -561,14 +529,14 @@ impl EquivalenceLibrary { ret.set_item("graph_nodes", graph_nodes.into_py(slf.py()))?; let graph_edges: Vec<(usize, usize, EdgeData)> = slf ._graph - .edge_indices() - .map(|edge_id| { + .edge_references() + .map(|edge| { ( - slf._graph.edge_endpoints(edge_id).unwrap(), - slf._graph.edge_weight(edge_id).unwrap(), + edge.source().index(), + edge.target().index(), + edge.weight().clone(), ) }) - .map(|((source, target), weight)| (source.index(), target.index(), weight.clone())) .collect_vec(); ret.set_item("graph_edges", graph_edges.into_py(slf.py()))?; Ok(ret) @@ -634,18 +602,18 @@ impl EquivalenceLibrary { /// implementing the given Gate. pub fn add_equiv( &mut self, - gate: CircuitInstruction, + gate: GateOper, equivalent_circuit: CircuitRep, ) -> Result<(), EquivalenceError> { raise_if_shape_mismatch(&gate, &equivalent_circuit)?; - raise_if_param_mismatch(gate.params(), &equivalent_circuit.params)?; + raise_if_param_mismatch(&gate.params, &equivalent_circuit.params)?; let key: Key = Key { - name: gate.operation().name().to_string(), - num_qubits: gate.operation().num_qubits(), + name: gate.operation.name().to_string(), + num_qubits: gate.operation.num_qubits(), }; let equiv = Equivalence { - params: gate.params().into(), + params: gate.params, circuit: equivalent_circuit.clone(), }; @@ -653,11 +621,7 @@ impl EquivalenceLibrary { if let Some(node) = self._graph.node_weight_mut(target) { node.equivs.push(equiv.clone()); } - let sources: HashSet = - HashSet::from_iter(equivalent_circuit.data.iter().map(|inst| Key { - name: inst.operation().name().to_string(), - num_qubits: inst.operation().num_qubits(), - })); + let sources: HashSet = get_sources_from_circuit_rep(&equivalent_circuit); let edges = Vec::from_iter(sources.iter().map(|source| { ( self.set_default_node(source.clone()), @@ -692,17 +656,17 @@ impl EquivalenceLibrary { /// equivalently implementing the given Gate. pub fn set_entry_native( &mut self, - gate: &CircuitInstruction, + gate: &GateOper, entry: &Vec, ) -> Result<(), EquivalenceError> { for equiv in entry { raise_if_shape_mismatch(gate, equiv)?; - raise_if_param_mismatch(gate.params(), &equiv.params)?; + raise_if_param_mismatch(&gate.params, &equiv.params)?; } let key = Key { - name: gate.operation().name().to_string(), - num_qubits: gate.operation().num_qubits(), + name: gate.operation.name().to_string(), + num_qubits: gate.operation.num_qubits(), }; let node_index = self.set_default_node(key); @@ -734,10 +698,6 @@ fn raise_if_param_mismatch( .iter() .filter(|param| matches!(param, Param::ParameterExpression(_))) .collect_vec(); - let circuit_parameters = circuit_parameters - .iter() - .filter(|param| matches!(param, Param::ParameterExpression(_))) - .collect_vec(); if gate_params.len() == circuit_parameters.len() && gate_params.iter().any(|x| !circuit_parameters.contains(x)) { @@ -751,19 +711,16 @@ fn raise_if_param_mismatch( Ok(()) } -fn raise_if_shape_mismatch( - gate: &CircuitInstruction, - circuit: &CircuitRep, -) -> Result<(), EquivalenceError> { - if gate.operation().num_qubits() != circuit.num_qubits - || gate.operation().num_clbits() != circuit.num_clbits +fn raise_if_shape_mismatch(gate: &GateOper, circuit: &CircuitRep) -> Result<(), EquivalenceError> { + if gate.operation.num_qubits() != circuit.num_qubits + || gate.operation.num_clbits() != circuit.num_clbits { return Err(EquivalenceError::new_err(format!( "Cannot add equivalence between circuit and gate \ of different shapes. Gate: {} qubits and {} clbits. \ Circuit: {} qubits and {} clbits.", - gate.operation().num_qubits(), - gate.operation().num_clbits(), + gate.operation.num_qubits(), + gate.operation.num_clbits(), circuit.num_qubits, circuit.num_clbits ))); @@ -776,11 +733,11 @@ fn rebind_equiv(equiv: Equivalence, query_params: &[Param]) -> Option = equiv_params .into_iter() - .filter_map(|param| match param { - Param::ParameterExpression(_) => Some(param), + .zip(query_params.iter().cloned()) + .filter_map(|(param_x, param_y)| match param_x { + Param::ParameterExpression(_) => Some((param_x, param_y)), _ => None, }) - .zip(query_params.iter().cloned()) .collect(); let dict = param_map.as_slice().into_py_dict_bound(py); let kwargs = [("inplace", false), ("flat_input", true)].into_py_dict_bound(py); @@ -794,6 +751,33 @@ fn rebind_equiv(equiv: Equivalence, query_params: &[Param]) -> Option HashSet { + let raw_sources = Python::with_gil(|py| -> PyResult> { + Ok(circuit + .object + .bind(py) + .getattr("data")? + .iter()? + .flat_map(|inst| -> PyResult<(String, u32)> { + let operation = inst?.getattr("operation")?; + Ok(( + operation + .getattr("name")? + .downcast::()? + .to_string(), + operation.getattr("num_qubits")?.extract::()?, + )) + }) + .collect()) + }) + .unwrap_or(vec![]); + // println!("{:#?}", raw_sources); + HashSet::from_iter(raw_sources.iter().map(|(name, num_qubits)| Key { + name: name.to_string(), + num_qubits: *num_qubits, + })) +} // Errors #[derive(Debug, Clone)] diff --git a/test/python/transpiler/test_basis_translator.py b/test/python/transpiler/test_basis_translator.py index fc933cd8f666..3083d096a3db 100644 --- a/test/python/transpiler/test_basis_translator.py +++ b/test/python/transpiler/test_basis_translator.py @@ -976,9 +976,9 @@ def test_cx_bell_to_ecr(self): expected = QuantumCircuit(2) expected.u(pi / 2, 0, pi, qr[0]) expected.u(0, 0, -pi / 2, qr[0]) - expected.u(pi, 0, 0, qr[0]) - expected.u(pi / 2, -pi / 2, pi / 2, qr[1]) + expected.u(-pi / 2, -pi / 2, pi / 2, qr[1]) expected.ecr(0, 1) + expected.u(pi, 0, pi, qr[0]) expected_dag = circuit_to_dag(expected) self.assertEqual(out_dag, expected_dag) From 6dec921152d4116be0c14dca622aee094bb47034 Mon Sep 17 00:00:00 2001 From: Raynel Sanchez <87539502+raynelfss@users.noreply.github.com> Date: Wed, 26 Jun 2024 11:13:32 -0400 Subject: [PATCH 16/45] Fix: Elide implicit lifetime of PyRef --- crates/circuit/src/equivalence.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/crates/circuit/src/equivalence.rs b/crates/circuit/src/equivalence.rs index 9c0d60aeb03b..0c2f4d8ad5b1 100644 --- a/crates/circuit/src/equivalence.rs +++ b/crates/circuit/src/equivalence.rs @@ -71,7 +71,7 @@ impl Key { hasher.finish() } - fn __repr__(slf: PyRef<'_, Self>) -> String { + fn __repr__(slf: PyRef) -> String { slf.to_string() } From 3b954e471dfbc21aa7788a34815983aac00013b3 Mon Sep 17 00:00:00 2001 From: Raynel Sanchez <87539502+raynelfss@users.noreply.github.com> Date: Wed, 26 Jun 2024 15:06:23 -0400 Subject: [PATCH 17/45] Fix: Make `CircuitRep` attributes OneCell-like. - Attributes from CircuitRep are only written once, reducing the overhead. - Modify `__setstate__` to avoid extra conversion. - Remove `get_sources_from_circuit_rep`. --- crates/circuit/src/equivalence.rs | 188 +++++++++++++++++------------- 1 file changed, 110 insertions(+), 78 deletions(-) diff --git a/crates/circuit/src/equivalence.rs b/crates/circuit/src/equivalence.rs index 0c2f4d8ad5b1..cd44f1b6ed78 100644 --- a/crates/circuit/src/equivalence.rs +++ b/crates/circuit/src/equivalence.rs @@ -23,7 +23,7 @@ use std::{error::Error, fmt::Display}; use exceptions::CircuitError; use hashbrown::{HashMap, HashSet}; -use pyo3::types::{PyDict, PyString}; +use pyo3::types::PyDict; use pyo3::{prelude::*, types::IntoPyDict}; use rustworkx_core::petgraph::{ @@ -31,7 +31,7 @@ use rustworkx_core::petgraph::{ visit::EdgeRef, }; -use crate::circuit_instruction::convert_py_to_operation_type; +use crate::circuit_instruction::{convert_py_to_operation_type, CircuitInstruction}; use crate::imports::ImportOnceCell; use crate::operations::Param; use crate::operations::{Operation, OperationType}; @@ -278,40 +278,82 @@ impl<'py> FromPyObject<'py> for GateOper { #[derive(Debug, Clone)] pub struct CircuitRep { object: PyObject, - pub num_qubits: u32, - pub num_clbits: u32, - pub label: Option, - pub params: SmallVec<[Param; 3]>, + num_qubits: Option, + num_clbits: Option, + params: Option>, + data: Option>, // TODO: Have a valid implementation of CircuiData that's usable in Rust. } -impl FromPyObject<'_> for CircuitRep { - fn extract(ob: &'_ PyAny) -> PyResult { - let num_qubits = match ob.getattr("num_qubits") { - Ok(num_qubits) => num_qubits.extract::().ok(), - Err(_) => None, +impl CircuitRep { + #[inline] + pub fn num_qubits(&mut self) -> u32 { + match &self.num_qubits { + Some(num_qubits) => *num_qubits, + None => { + let num_qubits = Python::with_gil(|py| -> PyResult { + self.object.getattr(py, "num_qubits")?.extract(py) + }) + .unwrap_or_default(); + self.num_qubits = Some(num_qubits); + num_qubits + } + } + } + + #[inline] + pub fn num_clbits(&mut self) -> u32 { + match &self.num_clbits { + Some(num_clbits) => *num_clbits, + None => { + let num_clbits = Python::with_gil(|py| -> PyResult { + self.object.getattr(py, "num_clbits")?.extract(py) + }) + .unwrap_or_default(); + self.num_clbits = Some(num_clbits); + num_clbits + } } + } + + #[inline] + pub fn params(&mut self) -> &[Param] { + if self.params.is_some() { + return self.params.as_ref().unwrap(); + } + let params = Python::with_gil(|py| -> PyResult> { + self.object + .getattr(py, "params")? + .getattr(py, "data")? + .extract(py) + }) .unwrap_or_default(); - let num_clbits = match ob.getattr("num_clbits") { - Ok(num_clbits) => num_clbits.extract::().ok(), - Err(_) => None, + self.params = Some(params); + self.params.as_ref().unwrap() + } + + #[inline] + pub fn data(&mut self) -> &[CircuitInstruction] { + if self.data.is_some() { + return self.data.as_ref().unwrap(); } + let data = Python::with_gil(|py| -> PyResult> { + self.object.getattr(py, "data")?.extract(py) + }) .unwrap_or_default(); - let label = match ob.getattr("label") { - Ok(label) => label.extract::().ok(), - Err(_) => None, - }; - let params = ob - .getattr("parameters")? - .getattr("data")? - .extract::>() - .unwrap_or_default(); + self.data = Some(data); + self.data.as_ref().unwrap() + } +} + +impl FromPyObject<'_> for CircuitRep { + fn extract_bound(ob: &Bound<'_, PyAny>) -> PyResult { Ok(Self { - object: ob.into(), - num_qubits, - num_clbits, - label, - params, + object: ob.to_object(ob.py()), + num_qubits: None, + num_clbits: None, + params: None, + data: None, }) } } @@ -349,10 +391,10 @@ impl Default for CircuitRep { fn default() -> Self { Self { object: Python::with_gil(|py| py.None()), - num_qubits: 0, - num_clbits: 0, - label: None, - params: smallvec![], + num_qubits: None, + num_clbits: None, + params: None, + data: None, } } } @@ -449,7 +491,8 @@ impl EquivalenceLibrary { /// entry (List['QuantumCircuit']) : A list of QuantumCircuits, each /// equivalently implementing the given Gate. fn set_entry(&mut self, gate: GateOper, entry: Vec) -> PyResult<()> { - match self.set_entry_native(&gate, &entry) { + let mut entry = entry; + match self.set_entry_native(&gate, &mut entry) { Ok(_) => Ok(()), Err(e) => Err(CircuitError::new_err(e.message)), } @@ -519,10 +562,10 @@ impl EquivalenceLibrary { fn __getstate__(slf: PyRef) -> PyResult> { let ret = PyDict::new_bound(slf.py()); ret.set_item("rule_id", slf.rule_id)?; - let key_to_usize_node: HashMap<(String, u32), usize> = HashMap::from_iter( + let key_to_usize_node: HashMap = HashMap::from_iter( slf.key_to_node_index .iter() - .map(|(key, val)| ((key.name.to_string(), key.num_qubits), val.index())), + .map(|(key, val)| (key.clone(), val.index())), ); ret.set_item("key_to_node_index", key_to_usize_node.into_py(slf.py()))?; let graph_nodes: Vec = slf._graph.node_weights().cloned().collect(); @@ -544,13 +587,21 @@ impl EquivalenceLibrary { fn __setstate__(mut slf: PyRefMut, state: &Bound<'_, PyDict>) -> PyResult<()> { slf.rule_id = state.get_item("rule_id")?.unwrap().extract()?; - slf.key_to_node_index = state + state .get_item("key_to_node_index")? .unwrap() - .extract::>()? - .into_iter() - .map(|((name, num_qubits), val)| (Key::new(name, num_qubits), NodeIndex::new(val))) - .collect(); + .downcast::()? + .items() + .iter() + .filter_map( + |item| match (item.extract::().ok(), item.extract::().ok()) { + (Some(key), Some(value)) => Some((key, value)), + _ => None, + }, + ) + .for_each(|(key, value)| { + slf.key_to_node_index.insert(key, NodeIndex::new(value)); + }); let graph_nodes: Vec = state.get_item("graph_nodes")?.unwrap().extract()?; let graph_edges: Vec<(usize, usize, EdgeData)> = state.get_item("graph_edges")?.unwrap().extract()?; @@ -603,10 +654,10 @@ impl EquivalenceLibrary { pub fn add_equiv( &mut self, gate: GateOper, - equivalent_circuit: CircuitRep, + mut equivalent_circuit: CircuitRep, ) -> Result<(), EquivalenceError> { - raise_if_shape_mismatch(&gate, &equivalent_circuit)?; - raise_if_param_mismatch(&gate.params, &equivalent_circuit.params)?; + raise_if_shape_mismatch(&gate, &mut equivalent_circuit)?; + raise_if_param_mismatch(&gate.params, equivalent_circuit.params())?; let key: Key = Key { name: gate.operation.name().to_string(), @@ -621,7 +672,11 @@ impl EquivalenceLibrary { if let Some(node) = self._graph.node_weight_mut(target) { node.equivs.push(equiv.clone()); } - let sources: HashSet = get_sources_from_circuit_rep(&equivalent_circuit); + let sources: HashSet = + HashSet::from_iter(equivalent_circuit.data().iter().map(|inst| Key { + name: inst.operation.name().to_string(), + num_qubits: inst.operation.num_qubits(), + })); let edges = Vec::from_iter(sources.iter().map(|source| { ( self.set_default_node(source.clone()), @@ -657,11 +712,11 @@ impl EquivalenceLibrary { pub fn set_entry_native( &mut self, gate: &GateOper, - entry: &Vec, + entry: &mut Vec, ) -> Result<(), EquivalenceError> { - for equiv in entry { + for equiv in &mut *entry { raise_if_shape_mismatch(gate, equiv)?; - raise_if_param_mismatch(&gate.params, &equiv.params)?; + raise_if_param_mismatch(&gate.params, equiv.params())?; } let key = Key { @@ -711,9 +766,12 @@ fn raise_if_param_mismatch( Ok(()) } -fn raise_if_shape_mismatch(gate: &GateOper, circuit: &CircuitRep) -> Result<(), EquivalenceError> { - if gate.operation.num_qubits() != circuit.num_qubits - || gate.operation.num_clbits() != circuit.num_clbits +fn raise_if_shape_mismatch( + gate: &GateOper, + circuit: &mut CircuitRep, +) -> Result<(), EquivalenceError> { + if gate.operation.num_qubits() != circuit.num_qubits() + || gate.operation.num_clbits() != circuit.num_clbits() { return Err(EquivalenceError::new_err(format!( "Cannot add equivalence between circuit and gate \ @@ -721,8 +779,8 @@ fn raise_if_shape_mismatch(gate: &GateOper, circuit: &CircuitRep) -> Result<(), Circuit: {} qubits and {} clbits.", gate.operation.num_qubits(), gate.operation.num_clbits(), - circuit.num_qubits, - circuit.num_clbits + circuit.num_qubits(), + circuit.num_clbits() ))); } Ok(()) @@ -752,32 +810,6 @@ fn rebind_equiv(equiv: Equivalence, query_params: &[Param]) -> Option HashSet { - let raw_sources = Python::with_gil(|py| -> PyResult> { - Ok(circuit - .object - .bind(py) - .getattr("data")? - .iter()? - .flat_map(|inst| -> PyResult<(String, u32)> { - let operation = inst?.getattr("operation")?; - Ok(( - operation - .getattr("name")? - .downcast::()? - .to_string(), - operation.getattr("num_qubits")?.extract::()?, - )) - }) - .collect()) - }) - .unwrap_or(vec![]); - // println!("{:#?}", raw_sources); - HashSet::from_iter(raw_sources.iter().map(|(name, num_qubits)| Key { - name: name.to_string(), - num_qubits: *num_qubits, - })) -} // Errors #[derive(Debug, Clone)] From 9a7d9a0a8f29be06fdb3d450c8a859b8f9d8d1f9 Mon Sep 17 00:00:00 2001 From: Raynel Sanchez <87539502+raynelfss@users.noreply.github.com> Date: Fri, 28 Jun 2024 09:01:48 -0400 Subject: [PATCH 18/45] Fix: Incorrect pickle attribute extraction --- crates/circuit/src/equivalence.rs | 157 ++++++++++++------------------ 1 file changed, 62 insertions(+), 95 deletions(-) diff --git a/crates/circuit/src/equivalence.rs b/crates/circuit/src/equivalence.rs index cd44f1b6ed78..58fbdd4265cf 100644 --- a/crates/circuit/src/equivalence.rs +++ b/crates/circuit/src/equivalence.rs @@ -12,6 +12,7 @@ use itertools::Itertools; +use pyo3::exceptions::PyTypeError; use rustworkx_core::petgraph::csr::IndexType; use rustworkx_core::petgraph::stable_graph::StableDiGraph; use rustworkx_core::petgraph::visit::IntoEdgeReferences; @@ -41,6 +42,7 @@ mod exceptions { import_exception_bound! {qiskit.circuit.exceptions, CircuitError} } pub static PYDIGRAPH: ImportOnceCell = ImportOnceCell::new("rustworkx", "PyDiGraph"); +pub static QUANTUMCIRCUIT: ImportOnceCell = ImportOnceCell::new("qiskit.circuit", "QuantumCircuit"); // Custom Structs @@ -278,83 +280,60 @@ impl<'py> FromPyObject<'py> for GateOper { #[derive(Debug, Clone)] pub struct CircuitRep { object: PyObject, - num_qubits: Option, - num_clbits: Option, + pub num_qubits: u32, + pub num_clbits: u32, params: Option>, data: Option>, // TODO: Have a valid implementation of CircuiData that's usable in Rust. } impl CircuitRep { - #[inline] - pub fn num_qubits(&mut self) -> u32 { - match &self.num_qubits { - Some(num_qubits) => *num_qubits, - None => { - let num_qubits = Python::with_gil(|py| -> PyResult { - self.object.getattr(py, "num_qubits")?.extract(py) - }) - .unwrap_or_default(); - self.num_qubits = Some(num_qubits); - num_qubits - } - } - } - - #[inline] - pub fn num_clbits(&mut self) -> u32 { - match &self.num_clbits { - Some(num_clbits) => *num_clbits, - None => { - let num_clbits = Python::with_gil(|py| -> PyResult { - self.object.getattr(py, "num_clbits")?.extract(py) - }) - .unwrap_or_default(); - self.num_clbits = Some(num_clbits); - num_clbits - } - } - } - - #[inline] - pub fn params(&mut self) -> &[Param] { - if self.params.is_some() { + pub fn parameters(&mut self) -> &[Param] { + if self.params.is_none() { + let params = Python::with_gil(|py| -> PyResult> { + self.object + .bind(py) + .getattr("parameters")? + .getattr("data")? + .extract() + }) + .unwrap_or_default(); + self.params = Some(params); return self.params.as_ref().unwrap(); } - let params = Python::with_gil(|py| -> PyResult> { - self.object - .getattr(py, "params")? - .getattr(py, "data")? - .extract(py) - }) - .unwrap_or_default(); - self.params = Some(params); - self.params.as_ref().unwrap() + return self.params.as_ref().unwrap(); } - #[inline] pub fn data(&mut self) -> &[CircuitInstruction] { - if self.data.is_some() { + if self.data.is_none() { + let data = Python::with_gil(|py| -> PyResult> { + self.object.bind(py).getattr("data")?.extract() + }) + .unwrap_or_default(); + self.data = Some(data); return self.data.as_ref().unwrap(); } - let data = Python::with_gil(|py| -> PyResult> { - self.object.getattr(py, "data")?.extract(py) - }) - .unwrap_or_default(); - self.data = Some(data); - self.data.as_ref().unwrap() + return self.data.as_ref().unwrap(); } } impl FromPyObject<'_> for CircuitRep { fn extract_bound(ob: &Bound<'_, PyAny>) -> PyResult { - Ok(Self { - object: ob.to_object(ob.py()), - num_qubits: None, - num_clbits: None, - params: None, - data: None, - }) + if ob.is_instance(QUANTUMCIRCUIT.get_bound(ob.py()))? { + let num_qubits = ob.getattr("num_qubits")?.extract()?; + let num_clbits = ob.getattr("num_clbits")?.extract()?; + Ok(Self { + object: ob.into_py(ob.py()), + num_qubits, + num_clbits, + params: None, + data: None, + }) + } else { + Err(PyTypeError::new_err( + "Provided object was not an instance of QuantumCircuit", + )) + } } } @@ -391,8 +370,8 @@ impl Default for CircuitRep { fn default() -> Self { Self { object: Python::with_gil(|py| py.None()), - num_qubits: None, - num_clbits: None, + num_qubits: 0, + num_clbits: 0, params: None, data: None, } @@ -491,8 +470,7 @@ impl EquivalenceLibrary { /// entry (List['QuantumCircuit']) : A list of QuantumCircuits, each /// equivalently implementing the given Gate. fn set_entry(&mut self, gate: GateOper, entry: Vec) -> PyResult<()> { - let mut entry = entry; - match self.set_entry_native(&gate, &mut entry) { + match self.set_entry_native(gate, entry) { Ok(_) => Ok(()), Err(e) => Err(CircuitError::new_err(e.message)), } @@ -562,10 +540,10 @@ impl EquivalenceLibrary { fn __getstate__(slf: PyRef) -> PyResult> { let ret = PyDict::new_bound(slf.py()); ret.set_item("rule_id", slf.rule_id)?; - let key_to_usize_node: HashMap = HashMap::from_iter( + let key_to_usize_node: HashMap<(String, u32), usize> = HashMap::from_iter( slf.key_to_node_index .iter() - .map(|(key, val)| (key.clone(), val.index())), + .map(|(key, val)| ((key.name.to_string(), key.num_qubits), val.index())), ); ret.set_item("key_to_node_index", key_to_usize_node.into_py(slf.py()))?; let graph_nodes: Vec = slf._graph.node_weights().cloned().collect(); @@ -587,21 +565,6 @@ impl EquivalenceLibrary { fn __setstate__(mut slf: PyRefMut, state: &Bound<'_, PyDict>) -> PyResult<()> { slf.rule_id = state.get_item("rule_id")?.unwrap().extract()?; - state - .get_item("key_to_node_index")? - .unwrap() - .downcast::()? - .items() - .iter() - .filter_map( - |item| match (item.extract::().ok(), item.extract::().ok()) { - (Some(key), Some(value)) => Some((key, value)), - _ => None, - }, - ) - .for_each(|(key, value)| { - slf.key_to_node_index.insert(key, NodeIndex::new(value)); - }); let graph_nodes: Vec = state.get_item("graph_nodes")?.unwrap().extract()?; let graph_edges: Vec<(usize, usize, EdgeData)> = state.get_item("graph_edges")?.unwrap().extract()?; @@ -616,6 +579,13 @@ impl EquivalenceLibrary { edge_weight, ); } + slf.key_to_node_index = state + .get_item("key_to_node_index")? + .unwrap() + .extract::>()? + .into_iter() + .map(|((name, num_qubits), val)| (Key::new(name, num_qubits), NodeIndex::new(val))) + .collect(); slf.graph = None; Ok(()) } @@ -656,8 +626,8 @@ impl EquivalenceLibrary { gate: GateOper, mut equivalent_circuit: CircuitRep, ) -> Result<(), EquivalenceError> { - raise_if_shape_mismatch(&gate, &mut equivalent_circuit)?; - raise_if_param_mismatch(&gate.params, equivalent_circuit.params())?; + raise_if_shape_mismatch(&gate, &equivalent_circuit)?; + raise_if_param_mismatch(&gate.params, equivalent_circuit.parameters())?; let key: Key = Key { name: gate.operation.name().to_string(), @@ -711,12 +681,12 @@ impl EquivalenceLibrary { /// equivalently implementing the given Gate. pub fn set_entry_native( &mut self, - gate: &GateOper, - entry: &mut Vec, + gate: GateOper, + mut entry: Vec, ) -> Result<(), EquivalenceError> { - for equiv in &mut *entry { - raise_if_shape_mismatch(gate, equiv)?; - raise_if_param_mismatch(&gate.params, equiv.params())?; + for equiv in entry.iter_mut() { + raise_if_shape_mismatch(&gate, equiv)?; + raise_if_param_mismatch(&gate.params, equiv.parameters())?; } let key = Key { @@ -766,12 +736,9 @@ fn raise_if_param_mismatch( Ok(()) } -fn raise_if_shape_mismatch( - gate: &GateOper, - circuit: &mut CircuitRep, -) -> Result<(), EquivalenceError> { - if gate.operation.num_qubits() != circuit.num_qubits() - || gate.operation.num_clbits() != circuit.num_clbits() +fn raise_if_shape_mismatch(gate: &GateOper, circuit: &CircuitRep) -> Result<(), EquivalenceError> { + if gate.operation.num_qubits() != circuit.num_qubits + || gate.operation.num_clbits() != circuit.num_clbits { return Err(EquivalenceError::new_err(format!( "Cannot add equivalence between circuit and gate \ @@ -779,8 +746,8 @@ fn raise_if_shape_mismatch( Circuit: {} qubits and {} clbits.", gate.operation.num_qubits(), gate.operation.num_clbits(), - circuit.num_qubits(), - circuit.num_clbits() + circuit.num_qubits, + circuit.num_clbits ))); } Ok(()) From dc3041ea3dcd64322532daccfa5379552d9f2a38 Mon Sep 17 00:00:00 2001 From: Raynel Sanchez <87539502+raynelfss@users.noreply.github.com> Date: Fri, 28 Jun 2024 13:45:43 -0400 Subject: [PATCH 19/45] Remove: Default initialization methods from custom datatypes. - Use `__getnewargs__ instead. --- crates/circuit/src/equivalence.rs | 69 ++++++++++++++++--------------- 1 file changed, 35 insertions(+), 34 deletions(-) diff --git a/crates/circuit/src/equivalence.rs b/crates/circuit/src/equivalence.rs index 58fbdd4265cf..e30c846d34ea 100644 --- a/crates/circuit/src/equivalence.rs +++ b/crates/circuit/src/equivalence.rs @@ -17,7 +17,7 @@ use rustworkx_core::petgraph::csr::IndexType; use rustworkx_core::petgraph::stable_graph::StableDiGraph; use rustworkx_core::petgraph::visit::IntoEdgeReferences; -use smallvec::{smallvec, SmallVec}; +use smallvec::SmallVec; use std::collections::hash_map::DefaultHasher; use std::hash::{Hash, Hasher}; use std::{error::Error, fmt::Display}; @@ -42,7 +42,8 @@ mod exceptions { import_exception_bound! {qiskit.circuit.exceptions, CircuitError} } pub static PYDIGRAPH: ImportOnceCell = ImportOnceCell::new("rustworkx", "PyDiGraph"); -pub static QUANTUMCIRCUIT: ImportOnceCell = ImportOnceCell::new("qiskit.circuit", "QuantumCircuit"); +pub static QUANTUMCIRCUIT: ImportOnceCell = + ImportOnceCell::new("qiskit.circuit.quantumcircuit", "QuantumCircuit"); // Custom Structs @@ -58,7 +59,7 @@ pub struct Key { #[pymethods] impl Key { #[new] - #[pyo3(signature = (name="".to_string(), num_qubits=0))] + #[pyo3(signature = (name, num_qubits))] fn new(name: String, num_qubits: u32) -> Self { Self { name, num_qubits } } @@ -81,6 +82,10 @@ impl Key { (slf.name.clone(), slf.num_qubits) } + fn __getnewargs__(slf: PyRef) -> (String, u32) { + Key::__getstate__(slf) + } + fn __setstate__(mut slf: PyRefMut, state: (String, u32)) { slf.name = state.0; slf.num_qubits = state.1; @@ -97,17 +102,8 @@ impl Display for Key { } } -impl Default for Key { - fn default() -> Self { - Self { - name: "".to_string(), - num_qubits: 0, - } - } -} - #[pyclass(sequence, module = "qiskit._accelerate.circuit.equivalence")] -#[derive(Debug, Clone, PartialEq, Default)] +#[derive(Debug, Clone, PartialEq)] pub struct Equivalence { #[pyo3(get)] pub params: SmallVec<[Param; 3]>, @@ -118,7 +114,7 @@ pub struct Equivalence { #[pymethods] impl Equivalence { #[new] - #[pyo3(signature = (params=smallvec![], circuit=CircuitRep::default()))] + #[pyo3(signature = (params, circuit))] fn new(params: SmallVec<[Param; 3]>, circuit: CircuitRep) -> Self { Self { circuit, params } } @@ -131,8 +127,12 @@ impl Equivalence { self.eq(&other) } - fn __getstate__(slf: PyRef) -> (SmallVec<[Param; 3]>, CircuitRep) { - (slf.params.clone(), slf.circuit.clone()) + fn __getstate__(&self, py: Python) -> (PyObject, PyObject) { + (self.params.to_object(py), self.circuit.object.clone_ref(py)) + } + + fn __getnewargs__(&self, py: Python) -> (PyObject, PyObject) { + self.__getstate__(py) } fn __setstate__(mut slf: PyRefMut, state: (SmallVec<[Param; 3]>, CircuitRep)) { @@ -153,7 +153,7 @@ impl Display for Equivalence { } #[pyclass(sequence, module = "qiskit._accelerate.circuit.equivalence")] -#[derive(Debug, Clone, PartialEq, Default)] +#[derive(Debug, Clone, PartialEq)] pub struct NodeData { #[pyo3(get)] key: Key, @@ -164,7 +164,7 @@ pub struct NodeData { #[pymethods] impl NodeData { #[new] - #[pyo3(signature = (key=Key::default(), equivs=vec![]))] + #[pyo3(signature = (key, equivs))] fn new(key: Key, equivs: Vec) -> Self { Self { key, equivs } } @@ -177,8 +177,12 @@ impl NodeData { self.eq(&other) } - fn __getstate__(slf: PyRef) -> (Key, Vec) { - (slf.key.clone(), slf.equivs.clone()) + fn __getstate__(&self) -> (Key, Vec) { + (self.key.clone(), self.equivs.clone()) + } + + fn __getnewargs__(&self) -> (Key, Vec) { + self.__getstate__() } fn __setstate__(mut slf: PyRefMut, state: (Key, Vec)) { @@ -199,7 +203,7 @@ impl Display for NodeData { } #[pyclass(sequence, module = "qiskit._accelerate.circuit.equivalence")] -#[derive(Debug, Clone, PartialEq, Default)] +#[derive(Debug, Clone, PartialEq)] pub struct EdgeData { #[pyo3(get)] pub index: usize, @@ -214,7 +218,7 @@ pub struct EdgeData { #[pymethods] impl EdgeData { #[new] - #[pyo3(signature = (index=0, num_gates=0, rule=Equivalence::default(), source=Key::default()))] + #[pyo3(signature = (index, num_gates, rule, source))] fn new(index: usize, num_gates: usize, rule: Equivalence, source: Key) -> Self { Self { index, @@ -241,6 +245,15 @@ impl EdgeData { ) } + fn __getnewargs__(slf: PyRef) -> (usize, usize, Equivalence, Key) { + ( + slf.index, + slf.num_gates, + slf.rule.clone(), + slf.source.clone(), + ) + } + fn __setstate__(mut slf: PyRefMut, state: (usize, usize, Equivalence, Key)) { slf.index = state.0; slf.num_gates = state.1; @@ -366,18 +379,6 @@ impl IntoPy for CircuitRep { } } -impl Default for CircuitRep { - fn default() -> Self { - Self { - object: Python::with_gil(|py| py.None()), - num_qubits: 0, - num_clbits: 0, - params: None, - data: None, - } - } -} - // Custom Types type GraphType = StableDiGraph; type KTIType = HashMap; From a0726350adb5eff63f7934cf8ad66a80e7d044d3 Mon Sep 17 00:00:00 2001 From: Raynel Sanchez <87539502+raynelfss@users.noreply.github.com> Date: Fri, 28 Jun 2024 14:52:40 -0400 Subject: [PATCH 20/45] Remove: `__getstate__`, `__setstate__`, use `__getnewargs__` instead. --- crates/circuit/src/equivalence.rs | 56 +++++-------------------------- 1 file changed, 8 insertions(+), 48 deletions(-) diff --git a/crates/circuit/src/equivalence.rs b/crates/circuit/src/equivalence.rs index e30c846d34ea..1d37029ae99a 100644 --- a/crates/circuit/src/equivalence.rs +++ b/crates/circuit/src/equivalence.rs @@ -24,7 +24,7 @@ use std::{error::Error, fmt::Display}; use exceptions::CircuitError; use hashbrown::{HashMap, HashSet}; -use pyo3::types::PyDict; +use pyo3::types::{PyDict, PyString}; use pyo3::{prelude::*, types::IntoPyDict}; use rustworkx_core::petgraph::{ @@ -78,17 +78,11 @@ impl Key { slf.to_string() } - fn __getstate__(slf: PyRef) -> (String, u32) { - (slf.name.clone(), slf.num_qubits) - } - - fn __getnewargs__(slf: PyRef) -> (String, u32) { - Key::__getstate__(slf) - } - - fn __setstate__(mut slf: PyRefMut, state: (String, u32)) { - slf.name = state.0; - slf.num_qubits = state.1; + fn __getnewargs__(slf: PyRef) -> (Bound, u32) { + ( + PyString::new_bound(slf.py(), slf.name.as_str()), + slf.num_qubits, + ) } } @@ -127,17 +121,8 @@ impl Equivalence { self.eq(&other) } - fn __getstate__(&self, py: Python) -> (PyObject, PyObject) { - (self.params.to_object(py), self.circuit.object.clone_ref(py)) - } - fn __getnewargs__(&self, py: Python) -> (PyObject, PyObject) { - self.__getstate__(py) - } - - fn __setstate__(mut slf: PyRefMut, state: (SmallVec<[Param; 3]>, CircuitRep)) { - slf.params = state.0; - slf.circuit = state.1; + (self.params.to_object(py), self.circuit.object.clone_ref(py)) } } @@ -177,17 +162,8 @@ impl NodeData { self.eq(&other) } - fn __getstate__(&self) -> (Key, Vec) { - (self.key.clone(), self.equivs.clone()) - } - fn __getnewargs__(&self) -> (Key, Vec) { - self.__getstate__() - } - - fn __setstate__(mut slf: PyRefMut, state: (Key, Vec)) { - slf.key = state.0; - slf.equivs = state.1; + (self.key.clone(), self.equivs.clone()) } } @@ -236,15 +212,6 @@ impl EdgeData { self.eq(&other) } - fn __getstate__(slf: PyRef) -> (usize, usize, Equivalence, Key) { - ( - slf.index, - slf.num_gates, - slf.rule.clone(), - slf.source.clone(), - ) - } - fn __getnewargs__(slf: PyRef) -> (usize, usize, Equivalence, Key) { ( slf.index, @@ -253,13 +220,6 @@ impl EdgeData { slf.source.clone(), ) } - - fn __setstate__(mut slf: PyRefMut, state: (usize, usize, Equivalence, Key)) { - slf.index = state.0; - slf.num_gates = state.1; - slf.rule = state.2; - slf.source = state.3; - } } impl Display for EdgeData { From 77dbec896677a67df3ab350448c537b23df973f9 Mon Sep 17 00:00:00 2001 From: Raynel Sanchez <87539502+raynelfss@users.noreply.github.com> Date: Mon, 1 Jul 2024 10:25:05 -0400 Subject: [PATCH 21/45] Fix: Further improvements to pickling - Use python structures to avoid extra conversions. - Add rust native `EquivalenceLibrary.keys()` and have the python method use it. --- crates/circuit/src/equivalence.rs | 53 +++++++++++++++++++------------ 1 file changed, 33 insertions(+), 20 deletions(-) diff --git a/crates/circuit/src/equivalence.rs b/crates/circuit/src/equivalence.rs index 1d37029ae99a..a1ea934af783 100644 --- a/crates/circuit/src/equivalence.rs +++ b/crates/circuit/src/equivalence.rs @@ -24,7 +24,7 @@ use std::{error::Error, fmt::Display}; use exceptions::CircuitError; use hashbrown::{HashMap, HashSet}; -use pyo3::types::{PyDict, PyString}; +use pyo3::types::{PyDict, PyList, PySet, PyString}; use pyo3::{prelude::*, types::IntoPyDict}; use rustworkx_core::petgraph::{ @@ -121,8 +121,9 @@ impl Equivalence { self.eq(&other) } - fn __getnewargs__(&self, py: Python) -> (PyObject, PyObject) { - (self.params.to_object(py), self.circuit.object.clone_ref(py)) + fn __getnewargs__(&self, py: Python) -> (Py, PyObject) { + let params = PyList::new_bound(py, self.params.iter().map(|param| param.to_object(py))); + (params.unbind(), self.circuit.object.clone_ref(py)) } } @@ -313,10 +314,8 @@ impl FromPyObject<'_> for CircuitRep { impl Display for CircuitRep { fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { let py_rep_str = Python::with_gil(|py| -> PyResult { - match self.object.call_method0(py, "__repr__") { - Ok(str_obj) => str_obj.extract::(py), - Err(_) => Ok("None".to_string()), - } + let bound = self.object.bind(py); + bound.repr().map(|pystring| pystring.to_string()) }) .unwrap(); write!(f, "{}", py_rep_str) @@ -396,6 +395,7 @@ impl EquivalenceLibrary { /// gate (Gate): A Gate instance. /// equivalent_circuit (QuantumCircuit): A circuit equivalently /// implementing the given Gate. + #[pyo3(text_signature = "(gate, equivalent_circuit, /,")] fn add_equivalence(&mut self, gate: GateOper, equivalent_circuit: CircuitRep) -> PyResult<()> { match self.add_equiv(gate, equivalent_circuit) { Ok(_) => Ok(()), @@ -411,6 +411,7 @@ impl EquivalenceLibrary { /// Returns: /// Bool: True if gate has a known decomposition in the library. /// False otherwise. + #[pyo3(text_signature = "(gate, /,)")] pub fn has_entry(&self, gate: GateOper) -> bool { let key = Key { name: gate.operation.name().to_string(), @@ -430,6 +431,7 @@ impl EquivalenceLibrary { /// gate (Gate): A Gate instance. /// entry (List['QuantumCircuit']) : A list of QuantumCircuits, each /// equivalently implementing the given Gate. + #[pyo3(text_signature = "(gate, entry, /,)")] fn set_entry(&mut self, gate: GateOper, entry: Vec) -> PyResult<()> { match self.set_entry_native(gate, entry) { Ok(_) => Ok(()), @@ -454,6 +456,7 @@ impl EquivalenceLibrary { /// the library, from earliest to latest, from top to base. The /// ordering of the StandardEquivalenceLibrary will not generally be /// consistent across Qiskit versions. + #[pyo3(text_signature = "(gate, /,)")] pub fn get_entry(&self, gate: GateOper) -> Vec { let key = Key { name: gate.operation.name().to_string(), @@ -490,8 +493,13 @@ impl EquivalenceLibrary { } } - fn keys(&self) -> HashSet { - self.key_to_node_index.keys().cloned().collect() + #[pyo3(name = "keys", text_signature = "()")] + pub fn py_keys(slf: PyRef) -> PyResult> { + let py_set = PySet::empty_bound(slf.py())?; + for key in slf.keys() { + py_set.add(key.clone().into_py(slf.py()))?; + } + Ok(py_set) } fn node_index(&self, key: Key) -> usize { @@ -501,12 +509,11 @@ impl EquivalenceLibrary { fn __getstate__(slf: PyRef) -> PyResult> { let ret = PyDict::new_bound(slf.py()); ret.set_item("rule_id", slf.rule_id)?; - let key_to_usize_node: HashMap<(String, u32), usize> = HashMap::from_iter( - slf.key_to_node_index - .iter() - .map(|(key, val)| ((key.name.to_string(), key.num_qubits), val.index())), - ); - ret.set_item("key_to_node_index", key_to_usize_node.into_py(slf.py()))?; + let key_to_usize_node: Bound = PyDict::new_bound(slf.py()); + for (key, val) in slf.key_to_node_index.iter() { + key_to_usize_node.set_item((&key.name, key.num_qubits), val.index())?; + } + ret.set_item("key_to_node_index", key_to_usize_node)?; let graph_nodes: Vec = slf._graph.node_weights().cloned().collect(); ret.set_item("graph_nodes", graph_nodes.into_py(slf.py()))?; let graph_edges: Vec<(usize, usize, EdgeData)> = slf @@ -526,14 +533,16 @@ impl EquivalenceLibrary { fn __setstate__(mut slf: PyRefMut, state: &Bound<'_, PyDict>) -> PyResult<()> { slf.rule_id = state.get_item("rule_id")?.unwrap().extract()?; - let graph_nodes: Vec = state.get_item("graph_nodes")?.unwrap().extract()?; - let graph_edges: Vec<(usize, usize, EdgeData)> = - state.get_item("graph_edges")?.unwrap().extract()?; + let graph_nodes_ref: Bound = state.get_item("graph_nodes")?.unwrap(); + let graph_nodes: &Bound = graph_nodes_ref.downcast()?; + let graph_edge_ref: Bound = state.get_item("graph_edges")?.unwrap(); + let graph_edges: &Bound = graph_edge_ref.downcast()?; slf._graph = GraphType::new(); for node_weight in graph_nodes { - slf._graph.add_node(node_weight); + slf._graph.add_node(node_weight.extract()?); } - for (source_node, target_node, edge_weight) in graph_edges { + for edge in graph_edges { + let (source_node, target_node, edge_weight) = edge.extract()?; slf._graph.add_edge( NodeIndex::new(source_node), NodeIndex::new(target_node), @@ -554,6 +563,10 @@ impl EquivalenceLibrary { // Rust native methods impl EquivalenceLibrary { + pub fn keys(&self) -> impl Iterator { + self.key_to_node_index.keys() + } + /// Create a new node if key not found fn set_default_node(&mut self, key: Key) -> NodeIndex { if let Some(value) = self.key_to_node_index.get(&key) { From 72e1c3336832f837540abecbe5a7a34b155df9e0 Mon Sep 17 00:00:00 2001 From: Raynel Sanchez <87539502+raynelfss@users.noreply.github.com> Date: Tue, 2 Jul 2024 16:42:43 -0400 Subject: [PATCH 22/45] Fix: Use `PyList` and iterators when possible to skip extra conversion. - Use a `py` token instead of `Python::with_gil()` for `rebind_params`. - Other tweaks and fixes. --- crates/circuit/src/equivalence.rs | 100 +++++++++++++++++------------- 1 file changed, 57 insertions(+), 43 deletions(-) diff --git a/crates/circuit/src/equivalence.rs b/crates/circuit/src/equivalence.rs index a1ea934af783..d112e07c85dd 100644 --- a/crates/circuit/src/equivalence.rs +++ b/crates/circuit/src/equivalence.rs @@ -163,8 +163,15 @@ impl NodeData { self.eq(&other) } - fn __getnewargs__(&self) -> (Key, Vec) { - (self.key.clone(), self.equivs.clone()) + fn __getnewargs__(&self, py: Python) -> (Key, Py) { + ( + self.key.clone(), + PyList::new_bound( + py, + self.equivs.iter().map(|equiv| equiv.clone().into_py(py)), + ) + .unbind(), + ) } } @@ -457,17 +464,22 @@ impl EquivalenceLibrary { /// ordering of the StandardEquivalenceLibrary will not generally be /// consistent across Qiskit versions. #[pyo3(text_signature = "(gate, /,)")] - pub fn get_entry(&self, gate: GateOper) -> Vec { + pub fn get_entry(&self, py: Python, gate: GateOper) -> PyResult> { let key = Key { name: gate.operation.name().to_string(), num_qubits: gate.operation.num_qubits(), }; let query_params = gate.params; - self._get_equivalences(&key) + let bound_equivalencies = self + ._get_equivalences(&key) .into_iter() - .filter_map(|equivalence| rebind_equiv(equivalence, &query_params)) - .collect() + .filter_map(|equivalence| rebind_equiv(py, equivalence, &query_params).ok()); + let return_list = PyList::empty_bound(py); + for equiv in bound_equivalencies { + return_list.append(equiv)?; + } + Ok(return_list.unbind()) } #[getter] @@ -493,7 +505,7 @@ impl EquivalenceLibrary { } } - #[pyo3(name = "keys", text_signature = "()")] + #[pyo3(name = "keys")] pub fn py_keys(slf: PyRef) -> PyResult> { let py_set = PySet::empty_bound(slf.py())?; for key in slf.keys() { @@ -514,20 +526,23 @@ impl EquivalenceLibrary { key_to_usize_node.set_item((&key.name, key.num_qubits), val.index())?; } ret.set_item("key_to_node_index", key_to_usize_node)?; - let graph_nodes: Vec = slf._graph.node_weights().cloned().collect(); - ret.set_item("graph_nodes", graph_nodes.into_py(slf.py()))?; - let graph_edges: Vec<(usize, usize, EdgeData)> = slf - ._graph - .edge_references() - .map(|edge| { - ( - edge.source().index(), - edge.target().index(), - edge.weight().clone(), - ) - }) - .collect_vec(); - ret.set_item("graph_edges", graph_edges.into_py(slf.py()))?; + let graph_nodes: Bound = PyList::empty_bound(slf.py()); + for weight in slf._graph.node_weights() { + graph_nodes.append(weight.clone().into_py(slf.py()))?; + } + ret.set_item("graph_nodes", graph_nodes.unbind())?; + let edges = slf._graph.edge_references().map(|edge| { + ( + edge.source().index(), + edge.target().index(), + edge.weight().clone().into_py(slf.py()), + ) + }); + let graph_edges = PyList::empty_bound(slf.py()); + for edge in edges { + graph_edges.add(edge)?; + } + ret.set_item("graph_edges", graph_edges.unbind())?; Ok(ret) } @@ -727,28 +742,27 @@ fn raise_if_shape_mismatch(gate: &GateOper, circuit: &CircuitRep) -> Result<(), Ok(()) } -fn rebind_equiv(equiv: Equivalence, query_params: &[Param]) -> Option { - Python::with_gil(|py| -> PyResult { - let (equiv_params, equiv_circuit) = (equiv.params, equiv.circuit); - let param_map: Vec<(Param, Param)> = equiv_params - .into_iter() - .zip(query_params.iter().cloned()) - .filter_map(|(param_x, param_y)| match param_x { - Param::ParameterExpression(_) => Some((param_x, param_y)), - _ => None, - }) - .collect(); - let dict = param_map.as_slice().into_py_dict_bound(py); - let kwargs = [("inplace", false), ("flat_input", true)].into_py_dict_bound(py); - let new_equiv = equiv_circuit.object.call_method_bound( - py, - "assign_parameters", - (dict,), - Some(&kwargs), - )?; - new_equiv.extract::(py) - }) - .ok() +fn rebind_equiv(py: Python, equiv: Equivalence, query_params: &[Param]) -> PyResult { + let (equiv_params, equiv_circuit) = (equiv.params, equiv.circuit); + let param_iter = equiv_params + .into_iter() + .zip(query_params.iter().cloned()) + .filter_map(|(param_x, param_y)| match param_x { + Param::ParameterExpression(_) => Some((param_x, param_y)), + _ => None, + }); + let param_map = PyDict::new_bound(py); + for (param_key, param_val) in param_iter { + param_map.set_item(param_key, param_val)?; + } + let kwargs = [("inplace", false), ("flat_input", true)].into_py_dict_bound(py); + let new_equiv = equiv_circuit.object.call_method_bound( + py, + "assign_parameters", + (param_map,), + Some(&kwargs), + )?; + Ok(new_equiv) } // Errors From 17eb9d29527fe884a8f2ddb369ed6a873fbb4b7b Mon Sep 17 00:00:00 2001 From: Raynel Sanchez <87539502+raynelfss@users.noreply.github.com> Date: Wed, 3 Jul 2024 11:02:02 -0400 Subject: [PATCH 23/45] Fix: incorrect list operation in `__getstate__` --- crates/circuit/src/equivalence.rs | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/crates/circuit/src/equivalence.rs b/crates/circuit/src/equivalence.rs index d112e07c85dd..cdb57d5f9ea6 100644 --- a/crates/circuit/src/equivalence.rs +++ b/crates/circuit/src/equivalence.rs @@ -132,7 +132,10 @@ impl Display for Equivalence { write!( f, "Equivalence(params=[{}], circuit={})", - self.params.iter().format(", "), + self.params + .iter() + .map(|param| format!("{:?}", param)) + .format(", "), self.circuit ) } @@ -540,7 +543,7 @@ impl EquivalenceLibrary { }); let graph_edges = PyList::empty_bound(slf.py()); for edge in edges { - graph_edges.add(edge)?; + graph_edges.append(edge)?; } ret.set_item("graph_edges", graph_edges.unbind())?; Ok(ret) From 9d39cf63539b932bec5c8e751a70f74f12d26516 Mon Sep 17 00:00:00 2001 From: Raynel Sanchez <87539502+raynelfss@users.noreply.github.com> Date: Wed, 3 Jul 2024 13:52:10 -0400 Subject: [PATCH 24/45] Fix: improvements on rust native methods - Accept `Operations` and `[Param]` instead of the custom `GateOper` when calling from rust. - Build custom `GateOper` inside of class. --- crates/circuit/src/equivalence.rs | 106 +++++++++++++++++++++--------- 1 file changed, 75 insertions(+), 31 deletions(-) diff --git a/crates/circuit/src/equivalence.rs b/crates/circuit/src/equivalence.rs index cdb57d5f9ea6..f8ca9880ea91 100644 --- a/crates/circuit/src/equivalence.rs +++ b/crates/circuit/src/equivalence.rs @@ -405,29 +405,29 @@ impl EquivalenceLibrary { /// gate (Gate): A Gate instance. /// equivalent_circuit (QuantumCircuit): A circuit equivalently /// implementing the given Gate. - #[pyo3(text_signature = "(gate, equivalent_circuit, /,")] - fn add_equivalence(&mut self, gate: GateOper, equivalent_circuit: CircuitRep) -> PyResult<()> { - match self.add_equiv(gate, equivalent_circuit) { + #[pyo3(name = "add_equivalence")] + fn py_add_equivalence( + &mut self, + gate: GateOper, + equivalent_circuit: CircuitRep, + ) -> PyResult<()> { + match self._add_equiv_native(gate, equivalent_circuit) { Ok(_) => Ok(()), Err(e) => Err(CircuitError::new_err(e.message)), } } /// Check if a library contains any decompositions for gate. - - /// Args: - /// gate (Gate): A Gate instance. - - /// Returns: - /// Bool: True if gate has a known decomposition in the library. - /// False otherwise. - #[pyo3(text_signature = "(gate, /,)")] - pub fn has_entry(&self, gate: GateOper) -> bool { - let key = Key { - name: gate.operation.name().to_string(), - num_qubits: gate.operation.num_qubits(), - }; - self.key_to_node_index.contains_key(&key) + /// + /// Args: + /// gate (Gate): A Gate instance. + /// + /// Returns: + /// Bool: True if gate has a known decomposition in the library. + /// False otherwise. + #[pyo3(name = "has_entry")] + pub fn py_has_entry(&self, gate: GateOper) -> bool { + self.has_entry(&gate.operation) } /// Set the equivalence record for a Gate. Future queries for the Gate @@ -441,9 +441,9 @@ impl EquivalenceLibrary { /// gate (Gate): A Gate instance. /// entry (List['QuantumCircuit']) : A list of QuantumCircuits, each /// equivalently implementing the given Gate. - #[pyo3(text_signature = "(gate, entry, /,)")] - fn set_entry(&mut self, gate: GateOper, entry: Vec) -> PyResult<()> { - match self.set_entry_native(gate, entry) { + #[pyo3(name = "set_entry")] + fn py_set_entry(&mut self, gate: GateOper, entry: Vec) -> PyResult<()> { + match self._set_entry_native(gate, entry) { Ok(_) => Ok(()), Err(e) => Err(CircuitError::new_err(e.message)), } @@ -581,6 +581,24 @@ impl EquivalenceLibrary { // Rust native methods impl EquivalenceLibrary { + /// Rust native equivalent to `EquivalenceLibrary.has_entry()` + /// + /// Check if a library contains any decompositions for gate. + /// + /// # Arguments: + /// * `operation` OperationType: A Gate instance. + /// + /// # Returns: + /// `bool`: `true` if gate has a known decomposition in the library. + /// `false` otherwise. + pub fn has_entry(&self, operation: &OperationType) -> bool { + let key = Key { + name: operation.name().to_string(), + num_qubits: operation.num_qubits(), + }; + self.key_to_node_index.contains_key(&key) + } + pub fn keys(&self) -> impl Iterator { self.key_to_node_index.keys() } @@ -609,11 +627,24 @@ impl EquivalenceLibrary { /// `Gate.params`) can be marked equivalent to parameterized circuits, /// provided the parameters match. /// - /// Args: - /// gate (Gate): A Gate instance. - /// equivalent_circuit (QuantumCircuit): A circuit equivalently - /// implementing the given Gate. - pub fn add_equiv( + /// # Arguments: + /// * `operation`: A Gate instance. + /// * `params`: A list of the gate's parameters. + /// * `equivalent_circuit`: A circuit equivalently implementing the given Gate. + pub fn add_equivalence( + &mut self, + operation: &OperationType, + params: &[Param], + equivalent_circuit: CircuitRep, + ) -> Result<(), EquivalenceError> { + let gate = GateOper { + operation: operation.clone(), + params: params.to_vec().into(), + }; + self._add_equiv_native(gate, equivalent_circuit) + } + + fn _add_equiv_native( &mut self, gate: GateOper, mut equivalent_circuit: CircuitRep, @@ -667,11 +698,24 @@ impl EquivalenceLibrary { /// `Gate.params`) can be marked equivalent to parameterized circuits, /// provided the parameters match. /// - /// Args: - /// gate (Gate): A Gate instance. - /// entry (List['QuantumCircuit']) : A list of QuantumCircuits, each - /// equivalently implementing the given Gate. - pub fn set_entry_native( + /// # Arguments: + /// * `operation`: A Gate instance. + /// * `params`: A list of the gate's parameters. + /// * `entry` : A list of QuantumCircuits, each equivalently implementing the given Gate. + pub fn set_entry( + &mut self, + operation: &OperationType, + params: &[Param], + entry: Vec, + ) -> Result<(), EquivalenceError> { + let gate = GateOper { + operation: operation.clone(), + params: params.to_vec().into(), + }; + self._set_entry_native(gate, entry) + } + + fn _set_entry_native( &mut self, gate: GateOper, mut entry: Vec, @@ -700,7 +744,7 @@ impl EquivalenceLibrary { self._graph.remove_edge(edge); } for equiv in entry { - self.add_equiv(gate.clone(), equiv.clone())? + self._add_equiv_native(gate.clone(), equiv.clone())? } self.graph = None; Ok(()) From e9e392188875ca1ffddeb417b43d10accd8d7c7e Mon Sep 17 00:00:00 2001 From: Raynel Sanchez <87539502+raynelfss@users.noreply.github.com> Date: Wed, 3 Jul 2024 15:08:52 -0400 Subject: [PATCH 25/45] Remove: `add_equiv`, `set_entry` from rust-native methods. - Add `node_index` Rust native method. - Use python set comparison for `Param` check. --- crates/circuit/src/equivalence.rs | 267 ++++++++++++------------------ 1 file changed, 105 insertions(+), 162 deletions(-) diff --git a/crates/circuit/src/equivalence.rs b/crates/circuit/src/equivalence.rs index f8ca9880ea91..b7456f470999 100644 --- a/crates/circuit/src/equivalence.rs +++ b/crates/circuit/src/equivalence.rs @@ -373,6 +373,7 @@ impl EquivalenceLibrary { /// base (Optional[EquivalenceLibrary]): Base equivalence library to /// be referenced if an entry is not found in this library. #[new] + #[pyo3(signature= (base=None))] fn new(base: Option<&EquivalenceLibrary>) -> Self { if let Some(base) = base { Self { @@ -405,16 +406,51 @@ impl EquivalenceLibrary { /// gate (Gate): A Gate instance. /// equivalent_circuit (QuantumCircuit): A circuit equivalently /// implementing the given Gate. - #[pyo3(name = "add_equivalence")] - fn py_add_equivalence( + fn add_equivalence( &mut self, + py: Python, gate: GateOper, - equivalent_circuit: CircuitRep, + mut equivalent_circuit: CircuitRep, ) -> PyResult<()> { - match self._add_equiv_native(gate, equivalent_circuit) { - Ok(_) => Ok(()), - Err(e) => Err(CircuitError::new_err(e.message)), + raise_if_shape_mismatch(&gate, &equivalent_circuit)?; + raise_if_param_mismatch(py, &gate.params, equivalent_circuit.parameters())?; + + let key: Key = Key { + name: gate.operation.name().to_string(), + num_qubits: gate.operation.num_qubits(), + }; + let equiv = Equivalence { + params: gate.params, + circuit: equivalent_circuit.clone(), + }; + + let target = self.set_default_node(key); + if let Some(node) = self._graph.node_weight_mut(target) { + node.equivs.push(equiv.clone()); + } + let sources: HashSet = + HashSet::from_iter(equivalent_circuit.data().iter().map(|inst| Key { + name: inst.operation.name().to_string(), + num_qubits: inst.operation.num_qubits(), + })); + let edges = Vec::from_iter(sources.iter().map(|source| { + ( + self.set_default_node(source.clone()), + target, + EdgeData { + index: self.rule_id, + num_gates: sources.len(), + rule: equiv.clone(), + source: source.clone(), + }, + ) + })); + for edge in edges { + self._graph.add_edge(edge.0, edge.1, edge.2); } + self.rule_id += 1; + self.graph = None; + Ok(()) } /// Check if a library contains any decompositions for gate. @@ -441,12 +477,40 @@ impl EquivalenceLibrary { /// gate (Gate): A Gate instance. /// entry (List['QuantumCircuit']) : A list of QuantumCircuits, each /// equivalently implementing the given Gate. - #[pyo3(name = "set_entry")] - fn py_set_entry(&mut self, gate: GateOper, entry: Vec) -> PyResult<()> { - match self._set_entry_native(gate, entry) { - Ok(_) => Ok(()), - Err(e) => Err(CircuitError::new_err(e.message)), + fn set_entry( + &mut self, + py: Python, + gate: GateOper, + mut entry: Vec, + ) -> PyResult<()> { + for equiv in entry.iter_mut() { + raise_if_shape_mismatch(&gate, equiv)?; + raise_if_param_mismatch(py, &gate.params, equiv.parameters())?; + } + + let key = Key { + name: gate.operation.name().to_string(), + num_qubits: gate.operation.num_qubits(), + }; + let node_index = self.set_default_node(key); + + if let Some(graph_ind) = self._graph.node_weight_mut(node_index) { + graph_ind.equivs.clear(); + } + + let edges: Vec = self + ._graph + .edges_directed(node_index, rustworkx_core::petgraph::Direction::Incoming) + .map(|x| x.id()) + .collect(); + for edge in edges { + self._graph.remove_edge(edge); } + for equiv in entry { + self.add_equivalence(py, gate.clone(), equiv.clone())? + } + self.graph = None; + Ok(()) } /// Gets the set of QuantumCircuits circuits from the library which @@ -466,8 +530,7 @@ impl EquivalenceLibrary { /// the library, from earliest to latest, from top to base. The /// ordering of the StandardEquivalenceLibrary will not generally be /// consistent across Qiskit versions. - #[pyo3(text_signature = "(gate, /,)")] - pub fn get_entry(&self, py: Python, gate: GateOper) -> PyResult> { + fn get_entry(&self, py: Python, gate: GateOper) -> PyResult> { let key = Key { name: gate.operation.name().to_string(), num_qubits: gate.operation.num_qubits(), @@ -486,7 +549,7 @@ impl EquivalenceLibrary { } #[getter] - fn get_graph(&mut self, py: Python<'_>) -> PyResult { + fn get_graph(&mut self, py: Python) -> PyResult { if let Some(graph) = &self.graph { Ok(graph.clone_ref(py)) } else { @@ -509,7 +572,7 @@ impl EquivalenceLibrary { } #[pyo3(name = "keys")] - pub fn py_keys(slf: PyRef) -> PyResult> { + fn py_keys(slf: PyRef) -> PyResult> { let py_set = PySet::empty_bound(slf.py())?; for key in slf.keys() { py_set.add(key.clone().into_py(slf.py()))?; @@ -517,11 +580,12 @@ impl EquivalenceLibrary { Ok(py_set) } - fn node_index(&self, key: Key) -> usize { - self.key_to_node_index[&key].index() + #[pyo3(name = "node_index")] + fn py_node_index(&self, key: &Key) -> usize { + self.node_index(key).index() } - fn __getstate__(slf: PyRef) -> PyResult> { + fn __getstate__(slf: PyRef) -> PyResult> { let ret = PyDict::new_bound(slf.py()); ret.set_item("rule_id", slf.rule_id)?; let key_to_usize_node: Bound = PyDict::new_bound(slf.py()); @@ -549,7 +613,7 @@ impl EquivalenceLibrary { Ok(ret) } - fn __setstate__(mut slf: PyRefMut, state: &Bound<'_, PyDict>) -> PyResult<()> { + fn __setstate__(mut slf: PyRefMut, state: &Bound) -> PyResult<()> { slf.rule_id = state.get_item("rule_id")?.unwrap().extract()?; let graph_nodes_ref: Bound = state.get_item("graph_nodes")?.unwrap(); let graph_nodes: &Bound = graph_nodes_ref.downcast()?; @@ -604,7 +668,7 @@ impl EquivalenceLibrary { } /// Create a new node if key not found - fn set_default_node(&mut self, key: Key) -> NodeIndex { + pub fn set_default_node(&mut self, key: Key) -> NodeIndex { if let Some(value) = self.key_to_node_index.get(&key) { *value } else { @@ -617,166 +681,45 @@ impl EquivalenceLibrary { } } - /// Rust native equivalent to `EquivalenceLibrary.add_equivalence()` - /// - /// Add a new equivalence to the library. Future queries for the Gate - /// will include the given circuit, in addition to all existing equivalences - /// (including those from base). - /// - /// Parameterized Gates (those including `qiskit.circuit.Parameters` in their - /// `Gate.params`) can be marked equivalent to parameterized circuits, - /// provided the parameters match. + /// Retrieve the `NodeIndex` that represents a `Key` /// /// # Arguments: - /// * `operation`: A Gate instance. - /// * `params`: A list of the gate's parameters. - /// * `equivalent_circuit`: A circuit equivalently implementing the given Gate. - pub fn add_equivalence( - &mut self, - operation: &OperationType, - params: &[Param], - equivalent_circuit: CircuitRep, - ) -> Result<(), EquivalenceError> { - let gate = GateOper { - operation: operation.clone(), - params: params.to_vec().into(), - }; - self._add_equiv_native(gate, equivalent_circuit) - } - - fn _add_equiv_native( - &mut self, - gate: GateOper, - mut equivalent_circuit: CircuitRep, - ) -> Result<(), EquivalenceError> { - raise_if_shape_mismatch(&gate, &equivalent_circuit)?; - raise_if_param_mismatch(&gate.params, equivalent_circuit.parameters())?; - - let key: Key = Key { - name: gate.operation.name().to_string(), - num_qubits: gate.operation.num_qubits(), - }; - let equiv = Equivalence { - params: gate.params, - circuit: equivalent_circuit.clone(), - }; - - let target = self.set_default_node(key); - if let Some(node) = self._graph.node_weight_mut(target) { - node.equivs.push(equiv.clone()); - } - let sources: HashSet = - HashSet::from_iter(equivalent_circuit.data().iter().map(|inst| Key { - name: inst.operation.name().to_string(), - num_qubits: inst.operation.num_qubits(), - })); - let edges = Vec::from_iter(sources.iter().map(|source| { - ( - self.set_default_node(source.clone()), - target, - EdgeData { - index: self.rule_id, - num_gates: sources.len(), - rule: equiv.clone(), - source: source.clone(), - }, - ) - })); - for edge in edges { - self._graph.add_edge(edge.0, edge.1, edge.2); - } - self.rule_id += 1; - self.graph = None; - Ok(()) - } - - /// Rust native equivalent to `EquivalenceLibrary.set_entry()` - /// Set the equivalence record for a Gate. Future queries for the Gate - /// will return only the circuits provided. + /// * `key`: The `Key` to look for. /// - /// Parameterized Gates (those including `qiskit.circuit.Parameters` in their - /// `Gate.params`) can be marked equivalent to parameterized circuits, - /// provided the parameters match. - /// - /// # Arguments: - /// * `operation`: A Gate instance. - /// * `params`: A list of the gate's parameters. - /// * `entry` : A list of QuantumCircuits, each equivalently implementing the given Gate. - pub fn set_entry( - &mut self, - operation: &OperationType, - params: &[Param], - entry: Vec, - ) -> Result<(), EquivalenceError> { - let gate = GateOper { - operation: operation.clone(), - params: params.to_vec().into(), - }; - self._set_entry_native(gate, entry) - } - - fn _set_entry_native( - &mut self, - gate: GateOper, - mut entry: Vec, - ) -> Result<(), EquivalenceError> { - for equiv in entry.iter_mut() { - raise_if_shape_mismatch(&gate, equiv)?; - raise_if_param_mismatch(&gate.params, equiv.parameters())?; - } - - let key = Key { - name: gate.operation.name().to_string(), - num_qubits: gate.operation.num_qubits(), - }; - let node_index = self.set_default_node(key); - - if let Some(graph_ind) = self._graph.node_weight_mut(node_index) { - graph_ind.equivs.clear(); - } - - let edges: Vec = self - ._graph - .edges_directed(node_index, rustworkx_core::petgraph::Direction::Incoming) - .map(|x| x.id()) - .collect(); - for edge in edges { - self._graph.remove_edge(edge); - } - for equiv in entry { - self._add_equiv_native(gate.clone(), equiv.clone())? - } - self.graph = None; - Ok(()) + /// # Returns: + /// `NodeIndex` + pub fn node_index(&self, key: &Key) -> NodeIndex { + self.key_to_node_index[key] } } fn raise_if_param_mismatch( + py: Python, gate_params: &[Param], circuit_parameters: &[Param], -) -> Result<(), EquivalenceError> { - let gate_params = gate_params - .iter() - .filter(|param| matches!(param, Param::ParameterExpression(_))) - .collect_vec(); - if gate_params.len() == circuit_parameters.len() - && gate_params.iter().any(|x| !circuit_parameters.contains(x)) - { - return Err(EquivalenceError::new_err(format!( +) -> PyResult<()> { + let gate_params_obj = PySet::new_bound( + py, + gate_params + .iter() + .filter(|param| matches!(param, Param::ParameterExpression(_))), + )?; + if !gate_params_obj.eq(PySet::new_bound(py, circuit_parameters)?)? { + return Err(CircuitError::new_err(format!( "Cannot add equivalence between circuit and gate \ - of different parameters. Gate params: {:#?}. \ - Circuit params: {:#?}.", + of different parameters. Gate params: {:?}. \ + Circuit params: {:?}.", gate_params, circuit_parameters ))); } Ok(()) } -fn raise_if_shape_mismatch(gate: &GateOper, circuit: &CircuitRep) -> Result<(), EquivalenceError> { +fn raise_if_shape_mismatch(gate: &GateOper, circuit: &CircuitRep) -> PyResult<()> { if gate.operation.num_qubits() != circuit.num_qubits || gate.operation.num_clbits() != circuit.num_clbits { - return Err(EquivalenceError::new_err(format!( + return Err(CircuitError::new_err(format!( "Cannot add equivalence between circuit and gate \ of different shapes. Gate: {} qubits and {} clbits. \ Circuit: {} qubits and {} clbits.", @@ -833,7 +776,7 @@ impl Display for EquivalenceError { } } -fn to_pygraph(py: Python<'_>, pet_graph: &StableDiGraph) -> PyResult +fn to_pygraph(py: Python, pet_graph: &StableDiGraph) -> PyResult where N: IntoPy + Clone, E: IntoPy + Clone, From bf9aedd72b019600b223e71f46e0107a295457b3 Mon Sep 17 00:00:00 2001 From: Raynel Sanchez <87539502+raynelfss@users.noreply.github.com> Date: Fri, 5 Jul 2024 12:15:24 -0400 Subject: [PATCH 26/45] Remove: Undo changes to Param - Fix comparison methods for `Key`, `Equivalence`, `EdgeData` and `NodeData` to account for the removal of `PartialEq` for `Param`. --- crates/circuit/src/equivalence.rs | 91 ++++++++++++++++++------------- crates/circuit/src/operations.rs | 37 ------------- 2 files changed, 54 insertions(+), 74 deletions(-) diff --git a/crates/circuit/src/equivalence.rs b/crates/circuit/src/equivalence.rs index b7456f470999..fa720ef107a4 100644 --- a/crates/circuit/src/equivalence.rs +++ b/crates/circuit/src/equivalence.rs @@ -64,8 +64,8 @@ impl Key { Self { name, num_qubits } } - fn __eq__(&self, other: Self) -> bool { - self.eq(&other) + fn __eq__(&self, other: &Self) -> bool { + self.eq(other) } fn __hash__(&self) -> u64 { @@ -97,7 +97,7 @@ impl Display for Key { } #[pyclass(sequence, module = "qiskit._accelerate.circuit.equivalence")] -#[derive(Debug, Clone, PartialEq)] +#[derive(Debug, Clone)] pub struct Equivalence { #[pyo3(get)] pub params: SmallVec<[Param; 3]>, @@ -117,8 +117,20 @@ impl Equivalence { self.to_string() } - fn __eq__(&self, other: Self) -> bool { - self.eq(&other) + fn __eq__(&self, py: Python, other: &Self) -> PyResult { + let bound_circ = self.circuit.object.bind(py); + let other_len = other.params.len(); + let mut other_iter = other.params.iter(); + + Ok(bound_circ.eq(&other.circuit.object)? + && self.params.len() == other_len + && self.params.iter().all(|param| { + let param = param.to_object(py); + let param_bound = param.bind(py); + param_bound + .eq(other_iter.next().unwrap()) + .unwrap_or_default() + })) } fn __getnewargs__(&self, py: Python) -> (Py, PyObject) { @@ -142,7 +154,7 @@ impl Display for Equivalence { } #[pyclass(sequence, module = "qiskit._accelerate.circuit.equivalence")] -#[derive(Debug, Clone, PartialEq)] +#[derive(Debug, Clone)] pub struct NodeData { #[pyo3(get)] key: Key, @@ -162,8 +174,16 @@ impl NodeData { self.to_string() } - fn __eq__(&self, other: Self) -> bool { - self.eq(&other) + fn __eq__(&self, py: Python, other: &Self) -> PyResult { + let other_len = other.equivs.len(); + let mut other_iter = other.equivs.iter(); + Ok(self.key == other.key + && self.equivs.len() == other_len + && self.equivs.iter().all(|equiv| { + equiv + .__eq__(py, other_iter.next().unwrap()) + .unwrap_or_default() + })) } fn __getnewargs__(&self, py: Python) -> (Key, Py) { @@ -190,7 +210,7 @@ impl Display for NodeData { } #[pyclass(sequence, module = "qiskit._accelerate.circuit.equivalence")] -#[derive(Debug, Clone, PartialEq)] +#[derive(Debug, Clone)] pub struct EdgeData { #[pyo3(get)] pub index: usize, @@ -219,8 +239,11 @@ impl EdgeData { self.to_string() } - fn __eq__(&self, other: Self) -> bool { - self.eq(&other) + fn __eq__(&self, py: Python, other: Self) -> PyResult { + Ok(self.index == other.index + && self.num_gates == other.num_gates + && self.source == other.source + && self.rule.__eq__(py, &other.rule)?) } fn __getnewargs__(slf: PyRef) -> (usize, usize, Equivalence, Key) { @@ -243,7 +266,7 @@ impl Display for EdgeData { } } -// Enum to extract circuit instructions more broadly +/// Enum that helps extract the Operation and Parameters on a Gate. #[derive(Debug, Clone)] pub struct GateOper { operation: OperationType, @@ -266,38 +289,32 @@ pub struct CircuitRep { object: PyObject, pub num_qubits: u32, pub num_clbits: u32, - params: Option>, + params: Option, data: Option>, // TODO: Have a valid implementation of CircuiData that's usable in Rust. } impl CircuitRep { - pub fn parameters(&mut self) -> &[Param] { + pub fn parameters(&mut self, py: Python) -> PyResult { if self.params.is_none() { - let params = Python::with_gil(|py| -> PyResult> { - self.object - .bind(py) - .getattr("parameters")? - .getattr("data")? - .extract() - }) - .unwrap_or_default(); - self.params = Some(params); - return self.params.as_ref().unwrap(); + let params = self.object.getattr(py, "parameters")?; + self.params = Some(params.clone_ref(py)); + return Ok(params); } - return self.params.as_ref().unwrap(); + return Ok(self + .params + .as_ref() + .map(|params| params.clone_ref(py)) + .unwrap()); } - pub fn data(&mut self) -> &[CircuitInstruction] { + pub fn data(&mut self, py: Python) -> PyResult<&[CircuitInstruction]> { if self.data.is_none() { - let data = Python::with_gil(|py| -> PyResult> { - self.object.bind(py).getattr("data")?.extract() - }) - .unwrap_or_default(); + let data = self.object.bind(py).getattr("data")?.extract()?; self.data = Some(data); - return self.data.as_ref().unwrap(); + return Ok(self.data.as_ref().unwrap()); } - return self.data.as_ref().unwrap(); + return Ok(self.data.as_ref().unwrap()); } } @@ -413,7 +430,7 @@ impl EquivalenceLibrary { mut equivalent_circuit: CircuitRep, ) -> PyResult<()> { raise_if_shape_mismatch(&gate, &equivalent_circuit)?; - raise_if_param_mismatch(py, &gate.params, equivalent_circuit.parameters())?; + raise_if_param_mismatch(py, &gate.params, equivalent_circuit.parameters(py)?)?; let key: Key = Key { name: gate.operation.name().to_string(), @@ -429,7 +446,7 @@ impl EquivalenceLibrary { node.equivs.push(equiv.clone()); } let sources: HashSet = - HashSet::from_iter(equivalent_circuit.data().iter().map(|inst| Key { + HashSet::from_iter(equivalent_circuit.data(py)?.iter().map(|inst| Key { name: inst.operation.name().to_string(), num_qubits: inst.operation.num_qubits(), })); @@ -485,7 +502,7 @@ impl EquivalenceLibrary { ) -> PyResult<()> { for equiv in entry.iter_mut() { raise_if_shape_mismatch(&gate, equiv)?; - raise_if_param_mismatch(py, &gate.params, equiv.parameters())?; + raise_if_param_mismatch(py, &gate.params, equiv.parameters(py)?)?; } let key = Key { @@ -696,7 +713,7 @@ impl EquivalenceLibrary { fn raise_if_param_mismatch( py: Python, gate_params: &[Param], - circuit_parameters: &[Param], + circuit_parameters: PyObject, ) -> PyResult<()> { let gate_params_obj = PySet::new_bound( py, @@ -704,7 +721,7 @@ fn raise_if_param_mismatch( .iter() .filter(|param| matches!(param, Param::ParameterExpression(_))), )?; - if !gate_params_obj.eq(PySet::new_bound(py, circuit_parameters)?)? { + if !gate_params_obj.eq(&circuit_parameters)? { return Err(CircuitError::new_err(format!( "Cannot add equivalence between circuit and gate \ of different parameters. Gate params: {:?}. \ diff --git a/crates/circuit/src/operations.rs b/crates/circuit/src/operations.rs index 6cb7ed7893bf..3bfef81d29ce 100644 --- a/crates/circuit/src/operations.rs +++ b/crates/circuit/src/operations.rs @@ -184,43 +184,6 @@ impl ToPyObject for Param { } } -impl Param { - fn compare(one: &PyObject, other: &PyObject) -> bool { - Python::with_gil(|py| -> PyResult { - let other_bound = other.bind(py); - Ok(other_bound.eq(one)? || other_bound.is(one)) - }) - .unwrap_or_default() - } -} - -impl PartialEq for Param { - fn eq(&self, other: &Self) -> bool { - match (self, other) { - (Param::Float(s), Param::Float(other)) => s == other, - (Param::ParameterExpression(one), Param::ParameterExpression(other)) => { - Self::compare(one, other) - } - (Param::Obj(one), Param::Obj(other)) => Self::compare(one, other), - _ => false, - } - } -} - -impl std::fmt::Display for Param { - fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { - let display_name: String = Python::with_gil(|py| -> PyResult { - match self { - Param::ParameterExpression(obj) => obj.call_method0(py, "__repr__")?.extract(py), - Param::Float(float_param) => Ok(format!("Parameter({})", float_param)), - Param::Obj(obj) => obj.call_method0(py, "__repr__")?.extract(py), - } - }) - .unwrap_or("None".to_owned()); - write!(f, "{}", display_name) - } -} - #[derive(Clone, Debug, Copy, Eq, PartialEq, Hash)] #[pyclass(module = "qiskit._accelerate.circuit")] pub enum StandardGate { From 08915c0ecc35f7833463b8fa543db2a89763e8c1 Mon Sep 17 00:00:00 2001 From: Raynel Sanchez <87539502+raynelfss@users.noreply.github.com> Date: Fri, 12 Jul 2024 16:49:27 -0400 Subject: [PATCH 27/45] Fix: Leverage usage of `CircuitData` for accessing the `QuantumCircuit` intructions in rust. - Change implementation of `CircuitRef, to leverage the use of `CircuitData`. --- crates/circuit/src/equivalence.rs | 144 +++++++++--------------------- 1 file changed, 42 insertions(+), 102 deletions(-) diff --git a/crates/circuit/src/equivalence.rs b/crates/circuit/src/equivalence.rs index fa720ef107a4..f5fb15e449a3 100644 --- a/crates/circuit/src/equivalence.rs +++ b/crates/circuit/src/equivalence.rs @@ -32,7 +32,8 @@ use rustworkx_core::petgraph::{ visit::EdgeRef, }; -use crate::circuit_instruction::{convert_py_to_operation_type, CircuitInstruction}; +use crate::circuit_data::CircuitData; +use crate::circuit_instruction::{convert_py_to_operation_type, PackedInstruction}; use crate::imports::ImportOnceCell; use crate::operations::Param; use crate::operations::{Operation, OperationType}; @@ -117,25 +118,16 @@ impl Equivalence { self.to_string() } - fn __eq__(&self, py: Python, other: &Self) -> PyResult { - let bound_circ = self.circuit.object.bind(py); - let other_len = other.params.len(); - let mut other_iter = other.params.iter(); - - Ok(bound_circ.eq(&other.circuit.object)? - && self.params.len() == other_len - && self.params.iter().all(|param| { - let param = param.to_object(py); - let param_bound = param.bind(py); - param_bound - .eq(other_iter.next().unwrap()) - .unwrap_or_default() - })) + fn __eq__(slf: &Bound, other: &Bound) -> PyResult { + let other_params = other.getattr("params")?; + let other_circuit = other.getattr("circuit")?; + Ok(other_params.eq(&slf.getattr("params")?)? + && other_circuit.eq(&slf.getattr("circuit")?)?) } - fn __getnewargs__(&self, py: Python) -> (Py, PyObject) { + fn __getnewargs__(&self, py: Python) -> (Py, CircuitRep) { let params = PyList::new_bound(py, self.params.iter().map(|param| param.to_object(py))); - (params.unbind(), self.circuit.object.clone_ref(py)) + (params.unbind(), self.circuit.clone()) } } @@ -143,7 +135,7 @@ impl Display for Equivalence { fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { write!( f, - "Equivalence(params=[{}], circuit={})", + "Equivalence(params=[{}], circuit={:?})", self.params .iter() .map(|param| format!("{:?}", param)) @@ -174,16 +166,9 @@ impl NodeData { self.to_string() } - fn __eq__(&self, py: Python, other: &Self) -> PyResult { - let other_len = other.equivs.len(); - let mut other_iter = other.equivs.iter(); - Ok(self.key == other.key - && self.equivs.len() == other_len - && self.equivs.iter().all(|equiv| { - equiv - .__eq__(py, other_iter.next().unwrap()) - .unwrap_or_default() - })) + fn __eq__(slf: &Bound, other: &Bound) -> PyResult { + Ok(slf.getattr("key")?.eq(other.getattr("key")?)? + && slf.getattr("equivs")?.eq(other.getattr("equivs")?)?) } fn __getnewargs__(&self, py: Python) -> (Key, Py) { @@ -239,11 +224,11 @@ impl EdgeData { self.to_string() } - fn __eq__(&self, py: Python, other: Self) -> PyResult { - Ok(self.index == other.index - && self.num_gates == other.num_gates - && self.source == other.source - && self.rule.__eq__(py, &other.rule)?) + fn __eq__(slf: &Bound, other: &Bound) -> PyResult { + Ok(slf.getattr("index")?.eq(other.getattr("index")?)? + && slf.getattr("num_gates")?.eq(other.getattr("num_gates")?)? + && slf.getattr("rule")?.eq(other.getattr("rule")?)? + && slf.getattr("source")?.eq(other.getattr("source")?)?) } fn __getnewargs__(slf: PyRef) -> (usize, usize, Equivalence, Key) { @@ -283,52 +268,27 @@ impl<'py> FromPyObject<'py> for GateOper { } } -/// Temporary interpretation of QuantumCircuit +/// Representation of QuantumCircuit which the original circuit object + an +/// instance of `CircuitData`. #[derive(Debug, Clone)] pub struct CircuitRep { object: PyObject, - pub num_qubits: u32, - pub num_clbits: u32, - params: Option, - data: Option>, - // TODO: Have a valid implementation of CircuiData that's usable in Rust. + data: CircuitData, } impl CircuitRep { - pub fn parameters(&mut self, py: Python) -> PyResult { - if self.params.is_none() { - let params = self.object.getattr(py, "parameters")?; - self.params = Some(params.clone_ref(py)); - return Ok(params); - } - return Ok(self - .params - .as_ref() - .map(|params| params.clone_ref(py)) - .unwrap()); - } - - pub fn data(&mut self, py: Python) -> PyResult<&[CircuitInstruction]> { - if self.data.is_none() { - let data = self.object.bind(py).getattr("data")?.extract()?; - self.data = Some(data); - return Ok(self.data.as_ref().unwrap()); - } - return Ok(self.data.as_ref().unwrap()); + pub fn iter(&self) -> impl Iterator { + self.data.iter() } } impl FromPyObject<'_> for CircuitRep { fn extract_bound(ob: &Bound<'_, PyAny>) -> PyResult { if ob.is_instance(QUANTUMCIRCUIT.get_bound(ob.py()))? { - let num_qubits = ob.getattr("num_qubits")?.extract()?; - let num_clbits = ob.getattr("num_clbits")?.extract()?; + let data = ob.getattr("_data")?.extract()?; Ok(Self { object: ob.into_py(ob.py()), - num_qubits, - num_clbits, - params: None, - data: None, + data, }) } else { Err(PyTypeError::new_err( @@ -338,27 +298,6 @@ impl FromPyObject<'_> for CircuitRep { } } -impl Display for CircuitRep { - fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { - let py_rep_str = Python::with_gil(|py| -> PyResult { - let bound = self.object.bind(py); - bound.repr().map(|pystring| pystring.to_string()) - }) - .unwrap(); - write!(f, "{}", py_rep_str) - } -} - -impl PartialEq for CircuitRep { - fn eq(&self, other: &Self) -> bool { - Python::with_gil(|py| -> PyResult { - let bound = other.object.bind(py); - bound.eq(&self.object) - }) - .unwrap_or_default() - } -} - impl IntoPy for CircuitRep { fn into_py(self, _py: Python<'_>) -> PyObject { self.object @@ -409,8 +348,6 @@ impl EquivalenceLibrary { } } - // TODO: Add a way of returning a graph - /// Add a new equivalence to the library. Future queries for the Gate /// will include the given circuit, in addition to all existing equivalences /// (including those from base). @@ -427,10 +364,14 @@ impl EquivalenceLibrary { &mut self, py: Python, gate: GateOper, - mut equivalent_circuit: CircuitRep, + equivalent_circuit: CircuitRep, ) -> PyResult<()> { raise_if_shape_mismatch(&gate, &equivalent_circuit)?; - raise_if_param_mismatch(py, &gate.params, equivalent_circuit.parameters(py)?)?; + raise_if_param_mismatch( + py, + &gate.params, + equivalent_circuit.data.get_params_unsorted(py)?, + )?; let key: Key = Key { name: gate.operation.name().to_string(), @@ -445,11 +386,10 @@ impl EquivalenceLibrary { if let Some(node) = self._graph.node_weight_mut(target) { node.equivs.push(equiv.clone()); } - let sources: HashSet = - HashSet::from_iter(equivalent_circuit.data(py)?.iter().map(|inst| Key { - name: inst.operation.name().to_string(), - num_qubits: inst.operation.num_qubits(), - })); + let sources: HashSet = HashSet::from_iter(equivalent_circuit.iter().map(|inst| Key { + name: inst.op.name().to_string(), + num_qubits: inst.op.num_qubits(), + })); let edges = Vec::from_iter(sources.iter().map(|source| { ( self.set_default_node(source.clone()), @@ -502,7 +442,7 @@ impl EquivalenceLibrary { ) -> PyResult<()> { for equiv in entry.iter_mut() { raise_if_shape_mismatch(&gate, equiv)?; - raise_if_param_mismatch(py, &gate.params, equiv.parameters(py)?)?; + raise_if_param_mismatch(py, &gate.params, equiv.data.get_params_unsorted(py)?)?; } let key = Key { @@ -713,7 +653,7 @@ impl EquivalenceLibrary { fn raise_if_param_mismatch( py: Python, gate_params: &[Param], - circuit_parameters: PyObject, + circuit_parameters: Py, ) -> PyResult<()> { let gate_params_obj = PySet::new_bound( py, @@ -733,8 +673,8 @@ fn raise_if_param_mismatch( } fn raise_if_shape_mismatch(gate: &GateOper, circuit: &CircuitRep) -> PyResult<()> { - if gate.operation.num_qubits() != circuit.num_qubits - || gate.operation.num_clbits() != circuit.num_clbits + if gate.operation.num_qubits() != circuit.data.num_qubits() as u32 + || gate.operation.num_clbits() != circuit.data.num_clbits() as u32 { return Err(CircuitError::new_err(format!( "Cannot add equivalence between circuit and gate \ @@ -742,8 +682,8 @@ fn raise_if_shape_mismatch(gate: &GateOper, circuit: &CircuitRep) -> PyResult<() Circuit: {} qubits and {} clbits.", gate.operation.num_qubits(), gate.operation.num_clbits(), - circuit.num_qubits, - circuit.num_clbits + circuit.data.num_qubits(), + circuit.data.num_clbits() ))); } Ok(()) @@ -763,7 +703,7 @@ fn rebind_equiv(py: Python, equiv: Equivalence, query_params: &[Param]) -> PyRes param_map.set_item(param_key, param_val)?; } let kwargs = [("inplace", false), ("flat_input", true)].into_py_dict_bound(py); - let new_equiv = equiv_circuit.object.call_method_bound( + let new_equiv = equiv_circuit.into_py(py).call_method_bound( py, "assign_parameters", (param_map,), From d24c13469740ca8127c487fe7488577ddfc64a66 Mon Sep 17 00:00:00 2001 From: Raynel Sanchez <87539502+raynelfss@users.noreply.github.com> Date: Mon, 15 Jul 2024 16:00:47 -0400 Subject: [PATCH 28/45] Add: `data()` method to avoid extracting `CircuitData` - Add `py_clone` to perform shallow clones of a `CircuitRef` object by cloning the references to the `QuantumCircuit` object. - Extract `num_qubits` and `num_clbits` for CircuitRep. - Add wrapper over `add_equivalence` to be able to accept references and avoid unnecessary cloning of `GateRep` objects in `set_entry`. - Remove stray mutability of `entry` in `set_entry`. --- crates/circuit/src/equivalence.rs | 193 +++++++++++++++++------------- 1 file changed, 111 insertions(+), 82 deletions(-) diff --git a/crates/circuit/src/equivalence.rs b/crates/circuit/src/equivalence.rs index f5fb15e449a3..2ef58072441c 100644 --- a/crates/circuit/src/equivalence.rs +++ b/crates/circuit/src/equivalence.rs @@ -33,7 +33,7 @@ use rustworkx_core::petgraph::{ }; use crate::circuit_data::CircuitData; -use crate::circuit_instruction::{convert_py_to_operation_type, PackedInstruction}; +use crate::circuit_instruction::convert_py_to_operation_type; use crate::imports::ImportOnceCell; use crate::operations::Param; use crate::operations::{Operation, OperationType}; @@ -125,9 +125,10 @@ impl Equivalence { && other_circuit.eq(&slf.getattr("circuit")?)?) } - fn __getnewargs__(&self, py: Python) -> (Py, CircuitRep) { - let params = PyList::new_bound(py, self.params.iter().map(|param| param.to_object(py))); - (params.unbind(), self.circuit.clone()) + fn __getnewargs__<'py>( + slf: &'py Bound<'py, Self>, + ) -> PyResult<(Bound<'py, PyAny>, Bound<'py, PyAny>)> { + Ok((slf.getattr("params")?, slf.getattr("circuit")?)) } } @@ -171,15 +172,10 @@ impl NodeData { && slf.getattr("equivs")?.eq(other.getattr("equivs")?)?) } - fn __getnewargs__(&self, py: Python) -> (Key, Py) { - ( - self.key.clone(), - PyList::new_bound( - py, - self.equivs.iter().map(|equiv| equiv.clone().into_py(py)), - ) - .unbind(), - ) + fn __getnewargs__<'py>( + slf: &'py Bound<'py, Self>, + ) -> PyResult<(Bound<'py, PyAny>, Bound<'py, PyAny>)> { + Ok((slf.getattr("key")?, slf.getattr("equivs")?)) } } @@ -224,11 +220,13 @@ impl EdgeData { self.to_string() } - fn __eq__(slf: &Bound, other: &Bound) -> PyResult { - Ok(slf.getattr("index")?.eq(other.getattr("index")?)? - && slf.getattr("num_gates")?.eq(other.getattr("num_gates")?)? - && slf.getattr("rule")?.eq(other.getattr("rule")?)? - && slf.getattr("source")?.eq(other.getattr("source")?)?) + fn __eq__(slf: &Bound, other: &Bound) -> PyResult { + let other_borrowed = other.borrow(); + let slf_borrowed = slf.borrow(); + Ok(slf_borrowed.index == other_borrowed.index + && slf_borrowed.num_gates == other_borrowed.num_gates + && slf_borrowed.source == other_borrowed.source + && other.getattr("rule")?.eq(slf.getattr("rule")?)?) } fn __getnewargs__(slf: PyRef) -> (usize, usize, Equivalence, Key) { @@ -273,22 +271,41 @@ impl<'py> FromPyObject<'py> for GateOper { #[derive(Debug, Clone)] pub struct CircuitRep { object: PyObject, - data: CircuitData, + pub num_qubits: usize, + pub num_clbits: usize, + data: Py, } impl CircuitRep { - pub fn iter(&self) -> impl Iterator { - self.data.iter() + /// Allows access to the circuit's data through a Python reference. + pub fn data<'py>(&'py self, py: Python<'py>) -> PyRef<'py, CircuitData> { + self.data.borrow(py) + } + + /// Performs a shallow cloning of the structure by using `clone_ref()`. + pub fn py_clone(&self, py: Python) -> Self { + Self { + object: self.object.clone_ref(py), + num_qubits: self.num_qubits, + num_clbits: self.num_clbits, + data: self.data.clone_ref(py), + } } } impl FromPyObject<'_> for CircuitRep { fn extract_bound(ob: &Bound<'_, PyAny>) -> PyResult { if ob.is_instance(QUANTUMCIRCUIT.get_bound(ob.py()))? { - let data = ob.getattr("_data")?.extract()?; + let data: Bound = ob.getattr("_data")?; + let data_downcast: Bound = data.downcast_into()?; + let data_borrow: PyRef = data_downcast.borrow(); + let num_qubits: usize = data_borrow.num_qubits(); + let num_clbits: usize = data_borrow.num_clbits(); Ok(Self { object: ob.into_py(ob.py()), - data, + num_qubits, + num_clbits, + data: data_downcast.unbind(), }) } else { Err(PyTypeError::new_err( @@ -304,6 +321,12 @@ impl IntoPy for CircuitRep { } } +impl ToPyObject for CircuitRep { + fn to_object(&self, py: Python<'_>) -> PyObject { + self.object.clone_ref(py) + } +} + // Custom Types type GraphType = StableDiGraph; type KTIType = HashMap; @@ -360,54 +383,14 @@ impl EquivalenceLibrary { /// gate (Gate): A Gate instance. /// equivalent_circuit (QuantumCircuit): A circuit equivalently /// implementing the given Gate. - fn add_equivalence( + #[pyo3(name = "add_equivalence")] + fn py_add_equivalence( &mut self, py: Python, gate: GateOper, equivalent_circuit: CircuitRep, ) -> PyResult<()> { - raise_if_shape_mismatch(&gate, &equivalent_circuit)?; - raise_if_param_mismatch( - py, - &gate.params, - equivalent_circuit.data.get_params_unsorted(py)?, - )?; - - let key: Key = Key { - name: gate.operation.name().to_string(), - num_qubits: gate.operation.num_qubits(), - }; - let equiv = Equivalence { - params: gate.params, - circuit: equivalent_circuit.clone(), - }; - - let target = self.set_default_node(key); - if let Some(node) = self._graph.node_weight_mut(target) { - node.equivs.push(equiv.clone()); - } - let sources: HashSet = HashSet::from_iter(equivalent_circuit.iter().map(|inst| Key { - name: inst.op.name().to_string(), - num_qubits: inst.op.num_qubits(), - })); - let edges = Vec::from_iter(sources.iter().map(|source| { - ( - self.set_default_node(source.clone()), - target, - EdgeData { - index: self.rule_id, - num_gates: sources.len(), - rule: equiv.clone(), - source: source.clone(), - }, - ) - })); - for edge in edges { - self._graph.add_edge(edge.0, edge.1, edge.2); - } - self.rule_id += 1; - self.graph = None; - Ok(()) + self.add_equivalence(py, &gate, equivalent_circuit) } /// Check if a library contains any decompositions for gate. @@ -434,15 +417,10 @@ impl EquivalenceLibrary { /// gate (Gate): A Gate instance. /// entry (List['QuantumCircuit']) : A list of QuantumCircuits, each /// equivalently implementing the given Gate. - fn set_entry( - &mut self, - py: Python, - gate: GateOper, - mut entry: Vec, - ) -> PyResult<()> { - for equiv in entry.iter_mut() { + fn set_entry(&mut self, py: Python, gate: GateOper, entry: Vec) -> PyResult<()> { + for equiv in entry.iter() { raise_if_shape_mismatch(&gate, equiv)?; - raise_if_param_mismatch(py, &gate.params, equiv.data.get_params_unsorted(py)?)?; + raise_if_param_mismatch(py, &gate.params, equiv.data(py).get_params_unsorted(py)?)?; } let key = Key { @@ -464,7 +442,7 @@ impl EquivalenceLibrary { self._graph.remove_edge(edge); } for equiv in entry { - self.add_equivalence(py, gate.clone(), equiv.clone())? + self.add_equivalence(py, &gate, equiv)? } self.graph = None; Ok(()) @@ -547,7 +525,7 @@ impl EquivalenceLibrary { ret.set_item("rule_id", slf.rule_id)?; let key_to_usize_node: Bound = PyDict::new_bound(slf.py()); for (key, val) in slf.key_to_node_index.iter() { - key_to_usize_node.set_item((&key.name, key.num_qubits), val.index())?; + key_to_usize_node.set_item(key.clone().into_py(slf.py()), val.index())?; } ret.set_item("key_to_node_index", key_to_usize_node)?; let graph_nodes: Bound = PyList::empty_bound(slf.py()); @@ -591,9 +569,9 @@ impl EquivalenceLibrary { slf.key_to_node_index = state .get_item("key_to_node_index")? .unwrap() - .extract::>()? + .extract::>()? .into_iter() - .map(|((name, num_qubits), val)| (Key::new(name, num_qubits), NodeIndex::new(val))) + .map(|(key, val)| (key, NodeIndex::new(val))) .collect(); slf.graph = None; Ok(()) @@ -602,6 +580,57 @@ impl EquivalenceLibrary { // Rust native methods impl EquivalenceLibrary { + fn add_equivalence( + &mut self, + py: Python, + gate: &GateOper, + equivalent_circuit: CircuitRep, + ) -> PyResult<()> { + raise_if_shape_mismatch(gate, &equivalent_circuit)?; + raise_if_param_mismatch( + py, + &gate.params, + equivalent_circuit.data(py).get_params_unsorted(py)?, + )?; + + let key: Key = Key { + name: gate.operation.name().to_string(), + num_qubits: gate.operation.num_qubits(), + }; + let equiv = Equivalence { + params: gate.params.clone(), + circuit: equivalent_circuit.py_clone(py), + }; + + let target = self.set_default_node(key); + if let Some(node) = self._graph.node_weight_mut(target) { + node.equivs.push(equiv.clone()); + } + let sources: HashSet = + HashSet::from_iter(equivalent_circuit.data(py).iter().map(|inst| Key { + name: inst.op.name().to_string(), + num_qubits: inst.op.num_qubits(), + })); + let edges = Vec::from_iter(sources.iter().map(|source| { + ( + self.set_default_node(source.clone()), + target, + EdgeData { + index: self.rule_id, + num_gates: sources.len(), + rule: equiv.clone(), + source: source.clone(), + }, + ) + })); + for edge in edges { + self._graph.add_edge(edge.0, edge.1, edge.2); + } + self.rule_id += 1; + self.graph = None; + Ok(()) + } + /// Rust native equivalent to `EquivalenceLibrary.has_entry()` /// /// Check if a library contains any decompositions for gate. @@ -673,8 +702,8 @@ fn raise_if_param_mismatch( } fn raise_if_shape_mismatch(gate: &GateOper, circuit: &CircuitRep) -> PyResult<()> { - if gate.operation.num_qubits() != circuit.data.num_qubits() as u32 - || gate.operation.num_clbits() != circuit.data.num_clbits() as u32 + if gate.operation.num_qubits() != circuit.num_qubits as u32 + || gate.operation.num_clbits() != circuit.num_clbits as u32 { return Err(CircuitError::new_err(format!( "Cannot add equivalence between circuit and gate \ @@ -682,8 +711,8 @@ fn raise_if_shape_mismatch(gate: &GateOper, circuit: &CircuitRep) -> PyResult<() Circuit: {} qubits and {} clbits.", gate.operation.num_qubits(), gate.operation.num_clbits(), - circuit.data.num_qubits(), - circuit.data.num_clbits() + circuit.num_qubits, + circuit.num_clbits ))); } Ok(()) From 67723059492b65c11247b833696bd4a32b49bf74 Mon Sep 17 00:00:00 2001 From: Raynel Sanchez <87539502+raynelfss@users.noreply.github.com> Date: Fri, 19 Jul 2024 12:44:43 -0400 Subject: [PATCH 29/45] Fix: Make `graph` attribute public. --- crates/circuit/src/equivalence.rs | 48 +++++++++++++++---------------- 1 file changed, 24 insertions(+), 24 deletions(-) diff --git a/crates/circuit/src/equivalence.rs b/crates/circuit/src/equivalence.rs index 2ef58072441c..dd0f3dc0e67a 100644 --- a/crates/circuit/src/equivalence.rs +++ b/crates/circuit/src/equivalence.rs @@ -338,10 +338,10 @@ type KTIType = HashMap; )] #[derive(Debug, Clone)] pub struct EquivalenceLibrary { - _graph: GraphType, + pub graph: GraphType, key_to_node_index: KTIType, rule_id: usize, - graph: Option, + _graph: Option, } #[pymethods] @@ -356,17 +356,17 @@ impl EquivalenceLibrary { fn new(base: Option<&EquivalenceLibrary>) -> Self { if let Some(base) = base { Self { - _graph: base._graph.clone(), + graph: base.graph.clone(), key_to_node_index: base.key_to_node_index.clone(), rule_id: base.rule_id, - graph: None, + _graph: None, } } else { Self { - _graph: GraphType::new(), + graph: GraphType::new(), key_to_node_index: KTIType::new(), rule_id: 0_usize, - graph: None, + _graph: None, } } } @@ -429,22 +429,22 @@ impl EquivalenceLibrary { }; let node_index = self.set_default_node(key); - if let Some(graph_ind) = self._graph.node_weight_mut(node_index) { + if let Some(graph_ind) = self.graph.node_weight_mut(node_index) { graph_ind.equivs.clear(); } let edges: Vec = self - ._graph + .graph .edges_directed(node_index, rustworkx_core::petgraph::Direction::Incoming) .map(|x| x.id()) .collect(); for edge in edges { - self._graph.remove_edge(edge); + self.graph.remove_edge(edge); } for equiv in entry { self.add_equivalence(py, &gate, equiv)? } - self.graph = None; + self._graph = None; Ok(()) } @@ -485,12 +485,12 @@ impl EquivalenceLibrary { #[getter] fn get_graph(&mut self, py: Python) -> PyResult { - if let Some(graph) = &self.graph { + if let Some(graph) = &self._graph { Ok(graph.clone_ref(py)) } else { - self.graph = Some(to_pygraph(py, &self._graph)?); + self._graph = Some(to_pygraph(py, &self.graph)?); Ok(self - .graph + ._graph .as_ref() .map(|graph| graph.clone_ref(py)) .unwrap()) @@ -500,7 +500,7 @@ impl EquivalenceLibrary { /// Get all the equivalences for the given key pub fn _get_equivalences(&self, key: &Key) -> Vec { if let Some(key_in) = self.key_to_node_index.get(key) { - self._graph[*key_in].equivs.clone() + self.graph[*key_in].equivs.clone() } else { vec![] } @@ -529,11 +529,11 @@ impl EquivalenceLibrary { } ret.set_item("key_to_node_index", key_to_usize_node)?; let graph_nodes: Bound = PyList::empty_bound(slf.py()); - for weight in slf._graph.node_weights() { + for weight in slf.graph.node_weights() { graph_nodes.append(weight.clone().into_py(slf.py()))?; } ret.set_item("graph_nodes", graph_nodes.unbind())?; - let edges = slf._graph.edge_references().map(|edge| { + let edges = slf.graph.edge_references().map(|edge| { ( edge.source().index(), edge.target().index(), @@ -554,13 +554,13 @@ impl EquivalenceLibrary { let graph_nodes: &Bound = graph_nodes_ref.downcast()?; let graph_edge_ref: Bound = state.get_item("graph_edges")?.unwrap(); let graph_edges: &Bound = graph_edge_ref.downcast()?; - slf._graph = GraphType::new(); + slf.graph = GraphType::new(); for node_weight in graph_nodes { - slf._graph.add_node(node_weight.extract()?); + slf.graph.add_node(node_weight.extract()?); } for edge in graph_edges { let (source_node, target_node, edge_weight) = edge.extract()?; - slf._graph.add_edge( + slf.graph.add_edge( NodeIndex::new(source_node), NodeIndex::new(target_node), edge_weight, @@ -573,7 +573,7 @@ impl EquivalenceLibrary { .into_iter() .map(|(key, val)| (key, NodeIndex::new(val))) .collect(); - slf.graph = None; + slf._graph = None; Ok(()) } } @@ -603,7 +603,7 @@ impl EquivalenceLibrary { }; let target = self.set_default_node(key); - if let Some(node) = self._graph.node_weight_mut(target) { + if let Some(node) = self.graph.node_weight_mut(target) { node.equivs.push(equiv.clone()); } let sources: HashSet = @@ -624,10 +624,10 @@ impl EquivalenceLibrary { ) })); for edge in edges { - self._graph.add_edge(edge.0, edge.1, edge.2); + self.graph.add_edge(edge.0, edge.1, edge.2); } self.rule_id += 1; - self.graph = None; + self._graph = None; Ok(()) } @@ -658,7 +658,7 @@ impl EquivalenceLibrary { if let Some(value) = self.key_to_node_index.get(&key) { *value } else { - let node = self._graph.add_node(NodeData { + let node = self.graph.add_node(NodeData { key: key.clone(), equivs: vec![], }); From 1e44817b5a158e90c296cad65c5c34955cca6801 Mon Sep 17 00:00:00 2001 From: Raynel Sanchez <87539502+raynelfss@users.noreply.github.com> Date: Fri, 19 Jul 2024 15:05:16 -0400 Subject: [PATCH 30/45] Fix: Make `NoteData` attributes public. --- crates/circuit/src/equivalence.rs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/crates/circuit/src/equivalence.rs b/crates/circuit/src/equivalence.rs index dd0f3dc0e67a..9128b045525d 100644 --- a/crates/circuit/src/equivalence.rs +++ b/crates/circuit/src/equivalence.rs @@ -150,9 +150,9 @@ impl Display for Equivalence { #[derive(Debug, Clone)] pub struct NodeData { #[pyo3(get)] - key: Key, + pub key: Key, #[pyo3(get)] - equivs: Vec, + pub equivs: Vec, } #[pymethods] From 22210e30142f642195b5d6f33c1c3d554ee2e186 Mon Sep 17 00:00:00 2001 From: Raynel Sanchez <87539502+raynelfss@users.noreply.github.com> Date: Sun, 21 Jul 2024 00:30:33 -0400 Subject: [PATCH 31/45] Fix: Revert reference to `CircuitData`, extract instead. --- crates/circuit/src/equivalence.rs | 23 +++++++++-------------- 1 file changed, 9 insertions(+), 14 deletions(-) diff --git a/crates/circuit/src/equivalence.rs b/crates/circuit/src/equivalence.rs index 9128b045525d..6e4e155d1431 100644 --- a/crates/circuit/src/equivalence.rs +++ b/crates/circuit/src/equivalence.rs @@ -273,22 +273,17 @@ pub struct CircuitRep { object: PyObject, pub num_qubits: usize, pub num_clbits: usize, - data: Py, + pub data: CircuitData, } impl CircuitRep { - /// Allows access to the circuit's data through a Python reference. - pub fn data<'py>(&'py self, py: Python<'py>) -> PyRef<'py, CircuitData> { - self.data.borrow(py) - } - /// Performs a shallow cloning of the structure by using `clone_ref()`. pub fn py_clone(&self, py: Python) -> Self { Self { object: self.object.clone_ref(py), num_qubits: self.num_qubits, num_clbits: self.num_clbits, - data: self.data.clone_ref(py), + data: self.data.clone(), } } } @@ -298,14 +293,14 @@ impl FromPyObject<'_> for CircuitRep { if ob.is_instance(QUANTUMCIRCUIT.get_bound(ob.py()))? { let data: Bound = ob.getattr("_data")?; let data_downcast: Bound = data.downcast_into()?; - let data_borrow: PyRef = data_downcast.borrow(); - let num_qubits: usize = data_borrow.num_qubits(); - let num_clbits: usize = data_borrow.num_clbits(); + let data_extract: CircuitData = data_downcast.extract()?; + let num_qubits: usize = data_extract.num_qubits(); + let num_clbits: usize = data_extract.num_clbits(); Ok(Self { object: ob.into_py(ob.py()), num_qubits, num_clbits, - data: data_downcast.unbind(), + data: data_extract, }) } else { Err(PyTypeError::new_err( @@ -420,7 +415,7 @@ impl EquivalenceLibrary { fn set_entry(&mut self, py: Python, gate: GateOper, entry: Vec) -> PyResult<()> { for equiv in entry.iter() { raise_if_shape_mismatch(&gate, equiv)?; - raise_if_param_mismatch(py, &gate.params, equiv.data(py).get_params_unsorted(py)?)?; + raise_if_param_mismatch(py, &gate.params, equiv.data.get_params_unsorted(py)?)?; } let key = Key { @@ -590,7 +585,7 @@ impl EquivalenceLibrary { raise_if_param_mismatch( py, &gate.params, - equivalent_circuit.data(py).get_params_unsorted(py)?, + equivalent_circuit.data.get_params_unsorted(py)?, )?; let key: Key = Key { @@ -607,7 +602,7 @@ impl EquivalenceLibrary { node.equivs.push(equiv.clone()); } let sources: HashSet = - HashSet::from_iter(equivalent_circuit.data(py).iter().map(|inst| Key { + HashSet::from_iter(equivalent_circuit.data.iter().map(|inst| Key { name: inst.op.name().to_string(), num_qubits: inst.op.num_qubits(), })); From c99ca3f17aea86b57365781ebbc4f00680d55f3d Mon Sep 17 00:00:00 2001 From: Raynel Sanchez Date: Sun, 21 Jul 2024 13:46:19 -0400 Subject: [PATCH 32/45] Add: Make `EquivalenceLibrary` graph weights optional. --- crates/circuit/src/equivalence.rs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/crates/circuit/src/equivalence.rs b/crates/circuit/src/equivalence.rs index 6e4e155d1431..58ccfe2d4377 100644 --- a/crates/circuit/src/equivalence.rs +++ b/crates/circuit/src/equivalence.rs @@ -323,7 +323,7 @@ impl ToPyObject for CircuitRep { } // Custom Types -type GraphType = StableDiGraph; +type GraphType = StableDiGraph>; type KTIType = HashMap; #[pyclass( @@ -619,7 +619,7 @@ impl EquivalenceLibrary { ) })); for edge in edges { - self.graph.add_edge(edge.0, edge.1, edge.2); + self.graph.add_edge(edge.0, edge.1, Some(edge.2)); } self.rule_id += 1; self._graph = None; From dd2773ac1d047a8e78ae780c2b6bcb1e683ae4d7 Mon Sep 17 00:00:00 2001 From: Raynel Sanchez Date: Wed, 24 Jul 2024 09:16:11 -0400 Subject: [PATCH 33/45] Fix: Adapt to #12730 --- crates/circuit/src/equivalence.rs | 45 ++++++++++++++++--------------- 1 file changed, 24 insertions(+), 21 deletions(-) diff --git a/crates/circuit/src/equivalence.rs b/crates/circuit/src/equivalence.rs index 58ccfe2d4377..a77f775be321 100644 --- a/crates/circuit/src/equivalence.rs +++ b/crates/circuit/src/equivalence.rs @@ -33,10 +33,11 @@ use rustworkx_core::petgraph::{ }; use crate::circuit_data::CircuitData; -use crate::circuit_instruction::convert_py_to_operation_type; +use crate::circuit_instruction::OperationFromPython; use crate::imports::ImportOnceCell; use crate::operations::Param; -use crate::operations::{Operation, OperationType}; +use crate::operations::{Operation, OperationRef}; +use crate::packed_instruction::PackedOperation; mod exceptions { use pyo3::import_exception_bound; @@ -252,13 +253,13 @@ impl Display for EdgeData { /// Enum that helps extract the Operation and Parameters on a Gate. #[derive(Debug, Clone)] pub struct GateOper { - operation: OperationType, + operation: PackedOperation, params: SmallVec<[Param; 3]>, } impl<'py> FromPyObject<'py> for GateOper { fn extract(ob: &'py PyAny) -> PyResult { - let op_struct = convert_py_to_operation_type(ob.py(), ob.into())?; + let op_struct: OperationFromPython = ob.extract()?; Ok(Self { operation: op_struct.operation, params: op_struct.params, @@ -417,10 +418,10 @@ impl EquivalenceLibrary { raise_if_shape_mismatch(&gate, equiv)?; raise_if_param_mismatch(py, &gate.params, equiv.data.get_params_unsorted(py)?)?; } - + let op_ref: OperationRef = gate.operation.view(); let key = Key { - name: gate.operation.name().to_string(), - num_qubits: gate.operation.num_qubits(), + name: op_ref.name().to_string(), + num_qubits: op_ref.num_qubits(), }; let node_index = self.set_default_node(key); @@ -461,9 +462,10 @@ impl EquivalenceLibrary { /// ordering of the StandardEquivalenceLibrary will not generally be /// consistent across Qiskit versions. fn get_entry(&self, py: Python, gate: GateOper) -> PyResult> { + let op_ref = gate.operation.view(); let key = Key { - name: gate.operation.name().to_string(), - num_qubits: gate.operation.num_qubits(), + name: op_ref.name().to_string(), + num_qubits: op_ref.num_qubits(), }; let query_params = gate.params; @@ -587,10 +589,10 @@ impl EquivalenceLibrary { &gate.params, equivalent_circuit.data.get_params_unsorted(py)?, )?; - + let op_ref = gate.operation.view(); let key: Key = Key { - name: gate.operation.name().to_string(), - num_qubits: gate.operation.num_qubits(), + name: op_ref.name().to_string(), + num_qubits: op_ref.num_qubits(), }; let equiv = Equivalence { params: gate.params.clone(), @@ -603,8 +605,8 @@ impl EquivalenceLibrary { } let sources: HashSet = HashSet::from_iter(equivalent_circuit.data.iter().map(|inst| Key { - name: inst.op.name().to_string(), - num_qubits: inst.op.num_qubits(), + name: inst.op.view().name().to_string(), + num_qubits: inst.op.view().num_qubits(), })); let edges = Vec::from_iter(sources.iter().map(|source| { ( @@ -636,10 +638,10 @@ impl EquivalenceLibrary { /// # Returns: /// `bool`: `true` if gate has a known decomposition in the library. /// `false` otherwise. - pub fn has_entry(&self, operation: &OperationType) -> bool { + pub fn has_entry(&self, operation: &PackedOperation) -> bool { let key = Key { - name: operation.name().to_string(), - num_qubits: operation.num_qubits(), + name: operation.view().name().to_string(), + num_qubits: operation.view().num_qubits(), }; self.key_to_node_index.contains_key(&key) } @@ -697,15 +699,16 @@ fn raise_if_param_mismatch( } fn raise_if_shape_mismatch(gate: &GateOper, circuit: &CircuitRep) -> PyResult<()> { - if gate.operation.num_qubits() != circuit.num_qubits as u32 - || gate.operation.num_clbits() != circuit.num_clbits as u32 + let op_ref = gate.operation.view(); + if op_ref.num_qubits() != circuit.num_qubits as u32 + || op_ref.num_clbits() != circuit.num_clbits as u32 { return Err(CircuitError::new_err(format!( "Cannot add equivalence between circuit and gate \ of different shapes. Gate: {} qubits and {} clbits. \ Circuit: {} qubits and {} clbits.", - gate.operation.num_qubits(), - gate.operation.num_clbits(), + op_ref.num_qubits(), + op_ref.num_clbits(), circuit.num_qubits, circuit.num_clbits ))); From e7b3cb09c9841ff2f91a2d0af625ef249a766da1 Mon Sep 17 00:00:00 2001 From: Raynel Sanchez <87539502+raynelfss@users.noreply.github.com> Date: Wed, 24 Jul 2024 14:54:06 -0400 Subject: [PATCH 34/45] Fix: Use `IndexSet` and `IndexMap` --- Cargo.lock | 2 ++ crates/circuit/Cargo.toml | 2 ++ crates/circuit/src/equivalence.rs | 14 ++++++++------ 3 files changed, 12 insertions(+), 6 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index 8e1097004d13..fc679b3e911d 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -1207,8 +1207,10 @@ dependencies = [ name = "qiskit-circuit" version = "1.2.0" dependencies = [ + "ahash 0.8.11", "bytemuck", "hashbrown 0.14.5", + "indexmap 2.2.6", "itertools 0.13.0", "ndarray", "num-complex", diff --git a/crates/circuit/Cargo.toml b/crates/circuit/Cargo.toml index 46d0577f3702..6fb31cbb38ed 100644 --- a/crates/circuit/Cargo.toml +++ b/crates/circuit/Cargo.toml @@ -18,6 +18,8 @@ ndarray.workspace = true numpy.workspace = true rustworkx-core = "0.14.2" thiserror.workspace = true +indexmap.workspace = true +ahash = "0.8.11" [dependencies.pyo3] workspace = true diff --git a/crates/circuit/src/equivalence.rs b/crates/circuit/src/equivalence.rs index a77f775be321..f6cfb58d65d4 100644 --- a/crates/circuit/src/equivalence.rs +++ b/crates/circuit/src/equivalence.rs @@ -23,7 +23,9 @@ use std::hash::{Hash, Hasher}; use std::{error::Error, fmt::Display}; use exceptions::CircuitError; -use hashbrown::{HashMap, HashSet}; + +use ahash::RandomState; +use indexmap::{IndexMap, IndexSet}; use pyo3::types::{PyDict, PyList, PySet, PyString}; use pyo3::{prelude::*, types::IntoPyDict}; @@ -325,7 +327,7 @@ impl ToPyObject for CircuitRep { // Custom Types type GraphType = StableDiGraph>; -type KTIType = HashMap; +type KTIType = IndexMap; #[pyclass( subclass, @@ -360,7 +362,7 @@ impl EquivalenceLibrary { } else { Self { graph: GraphType::new(), - key_to_node_index: KTIType::new(), + key_to_node_index: KTIType::default(), rule_id: 0_usize, _graph: None, } @@ -566,7 +568,7 @@ impl EquivalenceLibrary { slf.key_to_node_index = state .get_item("key_to_node_index")? .unwrap() - .extract::>()? + .extract::>()? .into_iter() .map(|(key, val)| (key, NodeIndex::new(val))) .collect(); @@ -603,8 +605,8 @@ impl EquivalenceLibrary { if let Some(node) = self.graph.node_weight_mut(target) { node.equivs.push(equiv.clone()); } - let sources: HashSet = - HashSet::from_iter(equivalent_circuit.data.iter().map(|inst| Key { + let sources: IndexSet = + IndexSet::from_iter(equivalent_circuit.data.iter().map(|inst| Key { name: inst.op.view().name().to_string(), num_qubits: inst.op.view().num_qubits(), })); From aa626b9302aa394875984de78b3c3d264f7824a2 Mon Sep 17 00:00:00 2001 From: Raynel Sanchez <87539502+raynelfss@users.noreply.github.com> Date: Wed, 24 Jul 2024 20:49:39 -0400 Subject: [PATCH 35/45] Fix: Revert changes from previously failing test --- crates/circuit/src/equivalence.rs | 40 +++++++++++++------ .../transpiler/test_basis_translator.py | 4 +- 2 files changed, 30 insertions(+), 14 deletions(-) diff --git a/crates/circuit/src/equivalence.rs b/crates/circuit/src/equivalence.rs index f6cfb58d65d4..703343234a00 100644 --- a/crates/circuit/src/equivalence.rs +++ b/crates/circuit/src/equivalence.rs @@ -51,7 +51,7 @@ pub static QUANTUMCIRCUIT: ImportOnceCell = // Custom Structs -#[pyclass(sequence, module = "qiskit._accelerate.circuit.equivalence")] +#[pyclass(frozen, sequence, module = "qiskit._accelerate.circuit.equivalence")] #[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord, Hash)] pub struct Key { #[pyo3(get)] @@ -68,10 +68,6 @@ impl Key { Self { name, num_qubits } } - fn __eq__(&self, other: &Self) -> bool { - self.eq(other) - } - fn __hash__(&self) -> u64 { let mut hasher = DefaultHasher::new(); (self.name.to_string(), self.num_qubits).hash(&mut hasher); @@ -88,6 +84,26 @@ impl Key { slf.num_qubits, ) } + + // Ord methods for Python + fn __lt__(&self, other: &Self) -> bool { + self.lt(other) + } + fn __le__(&self, other: &Self) -> bool { + self.le(other) + } + fn __eq__(&self, other: &Self) -> bool { + self.eq(other) + } + fn __ne__(&self, other: &Self) -> bool { + self.ne(other) + } + fn __ge__(&self, other: &Self) -> bool { + self.ge(other) + } + fn __gt__(&self, other: &Self) -> bool { + self.gt(other) + } } impl Display for Key { @@ -100,7 +116,7 @@ impl Display for Key { } } -#[pyclass(sequence, module = "qiskit._accelerate.circuit.equivalence")] +#[pyclass(frozen, sequence, module = "qiskit._accelerate.circuit.equivalence")] #[derive(Debug, Clone)] pub struct Equivalence { #[pyo3(get)] @@ -149,7 +165,7 @@ impl Display for Equivalence { } } -#[pyclass(sequence, module = "qiskit._accelerate.circuit.equivalence")] +#[pyclass(frozen, sequence, module = "qiskit._accelerate.circuit.equivalence")] #[derive(Debug, Clone)] pub struct NodeData { #[pyo3(get)] @@ -193,7 +209,7 @@ impl Display for NodeData { } } -#[pyclass(sequence, module = "qiskit._accelerate.circuit.equivalence")] +#[pyclass(frozen, sequence, module = "qiskit._accelerate.circuit.equivalence")] #[derive(Debug, Clone)] pub struct EdgeData { #[pyo3(get)] @@ -506,12 +522,12 @@ impl EquivalenceLibrary { } #[pyo3(name = "keys")] - fn py_keys(slf: PyRef) -> PyResult> { - let py_set = PySet::empty_bound(slf.py())?; + fn py_keys(slf: PyRef) -> PyResult { + let py_dict = PyDict::new_bound(slf.py()); for key in slf.keys() { - py_set.add(key.clone().into_py(slf.py()))?; + py_dict.set_item(key.clone().into_py(slf.py()), slf.py().None())?; } - Ok(py_set) + Ok(py_dict.as_any().call_method0("keys")?.into()) } #[pyo3(name = "node_index")] diff --git a/test/python/transpiler/test_basis_translator.py b/test/python/transpiler/test_basis_translator.py index 3083d096a3db..fc933cd8f666 100644 --- a/test/python/transpiler/test_basis_translator.py +++ b/test/python/transpiler/test_basis_translator.py @@ -976,9 +976,9 @@ def test_cx_bell_to_ecr(self): expected = QuantumCircuit(2) expected.u(pi / 2, 0, pi, qr[0]) expected.u(0, 0, -pi / 2, qr[0]) - expected.u(-pi / 2, -pi / 2, pi / 2, qr[1]) + expected.u(pi, 0, 0, qr[0]) + expected.u(pi / 2, -pi / 2, pi / 2, qr[1]) expected.ecr(0, 1) - expected.u(pi, 0, pi, qr[0]) expected_dag = circuit_to_dag(expected) self.assertEqual(out_dag, expected_dag) From 0d3a164175cbcd88bf88ccb82792db85a48548e2 Mon Sep 17 00:00:00 2001 From: Raynel Sanchez Date: Thu, 1 Aug 2024 11:00:32 -0400 Subject: [PATCH 36/45] Fix: Adapt to #12974 --- crates/circuit/src/equivalence.rs | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/crates/circuit/src/equivalence.rs b/crates/circuit/src/equivalence.rs index 703343234a00..cda9f777dd3b 100644 --- a/crates/circuit/src/equivalence.rs +++ b/crates/circuit/src/equivalence.rs @@ -434,7 +434,7 @@ impl EquivalenceLibrary { fn set_entry(&mut self, py: Python, gate: GateOper, entry: Vec) -> PyResult<()> { for equiv in entry.iter() { raise_if_shape_mismatch(&gate, equiv)?; - raise_if_param_mismatch(py, &gate.params, equiv.data.get_params_unsorted(py)?)?; + raise_if_param_mismatch(py, &gate.params, equiv.data.unsorted_parameters(py)?)?; } let op_ref: OperationRef = gate.operation.view(); let key = Key { @@ -605,7 +605,7 @@ impl EquivalenceLibrary { raise_if_param_mismatch( py, &gate.params, - equivalent_circuit.data.get_params_unsorted(py)?, + equivalent_circuit.data.unsorted_parameters(py)?, )?; let op_ref = gate.operation.view(); let key: Key = Key { @@ -697,7 +697,7 @@ impl EquivalenceLibrary { fn raise_if_param_mismatch( py: Python, gate_params: &[Param], - circuit_parameters: Py, + circuit_parameters: Bound, ) -> PyResult<()> { let gate_params_obj = PySet::new_bound( py, From feff6ebc418c8112279012fabc05be643c83a060 Mon Sep 17 00:00:00 2001 From: Raynel Sanchez Date: Thu, 1 Aug 2024 20:03:28 -0400 Subject: [PATCH 37/45] Fix: Use `EquivalenceLibrary.keys()` instead of `._key_to_node_index` --- qiskit/transpiler/passes/synthesis/high_level_synthesis.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/qiskit/transpiler/passes/synthesis/high_level_synthesis.py b/qiskit/transpiler/passes/synthesis/high_level_synthesis.py index 6974b1cce06c..740ec51c0e88 100644 --- a/qiskit/transpiler/passes/synthesis/high_level_synthesis.py +++ b/qiskit/transpiler/passes/synthesis/high_level_synthesis.py @@ -481,7 +481,7 @@ def _definitely_skip_node(self, node: DAGOpNode, qubits: Sequence[int] | None) - or ( self._equiv_lib is not None and equivalence.Key(name=node.name, num_qubits=node.num_qubits) - in self._equiv_lib._key_to_node_index + in self._equiv_lib.keys() ) ) ) From 132b6bee2b1fd571da0243cbbf551a4ccbb06325 Mon Sep 17 00:00:00 2001 From: Raynel Sanchez Date: Thu, 1 Aug 2024 20:39:03 -0400 Subject: [PATCH 38/45] Chore: update dependencies --- Cargo.lock | 61 ++++++++++++++++++++++++++++++++++++++++++++++-------- 1 file changed, 52 insertions(+), 9 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index 4858af1971e9..fc4d75c9f44a 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -548,6 +548,16 @@ version = "0.3.9" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "d231dfb89cfffdbc30e7fc41579ed6066ad03abda9e567ccafae602b97ec5024" +[[package]] +name = "indexmap" +version = "1.9.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bd070e393353796e801d209ad339e89596eb4c8d430d18ede6a1cced8fafbd99" +dependencies = [ + "autocfg", + "hashbrown 0.12.3", +] + [[package]] name = "indexmap" version = "2.3.0" @@ -896,7 +906,7 @@ checksum = "a8c3d637a7db9ddb3811719db8a466bd4960ea668df4b2d14043a1b0038465b0" dependencies = [ "cov-mark", "either", - "indexmap", + "indexmap 2.3.0", "itertools 0.10.5", "once_cell", "oq3_lexer", @@ -991,7 +1001,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "b4c5cc86750666a3ed20bdaf5ca2a0344f9c67674cae0515bec2da16fbaa47db" dependencies = [ "fixedbitset", - "indexmap", + "indexmap 2.3.0", ] [[package]] @@ -1006,6 +1016,16 @@ version = "0.2.17" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "5b40af805b3121feab8a3c29f04d8ad262fa8e0561883e7653e024ae4479e6de" +[[package]] +name = "priority-queue" +version = "1.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a0bda9164fe05bc9225752d54aae413343c36f684380005398a6a8fde95fe785" +dependencies = [ + "autocfg", + "indexmap 1.9.3", +] + [[package]] name = "priority-queue" version = "2.0.3" @@ -1014,7 +1034,7 @@ checksum = "70c501afe3a2e25c9bd219aa56ec1e04cdb3fcdd763055be268778c13fa82c1f" dependencies = [ "autocfg", "equivalent", - "indexmap", + "indexmap 2.3.0", ] [[package]] @@ -1095,7 +1115,7 @@ checksum = "a5e00b96a521718e08e03b1a622f01c8a8deb50719335de3f60b3b3950f069d8" dependencies = [ "cfg-if", "hashbrown 0.14.5", - "indexmap", + "indexmap 2.3.0", "indoc", "libc", "memoffset", @@ -1164,7 +1184,7 @@ dependencies = [ "faer", "faer-ext", "hashbrown 0.14.5", - "indexmap", + "indexmap 2.3.0", "itertools 0.13.0", "ndarray", "num-bigint", @@ -1178,7 +1198,7 @@ dependencies = [ "rand_distr", "rand_pcg", "rayon", - "rustworkx-core", + "rustworkx-core 0.15.1", "smallvec", "thiserror", ] @@ -1187,12 +1207,16 @@ dependencies = [ name = "qiskit-circuit" version = "1.3.0" dependencies = [ + "ahash 0.8.11", "bytemuck", "hashbrown 0.14.5", + "indexmap 2.3.0", + "itertools 0.13.0", "ndarray", "num-complex", "numpy", "pyo3", + "rustworkx-core 0.14.2", "smallvec", "thiserror", ] @@ -1223,7 +1247,7 @@ name = "qiskit-qasm3" version = "1.3.0" dependencies = [ "hashbrown 0.14.5", - "indexmap", + "indexmap 2.3.0", "oq3_semantics", "pyo3", ] @@ -1392,6 +1416,25 @@ version = "1.0.15" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "80af6f9131f277a45a3fba6ce8e2258037bb0477a67e610d3c1fe046ab31de47" +[[package]] +name = "rustworkx-core" +version = "0.14.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "529027dfaa8125aa61bb7736ae9484f41e8544f448af96918c8da6b1def7f57b" +dependencies = [ + "ahash 0.8.11", + "fixedbitset", + "hashbrown 0.14.5", + "indexmap 2.3.0", + "num-traits", + "petgraph", + "priority-queue 1.4.0", + "rand", + "rand_pcg", + "rayon", + "rayon-cond", +] + [[package]] name = "rustworkx-core" version = "0.15.1" @@ -1401,11 +1444,11 @@ dependencies = [ "ahash 0.8.11", "fixedbitset", "hashbrown 0.14.5", - "indexmap", + "indexmap 2.3.0", "ndarray", "num-traits", "petgraph", - "priority-queue", + "priority-queue 2.0.3", "rand", "rand_pcg", "rayon", From 3dbcc41b724ab5a183e3de400d2d8770c98bb70c Mon Sep 17 00:00:00 2001 From: Raynel Sanchez Date: Tue, 6 Aug 2024 16:45:53 -0400 Subject: [PATCH 39/45] Refactor: Move `EquivalenceLibrary` to `_accelerate`. --- .../src/equivalence.rs | 22 +++++++++---------- crates/accelerate/src/lib.rs | 1 + crates/circuit/src/lib.rs | 4 +--- crates/pyext/src/lib.rs | 15 +++++++------ qiskit/__init__.py | 2 +- qiskit/circuit/equivalence.py | 2 +- 6 files changed, 23 insertions(+), 23 deletions(-) rename crates/{circuit => accelerate}/src/equivalence.rs (97%) diff --git a/crates/circuit/src/equivalence.rs b/crates/accelerate/src/equivalence.rs similarity index 97% rename from crates/circuit/src/equivalence.rs rename to crates/accelerate/src/equivalence.rs index cda9f777dd3b..3bcff697ec93 100644 --- a/crates/circuit/src/equivalence.rs +++ b/crates/accelerate/src/equivalence.rs @@ -34,12 +34,12 @@ use rustworkx_core::petgraph::{ visit::EdgeRef, }; -use crate::circuit_data::CircuitData; -use crate::circuit_instruction::OperationFromPython; -use crate::imports::ImportOnceCell; -use crate::operations::Param; -use crate::operations::{Operation, OperationRef}; -use crate::packed_instruction::PackedOperation; +use qiskit_circuit::circuit_data::CircuitData; +use qiskit_circuit::circuit_instruction::OperationFromPython; +use qiskit_circuit::imports::ImportOnceCell; +use qiskit_circuit::operations::Param; +use qiskit_circuit::operations::{Operation, OperationRef}; +use qiskit_circuit::packed_instruction::PackedOperation; mod exceptions { use pyo3::import_exception_bound; @@ -51,7 +51,7 @@ pub static QUANTUMCIRCUIT: ImportOnceCell = // Custom Structs -#[pyclass(frozen, sequence, module = "qiskit._accelerate.circuit.equivalence")] +#[pyclass(frozen, sequence, module = "qiskit._accelerate.equivalence")] #[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord, Hash)] pub struct Key { #[pyo3(get)] @@ -116,7 +116,7 @@ impl Display for Key { } } -#[pyclass(frozen, sequence, module = "qiskit._accelerate.circuit.equivalence")] +#[pyclass(frozen, sequence, module = "qiskit._accelerate.equivalence")] #[derive(Debug, Clone)] pub struct Equivalence { #[pyo3(get)] @@ -165,7 +165,7 @@ impl Display for Equivalence { } } -#[pyclass(frozen, sequence, module = "qiskit._accelerate.circuit.equivalence")] +#[pyclass(frozen, sequence, module = "qiskit._accelerate.equivalence")] #[derive(Debug, Clone)] pub struct NodeData { #[pyo3(get)] @@ -209,7 +209,7 @@ impl Display for NodeData { } } -#[pyclass(frozen, sequence, module = "qiskit._accelerate.circuit.equivalence")] +#[pyclass(frozen, sequence, module = "qiskit._accelerate.equivalence")] #[derive(Debug, Clone)] pub struct EdgeData { #[pyo3(get)] @@ -348,7 +348,7 @@ type KTIType = IndexMap; #[pyclass( subclass, name = "BaseEquivalenceLibrary", - module = "qiskit._accelerate.circuit.equivalence" + module = "qiskit._accelerate.equivalence" )] #[derive(Debug, Clone)] pub struct EquivalenceLibrary { diff --git a/crates/accelerate/src/lib.rs b/crates/accelerate/src/lib.rs index 4e079ea84b57..9e60b63dea2b 100644 --- a/crates/accelerate/src/lib.rs +++ b/crates/accelerate/src/lib.rs @@ -17,6 +17,7 @@ use pyo3::import_exception; pub mod convert_2q_block_matrix; pub mod dense_layout; pub mod edge_collections; +pub mod equivalence; pub mod error_map; pub mod euler_one_qubit_decomposer; pub mod isometry; diff --git a/crates/circuit/src/lib.rs b/crates/circuit/src/lib.rs index 4dd0652efb89..8a13aab33cea 100644 --- a/crates/circuit/src/lib.rs +++ b/crates/circuit/src/lib.rs @@ -14,7 +14,6 @@ pub mod bit_data; pub mod circuit_data; pub mod circuit_instruction; pub mod dag_node; -pub mod equivalence; pub mod gate_matrix; pub mod imports; pub mod operations; @@ -25,7 +24,7 @@ pub mod util; mod interner; -use pyo3::{prelude::*, wrap_pymodule}; +use pyo3::prelude::*; pub type BitType = u32; #[derive(Copy, Clone, Debug, Hash, Ord, PartialOrd, Eq, PartialEq)] @@ -59,7 +58,6 @@ impl From for BitType { #[pymodule] pub fn circuit(m: Bound) -> PyResult<()> { - m.add_wrapped(wrap_pymodule!(equivalence::equivalence))?; m.add_class::()?; m.add_class::()?; m.add_class::()?; diff --git a/crates/pyext/src/lib.rs b/crates/pyext/src/lib.rs index f9711641a938..8c792b1f00de 100644 --- a/crates/pyext/src/lib.rs +++ b/crates/pyext/src/lib.rs @@ -15,13 +15,13 @@ use pyo3::wrap_pymodule; use qiskit_accelerate::{ convert_2q_block_matrix::convert_2q_block_matrix, dense_layout::dense_layout, - error_map::error_map, euler_one_qubit_decomposer::euler_one_qubit_decomposer, - isometry::isometry, nlayout::nlayout, optimize_1q_gates::optimize_1q_gates, - pauli_exp_val::pauli_expval, results::results, sabre::sabre, sampled_exp_val::sampled_exp_val, - sparse_pauli_op::sparse_pauli_op, star_prerouting::star_prerouting, - stochastic_swap::stochastic_swap, synthesis::synthesis, target_transpiler::target, - two_qubit_decompose::two_qubit_decompose, uc_gate::uc_gate, utils::utils, - vf2_layout::vf2_layout, + equivalence::equivalence, error_map::error_map, + euler_one_qubit_decomposer::euler_one_qubit_decomposer, isometry::isometry, nlayout::nlayout, + optimize_1q_gates::optimize_1q_gates, pauli_exp_val::pauli_expval, results::results, + sabre::sabre, sampled_exp_val::sampled_exp_val, sparse_pauli_op::sparse_pauli_op, + star_prerouting::star_prerouting, stochastic_swap::stochastic_swap, synthesis::synthesis, + target_transpiler::target, two_qubit_decompose::two_qubit_decompose, uc_gate::uc_gate, + utils::utils, vf2_layout::vf2_layout, }; #[pymodule] @@ -31,6 +31,7 @@ fn _accelerate(m: &Bound) -> PyResult<()> { m.add_wrapped(wrap_pymodule!(qiskit_qasm3::qasm3))?; m.add_wrapped(wrap_pymodule!(convert_2q_block_matrix))?; m.add_wrapped(wrap_pymodule!(dense_layout))?; + m.add_wrapped(wrap_pymodule!(equivalence))?; m.add_wrapped(wrap_pymodule!(error_map))?; m.add_wrapped(wrap_pymodule!(euler_one_qubit_decomposer))?; m.add_wrapped(wrap_pymodule!(isometry))?; diff --git a/qiskit/__init__.py b/qiskit/__init__.py index 92a7551c8394..4dc544b35b1b 100644 --- a/qiskit/__init__.py +++ b/qiskit/__init__.py @@ -60,9 +60,9 @@ # We manually define them on import so people can directly import qiskit._accelerate.* submodules # and not have to rely on attribute access. No action needed for top-level extension packages. sys.modules["qiskit._accelerate.circuit"] = _accelerate.circuit -sys.modules["qiskit._accelerate.circuit.equivalence"] = _accelerate.circuit.equivalence sys.modules["qiskit._accelerate.convert_2q_block_matrix"] = _accelerate.convert_2q_block_matrix sys.modules["qiskit._accelerate.dense_layout"] = _accelerate.dense_layout +sys.modules["qiskit._accelerate.equivalence"] = _accelerate.equivalence sys.modules["qiskit._accelerate.error_map"] = _accelerate.error_map sys.modules["qiskit._accelerate.isometry"] = _accelerate.isometry sys.modules["qiskit._accelerate.uc_gate"] = _accelerate.uc_gate diff --git a/qiskit/circuit/equivalence.py b/qiskit/circuit/equivalence.py index 6f9e1f43da0f..05441e68e600 100644 --- a/qiskit/circuit/equivalence.py +++ b/qiskit/circuit/equivalence.py @@ -17,7 +17,7 @@ from qiskit.exceptions import InvalidFileError -from qiskit._accelerate.circuit.equivalence import ( # pylint: disable=unused-import +from qiskit._accelerate.equivalence import ( # pylint: disable=unused-import BaseEquivalenceLibrary, Key, Equivalence, From d12dee6130f9d91a1f0b19facca6dc29de459f1e Mon Sep 17 00:00:00 2001 From: Raynel Sanchez Date: Tue, 6 Aug 2024 17:04:06 -0400 Subject: [PATCH 40/45] Fix: Erroneous `pymodule` function for `equivalence`. --- crates/accelerate/src/equivalence.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/crates/accelerate/src/equivalence.rs b/crates/accelerate/src/equivalence.rs index 3bcff697ec93..48dd60a437ec 100644 --- a/crates/accelerate/src/equivalence.rs +++ b/crates/accelerate/src/equivalence.rs @@ -801,7 +801,7 @@ where } #[pymodule] -pub fn equivalence(_py: Python, m: &Bound<'_, PyModule>) -> PyResult<()> { +pub fn equivalence(m: &Bound<'_, PyModule>) -> PyResult<()> { m.add_class::()?; m.add_class::()?; m.add_class::()?; From 347925467d1af959a1f71695c12f301629e024d8 Mon Sep 17 00:00:00 2001 From: Raynel Sanchez Date: Fri, 30 Aug 2024 12:50:36 -0400 Subject: [PATCH 41/45] Fix: Update `EquivalenceLibrary` to store `CircuitData`. - The equivalence library will now only store `CircuitData` instances as it does not need to call python directly to re-assign parameters. - An `IntoPy` trait was adapted so that it can be automatically converted to a `QuantumCircuit` instance using `_from_circuit_data`. - Updated all tests to use register-less qubits in circuit comparison. - Remove `num_qubits` and `num_clbits` from `CircuitRep`. --- crates/accelerate/src/equivalence.rs | 97 ++++++++------------ test/python/circuit/test_equivalence.py | 77 ++++++++-------- test/python/circuit/test_gate_definitions.py | 9 +- 3 files changed, 80 insertions(+), 103 deletions(-) diff --git a/crates/accelerate/src/equivalence.rs b/crates/accelerate/src/equivalence.rs index 48dd60a437ec..a4569e70082e 100644 --- a/crates/accelerate/src/equivalence.rs +++ b/crates/accelerate/src/equivalence.rs @@ -13,6 +13,7 @@ use itertools::Itertools; use pyo3::exceptions::PyTypeError; +use qiskit_circuit::parameter_table::ParameterUuid; use rustworkx_core::petgraph::csr::IndexType; use rustworkx_core::petgraph::stable_graph::StableDiGraph; use rustworkx_core::petgraph::visit::IntoEdgeReferences; @@ -26,8 +27,8 @@ use exceptions::CircuitError; use ahash::RandomState; use indexmap::{IndexMap, IndexSet}; +use pyo3::prelude::*; use pyo3::types::{PyDict, PyList, PySet, PyString}; -use pyo3::{prelude::*, types::IntoPyDict}; use rustworkx_core::petgraph::{ graph::{EdgeIndex, NodeIndex}, @@ -36,7 +37,7 @@ use rustworkx_core::petgraph::{ use qiskit_circuit::circuit_data::CircuitData; use qiskit_circuit::circuit_instruction::OperationFromPython; -use qiskit_circuit::imports::ImportOnceCell; +use qiskit_circuit::imports::{ImportOnceCell, QUANTUM_CIRCUIT}; use qiskit_circuit::operations::Param; use qiskit_circuit::operations::{Operation, OperationRef}; use qiskit_circuit::packed_instruction::PackedOperation; @@ -46,8 +47,6 @@ mod exceptions { import_exception_bound! {qiskit.circuit.exceptions, CircuitError} } pub static PYDIGRAPH: ImportOnceCell = ImportOnceCell::new("rustworkx", "PyDiGraph"); -pub static QUANTUMCIRCUIT: ImportOnceCell = - ImportOnceCell::new("qiskit.circuit.quantumcircuit", "QuantumCircuit"); // Custom Structs @@ -288,39 +287,15 @@ impl<'py> FromPyObject<'py> for GateOper { /// Representation of QuantumCircuit which the original circuit object + an /// instance of `CircuitData`. #[derive(Debug, Clone)] -pub struct CircuitRep { - object: PyObject, - pub num_qubits: usize, - pub num_clbits: usize, - pub data: CircuitData, -} - -impl CircuitRep { - /// Performs a shallow cloning of the structure by using `clone_ref()`. - pub fn py_clone(&self, py: Python) -> Self { - Self { - object: self.object.clone_ref(py), - num_qubits: self.num_qubits, - num_clbits: self.num_clbits, - data: self.data.clone(), - } - } -} +pub struct CircuitRep(CircuitData); impl FromPyObject<'_> for CircuitRep { fn extract_bound(ob: &Bound<'_, PyAny>) -> PyResult { - if ob.is_instance(QUANTUMCIRCUIT.get_bound(ob.py()))? { + if ob.is_instance(QUANTUM_CIRCUIT.get_bound(ob.py()))? { let data: Bound = ob.getattr("_data")?; let data_downcast: Bound = data.downcast_into()?; let data_extract: CircuitData = data_downcast.extract()?; - let num_qubits: usize = data_extract.num_qubits(); - let num_clbits: usize = data_extract.num_clbits(); - Ok(Self { - object: ob.into_py(ob.py()), - num_qubits, - num_clbits, - data: data_extract, - }) + Ok(Self(data_extract)) } else { Err(PyTypeError::new_err( "Provided object was not an instance of QuantumCircuit", @@ -330,14 +305,18 @@ impl FromPyObject<'_> for CircuitRep { } impl IntoPy for CircuitRep { - fn into_py(self, _py: Python<'_>) -> PyObject { - self.object + fn into_py(self, py: Python<'_>) -> PyObject { + QUANTUM_CIRCUIT + .get_bound(py) + .call_method1("_from_circuit_data", (self.0,)) + .unwrap() + .unbind() } } impl ToPyObject for CircuitRep { fn to_object(&self, py: Python<'_>) -> PyObject { - self.object.clone_ref(py) + self.clone().into_py(py) } } @@ -434,7 +413,7 @@ impl EquivalenceLibrary { fn set_entry(&mut self, py: Python, gate: GateOper, entry: Vec) -> PyResult<()> { for equiv in entry.iter() { raise_if_shape_mismatch(&gate, equiv)?; - raise_if_param_mismatch(py, &gate.params, equiv.data.unsorted_parameters(py)?)?; + raise_if_param_mismatch(py, &gate.params, equiv.0.unsorted_parameters(py)?)?; } let op_ref: OperationRef = gate.operation.view(); let key = Key { @@ -605,7 +584,7 @@ impl EquivalenceLibrary { raise_if_param_mismatch( py, &gate.params, - equivalent_circuit.data.unsorted_parameters(py)?, + equivalent_circuit.0.unsorted_parameters(py)?, )?; let op_ref = gate.operation.view(); let key: Key = Key { @@ -614,7 +593,7 @@ impl EquivalenceLibrary { }; let equiv = Equivalence { params: gate.params.clone(), - circuit: equivalent_circuit.py_clone(py), + circuit: equivalent_circuit.clone(), }; let target = self.set_default_node(key); @@ -622,7 +601,7 @@ impl EquivalenceLibrary { node.equivs.push(equiv.clone()); } let sources: IndexSet = - IndexSet::from_iter(equivalent_circuit.data.iter().map(|inst| Key { + IndexSet::from_iter(equivalent_circuit.0.iter().map(|inst| Key { name: inst.op.view().name().to_string(), num_qubits: inst.op.view().num_qubits(), })); @@ -718,8 +697,8 @@ fn raise_if_param_mismatch( fn raise_if_shape_mismatch(gate: &GateOper, circuit: &CircuitRep) -> PyResult<()> { let op_ref = gate.operation.view(); - if op_ref.num_qubits() != circuit.num_qubits as u32 - || op_ref.num_clbits() != circuit.num_clbits as u32 + if op_ref.num_qubits() != circuit.0.num_qubits() as u32 + || op_ref.num_clbits() != circuit.0.num_clbits() as u32 { return Err(CircuitError::new_err(format!( "Cannot add equivalence between circuit and gate \ @@ -727,34 +706,30 @@ fn raise_if_shape_mismatch(gate: &GateOper, circuit: &CircuitRep) -> PyResult<() Circuit: {} qubits and {} clbits.", op_ref.num_qubits(), op_ref.num_clbits(), - circuit.num_qubits, - circuit.num_clbits + circuit.0.num_qubits(), + circuit.0.num_clbits() ))); } Ok(()) } -fn rebind_equiv(py: Python, equiv: Equivalence, query_params: &[Param]) -> PyResult { - let (equiv_params, equiv_circuit) = (equiv.params, equiv.circuit); - let param_iter = equiv_params - .into_iter() - .zip(query_params.iter().cloned()) +fn rebind_equiv(py: Python, equiv: Equivalence, query_params: &[Param]) -> PyResult { + let (equiv_params, mut equiv_circuit) = (equiv.params, equiv.circuit); + let param_mapping: PyResult> = equiv_params + .iter() + .zip(query_params.iter()) .filter_map(|(param_x, param_y)| match param_x { - Param::ParameterExpression(_) => Some((param_x, param_y)), + Param::ParameterExpression(param) => { + let param_uuid = ParameterUuid::from_parameter(param.bind(py)); + Some(param_uuid.map(|uuid| (uuid, param_y))) + } _ => None, - }); - let param_map = PyDict::new_bound(py); - for (param_key, param_val) in param_iter { - param_map.set_item(param_key, param_val)?; - } - let kwargs = [("inplace", false), ("flat_input", true)].into_py_dict_bound(py); - let new_equiv = equiv_circuit.into_py(py).call_method_bound( - py, - "assign_parameters", - (param_map,), - Some(&kwargs), - )?; - Ok(new_equiv) + }) + .collect(); + equiv_circuit + .0 + .assign_parameters_from_mapping(py, param_mapping?)?; + Ok(equiv_circuit) } // Errors diff --git a/test/python/circuit/test_equivalence.py b/test/python/circuit/test_equivalence.py index acdcf9e9685c..0b523f7e5249 100644 --- a/test/python/circuit/test_equivalence.py +++ b/test/python/circuit/test_equivalence.py @@ -21,6 +21,7 @@ from qiskit.circuit import QuantumCircuit, Parameter, Gate from qiskit.circuit.library import U2Gate from qiskit.circuit.exceptions import CircuitError +from qiskit.circuit.quantumregister import Qubit from qiskit.converters import circuit_to_instruction, circuit_to_gate from qiskit.circuit.equivalence import EquivalenceLibrary, Key, Equivalence, NodeData, EdgeData from qiskit.utils import optionals @@ -69,7 +70,7 @@ def test_add_single_entry(self): eq_lib = EquivalenceLibrary() gate = OneQubitZeroParamGate() - equiv = QuantumCircuit(1) + equiv = QuantumCircuit([Qubit()]) equiv.h(0) eq_lib.add_equivalence(gate, equiv) @@ -85,12 +86,12 @@ def test_add_double_entry(self): eq_lib = EquivalenceLibrary() gate = OneQubitZeroParamGate() - first_equiv = QuantumCircuit(1) + first_equiv = QuantumCircuit([Qubit()]) first_equiv.h(0) eq_lib.add_equivalence(gate, first_equiv) - second_equiv = QuantumCircuit(1) + second_equiv = QuantumCircuit([Qubit()]) second_equiv.append(U2Gate(0, np.pi), [0]) eq_lib.add_equivalence(gate, second_equiv) @@ -109,12 +110,12 @@ def test_set_entry(self): gates = {key: Gate(key, 1, []) for key in "abcd"} target = Gate("target", 1, []) - old = QuantumCircuit(1) + old = QuantumCircuit([Qubit()]) old.append(gates["a"], [0]) old.append(gates["b"], [0]) eq_lib.add_equivalence(target, old) - outbound = QuantumCircuit(1) + outbound = QuantumCircuit([Qubit()]) outbound.append(target, [0]) eq_lib.add_equivalence(gates["c"], outbound) @@ -126,7 +127,7 @@ def test_set_entry(self): self.assertTrue(eq_lib.graph.has_edge(gate_indices["b"], gate_indices["target"])) self.assertTrue(eq_lib.graph.has_edge(gate_indices["target"], gate_indices["c"])) - new = QuantumCircuit(1) + new = QuantumCircuit([Qubit()]) new.append(gates["d"], [0]) eq_lib.set_entry(target, [new]) @@ -145,12 +146,12 @@ def test_set_entry_parallel_edges(self): gates = {key: Gate(key, 1, []) for key in "abcd"} target = Gate("target", 1, []) - old_1 = QuantumCircuit(1, name="a") + old_1 = QuantumCircuit([Qubit()], name="a") old_1.append(gates["a"], [0]) old_1.append(gates["b"], [0]) eq_lib.add_equivalence(target, old_1) - old_2 = QuantumCircuit(1, name="b") + old_2 = QuantumCircuit([Qubit()], name="b") old_2.append(gates["b"], [0]) old_2.append(gates["a"], [0]) eq_lib.add_equivalence(target, old_2) @@ -158,13 +159,13 @@ def test_set_entry_parallel_edges(self): # This extra rule is so that 'a' still has edges, so we can do an exact isomorphism test. # There's not particular requirement for `set_entry` to remove orphan nodes, so we'll just # craft a test that doesn't care either way. - a_to_b = QuantumCircuit(1) + a_to_b = QuantumCircuit([Qubit()]) a_to_b.append(gates["b"], [0]) eq_lib.add_equivalence(gates["a"], a_to_b) self.assertEqual(sorted(eq_lib.get_entry(target), key=lambda qc: qc.name), [old_1, old_2]) - new = QuantumCircuit(1, name="c") + new = QuantumCircuit([Qubit()], name="c") # No more use of 'a', but re-use 'b' and introduce 'c'. new.append(gates["b"], [0]) new.append(gates["c"], [0]) @@ -203,7 +204,7 @@ def test_has_entry(self): eq_lib = EquivalenceLibrary() gate = OneQubitZeroParamGate() - equiv = QuantumCircuit(1) + equiv = QuantumCircuit([Qubit()]) equiv.h(0) eq_lib.add_equivalence(gate, equiv) @@ -224,7 +225,7 @@ def test_equivalence_graph(self): eq_lib = EquivalenceLibrary() gate = OneQubitZeroParamGate() - first_equiv = QuantumCircuit(1) + first_equiv = QuantumCircuit([Qubit()]) first_equiv.h(0) eq_lib.add_equivalence(gate, first_equiv) @@ -281,7 +282,7 @@ def test_get_through_empty_library_to_base(self): base = EquivalenceLibrary() gate = OneQubitZeroParamGate() - equiv = QuantumCircuit(1) + equiv = QuantumCircuit([Qubit()]) equiv.h(0) base.add_equivalence(gate, equiv) @@ -298,13 +299,13 @@ def test_add_equivalence(self): base = EquivalenceLibrary() gate = OneQubitZeroParamGate() - first_equiv = QuantumCircuit(1) + first_equiv = QuantumCircuit([Qubit()]) first_equiv.h(0) base.add_equivalence(gate, first_equiv) eq_lib = EquivalenceLibrary(base=base) - second_equiv = QuantumCircuit(1) + second_equiv = QuantumCircuit([Qubit()]) second_equiv.append(U2Gate(0, np.pi), [0]) eq_lib.add_equivalence(gate, second_equiv) @@ -321,13 +322,13 @@ def test_set_entry(self): base = EquivalenceLibrary() gate = OneQubitZeroParamGate() - first_equiv = QuantumCircuit(1) + first_equiv = QuantumCircuit([Qubit()]) first_equiv.h(0) base.add_equivalence(gate, first_equiv) eq_lib = EquivalenceLibrary(base=base) - second_equiv = QuantumCircuit(1) + second_equiv = QuantumCircuit([Qubit()]) second_equiv.append(U2Gate(0, np.pi), [0]) eq_lib.set_entry(gate, [second_equiv]) @@ -343,7 +344,7 @@ def test_has_entry_in_base(self): base_eq_lib = EquivalenceLibrary() gate = OneQubitZeroParamGate() - equiv = QuantumCircuit(1) + equiv = QuantumCircuit([Qubit()]) equiv.h(0) base_eq_lib.add_equivalence(gate, equiv) @@ -354,7 +355,7 @@ def test_has_entry_in_base(self): self.assertTrue(eq_lib.has_entry(OneQubitZeroParamGate())) gate = OneQubitZeroParamGate() - equiv2 = QuantumCircuit(1) + equiv2 = QuantumCircuit([Qubit()]) equiv.append(U2Gate(0, np.pi), [0]) eq_lib.add_equivalence(gate, equiv2) @@ -382,7 +383,7 @@ def test_raise_if_gate_equiv_parameter_mismatch(self): phi = Parameter("phi") gate = OneQubitOneParamGate(theta) - equiv = QuantumCircuit(1) + equiv = QuantumCircuit([Qubit()]) equiv.p(phi, 0) with self.assertRaises(CircuitError): @@ -398,7 +399,7 @@ def test_parameter_in_parameter_out(self): theta = Parameter("theta") gate = OneQubitOneParamGate(theta) - equiv = QuantumCircuit(1) + equiv = QuantumCircuit([Qubit()]) equiv.p(theta, 0) eq_lib.add_equivalence(gate, equiv) @@ -408,7 +409,7 @@ def test_parameter_in_parameter_out(self): entry = eq_lib.get_entry(gate_phi) - expected = QuantumCircuit(1) + expected = QuantumCircuit([Qubit()]) expected.p(phi, 0) self.assertEqual(len(entry), 1) @@ -422,7 +423,7 @@ def test_partial_parameter_in_parameter_out(self): phi = Parameter("phi") gate = OneQubitTwoParamGate(theta, phi) - equiv = QuantumCircuit(1) + equiv = QuantumCircuit([Qubit()]) equiv.u(theta, phi, 0, 0) eq_lib.add_equivalence(gate, equiv) @@ -432,7 +433,7 @@ def test_partial_parameter_in_parameter_out(self): entry = eq_lib.get_entry(gate_partial) - expected = QuantumCircuit(1) + expected = QuantumCircuit([Qubit()]) expected.u(lam, 1.59, 0, 0) self.assertEqual(len(entry), 1) @@ -445,14 +446,14 @@ def test_adding_gate_under_different_parameters(self): theta = Parameter("theta") gate_theta = OneQubitOneParamGate(theta) - equiv_theta = QuantumCircuit(1) + equiv_theta = QuantumCircuit([Qubit()]) equiv_theta.p(theta, 0) eq_lib.add_equivalence(gate_theta, equiv_theta) phi = Parameter("phi") gate_phi = OneQubitOneParamGate(phi) - equiv_phi = QuantumCircuit(1) + equiv_phi = QuantumCircuit([Qubit()]) equiv_phi.rz(phi, 0) eq_lib.add_equivalence(gate_phi, equiv_phi) @@ -462,10 +463,10 @@ def test_adding_gate_under_different_parameters(self): entry = eq_lib.get_entry(gate_query) - first_expected = QuantumCircuit(1) + first_expected = QuantumCircuit([Qubit()]) first_expected.p(lam, 0) - second_expected = QuantumCircuit(1) + second_expected = QuantumCircuit([Qubit()]) second_expected.rz(lam, 0) self.assertEqual(len(entry), 2) @@ -481,13 +482,13 @@ def test_adding_gate_and_partially_specified_gate(self): # e.g. RGate(theta, phi) gate_full = OneQubitTwoParamGate(theta, phi) - equiv_full = QuantumCircuit(1) + equiv_full = QuantumCircuit([Qubit()]) equiv_full.append(U2Gate(theta, phi), [0]) eq_lib.add_equivalence(gate_full, equiv_full) gate_partial = OneQubitTwoParamGate(theta, 0) - equiv_partial = QuantumCircuit(1) + equiv_partial = QuantumCircuit([Qubit()]) equiv_partial.rx(theta, 0) eq_lib.add_equivalence(gate_partial, equiv_partial) @@ -497,10 +498,10 @@ def test_adding_gate_and_partially_specified_gate(self): entry = eq_lib.get_entry(gate_query) - first_expected = QuantumCircuit(1) + first_expected = QuantumCircuit([Qubit()]) first_expected.append(U2Gate(lam, 0), [0]) - second_expected = QuantumCircuit(1) + second_expected = QuantumCircuit([Qubit()]) second_expected.rx(lam, 0) self.assertEqual(len(entry), 2) @@ -513,7 +514,7 @@ class TestSessionEquivalenceLibrary(QiskitTestCase): def test_converter_gate_registration(self): """Verify converters register gates in session equivalence library.""" - qc_gate = QuantumCircuit(2) + qc_gate = QuantumCircuit([Qubit() for _ in range(2)]) qc_gate.h(0) qc_gate.cx(0, 1) @@ -521,7 +522,7 @@ def test_converter_gate_registration(self): bell_gate = circuit_to_gate(qc_gate, equivalence_library=sel) - qc_inst = QuantumCircuit(2) + qc_inst = QuantumCircuit([Qubit() for _ in range(2)]) qc_inst.h(0) qc_inst.cx(0, 1) @@ -538,7 +539,7 @@ def test_converter_gate_registration(self): def test_gate_decomposition_properties(self): """Verify decompositions are accessible via gate properties.""" - qc = QuantumCircuit(2) + qc = QuantumCircuit([Qubit() for _ in range(2)]) qc.h(0) qc.cx(0, 1) @@ -551,7 +552,7 @@ def test_gate_decomposition_properties(self): self.assertEqual(len(decomps), 1) self.assertEqual(decomps[0], qc) - qc2 = QuantumCircuit(2) + qc2 = QuantumCircuit([Qubit() for _ in range(2)]) qc2.h([0, 1]) qc2.cz(0, 1) qc2.h(1) @@ -581,12 +582,12 @@ def test_equivalence_draw(self): """Verify EquivalenceLibrary drawing with reference image.""" sel = EquivalenceLibrary() gate = OneQubitZeroParamGate() - first_equiv = QuantumCircuit(1) + first_equiv = QuantumCircuit([Qubit()]) first_equiv.h(0) sel.add_equivalence(gate, first_equiv) - second_equiv = QuantumCircuit(1) + second_equiv = QuantumCircuit([Qubit()]) second_equiv.append(U2Gate(0, np.pi), [0]) sel.add_equivalence(gate, second_equiv) diff --git a/test/python/circuit/test_gate_definitions.py b/test/python/circuit/test_gate_definitions.py index 914ac8bab985..b6f160a644d9 100644 --- a/test/python/circuit/test_gate_definitions.py +++ b/test/python/circuit/test_gate_definitions.py @@ -21,6 +21,7 @@ from qiskit import QuantumCircuit, QuantumRegister from qiskit.quantum_info import Operator from qiskit.circuit import ParameterVector, Gate, ControlledGate +from qiskit.circuit.quantumregister import Qubit from qiskit.circuit.singleton import SingletonGate, SingletonControlledGate from qiskit.circuit.library import standard_gates from qiskit.circuit.library import ( @@ -402,11 +403,11 @@ def test_definition_parameters(self, gate_class): self.assertGreaterEqual(len(param_entry), 1) self.assertGreaterEqual(len(float_entry), 1) - param_qc = QuantumCircuit(param_gate.num_qubits) - float_qc = QuantumCircuit(float_gate.num_qubits) + param_qc = QuantumCircuit([Qubit() for _ in range(param_gate.num_qubits)]) + float_qc = QuantumCircuit([Qubit() for _ in range(float_gate.num_qubits)]) - param_qc.append(param_gate, param_qc.qregs[0]) - float_qc.append(float_gate, float_qc.qregs[0]) + param_qc.append(param_gate, param_qc.qubits) + float_qc.append(float_gate, float_qc.qubits) self.assertTrue(any(equiv == param_qc.decompose() for equiv in param_entry)) self.assertTrue(any(equiv == float_qc.decompose() for equiv in float_entry)) From c433c87581fa17db84356038f354c5aa783e7bea Mon Sep 17 00:00:00 2001 From: Raynel Sanchez Date: Fri, 30 Aug 2024 12:57:56 -0400 Subject: [PATCH 42/45] Fix: Make inner `CircuitData` instance public. --- crates/accelerate/src/equivalence.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/crates/accelerate/src/equivalence.rs b/crates/accelerate/src/equivalence.rs index a4569e70082e..167124bcc314 100644 --- a/crates/accelerate/src/equivalence.rs +++ b/crates/accelerate/src/equivalence.rs @@ -287,7 +287,7 @@ impl<'py> FromPyObject<'py> for GateOper { /// Representation of QuantumCircuit which the original circuit object + an /// instance of `CircuitData`. #[derive(Debug, Clone)] -pub struct CircuitRep(CircuitData); +pub struct CircuitRep(pub CircuitData); impl FromPyObject<'_> for CircuitRep { fn extract_bound(ob: &Bound<'_, PyAny>) -> PyResult { From b7c6316ffde73bd085d91b9b2ddc63201aa81783 Mon Sep 17 00:00:00 2001 From: Raynel Sanchez <87539502+raynelfss@users.noreply.github.com> Date: Fri, 20 Sep 2024 13:01:17 -0400 Subject: [PATCH 43/45] Fix: Review comments and ownership issues. - Add `from_operation` constructor for `Key`. - Made `py_has_entry()` private, but kept its main method public. - Made `set_entry` more rust friendly. - Modify `add_equivalence` to accept a slice of `Param` and use `Into` to convert it into a `SmallVec` instance. --- crates/accelerate/src/equivalence.rs | 138 +++++++++++++++------------ 1 file changed, 76 insertions(+), 62 deletions(-) diff --git a/crates/accelerate/src/equivalence.rs b/crates/accelerate/src/equivalence.rs index 167124bcc314..9da5ac944814 100644 --- a/crates/accelerate/src/equivalence.rs +++ b/crates/accelerate/src/equivalence.rs @@ -104,6 +104,15 @@ impl Key { self.gt(other) } } +impl Key { + fn from_operation(operation: &PackedOperation) -> Self { + let op_ref: OperationRef = operation.view(); + Key { + name: op_ref.name().to_string(), + num_qubits: op_ref.num_qubits(), + } + } +} impl Display for Key { fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { @@ -268,6 +277,8 @@ impl Display for EdgeData { } /// Enum that helps extract the Operation and Parameters on a Gate. +/// It is highly derivative of `PackedOperation` while also tracking the specific +/// parameter objects. #[derive(Debug, Clone)] pub struct GateOper { operation: PackedOperation, @@ -284,8 +295,11 @@ impl<'py> FromPyObject<'py> for GateOper { } } -/// Representation of QuantumCircuit which the original circuit object + an -/// instance of `CircuitData`. +/// Representation of QuantumCircuit by using an instance of `CircuitData`.] +/// +/// TODO: Remove this implementation once the `EquivalenceLibrary` is no longer +/// called from Python, or once the API is able to seamlessly accept instances +/// of `CircuitData`. #[derive(Debug, Clone)] pub struct CircuitRep(pub CircuitData); @@ -383,7 +397,7 @@ impl EquivalenceLibrary { gate: GateOper, equivalent_circuit: CircuitRep, ) -> PyResult<()> { - self.add_equivalence(py, &gate, equivalent_circuit) + self.add_equivalence(py, &gate.operation, &gate.params, equivalent_circuit) } /// Check if a library contains any decompositions for gate. @@ -395,7 +409,7 @@ impl EquivalenceLibrary { /// Bool: True if gate has a known decomposition in the library. /// False otherwise. #[pyo3(name = "has_entry")] - pub fn py_has_entry(&self, gate: GateOper) -> bool { + fn py_has_entry(&self, gate: GateOper) -> bool { self.has_entry(&gate.operation) } @@ -410,35 +424,9 @@ impl EquivalenceLibrary { /// gate (Gate): A Gate instance. /// entry (List['QuantumCircuit']) : A list of QuantumCircuits, each /// equivalently implementing the given Gate. - fn set_entry(&mut self, py: Python, gate: GateOper, entry: Vec) -> PyResult<()> { - for equiv in entry.iter() { - raise_if_shape_mismatch(&gate, equiv)?; - raise_if_param_mismatch(py, &gate.params, equiv.0.unsorted_parameters(py)?)?; - } - let op_ref: OperationRef = gate.operation.view(); - let key = Key { - name: op_ref.name().to_string(), - num_qubits: op_ref.num_qubits(), - }; - let node_index = self.set_default_node(key); - - if let Some(graph_ind) = self.graph.node_weight_mut(node_index) { - graph_ind.equivs.clear(); - } - - let edges: Vec = self - .graph - .edges_directed(node_index, rustworkx_core::petgraph::Direction::Incoming) - .map(|x| x.id()) - .collect(); - for edge in edges { - self.graph.remove_edge(edge); - } - for equiv in entry { - self.add_equivalence(py, &gate, equiv)? - } - self._graph = None; - Ok(()) + #[pyo3(name = "set_entry")] + fn py_set_entry(&mut self, py: Python, gate: GateOper, entry: Vec) -> PyResult<()> { + self.set_entry(py, &gate.operation, &gate.params, entry) } /// Gets the set of QuantumCircuits circuits from the library which @@ -459,11 +447,7 @@ impl EquivalenceLibrary { /// ordering of the StandardEquivalenceLibrary will not generally be /// consistent across Qiskit versions. fn get_entry(&self, py: Python, gate: GateOper) -> PyResult> { - let op_ref = gate.operation.view(); - let key = Key { - name: op_ref.name().to_string(), - num_qubits: op_ref.num_qubits(), - }; + let key = Key::from_operation(&gate.operation); let query_params = gate.params; let bound_equivalencies = self @@ -477,6 +461,7 @@ impl EquivalenceLibrary { Ok(return_list.unbind()) } + // TODO: Remove once BasisTranslator is in Rust. #[getter] fn get_graph(&mut self, py: Python) -> PyResult { if let Some(graph) = &self._graph { @@ -574,37 +559,34 @@ impl EquivalenceLibrary { // Rust native methods impl EquivalenceLibrary { - fn add_equivalence( + /// Add a new equivalence to the library. Future queries for the Gate + /// will include the given circuit, in addition to all existing equivalences + /// (including those from base). + pub fn add_equivalence( &mut self, py: Python, - gate: &GateOper, + gate: &PackedOperation, + params: &[Param], equivalent_circuit: CircuitRep, ) -> PyResult<()> { raise_if_shape_mismatch(gate, &equivalent_circuit)?; - raise_if_param_mismatch( - py, - &gate.params, - equivalent_circuit.0.unsorted_parameters(py)?, - )?; - let op_ref = gate.operation.view(); - let key: Key = Key { - name: op_ref.name().to_string(), - num_qubits: op_ref.num_qubits(), - }; + raise_if_param_mismatch(py, params, equivalent_circuit.0.unsorted_parameters(py)?)?; + let key: Key = Key::from_operation(gate); let equiv = Equivalence { - params: gate.params.clone(), circuit: equivalent_circuit.clone(), + params: params.into(), }; let target = self.set_default_node(key); if let Some(node) = self.graph.node_weight_mut(target) { node.equivs.push(equiv.clone()); } - let sources: IndexSet = - IndexSet::from_iter(equivalent_circuit.0.iter().map(|inst| Key { - name: inst.op.view().name().to_string(), - num_qubits: inst.op.view().num_qubits(), - })); + let sources: IndexSet = IndexSet::from_iter( + equivalent_circuit + .0 + .iter() + .map(|inst| Key::from_operation(&inst.op)), + ); let edges = Vec::from_iter(sources.iter().map(|source| { ( self.set_default_node(source.clone()), @@ -625,6 +607,41 @@ impl EquivalenceLibrary { Ok(()) } + /// Set the equivalence record for a Gate. Future queries for the Gate + /// will return only the circuits provided. + pub fn set_entry( + &mut self, + py: Python, + gate: &PackedOperation, + params: &[Param], + entry: Vec, + ) -> PyResult<()> { + for equiv in entry.iter() { + raise_if_shape_mismatch(gate, equiv)?; + raise_if_param_mismatch(py, params, equiv.0.unsorted_parameters(py)?)?; + } + let key = Key::from_operation(gate); + let node_index = self.set_default_node(key); + + if let Some(graph_ind) = self.graph.node_weight_mut(node_index) { + graph_ind.equivs.clear(); + } + + let edges: Vec = self + .graph + .edges_directed(node_index, rustworkx_core::petgraph::Direction::Incoming) + .map(|x| x.id()) + .collect(); + for edge in edges { + self.graph.remove_edge(edge); + } + for equiv in entry { + self.add_equivalence(py, gate, params, equiv)? + } + self._graph = None; + Ok(()) + } + /// Rust native equivalent to `EquivalenceLibrary.has_entry()` /// /// Check if a library contains any decompositions for gate. @@ -636,10 +653,7 @@ impl EquivalenceLibrary { /// `bool`: `true` if gate has a known decomposition in the library. /// `false` otherwise. pub fn has_entry(&self, operation: &PackedOperation) -> bool { - let key = Key { - name: operation.view().name().to_string(), - num_qubits: operation.view().num_qubits(), - }; + let key = Key::from_operation(operation); self.key_to_node_index.contains_key(&key) } @@ -695,8 +709,8 @@ fn raise_if_param_mismatch( Ok(()) } -fn raise_if_shape_mismatch(gate: &GateOper, circuit: &CircuitRep) -> PyResult<()> { - let op_ref = gate.operation.view(); +fn raise_if_shape_mismatch(gate: &PackedOperation, circuit: &CircuitRep) -> PyResult<()> { + let op_ref = gate.view(); if op_ref.num_qubits() != circuit.0.num_qubits() as u32 || op_ref.num_clbits() != circuit.0.num_clbits() as u32 { From e7d99bccae005eb682c3c9a412986a3169ea8c6d Mon Sep 17 00:00:00 2001 From: Raynel Sanchez <87539502+raynelfss@users.noreply.github.com> Date: Mon, 23 Sep 2024 12:25:46 -0400 Subject: [PATCH 44/45] Fix: Use maximum possible integer value for Key in basis_translator. - Add method to immutably borrow the `EquivalenceLibrary`'s graph. --- crates/accelerate/src/equivalence.rs | 7 ++++++- qiskit/transpiler/passes/basis/basis_translator.py | 2 +- 2 files changed, 7 insertions(+), 2 deletions(-) diff --git a/crates/accelerate/src/equivalence.rs b/crates/accelerate/src/equivalence.rs index 9da5ac944814..e389151b6b04 100644 --- a/crates/accelerate/src/equivalence.rs +++ b/crates/accelerate/src/equivalence.rs @@ -345,7 +345,7 @@ type KTIType = IndexMap; )] #[derive(Debug, Clone)] pub struct EquivalenceLibrary { - pub graph: GraphType, + graph: GraphType, key_to_node_index: KTIType, rule_id: usize, _graph: Option, @@ -685,6 +685,11 @@ impl EquivalenceLibrary { pub fn node_index(&self, key: &Key) -> NodeIndex { self.key_to_node_index[key] } + + /// Expose an immutable view of the inner graph. + pub fn graph(&self) -> &GraphType { + &self.graph + } } fn raise_if_param_mismatch( diff --git a/qiskit/transpiler/passes/basis/basis_translator.py b/qiskit/transpiler/passes/basis/basis_translator.py index de6080f045a8..90887d8afea4 100644 --- a/qiskit/transpiler/passes/basis/basis_translator.py +++ b/qiskit/transpiler/passes/basis/basis_translator.py @@ -579,7 +579,7 @@ def _basis_search(equiv_lib, source_basis, target_basis): # we'll start the search from this dummy node. dummy = graph.add_node( NodeData( - key=Key("key", 0), + key=Key("key", 2**31 - 1), equivs=[Equivalence([], QuantumCircuit(0, name="dummy starting node"))], ) ) From bf7486c195e44ccab539efeb693d67339d0c2360 Mon Sep 17 00:00:00 2001 From: Raynel Sanchez <87539502+raynelfss@users.noreply.github.com> Date: Tue, 24 Sep 2024 09:06:53 -0400 Subject: [PATCH 45/45] Fix: Use generated string, instead of large int - Using large int as the key's number of qubits breaks compatibility with qpy, use a random string instead. --- qiskit/transpiler/passes/basis/basis_translator.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/qiskit/transpiler/passes/basis/basis_translator.py b/qiskit/transpiler/passes/basis/basis_translator.py index 90887d8afea4..0fdae973e1b9 100644 --- a/qiskit/transpiler/passes/basis/basis_translator.py +++ b/qiskit/transpiler/passes/basis/basis_translator.py @@ -13,6 +13,7 @@ """Translates gates to a target basis using a given equivalence library.""" +import random import time import logging @@ -579,7 +580,7 @@ def _basis_search(equiv_lib, source_basis, target_basis): # we'll start the search from this dummy node. dummy = graph.add_node( NodeData( - key=Key("key", 2**31 - 1), + key=Key("".join(chr(random.randint(0, 26) + 97) for _ in range(10)), 0), equivs=[Equivalence([], QuantumCircuit(0, name="dummy starting node"))], ) )