Skip to content
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

Fix Optimize1qGatesDecomposition length heuristic #6553

Merged
merged 33 commits into from
Jul 7, 2021
Merged
Show file tree
Hide file tree
Changes from 32 commits
Commits
Show all changes
33 commits
Select commit Hold shift + click to select a range
d50b7e0
fix 1q optimization heuristic
ecpeterson Jun 9, 2021
a863bba
drop unnecessary brackets
ecpeterson Jun 10, 2021
0666a65
actually fill out reno template :P
ecpeterson Jun 10, 2021
657de35
maker linter happier
ecpeterson Jun 10, 2021
ef39e8d
make lev happier
ecpeterson Jun 10, 2021
8cd954c
add a GH link to the sloppy synth warning
ecpeterson Jun 10, 2021
f13e554
improve source linking in changelog
ecpeterson Jun 10, 2021
c274e89
remember target basis name
ecpeterson Jun 11, 2021
d4549a8
update circuit definitions
ecpeterson Jun 11, 2021
93dcb6f
improve linter cheerfulness
ecpeterson Jun 11, 2021
764a30e
increase reluctance to decompose calibrated gates
ecpeterson Jun 11, 2021
147eee1
change .basis slot to ._decomposers
ecpeterson Jun 11, 2021
64f78fd
my local linter thinks everything is fine :/
ecpeterson Jun 11, 2021
9327d05
add some U3 special cases
ecpeterson Jun 14, 2021
7ed9d8c
make black happy
ecpeterson Jun 14, 2021
bccbf63
fix a claimed circular import
ecpeterson Jun 14, 2021
2fddee1
add a couple more rewrite tests
ecpeterson Jun 16, 2021
0a1febd
satisfy black
ecpeterson Jun 17, 2021
2038df7
avoid lambda = ± pi in 1Q KAK
ecpeterson Jun 21, 2021
7b7c363
optimize on strict length
ecpeterson Jun 22, 2021
1c5b531
more thorough gate elision during 1Q synthesis
ecpeterson Jun 23, 2021
fd605a4
respond to most of Lev's feedback
ecpeterson Jun 24, 2021
2c4de1f
add an 'allow_non_canonical' parameters
ecpeterson Jun 24, 2021
fbdf39a
normalize K rolls
ecpeterson Jul 1, 2021
f8f5e5f
Update qiskit/quantum_info/synthesis/one_qubit_decompose.py
ecpeterson Jul 6, 2021
a747b5a
Update qiskit/quantum_info/synthesis/one_qubit_decompose.py
ecpeterson Jul 6, 2021
a5272ac
add some Euler special case tests for pushing a K(pi) through an A(al…
ecpeterson Jul 6, 2021
4043322
Merge branch 'main' into bugfix/nonnative-1q-heuristic
ecpeterson Jul 6, 2021
d808985
add some Euler special case tests for pushing a K(pi) through an A(al…
ecpeterson Jul 6, 2021
97b7f00
Merge branch 'bugfix/nonnative-1q-heuristic' of github.com:ecpeterson…
ecpeterson Jul 6, 2021
c3e7d6c
Update one_qubit_decompose.py
ecpeterson Jul 6, 2021
75acef0
ok linter
ecpeterson Jul 6, 2021
1ebb329
Merge branch 'main' into bugfix/nonnative-1q-heuristic
ajavadia Jul 7, 2021
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
186 changes: 112 additions & 74 deletions qiskit/quantum_info/synthesis/one_qubit_decompose.py
Original file line number Diff line number Diff line change
Expand Up @@ -281,92 +281,121 @@ def _params_u1x(mat):
return theta, phi, lam, phase - 0.5 * (theta + phi + lam)

@staticmethod
def _circuit_zyz(theta, phi, lam, phase, simplify=True, atol=DEFAULT_ATOL):
gphase = phase - (phi + lam) / 2
qr = QuantumRegister(1, "qr")
circuit = QuantumCircuit(qr)
if not simplify:
atol = -1.0
if abs(theta) < atol:
tot = _mod_2pi(phi + lam, atol)
if abs(tot) > atol:
circuit._append(RZGate(tot), [qr[0]], [])
gphase += tot / 2
circuit.global_phase = gphase
return circuit
if abs(theta - np.pi) < atol:
gphase += phi
lam, phi = lam - phi, 0
lam = _mod_2pi(lam, atol)
if abs(lam) > atol:
gphase += lam / 2
circuit._append(RZGate(lam), [qr[0]], [])
circuit._append(RYGate(theta), [qr[0]], [])
phi = _mod_2pi(phi, atol)
if abs(phi) > atol:
gphase += phi / 2
circuit._append(RZGate(phi), [qr[0]], [])
circuit.global_phase = gphase
return circuit
def _circuit_kak(
theta,
phi,
lam,
phase,
simplify=True,
atol=DEFAULT_ATOL,
allow_non_canonical=True,
k_gate=RZGate,
a_gate=RYGate,
):
"""
Installs the angles phi, theta, and lam into a KAK-type decomposition of the form
K(phi) . A(theta) . K(lam) , where K and A are an orthogonal pair drawn from RZGate, RYGate,
and RXGate.

@staticmethod
def _circuit_zxz(theta, phi, lam, phase, simplify=True, atol=DEFAULT_ATOL):
gphase = phase - (phi + lam) / 2
qr = QuantumRegister(1, "qr")
circuit = QuantumCircuit(qr)
if not simplify:
atol = -1.0
if abs(theta) < atol:
tot = _mod_2pi(phi + lam)
if abs(tot) > atol:
circuit._append(RZGate(tot), [qr[0]], [])
gphase += tot / 2
circuit.global_phase = gphase
return circuit
if abs(theta - np.pi) < atol:
gphase += phi
lam, phi = lam - phi, 0
lam = _mod_2pi(lam, atol)
if abs(lam) > atol:
gphase += lam / 2
circuit._append(RZGate(lam), [qr[0]], [])
circuit._append(RXGate(theta), [qr[0]], [])
phi = _mod_2pi(phi, atol)
if abs(phi) > atol:
gphase += phi / 2
circuit._append(RZGate(phi), [qr[0]], [])
circuit.global_phase = gphase
return circuit
Args:
theta (float): The middle KAK parameter. Expected to lie in [0, pi).
phi (float): The first KAK parameter.
lam (float): The final KAK parameter.
phase (float): The input global phase.
k_gate (Callable): The constructor for the K gate Instruction.
a_gate (Callable): The constructor for the A gate Instruction.
simplify (bool): Indicates whether gates should be elided / coalesced where possible.
allow_non_canonical (bool): Indicates whether we are permitted to reverse the sign of
the middle parameter, theta, in the output. When this and `simplify` are both
enabled, we take the opportunity to commute half-rotations in the outer gates past
the middle gate, which permits us to coalesce them at the cost of reversing the sign
of theta.

@staticmethod
def _circuit_xyx(theta, phi, lam, phase, simplify=True, atol=DEFAULT_ATOL):
Returns:
QuantumCircuit: The assembled circuit.
"""
gphase = phase - (phi + lam) / 2
qr = QuantumRegister(1, "qr")
circuit = QuantumCircuit(qr)
if not simplify:
atol = -1.0
# Early return for the middle-gate-free case
if abs(theta) < atol:
tot = _mod_2pi(phi + lam, atol)
if abs(tot) > atol:
circuit._append(RXGate(tot), [qr[0]], [])
gphase += tot / 2
lam, phi = lam + phi, 0
# NOTE: The following normalization is safe, because the gphase correction below
# fixes a particular diagonal entry to 1, which prevents any potential phase
# slippage coming from _mod_2pi injecting multiples of 2pi.
lam = _mod_2pi(lam, atol)
if abs(lam) > atol:

circuit._append(k_gate(lam), [qr[0]], [])
gphase += lam / 2
circuit.global_phase = gphase
return circuit
if abs(theta - np.pi) < atol:
gphase += phi
lam, phi = lam - phi, 0
if allow_non_canonical and (
abs(_mod_2pi(lam + np.pi)) < atol or abs(_mod_2pi(phi + np.pi)) < atol
):
lam, theta, phi = lam + np.pi, -theta, phi + np.pi
lam = _mod_2pi(lam, atol)
if abs(lam) > atol:
gphase += lam / 2
circuit._append(RXGate(lam), [qr[0]], [])
circuit._append(RYGate(theta), [qr[0]], [])
circuit._append(k_gate(lam), [qr[0]], [])
circuit._append(a_gate(theta), [qr[0]], [])
phi = _mod_2pi(phi, atol)
if abs(phi) > atol:
gphase += phi / 2
circuit._append(RXGate(phi), [qr[0]], [])
circuit._append(k_gate(phi), [qr[0]], [])
circuit.global_phase = gphase
return circuit

def _circuit_zyz(
self, theta, phi, lam, phase, simplify=True, atol=DEFAULT_ATOL, allow_non_canonical=True
):
return self._circuit_kak(
theta,
phi,
lam,
phase,
simplify=simplify,
atol=atol,
allow_non_canonical=allow_non_canonical,
k_gate=RZGate,
a_gate=RYGate,
)

def _circuit_zxz(
self, theta, phi, lam, phase, simplify=True, atol=DEFAULT_ATOL, allow_non_canonical=True
):
return self._circuit_kak(
theta,
phi,
lam,
phase,
simplify=simplify,
atol=atol,
allow_non_canonical=allow_non_canonical,
k_gate=RZGate,
a_gate=RXGate,
)

def _circuit_xyx(
self, theta, phi, lam, phase, simplify=True, atol=DEFAULT_ATOL, allow_non_canonical=True
):
return self._circuit_kak(
theta,
phi,
lam,
phase,
simplify=simplify,
atol=atol,
allow_non_canonical=allow_non_canonical,
k_gate=RXGate,
a_gate=RYGate,
)

@staticmethod
def _circuit_u3(theta, phi, lam, phase, simplify=True, atol=DEFAULT_ATOL):
qr = QuantumRegister(1, "qr")
Expand Down Expand Up @@ -407,35 +436,44 @@ def _circuit_u(theta, phi, lam, phase, simplify=True, atol=DEFAULT_ATOL):

@staticmethod
def _circuit_psx_gen(theta, phi, lam, phase, atol, pfun, xfun, xpifun=None):
"""Generic X90, phase decomposition"""
"""
Generic X90, phase decomposition

NOTE: `pfun` is responsible for eliding gates where appropriate (e.g., at angle value 0).
"""
qr = QuantumRegister(1, "qr")
circuit = QuantumCircuit(qr, global_phase=phase)
# Check for decomposition into minimimal number required SX pulses
# Early return for zero SX decomposition
if np.abs(theta) < atol:
# Zero SX gate decomposition
pfun(circuit, qr, lam + phi)
return circuit
# Early return for single SX decomposition
if abs(theta - np.pi / 2) < atol:
# Single SX gate decomposition
pfun(circuit, qr, lam - np.pi / 2)
xfun(circuit, qr)
pfun(circuit, qr, phi + np.pi / 2)
return circuit
# General two-SX gate decomposition
# Shift theta and phi so decomposition is
# P(phi).SX.P(theta).SX.P(lam)
# General double SX decomposition
if abs(theta - np.pi) < atol:
circuit.global_phase += lam
phi, lam = phi - lam, 0
if abs(_mod_2pi(lam + np.pi)) < atol or abs(_mod_2pi(phi)) < atol:
lam, theta, phi = lam + np.pi, -theta, phi + np.pi
circuit.global_phase -= theta
# Shift theta and phi to turn the decomposition from
# RZ(phi).RY(theta).RZ(lam) = RZ(phi).RX(-pi/2).RZ(theta).RX(pi/2).RZ(lam)
# into RZ(phi+pi).SX.RZ(theta+pi).SX.RZ(lam) .
theta, phi = theta + np.pi, phi + np.pi
ecpeterson marked this conversation as resolved.
Show resolved Hide resolved
circuit.global_phase -= np.pi / 2
# Emit circuit
pfun(circuit, qr, lam)
if xpifun and abs(_mod_2pi(theta + np.pi)) < atol:
if xpifun and abs(_mod_2pi(theta)) < atol:
xpifun(circuit, qr)
else:
xfun(circuit, qr)
pfun(circuit, qr, theta + np.pi)
pfun(circuit, qr, theta)
xfun(circuit, qr)
pfun(circuit, qr, phi + np.pi)
pfun(circuit, qr, phi)

return circuit

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -14,13 +14,13 @@

import copy
import logging
import math
import warnings

import numpy as np

from qiskit.circuit.library.standard_gates import U3Gate
from qiskit.transpiler.basepasses import TransformationPass
from qiskit.quantum_info.synthesis import one_qubit_decompose
from qiskit.circuit.library.standard_gates import U3Gate
from qiskit.converters import circuit_to_dag

logger = logging.getLogger(__name__)
Expand All @@ -38,25 +38,26 @@ def __init__(self, basis=None):
and the Euler basis.
"""
super().__init__()
self.basis = None
self._target_basis = basis
self._decomposers = None
if basis:
self.basis = []
self._decomposers = []
basis_set = set(basis)
euler_basis_gates = one_qubit_decompose.ONE_QUBIT_EULER_BASIS_GATES
for euler_basis_name, gates in euler_basis_gates.items():
if set(gates).issubset(basis_set):
basis_copy = copy.copy(self.basis)
basis_copy = copy.copy(self._decomposers)
for base in basis_copy:
# check if gates are a superset of another basis
# and if so, remove that basis
if set(euler_basis_gates[base.basis]).issubset(set(gates)):
self.basis.remove(base)
self._decomposers.remove(base)
# check if the gates are a subset of another basis
elif set(gates).issubset(set(euler_basis_gates[base.basis])):
break
# if not a subset, add it to the list
else:
self.basis.append(
self._decomposers.append(
one_qubit_decompose.OneQubitEulerDecomposer(euler_basis_name)
)

Expand All @@ -69,40 +70,76 @@ def run(self, dag):
Returns:
DAGCircuit: the optimized DAG.
"""
if not self.basis:
if not self._decomposers:
logger.info("Skipping pass because no basis is set")
return dag
runs = dag.collect_1q_runs()
identity_matrix = np.eye(2)
for run in runs:
single_u3 = False
# Don't try to optimize a single 1q gate, except for U3
if len(run) <= 1:
params = run[0].op.params
# Remove single identity gates
if len(params) > 0 and np.array_equal(run[0].op.to_matrix(), identity_matrix):
# SPECIAL CASE: Don't bother to optimize single U3 gates which are in the basis set.
# The U3 decomposer is only going to emit a sequence of length 1 anyhow.
if "u3" in self._target_basis and len(run) == 1 and isinstance(run[0].op, U3Gate):
# Toss U3 gates equivalent to the identity; there we get off easy.
if np.array_equal(run[0].op.to_matrix(), np.eye(2)):
dag.remove_op_node(run[0])
continue
if isinstance(run[0].op, U3Gate):
param = float(params[0])
if math.isclose(param, 0, rel_tol=0, abs_tol=1e-12) or math.isclose(
param, np.pi / 2, abs_tol=1e-12, rel_tol=0
):
single_u3 = True
else:
continue
else:
# We might rewrite into lower `u`s if they're available.
if "u2" not in self._target_basis and "u1" not in self._target_basis:
continue

new_circs = []
operator = run[0].op.to_matrix()
for gate in run[1:]:
operator = gate.op.to_matrix().dot(operator)
for decomposer in self.basis:
for decomposer in self._decomposers:
new_circs.append(decomposer._decompose(operator))
if new_circs:
new_circ = min(new_circs, key=len)
if len(run) > len(new_circ) or (single_u3 and new_circ.data[0][0].name != "u3"):

# do we even have calibrations?
has_cals_p = dag.calibrations is not None and len(dag.calibrations) > 0
# is this run all in the target set and also uncalibrated?
rewriteable_and_in_basis_p = all(
g.name in self._target_basis
and (not has_cals_p or not dag.has_calibration_for(g))
for g in run
)
# does this run have uncalibrated gates?
uncalibrated_p = not has_cals_p or any(not dag.has_calibration_for(g) for g in run)
# does this run have gates not in the image of ._decomposers _and_ uncalibrated?
uncalibrated_and_not_basis_p = any(
g.name not in self._target_basis
and (not has_cals_p or not dag.has_calibration_for(g))
for g in run
)

if rewriteable_and_in_basis_p and len(run) < len(new_circ):
# NOTE: This is short-circuited on calibrated gates, which we're timid about
# reducing.
warnings.warn(
f"Resynthesized {run} and got {new_circ}, "
f"but the original was native and the new value is longer. This "
f"indicates an efficiency bug in synthesis. Please report it by "
f"opening an issue here: "
f"https://github.com/Qiskit/qiskit-terra/issues/new/choose",
stacklevel=2,
)
# if we're outside of the basis set, we're obligated to logically decompose.
# if we're outside of the set of gates for which we have physical definitions,
# then we _try_ to decompose, using the results if we see improvement.
# NOTE: Here we use circuit length as a weak proxy for "improvement"; in reality,
# we care about something more like fidelity at runtime, which would mean,
# e.g., a preference for `RZGate`s over `RXGate`s. In fact, users sometimes
# express a preference for a "canonical form" of a circuit, which may come in
# the form of some parameter values, also not visible at the level of circuit
# length. Since we don't have a framework for the caller to programmatically
# express what they want here, we include some special casing for particular
# gates which we've promised to normalize --- but this is fragile and should
# ultimately be done away with.
if (
uncalibrated_and_not_basis_p
or (uncalibrated_p and len(run) > len(new_circ))
or isinstance(run[0].op, U3Gate)
):
new_dag = circuit_to_dag(new_circ)
dag.substitute_node_with_dag(run[0], new_dag)
# Delete the other nodes in the run
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
ecpeterson marked this conversation as resolved.
Show resolved Hide resolved
fixes:
- |
Fixes a bug in :func:`~qiskit.transpiler.passes.Optimize1qGatesDecomposition` previously causing certain
short sequences of gates to erroneously not be rewritten.
Loading