Skip to content

Commit f0e33fe

Browse files
committed
Add infrastructure for gates, instruction, and operations in Rust
This commit adds a native representation of Gates, Instruction, and Operations to rust's circuit module. At a high level this works by either wrapping the Python object in a rust wrapper struct that tracks metadata about the operations (name, num_qubits, etc) and then for other details it calls back to Python to get dynamic details like the definition, matrix, etc. For standard library gates like Swap, CX, H, etc this replaces the on circuit representation with a new enum StandardGate. The enum representation is much more efficient and has a minimal memory footprint as all the gate properties are defined in code based on the enum variant (which represents the gate). The use of an enum to represent standard gates does mean a change in what we store on a CircuitInstruction. To represent a standard gate fully we need to store the mutable properties of the existing Gate class on the circuit instruction as the gate by itself doesn't contain this detail. That means, the parameters, label, unit, duration, and condition are added to the rust side of circuit instrucion. However no Python side access methods are added for these as they're internal only to the rust code and hopefully in Qiskit 2.0 we'll be able to drop, unit, duration, and condition from the api leaving only label and parameters. When an object is added to a CircuitInstruction the operation field is translated to it's internal representation automatically. For standard gates this translates it to the enum form, and for Python defined gates this involves just wrapping them. Then whenever the operation field of a circuit instruction object is read from python it converts it back to the normal Python object representation. This commit does not translate the full standard library of gates as that would make the pull request huge, instead this adds the basic infrastructure for having a more efficient standard gate representation on circuits. There will be follow up pull requests to add the missing gates and round out support in rust. The goal of this pull request is primarily to add the infrastructure for representing the full circuit model (and dag model in the future) in rust. By itself this is not expected to improve runtime performance (if anything it will probably hurt performance because of extra type conversions) but it is intended to enable writing native circuit manipulations in rust, including transpiler passes without needing involvement from Python. TODO: - [ ] Fix parameter handling on copy (might involve moving parameter table to rust) - [ ] Handle global phase in definitions (might mean moving a QuantumCircuit struct to rust instead of just CircuitData) - [ ] Add definitions for gates migrated so far Fixes: Qiskit#12205
1 parent 581f247 commit f0e33fe

25 files changed

+1645
-56
lines changed

Cargo.lock

+11
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

Cargo.toml

+5
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,11 @@ license = "Apache-2.0"
1616
[workspace.dependencies]
1717
indexmap.version = "2.2.6"
1818
hashbrown.version = "0.14.0"
19+
num-complex = "0.4"
20+
ndarray = "^0.15.6"
21+
numpy = "0.21.0"
22+
smallvec = "1.13"
23+
1924
# Most of the crates don't need the feature `extension-module`, since only `qiskit-pyext` builds an
2025
# actual C extension (the feature disables linking in `libpython`, which is forbidden in Python
2126
# distributions). We only activate that feature when building the C extension module; we still need

crates/accelerate/Cargo.toml

+4-4
Original file line numberDiff line numberDiff line change
@@ -11,29 +11,29 @@ doctest = false
1111

1212
[dependencies]
1313
rayon = "1.10"
14-
numpy = "0.21.0"
14+
numpy.workspace = true
1515
rand = "0.8"
1616
rand_pcg = "0.3"
1717
rand_distr = "0.4.3"
1818
ahash = "0.8.11"
1919
num-traits = "0.2"
20-
num-complex = "0.4"
20+
num-complex.workspace = true
2121
num-bigint = "0.4"
2222
rustworkx-core = "0.14"
2323
faer = "0.18.2"
2424
itertools = "0.12.1"
2525
qiskit-circuit.workspace = true
2626

2727
[dependencies.smallvec]
28-
version = "1.13"
28+
workspace = true
2929
features = ["union"]
3030

3131
[dependencies.pyo3]
3232
workspace = true
3333
features = ["hashbrown", "indexmap", "num-complex", "num-bigint", "smallvec"]
3434

3535
[dependencies.ndarray]
36-
version = "^0.15.6"
36+
workspace = true
3737
features = ["rayon", "approx-0_5"]
3838

