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.
Files changed (31) hide show
  1. qaravan-0.1.0/LICENSE +20 -0
  2. qaravan-0.1.0/PKG-INFO +19 -0
  3. qaravan-0.1.0/README.md +11 -0
  4. qaravan-0.1.0/pyproject.toml +3 -0
  5. qaravan-0.1.0/setup.cfg +4 -0
  6. qaravan-0.1.0/setup.py +19 -0
  7. qaravan-0.1.0/src/qaravan/__init__.py +1 -0
  8. qaravan-0.1.0/src/qaravan/algebraQ/__init__.py +1 -0
  9. qaravan-0.1.0/src/qaravan/algebraQ/matchgate_sim.py +273 -0
  10. qaravan-0.1.0/src/qaravan/core/__init__.py +9 -0
  11. qaravan-0.1.0/src/qaravan/core/base_sim.py +53 -0
  12. qaravan-0.1.0/src/qaravan/core/circuits.py +147 -0
  13. qaravan-0.1.0/src/qaravan/core/compilers.py +247 -0
  14. qaravan-0.1.0/src/qaravan/core/gates.py +406 -0
  15. qaravan-0.1.0/src/qaravan/core/hamiltonians.py +306 -0
  16. qaravan-0.1.0/src/qaravan/core/lattices.py +327 -0
  17. qaravan-0.1.0/src/qaravan/core/noise.py +172 -0
  18. qaravan-0.1.0/src/qaravan/core/paulis.py +57 -0
  19. qaravan-0.1.0/src/qaravan/core/utils.py +121 -0
  20. qaravan-0.1.0/src/qaravan/tensorQ/__init__.py +6 -0
  21. qaravan-0.1.0/src/qaravan/tensorQ/density_matrix_sim.py +129 -0
  22. qaravan-0.1.0/src/qaravan/tensorQ/initializations.py +142 -0
  23. qaravan-0.1.0/src/qaravan/tensorQ/mpdo_sim.py +168 -0
  24. qaravan-0.1.0/src/qaravan/tensorQ/mps_sim.py +46 -0
  25. qaravan-0.1.0/src/qaravan/tensorQ/statevector_sim.py +102 -0
  26. qaravan-0.1.0/src/qaravan/tensorQ/tn.py +500 -0
  27. qaravan-0.1.0/src/qaravan.egg-info/PKG-INFO +19 -0
  28. qaravan-0.1.0/src/qaravan.egg-info/SOURCES.txt +29 -0
  29. qaravan-0.1.0/src/qaravan.egg-info/dependency_links.txt +1 -0
  30. qaravan-0.1.0/src/qaravan.egg-info/requires.txt +5 -0
  31. 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
@@ -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)
@@ -0,0 +1,3 @@
1
+ [build-system]
2
+ requires = ["setuptools>=61.0", "wheel"]
3
+ build-backend = "setuptools.build_meta"
@@ -0,0 +1,4 @@
1
+ [egg_info]
2
+ tag_build =
3
+ tag_date = 0
4
+
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,9 @@
1
+ from .noise import *
2
+ from .gates import *
3
+ from .paulis import *
4
+ from .utils import *
5
+ from .base_sim import *
6
+ from .lattices import *
7
+ from .hamiltonians import *
8
+ from .circuits import *
9
+ from .compilers import *
@@ -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
+ ]