Skip to content

Commit ce96481

Browse files
author
pablolh
committed
Fix matrix expander
Let's fix modules one by one. This commit was initially only intended to move this piece of code to its own folder, to add structure to the codebase, but it turned out I discovered a bug in it. - Fix untested bug where expanded matrix was wrong (not even unitary anymore) - Move matrix expander to a utils folder - Fix formatting - Add auxiliary functions - Add doctest and test
1 parent 38cb4e5 commit ce96481

File tree

5 files changed

+201
-46
lines changed

5 files changed

+201
-46
lines changed

opensquirrel/matrix_expander.py

Lines changed: 0 additions & 44 deletions
This file was deleted.

opensquirrel/test_interpreter.py

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,7 @@
22

33
from opensquirrel.common import ArgType
44
from opensquirrel.gates import querySemantic, querySignature
5-
from opensquirrel.matrix_expander import getBigMatrix
5+
from opensquirrel.utils.matrix_expander import get_expanded_matrix
66

77

88
class TestInterpreter:
@@ -24,7 +24,7 @@ def process(self, squirrelAST):
2424
semantic = querySemantic(
2525
self.gates, gateName, *[gateArgs[i] for i in range(len(gateArgs)) if signature[i] != ArgType.QUBIT]
2626
)
27-
bigMatrix = getBigMatrix(semantic, qubitOperands, totalQubits=squirrelAST.nQubits)
27+
bigMatrix = get_expanded_matrix(semantic, qubitOperands, number_of_qubits=squirrelAST.nQubits)
2828
totalUnitary = bigMatrix @ totalUnitary
2929

3030
return totalUnitary

opensquirrel/utils/__init__.py

Whitespace-only changes.

opensquirrel/utils/matrix_expander.py

