-
Notifications
You must be signed in to change notification settings - Fork 1.1k
Implements optimizations A.1 and A.2 for Quantum Shannon Decomposition #7390
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: main
Are you sure you want to change the base?
Conversation
@NoureldinYosri Please have a look! |
For future reference, this is the script I used to benchmark the decomposition: # pylint: disable=wrong-or-nonexistent-copyright-notice
"""Tests performance of quantum Shannon decomposition.
"""
import sys
import time
from scipy.stats import unitary_group
import cirq
import qiskit.qasm2
if not sys.warnoptions:
import warnings
warnings.simplefilter("ignore")
def main():
for n_qubits in range(3, 9):
U = unitary_group.rvs(2**n_qubits)
qubits = [cirq.NamedQubit(f'q{i}') for i in range(n_qubits)]
start_time = time.time()
circuit = cirq.Circuit(cirq.quantum_shannon_decomposition(qubits, U))
elapsed_time = time.time() - start_time
print(f'Depth for {n_qubits} qubits (no transpiler): {len(circuit)} ({elapsed_time:.2f} sec)')
print(f'Number of CZ gates (no transpiler): {len(list(circuit.findall_operations_with_gate_type(cirq.CZPowGate)))}')
print(f'Number of CNOT gates (no transpiler): {len(list(circuit.findall_operations_with_gate_type(cirq.CXPowGate)))}')
# Export to qasm
qasm_str = cirq.qasm(circuit, args=cirq.QasmArgs(version="2.0"))
qiskit_circuit = qiskit.qasm2.loads(qasm_str, include_path=('.',), custom_instructions=qiskit.qasm2.LEGACY_CUSTOM_INSTRUCTIONS, custom_classical=(), strict=False)
# Run qiskit transpiler
start_time = time.time()
opt_circuit = qiskit.compiler.transpile(qiskit_circuit, basis_gates=["cz", "u3"], optimization_level=3)
elapsed_time = time.time() - start_time
print(f'Qiskit depth for {n_qubits} qubits (with transpiler): {opt_circuit.depth()} ({elapsed_time:.2f} sec)')
print(f'Qiskit gate counts for {n_qubits} qubits (with transpiler): {dict(opt_circuit.count_ops())}')
# Run cirq transpiler
start_time = time.time()
circuit = cirq.optimize_for_target_gateset(circuit, gateset=cirq.CZTargetGateset(preserve_moment_structure=False), max_num_passes=None)
elapsed_time = time.time() - start_time
print(f'Cirq depth for {n_qubits} qubits (with transpiler): {len(circuit)} ({elapsed_time:.2f} sec)')
# Count the operations
operation_counts = {}
for op in circuit.all_operations():
gate_type = op.gate.__class__
if isinstance(op.gate, cirq.CZPowGate):
gate_type = 'cz'
elif isinstance(op.gate, cirq.PhasedXZGate):
gate_type = 'u3'
if gate_type not in operation_counts:
operation_counts[gate_type] = 0
operation_counts[gate_type] += 1
print(f'Cirq gate counts for {n_qubits} qubits (with transpiler): {operation_counts}')
if __name__ == '__main__':
main() |
Codecov ReportAll modified and coverable lines are covered by tests ✅
Additional details and impacted files@@ Coverage Diff @@
## main #7390 +/- ##
==========================================
+ Coverage 98.68% 98.69% +0.01%
==========================================
Files 1112 1112
Lines 97642 98027 +385
==========================================
+ Hits 96359 96749 +390
+ Misses 1283 1278 -5 ☔ View full report in Codecov by Sentry. 🚀 New features to boost your workflow:
|
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
thanks @codrut3 and sorry for the late review ... I started reviewing and forgot to send
two_qubit_mat = [ | ||
SimpleNamespace(location=loc, matrix=unitary_protocol.unitary(o)) | ||
for loc, o in enumerate(shannon_decomp) | ||
if isinstance(o.gate, ops.MatrixGate) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
why MatrixGate
in particular? shouldn't is op.gate is not None
be sufficient
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I need a way to differentiate between end gates produced by the Shannon decomposition (such as CZ, CNOT), and 2-qubit gates that need to be further decomposed in the A.2 optimization step. A.2 requires splicing a diagonal matrix from the 2-gate decomposition and multiplying it with the previous 2-qubit gate. So I extract these gates in a separate list, perform A.2, and then replace the gates in the original list with the decomposition result.
shannon_decomp = [*_recursive_decomposition(qubits, u)] | ||
# Separate all 2-qubit generic gates while keeping track of location | ||
two_qubit_mat = [ | ||
SimpleNamespace(location=loc, matrix=unitary_protocol.unitary(o)) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
we don't use SimpleNamespace
anywhere else in Cirq. Also SimpleNamespace
doesn't support type checking. can you replace it with an attrs
dataclass, maybe even attrs.frozen
?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Thank you, I replaced with attrs.define. I can't use frozen because the matrix gets updated during the A.2 step: it gets multiplied with the diagonal gate extracted from the next gate decomposition.
If the Shannon decomposition is something like:
2-gate A, 1-qubit gate, 1-qubit gate, 2-gate B, ...
then for A.2 first I decompose B into a diagonal gate and a sequence of operations with at most 2 CNOTs:
2-gate A, 1-qubit gate, 1-qubit gate, (D, [sequence of operations]), ...
then multiply D (a diagonal matrix) with A:
D x 2-gate A, 1-qubit gate, 1-qubit gate, [sequence of operations]), ...
then decompose D x A, and so on.
yield from operations | ||
i, j = np.unravel_index(np.argmax(np.abs(u)), u.shape) | ||
new_unitary = unitary_protocol.unitary(FrozenCircuit.from_moments(*operations)) | ||
global_phase = np.angle(u[i, j]) - np.angle(new_unitary[i, j]) | ||
if np.abs(global_phase) > 1e-9: |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I know you didn't write this but can you modify this line to be
if np.abs(global_phase) > 1e-9: | |
if not np.isclose(global_phase, 0, atol=atol): |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Done.
operations.append(ops.global_phase_operation(np.exp(1j * global_phase))) | ||
shannon_decomp[two_qubit_mat[0].location] = operations | ||
# Yield the final operations in order | ||
for op in shannon_decomp: |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
for op in shannon_decomp: | |
yield from ops.flatten_op_tree(shannon_decomp) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Done. Unfortunately mypy required a cast.
# Yield ops from decomposition of multiplexed u1/u2 part | ||
yield from _msb_demuxer(qubits, u1, u2) | ||
|
||
|
||
def _global_phase_difference(u: np.ndarray, ops: list[cirq.Operation]) -> float: |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
do we still need this? I thought that the analytical decompositions now preserve global phase #6523, is this not the case?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
circuit = eject_phased_paulis(circuit) | ||
circuit = eject_z(circuit) | ||
circuit = circuits.Circuit(circuit.all_operations(), strategy=circuits.InsertStrategy.EARLIEST) | ||
return list(circuit.all_operations()) | ||
|
||
|
||
def _transform_single_qubit_operations_to_phased_x_and_z( | ||
operations: Sequence[ops.Operation], | ||
) -> Sequence[ops.Operation]: |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
instead of passing an arbitary atol
below, take it as input
) -> Sequence[ops.Operation]: | |
atol: float, | |
) -> Sequence[ops.Operation]: |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Done.
u = protocols.unitary(op) @ u | ||
return [ | ||
g(op.qubits[0]) | ||
for g in single_qubit_decompositions.single_qubit_matrix_to_phased_x_z(u, atol=1e-8) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
for g in single_qubit_decompositions.single_qubit_matrix_to_phased_x_z(u, atol=1e-8) | |
for g in single_qubit_decompositions.single_qubit_matrix_to_phased_x_z(u, atol=atol) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Done.
] | ||
|
||
|
||
def _merge_single_qubit_gates(operations: Sequence[ops.Operation]) -> Sequence[ops.Operation]: |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
can you make this a public method in a seprate PR, seems like a useful transformation?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I would like to optimize merge_single_qubit_gates_to_phased_x_and_z
in another PR. Actually I wrote this implementation after reading the code for merge_single_qubit_gates_to_phased_x_and_z
, just without the many conversions to frozen circuit :)
Thank you a lot for the comments @NoureldinYosri ! PTAL |
Fixes #6777
I checked that with the current PR, the circuit depth after transpiler matches that obtained by Qiskit.
The runtime is slow due
merge_single_qubit_gates_to_phased_x_and_z
. I wrote a different implementation for the special case used by the Shannon decomposition. With this, the runtime is halved. I believemerge_single_qubit_gates_to_phased_x_and_z
can be optimized too.