# -*- coding: utf-8 -*-
# This code is part of Qiskit.
#
# (C) Copyright IBM 2017, 2018.
#
# This code is licensed under the Apache License, Version 2.0. You may
# obtain a copy of this license in the LICENSE.txt file in the root directory
# of this source tree or at http://www.apache.org/licenses/LICENSE-2.0.
#
# Any modifications or derivative works of this code must retain this
# copyright notice, and modified files need to carry a notice indicating
# that they have been altered from the originals.
"""Optimize chains of single-qubit u1, u2, u3 gates by combining them into a single gate."""
from itertools import groupby
import numpy as np
from qiskit.transpiler.exceptions import TranspilerError
from qiskit.circuit.library.standard_gates.u1 import U1Gate
from qiskit.circuit.library.standard_gates.u2 import U2Gate
from qiskit.circuit.library.standard_gates.u3 import U3Gate
from qiskit.circuit.gate import Gate
from qiskit.transpiler.basepasses import TransformationPass
from qiskit.quantum_info.operators import Quaternion
_DECIMAL_ROUND = 15
_CHOP_THRESHOLD = 10 ** -(_DECIMAL_ROUND)
[docs]class Optimize1qGates(TransformationPass):
"""Optimize chains of single-qubit u1, u2, u3 gates by combining them into a single gate."""
def __init__(self, basis=None):
"""Optimize1qGates initializer.
Args:
basis (list[str]): Basis gates to consider, e.g. `['u3', 'cx']`. For the effects
of this pass, the basis is the set intersection between the `basis` parameter and
the set `{'u1','u2','u3'}`.
"""
super().__init__()
self.basis = basis if basis else ["u1", "u2", "u3"]
[docs] def run(self, dag):
"""Run the Optimize1qGates pass on `dag`.
Args:
dag (DAGCircuit): the DAG to be optimized.
Returns:
DAGCircuit: the optimized DAG.
Raises:
TranspilerError: if YZY and ZYZ angles do not give same rotation matrix.
"""
runs = dag.collect_runs(["u1", "u2", "u3"])
runs = _split_runs_on_parameters(runs)
for run in runs:
right_name = "u1"
right_parameters = (0, 0, 0) # (theta, phi, lambda)
for current_node in run:
left_name = current_node.name
if (current_node.condition is not None
or len(current_node.qargs) != 1
or left_name not in ["u1", "u2", "u3", "id"]):
raise TranspilerError("internal error")
if left_name == "u1":
left_parameters = (0, 0, current_node.op.params[0])
elif left_name == "u2":
left_parameters = (np.pi / 2, current_node.op.params[0],
current_node.op.params[1])
elif left_name == "u3":
left_parameters = tuple(current_node.op.params)
else:
left_name = "u1" # replace id with u1
left_parameters = (0, 0, 0)
# If there are any sympy objects coming from the gate convert
# to numpy.
left_parameters = tuple([float(x) for x in left_parameters])
# Compose gates
name_tuple = (left_name, right_name)
if name_tuple == ("u1", "u1"):
# u1(lambda1) * u1(lambda2) = u1(lambda1 + lambda2)
right_parameters = (0, 0, right_parameters[2] +
left_parameters[2])
elif name_tuple == ("u1", "u2"):
# u1(lambda1) * u2(phi2, lambda2) = u2(phi2 + lambda1, lambda2)
right_parameters = (np.pi / 2, right_parameters[1] +
left_parameters[2], right_parameters[2])
elif name_tuple == ("u2", "u1"):
# u2(phi1, lambda1) * u1(lambda2) = u2(phi1, lambda1 + lambda2)
right_name = "u2"
right_parameters = (np.pi / 2, left_parameters[1],
right_parameters[2] + left_parameters[2])
elif name_tuple == ("u1", "u3"):
# u1(lambda1) * u3(theta2, phi2, lambda2) =
# u3(theta2, phi2 + lambda1, lambda2)
right_parameters = (right_parameters[0], right_parameters[1] +
left_parameters[2], right_parameters[2])
elif name_tuple == ("u3", "u1"):
# u3(theta1, phi1, lambda1) * u1(lambda2) =
# u3(theta1, phi1, lambda1 + lambda2)
right_name = "u3"
right_parameters = (left_parameters[0], left_parameters[1],
right_parameters[2] + left_parameters[2])
elif name_tuple == ("u2", "u2"):
# Using Ry(pi/2).Rz(2*lambda).Ry(pi/2) =
# Rz(pi/2).Ry(pi-2*lambda).Rz(pi/2),
# u2(phi1, lambda1) * u2(phi2, lambda2) =
# u3(pi - lambda1 - phi2, phi1 + pi/2, lambda2 + pi/2)
right_name = "u3"
right_parameters = (np.pi - left_parameters[2] -
right_parameters[1], left_parameters[1] +
np.pi / 2, right_parameters[2] +
np.pi / 2)
elif name_tuple[1] == "nop":
right_name = left_name
right_parameters = left_parameters
else:
# For composing u3's or u2's with u3's, use
# u2(phi, lambda) = u3(pi/2, phi, lambda)
# together with the qiskit.mapper.compose_u3 method.
right_name = "u3"
# Evaluate the symbolic expressions for efficiency
right_parameters = Optimize1qGates.compose_u3(left_parameters[0],
left_parameters[1],
left_parameters[2],
right_parameters[0],
right_parameters[1],
right_parameters[2])
# Why evalf()? This program:
# OPENQASM 2.0;
# include "qelib1.inc";
# qreg q[2];
# creg c[2];
# u3(0.518016983430947*pi,1.37051598592907*pi,1.36816383603222*pi) q[0];
# u3(1.69867232277986*pi,0.371448347747471*pi,0.461117217930936*pi) q[0];
# u3(0.294319836336836*pi,0.450325871124225*pi,1.46804720442555*pi) q[0];
# measure q -> c;
# took >630 seconds (did not complete) to optimize without
# calling evalf() at all, 19 seconds to optimize calling
# evalf() AFTER compose_u3, and 1 second to optimize
# calling evalf() BEFORE compose_u3.
# 1. Here down, when we simplify, we add f(theta) to lambda to
# correct the global phase when f(theta) is 2*pi. This isn't
# necessary but the other steps preserve the global phase, so
# we continue in that manner.
# 2. The final step will remove Z rotations by 2*pi.
# 3. Note that is_zero is true only if the expression is exactly
# zero. If the input expressions have already been evaluated
# then these final simplifications will not occur.
# TODO After we refactor, we should have separate passes for
# exact and approximate rewriting.
# Y rotation is 0 mod 2*pi, so the gate is a u1
if np.round(np.mod(right_parameters[0], (2 * np.pi)), _DECIMAL_ROUND) == 0 \
and right_name != "u1":
right_name = "u1"
right_parameters = (0, 0, right_parameters[1] +
right_parameters[2] +
right_parameters[0])
# Y rotation is pi/2 or -pi/2 mod 2*pi, so the gate is a u2
if right_name == "u3":
# theta = pi/2 + 2*k*pi
if np.mod(np.round(right_parameters[0] - np.pi / 2, _DECIMAL_ROUND),
(2 * np.pi)) == 0:
right_name = "u2"
right_parameters = (np.pi / 2, right_parameters[1],
right_parameters[2] +
(right_parameters[0] - np.pi / 2))
# theta = -pi/2 + 2*k*pi
if np.mod(np.round(right_parameters[0] + np.pi / 2, _DECIMAL_ROUND),
(2 * np.pi)) == 0:
right_name = "u2"
right_parameters = (np.pi / 2, right_parameters[1] +
np.pi, right_parameters[2] -
np.pi + (right_parameters[0] +
np.pi / 2))
# u1 and lambda is 0 mod 2*pi so gate is nop (up to a global phase)
if right_name == "u1" and np.mod(np.round(right_parameters[2], _DECIMAL_ROUND),
(2 * np.pi)) == 0:
right_name = "nop"
if right_name == "u2" and "u2" not in self.basis:
right_name = "u3"
if right_name == "u1" and "u1" not in self.basis:
right_name = "u3"
new_op = Gate(name="", num_qubits=1, params=[])
if right_name == "u1":
new_op = U1Gate(right_parameters[2])
if right_name == "u2":
new_op = U2Gate(right_parameters[1], right_parameters[2])
if right_name == "u3":
if "u3" in self.basis:
new_op = U3Gate(*right_parameters)
else:
raise TranspilerError('It was not possible to use the basis %s' % self.basis)
if right_name != 'nop':
dag.substitute_node(run[0], new_op, inplace=True)
# Delete the other nodes in the run
for current_node in run[1:]:
dag.remove_op_node(current_node)
if right_name == "nop":
dag.remove_op_node(run[0])
return dag
[docs] @staticmethod
def compose_u3(theta1, phi1, lambda1, theta2, phi2, lambda2):
"""Return a triple theta, phi, lambda for the product.
u3(theta, phi, lambda)
= u3(theta1, phi1, lambda1).u3(theta2, phi2, lambda2)
= Rz(phi1).Ry(theta1).Rz(lambda1+phi2).Ry(theta2).Rz(lambda2)
= Rz(phi1).Rz(phi').Ry(theta').Rz(lambda').Rz(lambda2)
= u3(theta', phi1 + phi', lambda2 + lambda')
Return theta, phi, lambda.
"""
# Careful with the factor of two in yzy_to_zyz
thetap, phip, lambdap = Optimize1qGates.yzy_to_zyz((lambda1 + phi2), theta1, theta2)
(theta, phi, lamb) = (thetap, phi1 + phip, lambda2 + lambdap)
return (theta, phi, lamb)
[docs] @staticmethod
def yzy_to_zyz(xi, theta1, theta2, eps=1e-9): # pylint: disable=invalid-name
"""Express a Y.Z.Y single qubit gate as a Z.Y.Z gate.
Solve the equation
.. math::
Ry(theta1).Rz(xi).Ry(theta2) = Rz(phi).Ry(theta).Rz(lambda)
for theta, phi, and lambda.
Return a solution theta, phi, and lambda.
"""
quaternion_yzy = Quaternion.from_euler([theta1, xi, theta2], 'yzy')
euler = quaternion_yzy.to_zyz()
quaternion_zyz = Quaternion.from_euler(euler, 'zyz')
# output order different than rotation order
out_angles = (euler[1], euler[0], euler[2])
abs_inner = abs(quaternion_zyz.data.dot(quaternion_yzy.data))
if not np.allclose(abs_inner, 1, eps):
raise TranspilerError('YZY and ZYZ angles do not give same rotation matrix.')
out_angles = tuple(0 if np.abs(angle) < _CHOP_THRESHOLD else angle
for angle in out_angles)
return out_angles
def _split_runs_on_parameters(runs):
"""Finds runs containing parameterized gates and splits them into sequential
runs excluding the parameterized gates.
"""
out = []
for run in runs:
groups = groupby(run, lambda x: x.op.is_parameterized())
for group_is_parameterized, gates in groups:
if not group_is_parameterized:
out.append(list(gates))
return out