Skip to content

Commit

Permalink
Use Rust gates for 2q unitary synthesis (Qiskit#12740)
Browse files Browse the repository at this point in the history
* Use Rust gates for 2q unitary synthesis

This commit builds off of what Qiskit#12650 did for the 1q decomposer and
moves to using rust gates for the 2q decomposer too. This means that the
circuit sequence generation is using rust's StandardGate representation
directly instead of relying on mapping strings. For places where
circuits are generated (calling `TwoQubitWeylDecomposition.circuit()` or
or `TwoQubitBasisDecomposer.__call__` without the `use_dag` flag) the
entire circuit is generated in Rust and returned to Python.

* Run cargo fmt and black post rebase
  • Loading branch information
mtreinish authored Jul 23, 2024
1 parent cd6757a commit e362da5
Show file tree
Hide file tree
Showing 8 changed files with 210 additions and 98 deletions.
4 changes: 3 additions & 1 deletion crates/accelerate/src/convert_2q_block_matrix.rs
Original file line number Diff line number Diff line change
Expand Up @@ -126,7 +126,9 @@ pub fn change_basis(matrix: ArrayView2<Complex64>) -> Array2<Complex64> {

#[pyfunction]
pub fn collect_2q_blocks_filter(node: &Bound<PyAny>) -> Option<bool> {
let Ok(node) = node.downcast::<DAGOpNode>() else { return None };
let Ok(node) = node.downcast::<DAGOpNode>() else {
return None;
};
let node = node.borrow();
match node.instruction.op() {
gate @ (OperationRef::Standard(_) | OperationRef::Gate(_)) => Some(
Expand Down
4 changes: 3 additions & 1 deletion crates/accelerate/src/euler_one_qubit_decomposer.rs
Original file line number Diff line number Diff line change
Expand Up @@ -1045,7 +1045,9 @@ fn matmul_1q(operator: &mut [[Complex64; 2]; 2], other: Array2<Complex64>) {

#[pyfunction]
pub fn collect_1q_runs_filter(node: &Bound<PyAny>) -> bool {
let Ok(node) = node.downcast::<DAGOpNode>() else { return false };
let Ok(node) = node.downcast::<DAGOpNode>() else {
return false;
};
let node = node.borrow();
let op = node.instruction.op();
op.num_qubits() == 1
Expand Down
183 changes: 136 additions & 47 deletions crates/accelerate/src/two_qubit_decompose.rs

Large diffs are not rendered by default.

15 changes: 11 additions & 4 deletions crates/circuit/src/circuit_instruction.rs
Original file line number Diff line number Diff line change
Expand Up @@ -429,7 +429,9 @@ impl CircuitInstruction {
if other.is_instance_of::<PyTuple>() {
return Ok(Some(self_._legacy_format(py)?.eq(other)?));
}
let Ok(other) = other.downcast::<CircuitInstruction>() else { return Ok(None) };
let Ok(other) = other.downcast::<CircuitInstruction>() else {
return Ok(None);
};
let other = other.try_borrow()?;

Ok(Some(
Expand Down Expand Up @@ -471,7 +473,7 @@ impl CircuitInstruction {
/// though you can also accept `ob: OperationFromPython` directly, if you don't also need a handle
/// to the Python object that it came from. The handle is useful for the Python-operation caching.
#[derive(Debug)]
pub(crate) struct OperationFromPython {
pub struct OperationFromPython {
pub operation: PackedOperation,
pub params: SmallVec<[Param; 3]>,
pub extra_attrs: Option<Box<ExtraInstructionAttributes>>,
Expand Down Expand Up @@ -508,7 +510,10 @@ impl<'py> FromPyObject<'py> for OperationFromPython {
let Some(standard) = ob_type
.getattr(intern!(py, "_standard_gate"))
.and_then(|standard| standard.extract::<StandardGate>())
.ok() else { break 'standard };
.ok()
else {
break 'standard;
};

// If the instruction is a controlled gate with a not-all-ones control state, it doesn't
// fit our definition of standard. We abuse the fact that we know our standard-gate
Expand Down Expand Up @@ -581,7 +586,9 @@ impl<'py> FromPyObject<'py> for OperationFromPython {

/// Convert a sequence-like Python object to a tuple.
fn as_tuple<'py>(py: Python<'py>, seq: Option<Bound<'py, PyAny>>) -> PyResult<Bound<'py, PyTuple>> {
let Some(seq) = seq else { return Ok(PyTuple::empty_bound(py)) };
let Some(seq) = seq else {
return Ok(PyTuple::empty_bound(py));
};
if seq.is_instance_of::<PyTuple>() {
Ok(seq.downcast_into_exact::<PyTuple>()?)
} else if seq.is_instance_of::<PyList>() {
Expand Down
4 changes: 3 additions & 1 deletion crates/circuit/src/operations.rs
Original file line number Diff line number Diff line change
Expand Up @@ -412,7 +412,9 @@ impl StandardGate {

pub fn __eq__(&self, other: &Bound<PyAny>) -> Py<PyAny> {
let py = other.py();
let Ok(other) = other.extract::<Self>() else { return py.NotImplemented() };
let Ok(other) = other.extract::<Self>() else {
return py.NotImplemented();
};
(*self == other).into_py(py)
}

Expand Down
4 changes: 3 additions & 1 deletion crates/circuit/src/packed_instruction.rs
Original file line number Diff line number Diff line change
Expand Up @@ -378,7 +378,9 @@ impl Drop for PackedOperation {
fn drop_pointer_as<T>(slf: &mut PackedOperation) {
// This should only ever be called when the pointer is valid, but this is defensive just
// to 100% ensure that our `Drop` implementation doesn't panic.
let Some(pointer) = slf.try_pointer() else { return };
let Some(pointer) = slf.try_pointer() else {
return;
};
// SAFETY: `PackedOperation` asserts ownership over its contents, and the contained
// pointer can only be null if we were already dropped. We set our discriminant to mark
// ourselves as plain old data immediately just as a defensive measure.
Expand Down
84 changes: 46 additions & 38 deletions qiskit/synthesis/two_qubit/two_qubit_decompose.py
Original file line number Diff line number Diff line change
Expand Up @@ -35,7 +35,7 @@

import numpy as np

from qiskit.circuit import QuantumRegister, QuantumCircuit, Gate
from qiskit.circuit import QuantumRegister, QuantumCircuit, Gate, CircuitInstruction
from qiskit.circuit.library.standard_gates import (
CXGate,
U3Gate,
Expand All @@ -60,7 +60,7 @@
from qiskit._accelerate import two_qubit_decompose

if TYPE_CHECKING:
from qiskit.dagcircuit.dagcircuit import DAGCircuit
from qiskit.dagcircuit.dagcircuit import DAGCircuit, DAGOpNode

logger = logging.getLogger(__name__)

Expand Down Expand Up @@ -230,13 +230,10 @@ def circuit(
self, *, euler_basis: str | None = None, simplify: bool = False, atol: float = DEFAULT_ATOL
) -> QuantumCircuit:
"""Returns Weyl decomposition in circuit form."""
circuit_sequence = self._inner_decomposition.circuit(
circuit_data = self._inner_decomposition.circuit(
euler_basis=euler_basis, simplify=simplify, atol=atol
)
circ = QuantumCircuit(2, global_phase=circuit_sequence.global_phase)
for name, params, qubits in circuit_sequence:
getattr(circ, name)(*params, *qubits)
return circ
return QuantumCircuit._from_circuit_data(circuit_data)

def actual_fidelity(self, **kwargs) -> float:
"""Calculates the actual fidelity of the decomposed circuit to the input unitary."""
Expand Down Expand Up @@ -641,47 +638,58 @@ def __call__(
QiskitError: if ``pulse_optimize`` is True but we don't know how to do it.
"""

sequence = self._inner_decomposer(
np.asarray(unitary, dtype=complex),
basis_fidelity,
approximate,
_num_basis_uses=_num_basis_uses,
)
q = QuantumRegister(2)
if use_dag:
from qiskit.dagcircuit.dagcircuit import DAGCircuit
from qiskit.dagcircuit.dagcircuit import DAGCircuit, DAGOpNode

sequence = self._inner_decomposer(
np.asarray(unitary, dtype=complex),
basis_fidelity,
approximate,
_num_basis_uses=_num_basis_uses,
)
q = QuantumRegister(2)

dag = DAGCircuit()
dag.global_phase = sequence.global_phase
dag.add_qreg(q)
for name, params, qubits in sequence:
if name == "USER_GATE":
for gate, params, qubits in sequence:
if gate is None:
dag.apply_operation_back(self.gate, tuple(q[x] for x in qubits), check=False)
else:
gate = GATE_NAME_MAP[name](*params)
dag.apply_operation_back(gate, tuple(q[x] for x in qubits), check=False)
op = CircuitInstruction.from_standard(
gate, qubits=tuple(q[x] for x in qubits), params=params
)
node = DAGOpNode.from_instruction(op, dag=dag)
dag._apply_op_node_back(node)
return dag
else:
circ = QuantumCircuit(q, global_phase=sequence.global_phase)
for name, params, qubits in sequence:
try:
getattr(circ, name)(*params, *qubits)
except AttributeError as exc:
if name == "USER_GATE":
circ.append(self.gate, qubits)
elif name == "u3":
gate = U3Gate(*params)
circ.append(gate, qubits)
elif name == "u2":
gate = U2Gate(*params)
circ.append(gate, qubits)
elif name == "u1":
gate = U1Gate(*params)
circ.append(gate, qubits)
if getattr(self.gate, "_standard_gate", None):
circ_data = self._inner_decomposer.to_circuit(
np.asarray(unitary, dtype=complex),
self.gate,
basis_fidelity,
approximate,
_num_basis_uses=_num_basis_uses,
)
return QuantumCircuit._from_circuit_data(circ_data)
else:
sequence = self._inner_decomposer(
np.asarray(unitary, dtype=complex),
basis_fidelity,
approximate,
_num_basis_uses=_num_basis_uses,
)
q = QuantumRegister(2)
circ = QuantumCircuit(q, global_phase=sequence.global_phase)
for gate, params, qubits in sequence:
if gate is None:
circ._append(self.gate, qargs=tuple(q[x] for x in qubits))
else:
raise QiskitError(f"Unknown gate {name}") from exc

return circ
inst = CircuitInstruction.from_standard(
gate, qubits=tuple(q[x] for x in qubits), params=params
)
circ._append(inst)
return circ

def traces(self, target):
r"""
Expand Down
10 changes: 5 additions & 5 deletions qiskit/transpiler/passes/synthesis/unitary_synthesis.py
Original file line number Diff line number Diff line change
Expand Up @@ -561,11 +561,11 @@ def _run_main_loop(
qubits = node.qargs
user_gate_node = DAGOpNode(gate)
for (
op_name,
gate,
params,
qargs,
) in node_list:
if op_name == "USER_GATE":
if gate is None:
node = DAGOpNode.from_instruction(
user_gate_node._to_circuit_instruction().replace(
params=user_gate_node.params,
Expand All @@ -576,7 +576,7 @@ def _run_main_loop(
else:
node = DAGOpNode.from_instruction(
CircuitInstruction.from_standard(
GATE_NAME_MAP[op_name], tuple(qubits[x] for x in qargs), params
gate, tuple(qubits[x] for x in qargs), params
),
dag=out_dag,
)
Expand Down Expand Up @@ -1008,8 +1008,8 @@ def _synth_su4_no_dag(self, unitary, decomposer2q, preferred_direction, approxim
# if the gates in synthesis are in the opposite direction of the preferred direction
# resynthesize a new operator which is the original conjugated by swaps.
# this new operator is doubly mirrored from the original and is locally equivalent.
for op_name, _params, qubits in synth_circ:
if op_name in {"USER_GATE", "cx"}:
for gate, _params, qubits in synth_circ:
if gate is None or gate == CXGate._standard_gate:
synth_direction = qubits
if synth_direction is not None and synth_direction != preferred_direction:
# TODO: Avoid using a dag to correct the synthesis direction
Expand Down

0 comments on commit e362da5

Please sign in to comment.