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 Uc gate global phase (backport #8231) #8378

Merged
merged 2 commits into from
Jul 19, 2022
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
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
37 changes: 25 additions & 12 deletions qiskit/extensions/quantum_initializer/isometry.py
Original file line number Diff line number Diff line change
Expand Up @@ -22,7 +22,6 @@

import itertools
import numpy as np

from qiskit.circuit.exceptions import CircuitError
from qiskit.circuit.instruction import Instruction
from qiskit.circuit.quantumcircuit import QuantumCircuit
Expand Down Expand Up @@ -73,6 +72,8 @@ def __init__(self, isometry, num_ancillas_zero, num_ancillas_dirty, epsilon=_EPS
if len(isometry.shape) == 1:
isometry = isometry.reshape(isometry.shape[0], 1)

self.iso_data = isometry

self.num_ancillas_zero = num_ancillas_zero
self.num_ancillas_dirty = num_ancillas_dirty
self._inverse = None
Expand Down Expand Up @@ -103,15 +104,23 @@ def __init__(self, isometry, num_ancillas_zero, num_ancillas_dirty, epsilon=_EPS
super().__init__("isometry", num_qubits, 0, [isometry])

def _define(self):

# TODO The inverse().inverse() is because there is code to uncompute (_gates_to_uncompute)
# an isometry, but not for generating its decomposition. It would be cheaper to do the
# later here instead.
gate = self.inverse().inverse()
gate = self.inv_gate()
gate = gate.inverse()
q = QuantumRegister(self.num_qubits)
iso_circuit = QuantumCircuit(q)
iso_circuit.append(gate, q[:])
self.definition = iso_circuit

def inverse(self):
self.params = []
inv = super().inverse()
self.params = [self.iso_data]
return inv

def _gates_to_uncompute(self):
"""
Call to create a circuit with gates that take the desired isometry to the first 2^m columns
Expand All @@ -127,9 +136,9 @@ def _gates_to_uncompute(self):
) = self._define_qubit_role(q)
# Copy the isometry (this is computationally expensive for large isometries but guarantees
# to keep a copyof the input isometry)
remaining_isometry = self.params[0].astype(complex) # note: "astype" does copy the isometry
remaining_isometry = self.iso_data.astype(complex) # note: "astype" does copy the isometry
diag = []
m = int(np.log2((self.params[0]).shape[1]))
m = int(np.log2((self.iso_data).shape[1]))
# Decompose the column with index column_index and attache the gate to the circuit object.
# Return the isometry that is left to decompose, where the columns up to index column_index
# correspond to the firstfew columns of the identity matrix up to diag, and hence we only
Expand All @@ -148,7 +157,7 @@ def _decompose_column(self, circuit, q, diag, remaining_isometry, column_index):
"""
Decomposes the column with index column_index.
"""
n = int(np.log2(self.params[0].shape[0]))
n = int(np.log2(self.iso_data.shape[0]))
for s in range(n):
self._disentangle(circuit, q, diag, remaining_isometry, column_index, s)

Expand All @@ -163,7 +172,7 @@ def _disentangle(self, circuit, q, diag, remaining_isometry, column_index, s):
# (note that we remove columns of the isometry during the procedure for efficiency)
k_prime = 0
v = remaining_isometry
n = int(np.log2(self.params[0].shape[0]))
n = int(np.log2(self.iso_data.shape[0]))

# MCG to set one entry to zero (preparation for disentangling with UCGate):
index1 = 2 * _a(k, s + 1) * 2**s + _b(k, s + 1)
Expand Down Expand Up @@ -215,7 +224,7 @@ def _disentangle(self, circuit, q, diag, remaining_isometry, column_index, s):
# The qubit with label n-s-1 is disentangled into the basis state k_s(k,s).
def _find_squs_for_disentangling(self, v, k, s):
k_prime = 0
n = int(np.log2(self.params[0].shape[0]))
n = int(np.log2(self.iso_data.shape[0]))
if _b(k, s + 1) == 0:
i_start = _a(k, s + 1)
else:
Expand All @@ -242,7 +251,7 @@ def _append_ucg_up_to_diagonal(self, circ, q, single_qubit_gates, control_labels
q_ancillas_zero,
q_ancillas_dirty,
) = self._define_qubit_role(q)
n = int(np.log2(self.params[0].shape[0]))
n = int(np.log2(self.iso_data.shape[0]))
qubits = q_input + q_ancillas_for_output
# Note that we have to reverse the control labels, since controls are provided by
# increasing qubit number toa UCGate by convention
Expand All @@ -264,7 +273,7 @@ def _append_mcg_up_to_diagonal(self, circ, q, gate, control_labels, target_label
q_ancillas_zero,
q_ancillas_dirty,
) = self._define_qubit_role(q)
n = int(np.log2(self.params[0].shape[0]))
n = int(np.log2(self.iso_data.shape[0]))
qubits = q_input + q_ancillas_for_output
control_qubits = _reverse_qubit_oder(_get_qubits_by_label(control_labels, qubits, n))
target_qubit = _get_qubits_by_label([target_label], qubits, n)[0]
Expand All @@ -284,8 +293,10 @@ def _append_mcg_up_to_diagonal(self, circ, q, gate, control_labels, target_label
return mcg_up_to_diag._get_diagonal()

def _define_qubit_role(self, q):
n = int(np.log2((self.params[0]).shape[0]))
m = int(np.log2((self.params[0]).shape[1]))

n = int(np.log2(self.iso_data.shape[0]))
m = int(np.log2(self.iso_data.shape[1]))

# Define the role of the qubits
q_input = q[:m]
q_ancillas_for_output = q[m:n]
Expand All @@ -297,10 +308,12 @@ def validate_parameter(self, parameter):
"""Isometry parameter has to be an ndarray."""
if isinstance(parameter, np.ndarray):
return parameter
if isinstance(parameter, (list, int)):
return parameter
else:
raise CircuitError(f"invalid param type {type(parameter)} for gate {self.name}")

def inverse(self):
def inv_gate(self):
"""Return the adjoint of the unitary."""
if self._inverse is None:
# call to generate the circuit that takes the isometry to the first 2^m columns
Expand Down
6 changes: 3 additions & 3 deletions qiskit/extensions/quantum_initializer/uc.py
Original file line number Diff line number Diff line change
Expand Up @@ -138,8 +138,7 @@ def _dec_ucg(self):
circuit = QuantumCircuit(q)
# If there is no control, we use the ZYZ decomposition
if not q_controls:
theta, phi, lamb = _DECOMPOSER1Q.angles(self.params[0])
circuit.u(theta, phi, lamb, q)
circuit.unitary(self.params[0], [q])
return circuit, diag
# If there is at least one control, first,
# we find the single qubit gates of the decomposition.
Expand All @@ -160,7 +159,7 @@ def _dec_ucg(self):
.dot(HGate().to_matrix())
)
# Add single-qubit gate
circuit.squ(squ, q_target)
circuit.unitary(squ, [q_target])
# The number of the control qubit is given by the number of zeros at the end
# of the binary representation of (i+1)
binary_rep = np.binary_repr(i + 1)
Expand All @@ -169,6 +168,7 @@ def _dec_ucg(self):
# Add C-NOT gate
if not i == len(single_qubit_gates) - 1:
circuit.cx(q_controls[q_contr_index], q_target)
circuit.global_phase -= 0.25 * np.pi
if not self.up_to_diagonal:
# Important: the diagonal gate is given in the computational basis of the qubits
# q[k-1],...,q[0],q_target (ordered with decreasing significance),
Expand Down
4 changes: 4 additions & 0 deletions releasenotes/notes/global-phase-ucgate-cd61355e314a3e64.yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
---
fixes:
- |
Fixed a global phase bug in :class:`~.UCGate`.
21 changes: 18 additions & 3 deletions test/python/circuit/test_uc.py
Original file line number Diff line number Diff line change
Expand Up @@ -46,9 +46,9 @@ class TestUCGate(QiskitTestCase):
[_id, _id],
[_id, 1j * _id],
[_id, _not, _id, _not],
[random_unitary(2, seed=541234).data for i in range(2**2)],
[random_unitary(2, seed=975163).data for i in range(2**3)],
[random_unitary(2, seed=629462).data for i in range(2**4)],
[random_unitary(2, seed=541234).data for _ in range(2**2)],
[random_unitary(2, seed=975163).data for _ in range(2**3)],
[random_unitary(2, seed=629462).data for _ in range(2**4)],
],
up_to_diagonal=[True, False],
)
Expand All @@ -70,6 +70,21 @@ def test_ucg(self, squs, up_to_diagonal):
unitary_desired = _get_ucg_matrix(squs)
self.assertTrue(matrix_equal(unitary_desired, unitary, ignore_phase=True))

def test_global_phase_ucg(self):
""" "Test global phase of uniformly controlled gates"""
gates = [random_unitary(2).data for _ in range(2**2)]
num_con = int(np.log2(len(gates)))
q = QuantumRegister(num_con + 1)
qc = QuantumCircuit(q)

qc.uc(gates, q[1:], q[0], up_to_diagonal=False)
simulator = BasicAer.get_backend("unitary_simulator")
result = execute(qc, simulator).result()
unitary = result.get_unitary(qc)
unitary_desired = _get_ucg_matrix(gates)

self.assertTrue(np.allclose(unitary_desired, unitary))


def _get_ucg_matrix(squs):
return block_diag(*squs)
Expand Down