qaravan 0.1.0__tar.gz
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- qaravan-0.1.0/LICENSE +20 -0
- qaravan-0.1.0/PKG-INFO +19 -0
- qaravan-0.1.0/README.md +11 -0
- qaravan-0.1.0/pyproject.toml +3 -0
- qaravan-0.1.0/setup.cfg +4 -0
- qaravan-0.1.0/setup.py +19 -0
- qaravan-0.1.0/src/qaravan/__init__.py +1 -0
- qaravan-0.1.0/src/qaravan/algebraQ/__init__.py +1 -0
- qaravan-0.1.0/src/qaravan/algebraQ/matchgate_sim.py +273 -0
- qaravan-0.1.0/src/qaravan/core/__init__.py +9 -0
- qaravan-0.1.0/src/qaravan/core/base_sim.py +53 -0
- qaravan-0.1.0/src/qaravan/core/circuits.py +147 -0
- qaravan-0.1.0/src/qaravan/core/compilers.py +247 -0
- qaravan-0.1.0/src/qaravan/core/gates.py +406 -0
- qaravan-0.1.0/src/qaravan/core/hamiltonians.py +306 -0
- qaravan-0.1.0/src/qaravan/core/lattices.py +327 -0
- qaravan-0.1.0/src/qaravan/core/noise.py +172 -0
- qaravan-0.1.0/src/qaravan/core/paulis.py +57 -0
- qaravan-0.1.0/src/qaravan/core/utils.py +121 -0
- qaravan-0.1.0/src/qaravan/tensorQ/__init__.py +6 -0
- qaravan-0.1.0/src/qaravan/tensorQ/density_matrix_sim.py +129 -0
- qaravan-0.1.0/src/qaravan/tensorQ/initializations.py +142 -0
- qaravan-0.1.0/src/qaravan/tensorQ/mpdo_sim.py +168 -0
- qaravan-0.1.0/src/qaravan/tensorQ/mps_sim.py +46 -0
- qaravan-0.1.0/src/qaravan/tensorQ/statevector_sim.py +102 -0
- qaravan-0.1.0/src/qaravan/tensorQ/tn.py +500 -0
- qaravan-0.1.0/src/qaravan.egg-info/PKG-INFO +19 -0
- qaravan-0.1.0/src/qaravan.egg-info/SOURCES.txt +29 -0
- qaravan-0.1.0/src/qaravan.egg-info/dependency_links.txt +1 -0
- qaravan-0.1.0/src/qaravan.egg-info/requires.txt +5 -0
- qaravan-0.1.0/src/qaravan.egg-info/top_level.txt +1 -0
qaravan-0.1.0/LICENSE
ADDED
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2025 Faisal Alam
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights to
|
|
8
|
+
use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of
|
|
9
|
+
the Software, and to permit persons to whom the Software is furnished to do so,
|
|
10
|
+
subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS
|
|
17
|
+
FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR
|
|
18
|
+
COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER
|
|
19
|
+
IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN
|
|
20
|
+
CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
qaravan-0.1.0/PKG-INFO
ADDED
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: qaravan
|
|
3
|
+
Version: 0.1.0
|
|
4
|
+
Summary: Unified classical simulation of noiseless and noisy quantum circuits
|
|
5
|
+
Author: Faisal Alam
|
|
6
|
+
Author-email: mfalam2@illinois.edu
|
|
7
|
+
Requires-Python: >=3.11
|
|
8
|
+
License-File: LICENSE
|
|
9
|
+
Requires-Dist: numpy>=2.2.4
|
|
10
|
+
Requires-Dist: scipy>=1.15.2
|
|
11
|
+
Requires-Dist: matplotlib>=3.10.1
|
|
12
|
+
Requires-Dist: tqdm>=4.67.1
|
|
13
|
+
Requires-Dist: ncon>=2.0.0
|
|
14
|
+
Dynamic: author
|
|
15
|
+
Dynamic: author-email
|
|
16
|
+
Dynamic: license-file
|
|
17
|
+
Dynamic: requires-dist
|
|
18
|
+
Dynamic: requires-python
|
|
19
|
+
Dynamic: summary
|
qaravan-0.1.0/README.md
ADDED
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
# Qaravan
|
|
2
|
+
|
|
3
|
+
**Qaravan** is a Python library for simulating quantum circuits with and without noise using a variety of classical simulation techniques.
|
|
4
|
+
|
|
5
|
+
## Overview
|
|
6
|
+
|
|
7
|
+
Qaravan is organized into two main submodules
|
|
8
|
+
|
|
9
|
+
**tensorQ**: contains simulators based on tensor contraction: statevector, density matrix, matrix product states and matrix product density operators
|
|
10
|
+
|
|
11
|
+
**algebraQ**: contains simulators based on algebraic methods: Clifford, matchgate, Pauli path propagation and Lie algebraic simulation (in development)
|
qaravan-0.1.0/setup.cfg
ADDED
qaravan-0.1.0/setup.py
ADDED
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
from setuptools import setup, find_packages
|
|
2
|
+
|
|
3
|
+
setup(
|
|
4
|
+
name="qaravan",
|
|
5
|
+
packages=find_packages(where="src"),
|
|
6
|
+
python_requires='>=3.11',
|
|
7
|
+
package_dir={"": "src"},
|
|
8
|
+
version="0.1.0",
|
|
9
|
+
author="Faisal Alam",
|
|
10
|
+
author_email="mfalam2@illinois.edu",
|
|
11
|
+
description="Unified classical simulation of noiseless and noisy quantum circuits",
|
|
12
|
+
install_requires=[
|
|
13
|
+
"numpy>=2.2.4",
|
|
14
|
+
"scipy>=1.15.2",
|
|
15
|
+
"matplotlib>=3.10.1",
|
|
16
|
+
"tqdm>=4.67.1",
|
|
17
|
+
"ncon>=2.0.0"
|
|
18
|
+
]
|
|
19
|
+
)
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
__version__ = '0.1.0'
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
from .matchgate_sim import *
|
|
@@ -0,0 +1,273 @@
|
|
|
1
|
+
import numpy as np
|
|
2
|
+
from qaravan.core import BaseSim, pauli_I, pauli_X, pauli_Y, pauli_Z
|
|
3
|
+
from functools import reduce
|
|
4
|
+
from scipy.linalg import block_diag
|
|
5
|
+
import re
|
|
6
|
+
|
|
7
|
+
def is_special_orthogonal(mat):
|
|
8
|
+
return bool(np.allclose(np.dot(mat.T, mat), np.eye(mat.shape[0])) and np.allclose(np.dot(mat, mat.T), np.eye(mat.shape[0]))
|
|
9
|
+
and np.isclose(np.linalg.det(mat), 1.0))
|
|
10
|
+
|
|
11
|
+
def is_special_unitary(mat):
|
|
12
|
+
return bool(np.allclose(np.dot(mat.T.conj(), mat), np.eye(mat.shape[0])) and np.allclose(np.dot(mat, mat.T.conj()), np.eye(mat.shape[0]))
|
|
13
|
+
and np.isclose(np.linalg.det(mat), 1.0))
|
|
14
|
+
|
|
15
|
+
def random_su2():
|
|
16
|
+
x = np.random.randn(4)
|
|
17
|
+
x = x / np.linalg.norm(x)
|
|
18
|
+
a = x[0] + 1j*x[1]
|
|
19
|
+
b = x[2] + 1j*x[3]
|
|
20
|
+
return np.array([[a, -b.conj()],
|
|
21
|
+
[b, a.conj()]], dtype=complex)
|
|
22
|
+
|
|
23
|
+
def matchgate_from_blocks(A,B):
|
|
24
|
+
G = np.zeros((4, 4), dtype=complex)
|
|
25
|
+
G[0, 0] = A[0,0]
|
|
26
|
+
G[0, 3] = A[0,1]
|
|
27
|
+
G[3, 0] = A[1,0]
|
|
28
|
+
G[3, 3] = A[1,1]
|
|
29
|
+
G[1, 1] = B[0,0]
|
|
30
|
+
G[1, 2] = B[0,1]
|
|
31
|
+
G[2, 1] = B[1,0]
|
|
32
|
+
G[2, 2] = B[1,1]
|
|
33
|
+
return G
|
|
34
|
+
|
|
35
|
+
def random_matchgate():
|
|
36
|
+
A = random_su2()
|
|
37
|
+
B = random_su2()
|
|
38
|
+
return matchgate_from_blocks(A, B)
|
|
39
|
+
|
|
40
|
+
def generate_majorana_ops(n):
|
|
41
|
+
majorana_ops = []
|
|
42
|
+
for i in range(n):
|
|
43
|
+
kron_list = [pauli_Z] * i + [pauli_X] + [pauli_I] * (n - i - 1)
|
|
44
|
+
op = reduce(np.kron, kron_list)
|
|
45
|
+
majorana_ops.append(op)
|
|
46
|
+
|
|
47
|
+
kron_list = [pauli_Z] * i + [pauli_Y] + [pauli_I] * (n - i - 1)
|
|
48
|
+
op = reduce(np.kron, kron_list)
|
|
49
|
+
majorana_ops.append(op)
|
|
50
|
+
|
|
51
|
+
return majorana_ops
|
|
52
|
+
|
|
53
|
+
def generate_R(n, U, indices):
|
|
54
|
+
majorana_ops = generate_majorana_ops(2)
|
|
55
|
+
m_indices = np.arange(2*indices[0], 2*indices[1]+2)
|
|
56
|
+
|
|
57
|
+
R = np.eye(2*n, dtype=complex)
|
|
58
|
+
for i, ml in zip(m_indices, majorana_ops):
|
|
59
|
+
for j, mr in zip(m_indices, majorana_ops):
|
|
60
|
+
R[i,j] = np.trace(mr @ U @ ml @ U.conj().T)/(2**2)
|
|
61
|
+
|
|
62
|
+
Rp = np.array([R.T[2*i,:] + 1j*R.T[2*i+1,:] for i in range(n)])
|
|
63
|
+
assert is_special_orthogonal(R), "R is not special orthogonal"
|
|
64
|
+
return R, Rp
|
|
65
|
+
|
|
66
|
+
def circuit_to_R(circ):
|
|
67
|
+
R = np.eye(2*circ.num_sites, dtype=complex)
|
|
68
|
+
for gate in circ.gate_list:
|
|
69
|
+
if gate.name == "SWAP":
|
|
70
|
+
mats = [pauli_I, pauli_X, pauli_Y, pauli_Z]
|
|
71
|
+
ch = np.random.choice(np.arange(4))
|
|
72
|
+
mat = mats[ch]
|
|
73
|
+
matrix = np.kron(mat, mat)
|
|
74
|
+
elif gate.name == "M":
|
|
75
|
+
matrix = gate.matrix
|
|
76
|
+
else:
|
|
77
|
+
raise ValueError(f"Unsupported gate: {gate.name}")
|
|
78
|
+
|
|
79
|
+
r, _ = generate_R(circ.num_sites, matrix, gate.indices)
|
|
80
|
+
R = R @ r
|
|
81
|
+
|
|
82
|
+
Rp = np.array([R.T[2*i,:] + 1j*R.T[2*i+1,:] for i in range(circ.num_sites)])
|
|
83
|
+
return R, Rp
|
|
84
|
+
|
|
85
|
+
def generate_labels(instring, outstring):
|
|
86
|
+
in_labels = []
|
|
87
|
+
for i, bit in enumerate(instring):
|
|
88
|
+
if bit == '1':
|
|
89
|
+
in_labels.append(f'gamma_{2*i}')
|
|
90
|
+
|
|
91
|
+
out_labels = []
|
|
92
|
+
for i, bit in enumerate(outstring):
|
|
93
|
+
if bit == '0':
|
|
94
|
+
out_labels.append(f'gamma_m{i}')
|
|
95
|
+
out_labels.append(f'gamma_n{i}')
|
|
96
|
+
elif bit == '1':
|
|
97
|
+
out_labels.append(f'gamma_n{i}')
|
|
98
|
+
out_labels.append(f'gamma_m{i}')
|
|
99
|
+
|
|
100
|
+
return in_labels + out_labels + in_labels[::-1]
|
|
101
|
+
|
|
102
|
+
def parse_label(label):
|
|
103
|
+
m = re.match(r"gamma_([nm]?)(\d+)", label)
|
|
104
|
+
if not m:
|
|
105
|
+
raise ValueError(f"Label '{label}' does not match expected pattern.")
|
|
106
|
+
kind = m.group(1)
|
|
107
|
+
if kind == "":
|
|
108
|
+
kind = "gamma" # bare gamma, use its number directly
|
|
109
|
+
idx = int(m.group(2))
|
|
110
|
+
return kind, idx
|
|
111
|
+
|
|
112
|
+
def get_entry(Rp, H, label_i, label_j):
|
|
113
|
+
kind_i, idx_i = parse_label(label_i)
|
|
114
|
+
kind_j, idx_j = parse_label(label_j)
|
|
115
|
+
|
|
116
|
+
if kind_i == 'm' and kind_j == 'm':
|
|
117
|
+
return (Rp @ H @ Rp.T)[idx_i, idx_j] # gamma_mi, gamma_mj
|
|
118
|
+
|
|
119
|
+
elif kind_i == 'm' and kind_j == 'n':
|
|
120
|
+
return (Rp @ H @ Rp.conj().T)[idx_i, idx_j] # gamma_mi, gamma_nj
|
|
121
|
+
|
|
122
|
+
elif kind_i == 'm' and kind_j == 'gamma':
|
|
123
|
+
return (Rp @ H)[idx_i, idx_j]
|
|
124
|
+
|
|
125
|
+
elif kind_i == 'n' and kind_j == 'm':
|
|
126
|
+
return (Rp.conj() @ H @ Rp.T)[idx_i, idx_j]
|
|
127
|
+
|
|
128
|
+
elif kind_i == 'n' and kind_j == 'n':
|
|
129
|
+
return (Rp.conj() @ H @ Rp.conj().T)[idx_i, idx_j]
|
|
130
|
+
|
|
131
|
+
elif kind_i == 'n' and kind_j == 'gamma':
|
|
132
|
+
return (Rp.conj() @ H)[idx_i, idx_j]
|
|
133
|
+
|
|
134
|
+
elif kind_i == 'gamma' and kind_j == 'm':
|
|
135
|
+
return (H @ Rp.T)[idx_i, idx_j]
|
|
136
|
+
|
|
137
|
+
elif kind_i == 'gamma' and kind_j == 'n':
|
|
138
|
+
return (H @ Rp.conj().T)[idx_i, idx_j]
|
|
139
|
+
|
|
140
|
+
elif kind_i == 'gamma' and kind_j == 'gamma':
|
|
141
|
+
return int(idx_i == idx_j)
|
|
142
|
+
|
|
143
|
+
else:
|
|
144
|
+
raise ValueError(f"Invalid labels: {label_i}, {label_j}")
|
|
145
|
+
|
|
146
|
+
def exp_mat(n, labels, Rp):
|
|
147
|
+
H = block_diag(*[np.array([[1,1j], [-1j,1]])] * n)
|
|
148
|
+
mat = np.zeros((len(labels), len(labels)), dtype=complex)
|
|
149
|
+
for i, label_i in enumerate(labels):
|
|
150
|
+
for j, label_j in enumerate(labels):
|
|
151
|
+
if i < j:
|
|
152
|
+
mat[i,j] = get_entry(Rp, H, label_i, label_j)
|
|
153
|
+
elif i > j:
|
|
154
|
+
mat[i,j] = -mat[j,i]
|
|
155
|
+
|
|
156
|
+
return mat
|
|
157
|
+
|
|
158
|
+
def probability(instring, outstring, circ):
|
|
159
|
+
n = len(instring)
|
|
160
|
+
num_measured_qubits = len([bit for bit in outstring if bit != 'i'])
|
|
161
|
+
|
|
162
|
+
labels = generate_labels(instring, outstring)
|
|
163
|
+
_, Rp = circuit_to_R(circ)
|
|
164
|
+
mat = exp_mat(n, labels, Rp)
|
|
165
|
+
|
|
166
|
+
norm = 2**(2*num_measured_qubits)
|
|
167
|
+
return (np.sqrt(np.linalg.det(mat))/norm).real
|
|
168
|
+
|
|
169
|
+
def sample(instring, circ, epsilon=0.0):
|
|
170
|
+
s = ''
|
|
171
|
+
prev_probs = [1.0]
|
|
172
|
+
for i in range(circ.num_sites):
|
|
173
|
+
outstring = s + '0' + 'i'*(circ.num_sites - i - 1)
|
|
174
|
+
p = probability(instring, outstring, circ) + np.random.normal(0, epsilon)
|
|
175
|
+
if np.random.rand() < p/prev_probs[-1]:
|
|
176
|
+
s += '0'
|
|
177
|
+
normalizer = p
|
|
178
|
+
else:
|
|
179
|
+
s += '1'
|
|
180
|
+
normalizer = prev_probs[-1] - p
|
|
181
|
+
prev_probs.append(normalizer)
|
|
182
|
+
|
|
183
|
+
return s
|
|
184
|
+
|
|
185
|
+
class MatchgateSimH(BaseSim):
|
|
186
|
+
def __init__(self, circ, init_state=None):
|
|
187
|
+
super().__init__(circ, init_state=init_state, nm=None)
|
|
188
|
+
|
|
189
|
+
def initialize_state(self):
|
|
190
|
+
""" internal state is a SO(2n) matrix, where n is the number of sites """
|
|
191
|
+
self.state = np.eye(2*self.num_sites, dtype=complex)
|
|
192
|
+
self.instring = '0' * self.num_sites if self.init_state is None else self.init_state
|
|
193
|
+
|
|
194
|
+
def generate_R(self, gate):
|
|
195
|
+
U, indices = gate.matrix, gate.indices
|
|
196
|
+
majorana_ops = generate_majorana_ops(2)
|
|
197
|
+
m_indices = np.arange(2*indices[0], 2*indices[1]+2)
|
|
198
|
+
|
|
199
|
+
R = np.eye(2*self.num_sites, dtype=complex)
|
|
200
|
+
for i, ml in zip(m_indices, majorana_ops):
|
|
201
|
+
for j, mr in zip(m_indices, majorana_ops):
|
|
202
|
+
R[i,j] = np.trace(mr @ U @ ml @ U.conj().T)/(2**2)
|
|
203
|
+
|
|
204
|
+
assert is_special_orthogonal(R), "R is not special orthogonal"
|
|
205
|
+
return R
|
|
206
|
+
|
|
207
|
+
def apply_gate(self, gate):
|
|
208
|
+
R = self.generate_R(gate)
|
|
209
|
+
self.state = R @ self.state
|
|
210
|
+
|
|
211
|
+
def probability(self, outstring):
|
|
212
|
+
n = self.num_sites
|
|
213
|
+
num_measured_qubits = len([bit for bit in outstring if bit != 'i'])
|
|
214
|
+
|
|
215
|
+
labels = generate_labels(self.instring, outstring)
|
|
216
|
+
R = self.state
|
|
217
|
+
Rp = np.array([R.T[2*i,:] + 1j*R.T[2*i+1,:] for i in range(self.num_sites)])
|
|
218
|
+
mat = exp_mat(n, labels, Rp)
|
|
219
|
+
|
|
220
|
+
norm = 2**(2*num_measured_qubits)
|
|
221
|
+
return (np.sqrt(np.linalg.det(mat))/norm).real
|
|
222
|
+
|
|
223
|
+
def sample(self):
|
|
224
|
+
s = ''
|
|
225
|
+
prev_probs = [1.0]
|
|
226
|
+
for i in range(self.num_sites):
|
|
227
|
+
outstring = s + '0' + 'i'*(self.num_sites - i - 1)
|
|
228
|
+
p = self.probability(outstring)
|
|
229
|
+
if np.random.rand() < p/prev_probs[-1]:
|
|
230
|
+
s += '0'
|
|
231
|
+
normalizer = p
|
|
232
|
+
else:
|
|
233
|
+
s += '1'
|
|
234
|
+
normalizer = prev_probs[-1] - p
|
|
235
|
+
prev_probs.append(normalizer)
|
|
236
|
+
|
|
237
|
+
return s
|
|
238
|
+
|
|
239
|
+
class MatchgateSimS(BaseSim):
|
|
240
|
+
def __init__(self, circ, init_state=None):
|
|
241
|
+
super().__init__(circ, init_state=init_state, nm=None)
|
|
242
|
+
|
|
243
|
+
def initialize_state(self):
|
|
244
|
+
"""
|
|
245
|
+
internal state is a covariance matrix, which is a 2n x 2n matrix where n is the number of sites
|
|
246
|
+
init_state can be provided as a bitstring or a covariance matrix
|
|
247
|
+
"""
|
|
248
|
+
if self.init_state is None:
|
|
249
|
+
summands = [np.array([[0,-1], [1,0]]) for i in range(self.num_sites)]
|
|
250
|
+
self.state = block_diag(*summands)
|
|
251
|
+
|
|
252
|
+
elif type(self.init_state) == str:
|
|
253
|
+
mat = np.array([[0,-1], [1,0]])
|
|
254
|
+
# summand will have mat or -mat on the diagonal depending on the bitstring
|
|
255
|
+
summands = [-mat if b == '1' else mat for b in self.init_state]
|
|
256
|
+
self.state = block_diag(*summands)
|
|
257
|
+
|
|
258
|
+
elif type(self.init_state) == np.ndarray:
|
|
259
|
+
self.state = self.init_state
|
|
260
|
+
|
|
261
|
+
else:
|
|
262
|
+
raise ValueError("init_state must be either a covariance matrix or a bitstring.")
|
|
263
|
+
|
|
264
|
+
def apply_gate(self, gate):
|
|
265
|
+
gammas = [np.kron(pauli_X, pauli_I), np.kron(pauli_Y, pauli_I), np.kron(pauli_Z, pauli_X),
|
|
266
|
+
np.kron(pauli_Z, pauli_Y)]
|
|
267
|
+
rot_matrix = np.array([[0.5 * np.trace(gamma1 @ gate.matrix.conj().T @ gamma2 @ gate.matrix)
|
|
268
|
+
for gamma2 in gammas] for gamma1 in gammas])
|
|
269
|
+
summands = [np.eye(2) for i in range(self.num_sites)]
|
|
270
|
+
summands[gate.indices[0]] = rot_matrix
|
|
271
|
+
summands.remove(gate.indices[1])
|
|
272
|
+
full_rot_matrix = block_diag(*summands)
|
|
273
|
+
self.state = full_rot_matrix @ self.state @ full_rot_matrix.conj().T
|
|
@@ -0,0 +1,53 @@
|
|
|
1
|
+
from tqdm import tqdm
|
|
2
|
+
|
|
3
|
+
class BaseSim:
|
|
4
|
+
"""
|
|
5
|
+
accepts Circuit object and Noise object
|
|
6
|
+
different simulators use different representations of quantum state
|
|
7
|
+
and different methods for applying gates and measurements
|
|
8
|
+
"""
|
|
9
|
+
|
|
10
|
+
def __init__(self, circ, init_state=None, nm=None):
|
|
11
|
+
self.circ = circ
|
|
12
|
+
self.num_sites = circ.num_sites
|
|
13
|
+
self.local_dim = circ.local_dim
|
|
14
|
+
self.nm = nm
|
|
15
|
+
self.init_state = init_state
|
|
16
|
+
self.state = None
|
|
17
|
+
|
|
18
|
+
def initialize_state(self):
|
|
19
|
+
raise NotImplementedError("must be implemented by child classes.")
|
|
20
|
+
|
|
21
|
+
def apply_gate(self, gate):
|
|
22
|
+
raise NotImplementedError("must be implemented by child classes.")
|
|
23
|
+
|
|
24
|
+
def run(self, progress_bar=True, debug=0):
|
|
25
|
+
"""
|
|
26
|
+
progress_bar: If True, display a progress bar during simulation.
|
|
27
|
+
debug: If 1, prints intermediate state after each layer.
|
|
28
|
+
debug: If 2, prints intermediate state after each gate.
|
|
29
|
+
"""
|
|
30
|
+
self.initialize_state()
|
|
31
|
+
circ = self.circ.build(self.nm) if not self.circ.built else self.circ
|
|
32
|
+
layers = tqdm(circ.layers) if progress_bar else circ.layers
|
|
33
|
+
|
|
34
|
+
for layer in layers:
|
|
35
|
+
for gate in layer:
|
|
36
|
+
self.apply_gate(gate)
|
|
37
|
+
if debug == 2:
|
|
38
|
+
print(self)
|
|
39
|
+
|
|
40
|
+
if debug == 1:
|
|
41
|
+
print(self)
|
|
42
|
+
|
|
43
|
+
if circ.meas_sites is not None:
|
|
44
|
+
self.measure(circ.meas_sites)
|
|
45
|
+
|
|
46
|
+
return self.state
|
|
47
|
+
|
|
48
|
+
def measure(self, meas_sites):
|
|
49
|
+
raise NotImplementedError("must be implemented by child classes.")
|
|
50
|
+
|
|
51
|
+
def __str__(self):
|
|
52
|
+
""" pretty prints the state """
|
|
53
|
+
raise NotImplementedError("must be implemented by child classes.")
|
|
@@ -0,0 +1,147 @@
|
|
|
1
|
+
from .gates import ID, SuperOp
|
|
2
|
+
from .noise import gate_time, ThermalNoise
|
|
3
|
+
import numpy as np
|
|
4
|
+
import copy
|
|
5
|
+
from functools import reduce
|
|
6
|
+
|
|
7
|
+
class Circuit:
|
|
8
|
+
def __init__(self, gate_list, n=None, meas_sites=None):
|
|
9
|
+
self.gate_list = gate_list
|
|
10
|
+
sites = np.concatenate([gate.indices for gate in gate_list])
|
|
11
|
+
self.num_sites = int(max(sites) + 1) if n is None else n
|
|
12
|
+
self.local_dim = gate_list[0].get_local_dim()
|
|
13
|
+
self.layers = None
|
|
14
|
+
self.built = False
|
|
15
|
+
self.meas_sites = meas_sites
|
|
16
|
+
|
|
17
|
+
def update_gate(self, gate_idx, new_array):
|
|
18
|
+
self.gate_list[gate_idx].matrix = new_array
|
|
19
|
+
if self.layers is not None:
|
|
20
|
+
self.construct_layers()
|
|
21
|
+
|
|
22
|
+
def decompose(self, basis='ZSX'):
|
|
23
|
+
gate_list = []
|
|
24
|
+
for gate in self.gate_list:
|
|
25
|
+
if gate.name == "U" and gate.angles is not None:
|
|
26
|
+
decomposed_gates = gate.decompose(basis)
|
|
27
|
+
gate_list.extend(decomposed_gates)
|
|
28
|
+
else:
|
|
29
|
+
gate_list.append(gate)
|
|
30
|
+
|
|
31
|
+
self.gate_list = gate_list
|
|
32
|
+
|
|
33
|
+
def construct_layers(self):
|
|
34
|
+
layers = []
|
|
35
|
+
for gate in self.gate_list:
|
|
36
|
+
place_available = True
|
|
37
|
+
layer_ind = len(layers) - 1
|
|
38
|
+
|
|
39
|
+
# first we find the oldest layer that fails to commute with current gate
|
|
40
|
+
while place_available and layer_ind >= 0:
|
|
41
|
+
g_indices = set(gate.indices)
|
|
42
|
+
existing_indices = {ind for gate in layers[layer_ind] for ind in gate.indices}
|
|
43
|
+
if g_indices.intersection(existing_indices):
|
|
44
|
+
place_available = False
|
|
45
|
+
else:
|
|
46
|
+
layer_ind -= 1
|
|
47
|
+
|
|
48
|
+
# then we add current gate to the following layer
|
|
49
|
+
if layer_ind < len(layers) - 1:
|
|
50
|
+
layers[layer_ind + 1].append(gate)
|
|
51
|
+
else:
|
|
52
|
+
layers.append([gate])
|
|
53
|
+
|
|
54
|
+
self.layers = layers
|
|
55
|
+
|
|
56
|
+
def add_noise(self, nm):
|
|
57
|
+
layers = []
|
|
58
|
+
for layer in self.layers:
|
|
59
|
+
if not any(isinstance(obj, SuperOp) for obj in layer):
|
|
60
|
+
if isinstance(nm, ThermalNoise):
|
|
61
|
+
layer_time = max([gate_time(gate, nm) for gate in layer])
|
|
62
|
+
active_qubits = set(np.concatenate([gate.indices for gate in layer]))
|
|
63
|
+
idle_qubits = set(range(self.num_sites)) - active_qubits
|
|
64
|
+
noise_layer = [ID(nm.local_dim, layer_time, i, nm=nm, dd=(i in idle_qubits))
|
|
65
|
+
for i in range(self.num_sites)]
|
|
66
|
+
|
|
67
|
+
else:
|
|
68
|
+
noise_layer = [ID(2, 0.0, [i for i in range(self.num_sites)], nm=nm)]
|
|
69
|
+
|
|
70
|
+
layer.extend(noise_layer)
|
|
71
|
+
layers.append(layer)
|
|
72
|
+
else:
|
|
73
|
+
layers.append(layer)
|
|
74
|
+
|
|
75
|
+
self.layers = layers
|
|
76
|
+
|
|
77
|
+
def build(self, nm=None):
|
|
78
|
+
""" note that this returns a new circuit instance built with provided noise model
|
|
79
|
+
and leaves the current circuit instance unchanged """
|
|
80
|
+
new_circ = copy.deepcopy(self)
|
|
81
|
+
new_circ.decompose()
|
|
82
|
+
new_circ.construct_layers()
|
|
83
|
+
if nm is not None:
|
|
84
|
+
new_circ.add_noise(nm)
|
|
85
|
+
|
|
86
|
+
new_circ.built = True
|
|
87
|
+
return new_circ
|
|
88
|
+
|
|
89
|
+
def add_meas(self, meas_sites):
|
|
90
|
+
self.meas_sites = meas_sites
|
|
91
|
+
|
|
92
|
+
def dag(self):
|
|
93
|
+
gate_list = [g.dag() for g in self.gate_list[::-1]]
|
|
94
|
+
return Circuit(gate_list, self.num_sites)
|
|
95
|
+
|
|
96
|
+
def to_matrix(self):
|
|
97
|
+
return circ_to_mat(self)
|
|
98
|
+
|
|
99
|
+
def __str__(self):
|
|
100
|
+
if self.layers is not None:
|
|
101
|
+
full_str = []
|
|
102
|
+
for layer in self.layers:
|
|
103
|
+
layer_str = " \n".join([str(gate) for gate in layer])
|
|
104
|
+
full_str.append(layer_str)
|
|
105
|
+
full_str = " \n\n".join(full_str)
|
|
106
|
+
return full_str
|
|
107
|
+
|
|
108
|
+
else:
|
|
109
|
+
return " \n".join([str(gate) for gate in self.gate_list])
|
|
110
|
+
|
|
111
|
+
def draw(self): # TODO
|
|
112
|
+
""" use svgwrite to visualize the circuit """
|
|
113
|
+
return None
|
|
114
|
+
|
|
115
|
+
def compose_circuits(circ_list):
|
|
116
|
+
gate_list = [circ.gate_list for circ in circ_list]
|
|
117
|
+
gate_list = [gate for sublist in gate_list for gate in sublist]
|
|
118
|
+
return Circuit(gate_list, n=circ_list[0].num_sites)
|
|
119
|
+
|
|
120
|
+
def circ_to_mat(circ, n=None):
|
|
121
|
+
if circ.layers == None:
|
|
122
|
+
circ.construct_layers()
|
|
123
|
+
n = circ.num_sites if n is None else n
|
|
124
|
+
|
|
125
|
+
layer_mats = []
|
|
126
|
+
for layer in circ.layers:
|
|
127
|
+
missing_indices = set(range(n)) - set(np.concatenate([gate.indices for gate in layer]))
|
|
128
|
+
for index in missing_indices:
|
|
129
|
+
layer.append(ID(circ.local_dim, 0.0, [index]))
|
|
130
|
+
|
|
131
|
+
sorted_layer = sorted(layer, key=lambda gate: gate.indices[0])
|
|
132
|
+
mat_list = [gate.matrix for gate in sorted_layer]
|
|
133
|
+
layer_mats.append(reduce(np.kron, mat_list))
|
|
134
|
+
|
|
135
|
+
return np.linalg.multi_dot(layer_mats[::-1]) if len(layer_mats) > 1 else layer_mats[0]
|
|
136
|
+
|
|
137
|
+
def shift_gate_list(circ, start_site):
|
|
138
|
+
""" return a new gate list with all gate indices shifted by start_site."""
|
|
139
|
+
def _shifted_gate(gate, start_site):
|
|
140
|
+
new_gate = copy.deepcopy(gate)
|
|
141
|
+
new_gate.indices = np.array(gate.indices) + start_site
|
|
142
|
+
return new_gate
|
|
143
|
+
|
|
144
|
+
return [
|
|
145
|
+
_shifted_gate(gate, start_site)
|
|
146
|
+
for gate in circ.gate_list
|
|
147
|
+
]
|