Skip to content

Commit f3b457a

Browse files
committed
Implementation of change_of_basis for commuting groups of QubitOperators
1 parent f56165e commit f3b457a

File tree

4 files changed

+395
-32
lines changed

4 files changed

+395
-32
lines changed

src/qrisp/operators/qubit/__init__.py

+1
Original file line numberDiff line numberDiff line change
@@ -22,3 +22,4 @@
2222
from qrisp.operators.qubit.bound_qubit_operator import *
2323
#from qrisp.operators.qubit.pauli_measurement import *
2424
from qrisp.operators.qubit.operator_factors import *
25+
from qrisp.operators.qubit.commutativity_tools import *
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,256 @@
1+
"""
2+
\********************************************************************************
3+
* Copyright (c) 2024 the Qrisp authors
4+
*
5+
* This program and the accompanying materials are made available under the
6+
* terms of the Eclipse Public License 2.0 which is available at
7+
* http://www.eclipse.org/legal/epl-2.0.
8+
*
9+
* This Source Code may also be made available under the following Secondary
10+
* Licenses when the conditions for such availability set forth in the Eclipse
11+
* Public License, v. 2.0 are satisfied: GNU General Public License, version 2
12+
* with the GNU Classpath Exception which is
13+
* available at https://www.gnu.org/software/classpath/license.html.
14+
*
15+
* SPDX-License-Identifier: EPL-2.0 OR GPL-2.0 WITH Classpath-exception-2.0
16+
********************************************************************************/
17+
"""
18+
19+
import numpy as np
20+
21+
22+
def inverse_mod2(matrix):
23+
"""
24+
Calculates the inverse of a binary matrix over the field of integers modulo 2.
25+
26+
Parameters
27+
----------
28+
matrix : numpy.array
29+
An invertible binary matrix.
30+
result : numpy.array
31+
The inverse of the given matrix over the field of integers modulo 2.
32+
33+
"""
34+
rows, columns = matrix.shape
35+
if rows != columns:
36+
raise Exception("The matrix must be a square matrix")
37+
38+
matrix = matrix.copy()
39+
result = np.eye(rows, dtype=int)
40+
41+
for i in range(rows):
42+
# Find the pivot row
43+
max_row = i + np.argmax(matrix[i:,i])
44+
45+
# Swap the current row with the pivot row
46+
matrix[[i, max_row]] = matrix[[max_row, i]]
47+
result[[i, max_row]] = result[[max_row, i]]
48+
49+
# Eliminate all rows below the pivot
50+
for j in range(i + 1, rows):
51+
if matrix[j, i] == 1:
52+
matrix[j] = (matrix[j] + matrix[i]) % 2
53+
result[j] = (result[j] + result[i]) % 2
54+
55+
# Backward elimination
56+
for i in range(rows - 1, -1, -1):
57+
for j in range(i - 1, -1, -1):
58+
if matrix[j, i] == 1:
59+
matrix[j] = (matrix[j] + matrix[i]) % 2
60+
result[j] = (result[j] + result[i]) % 2
61+
62+
return result
63+
64+
65+
def gaussian_elimination_mod2(matrix, type='row', show_pivots=False):
66+
r"""
67+
Performs Gaussian elimination in the field $F_2$.
68+
69+
Parameters
70+
----------
71+
matrix : numpy.array
72+
A binary matrix.
73+
type : str, optional
74+
Available aore ``row`` for row echelon form, and ``column`` for column echelon form.
75+
The default is ``row``.
76+
show_pivots : Boolean, optional
77+
If ``True``, the pivot columns (rows) are returned.
78+
79+
Returns
80+
-------
81+
matrix : numpy.array
82+
The row (column) echelon form of the given matrix.
83+
pivots : list[int], optional
84+
A list of indices for the pivot columns (rows).
85+
86+
"""
87+
88+
matrix = matrix.copy()
89+
90+
rows, columns = matrix.shape
91+
column_index = 0
92+
row_index = 0
93+
pivots = [] # the pivot columns (type='row') or rows (type='column')
94+
95+
if type=='row':
96+
while row_index<rows and column_index<columns:
97+
# Find the pivot row
98+
max_row = row_index + np.argmax(matrix[row_index:,column_index])
99+
100+
if matrix[max_row,column_index]==0:
101+
column_index+=1
102+
else:
103+
# Pivot
104+
pivots.append(column_index)
105+
106+
# Swap the current row with the pivot row
107+
matrix[[row_index, max_row]] = matrix[[max_row, row_index]]
108+
109+
# Eliminate all rows below the pivot
110+
for j in range(row_index + 1, rows):
111+
if matrix[j, column_index] == 1:
112+
matrix[j] = (matrix[j] + matrix[row_index]) % 2
113+
114+
row_index+=1
115+
column_index+=1
116+
117+
elif type=='column':
118+
while row_index<rows and column_index<columns:
119+
# Find the pivot column
120+
max_column = column_index + np.argmax(matrix[row_index,column_index:])
121+
122+
if matrix[row_index,max_column]==0:
123+
row_index+=1
124+
else:
125+
# Pivot
126+
pivots.append(row_index)
127+
128+
# Swap the current column with the pivot column
129+
matrix[:,[column_index, max_column]] = matrix[:,[max_column, column_index]]
130+
131+
# Eliminate all rows below the pivot
132+
for j in range(column_index + 1, columns):
133+
if matrix[row_index, j] == 1:
134+
matrix[:,j] = (matrix[:,j] + matrix[:,column_index]) % 2
135+
136+
row_index+=1
137+
column_index+=1
138+
139+
if show_pivots:
140+
return matrix, pivots #(pivot_rows,pivot_cols)
141+
else:
142+
return matrix
143+
144+
145+
def construct_change_of_basis(S):
146+
"""
147+
Implements the CZ construction outlined in https://quantum-journal.org/papers/q-2021-01-20-385/.
148+
149+
Parameters
150+
----------
151+
S : numpy.array
152+
A matrix representing a list of commuting Pauli operators: Each column is a Pauli operator in binary representation.
153+
154+
Returns
155+
-------
156+
A : numpy.array
157+
Adjacency matrix for the graph state.
158+
R_inv : numpy.array
159+
A matrix representing a list of new Pauli operators.
160+
h_list : numpy.array
161+
A list indicating the qubits on which a Hadamard gate is applied.
162+
s_list : numpy.array
163+
A list indicating the qubits on which an S gate is applied.
164+
perm_vec : numpy.array
165+
A vector repesenting a permutation.
166+
167+
"""
168+
169+
n = int(S.shape[0]/2)
170+
171+
####################
172+
# Step 0: Calculate S_0: Independent columns (i.e., Pauli terms) of S
173+
####################
174+
175+
S_reduced, independent_cols = gaussian_elimination_mod2(S, show_pivots=True)
176+
k = len(independent_cols)
177+
178+
#S0 = np.vstack((z_matrix[:,independent_cols], x_matrix[:,independent_cols]))
179+
S0 = S[:,independent_cols]
180+
181+
R0_inv = S_reduced[:k, :]
182+
183+
####################
184+
# Step 1: Calculate S_1: The first k rows of the X component have full rank k
185+
####################
186+
187+
# Find independent rows in X component of S0
188+
S0X_reduced, independent_rows = gaussian_elimination_mod2(S0[-n:, :], type='column', show_pivots=True)
189+
190+
# Construnct S1 by applying a Hadamard (i.e., a swap) to the rows of S0 not in independent_rows
191+
h_list = [i for i in range(n) if i not in independent_rows]
192+
S1 = S0.copy()
193+
for i in h_list:
194+
S1[[i, n+i]] = S1[[n+i, i]]
195+
196+
# Find independent rows in X component of S1
197+
S1X_reduced, independent_rows = gaussian_elimination_mod2(S1[-n:, :], type="column", show_pivots=True)
198+
199+
# Construct permutation achieving that the first k rows in X component of S1 are independent
200+
perm = np.arange(0,n)
201+
for index1, index2 in enumerate(independent_rows):
202+
perm[index1] = index2
203+
perm[index2] = index1
204+
205+
# Apply permutation to rows of S1 for Z and X component
206+
S1 = np.vstack((S1[:n,:][perm],S1[-n:,:][perm]))
207+
208+
####################
209+
# Step 2: Calculate S2: The first k rows of the X component are the identity matrix
210+
####################
211+
212+
R1_inv = S1[n:n+k,:]
213+
R1 = inverse_mod2(R1_inv)
214+
S2 = S1 @ R1 % 2
215+
216+
####################
217+
# Step 3: Calculate S3: Basis extension
218+
####################
219+
220+
C = S2[:k, :]
221+
D = S2[k:n, :]
222+
F = S2[-(n-k):, :]
223+
224+
S3 = np.block([[C, np.transpose(D)],
225+
[D, np.zeros((n-k,n-k), dtype=int)],
226+
[np.eye(k, dtype=int), np.zeros((k,n-k), dtype=int)],
227+
[F, np.eye(n-k, dtype=int)]])
228+
R2_inv = np.block([[np.eye(k, dtype=int)],
229+
[np.zeros((n-k,k), dtype=int)]])
230+
231+
####################
232+
# Step 4: Calculate S4
233+
####################
234+
235+
R3_inv = S3[-n:,:]
236+
R3 = inverse_mod2(R3_inv)
237+
238+
S4 = S3 @ R3 % 2
239+
240+
# Remove diagonal entries in upper left block
241+
s_list = []
242+
for i in range(n):
243+
if S4[:n, :][i,i]==1:
244+
s_list.append(i)
245+
246+
Q2 = np.block([[np.eye(n, dtype=int),S4[:n, :]*np.eye(n, dtype=int)],
247+
[np.zeros((n,n), dtype=int),np.eye(n, dtype=int)]])
248+
249+
S4 = Q2 @ S4 % 2
250+
251+
R_inv = R3_inv @ R2_inv @ R1_inv @ R0_inv % 2
252+
253+
# Adjacency matrix for the graph
254+
A = S4[:n, :]
255+
256+
return A, R_inv, h_list, s_list, perm

0 commit comments

Comments
 (0)