Lines changed: 168 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,168 @@
1+
import math
2+
3+
import numpy as np
4+
5+
from opensquirrel.common import Can1
6+
from opensquirrel.gates import MultiQubitMatrixSemantic, Semantic, SingleQubitAxisAngleSemantic
7+
from typing import List
8+
9+
10+
def get_reduced_ket(ket: int, qubits: List[int]) -> int:
11+
"""
12+
Given a quantum ket represented by its corresponding base-10 integer, this computes the reduced ket
13+
where only the given qubits appear, in order.
14+
Roughly equivalent to the `pext` assembly instruction (bits extraction).
15+
16+
Args:
17+
ket: A quantum ket, represented by its corresponding non-negative integer.
18+
By convention, qubit #0 corresponds to the least significant bit.
19+
qubits: The indices of the qubits to extract. Order matters.
20+
21+
Returns:
22+
The non-negative integer corresponding to the reduced ket.
23+
24+
Examples:
25+
>>> get_reduced_ket(1, [0]) # 0b01
26+
1
27+
>>> get_reduced_ket(1111, [2]) # 0b01
28+
1
29+
>>> get_reduced_ket(1111, [5]) # 0b0
30+
0
31+
>>> get_reduced_ket(1111, [2, 5]) # 0b01
32+
1
33+
>>> get_reduced_ket(101, [1, 0]) # 0b10
34+
2
35+
>>> get_reduced_ket(101, [0, 1]) # 0b01
36+
1
37+
"""
38+
reduced_ket = 0
39+
for i, qubit in enumerate(qubits):
40+
reduced_ket |= ((ket & (1 << qubit)) >> qubit) << i
41+
42+
return reduced_ket
43+
44+
45+
def expand_ket(base_ket: int, reduced_ket: int, qubits: List[int]) -> int:
46+
"""
47+
Given a base quantum ket on n qubits and a reduced ket on a subset of those qubits, this computes the expanded ket
48+
where the reduction qubits and the other qubits are set based on the reduced ket and the base ket, respectively.
49+
Roughly equivalent to the `pdep` assembly instruction (bits deposit).
50+
51+
Args:
52+
base_ket: A quantum ket, represented by its corresponding non-negative integer.
53+
By convention, qubit #0 corresponds to the least significant bit.
54+
reduced_ket: A quantum ket, represented by its corresponding non-negative integer.
55+
By convention, qubit #0 corresponds to the least significant bit.
56+
qubits: The indices of the qubits to expand from the reduced ket. Order matters.
57+
58+
Returns:
59+
The non-negative integer corresponding to the expanded ket.
60+
61+
Examples:
62+
>>> expand_ket(0b00000, 0b0, [5]) # 0b000000
63+
0
64+
>>> expand_ket(0b00000, 0b1, [5]) # 0b100000
65+
32
66+
>>> expand_ket(0b00111, 0b0, [5]) # 0b000111
67+
7
68+
>>> expand_ket(0b00111, 0b1, [5]) # 0b100111
69+
39
70+
>>> expand_ket(0b0000, 0b000, [1, 2, 3]) # 0b0000
71+
0
72+
>>> expand_ket(0b0000, 0b001, [1, 2, 3]) # 0b0010
73+
2
74+
>>> expand_ket(0b0000, 0b011, [1, 2, 3]) # 0b0110
75+
6
76+
>>> expand_ket(0b0000, 0b101, [1, 2, 3]) # 0b1010
77+
10
78+
>>> expand_ket(0b0001, 0b101, [1, 2, 3]) # 0b1011
79+
11
80+
"""
81+
expanded_ket = base_ket
82+
for i, qubit in enumerate(qubits):
83+
expanded_ket &= ~(1 << qubit) # Erase bit.
84+
expanded_ket |= ((reduced_ket & (1 << i)) >> i) << qubit # Set bit to value from reduced_ket.
85+
86+
return expanded_ket
87+
88+
89+
def get_expanded_matrix(semantic: Semantic, qubit_operands: List[int], number_of_qubits: int) -> np.ndarray:
90+
"""
91+
Compute the unitary matrix corresponding to the gate applied to those qubit operands, taken among any number of qubits.
92+
This can be used for, e.g.,
93+
- testing,
94+
- permuting the operands of multi-qubit gates,
95+
- simulating a circuit (simulation in this way is inefficient for large numbers of qubits).
96+
97+
Args:
98+
semantic: The semantic of the gate.
99+
qubit_operands: The qubit indices on which the gate operates.
100+
number_of_qubits: The total number of qubits.
101+
102+
Examples:
103+
>>> X = SingleQubitAxisAngleSemantic((1, 0, 0), math.pi, math.pi / 2)
104+
>>> get_expanded_matrix(X, [1], 2).astype(int) # X q[1]
105+
array([[0, 0, 1, 0],
106+
[0, 0, 0, 1],
107+
[1, 0, 0, 0],
108+
[0, 1, 0, 0]])
109+
110+
>>> CNOT = MultiQubitMatrixSemantic(np.array([[1, 0, 0, 0], [0, 1, 0, 0], [0, 0, 0, 1], [0, 0, 1, 0]]))
111+
>>> get_expanded_matrix(CNOT, [0, 2], 3) # CNOT q[0], q[2]
112+
array([[1, 0, 0, 0, 0, 0, 0, 0],
113+
[0, 0, 0, 0, 0, 1, 0, 0],
114+
[0, 0, 1, 0, 0, 0, 0, 0],
115+
[0, 0, 0, 0, 0, 0, 0, 1],
116+
[0, 0, 0, 0, 1, 0, 0, 0],
117+
[0, 1, 0, 0, 0, 0, 0, 0],
118+
[0, 0, 0, 0, 0, 0, 1, 0],
119+
[0, 0, 0, 1, 0, 0, 0, 0]])
120+
>>> get_expanded_matrix(CNOT, [1, 2], 3) # CNOT q[1], q[2]
121+
array([[1, 0, 0, 0, 0, 0, 0, 0],
122+
[0, 1, 0, 0, 0, 0, 0, 0],
123+
[0, 0, 0, 0, 0, 0, 1, 0],
124+
[0, 0, 0, 0, 0, 0, 0, 1],
125+
[0, 0, 0, 0, 1, 0, 0, 0],
126+
[0, 0, 0, 0, 0, 1, 0, 0],
127+
[0, 0, 1, 0, 0, 0, 0, 0],
128+
[0, 0, 0, 1, 0, 0, 0, 0]])
129+
"""
130+
if isinstance(semantic, SingleQubitAxisAngleSemantic):
131+
assert len(qubit_operands) == 1
132+
133+
which_qubit = qubit_operands[0]
134+
135+
axis, angle, phase = semantic.axis, semantic.angle, semantic.phase
136+
result = np.kron(
137+
np.kron(np.eye(1 << (number_of_qubits - which_qubit - 1)), Can1(axis, angle, phase)), np.eye(1 << which_qubit)
138+
)
139+
assert result.shape == (1 << number_of_qubits, 1 << number_of_qubits)
140+
return result
141+
142+
assert isinstance(semantic, MultiQubitMatrixSemantic)
143+
144+
# The convention is to write gate matrices with operands reversed.
145+
# For instance, the first operand of CNOT is the control qubit, and this is written as
146+
# 1, 0, 0, 0
147+
# 0, 1, 0, 0
148+
# 0, 0, 0, 1
149+
# 0, 0, 1, 0
150+
# which corresponds to control being q[1] and target being q[0],
151+
# since qubit #i corresponds to the i-th LEAST significant bit.
152+
qubit_operands.reverse()
153+
154+
m = semantic.matrix
155+
156+
assert m.shape == (1 << len(qubit_operands), 1 << len(qubit_operands))
157+
158+
expanded_matrix = np.zeros((1 << number_of_qubits, 1 << number_of_qubits), dtype=m.dtype)
159+
160+
for expanded_matrix_column in range(expanded_matrix.shape[1]):
161+
small_matrix_col = get_reduced_ket(expanded_matrix_column, qubit_operands)
162+
163+
for small_matrix_row, value in enumerate(m[:, small_matrix_col]):
164+
expanded_matrix_row = expand_ket(expanded_matrix_column, small_matrix_row, qubit_operands)
165+
expanded_matrix[expanded_matrix_row][expanded_matrix_column] = value
166+
167+
assert expanded_matrix.shape == (1 << number_of_qubits, 1 << number_of_qubits)
168+
return expanded_matrix