3939
[dependencies.approx]

crates/circuit/Cargo.toml

+12-1
Original file line numberDiff line numberDiff line change
@@ -11,4 +11,15 @@ doctest = false
1111

1212
[dependencies]
1313
hashbrown.workspace = true
14-
pyo3.workspace = true
14+
num-complex.workspace = true
15+
ndarray.workspace = true
16+
numpy.workspace = true
17+
lazy_static = "1.4"
18+
19+
[dependencies.pyo3]
20+
workspace = true
21+
features = ["hashbrown", "indexmap", "num-complex", "num-bigint", "smallvec"]
22+
23+
[dependencies.smallvec]
24+
workspace = true
25+
features = ["union"]

crates/circuit/src/circuit_data.rs

+167-9
Original file line numberDiff line numberDiff line change
@@ -10,9 +10,14 @@
1010
// copyright notice, and modified files need to carry a notice indicating
1111
// that they have been altered from the originals.
1212

13-
use crate::circuit_instruction::CircuitInstruction;
13+
use crate::circuit_instruction::{
14+
convert_py_to_operation_type, operation_type_and_data_to_py, CircuitInstruction,
15+
};
1416
use crate::intern_context::{BitType, IndexType, InternContext};
1517
use crate::SliceOrInt;
18+
use smallvec::SmallVec;
19+
20+
use crate::operations::{OperationType, Param};
1621

1722
use hashbrown::HashMap;
1823
use pyo3::exceptions::{PyIndexError, PyKeyError, PyRuntimeError, PyValueError};
@@ -25,11 +30,16 @@ use std::hash::{Hash, Hasher};
2530
#[derive(Clone, Debug)]
2631
struct PackedInstruction {
2732
/// The Python-side operation instance.
28-
op: PyObject,
33+
op: OperationType,
2934
/// The index under which the interner has stored `qubits`.
3035
qubits_id: IndexType,
3136
/// The index under which the interner has stored `clbits`.
3237
clbits_id: IndexType,
38+
params: Option<SmallVec<[Param; 3]>>,
39+
label: Option<String>,
40+
duration: Option<PyObject>,
41+
unit: Option<String>,
42+
condition: Option<PyObject>,
3343
}
3444

3545
/// Private wrapper for Python-side Bit instances that implements
@@ -154,6 +164,71 @@ pub struct CircuitData {
154164
clbits: Py<PyList>,
155165
}
156166

