-
Notifications
You must be signed in to change notification settings - Fork 2.4k
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
Implementation of the Gray-Synth and Patel–Markov–Hayes algorithms (#…
…2457) * implemented Gray-Synth algorithm * fixed formatting of the code * added functionality for processing matrices with same columns (multiple gates applied to same state) * added functionality for processing matrices with same columns (multiple gates applied to same state) also during the algorithm * fixed 2 bugs in lwr_cnot_synth() and in graysynth() * minor formatting fixes * Created test_graysynth.py file, for checking if graysynth.py works properly * minor improvements * fixed error as explained by @ajavadia * changed __init__.py * Fixed cyclic import error (suggested by @ajavadia) Co-Authored-By: Ali Javadi-Abhari <[email protected]> * Created new folder transpiler/synthesis for synthesis algorithms and put GraySynth algorithm into it. More changes to follow * upated testing function accordingly * added function documentations, removed redundant state = np.matrix(state) line * removed storage class, and replaced it with regular list using its stack functionality, and added raise Exception for numpy.ndarray * added documentation for Exception in function cnot_synth() * instead of using T-gates by default, it is now possible to specify any arbitrary phase-gate rotation * changelog.md * type fix angels -> angles * docstring edits * remove qreg from cnot_synth args * remove number (n_qubits) from function args as it can be inferred * specify a default of 2 for n_sections * simplify test via Operator class, and add test from paper example * make cnot_synth standalone (not cnot appender), and use QiskitError * fix bug in cnot_synth and add test * cnot_synth takes python lists or numpy ndarray * lint * Update graysynth.py * renamed n_sections --> section_size
- Loading branch information
1 parent
92446e6
commit 8d80735
Showing
4 changed files
with
537 additions
and
0 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,18 @@ | ||
# -*- 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. | ||
|
||
"""Module containing transpiler synthesize.""" | ||
|
||
|
||
from .graysynth import graysynth, cnot_synth |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,304 @@ | ||
# -*- coding: utf-8 -*- | ||
|
||
# This code is part of Qiskit. | ||
# | ||
# (C) Copyright IBM 2017, 2019. | ||
# | ||
# 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. | ||
|
||
""" | ||
Implementation of the GraySynth algorithm for synthesizing CNOT-Phase | ||
circuits with efficient CNOT cost, and the Patel-Hayes-Markov algorithm | ||
for optimal synthesis of linear (CNOT-only) reversible circuits. | ||
""" | ||
|
||
import copy | ||
import numpy as np | ||
from qiskit.circuit import QuantumCircuit | ||
from qiskit.exceptions import QiskitError | ||
|
||
|
||
def graysynth(cnots, angles, section_size=2): | ||
""" | ||
This function is an implementation of the GraySynth algorithm. | ||
GraySynth is a heuristic algorithm for synthesizing small parity networks. | ||
It is inspired by Gray codes. Given a set of binary strings S | ||
(called "cnots" bellow), the algorithm synthesizes a parity network for S by | ||
repeatedly choosing an index i to expand and then effectively recursing on | ||
the co-factors S_0 and S_1, consisting of the strings y in S, | ||
with y_i = 0 or 1 respectively. As a subset S is recursively expanded, | ||
CNOT gates are applied so that a designated target bit contains the | ||
(partial) parity ksi_y(x) where y_i = 1 if and only if y'_i = 1 for for all | ||
y' in S. If S is a singleton {y'}, then y = y', hence the target bit contains | ||
the value ksi_y'(x) as desired. | ||
Notably, rather than uncomputing this sequence of CNOT gates when a subset S | ||
is finished being synthesized, the algorithm maintains the invariant | ||
that the remaining parities to be computed are expressed over the current state | ||
of bits. This allows the algorithm to avoid the 'backtracking' inherent in | ||
uncomputing-based methods. | ||
The algorithm is described in detail in the following paper in section 4: | ||
"On the controlled-NOT complexity of controlled-NOT–phase circuits." | ||
Amy, Matthew, Parsiad Azimzadeh, and Michele Mosca. | ||
Quantum Science and Technology 4.1 (2018): 015002. | ||
Args: | ||
cnots (list[list]): a matrix whose columns are the parities to be synthesized | ||
e.g. | ||
[[0, 1, 1, 1, 1, 1], | ||
[1, 0, 0, 1, 1, 1], | ||
[1, 0, 0, 1, 0, 0], | ||
[0, 0, 1, 0, 1, 0]] | ||
corresponds to: | ||
x1^x2 + x0 + x0^x3 + x0^x1^x2 + x0^x1^x3 + x0^x1 | ||
angles (list): a list containing all the phase-shift gates which are | ||
to be applied, in the same order as in "cnots". A number is | ||
interpreted as the angle of u1(angle), otherwise the elements | ||
have to be 't', 'tdg', 's', 'sdg' or 'z'. | ||
section_size (int): the size of every section, used in _lwr_cnot_synth(), in the | ||
Patel–Markov–Hayes algorithm. section_size must be a factor of n_qubits. | ||
Returns: | ||
QuantumCircuit: the quantum circuit | ||
Raises: | ||
QiskitError: when dimensions of cnots and angles don't align | ||
""" | ||
n_qubits = len(cnots) | ||
|
||
# Create a quantum circuit on n_qubits | ||
qcir = QuantumCircuit(n_qubits) | ||
|
||
if len(cnots[0]) != len(angles): | ||
raise QiskitError('Size of "cnots" and "angles" do not match.') | ||
|
||
range_list = list(range(n_qubits)) | ||
epsilon = n_qubits | ||
sta = [] | ||
cnots_copy = np.transpose(np.array(copy.deepcopy(cnots))) | ||
state = np.eye(n_qubits).astype('int') # This matrix keeps track of the state in the algorithm | ||
|
||
# Check if some phase-shift gates can be applied, before adding any C-NOT gates | ||
for qubit in range(n_qubits): | ||
index = 0 | ||
for icnots in cnots_copy: | ||
if np.array_equal(icnots, state[qubit]): | ||
if angles[index] == 't': | ||
qcir.t(qubit) | ||
elif angles[index] == 'tdg': | ||
qcir.tdg(qubit) | ||
elif angles[index] == 's': | ||
qcir.s(qubit) | ||
elif angles[index] == 'sdg': | ||
qcir.sdg(qubit) | ||
elif angles[index] == 'z': | ||
qcir.z(qubit) | ||
else: | ||
qcir.u1(angles[index] % np.pi, qubit) | ||
del angles[index] | ||
cnots_copy = np.delete(cnots_copy, index, axis=0) | ||
if index == len(cnots_copy): | ||
break | ||
index -= 1 | ||
index += 1 | ||
|
||
# Implementation of the pseudo-code (Algorithm 1) in the aforementioned paper | ||
sta.append([cnots, range_list, epsilon]) | ||
while sta != []: | ||
[cnots, ilist, qubit] = sta.pop() | ||
if cnots == []: | ||
continue | ||
elif 0 <= qubit < n_qubits: | ||
condition = True | ||
while condition: | ||
condition = False | ||
for j in range(n_qubits): | ||
if (j != qubit) and (sum(cnots[j]) == len(cnots[j])): | ||
condition = True | ||
qcir.cx(j, qubit) | ||
state[qubit] ^= state[j] | ||
index = 0 | ||
for icnots in cnots_copy: | ||
if np.array_equal(icnots, state[qubit]): | ||
if angles[index] == 't': | ||
qcir.t(qubit) | ||
elif angles[index] == 'tdg': | ||
qcir.tdg(qubit) | ||
elif angles[index] == 's': | ||
qcir.s(qubit) | ||
elif angles[index] == 'sdg': | ||
qcir.sdg(qubit) | ||
elif angles[index] == 'z': | ||
qcir.z(qubit) | ||
else: | ||
qcir.u1(angles[index] % np.pi, qubit) | ||
del angles[index] | ||
cnots_copy = np.delete(cnots_copy, index, axis=0) | ||
if index == len(cnots_copy): | ||
break | ||
index -= 1 | ||
index += 1 | ||
for x in _remove_duplicates(sta + [[cnots, ilist, qubit]]): | ||
[cnotsp, _, _] = x | ||
if cnotsp == []: | ||
continue | ||
for ttt in range(len(cnotsp[j])): | ||
cnotsp[j][ttt] ^= cnotsp[qubit][ttt] | ||
if ilist == []: | ||
continue | ||
# See line 18 in pseudo-code of Algorithm 1 in the aforementioned paper | ||
# this choice of j maximizes the size of the largest subset (S_0 or S_1) | ||
# and the larger a subset, the closer it gets to the ideal in the | ||
# Gray code of one CNOT per string. | ||
j = ilist[np.argmax([[max(row.count(0), row.count(1)) for row in cnots][k] for k in ilist])] | ||
cnots0 = [] | ||
cnots1 = [] | ||
for y in list(map(list, zip(*cnots))): | ||
if y[j] == 0: | ||
cnots0.append(y) | ||
elif y[j] == 1: | ||
cnots1.append(y) | ||
cnots0 = list(map(list, zip(*cnots0))) | ||
cnots1 = list(map(list, zip(*cnots1))) | ||
if qubit == epsilon: | ||
sta.append([cnots1, list(set(ilist).difference([j])), j]) | ||
else: | ||
sta.append([cnots1, list(set(ilist).difference([j])), qubit]) | ||
sta.append([cnots0, list(set(ilist).difference([j])), qubit]) | ||
qcir += cnot_synth(state, section_size) | ||
return qcir | ||
|
||
|
||
def cnot_synth(state, section_size=2): | ||
""" | ||
This function is an implementation of the Patel–Markov–Hayes algorithm | ||
for optimal synthesis of linear reversible circuits, as specified by an | ||
n x n matrix. | ||
The algorithm is described in detail in the following paper: | ||
"Optimal synthesis of linear reversible circuits." | ||
Patel, Ketan N., Igor L. Markov, and John P. Hayes. | ||
Quantum Information & Computation 8.3 (2008): 282-294. | ||
Args: | ||
state (list[list] or ndarray): n x n matrix, describing the state | ||
of the input circuit | ||
section_size (int): the size of each section, used in _lwr_cnot_synth(), in the | ||
Patel–Markov–Hayes algorithm. section_size must be a factor of n_qubits. | ||
Returns: | ||
QuantumCircuit: a CNOT-only circuit implementing the | ||
desired linear transformation | ||
Raises: | ||
QiskitError: when variable "state" isn't of type numpy.matrix | ||
""" | ||
if not isinstance(state, (list, np.ndarray)): | ||
raise QiskitError('state should be of type list or numpy.ndarray, ' | ||
'but was of the type {}'.format(type(state))) | ||
state = np.array(state) | ||
# Synthesize lower triangular part | ||
[state, circuit_l] = _lwr_cnot_synth(state, section_size) | ||
state = np.transpose(state) | ||
# Synthesize upper triangular part | ||
[state, circuit_u] = _lwr_cnot_synth(state, section_size) | ||
circuit_l.reverse() | ||
for i in circuit_u: | ||
i.reverse() | ||
# Convert the list into a circuit of C-NOT gates | ||
circ = QuantumCircuit(state.shape[0]) | ||
for i in circuit_u + circuit_l: | ||
circ.cx(i[0], i[1]) | ||
return circ | ||
|
||
|
||
def _lwr_cnot_synth(state, section_size): | ||
""" | ||
This function is a helper function of the algorithm for optimal synthesis | ||
of linear reversible circuits (the Patel–Markov–Hayes algorithm). It works | ||
like gaussian elimination, except that it works a lot faster, and requires | ||
fewer steps (and therefore fewer CNOTs). It takes the matrix "state" and | ||
splits it into sections of size section_size. Then it eliminates all non-zero | ||
sub-rows within each section, which are the same as a non-zero sub-row | ||
above. Once this has been done, it continues with normal gaussian elimination. | ||
The benefit is that with small section sizes (m), most of the sub-rows will | ||
be cleared in the first step, resulting in a factor m fewer row row operations | ||
during Gaussian elimination. | ||
The algorithm is described in detail in the following paper | ||
"Optimal synthesis of linear reversible circuits." | ||
Patel, Ketan N., Igor L. Markov, and John P. Hayes. | ||
Quantum Information & Computation 8.3 (2008): 282-294. | ||
Args: | ||
state (ndarray): n x n matrix, describing a linear quantum circuit | ||
section_size (int): the section size the matrix columns are divided into | ||
Returns: | ||
numpy.matrix: n by n matrix, describing the state of the output circuit | ||
list: a k by 2 list of C-NOT operations that need to be applied | ||
""" | ||
circuit = [] | ||
n_qubits = state.shape[0] | ||
|
||
# If the matrix is already an upper triangular one, | ||
# there is no need for any transformations | ||
if np.allclose(state, np.triu(state)): | ||
return [state, circuit] | ||
# Iterate over column sections | ||
for sec in range(1, int(np.ceil(n_qubits/section_size)+1)): | ||
# Remove duplicate sub-rows in section sec | ||
patt = {} | ||
for row in range((sec-1)*section_size, n_qubits): | ||
sub_row_patt = copy.deepcopy(state[row, (sec-1)*section_size:sec*section_size]) | ||
if np.sum(sub_row_patt) == 0: | ||
continue | ||
if str(sub_row_patt) not in patt: | ||
patt[str(sub_row_patt)] = row | ||
else: | ||
state[row, :] ^= state[patt[str(sub_row_patt)], :] | ||
circuit.append([patt[str(sub_row_patt)], row]) | ||
# Use gaussian elimination for remaining entries in column section | ||
for col in range((sec-1)*section_size, sec*section_size): | ||
# Check if 1 on diagonal | ||
diag_one = 1 | ||
if state[col, col] == 0: | ||
diag_one = 0 | ||
# Remove ones in rows below column col | ||
for row in range(col+1, n_qubits): | ||
if state[row, col] == 1: | ||
if diag_one == 0: | ||
state[col, :] ^= state[row, :] | ||
circuit.append([row, col]) | ||
diag_one = 1 | ||
state[row, :] ^= state[col, :] | ||
circuit.append([col, row]) | ||
return [state, circuit] | ||
|
||
|
||
def _remove_duplicates(lists): | ||
""" | ||
Remove duplicates in list | ||
Args: | ||
lists (list): a list which may contain duplicate elements. | ||
Returns: | ||
list: a list which contains only unique elements. | ||
""" | ||
|
||
unique_list = [] | ||
for element in lists: | ||
if element not in unique_list: | ||
unique_list.append(element) | ||
return unique_list |
Oops, something went wrong.