test/test_testinterpreter.py

Lines changed: 31 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -198,6 +198,37 @@ def test_hadamard_cnot(self):
198198
)
199199
)
200200

201+
def test_hadamard_cnot_0_2(self):
202+
circuit = Circuit.from_string(
203+
DefaultGates,
204+
r"""
205+
version 3.0
206+
qubit[3] q
207+
208+
h q[0]
209+
cnot q[0], q[2]
210+
""",
211+
)
212+
print(circuit.test_get_circuit_matrix())
213+
self.assertTrue(
214+
np.allclose(
215+
circuit.test_get_circuit_matrix(),
216+
math.sqrt(0.5)
217+
* np.array(
218+
[
219+
[1, 1, 0, 0, 0, 0, 0, 0],
220+
[0, 0, 0, 0, 1, -1, 0, 0],
221+
[0, 0, 1, 1, 0, 0, 0, 0],
222+
[0, 0, 0, 0, 0, 0, 1, -1],
223+
[0, 0, 0, 0, 1, 1, 0, 0],
224+
[1, -1, 0, 0, 0, 0, 0, 0],
225+
[0, 0, 0, 0, 0, 0, 1, 1],
226+
[0, 0, 1, -1, 0, 0, 0, 0],
227+
]
228+
),
229+
)
230+
)
231+
201232

202233
if __name__ == "__main__":
203234
unittest.main()

0 commit comments

Comments
 (0)