167+
impl CircuitData {
168+
/// A helper method to build a new CircuitData from an owned definition
169+
/// as a slice of OperationType, parameters, and qubits.
170+
pub fn build_new_from(
171+
py: Python,
172+
num_qubits: usize,
173+
num_clbits: usize,
174+
instructions: &[(OperationType, &[Param], &[u32])],
175+
) -> PyResult<Self> {
176+
let mut res = CircuitData {
177+
data: Vec::with_capacity(instructions.len()),
178+
intern_context: InternContext::new(),
179+
qubits_native: Vec::with_capacity(num_qubits),
180+
clbits_native: Vec::with_capacity(num_clbits),
181+
qubit_indices_native: HashMap::with_capacity(num_qubits),
182+
clbit_indices_native: HashMap::with_capacity(num_clbits),
183+
qubits: PyList::empty_bound(py).unbind(),
184+
clbits: PyList::empty_bound(py).unbind(),
185+
};
186+
if num_qubits > 0 {
187+
let qubit_mod = py.import_bound("qiskit.circuit.quantumregister")?;
188+
let qubit_cls = qubit_mod.getattr("Qubit")?;
189+
for _i in 0..num_qubits {
190+
let bit = qubit_cls.call0()?;
191+
res.add_qubit(py, &bit, true)?;
192+
}
193+
}
194+
if num_clbits > 0 {
195+
let clbit_mod = py.import_bound("qiskit.circuit.classicalregister")?;
196+
let clbit_cls = clbit_mod.getattr("Clbit")?;
197+
for _i in 0..num_clbits {
198+
let bit = clbit_cls.call0()?;
199+
res.add_clbit(py, &bit, true)?;
200+
}
201+
}
202+
for (operation, params, qargs) in instructions {
203+
let qubits = PyTuple::new_bound(
204+
py,
205+
qargs
206+
.iter()
207+
.map(|x| res.qubits_native[*x as usize].clone_ref(py))
208+
.collect::<Vec<PyObject>>(),
209+
)
210+
.unbind();
211+
let empty: [u8; 0] = [];
212+
let clbits = PyTuple::new_bound(py, empty);
213+
let inst = res.pack_owned(
214+
py,
215+
&CircuitInstruction {
216+
operation: operation.clone(),
217+
qubits,
218+
clbits: clbits.into(),
219+
params: Some(params.iter().cloned().collect()),
220+
label: None,
221+
duration: None,
222+
unit: None,
223+
condition: None,
224+
},
225+
)?;
226+
res.data.push(inst);
227+
}
228+
Ok(res)
229+
}
230+
}
231+
157232
#[pymethods]
158233
impl CircuitData {
159234
#[new]
@@ -366,7 +441,15 @@ impl CircuitData {
366441
#[pyo3(signature = (func))]
367442
pub fn foreach_op(&self, py: Python<'_>, func: &Bound<PyAny>) -> PyResult<()> {
368443
for inst in self.data.iter() {
369-
func.call1((inst.op.bind(py),))?;
444+
match &inst.op {
445+
OperationType::Standard(op) => {
446+
let op = op.into_py(py);
447+
func.call1((op,))
448+
}
449+
OperationType::Instruction(op) => func.call1((op.instruction.clone_ref(py),)),
450+
OperationType::Gate(op) => func.call1((op.gate.clone_ref(py),)),
451+
OperationType::Operation(op) => func.call1((op.operation.clone_ref(py),)),
452+
}?;
370453
}
371454
Ok(())
372455
}
@@ -380,7 +463,15 @@ impl CircuitData {
380463
#[pyo3(signature = (func))]
381464
pub fn foreach_op_indexed(&self, py: Python<'_>, func: &Bound<PyAny>) -> PyResult<()> {
382465
for (index, inst) in self.data.iter().enumerate() {
383-
func.call1((index, inst.op.bind(py)))?;
466+
match &inst.op {
467+
OperationType::Standard(op) => {
468+
let op = op.into_py(py);
469+
func.call1((index, op))
470+
}
471+
OperationType::Instruction(op) => func.call1((index, op.instruction.clone_ref(py))),
472+
OperationType::Gate(op) => func.call1((index, op.gate.clone_ref(py))),
473+
OperationType::Operation(op) => func.call1((index, op.operation.clone_ref(py))),
474+
}?;
384475
}
385476
Ok(())
386477
}
@@ -395,7 +486,30 @@ impl CircuitData {
395486
#[pyo3(signature = (func))]
396487
pub fn map_ops(&mut self, py: Python<'_>, func: &Bound<PyAny>) -> PyResult<()> {
397488
for inst in self.data.iter_mut() {
398-
inst.op = func.call1((inst.op.bind(py),))?.into_py(py);
489+
let new_op = match &inst.op {
490+
OperationType::Standard(_op) => {
491+
let op = operation_type_and_data_to_py(
492+
py,
493+
&inst.op,
494+
&inst.params,
495+
&inst.label,
496+
&inst.duration,
497+
&inst.unit,
498+
)?;
499+
func.call1((op,))
500+
}
501+
OperationType::Instruction(op) => func.call1((op.instruction.clone_ref(py),)),
502+
OperationType::Gate(op) => func.call1((op.gate.clone_ref(py),)),
503+
OperationType::Operation(op) => func.call1((op.operation.clone_ref(py),)),
504+
}?;
505+
506+
let new_inst_details = convert_py_to_operation_type(py, new_op.into())?;
507+
inst.op = new_inst_details.operation;
508+
inst.params = new_inst_details.params;
509+
inst.label = new_inst_details.label;
510+
inst.duration = new_inst_details.duration;
511+
inst.unit = new_inst_details.unit;
512+
inst.condition = new_inst_details.condition;
399513
}
400514
Ok(())
401515
}
@@ -666,9 +780,14 @@ impl CircuitData {
666780
.collect::<PyResult<Vec<BitType>>>()?;
667781

668782
self.data.push(PackedInstruction {
669-
op: inst.op.clone_ref(py),
783+
op: inst.op.clone(),
670784
qubits_id: self.intern_context.intern(qubits)?,
671785
clbits_id: self.intern_context.intern(clbits)?,
786+
params: inst.params.clone(),
787+
label: inst.label.clone(),
788+
duration: inst.duration.clone(),
789+
unit: inst.unit.clone(),
790+
condition: inst.condition.clone(),
672791
});
673792
}
674793
return Ok(());
@@ -720,7 +839,7 @@ impl CircuitData {
720839

721840
fn __traverse__(&self, visit: PyVisit<'_>) -> Result<(), PyTraverseError> {
722841
for packed in self.data.iter() {
723-
visit.call(&packed.op)?;
842+
visit.call(&packed.duration)?;
724843
}
725844
for bit in self.qubits_native.iter().chain(self.clbits_native.iter()) {
726845
visit.call(bit)?;
@@ -820,17 +939,51 @@ impl CircuitData {
820939
self.intern_context.intern(args)
821940
};
822941
Ok(PackedInstruction {
823-
op: inst.operation.clone_ref(py),
942+
op: inst.operation.clone(),
943+
qubits_id: interned_bits(&self.qubit_indices_native, inst.qubits.bind(py))?,
944+
clbits_id: interned_bits(&self.clbit_indices_native, inst.clbits.bind(py))?,
945+
params: inst.params.clone(),
946+
label: inst.label.clone(),
947+
duration: inst.duration.clone(),
948+
unit: inst.unit.clone(),
949+
condition: inst.condition.clone(),
950+
})
951+
}
952+
953+
fn pack_owned(&mut self, py: Python, inst: &CircuitInstruction) -> PyResult<PackedInstruction> {
954+
let mut interned_bits =
955+
|indices: &HashMap<BitAsKey, BitType>, bits: &Bound<PyTuple>| -> PyResult<IndexType> {
956+
let args = bits
957+
.into_iter()
958+
.map(|b| {
959+
let key = BitAsKey::new(&b)?;
960+
indices.get(&key).copied().ok_or_else(|| {
961+
PyKeyError::new_err(format!(
962+
"Bit {:?} has not been added to this circuit.",
963+
b
964+
))
965+
})
966+
})
967+
.collect::<PyResult<Vec<BitType>>>()?;
968+
self.intern_context.intern(args)
969+
};
970+
Ok(PackedInstruction {
971+
op: inst.operation.clone(),
824972
qubits_id: interned_bits(&self.qubit_indices_native, inst.qubits.bind(py))?,
825973
clbits_id: interned_bits(&self.clbit_indices_native, inst.clbits.bind(py))?,
974+
params: inst.params.clone(),
975+
label: inst.label.clone(),
976+
duration: inst.duration.clone(),
977+
unit: inst.unit.clone(),
978+
condition: inst.condition.clone(),
826979
})
827980
}
828981

829982
fn unpack(&self, py: Python<'_>, inst: &PackedInstruction) -> PyResult<Py<CircuitInstruction>> {
830983
Py::new(
831984
py,
832985
CircuitInstruction {
833-
operation: inst.op.clone_ref(py),
986+
operation: inst.op.clone(),
834987
qubits: PyTuple::new_bound(
835988
py,
836989
self.intern_context
@@ -849,6 +1002,11 @@ impl CircuitData {
8491002
.collect::<Vec<_>>(),
8501003
)
8511004
.unbind(),
1005+
params: inst.params.clone(),
1006+
label: inst.label.clone(),
1007+
duration: inst.duration.clone(),
1008+
unit: inst.unit.clone(),
1009+
condition: inst.condition.clone(),
8521010
},
8531011
)
8541012
}

0 commit comments

Comments
 (0)