Skip to content

Commit fb7648e

Browse files
authored
Leverage native UnitaryGate from rust (#13765)
This commit builds off of the native rust representation of a UnitaryGate added in #13759 and uses the native representation everywhere we were using UnitaryGate in rust via python previously: the quantum_volume() function, consolidate blocks, split2qunitaries, and unitary synthesis. One future item is consolidate blocks can be updated to use nalgebra types internally instead of ndarray as for the 1 and 2q cases we know the fixed size of the array ahead of time. However the block consolidation code is built using ndarray currently and later synthesis code also works in ndarray so there isn't any real benefit yet, and we'd just add unecessary conversions and allocations. However, once #13649 merges this will change and it would make more sense to add the unitary gate with a Matrix4. But this can be handled separately after this merges.
1 parent 32eae98 commit fb7648e

File tree

8 files changed

+221
-148
lines changed

8 files changed

+221
-148
lines changed

Diff for: Cargo.lock

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

Diff for: crates/accelerate/Cargo.toml

+1
Original file line numberDiff line numberDiff line change
@@ -31,6 +31,7 @@ ndarray_einsum_beta = "0.7"
3131
once_cell = "1.20.2"
3232
rustiq-core = "0.0.10"
3333
bytemuck.workspace = true
34+
nalgebra.workspace = true
3435

3536
[dependencies.smallvec]
3637
workspace = true

Diff for: crates/accelerate/src/circuit_library/quantum_volume.rs

+16-40
Original file line numberDiff line numberDiff line change
@@ -15,19 +15,15 @@ use pyo3::prelude::*;
1515
use pyo3::types::PyDict;
1616

1717
use crate::getenv_use_multiple_threads;
18-
use faer_ext::{IntoFaerComplex, IntoNdarrayComplex};
19-
use ndarray::prelude::*;
20-
use num_complex::Complex64;
21-
use numpy::IntoPyArray;
18+
use nalgebra::Matrix4;
19+
use num_complex::{Complex64, ComplexFloat};
2220
use rand::prelude::*;
2321
use rand_distr::StandardNormal;
2422
use rand_pcg::Pcg64Mcg;
2523
use rayon::prelude::*;
2624

2725
use qiskit_circuit::circuit_data::CircuitData;
28-
use qiskit_circuit::imports::UNITARY_GATE;
29-
use qiskit_circuit::operations::Param;
30-
use qiskit_circuit::operations::PyGate;
26+
use qiskit_circuit::operations::{ArrayType, Param, UnitaryGate};
3127
use qiskit_circuit::packed_instruction::PackedOperation;
3228
use qiskit_circuit::{Clbit, Qubit};
3329
use smallvec::{smallvec, SmallVec};
@@ -50,11 +46,11 @@ fn random_complex(rng: &mut Pcg64Mcg) -> Complex64 {
5046
//
5147
// https://github.com/scipy/scipy/blob/v1.14.1/scipy/stats/_multivariate.py#L4224-L4256
5248
#[inline]
53-
fn random_unitaries(seed: u64, size: usize) -> impl Iterator<Item = Array2<Complex64>> {
49+
fn random_unitaries(seed: u64, size: usize) -> impl Iterator<Item = Matrix4<Complex64>> {
5450
let mut rng = Pcg64Mcg::seed_from_u64(seed);
5551

5652
(0..size).map(move |_| {
57-
let raw_numbers: [[Complex64; 4]; 4] = [
53+
let mat: Matrix4<Complex64> = [
5854
[
5955
random_complex(&mut rng),
6056
random_complex(&mut rng),
@@ -79,23 +75,11 @@ fn random_unitaries(seed: u64, size: usize) -> impl Iterator<Item = Array2<Compl
7975
random_complex(&mut rng),
8076
random_complex(&mut rng),
8177
],
82-
];
83-
84-
let qr = aview2(&raw_numbers).into_faer_complex().qr();
85-
let r = qr.compute_r();
86-
let diag: [Complex64; 4] = [
87-
r[(0, 0)].to_num_complex() / r[(0, 0)].abs(),
88-
r[(1, 1)].to_num_complex() / r[(1, 1)].abs(),
89-
r[(2, 2)].to_num_complex() / r[(2, 2)].abs(),
90-
r[(3, 3)].to_num_complex() / r[(3, 3)].abs(),
91-
];
92-
let mut q = qr.compute_q().as_ref().into_ndarray_complex().to_owned();
93-
q.axis_iter_mut(Axis(0)).for_each(|mut row| {
94-
row.iter_mut()
95-
.enumerate()
96-
.for_each(|(index, val)| *val *= diag[index])
97-
});
98-
q
78+
]
79+
.into();
80+
let (q, r) = mat.qr().unpack();
81+
let diag = r.map_diagonal(|x| x / x.abs());
82+
q.map_with_location(|i, _j, val| val * diag[i])
9983
})
10084
}
10185

@@ -115,29 +99,21 @@ pub fn quantum_volume(
11599

116100
let kwargs = PyDict::new(py);
117101
kwargs.set_item(intern!(py, "num_qubits"), 2)?;
118-
let mut build_instruction = |(unitary_index, unitary_array): (usize, Array2<Complex64>),
102+
let mut build_instruction = |(unitary_index, unitary_array): (usize, Matrix4<Complex64>),
119103
rng: &mut Pcg64Mcg|
120104
-> PyResult<Instruction> {
121105
let layer_index = unitary_index % width;
122106
if layer_index == 0 {
123107
permutation.shuffle(rng);
124108
}
125-
let unitary = unitary_array.into_pyarray(py);
126109

127-
let unitary_gate = UNITARY_GATE
128-
.get_bound(py)
129-
.call((unitary.clone(), py.None(), false), Some(&kwargs))?;
130-
let instruction = PyGate {
131-
qubits: 2,
132-
clbits: 0,
133-
params: 1,
134-
op_name: "unitary".to_string(),
135-
gate: unitary_gate.unbind(),
110+
let unitary_gate = UnitaryGate {
111+
array: ArrayType::TwoQ(unitary_array),
136112
};
137113
let qubit = layer_index * 2;
138114
Ok((
139-
PackedOperation::from_gate(Box::new(instruction)),
140-
smallvec![Param::Obj(unitary.into_any().unbind())],
115+
PackedOperation::from_unitary(Box::new(unitary_gate)),
116+
smallvec![],
141117
vec![permutation[qubit], permutation[qubit + 1]],
142118
vec![],
143119
))
@@ -156,7 +132,7 @@ pub fn quantum_volume(
156132
.take(num_unitaries)
157133
.collect();
158134

159-
let unitaries: Vec<Array2<Complex64>> = if getenv_use_multiple_threads() && num_unitaries > 200
135+
let unitaries: Vec<Matrix4<Complex64>> = if getenv_use_multiple_threads() && num_unitaries > 200
160136
{
161137
seed_vec
162138
.par_chunks(per_thread)

Diff for: crates/accelerate/src/consolidate_blocks.rs

+53-36
Original file line numberDiff line numberDiff line change
@@ -11,18 +11,22 @@
1111
// that they have been altered from the originals.
1212

1313
use hashbrown::{HashMap, HashSet};
14+
use nalgebra::Matrix2;
1415
use ndarray::{aview2, Array2};
1516
use num_complex::Complex64;
16-
use numpy::{IntoPyArray, PyReadonlyArray2};
17+
use numpy::PyReadonlyArray2;
1718
use pyo3::intern;
1819
use pyo3::prelude::*;
1920
use rustworkx_core::petgraph::stable_graph::NodeIndex;
21+
use smallvec::smallvec;
2022

2123
use qiskit_circuit::circuit_data::CircuitData;
24+
use qiskit_circuit::circuit_instruction::ExtraInstructionAttributes;
2225
use qiskit_circuit::dag_circuit::DAGCircuit;
2326
use qiskit_circuit::gate_matrix::{ONE_QUBIT_IDENTITY, TWO_QUBIT_IDENTITY};
24-
use qiskit_circuit::imports::{QI_OPERATOR, QUANTUM_CIRCUIT, UNITARY_GATE};
25-
use qiskit_circuit::operations::{Operation, Param};
27+
use qiskit_circuit::imports::{QI_OPERATOR, QUANTUM_CIRCUIT};
28+
use qiskit_circuit::operations::{ArrayType, Operation, Param, UnitaryGate};
29+
use qiskit_circuit::packed_instruction::PackedOperation;
2630
use qiskit_circuit::Qubit;
2731

2832
use crate::convert_2q_block_matrix::{blocks_to_matrix, get_matrix_from_inst};
@@ -112,11 +116,17 @@ pub(crate) fn consolidate_blocks(
112116
Ok(mat) => mat,
113117
Err(_) => continue,
114118
};
115-
let array = matrix.into_pyarray(py);
116-
let unitary_gate = UNITARY_GATE
117-
.get_bound(py)
118-
.call1((array, py.None(), false))?;
119-
dag.substitute_node_with_py_op(py, inst_node, &unitary_gate, false)?;
119+
// TODO: Use Matrix2/ArrayType::OneQ when we're using nalgebra
120+
// for consolidation
121+
let unitary_gate = UnitaryGate {
122+
array: ArrayType::NDArray(matrix),
123+
};
124+
dag.substitute_op(
125+
inst_node,
126+
PackedOperation::from_unitary(Box::new(unitary_gate)),
127+
smallvec![],
128+
ExtraInstructionAttributes::default(),
129+
)?;
120130
continue;
121131
}
122132
}
@@ -180,16 +190,16 @@ pub(crate) fn consolidate_blocks(
180190
dag.remove_op_node(node);
181191
}
182192
} else {
183-
let unitary_gate = UNITARY_GATE.get_bound(py).call1((
184-
array.as_ref().into_pyobject(py)?,
185-
py.None(),
186-
false,
187-
))?;
193+
let matrix = array.as_array().to_owned();
194+
let unitary_gate = UnitaryGate {
195+
array: ArrayType::NDArray(matrix),
196+
};
188197
let clbit_pos_map = HashMap::new();
189-
dag.replace_block_with_py_op(
190-
py,
198+
dag.replace_block(
191199
&block,
192-
unitary_gate,
200+
PackedOperation::from_unitary(Box::new(unitary_gate)),
201+
smallvec![],
202+
ExtraInstructionAttributes::default(),
193203
false,
194204
&block_index_map,
195205
&clbit_pos_map,
@@ -213,21 +223,22 @@ pub(crate) fn consolidate_blocks(
213223
dag.remove_op_node(node);
214224
}
215225
} else {
216-
let array = matrix.into_pyarray(py);
217-
let unitary_gate =
218-
UNITARY_GATE
219-
.get_bound(py)
220-
.call1((array, py.None(), false))?;
226+
// TODO: Use Matrix4/ArrayType::TwoQ when we're using nalgebra
227+
// for consolidation
228+
let unitary_gate = UnitaryGate {
229+
array: ArrayType::NDArray(matrix),
230+
};
221231
let qubit_pos_map = block_index_map
222232
.into_iter()
223233
.enumerate()
224234
.map(|(idx, qubit)| (qubit, idx))
225235
.collect();
226236
let clbit_pos_map = HashMap::new();
227-
dag.replace_block_with_py_op(
228-
py,
237+
dag.replace_block(
229238
&block,
230-
unitary_gate,
239+
PackedOperation::from_unitary(Box::new(unitary_gate)),
240+
smallvec![],
241+
ExtraInstructionAttributes::default(),
231242
false,
232243
&qubit_pos_map,
233244
&clbit_pos_map,
@@ -258,11 +269,15 @@ pub(crate) fn consolidate_blocks(
258269
Ok(mat) => mat,
259270
Err(_) => continue,
260271
};
261-
let array = matrix.into_pyarray(py);
262-
let unitary_gate = UNITARY_GATE
263-
.get_bound(py)
264-
.call1((array, py.None(), false))?;
265-
dag.substitute_node_with_py_op(py, first_inst_node, &unitary_gate, false)?;
272+
let unitary_gate = UnitaryGate {
273+
array: ArrayType::NDArray(matrix),
274+
};
275+
dag.substitute_op(
276+
first_inst_node,
277+
PackedOperation::from_unitary(Box::new(unitary_gate)),
278+
smallvec![],
279+
ExtraInstructionAttributes::default(),
280+
)?;
266281
continue;
267282
}
268283
let qubit = first_qubits[0];
@@ -293,17 +308,19 @@ pub(crate) fn consolidate_blocks(
293308
dag.remove_op_node(node);
294309
}
295310
} else {
296-
let array = aview2(&matrix).to_owned().into_pyarray(py);
297-
let unitary_gate = UNITARY_GATE
298-
.get_bound(py)
299-
.call1((array, py.None(), false))?;
311+
let array: Matrix2<Complex64> =
312+
Matrix2::from_row_iterator(matrix.into_iter().flat_map(|x| x.into_iter()));
313+
let unitary_gate = UnitaryGate {
314+
array: ArrayType::OneQ(array),
315+
};
300316
let mut block_index_map: HashMap<Qubit, usize> = HashMap::with_capacity(1);
301317
block_index_map.insert(qubit, 0);
302318
let clbit_pos_map = HashMap::new();
303-
dag.replace_block_with_py_op(
304-
py,
319+
dag.replace_block(
305320
&run,
306-
unitary_gate,
321+
PackedOperation::from_unitary(Box::new(unitary_gate)),
322+
smallvec![],
323+
ExtraInstructionAttributes::default(),
307324
false,
308325
&block_index_map,
309326
&clbit_pos_map,

Diff for: crates/accelerate/src/split_2q_unitaries.rs

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

13-
use pyo3::intern;
13+
use nalgebra::Matrix2;
14+
use num_complex::Complex64;
1415
use pyo3::prelude::*;
15-
use pyo3::types::PyDict;
1616
use rustworkx_core::petgraph::stable_graph::NodeIndex;
17+
use smallvec::{smallvec, SmallVec};
1718

18-
use qiskit_circuit::circuit_instruction::OperationFromPython;
1919
use qiskit_circuit::dag_circuit::{DAGCircuit, NodeType, Wire};
20-
use qiskit_circuit::imports::UNITARY_GATE;
21-
use qiskit_circuit::operations::{Operation, Param};
20+
use qiskit_circuit::operations::{ArrayType, Operation, OperationRef, Param, UnitaryGate};
21+
use qiskit_circuit::packed_instruction::PackedOperation;
2222

2323
use crate::two_qubit_decompose::{Specialization, TwoQubitWeylDecomposition};
2424

@@ -39,7 +39,7 @@ pub fn split_2q_unitaries(
3939
// We only attempt to split UnitaryGate objects, but this could be extended in future
4040
// -- however we need to ensure that we can compile the resulting single-qubit unitaries
4141
// to the supported basis gate set.
42-
if qubits.len() != 2 || inst.op.name() != "unitary" {
42+
if qubits.len() != 2 || !matches!(inst.op.view(), OperationRef::Unitary(_)) {
4343
continue;
4444
}
4545
let matrix = inst
@@ -52,22 +52,33 @@ pub fn split_2q_unitaries(
5252
None,
5353
)?;
5454
if matches!(decomp.specialization, Specialization::IdEquiv) {
55-
let k1r_arr = decomp.K1r(py);
56-
let k1l_arr = decomp.K1l(py);
57-
let kwargs = PyDict::new(py);
58-
kwargs.set_item(intern!(py, "num_qubits"), 1)?;
59-
let k1r_gate = UNITARY_GATE
60-
.get_bound(py)
61-
.call((k1r_arr, py.None(), false), Some(&kwargs))?;
62-
let k1l_gate = UNITARY_GATE
63-
.get_bound(py)
64-
.call((k1l_arr, py.None(), false), Some(&kwargs))?;
65-
let insert_fn = |edge: &Wire| -> PyResult<OperationFromPython> {
55+
let k1r_arr = decomp.k1r_view();
56+
let k1l_arr = decomp.k1l_view();
57+
58+
let insert_fn = |edge: &Wire| -> (PackedOperation, SmallVec<[Param; 3]>) {
6659
if let Wire::Qubit(qubit) = edge {
6760
if *qubit == qubits[0] {
68-
k1r_gate.extract()
61+
let mat: Matrix2<Complex64> = [
62+
[k1r_arr[[0, 0]], k1r_arr[[0, 1]]],
63+
[k1r_arr[[1, 0]], k1r_arr[[1, 1]]],
64+
]
65+
.into();
66+
let k1r_gate = Box::new(UnitaryGate {
67+
array: ArrayType::OneQ(mat),
68+
});
69+
(PackedOperation::from_unitary(k1r_gate), smallvec![])
6970
} else {
70-
k1l_gate.extract()
71+
let mat: Matrix2<Complex64> = [
72+
[k1l_arr[[0, 0]], k1l_arr[[0, 1]]],
73+
[k1l_arr[[1, 0]], k1l_arr[[1, 1]]],
74+
]
75+
.into();
76+
77+
let k1l_gate = Box::new(UnitaryGate {
78+
array: ArrayType::OneQ(mat),
79+
});
80+
81+
(PackedOperation::from_unitary(k1l_gate), smallvec![])
7182
}
7283
} else {
7384
unreachable!("This will only be called on ops with no classical wires.");

Diff for: crates/accelerate/src/two_qubit_decompose.rs

+17
Original file line numberDiff line numberDiff line change
@@ -533,6 +533,23 @@ impl TwoQubitWeylDecomposition {
533533
pub fn c(&self) -> f64 {
534534
self.c
535535
}
536+
537+
pub fn k1l_view(&self) -> ArrayView2<Complex64> {
538+
self.K1l.view()
539+
}
540+
541+
pub fn k2l_view(&self) -> ArrayView2<Complex64> {
542+
self.K2l.view()
543+
}
544+
545+
pub fn k1r_view(&self) -> ArrayView2<Complex64> {
546+
self.K1r.view()
547+
}
548+
549+
pub fn k2r_view(&self) -> ArrayView2<Complex64> {
550+
self.K2r.view()
551+
}
552+
536553
fn weyl_gate(
537554
&self,
538555
simplify: bool,

Diff for: crates/accelerate/src/unitary_synthesis.rs

+1-1
Original file line numberDiff line numberDiff line change
@@ -332,7 +332,7 @@ fn py_run_main_loop(
332332
py_op: new_node.unbind().into(),
333333
};
334334
}
335-
if !(packed_instr.op.name() == "unitary"
335+
if !(matches!(packed_instr.op.view(), OperationRef::Unitary(_))
336336
&& packed_instr.op.num_qubits() >= min_qubits as u32)
337337
{
338338
out_dag.push_back(py, packed_instr)?;

0 commit comments

Comments
 (0)