Trajectree 0.0.0__py3-none-any.whl → 0.0.1__py3-none-any.whl
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.
- trajectree/__init__.py +3 -0
- trajectree/fock_optics/devices.py +1 -1
- trajectree/fock_optics/light_sources.py +2 -2
- trajectree/fock_optics/measurement.py +3 -3
- trajectree/fock_optics/utils.py +6 -6
- trajectree/quimb/docs/_pygments/_pygments_dark.py +118 -0
- trajectree/quimb/docs/_pygments/_pygments_light.py +118 -0
- trajectree/quimb/docs/conf.py +158 -0
- trajectree/quimb/docs/examples/ex_mpi_expm_evo.py +62 -0
- trajectree/quimb/quimb/__init__.py +507 -0
- trajectree/quimb/quimb/calc.py +1491 -0
- trajectree/quimb/quimb/core.py +2279 -0
- trajectree/quimb/quimb/evo.py +712 -0
- trajectree/quimb/quimb/experimental/__init__.py +0 -0
- trajectree/quimb/quimb/experimental/autojittn.py +129 -0
- trajectree/quimb/quimb/experimental/belief_propagation/__init__.py +109 -0
- trajectree/quimb/quimb/experimental/belief_propagation/bp_common.py +397 -0
- trajectree/quimb/quimb/experimental/belief_propagation/d1bp.py +316 -0
- trajectree/quimb/quimb/experimental/belief_propagation/d2bp.py +653 -0
- trajectree/quimb/quimb/experimental/belief_propagation/hd1bp.py +571 -0
- trajectree/quimb/quimb/experimental/belief_propagation/hv1bp.py +775 -0
- trajectree/quimb/quimb/experimental/belief_propagation/l1bp.py +316 -0
- trajectree/quimb/quimb/experimental/belief_propagation/l2bp.py +537 -0
- trajectree/quimb/quimb/experimental/belief_propagation/regions.py +194 -0
- trajectree/quimb/quimb/experimental/cluster_update.py +286 -0
- trajectree/quimb/quimb/experimental/merabuilder.py +865 -0
- trajectree/quimb/quimb/experimental/operatorbuilder/__init__.py +15 -0
- trajectree/quimb/quimb/experimental/operatorbuilder/operatorbuilder.py +1631 -0
- trajectree/quimb/quimb/experimental/schematic.py +7 -0
- trajectree/quimb/quimb/experimental/tn_marginals.py +130 -0
- trajectree/quimb/quimb/experimental/tnvmc.py +1483 -0
- trajectree/quimb/quimb/gates.py +36 -0
- trajectree/quimb/quimb/gen/__init__.py +2 -0
- trajectree/quimb/quimb/gen/operators.py +1167 -0
- trajectree/quimb/quimb/gen/rand.py +713 -0
- trajectree/quimb/quimb/gen/states.py +479 -0
- trajectree/quimb/quimb/linalg/__init__.py +6 -0
- trajectree/quimb/quimb/linalg/approx_spectral.py +1109 -0
- trajectree/quimb/quimb/linalg/autoblock.py +258 -0
- trajectree/quimb/quimb/linalg/base_linalg.py +719 -0
- trajectree/quimb/quimb/linalg/mpi_launcher.py +397 -0
- trajectree/quimb/quimb/linalg/numpy_linalg.py +244 -0
- trajectree/quimb/quimb/linalg/rand_linalg.py +514 -0
- trajectree/quimb/quimb/linalg/scipy_linalg.py +293 -0
- trajectree/quimb/quimb/linalg/slepc_linalg.py +892 -0
- trajectree/quimb/quimb/schematic.py +1518 -0
- trajectree/quimb/quimb/tensor/__init__.py +401 -0
- trajectree/quimb/quimb/tensor/array_ops.py +610 -0
- trajectree/quimb/quimb/tensor/circuit.py +4824 -0
- trajectree/quimb/quimb/tensor/circuit_gen.py +411 -0
- trajectree/quimb/quimb/tensor/contraction.py +336 -0
- trajectree/quimb/quimb/tensor/decomp.py +1255 -0
- trajectree/quimb/quimb/tensor/drawing.py +1646 -0
- trajectree/quimb/quimb/tensor/fitting.py +385 -0
- trajectree/quimb/quimb/tensor/geometry.py +583 -0
- trajectree/quimb/quimb/tensor/interface.py +114 -0
- trajectree/quimb/quimb/tensor/networking.py +1058 -0
- trajectree/quimb/quimb/tensor/optimize.py +1818 -0
- trajectree/quimb/quimb/tensor/tensor_1d.py +4778 -0
- trajectree/quimb/quimb/tensor/tensor_1d_compress.py +1854 -0
- trajectree/quimb/quimb/tensor/tensor_1d_tebd.py +662 -0
- trajectree/quimb/quimb/tensor/tensor_2d.py +5954 -0
- trajectree/quimb/quimb/tensor/tensor_2d_compress.py +96 -0
- trajectree/quimb/quimb/tensor/tensor_2d_tebd.py +1230 -0
- trajectree/quimb/quimb/tensor/tensor_3d.py +2869 -0
- trajectree/quimb/quimb/tensor/tensor_3d_tebd.py +46 -0
- trajectree/quimb/quimb/tensor/tensor_approx_spectral.py +60 -0
- trajectree/quimb/quimb/tensor/tensor_arbgeom.py +3237 -0
- trajectree/quimb/quimb/tensor/tensor_arbgeom_compress.py +565 -0
- trajectree/quimb/quimb/tensor/tensor_arbgeom_tebd.py +1138 -0
- trajectree/quimb/quimb/tensor/tensor_builder.py +5411 -0
- trajectree/quimb/quimb/tensor/tensor_core.py +11179 -0
- trajectree/quimb/quimb/tensor/tensor_dmrg.py +1472 -0
- trajectree/quimb/quimb/tensor/tensor_mera.py +204 -0
- trajectree/quimb/quimb/utils.py +892 -0
- trajectree/quimb/tests/__init__.py +0 -0
- trajectree/quimb/tests/test_accel.py +501 -0
- trajectree/quimb/tests/test_calc.py +788 -0
- trajectree/quimb/tests/test_core.py +847 -0
- trajectree/quimb/tests/test_evo.py +565 -0
- trajectree/quimb/tests/test_gen/__init__.py +0 -0
- trajectree/quimb/tests/test_gen/test_operators.py +361 -0
- trajectree/quimb/tests/test_gen/test_rand.py +296 -0
- trajectree/quimb/tests/test_gen/test_states.py +261 -0
- trajectree/quimb/tests/test_linalg/__init__.py +0 -0
- trajectree/quimb/tests/test_linalg/test_approx_spectral.py +368 -0
- trajectree/quimb/tests/test_linalg/test_base_linalg.py +351 -0
- trajectree/quimb/tests/test_linalg/test_mpi_linalg.py +127 -0
- trajectree/quimb/tests/test_linalg/test_numpy_linalg.py +84 -0
- trajectree/quimb/tests/test_linalg/test_rand_linalg.py +134 -0
- trajectree/quimb/tests/test_linalg/test_slepc_linalg.py +283 -0
- trajectree/quimb/tests/test_tensor/__init__.py +0 -0
- trajectree/quimb/tests/test_tensor/test_belief_propagation/__init__.py +0 -0
- trajectree/quimb/tests/test_tensor/test_belief_propagation/test_d1bp.py +39 -0
- trajectree/quimb/tests/test_tensor/test_belief_propagation/test_d2bp.py +67 -0
- trajectree/quimb/tests/test_tensor/test_belief_propagation/test_hd1bp.py +64 -0
- trajectree/quimb/tests/test_tensor/test_belief_propagation/test_hv1bp.py +51 -0
- trajectree/quimb/tests/test_tensor/test_belief_propagation/test_l1bp.py +142 -0
- trajectree/quimb/tests/test_tensor/test_belief_propagation/test_l2bp.py +101 -0
- trajectree/quimb/tests/test_tensor/test_circuit.py +816 -0
- trajectree/quimb/tests/test_tensor/test_contract.py +67 -0
- trajectree/quimb/tests/test_tensor/test_decomp.py +40 -0
- trajectree/quimb/tests/test_tensor/test_mera.py +52 -0
- trajectree/quimb/tests/test_tensor/test_optimizers.py +488 -0
- trajectree/quimb/tests/test_tensor/test_tensor_1d.py +1171 -0
- trajectree/quimb/tests/test_tensor/test_tensor_2d.py +606 -0
- trajectree/quimb/tests/test_tensor/test_tensor_2d_tebd.py +144 -0
- trajectree/quimb/tests/test_tensor/test_tensor_3d.py +123 -0
- trajectree/quimb/tests/test_tensor/test_tensor_arbgeom.py +226 -0
- trajectree/quimb/tests/test_tensor/test_tensor_builder.py +441 -0
- trajectree/quimb/tests/test_tensor/test_tensor_core.py +2066 -0
- trajectree/quimb/tests/test_tensor/test_tensor_dmrg.py +388 -0
- trajectree/quimb/tests/test_tensor/test_tensor_spectral_approx.py +63 -0
- trajectree/quimb/tests/test_tensor/test_tensor_tebd.py +270 -0
- trajectree/quimb/tests/test_utils.py +85 -0
- trajectree/trajectory.py +2 -2
- {trajectree-0.0.0.dist-info → trajectree-0.0.1.dist-info}/METADATA +2 -2
- trajectree-0.0.1.dist-info/RECORD +126 -0
- trajectree-0.0.0.dist-info/RECORD +0 -16
- {trajectree-0.0.0.dist-info → trajectree-0.0.1.dist-info}/WHEEL +0 -0
- {trajectree-0.0.0.dist-info → trajectree-0.0.1.dist-info}/licenses/LICENSE +0 -0
- {trajectree-0.0.0.dist-info → trajectree-0.0.1.dist-info}/top_level.txt +0 -0
|
@@ -0,0 +1,4824 @@
|
|
|
1
|
+
"""Tools for quantum circuit simulation using tensor networks.
|
|
2
|
+
|
|
3
|
+
TODO:
|
|
4
|
+
- [ ] gate-by-gate sampling
|
|
5
|
+
- [ ] sub-MPO apply for MPS simulation
|
|
6
|
+
- [ ] multi qubit gates via MPO for MPS simulation
|
|
7
|
+
"""
|
|
8
|
+
|
|
9
|
+
import functools
|
|
10
|
+
import itertools
|
|
11
|
+
import math
|
|
12
|
+
import numbers
|
|
13
|
+
import operator
|
|
14
|
+
import re
|
|
15
|
+
import warnings
|
|
16
|
+
|
|
17
|
+
import numpy as np
|
|
18
|
+
from autoray import backend_like, do, reshape
|
|
19
|
+
|
|
20
|
+
import quimb as qu
|
|
21
|
+
|
|
22
|
+
from ..utils import (
|
|
23
|
+
LRU,
|
|
24
|
+
concatv,
|
|
25
|
+
deprecated,
|
|
26
|
+
ensure_dict,
|
|
27
|
+
partition_all,
|
|
28
|
+
partitionby,
|
|
29
|
+
tree_map,
|
|
30
|
+
)
|
|
31
|
+
from ..utils import progbar as _progbar
|
|
32
|
+
from . import array_ops as ops
|
|
33
|
+
from .tensor_1d import Dense1D, MatrixProductOperator
|
|
34
|
+
from .tensor_arbgeom import TensorNetworkGenOperator, TensorNetworkGenVector
|
|
35
|
+
from .tensor_builder import (
|
|
36
|
+
HTN_CP_operator_from_products,
|
|
37
|
+
MPO_identity_like,
|
|
38
|
+
MPS_computational_state,
|
|
39
|
+
TN_from_sites_computational_state,
|
|
40
|
+
)
|
|
41
|
+
from .tensor_core import (
|
|
42
|
+
PTensor,
|
|
43
|
+
Tensor,
|
|
44
|
+
get_tags,
|
|
45
|
+
oset_union,
|
|
46
|
+
rand_uuid,
|
|
47
|
+
tags_to_oset,
|
|
48
|
+
tensor_contract,
|
|
49
|
+
)
|
|
50
|
+
|
|
51
|
+
|
|
52
|
+
def recursive_stack(x):
|
|
53
|
+
if not isinstance(x, (list, tuple)):
|
|
54
|
+
return x
|
|
55
|
+
return do("stack", tuple(map(recursive_stack, x)))
|
|
56
|
+
|
|
57
|
+
|
|
58
|
+
def _convert_ints_and_floats(x):
|
|
59
|
+
if isinstance(x, str):
|
|
60
|
+
try:
|
|
61
|
+
return int(x)
|
|
62
|
+
except ValueError:
|
|
63
|
+
pass
|
|
64
|
+
|
|
65
|
+
try:
|
|
66
|
+
return float(x)
|
|
67
|
+
except ValueError:
|
|
68
|
+
pass
|
|
69
|
+
|
|
70
|
+
return x
|
|
71
|
+
|
|
72
|
+
|
|
73
|
+
def _put_registers_last(x):
|
|
74
|
+
# no need to do anything unless parameter (i.e. float) is found last
|
|
75
|
+
if not isinstance(x[-1], float):
|
|
76
|
+
return x
|
|
77
|
+
|
|
78
|
+
# swap this last group of floats with the penultimate group of integers
|
|
79
|
+
parts = tuple(partitionby(type, x))
|
|
80
|
+
return tuple(concatv(*parts[:-2], parts[-1], parts[-2]))
|
|
81
|
+
|
|
82
|
+
|
|
83
|
+
def parse_qsim_str(contents):
|
|
84
|
+
"""Parse a 'qsim' input format string into circuit information.
|
|
85
|
+
|
|
86
|
+
The format is described here: https://quantumai.google/qsim/input_format.
|
|
87
|
+
|
|
88
|
+
Parameters
|
|
89
|
+
----------
|
|
90
|
+
contents : str
|
|
91
|
+
The full string of the qsim file.
|
|
92
|
+
|
|
93
|
+
Returns
|
|
94
|
+
-------
|
|
95
|
+
circuit_info : dict
|
|
96
|
+
Information about the circuit:
|
|
97
|
+
|
|
98
|
+
- circuit_info['n']: the number of qubits
|
|
99
|
+
- circuit_info['n_gates']: the number of gates in total
|
|
100
|
+
- circuit_info['gates']: list[list[str]], list of gates, each of which
|
|
101
|
+
is a list of strings read from a line of the qsim file.
|
|
102
|
+
"""
|
|
103
|
+
|
|
104
|
+
lines = contents.split("\n")
|
|
105
|
+
n = int(lines[0])
|
|
106
|
+
|
|
107
|
+
# turn into tuples of python types
|
|
108
|
+
gates = [
|
|
109
|
+
tuple(map(_convert_ints_and_floats, line.strip().split(" ")))
|
|
110
|
+
for line in lines[1:]
|
|
111
|
+
if line
|
|
112
|
+
]
|
|
113
|
+
|
|
114
|
+
# put registers/parameters in standard order and detect if gate round used
|
|
115
|
+
gates = tuple(map(_put_registers_last, gates))
|
|
116
|
+
round_specified = isinstance(gates[0][0], numbers.Integral)
|
|
117
|
+
|
|
118
|
+
return {
|
|
119
|
+
"n": n,
|
|
120
|
+
"gates": gates,
|
|
121
|
+
"n_gates": len(gates),
|
|
122
|
+
"round_specified": round_specified,
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
|
|
126
|
+
def parse_qsim_file(fname, **kwargs):
|
|
127
|
+
"""Parse a qsim file."""
|
|
128
|
+
with open(fname) as f:
|
|
129
|
+
return parse_qsim_str(f.read(), **kwargs)
|
|
130
|
+
|
|
131
|
+
|
|
132
|
+
def parse_qsim_url(url, **kwargs):
|
|
133
|
+
"""Parse a qsim url."""
|
|
134
|
+
from urllib import request
|
|
135
|
+
|
|
136
|
+
return parse_qsim_str(request.urlopen(url).read().decode(), **kwargs)
|
|
137
|
+
|
|
138
|
+
|
|
139
|
+
def to_clean_list(s, delimiter):
|
|
140
|
+
"""Split, strip and filter a string by a given character into a list."""
|
|
141
|
+
if s is None:
|
|
142
|
+
return []
|
|
143
|
+
return list(filter(None, (w.strip() for w in s.split(delimiter))))
|
|
144
|
+
|
|
145
|
+
|
|
146
|
+
def multi_replace(s, replacements):
|
|
147
|
+
"""Replace multiple substrings in a string."""
|
|
148
|
+
for w, r in replacements.items():
|
|
149
|
+
s = s.replace(w, r)
|
|
150
|
+
return s
|
|
151
|
+
|
|
152
|
+
|
|
153
|
+
@functools.lru_cache(None)
|
|
154
|
+
def get_openqasm2_regexes():
|
|
155
|
+
return {
|
|
156
|
+
"header": re.compile(r"(OPENQASM\s+2.0;)|(include\s+\"qelib1.inc\";)"),
|
|
157
|
+
"comment": re.compile(r"^//"),
|
|
158
|
+
"comment_start": re.compile(r"/\*"),
|
|
159
|
+
"comment_end": re.compile(r"\*/"),
|
|
160
|
+
"qreg": re.compile(r"qreg\s+(\w+)\s*\[(\d+)\];"),
|
|
161
|
+
"gate": re.compile(r"(\w+)\s*(\((.+)\))?\s*(.*);"),
|
|
162
|
+
"error": re.compile(r"^(if|for)"),
|
|
163
|
+
"ignore": re.compile(r"^(creg|measure|barrier)"),
|
|
164
|
+
"gate_def": re.compile(r"^gate"),
|
|
165
|
+
"gate_sig": re.compile(r"^gate\s+(\w+)\s*(\((.+)\))?\s*(.*)"),
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
|
|
169
|
+
def parse_openqasm2_str(contents):
|
|
170
|
+
"""Parse the string contents of an OpenQASM 2.0 file. This parser does not
|
|
171
|
+
support classical control flow is not guaranteed to check the full openqasm
|
|
172
|
+
grammar.
|
|
173
|
+
"""
|
|
174
|
+
# define regular expressions for parsing
|
|
175
|
+
rgxs = get_openqasm2_regexes()
|
|
176
|
+
|
|
177
|
+
# initialise number of qubits to zero and an empty list for gates
|
|
178
|
+
sitemap = {}
|
|
179
|
+
gates = []
|
|
180
|
+
custom_gates = {}
|
|
181
|
+
# only want to warn once about each ignored instruction
|
|
182
|
+
warned = {}
|
|
183
|
+
|
|
184
|
+
# Process each line
|
|
185
|
+
in_comment = False
|
|
186
|
+
lines = contents.split("\n")
|
|
187
|
+
while lines:
|
|
188
|
+
line = lines.pop(0).strip()
|
|
189
|
+
if not line:
|
|
190
|
+
# blank line
|
|
191
|
+
continue
|
|
192
|
+
if rgxs["comment"].match(line):
|
|
193
|
+
# single comment
|
|
194
|
+
continue
|
|
195
|
+
if rgxs["comment_start"].match(line):
|
|
196
|
+
# start of multiline comments
|
|
197
|
+
in_comment = True
|
|
198
|
+
if in_comment:
|
|
199
|
+
# in multiline comment, check if its ending
|
|
200
|
+
in_comment = not bool(rgxs["comment_end"].match(line))
|
|
201
|
+
continue
|
|
202
|
+
if rgxs["header"].match(line):
|
|
203
|
+
# ignore standard header lines
|
|
204
|
+
continue
|
|
205
|
+
|
|
206
|
+
match = rgxs["qreg"].match(line)
|
|
207
|
+
if match:
|
|
208
|
+
# quantum register -> extend sites
|
|
209
|
+
name, nq = match.groups()
|
|
210
|
+
for i in range(int(nq)):
|
|
211
|
+
sitemap[f"{name}[{i}]"] = len(sitemap)
|
|
212
|
+
continue
|
|
213
|
+
|
|
214
|
+
match = rgxs["ignore"].match(line)
|
|
215
|
+
if match:
|
|
216
|
+
# certain operations we can just ignore and warn about
|
|
217
|
+
(op,) = match.groups()
|
|
218
|
+
if not warned.get(op, False):
|
|
219
|
+
warnings.warn(
|
|
220
|
+
f"Unsupported operation ignored: {op}", SyntaxWarning
|
|
221
|
+
)
|
|
222
|
+
warned[op] = True
|
|
223
|
+
continue
|
|
224
|
+
|
|
225
|
+
if rgxs["error"].match(line):
|
|
226
|
+
# raise hard error for custom tate defns etc
|
|
227
|
+
raise NotImplementedError(
|
|
228
|
+
f"The following instruction is not supported: {line}"
|
|
229
|
+
)
|
|
230
|
+
|
|
231
|
+
if rgxs["gate_def"].match(line):
|
|
232
|
+
# custom gate definition:
|
|
233
|
+
# first gather all lines involved in the gate definition
|
|
234
|
+
gate_lines = [line]
|
|
235
|
+
while True:
|
|
236
|
+
if "}" in line:
|
|
237
|
+
# finished -> break
|
|
238
|
+
break
|
|
239
|
+
else:
|
|
240
|
+
# not finished -> need next line
|
|
241
|
+
line = lines.pop(0)
|
|
242
|
+
gate_lines.append(line)
|
|
243
|
+
|
|
244
|
+
# then combine this full gate definition, without newlines
|
|
245
|
+
gate_body = "".join(gate_lines)
|
|
246
|
+
# separate the signature and body
|
|
247
|
+
gate_sig, gate_body = re.match(
|
|
248
|
+
r"(.*)\s*{(.*)}", gate_body
|
|
249
|
+
).groups()
|
|
250
|
+
|
|
251
|
+
# parse the signature
|
|
252
|
+
match = rgxs["gate_sig"].match(gate_sig)
|
|
253
|
+
label = match[1]
|
|
254
|
+
sig_params = to_clean_list(match[3], ",")
|
|
255
|
+
sig_qubits = to_clean_list(match[4], ",")
|
|
256
|
+
|
|
257
|
+
# break body only back into individual lines, include semicolons
|
|
258
|
+
gate_body = to_clean_list(gate_body, ";")
|
|
259
|
+
# insert formatters, (using simple `replace` on the whole line will
|
|
260
|
+
# scramble the label if parameters or qubits are letters etc)
|
|
261
|
+
for i, gate_line in enumerate(gate_body):
|
|
262
|
+
gm = rgxs["gate"].match(gate_line + ";")
|
|
263
|
+
glabel = gm[1]
|
|
264
|
+
gqubits = multi_replace(
|
|
265
|
+
gm[4], {q: f"{{{q}}}" for q in sig_qubits}
|
|
266
|
+
)
|
|
267
|
+
if gm[3]:
|
|
268
|
+
# sub gate line is parametrized gate
|
|
269
|
+
gparams = multi_replace(
|
|
270
|
+
gm[3], {p: f"{{{p}}}" for p in sig_params}
|
|
271
|
+
)
|
|
272
|
+
gate_body[i] = f"{glabel}({gparams}) {gqubits};"
|
|
273
|
+
else:
|
|
274
|
+
# sub gate line is standard gate
|
|
275
|
+
gate_body[i] = f"{glabel} {gqubits};"
|
|
276
|
+
|
|
277
|
+
custom_gates[label] = sig_params, sig_qubits, gate_body
|
|
278
|
+
continue
|
|
279
|
+
|
|
280
|
+
match = rgxs["gate"].search(line)
|
|
281
|
+
if match:
|
|
282
|
+
# apply a gate
|
|
283
|
+
label, params, qubits = (
|
|
284
|
+
match.group(1),
|
|
285
|
+
match.group(3),
|
|
286
|
+
match.group(4),
|
|
287
|
+
)
|
|
288
|
+
|
|
289
|
+
if label in custom_gates:
|
|
290
|
+
# custom gate -> resolve parameters and qubits and prepend
|
|
291
|
+
# the constituent gate lines to the main list
|
|
292
|
+
sig_params, sig_qubits, gate_body = custom_gates[label]
|
|
293
|
+
replacer = {
|
|
294
|
+
**dict(zip(sig_params, to_clean_list(params, ","))),
|
|
295
|
+
**dict(zip(sig_qubits, to_clean_list(qubits, ","))),
|
|
296
|
+
}
|
|
297
|
+
|
|
298
|
+
# recurse by prepending the translated gate body
|
|
299
|
+
for gl in reversed(gate_body):
|
|
300
|
+
lines.insert(0, gl.format(**replacer))
|
|
301
|
+
|
|
302
|
+
continue
|
|
303
|
+
|
|
304
|
+
# standard gate -> add to list directly
|
|
305
|
+
if params:
|
|
306
|
+
params = tuple(
|
|
307
|
+
eval(param, {"pi": math.pi}) for param in params.split(",")
|
|
308
|
+
)
|
|
309
|
+
else:
|
|
310
|
+
params = ()
|
|
311
|
+
|
|
312
|
+
qubits = tuple(
|
|
313
|
+
sitemap[qubit.strip()] for qubit in qubits.split(",")
|
|
314
|
+
)
|
|
315
|
+
gates.append(Gate(label, params, qubits))
|
|
316
|
+
continue
|
|
317
|
+
|
|
318
|
+
# if not covered by previous checks, simply raise
|
|
319
|
+
raise SyntaxError(f"{line}")
|
|
320
|
+
|
|
321
|
+
return {
|
|
322
|
+
"n": len(sitemap),
|
|
323
|
+
"sitemap": sitemap,
|
|
324
|
+
"gates": gates,
|
|
325
|
+
"n_gates": len(gates),
|
|
326
|
+
}
|
|
327
|
+
|
|
328
|
+
|
|
329
|
+
def parse_openqasm2_file(fname, **kwargs):
|
|
330
|
+
"""Parse an OpenQASM 2.0 file."""
|
|
331
|
+
with open(fname) as f:
|
|
332
|
+
return parse_openqasm2_str(f.read(), **kwargs)
|
|
333
|
+
|
|
334
|
+
|
|
335
|
+
def parse_openqasm2_url(url, **kwargs):
|
|
336
|
+
"""Parse an OpenQASM 2.0 url."""
|
|
337
|
+
from urllib import request
|
|
338
|
+
|
|
339
|
+
return parse_openqasm2_str(request.urlopen(url).read().decode(), **kwargs)
|
|
340
|
+
|
|
341
|
+
|
|
342
|
+
# -------------------------- core gate functions ---------------------------- #
|
|
343
|
+
|
|
344
|
+
|
|
345
|
+
ALL_GATES = set()
|
|
346
|
+
ONE_QUBIT_GATES = set()
|
|
347
|
+
TWO_QUBIT_GATES = set()
|
|
348
|
+
ALL_PARAM_GATES = set()
|
|
349
|
+
ONE_QUBIT_PARAM_GATES = set()
|
|
350
|
+
TWO_QUBIT_PARAM_GATES = set()
|
|
351
|
+
|
|
352
|
+
# the tensor tags to use for each gate (defaults to label)
|
|
353
|
+
GATE_TAGS = {}
|
|
354
|
+
|
|
355
|
+
# the number of qubits a gate acts on
|
|
356
|
+
GATE_SIZE = {}
|
|
357
|
+
|
|
358
|
+
# gates which just require a constant array
|
|
359
|
+
CONSTANT_GATES = {}
|
|
360
|
+
|
|
361
|
+
# gates which are parametrized
|
|
362
|
+
PARAM_GATES = {}
|
|
363
|
+
|
|
364
|
+
# gates which involve a non-array operation such as reindexing only
|
|
365
|
+
SPECIAL_GATES = {}
|
|
366
|
+
|
|
367
|
+
|
|
368
|
+
def register_constant_gate(name, G, num_qubits, tag=None):
|
|
369
|
+
if tag is None:
|
|
370
|
+
tag = name
|
|
371
|
+
GATE_TAGS[name] = tag
|
|
372
|
+
CONSTANT_GATES[name] = G
|
|
373
|
+
GATE_SIZE[name] = num_qubits
|
|
374
|
+
if num_qubits == 1:
|
|
375
|
+
ONE_QUBIT_GATES.add(name)
|
|
376
|
+
elif num_qubits == 2:
|
|
377
|
+
TWO_QUBIT_GATES.add(name)
|
|
378
|
+
ALL_GATES.add(name)
|
|
379
|
+
|
|
380
|
+
|
|
381
|
+
def register_param_gate(name, param_fn, num_qubits, tag=None):
|
|
382
|
+
if tag is None:
|
|
383
|
+
tag = name
|
|
384
|
+
GATE_TAGS[name] = tag
|
|
385
|
+
PARAM_GATES[name] = param_fn
|
|
386
|
+
GATE_SIZE[name] = num_qubits
|
|
387
|
+
if num_qubits == 1:
|
|
388
|
+
ONE_QUBIT_GATES.add(name)
|
|
389
|
+
ONE_QUBIT_PARAM_GATES.add(name)
|
|
390
|
+
elif num_qubits == 2:
|
|
391
|
+
TWO_QUBIT_GATES.add(name)
|
|
392
|
+
TWO_QUBIT_PARAM_GATES.add(name)
|
|
393
|
+
ALL_GATES.add(name)
|
|
394
|
+
ALL_PARAM_GATES.add(name)
|
|
395
|
+
|
|
396
|
+
|
|
397
|
+
def register_special_gate(name, fn, num_qubits, tag=None, array=None):
|
|
398
|
+
if tag is None:
|
|
399
|
+
tag = name
|
|
400
|
+
GATE_TAGS[name] = tag
|
|
401
|
+
GATE_SIZE[name] = num_qubits
|
|
402
|
+
if num_qubits == 1:
|
|
403
|
+
ONE_QUBIT_GATES.add(name)
|
|
404
|
+
elif num_qubits == 2:
|
|
405
|
+
TWO_QUBIT_GATES.add(name)
|
|
406
|
+
SPECIAL_GATES[name] = fn
|
|
407
|
+
ALL_GATES.add(name)
|
|
408
|
+
if array is not None:
|
|
409
|
+
CONSTANT_GATES[name] = array
|
|
410
|
+
|
|
411
|
+
|
|
412
|
+
# constant single qubit gates
|
|
413
|
+
register_constant_gate("H", qu.hadamard(), 1)
|
|
414
|
+
register_constant_gate("X", qu.pauli("X"), 1)
|
|
415
|
+
register_constant_gate("Y", qu.pauli("Y"), 1)
|
|
416
|
+
register_constant_gate("Z", qu.pauli("Z"), 1)
|
|
417
|
+
register_constant_gate("S", qu.S_gate(), 1)
|
|
418
|
+
register_constant_gate("SDG", qu.S_gate().H, 1)
|
|
419
|
+
register_constant_gate("T", qu.T_gate(), 1)
|
|
420
|
+
register_constant_gate("TDG", qu.T_gate().H, 1)
|
|
421
|
+
register_constant_gate("X_1_2", qu.Xsqrt(), 1, "X_1/2")
|
|
422
|
+
register_constant_gate("Y_1_2", qu.Ysqrt(), 1, "Y_1/2")
|
|
423
|
+
register_constant_gate("Z_1_2", qu.Zsqrt(), 1, "Z_1/2")
|
|
424
|
+
register_constant_gate("W_1_2", qu.Wsqrt(), 1, "W_1/2")
|
|
425
|
+
register_constant_gate("HZ_1_2", qu.Wsqrt(), 1, "W_1/2")
|
|
426
|
+
|
|
427
|
+
|
|
428
|
+
# constant two qubit gates
|
|
429
|
+
register_constant_gate("CX", qu.cX(), 2)
|
|
430
|
+
register_constant_gate("CNOT", qu.CNOT(), 2, "CX")
|
|
431
|
+
register_constant_gate("CY", qu.cY(), 2)
|
|
432
|
+
register_constant_gate("CZ", qu.cZ(), 2)
|
|
433
|
+
register_constant_gate("ISWAP", qu.iswap(), 2)
|
|
434
|
+
register_constant_gate("IS", qu.iswap(), 2, "ISWAP")
|
|
435
|
+
|
|
436
|
+
|
|
437
|
+
# constant three qubit gates
|
|
438
|
+
register_constant_gate("CCX", qu.ccX(), 3)
|
|
439
|
+
register_constant_gate("CCNOT", qu.ccX(), 3, "CCX")
|
|
440
|
+
register_constant_gate("TOFFOLI", qu.ccX(), 3, "CCX")
|
|
441
|
+
register_constant_gate("CCY", qu.ccY(), 3)
|
|
442
|
+
register_constant_gate("CCZ", qu.ccZ(), 3)
|
|
443
|
+
register_constant_gate("CSWAP", qu.cswap(), 3)
|
|
444
|
+
register_constant_gate("FREDKIN", qu.cswap(), 3, "CSWAP")
|
|
445
|
+
|
|
446
|
+
|
|
447
|
+
# single parametrizable gates
|
|
448
|
+
|
|
449
|
+
|
|
450
|
+
def rx_gate_param_gen(params):
|
|
451
|
+
phi = params[0]
|
|
452
|
+
|
|
453
|
+
with backend_like(phi):
|
|
454
|
+
# get a real backend zero
|
|
455
|
+
zero = phi * 0.0
|
|
456
|
+
|
|
457
|
+
c = do("complex", do("cos", phi / 2), zero)
|
|
458
|
+
s = do("complex", zero, -do("sin", phi / 2))
|
|
459
|
+
|
|
460
|
+
return recursive_stack(((c, s), (s, c)))
|
|
461
|
+
|
|
462
|
+
|
|
463
|
+
register_param_gate("RX", rx_gate_param_gen, 1)
|
|
464
|
+
|
|
465
|
+
|
|
466
|
+
def ry_gate_param_gen(params):
|
|
467
|
+
phi = params[0]
|
|
468
|
+
|
|
469
|
+
with backend_like(phi):
|
|
470
|
+
# get a real backend zero
|
|
471
|
+
zero = phi * 0.0
|
|
472
|
+
|
|
473
|
+
c = do("complex", do("cos", phi / 2), zero)
|
|
474
|
+
s = do("complex", do("sin", phi / 2), zero)
|
|
475
|
+
|
|
476
|
+
return recursive_stack(((c, -s), (s, c)))
|
|
477
|
+
|
|
478
|
+
|
|
479
|
+
register_param_gate("RY", ry_gate_param_gen, 1)
|
|
480
|
+
|
|
481
|
+
|
|
482
|
+
def rz_gate_param_gen(params):
|
|
483
|
+
phi = params[0]
|
|
484
|
+
|
|
485
|
+
with backend_like(phi):
|
|
486
|
+
# get a real backend zero
|
|
487
|
+
zero = phi * 0.0
|
|
488
|
+
|
|
489
|
+
c = do("complex", do("cos", phi / 2), zero)
|
|
490
|
+
s = do("complex", zero, -do("sin", phi / 2))
|
|
491
|
+
|
|
492
|
+
# get a complex backend zero
|
|
493
|
+
zero = do("complex", zero, zero)
|
|
494
|
+
|
|
495
|
+
return recursive_stack(((c + s, zero), (zero, c - s)))
|
|
496
|
+
|
|
497
|
+
|
|
498
|
+
register_param_gate("RZ", rz_gate_param_gen, 1)
|
|
499
|
+
|
|
500
|
+
|
|
501
|
+
def u3_gate_param_gen(params):
|
|
502
|
+
theta, phi, lamda = params[0], params[1], params[2]
|
|
503
|
+
|
|
504
|
+
with backend_like(theta):
|
|
505
|
+
# get a real backend zero
|
|
506
|
+
zero = theta * 0.0
|
|
507
|
+
|
|
508
|
+
theta_2 = theta / 2
|
|
509
|
+
c2 = do("complex", do("cos", theta_2), zero)
|
|
510
|
+
s2 = do("complex", do("sin", theta_2), zero)
|
|
511
|
+
el = do("exp", do("complex", zero, lamda))
|
|
512
|
+
ep = do("exp", do("complex", zero, phi))
|
|
513
|
+
elp = do("exp", do("complex", zero, lamda + phi))
|
|
514
|
+
|
|
515
|
+
return recursive_stack(((c2, -el * s2), (ep * s2, elp * c2)))
|
|
516
|
+
|
|
517
|
+
|
|
518
|
+
register_param_gate("U3", u3_gate_param_gen, 1)
|
|
519
|
+
|
|
520
|
+
|
|
521
|
+
def u2_gate_param_gen(params):
|
|
522
|
+
phi, lamda = params[0], params[1]
|
|
523
|
+
|
|
524
|
+
with backend_like(phi):
|
|
525
|
+
# get a real backend zero
|
|
526
|
+
zero = phi * 0.0
|
|
527
|
+
|
|
528
|
+
c01 = -do("exp", do("complex", zero, lamda))
|
|
529
|
+
c10 = do("exp", do("complex", zero, phi))
|
|
530
|
+
c11 = do("exp", do("complex", zero, phi + lamda))
|
|
531
|
+
|
|
532
|
+
# get a complex backend zero and backend one
|
|
533
|
+
zero = do("complex", zero, zero)
|
|
534
|
+
one = zero + 1.0
|
|
535
|
+
|
|
536
|
+
return recursive_stack(((one, c01), (c10, c11))) / 2**0.5
|
|
537
|
+
|
|
538
|
+
|
|
539
|
+
register_param_gate("U2", u2_gate_param_gen, 1)
|
|
540
|
+
|
|
541
|
+
|
|
542
|
+
def u1_gate_param_gen(params):
|
|
543
|
+
lamda = params[0]
|
|
544
|
+
|
|
545
|
+
with backend_like(lamda):
|
|
546
|
+
# get a real backend zero
|
|
547
|
+
zero = lamda * 0.0
|
|
548
|
+
|
|
549
|
+
c11 = do("exp", do("complex", zero, lamda))
|
|
550
|
+
|
|
551
|
+
# get a complex backend zero and backend one
|
|
552
|
+
zero = do("complex", zero, zero)
|
|
553
|
+
one = zero + 1.0
|
|
554
|
+
|
|
555
|
+
return recursive_stack(((one, zero), (zero, c11)))
|
|
556
|
+
|
|
557
|
+
|
|
558
|
+
register_param_gate("U1", u1_gate_param_gen, 1)
|
|
559
|
+
register_param_gate("PHASE", u1_gate_param_gen, 1)
|
|
560
|
+
|
|
561
|
+
|
|
562
|
+
# two qubit parametrizable gates
|
|
563
|
+
|
|
564
|
+
|
|
565
|
+
def cu3_param_gen(params):
|
|
566
|
+
U3 = u3_gate_param_gen(params)
|
|
567
|
+
|
|
568
|
+
with backend_like(U3):
|
|
569
|
+
# get a 'backend zero'
|
|
570
|
+
zero = 0.0 * U3[0, 0]
|
|
571
|
+
# get a 'backend one'
|
|
572
|
+
one = zero + 1.0
|
|
573
|
+
|
|
574
|
+
data = (
|
|
575
|
+
(((one, zero), (zero, zero)), ((zero, one), (zero, zero))),
|
|
576
|
+
(
|
|
577
|
+
((zero, zero), (U3[0, 0], U3[0, 1])),
|
|
578
|
+
((zero, zero), (U3[1, 0], U3[1, 1])),
|
|
579
|
+
),
|
|
580
|
+
)
|
|
581
|
+
|
|
582
|
+
return recursive_stack(data)
|
|
583
|
+
|
|
584
|
+
|
|
585
|
+
register_param_gate("CU3", cu3_param_gen, 2)
|
|
586
|
+
|
|
587
|
+
|
|
588
|
+
def cu2_param_gen(params):
|
|
589
|
+
U2 = u2_gate_param_gen(params)
|
|
590
|
+
|
|
591
|
+
with backend_like(U2):
|
|
592
|
+
# get a 'backend zero'
|
|
593
|
+
zero = 0.0 * U2[0, 0]
|
|
594
|
+
# get a 'backend one'
|
|
595
|
+
one = zero + 1.0
|
|
596
|
+
|
|
597
|
+
data = (
|
|
598
|
+
(((one, zero), (zero, zero)), ((zero, one), (zero, zero))),
|
|
599
|
+
(
|
|
600
|
+
((zero, zero), (U2[0, 0], U2[0, 1])),
|
|
601
|
+
((zero, zero), (U2[1, 0], U2[1, 1])),
|
|
602
|
+
),
|
|
603
|
+
)
|
|
604
|
+
|
|
605
|
+
return recursive_stack(data)
|
|
606
|
+
|
|
607
|
+
|
|
608
|
+
register_param_gate("CU2", cu2_param_gen, 2)
|
|
609
|
+
|
|
610
|
+
|
|
611
|
+
def cu1_param_gen(params):
|
|
612
|
+
lamda = params[0]
|
|
613
|
+
|
|
614
|
+
with backend_like(lamda):
|
|
615
|
+
# get a real backend zero
|
|
616
|
+
zero = 0.0 * lamda
|
|
617
|
+
|
|
618
|
+
c11 = do("exp", do("complex", zero, lamda))
|
|
619
|
+
|
|
620
|
+
# get a complex backend zero and backend one
|
|
621
|
+
zero = do("complex", zero, zero)
|
|
622
|
+
one = zero + 1.0
|
|
623
|
+
|
|
624
|
+
data = (
|
|
625
|
+
(((one, zero), (zero, zero)), ((zero, one), (zero, zero))),
|
|
626
|
+
(((zero, zero), (one, zero)), ((zero, zero), (zero, c11))),
|
|
627
|
+
)
|
|
628
|
+
|
|
629
|
+
return recursive_stack(data)
|
|
630
|
+
|
|
631
|
+
|
|
632
|
+
register_param_gate("CU1", cu1_param_gen, 2)
|
|
633
|
+
register_param_gate("CPHASE", cu1_param_gen, 2)
|
|
634
|
+
|
|
635
|
+
|
|
636
|
+
def crx_param_gen(params):
|
|
637
|
+
"""Parametrized controlled X-rotation."""
|
|
638
|
+
theta = params[0]
|
|
639
|
+
|
|
640
|
+
with backend_like(theta):
|
|
641
|
+
# get a real backend zero
|
|
642
|
+
zero = 0.0 * theta
|
|
643
|
+
|
|
644
|
+
ccos = do("complex", do("cos", theta / 2), zero)
|
|
645
|
+
csin = do("complex", zero, -do("sin", theta / 2))
|
|
646
|
+
|
|
647
|
+
# get a complex backend zero and backend one
|
|
648
|
+
zero = do("complex", zero, zero)
|
|
649
|
+
one = zero + 1.0
|
|
650
|
+
|
|
651
|
+
data = (
|
|
652
|
+
(((one, zero), (zero, zero)), ((zero, one), (zero, zero))),
|
|
653
|
+
(((zero, zero), (ccos, csin)), ((zero, zero), (csin, ccos))),
|
|
654
|
+
)
|
|
655
|
+
|
|
656
|
+
return recursive_stack(data)
|
|
657
|
+
|
|
658
|
+
|
|
659
|
+
register_param_gate("CRX", crx_param_gen, 2)
|
|
660
|
+
|
|
661
|
+
|
|
662
|
+
def cry_param_gen(params):
|
|
663
|
+
"""Parametrized controlled Y-rotation."""
|
|
664
|
+
theta = params[0]
|
|
665
|
+
|
|
666
|
+
with backend_like(theta):
|
|
667
|
+
# get a real backend zero
|
|
668
|
+
zero = 0.0 * theta
|
|
669
|
+
|
|
670
|
+
ccos = do("complex", do("cos", theta / 2), zero)
|
|
671
|
+
csin = do("complex", do("sin", theta / 2), zero)
|
|
672
|
+
|
|
673
|
+
# get a complex backend zero and backend one
|
|
674
|
+
zero = do("complex", zero, zero)
|
|
675
|
+
one = zero + 1.0
|
|
676
|
+
|
|
677
|
+
data = (
|
|
678
|
+
(((one, zero), (zero, zero)), ((zero, one), (zero, zero))),
|
|
679
|
+
(((zero, zero), (ccos, -csin)), ((zero, zero), (csin, ccos))),
|
|
680
|
+
)
|
|
681
|
+
|
|
682
|
+
return recursive_stack(data)
|
|
683
|
+
|
|
684
|
+
|
|
685
|
+
register_param_gate("CRY", cry_param_gen, 2)
|
|
686
|
+
|
|
687
|
+
|
|
688
|
+
def crz_param_gen(params):
|
|
689
|
+
"""Parametrized controlled Z-rotation."""
|
|
690
|
+
theta = params[0]
|
|
691
|
+
|
|
692
|
+
with backend_like(theta):
|
|
693
|
+
# get a real backend zero
|
|
694
|
+
zero = 0.0 * theta
|
|
695
|
+
|
|
696
|
+
theta_2 = theta / 2
|
|
697
|
+
c = do("complex", do("cos", theta_2), zero)
|
|
698
|
+
s = do("complex", zero, -do("sin", theta_2))
|
|
699
|
+
|
|
700
|
+
# get a complex backend zero and backend one
|
|
701
|
+
zero = do("complex", zero, zero)
|
|
702
|
+
one = zero + 1.0
|
|
703
|
+
|
|
704
|
+
data = (
|
|
705
|
+
(((one, zero), (zero, zero)), ((zero, one), (zero, zero))),
|
|
706
|
+
(((zero, zero), (c + s, zero)), ((zero, zero), (zero, c - s))),
|
|
707
|
+
)
|
|
708
|
+
|
|
709
|
+
return recursive_stack(data)
|
|
710
|
+
|
|
711
|
+
|
|
712
|
+
register_param_gate("CRZ", crz_param_gen, 2)
|
|
713
|
+
|
|
714
|
+
|
|
715
|
+
def fsim_param_gen(params):
|
|
716
|
+
theta, phi = params[0], params[1]
|
|
717
|
+
|
|
718
|
+
with backend_like(theta):
|
|
719
|
+
# get a real backend zero
|
|
720
|
+
zero = theta * 0.0
|
|
721
|
+
|
|
722
|
+
a = do("complex", do("cos", theta), zero)
|
|
723
|
+
b = do("complex", zero, -do("sin", theta))
|
|
724
|
+
c = do("exp", do("complex", zero, -phi))
|
|
725
|
+
|
|
726
|
+
# get a complex backend zero and backend one
|
|
727
|
+
zero = do("complex", zero, zero)
|
|
728
|
+
one = zero + 1.0
|
|
729
|
+
|
|
730
|
+
data = (
|
|
731
|
+
(((one, zero), (zero, zero)), ((zero, a), (b, zero))),
|
|
732
|
+
(((zero, b), (a, zero)), ((zero, zero), (zero, c))),
|
|
733
|
+
)
|
|
734
|
+
|
|
735
|
+
return recursive_stack(data)
|
|
736
|
+
|
|
737
|
+
|
|
738
|
+
register_param_gate("FSIM", fsim_param_gen, 2)
|
|
739
|
+
register_param_gate("FS", fsim_param_gen, 2, "FSIM")
|
|
740
|
+
|
|
741
|
+
|
|
742
|
+
def fsimg_param_gen(params):
|
|
743
|
+
theta, zeta, chi, gamma, phi = (
|
|
744
|
+
params[0],
|
|
745
|
+
params[1],
|
|
746
|
+
params[2],
|
|
747
|
+
params[3],
|
|
748
|
+
params[4],
|
|
749
|
+
)
|
|
750
|
+
"""Parametrized, most general number conserving two qubit gate.
|
|
751
|
+
"""
|
|
752
|
+
|
|
753
|
+
with backend_like(theta):
|
|
754
|
+
# get a real backend zero
|
|
755
|
+
zero = 0.0 * theta
|
|
756
|
+
|
|
757
|
+
cos = do("cos", theta)
|
|
758
|
+
sin = do("sin", theta)
|
|
759
|
+
|
|
760
|
+
c11 = do("exp", do("complex", zero, -(gamma + zeta))) * do(
|
|
761
|
+
"complex", cos, zero
|
|
762
|
+
)
|
|
763
|
+
c12 = do("exp", do("complex", zero, -(gamma - chi))) * do(
|
|
764
|
+
"complex", zero, -sin
|
|
765
|
+
)
|
|
766
|
+
c21 = do("exp", do("complex", zero, -(gamma + chi))) * do(
|
|
767
|
+
"complex", zero, -sin
|
|
768
|
+
)
|
|
769
|
+
c22 = do("exp", do("complex", zero, -(gamma - zeta))) * do(
|
|
770
|
+
"complex", cos, zero
|
|
771
|
+
)
|
|
772
|
+
c33 = do("exp", do("complex", zero, -(2 * gamma + phi)))
|
|
773
|
+
|
|
774
|
+
# get a complex backend zero and backend one
|
|
775
|
+
zero = do("complex", zero, zero)
|
|
776
|
+
one = zero + 1.0
|
|
777
|
+
|
|
778
|
+
data = (
|
|
779
|
+
(((one, zero), (zero, zero)), ((zero, c11), (c12, zero))),
|
|
780
|
+
(((zero, c21), (c22, zero)), ((zero, zero), (zero, c33))),
|
|
781
|
+
)
|
|
782
|
+
|
|
783
|
+
return recursive_stack(data)
|
|
784
|
+
|
|
785
|
+
|
|
786
|
+
register_param_gate("FSIMG", fsimg_param_gen, 2)
|
|
787
|
+
|
|
788
|
+
|
|
789
|
+
def givens_param_gen(params):
|
|
790
|
+
theta = params[0]
|
|
791
|
+
|
|
792
|
+
with backend_like(theta):
|
|
793
|
+
# get a real backend zero
|
|
794
|
+
zero = 0.0 * theta
|
|
795
|
+
|
|
796
|
+
a = do("complex", do("cos", theta), zero)
|
|
797
|
+
b = do("complex", do("sin", theta), zero)
|
|
798
|
+
|
|
799
|
+
# get a complex backend zero and backend one
|
|
800
|
+
zero = do("complex", zero, zero)
|
|
801
|
+
one = zero + 1.0
|
|
802
|
+
|
|
803
|
+
data = (
|
|
804
|
+
(((one, zero), (zero, zero)), ((zero, a), (-b, zero))),
|
|
805
|
+
(((zero, b), (a, zero)), ((zero, zero), (zero, one))),
|
|
806
|
+
)
|
|
807
|
+
|
|
808
|
+
return recursive_stack(data)
|
|
809
|
+
|
|
810
|
+
|
|
811
|
+
register_param_gate("GIVENS", givens_param_gen, num_qubits=2)
|
|
812
|
+
|
|
813
|
+
|
|
814
|
+
def givens2_param_gen(params):
|
|
815
|
+
theta, phi = params[0], params[1]
|
|
816
|
+
|
|
817
|
+
with backend_like(theta):
|
|
818
|
+
# get a real backend zero
|
|
819
|
+
zero = 0.0 * theta
|
|
820
|
+
|
|
821
|
+
a = do("complex", do("cos", theta), zero)
|
|
822
|
+
b = do("exp", do("complex", zero, phi)) * do(
|
|
823
|
+
"complex", do("sin", theta), zero
|
|
824
|
+
)
|
|
825
|
+
b_conj = do("exp", do("complex", zero, -phi)) * do(
|
|
826
|
+
"complex", do("sin", theta), zero
|
|
827
|
+
)
|
|
828
|
+
|
|
829
|
+
# get a complex backend zero and backend one
|
|
830
|
+
zero = do("complex", zero, zero)
|
|
831
|
+
one = zero + 1.0
|
|
832
|
+
|
|
833
|
+
data = (
|
|
834
|
+
(((one, zero), (zero, zero)), ((zero, a), (-b, zero))),
|
|
835
|
+
(((zero, b_conj), (a, zero)), ((zero, zero), (zero, one))),
|
|
836
|
+
)
|
|
837
|
+
|
|
838
|
+
return recursive_stack(data)
|
|
839
|
+
|
|
840
|
+
|
|
841
|
+
register_param_gate("GIVENS2", givens2_param_gen, num_qubits=2)
|
|
842
|
+
|
|
843
|
+
|
|
844
|
+
def rxx_param_gen(params):
|
|
845
|
+
r"""Parametrized two qubit XX-rotation.
|
|
846
|
+
|
|
847
|
+
.. math::
|
|
848
|
+
|
|
849
|
+
\mathrm{RXX}(\theta) = \exp(-i \frac{\theta}{2} X_i X_j)
|
|
850
|
+
|
|
851
|
+
"""
|
|
852
|
+
theta = params[0]
|
|
853
|
+
|
|
854
|
+
with backend_like(theta):
|
|
855
|
+
# get a real 'backend zero'
|
|
856
|
+
zero = 0.0 * theta
|
|
857
|
+
|
|
858
|
+
theta_2 = theta / 2
|
|
859
|
+
ccos = do("complex", do("cos", theta_2), zero)
|
|
860
|
+
csin = do("complex", zero, -do("sin", theta_2))
|
|
861
|
+
|
|
862
|
+
# get a complex backend zero
|
|
863
|
+
zero = do("complex", zero, zero)
|
|
864
|
+
|
|
865
|
+
data = (
|
|
866
|
+
(((ccos, zero), (zero, csin)), ((zero, ccos), (csin, zero))),
|
|
867
|
+
(((zero, csin), (ccos, zero)), ((csin, zero), (zero, ccos))),
|
|
868
|
+
)
|
|
869
|
+
|
|
870
|
+
return recursive_stack(data)
|
|
871
|
+
|
|
872
|
+
|
|
873
|
+
register_param_gate("RXX", rxx_param_gen, 2)
|
|
874
|
+
|
|
875
|
+
|
|
876
|
+
def ryy_param_gen(params):
|
|
877
|
+
r"""Parametrized two qubit YY-rotation.
|
|
878
|
+
|
|
879
|
+
.. math::
|
|
880
|
+
|
|
881
|
+
\mathrm{RYY}(\theta) = \exp(-i \frac{\theta}{2} Y_i Y_j)
|
|
882
|
+
|
|
883
|
+
"""
|
|
884
|
+
theta = params[0]
|
|
885
|
+
|
|
886
|
+
with backend_like(theta):
|
|
887
|
+
# get a real 'backend zero'
|
|
888
|
+
zero = 0.0 * theta
|
|
889
|
+
|
|
890
|
+
theta_2 = theta / 2
|
|
891
|
+
ccos = do("complex", do("cos", theta_2), zero)
|
|
892
|
+
csin = do("complex", zero, do("sin", theta_2))
|
|
893
|
+
|
|
894
|
+
# get a complex backend zero
|
|
895
|
+
zero = do("complex", zero, zero)
|
|
896
|
+
|
|
897
|
+
data = (
|
|
898
|
+
(((ccos, zero), (zero, csin)), ((zero, ccos), (-csin, zero))),
|
|
899
|
+
(((zero, -csin), (ccos, zero)), ((csin, zero), (zero, ccos))),
|
|
900
|
+
)
|
|
901
|
+
|
|
902
|
+
return recursive_stack(data)
|
|
903
|
+
|
|
904
|
+
|
|
905
|
+
register_param_gate("RYY", ryy_param_gen, 2)
|
|
906
|
+
|
|
907
|
+
|
|
908
|
+
def rzz_param_gen(params):
|
|
909
|
+
r"""Parametrized two qubit ZZ-rotation.
|
|
910
|
+
|
|
911
|
+
.. math::
|
|
912
|
+
|
|
913
|
+
\mathrm{RZZ}(\theta) = \exp(-i \frac{\theta}{2} Z_i Z_j)
|
|
914
|
+
|
|
915
|
+
"""
|
|
916
|
+
theta = params[0]
|
|
917
|
+
|
|
918
|
+
with backend_like(theta):
|
|
919
|
+
# get a real 'backend zero'
|
|
920
|
+
zero = 0.0 * theta
|
|
921
|
+
|
|
922
|
+
theta_2 = theta / 2
|
|
923
|
+
c00 = c11 = do("complex", do("cos", theta_2), do("sin", -theta_2))
|
|
924
|
+
c01 = c10 = do("complex", do("cos", theta_2), do("sin", theta_2))
|
|
925
|
+
|
|
926
|
+
# get a complex backend zero
|
|
927
|
+
zero = do("complex", zero, zero)
|
|
928
|
+
|
|
929
|
+
data = (
|
|
930
|
+
(((c00, zero), (zero, zero)), ((zero, c01), (zero, zero))),
|
|
931
|
+
(((zero, zero), (c10, zero)), ((zero, zero), (zero, c11))),
|
|
932
|
+
)
|
|
933
|
+
|
|
934
|
+
return recursive_stack(data)
|
|
935
|
+
|
|
936
|
+
|
|
937
|
+
register_param_gate("RZZ", rzz_param_gen, 2)
|
|
938
|
+
|
|
939
|
+
|
|
940
|
+
def su4_gate_param_gen(params):
|
|
941
|
+
"""See https://arxiv.org/abs/quant-ph/0308006 - Fig. 7.
|
|
942
|
+
params:
|
|
943
|
+
# theta1, phi1, lamda1,
|
|
944
|
+
# theta2, phi2, lamda2,
|
|
945
|
+
# theta3, phi3, lamda3,
|
|
946
|
+
# theta4, phi4, lamda4,
|
|
947
|
+
# t1, t2, t3,
|
|
948
|
+
"""
|
|
949
|
+
|
|
950
|
+
TA1 = Tensor(u3_gate_param_gen(params[0:3]), ["a1", "a0"])
|
|
951
|
+
TA2 = Tensor(u3_gate_param_gen(params[3:6]), ["b1", "b0"])
|
|
952
|
+
|
|
953
|
+
cnot = do(
|
|
954
|
+
"array",
|
|
955
|
+
qu.CNOT().reshape(2, 2, 2, 2),
|
|
956
|
+
like=params,
|
|
957
|
+
dtype=TA1.data.dtype,
|
|
958
|
+
)
|
|
959
|
+
|
|
960
|
+
TNOTC1 = Tensor(cnot, ["b2", "a2", "b1", "a1"])
|
|
961
|
+
TRz1 = Tensor(rz_gate_param_gen(params[12:13]), inds=["a3", "a2"])
|
|
962
|
+
TRy2 = Tensor(ry_gate_param_gen(params[13:14]), inds=["b3", "b2"])
|
|
963
|
+
TCNOT2 = Tensor(cnot, ["a5", "b4", "a3", "b3"])
|
|
964
|
+
TRy3 = Tensor(ry_gate_param_gen(params[14:15]), inds=["b5", "b4"])
|
|
965
|
+
TNOTC3 = Tensor(cnot, ["b6", "a6", "b5", "a5"])
|
|
966
|
+
TA3 = Tensor(u3_gate_param_gen(params[6:9]), ["a7", "a6"])
|
|
967
|
+
TA4 = Tensor(u3_gate_param_gen(params[9:12]), ["b7", "b6"])
|
|
968
|
+
|
|
969
|
+
return tensor_contract(
|
|
970
|
+
TA1,
|
|
971
|
+
TA2,
|
|
972
|
+
TNOTC1,
|
|
973
|
+
TRz1,
|
|
974
|
+
TRy2,
|
|
975
|
+
TCNOT2,
|
|
976
|
+
TRy3,
|
|
977
|
+
TNOTC3,
|
|
978
|
+
TA3,
|
|
979
|
+
TA4,
|
|
980
|
+
output_inds=["a7", "b7"] + ["a0", "b0"],
|
|
981
|
+
optimize="auto-hq",
|
|
982
|
+
).data
|
|
983
|
+
|
|
984
|
+
|
|
985
|
+
register_param_gate("SU4", su4_gate_param_gen, 2)
|
|
986
|
+
|
|
987
|
+
|
|
988
|
+
# special non-tensor gates
|
|
989
|
+
|
|
990
|
+
_MPS_METHODS = {
|
|
991
|
+
"auto-mps",
|
|
992
|
+
"nonlocal",
|
|
993
|
+
"swap+split",
|
|
994
|
+
}
|
|
995
|
+
|
|
996
|
+
|
|
997
|
+
def apply_swap(psi, i, j, **gate_opts):
|
|
998
|
+
contract = gate_opts.pop("contract", None)
|
|
999
|
+
|
|
1000
|
+
if contract not in _MPS_METHODS:
|
|
1001
|
+
# just do swap by lazily reindexing
|
|
1002
|
+
iind, jind = map(psi.site_ind, (int(i), int(j)))
|
|
1003
|
+
psi.reindex_({iind: jind, jind: iind})
|
|
1004
|
+
|
|
1005
|
+
else:
|
|
1006
|
+
# tensors are absorbed so propagate_tags is not needed
|
|
1007
|
+
gate_opts.pop("propagate_tags", None)
|
|
1008
|
+
|
|
1009
|
+
if contract == "nonlocal":
|
|
1010
|
+
psi.gate_nonlocal_(qu.swap(2), (i, j), **gate_opts)
|
|
1011
|
+
else: # {"swap+split", "auto-mps"}:
|
|
1012
|
+
psi.swap_sites_with_compress_(i, j, **gate_opts)
|
|
1013
|
+
|
|
1014
|
+
|
|
1015
|
+
register_special_gate("SWAP", apply_swap, 2, array=qu.swap(2))
|
|
1016
|
+
register_special_gate("IDEN", lambda *_, **__: None, 1, array=qu.identity(2))
|
|
1017
|
+
|
|
1018
|
+
|
|
1019
|
+
def build_controlled_gate_htn(
|
|
1020
|
+
ncontrol,
|
|
1021
|
+
gate,
|
|
1022
|
+
upper_inds,
|
|
1023
|
+
lower_inds,
|
|
1024
|
+
tags_each=None,
|
|
1025
|
+
tags_all=None,
|
|
1026
|
+
bond_ind=None,
|
|
1027
|
+
):
|
|
1028
|
+
"""Build a low rank hyper tensor network (CP-decomp like) representation of
|
|
1029
|
+
a multi controlled gate.
|
|
1030
|
+
"""
|
|
1031
|
+
ngate = len(gate.qubits)
|
|
1032
|
+
gate_shape = (2,) * (2 * ngate)
|
|
1033
|
+
array = gate.array.reshape(gate_shape)
|
|
1034
|
+
|
|
1035
|
+
I2 = qu.identity(2, dtype=array.dtype)
|
|
1036
|
+
IG = qu.identity(2**ngate, dtype=array.dtype).reshape(gate_shape)
|
|
1037
|
+
p1 = qu.down(qtype="dop", dtype=array.dtype) # |1><1|
|
|
1038
|
+
|
|
1039
|
+
array_seqs = [[I2] * ncontrol + [IG], [p1] * ncontrol + [array - IG]]
|
|
1040
|
+
|
|
1041
|
+
# might need to group indices and tags on the target gate if multi-qubit
|
|
1042
|
+
if ngate > 1:
|
|
1043
|
+
upper_inds = (*upper_inds[:ncontrol], upper_inds[ncontrol:])
|
|
1044
|
+
lower_inds = (*lower_inds[:ncontrol], lower_inds[ncontrol:])
|
|
1045
|
+
tags_each = (*tags_each[:ncontrol], tags_each[ncontrol:])
|
|
1046
|
+
|
|
1047
|
+
htn = HTN_CP_operator_from_products(
|
|
1048
|
+
array_seqs,
|
|
1049
|
+
upper_inds=upper_inds,
|
|
1050
|
+
lower_inds=lower_inds,
|
|
1051
|
+
tags_each=tags_each,
|
|
1052
|
+
tags_all=tags_all,
|
|
1053
|
+
bond_ind=bond_ind,
|
|
1054
|
+
)
|
|
1055
|
+
|
|
1056
|
+
return htn
|
|
1057
|
+
|
|
1058
|
+
|
|
1059
|
+
def _apply_controlled_gate_mps(psi, gate, tags=None, **gate_opts):
|
|
1060
|
+
"""Apply a multi-controlled gate to a state represented as an MPS."""
|
|
1061
|
+
submpo = gate.build_mpo()
|
|
1062
|
+
where = sorted((*gate.controls, *gate.qubits))
|
|
1063
|
+
psi.gate_with_submpo_(submpo, where, **gate_opts)
|
|
1064
|
+
|
|
1065
|
+
|
|
1066
|
+
def _apply_controlled_gate_htn(
|
|
1067
|
+
psi, gate, tags=None, propagate_tags="register", **gate_opts
|
|
1068
|
+
):
|
|
1069
|
+
assert propagate_tags == "register"
|
|
1070
|
+
|
|
1071
|
+
all_qubits = (*gate.controls, *gate.qubits)
|
|
1072
|
+
ncontrol = len(gate.controls)
|
|
1073
|
+
ngate = len(gate.qubits)
|
|
1074
|
+
ntotal = ncontrol + ngate
|
|
1075
|
+
|
|
1076
|
+
upper_inds = [rand_uuid() for _ in range(ntotal)]
|
|
1077
|
+
lower_inds = [rand_uuid() for _ in range(ntotal)]
|
|
1078
|
+
tags_sequence = [psi.site_tag(i) for i in all_qubits]
|
|
1079
|
+
|
|
1080
|
+
htn = build_controlled_gate_htn(
|
|
1081
|
+
ncontrol,
|
|
1082
|
+
gate,
|
|
1083
|
+
upper_inds=upper_inds,
|
|
1084
|
+
lower_inds=lower_inds,
|
|
1085
|
+
tags_each=tags_sequence,
|
|
1086
|
+
tags_all=tags,
|
|
1087
|
+
)
|
|
1088
|
+
|
|
1089
|
+
psi.gate_inds_with_tn_(
|
|
1090
|
+
[psi.site_ind(i) for i in all_qubits],
|
|
1091
|
+
htn,
|
|
1092
|
+
lower_inds,
|
|
1093
|
+
upper_inds,
|
|
1094
|
+
**gate_opts,
|
|
1095
|
+
)
|
|
1096
|
+
|
|
1097
|
+
|
|
1098
|
+
def apply_controlled_gate(
|
|
1099
|
+
psi,
|
|
1100
|
+
gate,
|
|
1101
|
+
tags=None,
|
|
1102
|
+
contract="auto-split-gate",
|
|
1103
|
+
propagate_tags="register",
|
|
1104
|
+
**gate_opts,
|
|
1105
|
+
):
|
|
1106
|
+
if contract in ("auto-mps", "nonlocal"):
|
|
1107
|
+
_apply_controlled_gate_mps(psi, gate, tags=tags, **gate_opts)
|
|
1108
|
+
elif contract in (
|
|
1109
|
+
"auto-split-gate",
|
|
1110
|
+
"split-gate",
|
|
1111
|
+
):
|
|
1112
|
+
_apply_controlled_gate_htn(
|
|
1113
|
+
psi, gate, tags=tags, propagate_tags=propagate_tags, **gate_opts
|
|
1114
|
+
)
|
|
1115
|
+
else:
|
|
1116
|
+
raise ValueError(
|
|
1117
|
+
f"Contract method '{contract}' not "
|
|
1118
|
+
"supported for multi-controlled gates."
|
|
1119
|
+
)
|
|
1120
|
+
|
|
1121
|
+
|
|
1122
|
+
@functools.lru_cache(2**15)
|
|
1123
|
+
def _cached_param_gate_build(fn, params):
|
|
1124
|
+
return fn(params)
|
|
1125
|
+
|
|
1126
|
+
|
|
1127
|
+
class Gate:
|
|
1128
|
+
"""A simple class for storing the details of a quantum circuit gate.
|
|
1129
|
+
|
|
1130
|
+
Parameters
|
|
1131
|
+
----------
|
|
1132
|
+
label : str
|
|
1133
|
+
The name or 'identifier' of the gate.
|
|
1134
|
+
params : Iterable[float]
|
|
1135
|
+
The parameters of the gate.
|
|
1136
|
+
qubits : Iterable[int], optional
|
|
1137
|
+
Which qubits the gate acts on.
|
|
1138
|
+
controls : Iterable[int], optional
|
|
1139
|
+
Which qubits are the controls.
|
|
1140
|
+
round : int, optional
|
|
1141
|
+
If given, which round or layer the gate is part of.
|
|
1142
|
+
parametrize : bool, optional
|
|
1143
|
+
Whether the gate will correspond to a parametrized tensor.
|
|
1144
|
+
"""
|
|
1145
|
+
|
|
1146
|
+
__slots__ = (
|
|
1147
|
+
"_label",
|
|
1148
|
+
"_params",
|
|
1149
|
+
"_qubits",
|
|
1150
|
+
"_controls",
|
|
1151
|
+
"_round",
|
|
1152
|
+
"_parametrize",
|
|
1153
|
+
"_tag",
|
|
1154
|
+
"_special",
|
|
1155
|
+
"_constant",
|
|
1156
|
+
"_array",
|
|
1157
|
+
)
|
|
1158
|
+
|
|
1159
|
+
def __init__(
|
|
1160
|
+
self,
|
|
1161
|
+
label,
|
|
1162
|
+
params,
|
|
1163
|
+
qubits=None,
|
|
1164
|
+
controls=None,
|
|
1165
|
+
round=None,
|
|
1166
|
+
parametrize=False,
|
|
1167
|
+
):
|
|
1168
|
+
self._label = label.upper()
|
|
1169
|
+
|
|
1170
|
+
if self._label not in ALL_GATES:
|
|
1171
|
+
raise ValueError(f"Unknown gate: {self._label}.")
|
|
1172
|
+
|
|
1173
|
+
self._params = ops.asarray(params)
|
|
1174
|
+
if qubits is None:
|
|
1175
|
+
self._qubits = None
|
|
1176
|
+
else:
|
|
1177
|
+
self._qubits = tuple(qubits)
|
|
1178
|
+
|
|
1179
|
+
if controls is None:
|
|
1180
|
+
self._controls = None
|
|
1181
|
+
else:
|
|
1182
|
+
self._controls = tuple(controls)
|
|
1183
|
+
|
|
1184
|
+
self._round = int(round) if round is not None else round
|
|
1185
|
+
self._parametrize = bool(parametrize)
|
|
1186
|
+
|
|
1187
|
+
self._tag = GATE_TAGS[self._label]
|
|
1188
|
+
self._special = self._label in SPECIAL_GATES
|
|
1189
|
+
self._constant = self._label in CONSTANT_GATES
|
|
1190
|
+
if (self._special or self._constant) and self._parametrize:
|
|
1191
|
+
raise ValueError(f"Cannot parametrize the gate: {self._label}.")
|
|
1192
|
+
self._array = None
|
|
1193
|
+
|
|
1194
|
+
@classmethod
|
|
1195
|
+
def from_raw(cls, U, qubits=None, controls=None, round=None):
|
|
1196
|
+
new = object.__new__(cls)
|
|
1197
|
+
new._label = f"RAW{id(U)}"
|
|
1198
|
+
new._params = "raw"
|
|
1199
|
+
if qubits is None:
|
|
1200
|
+
new._qubits = None
|
|
1201
|
+
else:
|
|
1202
|
+
new._qubits = tuple(qubits)
|
|
1203
|
+
if controls is None:
|
|
1204
|
+
new._controls = None
|
|
1205
|
+
else:
|
|
1206
|
+
new._controls = tuple(controls)
|
|
1207
|
+
new._round = int(round) if round is not None else round
|
|
1208
|
+
new._special = False
|
|
1209
|
+
new._parametrize = isinstance(U, ops.PArray)
|
|
1210
|
+
new._tag = None
|
|
1211
|
+
new._array = U
|
|
1212
|
+
return new
|
|
1213
|
+
|
|
1214
|
+
def copy(self):
|
|
1215
|
+
new = object.__new__(self.__class__)
|
|
1216
|
+
new._label = self._label
|
|
1217
|
+
new._params = self._params
|
|
1218
|
+
new._qubits = self._qubits
|
|
1219
|
+
new._controls = self._controls
|
|
1220
|
+
new._round = self._round
|
|
1221
|
+
new._parametrize = self._parametrize
|
|
1222
|
+
new._tag = self._tag
|
|
1223
|
+
new._special = self._special
|
|
1224
|
+
new._constant = self._constant
|
|
1225
|
+
new._array = self._array
|
|
1226
|
+
return new
|
|
1227
|
+
|
|
1228
|
+
@property
|
|
1229
|
+
def label(self):
|
|
1230
|
+
return self._label
|
|
1231
|
+
|
|
1232
|
+
@property
|
|
1233
|
+
def params(self):
|
|
1234
|
+
return self._params
|
|
1235
|
+
|
|
1236
|
+
@property
|
|
1237
|
+
def qubits(self):
|
|
1238
|
+
return self._qubits
|
|
1239
|
+
|
|
1240
|
+
@qubits.setter
|
|
1241
|
+
def qubits(self, qubits):
|
|
1242
|
+
if qubits is None:
|
|
1243
|
+
self._qubits = None
|
|
1244
|
+
else:
|
|
1245
|
+
self._qubits = tuple(qubits)
|
|
1246
|
+
|
|
1247
|
+
@property
|
|
1248
|
+
def total_qubit_count(self):
|
|
1249
|
+
nq = len(self._qubits)
|
|
1250
|
+
if self._controls:
|
|
1251
|
+
nq += len(self._controls)
|
|
1252
|
+
return nq
|
|
1253
|
+
|
|
1254
|
+
@property
|
|
1255
|
+
def controls(self):
|
|
1256
|
+
return self._controls
|
|
1257
|
+
|
|
1258
|
+
@property
|
|
1259
|
+
def round(self):
|
|
1260
|
+
return self._round
|
|
1261
|
+
|
|
1262
|
+
@property
|
|
1263
|
+
def special(self):
|
|
1264
|
+
return self._special
|
|
1265
|
+
|
|
1266
|
+
@property
|
|
1267
|
+
def parametrize(self):
|
|
1268
|
+
return self._parametrize
|
|
1269
|
+
|
|
1270
|
+
@property
|
|
1271
|
+
def tag(self):
|
|
1272
|
+
return self._tag
|
|
1273
|
+
|
|
1274
|
+
def copy_with(self, **kwargs):
|
|
1275
|
+
"""Take a copy of this gate but with some attributes changed."""
|
|
1276
|
+
label = kwargs.get("label", self._label)
|
|
1277
|
+
params = kwargs.get("params", self._params)
|
|
1278
|
+
qubits = kwargs.get("qubits", self._qubits)
|
|
1279
|
+
controls = kwargs.get("controls", self._controls)
|
|
1280
|
+
round = kwargs.get("round", self._round)
|
|
1281
|
+
parametrize = kwargs.get("parametrize", self._parametrize)
|
|
1282
|
+
return self.__class__(
|
|
1283
|
+
label, params, qubits, controls, round, parametrize
|
|
1284
|
+
)
|
|
1285
|
+
|
|
1286
|
+
def build_array(self):
|
|
1287
|
+
"""Build the array representation of the gate. For controlled gates
|
|
1288
|
+
this *excludes* the control qubits.
|
|
1289
|
+
"""
|
|
1290
|
+
if self._special and (self._label not in CONSTANT_GATES):
|
|
1291
|
+
# these don't have an array representation
|
|
1292
|
+
raise ValueError(f"{self.label} gates have no array to build.")
|
|
1293
|
+
|
|
1294
|
+
if self._constant:
|
|
1295
|
+
# simply return the constant array
|
|
1296
|
+
return CONSTANT_GATES[self._label]
|
|
1297
|
+
|
|
1298
|
+
# build the array
|
|
1299
|
+
param_fn = PARAM_GATES[self._label]
|
|
1300
|
+
if self._parametrize:
|
|
1301
|
+
# either lazily, as tensor will be parametrized
|
|
1302
|
+
return ops.PArray(param_fn, self._params)
|
|
1303
|
+
|
|
1304
|
+
# or cached directly into array
|
|
1305
|
+
try:
|
|
1306
|
+
return _cached_param_gate_build(param_fn, self._params)
|
|
1307
|
+
except TypeError:
|
|
1308
|
+
return param_fn(self._params)
|
|
1309
|
+
|
|
1310
|
+
@property
|
|
1311
|
+
def array(self):
|
|
1312
|
+
if self._array is None:
|
|
1313
|
+
self._array = self.build_array()
|
|
1314
|
+
return self._array
|
|
1315
|
+
|
|
1316
|
+
def build_mpo(self, L=None, **kwargs):
|
|
1317
|
+
"""Build an MPO representation of this gate."""
|
|
1318
|
+
G = self.array
|
|
1319
|
+
|
|
1320
|
+
if L is None:
|
|
1321
|
+
L = max((*self.qubits, *self.controls), default=0) + 1
|
|
1322
|
+
|
|
1323
|
+
if not self.controls:
|
|
1324
|
+
return MatrixProductOperator.from_dense(
|
|
1325
|
+
G, sites=self.qubits, L=L, **kwargs
|
|
1326
|
+
)
|
|
1327
|
+
|
|
1328
|
+
IG = qu.identity(2 ** len(self.qubits))
|
|
1329
|
+
IG = reshape(IG, G.shape)
|
|
1330
|
+
p1 = qu.down(qtype="dop")
|
|
1331
|
+
|
|
1332
|
+
# form (G - 1) on target qubits
|
|
1333
|
+
mpo = MatrixProductOperator.from_dense(
|
|
1334
|
+
G - IG, sites=self.qubits, L=L, **kwargs
|
|
1335
|
+
)
|
|
1336
|
+
|
|
1337
|
+
# take tensor product with |11...><11...| on controls
|
|
1338
|
+
mpo.fill_empty_sites_(mode=self.controls, fill_array=p1)
|
|
1339
|
+
|
|
1340
|
+
# add with identity on all qubits
|
|
1341
|
+
mpo_I = MPO_identity_like(
|
|
1342
|
+
mpo, sites=sorted((*self.qubits, *self.controls))
|
|
1343
|
+
)
|
|
1344
|
+
|
|
1345
|
+
return mpo.add_MPO_(mpo_I)
|
|
1346
|
+
|
|
1347
|
+
def __repr__(self):
|
|
1348
|
+
return (
|
|
1349
|
+
f"<{self.__class__.__name__}("
|
|
1350
|
+
+ f"label={self._label}, "
|
|
1351
|
+
+ f"params={self._params}, "
|
|
1352
|
+
+ f"qubits={self._qubits}"
|
|
1353
|
+
+ (f", controls={self._controls})" if self._controls else "")
|
|
1354
|
+
+ (f", round={self._round}" if self._round is not None else "")
|
|
1355
|
+
+ (
|
|
1356
|
+
f", parametrize={self._parametrize})"
|
|
1357
|
+
if self._parametrize
|
|
1358
|
+
else ""
|
|
1359
|
+
)
|
|
1360
|
+
+ ")>"
|
|
1361
|
+
)
|
|
1362
|
+
|
|
1363
|
+
|
|
1364
|
+
def sample_bitstring_from_prob_ndarray(p, seed=None):
|
|
1365
|
+
"""Sample a bitstring from n-dimensional tensor ``p`` of probabilities.
|
|
1366
|
+
|
|
1367
|
+
Examples
|
|
1368
|
+
--------
|
|
1369
|
+
|
|
1370
|
+
>>> import numpy as np
|
|
1371
|
+
>>> p = np.zeros(shape=(2, 2, 2, 2, 2))
|
|
1372
|
+
>>> p[0, 1, 0, 1, 1] = 1.0
|
|
1373
|
+
>>> sample_bitstring_from_prob_ndarray(p)
|
|
1374
|
+
'01011'
|
|
1375
|
+
"""
|
|
1376
|
+
rng = np.random.default_rng(seed)
|
|
1377
|
+
b = rng.choice(p.size, p=p.ravel())
|
|
1378
|
+
return f"{b:0>{p.ndim}b}"
|
|
1379
|
+
|
|
1380
|
+
|
|
1381
|
+
def rehearsal_dict(tn, tree):
|
|
1382
|
+
return {
|
|
1383
|
+
"tn": tn,
|
|
1384
|
+
"tree": tree,
|
|
1385
|
+
"W": tree.contraction_width(),
|
|
1386
|
+
"C": math.log10(max(tree.contraction_cost(), 1)),
|
|
1387
|
+
}
|
|
1388
|
+
|
|
1389
|
+
|
|
1390
|
+
def parse_to_gate(
|
|
1391
|
+
gate_id,
|
|
1392
|
+
*gate_args,
|
|
1393
|
+
params=None,
|
|
1394
|
+
qubits=None,
|
|
1395
|
+
controls=None,
|
|
1396
|
+
gate_round=None,
|
|
1397
|
+
parametrize=None,
|
|
1398
|
+
):
|
|
1399
|
+
"""Map all types of gate specification into a `Gate` object."""
|
|
1400
|
+
|
|
1401
|
+
if isinstance(gate_id, Gate):
|
|
1402
|
+
# already a gate
|
|
1403
|
+
if gate_args:
|
|
1404
|
+
raise ValueError(
|
|
1405
|
+
"You cannot specify ``gate_args`` for an already "
|
|
1406
|
+
"encapsulated `Gate` object."
|
|
1407
|
+
)
|
|
1408
|
+
|
|
1409
|
+
if any((params, qubits, controls, gate_round, parametrize)):
|
|
1410
|
+
raise ValueError(
|
|
1411
|
+
"You cannot specify ``controls`` or ``gate_round`` for an "
|
|
1412
|
+
"already encapsulated gate - supply directly to the `Gate` "
|
|
1413
|
+
"constructor instead."
|
|
1414
|
+
)
|
|
1415
|
+
return gate_id
|
|
1416
|
+
|
|
1417
|
+
if hasattr(gate_id, "shape") and not isinstance(gate_id, str):
|
|
1418
|
+
# raw gate (numpy strings have a shape - ignore those)
|
|
1419
|
+
|
|
1420
|
+
if parametrize is not None:
|
|
1421
|
+
raise ValueError(
|
|
1422
|
+
"You cannot specify ``parametrize`` for raw gate, supply a "
|
|
1423
|
+
"``PArray`` instead."
|
|
1424
|
+
)
|
|
1425
|
+
|
|
1426
|
+
return Gate.from_raw(
|
|
1427
|
+
U=gate_id,
|
|
1428
|
+
qubits=gate_args,
|
|
1429
|
+
controls=controls,
|
|
1430
|
+
round=gate_round,
|
|
1431
|
+
)
|
|
1432
|
+
|
|
1433
|
+
# else gate is specified as a tuple or kwargs
|
|
1434
|
+
|
|
1435
|
+
if isinstance(gate_id, numbers.Integral) or gate_id.isdigit():
|
|
1436
|
+
# gate round given as first entry of tuple
|
|
1437
|
+
if gate_round is None:
|
|
1438
|
+
# explicilty specified ``gate_round`` takes precedence
|
|
1439
|
+
gate_round = gate_id
|
|
1440
|
+
gate_id, gate_args = gate_args[0], gate_args[1:]
|
|
1441
|
+
|
|
1442
|
+
if parametrize is None:
|
|
1443
|
+
parametrize = False
|
|
1444
|
+
|
|
1445
|
+
if gate_args:
|
|
1446
|
+
if any((params, qubits)):
|
|
1447
|
+
raise ValueError(
|
|
1448
|
+
"You cannot specify ``params`` or ``qubits`` "
|
|
1449
|
+
"when supplying ``gate_args``."
|
|
1450
|
+
)
|
|
1451
|
+
|
|
1452
|
+
nq = GATE_SIZE[gate_id.upper()]
|
|
1453
|
+
(
|
|
1454
|
+
params,
|
|
1455
|
+
qubits,
|
|
1456
|
+
) = (
|
|
1457
|
+
gate_args[:-nq],
|
|
1458
|
+
gate_args[-nq:],
|
|
1459
|
+
)
|
|
1460
|
+
|
|
1461
|
+
else:
|
|
1462
|
+
# qubits and params specified directly
|
|
1463
|
+
if params is None:
|
|
1464
|
+
params = ()
|
|
1465
|
+
|
|
1466
|
+
return Gate(
|
|
1467
|
+
label=gate_id,
|
|
1468
|
+
params=params,
|
|
1469
|
+
qubits=qubits,
|
|
1470
|
+
controls=controls,
|
|
1471
|
+
round=gate_round,
|
|
1472
|
+
parametrize=parametrize,
|
|
1473
|
+
)
|
|
1474
|
+
|
|
1475
|
+
|
|
1476
|
+
# --------------------------- main circuit class ---------------------------- #
|
|
1477
|
+
|
|
1478
|
+
|
|
1479
|
+
class Circuit:
|
|
1480
|
+
"""Class for simulating quantum circuits using tensor networks. The class
|
|
1481
|
+
keeps a list of :class:`Gate` objects in sync with a tensor network
|
|
1482
|
+
representing the current state of the circuit.
|
|
1483
|
+
|
|
1484
|
+
Parameters
|
|
1485
|
+
----------
|
|
1486
|
+
N : int, optional
|
|
1487
|
+
The number of qubits.
|
|
1488
|
+
psi0 : TensorNetwork1DVector, optional
|
|
1489
|
+
The initial state, assumed to be ``|00000....0>`` if not given. The
|
|
1490
|
+
state is always copied and the tag ``PSI0`` added.
|
|
1491
|
+
gate_opts : dict_like, optional
|
|
1492
|
+
Default keyword arguments to supply to each
|
|
1493
|
+
:func:`~quimb.tensor.tensor_1d.gate_TN_1D` call during the circuit.
|
|
1494
|
+
gate_contract : str, optional
|
|
1495
|
+
Shortcut for setting the default `'contract'` option in `gate_opts`.
|
|
1496
|
+
gate_propagate_tags : str, optional
|
|
1497
|
+
Shortcut for setting the default `'propagate_tags'` option in
|
|
1498
|
+
`gate_opts`.
|
|
1499
|
+
tags : str or sequence of str, optional
|
|
1500
|
+
Tag(s) to add to the initial wavefunction tensors (whether these are
|
|
1501
|
+
propagated to the rest of the circuit's tensors depends on
|
|
1502
|
+
``gate_opts``).
|
|
1503
|
+
psi0_dtype : str, optional
|
|
1504
|
+
Ensure the initial state has this dtype.
|
|
1505
|
+
psi0_tag : str, optional
|
|
1506
|
+
Ensure the initial state has this tag.
|
|
1507
|
+
tag_gate_numbers : bool, optional
|
|
1508
|
+
Whether to tag each gate tensor with its number in the circuit, like
|
|
1509
|
+
``"GATE_{g}"``. This is required for updating the circuit parameters.
|
|
1510
|
+
gate_tag_id : str, optional
|
|
1511
|
+
The format string for tagging each gate tensor, by default e.g.
|
|
1512
|
+
``"GATE_{g}"``.
|
|
1513
|
+
tag_gate_rounds : bool, optional
|
|
1514
|
+
Whether to tag each gate tensor with its number in the circuit, like
|
|
1515
|
+
``"ROUND_{r}"``.
|
|
1516
|
+
round_tag_id : str, optional
|
|
1517
|
+
The format string for tagging each round of gates, by default e.g.
|
|
1518
|
+
``"ROUND_{r}"``.
|
|
1519
|
+
tag_gate_labels : bool, optional
|
|
1520
|
+
Whether to tag each gate tensor with its gate type label, e.g.
|
|
1521
|
+
``{"X_1/2", "ISWAP", "CCX", ...}``..
|
|
1522
|
+
bra_site_ind_id : str, optional
|
|
1523
|
+
Use this to label 'bra' site indices when creating certain (mostly
|
|
1524
|
+
internal) intermediate tensor networks.
|
|
1525
|
+
|
|
1526
|
+
Attributes
|
|
1527
|
+
----------
|
|
1528
|
+
psi : TensorNetwork1DVector
|
|
1529
|
+
The current circuit wavefunction as a tensor network.
|
|
1530
|
+
uni : TensorNetwork1DOperator
|
|
1531
|
+
The current circuit unitary operator as a tensor network.
|
|
1532
|
+
gates : tuple[Gate]
|
|
1533
|
+
The gates in the circuit.
|
|
1534
|
+
|
|
1535
|
+
Examples
|
|
1536
|
+
--------
|
|
1537
|
+
|
|
1538
|
+
Create 3-qubit GHZ-state:
|
|
1539
|
+
|
|
1540
|
+
>>> qc = qtn.Circuit(3)
|
|
1541
|
+
>>> gates = [
|
|
1542
|
+
('H', 0),
|
|
1543
|
+
('H', 1),
|
|
1544
|
+
('CNOT', 1, 2),
|
|
1545
|
+
('CNOT', 0, 2),
|
|
1546
|
+
('H', 0),
|
|
1547
|
+
('H', 1),
|
|
1548
|
+
('H', 2),
|
|
1549
|
+
]
|
|
1550
|
+
>>> qc.apply_gates(gates)
|
|
1551
|
+
>>> qc.psi
|
|
1552
|
+
<TensorNetwork1DVector(tensors=12, indices=14, L=3, max_bond=2)>
|
|
1553
|
+
|
|
1554
|
+
>>> qc.psi.to_dense().round(4)
|
|
1555
|
+
qarray([[ 0.7071+0.j],
|
|
1556
|
+
[ 0. +0.j],
|
|
1557
|
+
[ 0. +0.j],
|
|
1558
|
+
[-0. +0.j],
|
|
1559
|
+
[-0. +0.j],
|
|
1560
|
+
[ 0. +0.j],
|
|
1561
|
+
[ 0. +0.j],
|
|
1562
|
+
[ 0.7071+0.j]])
|
|
1563
|
+
|
|
1564
|
+
>>> for b in qc.sample(10):
|
|
1565
|
+
... print(b)
|
|
1566
|
+
000
|
|
1567
|
+
000
|
|
1568
|
+
111
|
|
1569
|
+
000
|
|
1570
|
+
111
|
|
1571
|
+
111
|
|
1572
|
+
000
|
|
1573
|
+
111
|
|
1574
|
+
000
|
|
1575
|
+
000
|
|
1576
|
+
|
|
1577
|
+
See Also
|
|
1578
|
+
--------
|
|
1579
|
+
Gate
|
|
1580
|
+
"""
|
|
1581
|
+
|
|
1582
|
+
def __init__(
|
|
1583
|
+
self,
|
|
1584
|
+
N=None,
|
|
1585
|
+
psi0=None,
|
|
1586
|
+
gate_opts=None,
|
|
1587
|
+
gate_contract="auto-split-gate",
|
|
1588
|
+
gate_propagate_tags="register",
|
|
1589
|
+
tags=None,
|
|
1590
|
+
psi0_dtype="complex128",
|
|
1591
|
+
psi0_tag="PSI0",
|
|
1592
|
+
tag_gate_numbers=True,
|
|
1593
|
+
gate_tag_id="GATE_{}",
|
|
1594
|
+
tag_gate_rounds=True,
|
|
1595
|
+
round_tag_id="ROUND_{}",
|
|
1596
|
+
tag_gate_labels=True,
|
|
1597
|
+
bra_site_ind_id="b{}",
|
|
1598
|
+
to_backend=None,
|
|
1599
|
+
):
|
|
1600
|
+
if (N is None) and (psi0 is None):
|
|
1601
|
+
raise ValueError("You must supply one of `N` or `psi0`.")
|
|
1602
|
+
|
|
1603
|
+
elif psi0 is None:
|
|
1604
|
+
self.N = N
|
|
1605
|
+
self._psi = self._init_state(N, dtype=psi0_dtype)
|
|
1606
|
+
|
|
1607
|
+
elif N is None:
|
|
1608
|
+
self._psi = psi0.copy()
|
|
1609
|
+
self.N = psi0.nsites
|
|
1610
|
+
|
|
1611
|
+
else:
|
|
1612
|
+
if N != psi0.nsites:
|
|
1613
|
+
raise ValueError("`N` doesn't match `psi0`.")
|
|
1614
|
+
self.N = N
|
|
1615
|
+
self._psi = psi0.copy()
|
|
1616
|
+
|
|
1617
|
+
self._psi.add_tag(psi0_tag)
|
|
1618
|
+
|
|
1619
|
+
if tags is not None:
|
|
1620
|
+
if isinstance(tags, str):
|
|
1621
|
+
tags = (tags,)
|
|
1622
|
+
for tag in tags:
|
|
1623
|
+
self._psi.add_tag(tag)
|
|
1624
|
+
|
|
1625
|
+
self.tag_gate_numbers = tag_gate_numbers
|
|
1626
|
+
self.tag_gate_rounds = tag_gate_rounds
|
|
1627
|
+
self.tag_gate_labels = tag_gate_labels
|
|
1628
|
+
|
|
1629
|
+
self.to_backend = to_backend
|
|
1630
|
+
if self.to_backend is not None:
|
|
1631
|
+
self._psi.apply_to_arrays(self.to_backend)
|
|
1632
|
+
self._backend_gate_cache = {}
|
|
1633
|
+
else:
|
|
1634
|
+
self._backend_gate_cache = None
|
|
1635
|
+
|
|
1636
|
+
self.gate_opts = ensure_dict(gate_opts)
|
|
1637
|
+
self.gate_opts.setdefault("contract", gate_contract)
|
|
1638
|
+
self.gate_opts.setdefault("propagate_tags", gate_propagate_tags)
|
|
1639
|
+
self._gates = []
|
|
1640
|
+
|
|
1641
|
+
self._ket_site_ind_id = self._psi.site_ind_id
|
|
1642
|
+
self._bra_site_ind_id = bra_site_ind_id
|
|
1643
|
+
self._gate_tag_id = gate_tag_id
|
|
1644
|
+
self._round_tag_id = round_tag_id
|
|
1645
|
+
|
|
1646
|
+
if self._ket_site_ind_id == self._bra_site_ind_id:
|
|
1647
|
+
raise ValueError(
|
|
1648
|
+
"The 'ket' and 'bra' site ind ids clash : "
|
|
1649
|
+
"'{}' and '{}".format(
|
|
1650
|
+
self._ket_site_ind_id, self._bra_site_ind_id
|
|
1651
|
+
)
|
|
1652
|
+
)
|
|
1653
|
+
|
|
1654
|
+
self._sample_n_gates = -1
|
|
1655
|
+
self._storage = dict()
|
|
1656
|
+
self._sampled_conditionals = dict()
|
|
1657
|
+
|
|
1658
|
+
def copy(self):
|
|
1659
|
+
"""Copy the circuit and its state."""
|
|
1660
|
+
new = object.__new__(self.__class__)
|
|
1661
|
+
new.N = self.N
|
|
1662
|
+
new._psi = self._psi.copy()
|
|
1663
|
+
new.gate_opts = tree_map(lambda x: x, self.gate_opts)
|
|
1664
|
+
new.tag_gate_numbers = self.tag_gate_numbers
|
|
1665
|
+
new.tag_gate_rounds = self.tag_gate_rounds
|
|
1666
|
+
new.tag_gate_labels = self.tag_gate_labels
|
|
1667
|
+
new.to_backend = self.to_backend
|
|
1668
|
+
new._backend_gate_cache = self._backend_gate_cache
|
|
1669
|
+
new._gates = self._gates.copy()
|
|
1670
|
+
new._ket_site_ind_id = self._ket_site_ind_id
|
|
1671
|
+
new._bra_site_ind_id = self._bra_site_ind_id
|
|
1672
|
+
new._gate_tag_id = self._gate_tag_id
|
|
1673
|
+
new._round_tag_id = self._round_tag_id
|
|
1674
|
+
new._sample_n_gates = self._sample_n_gates
|
|
1675
|
+
new._storage = self._storage.copy()
|
|
1676
|
+
new._sampled_conditionals = self._sampled_conditionals.copy()
|
|
1677
|
+
return new
|
|
1678
|
+
|
|
1679
|
+
def apply_to_arrays(self, fn):
|
|
1680
|
+
"""Apply a function to all the arrays in the circuit."""
|
|
1681
|
+
self._psi.apply_to_arrays(fn)
|
|
1682
|
+
|
|
1683
|
+
def get_params(self):
|
|
1684
|
+
"""Get a pytree - in this case a dict - of all the parameters in the
|
|
1685
|
+
circuit.
|
|
1686
|
+
|
|
1687
|
+
Returns
|
|
1688
|
+
-------
|
|
1689
|
+
dict[int, tuple]
|
|
1690
|
+
A dictionary mapping gate numbers to their parameters.
|
|
1691
|
+
"""
|
|
1692
|
+
return {
|
|
1693
|
+
i: self._psi[self.gate_tag(i)].params
|
|
1694
|
+
for i, gate in enumerate(self._gates)
|
|
1695
|
+
if gate.parametrize
|
|
1696
|
+
}
|
|
1697
|
+
|
|
1698
|
+
def set_params(self, params):
|
|
1699
|
+
"""Set the parameters of the circuit.
|
|
1700
|
+
|
|
1701
|
+
Parameters
|
|
1702
|
+
----------
|
|
1703
|
+
params : dict`
|
|
1704
|
+
A dictionary mapping gate numbers to the new parameters.
|
|
1705
|
+
"""
|
|
1706
|
+
for i, p in params.items():
|
|
1707
|
+
self._psi[self.gate_tag(i)].params = p
|
|
1708
|
+
self._gates[i] = self._gates[i].copy_with(params=ops.asarray(p))
|
|
1709
|
+
|
|
1710
|
+
self.clear_storage()
|
|
1711
|
+
|
|
1712
|
+
@classmethod
|
|
1713
|
+
def from_qsim_str(cls, contents, **circuit_opts):
|
|
1714
|
+
"""Generate a ``Circuit`` instance from a 'qsim' string."""
|
|
1715
|
+
info = parse_qsim_str(contents)
|
|
1716
|
+
qc = cls(info["n"], **circuit_opts)
|
|
1717
|
+
qc.apply_gates(info["gates"])
|
|
1718
|
+
return qc
|
|
1719
|
+
|
|
1720
|
+
@classmethod
|
|
1721
|
+
def from_qsim_file(cls, fname, **circuit_opts):
|
|
1722
|
+
"""Generate a ``Circuit`` instance from a 'qsim' file.
|
|
1723
|
+
|
|
1724
|
+
The qsim file format is described here:
|
|
1725
|
+
https://quantumai.google/qsim/input_format.
|
|
1726
|
+
"""
|
|
1727
|
+
info = parse_qsim_file(fname)
|
|
1728
|
+
qc = cls(info["n"], **circuit_opts)
|
|
1729
|
+
qc.apply_gates(info["gates"])
|
|
1730
|
+
return qc
|
|
1731
|
+
|
|
1732
|
+
@classmethod
|
|
1733
|
+
def from_qsim_url(cls, url, **circuit_opts):
|
|
1734
|
+
"""Generate a ``Circuit`` instance from a 'qsim' url."""
|
|
1735
|
+
info = parse_qsim_url(url)
|
|
1736
|
+
qc = cls(info["n"], **circuit_opts)
|
|
1737
|
+
qc.apply_gates(info["gates"])
|
|
1738
|
+
return qc
|
|
1739
|
+
|
|
1740
|
+
from_qasm = deprecated(from_qsim_str, "from_qasm", "from_qsim_str")
|
|
1741
|
+
from_qasm_file = deprecated(
|
|
1742
|
+
from_qsim_file, "from_qasm_file", "from_qsim_file"
|
|
1743
|
+
)
|
|
1744
|
+
from_qasm_url = deprecated(from_qsim_url, "from_qasm_url", "from_qsim_url")
|
|
1745
|
+
|
|
1746
|
+
@classmethod
|
|
1747
|
+
def from_openqasm2_str(cls, contents, **circuit_opts):
|
|
1748
|
+
"""Generate a ``Circuit`` instance from an OpenQASM 2.0 string."""
|
|
1749
|
+
info = parse_openqasm2_str(contents)
|
|
1750
|
+
qc = cls(info["n"], **circuit_opts)
|
|
1751
|
+
qc.apply_gates(info["gates"])
|
|
1752
|
+
return qc
|
|
1753
|
+
|
|
1754
|
+
@classmethod
|
|
1755
|
+
def from_openqasm2_file(cls, fname, **circuit_opts):
|
|
1756
|
+
"""Generate a ``Circuit`` instance from an OpenQASM 2.0 file."""
|
|
1757
|
+
info = parse_openqasm2_file(fname)
|
|
1758
|
+
qc = cls(info["n"], **circuit_opts)
|
|
1759
|
+
qc.apply_gates(info["gates"])
|
|
1760
|
+
return qc
|
|
1761
|
+
|
|
1762
|
+
@classmethod
|
|
1763
|
+
def from_openqasm2_url(cls, url, **circuit_opts):
|
|
1764
|
+
"""Generate a ``Circuit`` instance from an OpenQASM 2.0 url."""
|
|
1765
|
+
info = parse_openqasm2_url(url)
|
|
1766
|
+
qc = cls(info["n"], **circuit_opts)
|
|
1767
|
+
qc.apply_gates(info["gates"])
|
|
1768
|
+
return qc
|
|
1769
|
+
|
|
1770
|
+
@classmethod
|
|
1771
|
+
def from_gates(cls, gates, N=None, progbar=False, **kwargs):
|
|
1772
|
+
"""Generate a ``Circuit`` instance from a sequence of gates.
|
|
1773
|
+
|
|
1774
|
+
Parameters
|
|
1775
|
+
----------
|
|
1776
|
+
gates : sequence[Gate] or sequence[tuple]
|
|
1777
|
+
The sequence of gates to apply.
|
|
1778
|
+
N : int, optional
|
|
1779
|
+
The number of qubits. If not given, will be inferred from the
|
|
1780
|
+
gates.
|
|
1781
|
+
progbar : bool, optional
|
|
1782
|
+
Whether to show a progress bar.
|
|
1783
|
+
kwargs
|
|
1784
|
+
Supplied to the ``Circuit`` constructor.
|
|
1785
|
+
"""
|
|
1786
|
+
if N is None:
|
|
1787
|
+
gates = tuple(gates)
|
|
1788
|
+
|
|
1789
|
+
N = 0
|
|
1790
|
+
for gate in gates:
|
|
1791
|
+
if gate.qubits:
|
|
1792
|
+
N = max(N, max(gate.qubits) + 1)
|
|
1793
|
+
if gate.controls:
|
|
1794
|
+
N = max(N, max(gate.controls) + 1)
|
|
1795
|
+
|
|
1796
|
+
qc = cls(N, **kwargs)
|
|
1797
|
+
qc.apply_gates(gates, progbar=progbar)
|
|
1798
|
+
return qc
|
|
1799
|
+
|
|
1800
|
+
@property
|
|
1801
|
+
def gates(self):
|
|
1802
|
+
return tuple(self._gates)
|
|
1803
|
+
|
|
1804
|
+
@property
|
|
1805
|
+
def num_gates(self):
|
|
1806
|
+
return len(self._gates)
|
|
1807
|
+
|
|
1808
|
+
def ket_site_ind(self, i):
|
|
1809
|
+
"""Get the site index for the given qubit."""
|
|
1810
|
+
return self._ket_site_ind_id.format(i)
|
|
1811
|
+
|
|
1812
|
+
def bra_site_ind(self, i):
|
|
1813
|
+
"""Get the 'bra' site index for the given qubit, if forming an operator."""
|
|
1814
|
+
return self._bra_site_ind_id.format(i)
|
|
1815
|
+
|
|
1816
|
+
def gate_tag(self, g):
|
|
1817
|
+
"""Get the tag for the given gate, indexed linearly."""
|
|
1818
|
+
return self._gate_tag_id.format(g)
|
|
1819
|
+
|
|
1820
|
+
def round_tag(self, r):
|
|
1821
|
+
"""Get the tag for the given round (/layer)."""
|
|
1822
|
+
return self._round_tag_id.format(r)
|
|
1823
|
+
|
|
1824
|
+
def _init_state(self, N, dtype="complex128"):
|
|
1825
|
+
return TN_from_sites_computational_state(
|
|
1826
|
+
site_map={i: "0" for i in range(N)}, dtype=dtype
|
|
1827
|
+
)
|
|
1828
|
+
|
|
1829
|
+
def _apply_gate(self, gate, tags=None, **gate_opts):
|
|
1830
|
+
"""Apply a ``Gate`` to this ``Circuit``. This is the main method that
|
|
1831
|
+
all calls to apply a gate should go through.
|
|
1832
|
+
|
|
1833
|
+
Parameters
|
|
1834
|
+
----------
|
|
1835
|
+
gate : Gate
|
|
1836
|
+
The gate to apply.
|
|
1837
|
+
tags : str or sequence of str, optional
|
|
1838
|
+
Tags to add to the gate tensor(s).
|
|
1839
|
+
"""
|
|
1840
|
+
tags = tags_to_oset(tags)
|
|
1841
|
+
if self.tag_gate_numbers:
|
|
1842
|
+
tags.add(self.gate_tag(self.num_gates))
|
|
1843
|
+
if self.tag_gate_rounds and (gate.round is not None):
|
|
1844
|
+
tags.add(self.round_tag(gate.round))
|
|
1845
|
+
if self.tag_gate_labels and (gate.tag is not None):
|
|
1846
|
+
tags.add(gate.tag)
|
|
1847
|
+
|
|
1848
|
+
# overide any default gate opts
|
|
1849
|
+
opts = {**self.gate_opts, **gate_opts}
|
|
1850
|
+
|
|
1851
|
+
if gate.controls:
|
|
1852
|
+
# handle extra (low-rank) control structure
|
|
1853
|
+
apply_controlled_gate(self._psi, gate, tags=tags, **opts)
|
|
1854
|
+
|
|
1855
|
+
elif gate.special:
|
|
1856
|
+
# these are specified as a general function
|
|
1857
|
+
SPECIAL_GATES[gate.label](
|
|
1858
|
+
self._psi, *gate.params, *gate.qubits, **opts
|
|
1859
|
+
)
|
|
1860
|
+
|
|
1861
|
+
else:
|
|
1862
|
+
# gate supplied as a matrix/tensor
|
|
1863
|
+
G = gate.array
|
|
1864
|
+
if self.to_backend is not None:
|
|
1865
|
+
key = id(G)
|
|
1866
|
+
if key not in self._backend_gate_cache:
|
|
1867
|
+
self._backend_gate_cache[key] = self.to_backend(G)
|
|
1868
|
+
G = self._backend_gate_cache[key]
|
|
1869
|
+
|
|
1870
|
+
# apply the gate to the TN!
|
|
1871
|
+
self._psi.gate_(G, gate.qubits, tags=tags, **opts)
|
|
1872
|
+
|
|
1873
|
+
# keep track of the gates applied
|
|
1874
|
+
self._gates.append(gate)
|
|
1875
|
+
|
|
1876
|
+
def apply_gate(
|
|
1877
|
+
self,
|
|
1878
|
+
gate_id,
|
|
1879
|
+
*gate_args,
|
|
1880
|
+
params=None,
|
|
1881
|
+
qubits=None,
|
|
1882
|
+
controls=None,
|
|
1883
|
+
gate_round=None,
|
|
1884
|
+
parametrize=None,
|
|
1885
|
+
**gate_opts,
|
|
1886
|
+
):
|
|
1887
|
+
"""Apply a single gate to this tensor network quantum circuit. If
|
|
1888
|
+
``gate_round`` is supplied the tensor(s) added will be tagged with
|
|
1889
|
+
``'ROUND_{gate_round}'``. Alternatively, putting an integer first like
|
|
1890
|
+
so::
|
|
1891
|
+
|
|
1892
|
+
circuit.apply_gate(10, 'H', 7)
|
|
1893
|
+
|
|
1894
|
+
Is automatically translated to::
|
|
1895
|
+
|
|
1896
|
+
circuit.apply_gate('H', 7, gate_round=10)
|
|
1897
|
+
|
|
1898
|
+
Parameters
|
|
1899
|
+
----------
|
|
1900
|
+
gate_id : Gate, str, or array_like
|
|
1901
|
+
Which gate to apply. This can be:
|
|
1902
|
+
|
|
1903
|
+
- A ``Gate`` instance, i.e. with parameters and qubits already
|
|
1904
|
+
specified.
|
|
1905
|
+
- A string, e.g. ``'H'``, ``'U3'``, etc. in which case
|
|
1906
|
+
``gate_args`` should be supplied with ``(*params, *qubits)``.
|
|
1907
|
+
- A raw array, in which case ``gate_args`` should be supplied
|
|
1908
|
+
with ``(*qubits,)``.
|
|
1909
|
+
|
|
1910
|
+
gate_args : list[str]
|
|
1911
|
+
The arguments to supply to it.
|
|
1912
|
+
gate_round : int, optional
|
|
1913
|
+
The gate round. If ``gate_id`` is integer-like, will also be taken
|
|
1914
|
+
from here, with then ``gate_id, gate_args = gate_args[0],
|
|
1915
|
+
gate_args[1:]``.
|
|
1916
|
+
gate_opts
|
|
1917
|
+
Supplied to the gate function, options here will override the
|
|
1918
|
+
default ``gate_opts``.
|
|
1919
|
+
"""
|
|
1920
|
+
gate = parse_to_gate(
|
|
1921
|
+
gate_id,
|
|
1922
|
+
*gate_args,
|
|
1923
|
+
params=params,
|
|
1924
|
+
qubits=qubits,
|
|
1925
|
+
controls=controls,
|
|
1926
|
+
gate_round=gate_round,
|
|
1927
|
+
parametrize=parametrize,
|
|
1928
|
+
)
|
|
1929
|
+
self._apply_gate(gate, **gate_opts)
|
|
1930
|
+
|
|
1931
|
+
def apply_gate_raw(
|
|
1932
|
+
self, U, where, controls=None, gate_round=None, **gate_opts
|
|
1933
|
+
):
|
|
1934
|
+
"""Apply the raw array ``U`` as a gate on qubits in ``where``. It will
|
|
1935
|
+
be assumed to be unitary for the sake of computing reverse lightcones.
|
|
1936
|
+
"""
|
|
1937
|
+
gate = Gate.from_raw(U, where, controls=controls, round=gate_round)
|
|
1938
|
+
self._apply_gate(gate, **gate_opts)
|
|
1939
|
+
|
|
1940
|
+
def apply_gates(self, gates, progbar=False, **gate_opts):
|
|
1941
|
+
"""Apply a sequence of gates to this tensor network quantum circuit.
|
|
1942
|
+
|
|
1943
|
+
Parameters
|
|
1944
|
+
----------
|
|
1945
|
+
gates : Sequence[Gate] or Sequence[Tuple]
|
|
1946
|
+
The sequence of gates to apply.
|
|
1947
|
+
gate_opts
|
|
1948
|
+
Supplied to :meth:`~quimb.tensor.circuit.Circuit.apply_gate`.
|
|
1949
|
+
"""
|
|
1950
|
+
if progbar:
|
|
1951
|
+
from ..utils import progbar as _progbar
|
|
1952
|
+
|
|
1953
|
+
gates = _progbar(gates)
|
|
1954
|
+
|
|
1955
|
+
for gate in gates:
|
|
1956
|
+
if isinstance(gate, Gate):
|
|
1957
|
+
self._apply_gate(gate, **gate_opts)
|
|
1958
|
+
else:
|
|
1959
|
+
self.apply_gate(*gate, **gate_opts)
|
|
1960
|
+
|
|
1961
|
+
self._psi.squeeze_()
|
|
1962
|
+
|
|
1963
|
+
def h(self, i, gate_round=None, **kwargs):
|
|
1964
|
+
self.apply_gate("H", i, gate_round=gate_round, **kwargs)
|
|
1965
|
+
|
|
1966
|
+
def x(self, i, gate_round=None, **kwargs):
|
|
1967
|
+
self.apply_gate("X", i, gate_round=gate_round, **kwargs)
|
|
1968
|
+
|
|
1969
|
+
def y(self, i, gate_round=None, **kwargs):
|
|
1970
|
+
self.apply_gate("Y", i, gate_round=gate_round, **kwargs)
|
|
1971
|
+
|
|
1972
|
+
def z(self, i, gate_round=None, **kwargs):
|
|
1973
|
+
self.apply_gate("Z", i, gate_round=gate_round, **kwargs)
|
|
1974
|
+
|
|
1975
|
+
def s(self, i, gate_round=None, **kwargs):
|
|
1976
|
+
self.apply_gate("S", i, gate_round=gate_round, **kwargs)
|
|
1977
|
+
|
|
1978
|
+
def sdg(self, i, gate_round=None, **kwargs):
|
|
1979
|
+
self.apply_gate("SDG", i, gate_round=gate_round, **kwargs)
|
|
1980
|
+
|
|
1981
|
+
def t(self, i, gate_round=None, **kwargs):
|
|
1982
|
+
self.apply_gate("T", i, gate_round=gate_round, **kwargs)
|
|
1983
|
+
|
|
1984
|
+
def tdg(self, i, gate_round=None, **kwargs):
|
|
1985
|
+
self.apply_gate("TDG", i, gate_round=gate_round, **kwargs)
|
|
1986
|
+
|
|
1987
|
+
def x_1_2(self, i, gate_round=None, **kwargs):
|
|
1988
|
+
self.apply_gate("X_1_2", i, gate_round=gate_round, **kwargs)
|
|
1989
|
+
|
|
1990
|
+
def y_1_2(self, i, gate_round=None, **kwargs):
|
|
1991
|
+
self.apply_gate("Y_1_2", i, gate_round=gate_round, **kwargs)
|
|
1992
|
+
|
|
1993
|
+
def z_1_2(self, i, gate_round=None, **kwargs):
|
|
1994
|
+
self.apply_gate("Z_1_2", i, gate_round=gate_round, **kwargs)
|
|
1995
|
+
|
|
1996
|
+
def w_1_2(self, i, gate_round=None, **kwargs):
|
|
1997
|
+
self.apply_gate("W_1_2", i, gate_round=gate_round, **kwargs)
|
|
1998
|
+
|
|
1999
|
+
def hz_1_2(self, i, gate_round=None, **kwargs):
|
|
2000
|
+
self.apply_gate("HZ_1_2", i, gate_round=gate_round, **kwargs)
|
|
2001
|
+
|
|
2002
|
+
# constant two qubit gates
|
|
2003
|
+
|
|
2004
|
+
def cnot(self, i, j, gate_round=None, **kwargs):
|
|
2005
|
+
self.apply_gate("CNOT", i, j, gate_round=gate_round, **kwargs)
|
|
2006
|
+
|
|
2007
|
+
def cx(self, i, j, gate_round=None, **kwargs):
|
|
2008
|
+
self.apply_gate("CX", i, j, gate_round=gate_round, **kwargs)
|
|
2009
|
+
|
|
2010
|
+
def cy(self, i, j, gate_round=None, **kwargs):
|
|
2011
|
+
self.apply_gate("CY", i, j, gate_round=gate_round, **kwargs)
|
|
2012
|
+
|
|
2013
|
+
def cz(self, i, j, gate_round=None, **kwargs):
|
|
2014
|
+
self.apply_gate("CZ", i, j, gate_round=gate_round, **kwargs)
|
|
2015
|
+
|
|
2016
|
+
def iswap(self, i, j, gate_round=None, **kwargs):
|
|
2017
|
+
self.apply_gate("ISWAP", i, j, **kwargs)
|
|
2018
|
+
|
|
2019
|
+
# special non-tensor gates
|
|
2020
|
+
|
|
2021
|
+
def iden(self, i, gate_round=None):
|
|
2022
|
+
pass
|
|
2023
|
+
|
|
2024
|
+
def swap(self, i, j, gate_round=None, **kwargs):
|
|
2025
|
+
self.apply_gate("SWAP", i, j, **kwargs)
|
|
2026
|
+
|
|
2027
|
+
# parametrizable gates
|
|
2028
|
+
|
|
2029
|
+
def rx(self, theta, i, gate_round=None, parametrize=False, **kwargs):
|
|
2030
|
+
self.apply_gate(
|
|
2031
|
+
"RX",
|
|
2032
|
+
theta,
|
|
2033
|
+
i,
|
|
2034
|
+
gate_round=gate_round,
|
|
2035
|
+
parametrize=parametrize,
|
|
2036
|
+
**kwargs,
|
|
2037
|
+
)
|
|
2038
|
+
|
|
2039
|
+
def ry(self, theta, i, gate_round=None, parametrize=False, **kwargs):
|
|
2040
|
+
self.apply_gate(
|
|
2041
|
+
"RY",
|
|
2042
|
+
theta,
|
|
2043
|
+
i,
|
|
2044
|
+
gate_round=gate_round,
|
|
2045
|
+
parametrize=parametrize,
|
|
2046
|
+
**kwargs,
|
|
2047
|
+
)
|
|
2048
|
+
|
|
2049
|
+
def rz(self, theta, i, gate_round=None, parametrize=False, **kwargs):
|
|
2050
|
+
self.apply_gate(
|
|
2051
|
+
"RZ",
|
|
2052
|
+
theta,
|
|
2053
|
+
i,
|
|
2054
|
+
gate_round=gate_round,
|
|
2055
|
+
parametrize=parametrize,
|
|
2056
|
+
**kwargs,
|
|
2057
|
+
)
|
|
2058
|
+
|
|
2059
|
+
def u3(
|
|
2060
|
+
self,
|
|
2061
|
+
theta,
|
|
2062
|
+
phi,
|
|
2063
|
+
lamda,
|
|
2064
|
+
i,
|
|
2065
|
+
gate_round=None,
|
|
2066
|
+
parametrize=False,
|
|
2067
|
+
**kwargs,
|
|
2068
|
+
):
|
|
2069
|
+
self.apply_gate(
|
|
2070
|
+
"U3",
|
|
2071
|
+
theta,
|
|
2072
|
+
phi,
|
|
2073
|
+
lamda,
|
|
2074
|
+
i,
|
|
2075
|
+
gate_round=gate_round,
|
|
2076
|
+
parametrize=parametrize,
|
|
2077
|
+
**kwargs,
|
|
2078
|
+
)
|
|
2079
|
+
|
|
2080
|
+
def u2(self, phi, lamda, i, gate_round=None, parametrize=False, **kwargs):
|
|
2081
|
+
self.apply_gate(
|
|
2082
|
+
"U2",
|
|
2083
|
+
phi,
|
|
2084
|
+
lamda,
|
|
2085
|
+
i,
|
|
2086
|
+
gate_round=gate_round,
|
|
2087
|
+
parametrize=parametrize,
|
|
2088
|
+
**kwargs,
|
|
2089
|
+
)
|
|
2090
|
+
|
|
2091
|
+
def u1(self, lamda, i, gate_round=None, parametrize=False, **kwargs):
|
|
2092
|
+
self.apply_gate(
|
|
2093
|
+
"U1",
|
|
2094
|
+
lamda,
|
|
2095
|
+
i,
|
|
2096
|
+
gate_round=gate_round,
|
|
2097
|
+
parametrize=parametrize,
|
|
2098
|
+
**kwargs,
|
|
2099
|
+
)
|
|
2100
|
+
|
|
2101
|
+
def phase(self, lamda, i, gate_round=None, parametrize=False, **kwargs):
|
|
2102
|
+
self.apply_gate(
|
|
2103
|
+
"PHASE",
|
|
2104
|
+
lamda,
|
|
2105
|
+
i,
|
|
2106
|
+
gate_round=gate_round,
|
|
2107
|
+
parametrize=parametrize,
|
|
2108
|
+
**kwargs,
|
|
2109
|
+
)
|
|
2110
|
+
|
|
2111
|
+
def cu3(
|
|
2112
|
+
self,
|
|
2113
|
+
theta,
|
|
2114
|
+
phi,
|
|
2115
|
+
lamda,
|
|
2116
|
+
i,
|
|
2117
|
+
j,
|
|
2118
|
+
gate_round=None,
|
|
2119
|
+
parametrize=False,
|
|
2120
|
+
**kwargs,
|
|
2121
|
+
):
|
|
2122
|
+
self.apply_gate(
|
|
2123
|
+
"CU3",
|
|
2124
|
+
theta,
|
|
2125
|
+
phi,
|
|
2126
|
+
lamda,
|
|
2127
|
+
i,
|
|
2128
|
+
j,
|
|
2129
|
+
gate_round=gate_round,
|
|
2130
|
+
parametrize=parametrize,
|
|
2131
|
+
**kwargs,
|
|
2132
|
+
)
|
|
2133
|
+
|
|
2134
|
+
def cu2(
|
|
2135
|
+
self, phi, lamda, i, j, gate_round=None, parametrize=False, **kwargs
|
|
2136
|
+
):
|
|
2137
|
+
self.apply_gate(
|
|
2138
|
+
"CU2",
|
|
2139
|
+
phi,
|
|
2140
|
+
lamda,
|
|
2141
|
+
i,
|
|
2142
|
+
j,
|
|
2143
|
+
gate_round=gate_round,
|
|
2144
|
+
parametrize=parametrize,
|
|
2145
|
+
**kwargs,
|
|
2146
|
+
)
|
|
2147
|
+
|
|
2148
|
+
def cu1(self, lamda, i, j, gate_round=None, parametrize=False, **kwargs):
|
|
2149
|
+
self.apply_gate(
|
|
2150
|
+
"CU1",
|
|
2151
|
+
lamda,
|
|
2152
|
+
i,
|
|
2153
|
+
j,
|
|
2154
|
+
gate_round=gate_round,
|
|
2155
|
+
parametrize=parametrize,
|
|
2156
|
+
**kwargs,
|
|
2157
|
+
)
|
|
2158
|
+
|
|
2159
|
+
def cphase(
|
|
2160
|
+
self, lamda, i, j, gate_round=None, parametrize=False, **kwargs
|
|
2161
|
+
):
|
|
2162
|
+
self.apply_gate(
|
|
2163
|
+
"CPHASE",
|
|
2164
|
+
lamda,
|
|
2165
|
+
i,
|
|
2166
|
+
j,
|
|
2167
|
+
gate_round=gate_round,
|
|
2168
|
+
parametrize=parametrize,
|
|
2169
|
+
**kwargs,
|
|
2170
|
+
)
|
|
2171
|
+
|
|
2172
|
+
def fsim(
|
|
2173
|
+
self, theta, phi, i, j, gate_round=None, parametrize=False, **kwargs
|
|
2174
|
+
):
|
|
2175
|
+
self.apply_gate(
|
|
2176
|
+
"FSIM",
|
|
2177
|
+
theta,
|
|
2178
|
+
phi,
|
|
2179
|
+
i,
|
|
2180
|
+
j,
|
|
2181
|
+
gate_round=gate_round,
|
|
2182
|
+
parametrize=parametrize,
|
|
2183
|
+
**kwargs,
|
|
2184
|
+
)
|
|
2185
|
+
|
|
2186
|
+
def fsimg(
|
|
2187
|
+
self,
|
|
2188
|
+
theta,
|
|
2189
|
+
zeta,
|
|
2190
|
+
chi,
|
|
2191
|
+
gamma,
|
|
2192
|
+
phi,
|
|
2193
|
+
i,
|
|
2194
|
+
j,
|
|
2195
|
+
gate_round=None,
|
|
2196
|
+
parametrize=False,
|
|
2197
|
+
**kwargs,
|
|
2198
|
+
):
|
|
2199
|
+
self.apply_gate(
|
|
2200
|
+
"FSIMG",
|
|
2201
|
+
theta,
|
|
2202
|
+
zeta,
|
|
2203
|
+
chi,
|
|
2204
|
+
gamma,
|
|
2205
|
+
phi,
|
|
2206
|
+
i,
|
|
2207
|
+
j,
|
|
2208
|
+
gate_round=gate_round,
|
|
2209
|
+
parametrize=parametrize,
|
|
2210
|
+
**kwargs,
|
|
2211
|
+
)
|
|
2212
|
+
|
|
2213
|
+
def givens(
|
|
2214
|
+
self, theta, i, j, gate_round=None, parametrize=False, **kwargs
|
|
2215
|
+
):
|
|
2216
|
+
self.apply_gate(
|
|
2217
|
+
"GIVENS",
|
|
2218
|
+
theta,
|
|
2219
|
+
i,
|
|
2220
|
+
j,
|
|
2221
|
+
gate_round=gate_round,
|
|
2222
|
+
parametrize=parametrize,
|
|
2223
|
+
**kwargs,
|
|
2224
|
+
)
|
|
2225
|
+
|
|
2226
|
+
def givens2(
|
|
2227
|
+
self, theta, phi, i, j, gate_round=None, parametrize=False, **kwargs
|
|
2228
|
+
):
|
|
2229
|
+
self.apply_gate(
|
|
2230
|
+
"GIVENS2",
|
|
2231
|
+
theta,
|
|
2232
|
+
phi,
|
|
2233
|
+
i,
|
|
2234
|
+
j,
|
|
2235
|
+
gate_round=gate_round,
|
|
2236
|
+
parametrize=parametrize,
|
|
2237
|
+
**kwargs,
|
|
2238
|
+
)
|
|
2239
|
+
|
|
2240
|
+
def rxx(self, theta, i, j, gate_round=None, parametrize=False, **kwargs):
|
|
2241
|
+
self.apply_gate(
|
|
2242
|
+
"RXX",
|
|
2243
|
+
theta,
|
|
2244
|
+
i,
|
|
2245
|
+
j,
|
|
2246
|
+
gate_round=gate_round,
|
|
2247
|
+
parametrize=parametrize,
|
|
2248
|
+
**kwargs,
|
|
2249
|
+
)
|
|
2250
|
+
|
|
2251
|
+
def ryy(self, theta, i, j, gate_round=None, parametrize=False, **kwargs):
|
|
2252
|
+
self.apply_gate(
|
|
2253
|
+
"RYY",
|
|
2254
|
+
theta,
|
|
2255
|
+
i,
|
|
2256
|
+
j,
|
|
2257
|
+
gate_round=gate_round,
|
|
2258
|
+
parametrize=parametrize,
|
|
2259
|
+
**kwargs,
|
|
2260
|
+
)
|
|
2261
|
+
|
|
2262
|
+
def rzz(self, theta, i, j, gate_round=None, parametrize=False, **kwargs):
|
|
2263
|
+
self.apply_gate(
|
|
2264
|
+
"RZZ",
|
|
2265
|
+
theta,
|
|
2266
|
+
i,
|
|
2267
|
+
j,
|
|
2268
|
+
gate_round=gate_round,
|
|
2269
|
+
parametrize=parametrize,
|
|
2270
|
+
**kwargs,
|
|
2271
|
+
)
|
|
2272
|
+
|
|
2273
|
+
def crx(self, theta, i, j, gate_round=None, parametrize=False, **kwargs):
|
|
2274
|
+
self.apply_gate(
|
|
2275
|
+
"CRX",
|
|
2276
|
+
theta,
|
|
2277
|
+
i,
|
|
2278
|
+
j,
|
|
2279
|
+
gate_round=gate_round,
|
|
2280
|
+
parametrize=parametrize,
|
|
2281
|
+
**kwargs,
|
|
2282
|
+
)
|
|
2283
|
+
|
|
2284
|
+
def cry(self, theta, i, j, gate_round=None, parametrize=False, **kwargs):
|
|
2285
|
+
self.apply_gate(
|
|
2286
|
+
"CRY",
|
|
2287
|
+
theta,
|
|
2288
|
+
i,
|
|
2289
|
+
j,
|
|
2290
|
+
gate_round=gate_round,
|
|
2291
|
+
parametrize=parametrize,
|
|
2292
|
+
**kwargs,
|
|
2293
|
+
)
|
|
2294
|
+
|
|
2295
|
+
def crz(self, theta, i, j, gate_round=None, parametrize=False, **kwargs):
|
|
2296
|
+
self.apply_gate(
|
|
2297
|
+
"CRZ",
|
|
2298
|
+
theta,
|
|
2299
|
+
i,
|
|
2300
|
+
j,
|
|
2301
|
+
gate_round=gate_round,
|
|
2302
|
+
parametrize=parametrize,
|
|
2303
|
+
**kwargs,
|
|
2304
|
+
)
|
|
2305
|
+
|
|
2306
|
+
def su4(
|
|
2307
|
+
self,
|
|
2308
|
+
theta1,
|
|
2309
|
+
phi1,
|
|
2310
|
+
lamda1,
|
|
2311
|
+
theta2,
|
|
2312
|
+
phi2,
|
|
2313
|
+
lamda2,
|
|
2314
|
+
theta3,
|
|
2315
|
+
phi3,
|
|
2316
|
+
lamda3,
|
|
2317
|
+
theta4,
|
|
2318
|
+
phi4,
|
|
2319
|
+
lamda4,
|
|
2320
|
+
t1,
|
|
2321
|
+
t2,
|
|
2322
|
+
t3,
|
|
2323
|
+
i,
|
|
2324
|
+
j,
|
|
2325
|
+
gate_round=None,
|
|
2326
|
+
parametrize=False,
|
|
2327
|
+
**kwargs,
|
|
2328
|
+
):
|
|
2329
|
+
self.apply_gate(
|
|
2330
|
+
"SU4",
|
|
2331
|
+
theta1,
|
|
2332
|
+
phi1,
|
|
2333
|
+
lamda1,
|
|
2334
|
+
theta2,
|
|
2335
|
+
phi2,
|
|
2336
|
+
lamda2,
|
|
2337
|
+
theta3,
|
|
2338
|
+
phi3,
|
|
2339
|
+
lamda3,
|
|
2340
|
+
theta4,
|
|
2341
|
+
phi4,
|
|
2342
|
+
lamda4,
|
|
2343
|
+
t1,
|
|
2344
|
+
t2,
|
|
2345
|
+
t3,
|
|
2346
|
+
i,
|
|
2347
|
+
j,
|
|
2348
|
+
gate_round=gate_round,
|
|
2349
|
+
parametrize=parametrize,
|
|
2350
|
+
**kwargs,
|
|
2351
|
+
)
|
|
2352
|
+
|
|
2353
|
+
def ccx(self, i, j, k, gate_round=None, **kwargs):
|
|
2354
|
+
self.apply_gate("CCX", i, j, k, gate_round=gate_round, **kwargs)
|
|
2355
|
+
|
|
2356
|
+
def ccnot(self, i, j, k, gate_round=None, **kwargs):
|
|
2357
|
+
self.apply_gate("CCNOT", i, j, k, gate_round=gate_round, **kwargs)
|
|
2358
|
+
|
|
2359
|
+
def toffoli(self, i, j, k, gate_round=None, **kwargs):
|
|
2360
|
+
self.apply_gate("TOFFOLI", i, j, k, gate_round=gate_round, **kwargs)
|
|
2361
|
+
|
|
2362
|
+
def ccy(self, i, j, k, gate_round=None, **kwargs):
|
|
2363
|
+
self.apply_gate("CCY", i, j, k, gate_round=gate_round, **kwargs)
|
|
2364
|
+
|
|
2365
|
+
def ccz(self, i, j, k, gate_round=None, **kwargs):
|
|
2366
|
+
self.apply_gate("CCZ", i, j, k, gate_round=gate_round, **kwargs)
|
|
2367
|
+
|
|
2368
|
+
def cswap(self, i, j, k, gate_round=None, **kwargs):
|
|
2369
|
+
self.apply_gate("CSWAP", i, j, k, gate_round=gate_round, **kwargs)
|
|
2370
|
+
|
|
2371
|
+
def fredkin(self, i, j, k, gate_round=None, **kwargs):
|
|
2372
|
+
self.apply_gate("FREDKIN", i, j, k, gate_round=gate_round, **kwargs)
|
|
2373
|
+
|
|
2374
|
+
@property
|
|
2375
|
+
def psi(self):
|
|
2376
|
+
"""Tensor network representation of the wavefunction."""
|
|
2377
|
+
# make sure all same dtype and drop singlet dimensions
|
|
2378
|
+
psi = self._psi.copy()
|
|
2379
|
+
psi.squeeze_()
|
|
2380
|
+
psi.astype_(psi.dtype)
|
|
2381
|
+
return psi
|
|
2382
|
+
|
|
2383
|
+
def get_uni(self, transposed=False):
|
|
2384
|
+
"""Tensor network representation of the unitary operator (i.e. with
|
|
2385
|
+
the initial state removed).
|
|
2386
|
+
"""
|
|
2387
|
+
U = self.psi
|
|
2388
|
+
|
|
2389
|
+
if transposed:
|
|
2390
|
+
# rename the initial state rand_uuid bonds to 1D site inds
|
|
2391
|
+
ixmap = {
|
|
2392
|
+
self.ket_site_ind(i): self.bra_site_ind(i)
|
|
2393
|
+
for i in range(self.N)
|
|
2394
|
+
}
|
|
2395
|
+
else:
|
|
2396
|
+
ixmap = {}
|
|
2397
|
+
|
|
2398
|
+
# the first `N` tensors should be the tensors of input state
|
|
2399
|
+
tids = tuple(U.tensor_map)[: self.N]
|
|
2400
|
+
for i, tid in enumerate(tids):
|
|
2401
|
+
t = U.pop_tensor(tid)
|
|
2402
|
+
(old_ix,) = t.inds
|
|
2403
|
+
|
|
2404
|
+
if transposed:
|
|
2405
|
+
ixmap[old_ix] = f"k{i}"
|
|
2406
|
+
else:
|
|
2407
|
+
ixmap[old_ix] = f"b{i}"
|
|
2408
|
+
|
|
2409
|
+
U.reindex_(ixmap)
|
|
2410
|
+
U.view_as_(
|
|
2411
|
+
TensorNetworkGenOperator,
|
|
2412
|
+
upper_ind_id=self._ket_site_ind_id,
|
|
2413
|
+
lower_ind_id=self._bra_site_ind_id,
|
|
2414
|
+
)
|
|
2415
|
+
|
|
2416
|
+
return U
|
|
2417
|
+
|
|
2418
|
+
@property
|
|
2419
|
+
def uni(self):
|
|
2420
|
+
import warnings
|
|
2421
|
+
|
|
2422
|
+
warnings.warn(
|
|
2423
|
+
"In future the tensor network returned by ``circ.uni`` will not "
|
|
2424
|
+
"be transposed as it is currently, to match the expectation from "
|
|
2425
|
+
"``U = circ.uni.to_dense()`` behaving like ``U @ psi``. You can "
|
|
2426
|
+
"retain this behaviour with ``circ.get_uni(transposed=True)``.",
|
|
2427
|
+
FutureWarning,
|
|
2428
|
+
)
|
|
2429
|
+
return self.get_uni(transposed=True)
|
|
2430
|
+
|
|
2431
|
+
def get_reverse_lightcone_tags(self, where):
|
|
2432
|
+
"""Get the tags of gates in this circuit corresponding to the 'reverse'
|
|
2433
|
+
lightcone propagating backwards from registers in ``where``.
|
|
2434
|
+
|
|
2435
|
+
Parameters
|
|
2436
|
+
----------
|
|
2437
|
+
where : int or sequence of int
|
|
2438
|
+
The register or register to get the reverse lightcone of.
|
|
2439
|
+
|
|
2440
|
+
Returns
|
|
2441
|
+
-------
|
|
2442
|
+
tuple[str]
|
|
2443
|
+
The sequence of gate tags (``GATE_{i}``, ...) corresponding to the
|
|
2444
|
+
lightcone.
|
|
2445
|
+
"""
|
|
2446
|
+
if isinstance(where, numbers.Integral):
|
|
2447
|
+
cone = {where}
|
|
2448
|
+
else:
|
|
2449
|
+
cone = set(where)
|
|
2450
|
+
|
|
2451
|
+
lightcone_tags = []
|
|
2452
|
+
|
|
2453
|
+
for i, gate in reversed(tuple(enumerate(self._gates))):
|
|
2454
|
+
if gate.label == "IDEN":
|
|
2455
|
+
continue
|
|
2456
|
+
elif gate.controls:
|
|
2457
|
+
# TODO: only add if any *targets* in cone, requires changes
|
|
2458
|
+
# elsewhere to make sure tensors aren't then missing
|
|
2459
|
+
regs = {*gate.controls, *gate.qubits}
|
|
2460
|
+
if regs & cone:
|
|
2461
|
+
lightcone_tags.append(self.gate_tag(i))
|
|
2462
|
+
cone |= regs
|
|
2463
|
+
elif gate.label == "SWAP":
|
|
2464
|
+
i, j = gate.qubits
|
|
2465
|
+
i_in_cone = i in cone
|
|
2466
|
+
j_in_cone = j in cone
|
|
2467
|
+
if i_in_cone:
|
|
2468
|
+
cone.add(j)
|
|
2469
|
+
else:
|
|
2470
|
+
cone.discard(j)
|
|
2471
|
+
if j_in_cone:
|
|
2472
|
+
cone.add(i)
|
|
2473
|
+
else:
|
|
2474
|
+
cone.discard(i)
|
|
2475
|
+
else:
|
|
2476
|
+
regs = set(gate.qubits)
|
|
2477
|
+
if regs & cone:
|
|
2478
|
+
lightcone_tags.append(self.gate_tag(i))
|
|
2479
|
+
cone |= regs
|
|
2480
|
+
|
|
2481
|
+
# initial state is always part of the lightcone
|
|
2482
|
+
lightcone_tags.append("PSI0")
|
|
2483
|
+
lightcone_tags.reverse()
|
|
2484
|
+
|
|
2485
|
+
return tuple(lightcone_tags)
|
|
2486
|
+
|
|
2487
|
+
def get_psi_reverse_lightcone(self, where, keep_psi0=False):
|
|
2488
|
+
"""Get just the bit of the wavefunction in the reverse lightcone of
|
|
2489
|
+
sites in ``where`` - i.e. causally linked.
|
|
2490
|
+
|
|
2491
|
+
Parameters
|
|
2492
|
+
----------
|
|
2493
|
+
where : int, or sequence of int
|
|
2494
|
+
The sites to propagate the the lightcone back from, supplied to
|
|
2495
|
+
:meth:`~quimb.tensor.circuit.Circuit.get_reverse_lightcone_tags`.
|
|
2496
|
+
keep_psi0 : bool, optional
|
|
2497
|
+
Keep the tensors corresponding to the initial wavefunction
|
|
2498
|
+
regardless of whether they are outside of the lightcone.
|
|
2499
|
+
|
|
2500
|
+
Returns
|
|
2501
|
+
-------
|
|
2502
|
+
psi_lc : TensorNetwork1DVector
|
|
2503
|
+
"""
|
|
2504
|
+
if isinstance(where, numbers.Integral):
|
|
2505
|
+
where = (where,)
|
|
2506
|
+
|
|
2507
|
+
psi = self.psi
|
|
2508
|
+
lightcone_tags = self.get_reverse_lightcone_tags(where)
|
|
2509
|
+
psi_lc = psi.select_any(lightcone_tags).view_like_(psi)
|
|
2510
|
+
|
|
2511
|
+
if not keep_psi0:
|
|
2512
|
+
# these sites are in the lightcone regardless of being alone
|
|
2513
|
+
site_inds = set(map(psi.site_ind, where))
|
|
2514
|
+
|
|
2515
|
+
for tid, t in tuple(psi_lc.tensor_map.items()):
|
|
2516
|
+
# get all tensors connected to this tensor (incld itself)
|
|
2517
|
+
neighbors = oset_union(psi_lc.ind_map[ix] for ix in t.inds)
|
|
2518
|
+
|
|
2519
|
+
# lone tensor not attached to anything - drop it
|
|
2520
|
+
# but only if it isn't directly in the ``where`` region
|
|
2521
|
+
if (len(neighbors) == 1) and set(t.inds).isdisjoint(site_inds):
|
|
2522
|
+
psi_lc.pop_tensor(tid)
|
|
2523
|
+
|
|
2524
|
+
return psi_lc
|
|
2525
|
+
|
|
2526
|
+
def clear_storage(self):
|
|
2527
|
+
"""Clear all cached data."""
|
|
2528
|
+
self._storage.clear()
|
|
2529
|
+
self._sampled_conditionals.clear()
|
|
2530
|
+
self._marginal_storage_size = 0
|
|
2531
|
+
self._sample_n_gates = self.num_gates
|
|
2532
|
+
|
|
2533
|
+
def _maybe_init_storage(self):
|
|
2534
|
+
# clear/create the cache if circuit has changed
|
|
2535
|
+
if self._sample_n_gates != self.num_gates:
|
|
2536
|
+
self.clear_storage()
|
|
2537
|
+
|
|
2538
|
+
def get_psi_simplified(
|
|
2539
|
+
self, seq="ADCRS", atol=1e-12, equalize_norms=False
|
|
2540
|
+
):
|
|
2541
|
+
"""Get the full wavefunction post local tensor network simplification.
|
|
2542
|
+
|
|
2543
|
+
Parameters
|
|
2544
|
+
----------
|
|
2545
|
+
seq : str, optional
|
|
2546
|
+
Which local tensor network simplifications to perform and in which
|
|
2547
|
+
order, see
|
|
2548
|
+
:meth:`~quimb.tensor.tensor_core.TensorNetwork.full_simplify`.
|
|
2549
|
+
atol : float, optional
|
|
2550
|
+
The tolerance with which to compare to zero when applying
|
|
2551
|
+
:meth:`~quimb.tensor.tensor_core.TensorNetwork.full_simplify`.
|
|
2552
|
+
equalize_norms : bool, optional
|
|
2553
|
+
Actively renormalize tensor norms during simplification.
|
|
2554
|
+
|
|
2555
|
+
Returns
|
|
2556
|
+
-------
|
|
2557
|
+
psi : TensorNetwork1DVector
|
|
2558
|
+
"""
|
|
2559
|
+
self._maybe_init_storage()
|
|
2560
|
+
|
|
2561
|
+
key = ("psi_simplified", seq, atol)
|
|
2562
|
+
if key in self._storage:
|
|
2563
|
+
return self._storage[key].copy()
|
|
2564
|
+
|
|
2565
|
+
psi = self.psi
|
|
2566
|
+
# make sure to keep all outer indices
|
|
2567
|
+
output_inds = tuple(map(psi.site_ind, range(self.N)))
|
|
2568
|
+
|
|
2569
|
+
# simplify the state and cache it
|
|
2570
|
+
psi.full_simplify_(
|
|
2571
|
+
seq=seq,
|
|
2572
|
+
atol=atol,
|
|
2573
|
+
output_inds=output_inds,
|
|
2574
|
+
equalize_norms=equalize_norms,
|
|
2575
|
+
)
|
|
2576
|
+
self._storage[key] = psi
|
|
2577
|
+
|
|
2578
|
+
# return a copy so we can modify it inplace
|
|
2579
|
+
return psi.copy()
|
|
2580
|
+
|
|
2581
|
+
def get_rdm_lightcone_simplified(
|
|
2582
|
+
self,
|
|
2583
|
+
where,
|
|
2584
|
+
seq="ADCRS",
|
|
2585
|
+
atol=1e-12,
|
|
2586
|
+
equalize_norms=False,
|
|
2587
|
+
):
|
|
2588
|
+
"""Get a simplified TN of the norm of the wavefunction, with
|
|
2589
|
+
gates outside reverse lightcone of ``where`` cancelled, and physical
|
|
2590
|
+
indices within ``where`` preserved so that they can be fixed (sliced)
|
|
2591
|
+
or used as output indices.
|
|
2592
|
+
|
|
2593
|
+
Parameters
|
|
2594
|
+
----------
|
|
2595
|
+
where : int or sequence of int
|
|
2596
|
+
The region assumed to be the target density matrix essentially.
|
|
2597
|
+
Supplied to
|
|
2598
|
+
:meth:`~quimb.tensor.circuit.Circuit.get_reverse_lightcone_tags`.
|
|
2599
|
+
seq : str, optional
|
|
2600
|
+
Which local tensor network simplifications to perform and in which
|
|
2601
|
+
order, see
|
|
2602
|
+
:meth:`~quimb.tensor.tensor_core.TensorNetwork.full_simplify`.
|
|
2603
|
+
atol : float, optional
|
|
2604
|
+
The tolerance with which to compare to zero when applying
|
|
2605
|
+
:meth:`~quimb.tensor.tensor_core.TensorNetwork.full_simplify`.
|
|
2606
|
+
equalize_norms : bool, optional
|
|
2607
|
+
Actively renormalize tensor norms during simplification.
|
|
2608
|
+
|
|
2609
|
+
Returns
|
|
2610
|
+
-------
|
|
2611
|
+
TensorNetwork
|
|
2612
|
+
"""
|
|
2613
|
+
key = ("rdm_lightcone_simplified", tuple(sorted(where)), seq, atol)
|
|
2614
|
+
if key in self._storage:
|
|
2615
|
+
return self._storage[key].copy()
|
|
2616
|
+
|
|
2617
|
+
ket_lc = self.get_psi_reverse_lightcone(where)
|
|
2618
|
+
|
|
2619
|
+
k_inds = tuple(map(self.ket_site_ind, where))
|
|
2620
|
+
b_inds = tuple(map(self.bra_site_ind, where))
|
|
2621
|
+
|
|
2622
|
+
bra_lc = ket_lc.conj().reindex(dict(zip(k_inds, b_inds)))
|
|
2623
|
+
rho_lc = bra_lc | ket_lc
|
|
2624
|
+
|
|
2625
|
+
# don't want to simplify site indices in region away
|
|
2626
|
+
output_inds = b_inds + k_inds
|
|
2627
|
+
|
|
2628
|
+
# # simplify the norm and cache it
|
|
2629
|
+
rho_lc.full_simplify_(
|
|
2630
|
+
seq=seq,
|
|
2631
|
+
atol=atol,
|
|
2632
|
+
output_inds=output_inds,
|
|
2633
|
+
equalize_norms=equalize_norms,
|
|
2634
|
+
)
|
|
2635
|
+
self._storage[key] = rho_lc
|
|
2636
|
+
|
|
2637
|
+
# return a copy so we can modify it inplace
|
|
2638
|
+
return rho_lc.copy()
|
|
2639
|
+
|
|
2640
|
+
def amplitude(
|
|
2641
|
+
self,
|
|
2642
|
+
b,
|
|
2643
|
+
optimize="auto-hq",
|
|
2644
|
+
simplify_sequence="ADCRS",
|
|
2645
|
+
simplify_atol=1e-12,
|
|
2646
|
+
simplify_equalize_norms=True,
|
|
2647
|
+
backend=None,
|
|
2648
|
+
dtype="complex128",
|
|
2649
|
+
rehearse=False,
|
|
2650
|
+
):
|
|
2651
|
+
r"""Get the amplitude coefficient of bitstring ``b``.
|
|
2652
|
+
|
|
2653
|
+
.. math::
|
|
2654
|
+
|
|
2655
|
+
c_b = \langle b | \psi \rangle
|
|
2656
|
+
|
|
2657
|
+
Parameters
|
|
2658
|
+
----------
|
|
2659
|
+
b : str or sequence of int
|
|
2660
|
+
The bitstring to compute the transition amplitude for.
|
|
2661
|
+
optimize : str, optional
|
|
2662
|
+
Contraction path optimizer to use for the amplitude, can be a
|
|
2663
|
+
non-reusable path optimizer as only called once (though path won't
|
|
2664
|
+
be cached for later use in that case).
|
|
2665
|
+
simplify_sequence : str, optional
|
|
2666
|
+
Which local tensor network simplifications to perform and in which
|
|
2667
|
+
order, see
|
|
2668
|
+
:meth:`~quimb.tensor.tensor_core.TensorNetwork.full_simplify`.
|
|
2669
|
+
simplify_atol : float, optional
|
|
2670
|
+
The tolerance with which to compare to zero when applying
|
|
2671
|
+
:meth:`~quimb.tensor.tensor_core.TensorNetwork.full_simplify`.
|
|
2672
|
+
simplify_equalize_norms : bool, optional
|
|
2673
|
+
Actively renormalize tensor norms during simplification.
|
|
2674
|
+
backend : str, optional
|
|
2675
|
+
Backend to perform the contraction with, e.g. ``'numpy'``,
|
|
2676
|
+
``'cupy'`` or ``'jax'``. Passed to ``cotengra``.
|
|
2677
|
+
dtype : str, optional
|
|
2678
|
+
Data type to cast the TN to before contraction.
|
|
2679
|
+
rehearse : bool or "tn", optional
|
|
2680
|
+
If ``True``, generate and cache the simplified tensor network and
|
|
2681
|
+
contraction tree but don't actually perform the contraction.
|
|
2682
|
+
Returns a dict with keys ``"tn"`` and ``'tree'`` with the tensor
|
|
2683
|
+
network that will be contracted and the corresponding contraction
|
|
2684
|
+
tree if so.
|
|
2685
|
+
"""
|
|
2686
|
+
self._maybe_init_storage()
|
|
2687
|
+
|
|
2688
|
+
if len(b) != self.N:
|
|
2689
|
+
raise ValueError(
|
|
2690
|
+
f"Bit-string {b} length does not "
|
|
2691
|
+
f"match number of qubits {self.N}."
|
|
2692
|
+
)
|
|
2693
|
+
|
|
2694
|
+
fs_opts = {
|
|
2695
|
+
"seq": simplify_sequence,
|
|
2696
|
+
"atol": simplify_atol,
|
|
2697
|
+
"equalize_norms": simplify_equalize_norms,
|
|
2698
|
+
}
|
|
2699
|
+
|
|
2700
|
+
# get the full wavefunction simplified
|
|
2701
|
+
psi_b = self.get_psi_simplified(**fs_opts)
|
|
2702
|
+
|
|
2703
|
+
# fix the output indices to the correct bitstring
|
|
2704
|
+
for i, x in zip(range(self.N), b):
|
|
2705
|
+
psi_b.isel_({psi_b.site_ind(i): x})
|
|
2706
|
+
|
|
2707
|
+
# perform a final simplification and cast
|
|
2708
|
+
psi_b.full_simplify_(**fs_opts)
|
|
2709
|
+
psi_b.astype_(dtype)
|
|
2710
|
+
|
|
2711
|
+
if rehearse == "tn":
|
|
2712
|
+
return psi_b
|
|
2713
|
+
|
|
2714
|
+
tree = psi_b.contraction_tree(output_inds=(), optimize=optimize)
|
|
2715
|
+
|
|
2716
|
+
if rehearse:
|
|
2717
|
+
return rehearsal_dict(psi_b, tree)
|
|
2718
|
+
|
|
2719
|
+
# perform the full contraction with the tree found
|
|
2720
|
+
c_b = psi_b.contract(
|
|
2721
|
+
all, output_inds=(), optimize=tree, backend=backend
|
|
2722
|
+
)
|
|
2723
|
+
|
|
2724
|
+
return c_b
|
|
2725
|
+
|
|
2726
|
+
def amplitude_rehearse(
|
|
2727
|
+
self,
|
|
2728
|
+
b="random",
|
|
2729
|
+
simplify_sequence="ADCRS",
|
|
2730
|
+
simplify_atol=1e-12,
|
|
2731
|
+
simplify_equalize_norms=True,
|
|
2732
|
+
optimize="auto-hq",
|
|
2733
|
+
dtype="complex128",
|
|
2734
|
+
rehearse=True,
|
|
2735
|
+
):
|
|
2736
|
+
"""Perform just the tensor network simplifications and contraction tree
|
|
2737
|
+
finding associated with computing a single amplitude (caching the
|
|
2738
|
+
results) but don't perform the actual contraction.
|
|
2739
|
+
|
|
2740
|
+
Parameters
|
|
2741
|
+
----------
|
|
2742
|
+
b : 'random', str or sequence of int
|
|
2743
|
+
The bitstring to rehearse computing the transition amplitude for,
|
|
2744
|
+
if ``'random'`` (the default) a random bitstring will be used.
|
|
2745
|
+
optimize : str, optional
|
|
2746
|
+
Contraction path optimizer to use for the marginal, can be a
|
|
2747
|
+
non-reusable path optimizer as only called once (though path won't
|
|
2748
|
+
be cached for later use in that case).
|
|
2749
|
+
simplify_sequence : str, optional
|
|
2750
|
+
Which local tensor network simplifications to perform and in which
|
|
2751
|
+
order, see
|
|
2752
|
+
:meth:`~quimb.tensor.tensor_core.TensorNetwork.full_simplify`.
|
|
2753
|
+
simplify_atol : float, optional
|
|
2754
|
+
The tolerance with which to compare to zero when applying
|
|
2755
|
+
:meth:`~quimb.tensor.tensor_core.TensorNetwork.full_simplify`.
|
|
2756
|
+
simplify_equalize_norms : bool, optional
|
|
2757
|
+
Actively renormalize tensor norms during simplification.
|
|
2758
|
+
backend : str, optional
|
|
2759
|
+
Backend to perform the marginal contraction with, e.g. ``'numpy'``,
|
|
2760
|
+
``'cupy'`` or ``'jax'``. Passed to ``cotengra``.
|
|
2761
|
+
dtype : str, optional
|
|
2762
|
+
Data type to cast the TN to before contraction.
|
|
2763
|
+
|
|
2764
|
+
Returns
|
|
2765
|
+
-------
|
|
2766
|
+
dict
|
|
2767
|
+
|
|
2768
|
+
"""
|
|
2769
|
+
if b == "random":
|
|
2770
|
+
b = "r" * self.N
|
|
2771
|
+
|
|
2772
|
+
return self.amplitude(
|
|
2773
|
+
b=b,
|
|
2774
|
+
optimize=optimize,
|
|
2775
|
+
dtype=dtype,
|
|
2776
|
+
rehearse=rehearse,
|
|
2777
|
+
simplify_sequence=simplify_sequence,
|
|
2778
|
+
simplify_atol=simplify_atol,
|
|
2779
|
+
simplify_equalize_norms=simplify_equalize_norms,
|
|
2780
|
+
)
|
|
2781
|
+
|
|
2782
|
+
amplitude_tn = functools.partialmethod(amplitude_rehearse, rehearse="tn")
|
|
2783
|
+
|
|
2784
|
+
def partial_trace(
|
|
2785
|
+
self,
|
|
2786
|
+
keep,
|
|
2787
|
+
optimize="auto-hq",
|
|
2788
|
+
simplify_sequence="ADCRS",
|
|
2789
|
+
simplify_atol=1e-12,
|
|
2790
|
+
simplify_equalize_norms=True,
|
|
2791
|
+
backend=None,
|
|
2792
|
+
dtype="complex128",
|
|
2793
|
+
rehearse=False,
|
|
2794
|
+
):
|
|
2795
|
+
r"""Perform the partial trace on the circuit wavefunction, retaining
|
|
2796
|
+
only qubits in ``keep``, and making use of reverse lightcone
|
|
2797
|
+
cancellation:
|
|
2798
|
+
|
|
2799
|
+
.. math::
|
|
2800
|
+
|
|
2801
|
+
\rho_{\bar{q}} = Tr_{\bar{p}}
|
|
2802
|
+
|\psi_{\bar{q}} \rangle \langle \psi_{\bar{q}}|
|
|
2803
|
+
|
|
2804
|
+
Where :math:`\bar{q}` is the set of qubits to keep,
|
|
2805
|
+
:math:`\psi_{\bar{q}}` is the circuit wavefunction only with gates in
|
|
2806
|
+
the causal cone of this set, and :math:`\bar{p}` is the remaining
|
|
2807
|
+
qubits.
|
|
2808
|
+
|
|
2809
|
+
Parameters
|
|
2810
|
+
----------
|
|
2811
|
+
keep : int or sequence of int
|
|
2812
|
+
The qubit(s) to keep as we trace out the rest.
|
|
2813
|
+
optimize : str, optional
|
|
2814
|
+
Contraction path optimizer to use for the reduced density matrix,
|
|
2815
|
+
can be a non-reusable path optimizer as only called once (though
|
|
2816
|
+
path won't be cached for later use in that case).
|
|
2817
|
+
simplify_sequence : str, optional
|
|
2818
|
+
Which local tensor network simplifications to perform and in which
|
|
2819
|
+
order, see
|
|
2820
|
+
:meth:`~quimb.tensor.tensor_core.TensorNetwork.full_simplify`.
|
|
2821
|
+
simplify_atol : float, optional
|
|
2822
|
+
The tolerance with which to compare to zero when applying
|
|
2823
|
+
:meth:`~quimb.tensor.tensor_core.TensorNetwork.full_simplify`.
|
|
2824
|
+
simplify_equalize_norms : bool, optional
|
|
2825
|
+
Actively renormalize tensor norms during simplification.
|
|
2826
|
+
backend : str, optional
|
|
2827
|
+
Backend to perform the marginal contraction with, e.g. ``'numpy'``,
|
|
2828
|
+
``'cupy'`` or ``'jax'``. Passed to ``cotengra``.
|
|
2829
|
+
dtype : str, optional
|
|
2830
|
+
Data type to cast the TN to before contraction.
|
|
2831
|
+
rehearse : bool or "tn", optional
|
|
2832
|
+
If ``True``, generate and cache the simplified tensor network and
|
|
2833
|
+
contraction tree but don't actually perform the contraction.
|
|
2834
|
+
Returns a dict with keys ``"tn"`` and ``'tree'`` with the tensor
|
|
2835
|
+
network that will be contracted and the corresponding contraction
|
|
2836
|
+
tree if so.
|
|
2837
|
+
|
|
2838
|
+
Returns
|
|
2839
|
+
-------
|
|
2840
|
+
array or dict
|
|
2841
|
+
"""
|
|
2842
|
+
|
|
2843
|
+
if isinstance(keep, numbers.Integral):
|
|
2844
|
+
keep = (keep,)
|
|
2845
|
+
|
|
2846
|
+
output_inds = tuple(map(self.ket_site_ind, keep)) + tuple(
|
|
2847
|
+
map(self.bra_site_ind, keep)
|
|
2848
|
+
)
|
|
2849
|
+
|
|
2850
|
+
rho = self.get_rdm_lightcone_simplified(
|
|
2851
|
+
where=keep,
|
|
2852
|
+
seq=simplify_sequence,
|
|
2853
|
+
atol=simplify_atol,
|
|
2854
|
+
equalize_norms=simplify_equalize_norms,
|
|
2855
|
+
).astype_(dtype)
|
|
2856
|
+
|
|
2857
|
+
if rehearse == "tn":
|
|
2858
|
+
return rho
|
|
2859
|
+
|
|
2860
|
+
tree = rho.contraction_tree(output_inds=output_inds, optimize=optimize)
|
|
2861
|
+
|
|
2862
|
+
if rehearse:
|
|
2863
|
+
return rehearsal_dict(rho, tree)
|
|
2864
|
+
|
|
2865
|
+
# perform the full contraction with the tree found
|
|
2866
|
+
rho_dense = rho.contract(
|
|
2867
|
+
all,
|
|
2868
|
+
output_inds=output_inds,
|
|
2869
|
+
optimize=tree,
|
|
2870
|
+
backend=backend,
|
|
2871
|
+
).data
|
|
2872
|
+
|
|
2873
|
+
return ops.reshape(rho_dense, [2 ** len(keep), 2 ** len(keep)])
|
|
2874
|
+
|
|
2875
|
+
partial_trace_rehearse = functools.partialmethod(
|
|
2876
|
+
partial_trace, rehearse=True
|
|
2877
|
+
)
|
|
2878
|
+
partial_trace_tn = functools.partialmethod(partial_trace, rehearse="tn")
|
|
2879
|
+
|
|
2880
|
+
def local_expectation(
|
|
2881
|
+
self,
|
|
2882
|
+
G,
|
|
2883
|
+
where,
|
|
2884
|
+
optimize="auto-hq",
|
|
2885
|
+
simplify_sequence="ADCRS",
|
|
2886
|
+
simplify_atol=1e-12,
|
|
2887
|
+
simplify_equalize_norms=True,
|
|
2888
|
+
backend=None,
|
|
2889
|
+
dtype="complex128",
|
|
2890
|
+
rehearse=False,
|
|
2891
|
+
):
|
|
2892
|
+
r"""Compute the a single expectation value of operator ``G``, acting on
|
|
2893
|
+
sites ``where``, making use of reverse lightcone cancellation.
|
|
2894
|
+
|
|
2895
|
+
.. math::
|
|
2896
|
+
|
|
2897
|
+
\langle \psi_{\bar{q}} | G_{\bar{q}} | \psi_{\bar{q}} \rangle
|
|
2898
|
+
|
|
2899
|
+
where :math:`\bar{q}` is the set of qubits :math:`G` acts one and
|
|
2900
|
+
:math:`\psi_{\bar{q}}` is the circuit wavefunction only with gates in
|
|
2901
|
+
the causal cone of this set. If you supply a tuple or list of gates
|
|
2902
|
+
then the expectations will be computed simultaneously.
|
|
2903
|
+
|
|
2904
|
+
Parameters
|
|
2905
|
+
----------
|
|
2906
|
+
G : array or sequence[array]
|
|
2907
|
+
The raw operator(s) to find the expectation of.
|
|
2908
|
+
where : int or sequence of int
|
|
2909
|
+
Which qubits the operator acts on.
|
|
2910
|
+
optimize : str, optional
|
|
2911
|
+
Contraction path optimizer to use for the local expectation,
|
|
2912
|
+
can be a non-reusable path optimizer as only called once (though
|
|
2913
|
+
path won't be cached for later use in that case).
|
|
2914
|
+
simplify_sequence : str, optional
|
|
2915
|
+
Which local tensor network simplifications to perform and in which
|
|
2916
|
+
order, see
|
|
2917
|
+
:meth:`~quimb.tensor.tensor_core.TensorNetwork.full_simplify`.
|
|
2918
|
+
simplify_atol : float, optional
|
|
2919
|
+
The tolerance with which to compare to zero when applying
|
|
2920
|
+
:meth:`~quimb.tensor.tensor_core.TensorNetwork.full_simplify`.
|
|
2921
|
+
simplify_equalize_norms : bool, optional
|
|
2922
|
+
Actively renormalize tensor norms during simplification.
|
|
2923
|
+
backend : str, optional
|
|
2924
|
+
Backend to perform the marginal contraction with, e.g. ``'numpy'``,
|
|
2925
|
+
``'cupy'`` or ``'jax'``. Passed to ``cotengra``.
|
|
2926
|
+
dtype : str, optional
|
|
2927
|
+
Data type to cast the TN to before contraction.
|
|
2928
|
+
gate_opts : None or dict_like
|
|
2929
|
+
Options to use when applying ``G`` to the wavefunction.
|
|
2930
|
+
rehearse : bool or "tn", optional
|
|
2931
|
+
If ``True``, generate and cache the simplified tensor network and
|
|
2932
|
+
contraction tree but don't actually perform the contraction.
|
|
2933
|
+
Returns a dict with keys ``'tn'`` and ``'tree'`` with the tensor
|
|
2934
|
+
network that will be contracted and the corresponding contraction
|
|
2935
|
+
tree if so.
|
|
2936
|
+
|
|
2937
|
+
Returns
|
|
2938
|
+
-------
|
|
2939
|
+
scalar, tuple[scalar] or dict
|
|
2940
|
+
"""
|
|
2941
|
+
if isinstance(where, numbers.Integral):
|
|
2942
|
+
where = (where,)
|
|
2943
|
+
|
|
2944
|
+
fs_opts = {
|
|
2945
|
+
"seq": simplify_sequence,
|
|
2946
|
+
"atol": simplify_atol,
|
|
2947
|
+
"equalize_norms": simplify_equalize_norms,
|
|
2948
|
+
}
|
|
2949
|
+
|
|
2950
|
+
rho = self.get_rdm_lightcone_simplified(where=where, **fs_opts)
|
|
2951
|
+
k_inds = tuple(self.ket_site_ind(i) for i in where)
|
|
2952
|
+
b_inds = tuple(self.bra_site_ind(i) for i in where)
|
|
2953
|
+
|
|
2954
|
+
if isinstance(G, (list, tuple)):
|
|
2955
|
+
# if we have multiple expectations create an extra indexed stack
|
|
2956
|
+
nG = len(G)
|
|
2957
|
+
G_data = do("stack", G)
|
|
2958
|
+
G_data = reshape(G_data, (nG,) + (2,) * 2 * len(where))
|
|
2959
|
+
output_inds = (rand_uuid(),)
|
|
2960
|
+
else:
|
|
2961
|
+
G_data = reshape(G, (2,) * 2 * len(where))
|
|
2962
|
+
output_inds = ()
|
|
2963
|
+
|
|
2964
|
+
TG = Tensor(data=G_data, inds=output_inds + b_inds + k_inds)
|
|
2965
|
+
|
|
2966
|
+
rhoG = rho | TG
|
|
2967
|
+
|
|
2968
|
+
rhoG.full_simplify_(output_inds=output_inds, **fs_opts)
|
|
2969
|
+
rhoG.astype_(dtype)
|
|
2970
|
+
|
|
2971
|
+
if rehearse == "tn":
|
|
2972
|
+
return rhoG
|
|
2973
|
+
|
|
2974
|
+
tree = rhoG.contraction_tree(
|
|
2975
|
+
output_inds=output_inds, optimize=optimize
|
|
2976
|
+
)
|
|
2977
|
+
|
|
2978
|
+
if rehearse:
|
|
2979
|
+
return rehearsal_dict(rhoG, tree)
|
|
2980
|
+
|
|
2981
|
+
g_ex = rhoG.contract(
|
|
2982
|
+
all,
|
|
2983
|
+
output_inds=output_inds,
|
|
2984
|
+
optimize=tree,
|
|
2985
|
+
backend=backend,
|
|
2986
|
+
)
|
|
2987
|
+
|
|
2988
|
+
if isinstance(g_ex, Tensor):
|
|
2989
|
+
g_ex = tuple(g_ex.data)
|
|
2990
|
+
|
|
2991
|
+
return g_ex
|
|
2992
|
+
|
|
2993
|
+
local_expectation_rehearse = functools.partialmethod(
|
|
2994
|
+
local_expectation, rehearse=True
|
|
2995
|
+
)
|
|
2996
|
+
local_expectation_tn = functools.partialmethod(
|
|
2997
|
+
local_expectation, rehearse="tn"
|
|
2998
|
+
)
|
|
2999
|
+
|
|
3000
|
+
def compute_marginal(
|
|
3001
|
+
self,
|
|
3002
|
+
where,
|
|
3003
|
+
fix=None,
|
|
3004
|
+
optimize="auto-hq",
|
|
3005
|
+
backend=None,
|
|
3006
|
+
dtype="complex64",
|
|
3007
|
+
simplify_sequence="ADCRS",
|
|
3008
|
+
simplify_atol=1e-6,
|
|
3009
|
+
simplify_equalize_norms=True,
|
|
3010
|
+
rehearse=False,
|
|
3011
|
+
):
|
|
3012
|
+
"""Compute the probability tensor of qubits in ``where``, given
|
|
3013
|
+
possibly fixed qubits in ``fix`` and tracing everything else having
|
|
3014
|
+
removed redundant unitary gates.
|
|
3015
|
+
|
|
3016
|
+
Parameters
|
|
3017
|
+
----------
|
|
3018
|
+
where : sequence of int
|
|
3019
|
+
The qubits to compute the marginal probability distribution of.
|
|
3020
|
+
fix : None or dict[int, str], optional
|
|
3021
|
+
Measurement results on other qubits to fix.
|
|
3022
|
+
optimize : str, optional
|
|
3023
|
+
Contraction path optimizer to use for the marginal, can be a
|
|
3024
|
+
non-reusable path optimizer as only called once (though path won't
|
|
3025
|
+
be cached for later use in that case).
|
|
3026
|
+
backend : str, optional
|
|
3027
|
+
Backend to perform the marginal contraction with, e.g. ``'numpy'``,
|
|
3028
|
+
``'cupy'`` or ``'jax'``. Passed to ``cotengra``.
|
|
3029
|
+
dtype : str, optional
|
|
3030
|
+
Data type to cast the TN to before contraction.
|
|
3031
|
+
simplify_sequence : str, optional
|
|
3032
|
+
Which local tensor network simplifications to perform and in which
|
|
3033
|
+
order, see
|
|
3034
|
+
:meth:`~quimb.tensor.tensor_core.TensorNetwork.full_simplify`.
|
|
3035
|
+
simplify_atol : float, optional
|
|
3036
|
+
The tolerance with which to compare to zero when applying
|
|
3037
|
+
:meth:`~quimb.tensor.tensor_core.TensorNetwork.full_simplify`.
|
|
3038
|
+
simplify_equalize_norms : bool, optional
|
|
3039
|
+
Actively renormalize tensor norms during simplification.
|
|
3040
|
+
rehearse : bool or "tn", optional
|
|
3041
|
+
Whether to perform the marginal contraction or just return the
|
|
3042
|
+
associated TN and contraction tree.
|
|
3043
|
+
"""
|
|
3044
|
+
self._maybe_init_storage()
|
|
3045
|
+
|
|
3046
|
+
# index trick to contract straight to reduced density matrix diagonal
|
|
3047
|
+
# rho_ii -> p_i (i.e. insert a COPY tensor into the norm)
|
|
3048
|
+
output_inds = [self.ket_site_ind(i) for i in where]
|
|
3049
|
+
|
|
3050
|
+
fs_opts = {
|
|
3051
|
+
"seq": simplify_sequence,
|
|
3052
|
+
"atol": simplify_atol,
|
|
3053
|
+
"equalize_norms": simplify_equalize_norms,
|
|
3054
|
+
}
|
|
3055
|
+
|
|
3056
|
+
# lightcone region is target qubit plus fixed qubits
|
|
3057
|
+
region = set(where)
|
|
3058
|
+
if fix is not None:
|
|
3059
|
+
region |= set(fix)
|
|
3060
|
+
region = tuple(sorted(region))
|
|
3061
|
+
|
|
3062
|
+
# have we fixed or are measuring all qubits?
|
|
3063
|
+
final_marginal = len(region) == self.N
|
|
3064
|
+
|
|
3065
|
+
# these both are cached and produce TN copies
|
|
3066
|
+
if final_marginal:
|
|
3067
|
+
# won't need to partially trace anything -> just need ket
|
|
3068
|
+
nm_lc = self.get_psi_simplified(**fs_opts)
|
|
3069
|
+
else:
|
|
3070
|
+
# can use lightcone cancellation on partially traced qubits
|
|
3071
|
+
nm_lc = self.get_rdm_lightcone_simplified(region, **fs_opts)
|
|
3072
|
+
# re-connect the ket and bra indices as taking diagonal
|
|
3073
|
+
nm_lc.reindex_(
|
|
3074
|
+
{self.bra_site_ind(i): self.ket_site_ind(i) for i in region}
|
|
3075
|
+
)
|
|
3076
|
+
|
|
3077
|
+
if fix:
|
|
3078
|
+
# project (slice) fixed tensors with bitstring
|
|
3079
|
+
# this severs the indices connecting bra and ket on fixed sites
|
|
3080
|
+
nm_lc.isel_({self.ket_site_ind(i): b for i, b in fix.items()})
|
|
3081
|
+
|
|
3082
|
+
# having sliced we can do a final simplify
|
|
3083
|
+
nm_lc.full_simplify_(output_inds=output_inds, **fs_opts)
|
|
3084
|
+
|
|
3085
|
+
# for stability with very small probabilities, scale by average prob
|
|
3086
|
+
if fix is not None:
|
|
3087
|
+
nfact = 2 ** len(fix)
|
|
3088
|
+
if final_marginal:
|
|
3089
|
+
nm_lc.multiply_(nfact**0.5, spread_over="all")
|
|
3090
|
+
else:
|
|
3091
|
+
nm_lc.multiply_(nfact, spread_over="all")
|
|
3092
|
+
|
|
3093
|
+
# cast to desired data type
|
|
3094
|
+
nm_lc.astype_(dtype)
|
|
3095
|
+
|
|
3096
|
+
if rehearse == "tn":
|
|
3097
|
+
return nm_lc
|
|
3098
|
+
|
|
3099
|
+
# NB. the tree isn't *neccesarily* the same each time due to the post
|
|
3100
|
+
# slicing full simplify, however there is also the lower level
|
|
3101
|
+
# contraction path cache if the structure generated *is* the same
|
|
3102
|
+
# so still pretty efficient to just overwrite
|
|
3103
|
+
tree = nm_lc.contraction_tree(
|
|
3104
|
+
output_inds=output_inds,
|
|
3105
|
+
optimize=optimize,
|
|
3106
|
+
)
|
|
3107
|
+
|
|
3108
|
+
if rehearse:
|
|
3109
|
+
return rehearsal_dict(nm_lc, tree)
|
|
3110
|
+
|
|
3111
|
+
# perform the full contraction with the tree found
|
|
3112
|
+
p_marginal = abs(
|
|
3113
|
+
nm_lc.contract(
|
|
3114
|
+
all,
|
|
3115
|
+
output_inds=output_inds,
|
|
3116
|
+
optimize=tree,
|
|
3117
|
+
backend=backend,
|
|
3118
|
+
).data
|
|
3119
|
+
)
|
|
3120
|
+
|
|
3121
|
+
if final_marginal:
|
|
3122
|
+
# we only did half the ket contraction so need to square
|
|
3123
|
+
p_marginal = p_marginal**2
|
|
3124
|
+
|
|
3125
|
+
if fix is not None:
|
|
3126
|
+
p_marginal = p_marginal / nfact
|
|
3127
|
+
|
|
3128
|
+
return p_marginal
|
|
3129
|
+
|
|
3130
|
+
compute_marginal_rehearse = functools.partialmethod(
|
|
3131
|
+
compute_marginal, rehearse=True
|
|
3132
|
+
)
|
|
3133
|
+
compute_marginal_tn = functools.partialmethod(
|
|
3134
|
+
compute_marginal, rehearse="tn"
|
|
3135
|
+
)
|
|
3136
|
+
|
|
3137
|
+
def calc_qubit_ordering(self, qubits=None, method="greedy-lightcone"):
|
|
3138
|
+
"""Get a order to measure ``qubits`` in, by greedily choosing whichever
|
|
3139
|
+
has the smallest reverse lightcone followed by whichever expands this
|
|
3140
|
+
lightcone *least*.
|
|
3141
|
+
|
|
3142
|
+
Parameters
|
|
3143
|
+
----------
|
|
3144
|
+
qubits : None or sequence of int
|
|
3145
|
+
The qubits to generate a lightcone ordering for, if ``None``,
|
|
3146
|
+
assume all qubits.
|
|
3147
|
+
|
|
3148
|
+
Returns
|
|
3149
|
+
-------
|
|
3150
|
+
tuple[int]
|
|
3151
|
+
The order to 'measure' qubits in.
|
|
3152
|
+
"""
|
|
3153
|
+
self._maybe_init_storage()
|
|
3154
|
+
|
|
3155
|
+
if qubits is None:
|
|
3156
|
+
qubits = tuple(range(self.N))
|
|
3157
|
+
else:
|
|
3158
|
+
qubits = tuple(sorted(qubits))
|
|
3159
|
+
|
|
3160
|
+
key = ("lightcone_ordering", method, qubits)
|
|
3161
|
+
|
|
3162
|
+
# check the cache first
|
|
3163
|
+
if key in self._storage:
|
|
3164
|
+
return self._storage[key]
|
|
3165
|
+
|
|
3166
|
+
if method == "greedy-lightcone":
|
|
3167
|
+
cone = set()
|
|
3168
|
+
lctgs = {
|
|
3169
|
+
i: set(self.get_reverse_lightcone_tags(i)) for i in qubits
|
|
3170
|
+
}
|
|
3171
|
+
|
|
3172
|
+
order = []
|
|
3173
|
+
while lctgs:
|
|
3174
|
+
# get the next qubit which adds least num gates to lightcone
|
|
3175
|
+
next_qubit = min(lctgs, key=lambda i: len(lctgs[i] - cone))
|
|
3176
|
+
cone |= lctgs.pop(next_qubit)
|
|
3177
|
+
order.append(next_qubit)
|
|
3178
|
+
|
|
3179
|
+
else:
|
|
3180
|
+
# use graph distance based hierachical clustering
|
|
3181
|
+
psi = self.get_psi_simplified("R")
|
|
3182
|
+
qubit_inds = tuple(map(psi.site_ind, qubits))
|
|
3183
|
+
tids = psi._get_tids_from_inds(qubit_inds, "any")
|
|
3184
|
+
matcher = re.compile(psi.site_ind_id.format(r"(\d+)"))
|
|
3185
|
+
order = []
|
|
3186
|
+
for tid in psi.compute_hierarchical_ordering(tids, method=method):
|
|
3187
|
+
t = psi.tensor_map[tid]
|
|
3188
|
+
for ind in t.inds:
|
|
3189
|
+
for sq in matcher.findall(ind):
|
|
3190
|
+
order.append(int(sq))
|
|
3191
|
+
|
|
3192
|
+
order = self._storage[key] = tuple(order)
|
|
3193
|
+
return order
|
|
3194
|
+
|
|
3195
|
+
def _parse_qubits_order(self, qubits=None, order=None):
|
|
3196
|
+
"""Simply initializes the default of measuring all qubits, and the
|
|
3197
|
+
default order, or checks that ``order`` is a permutation of ``qubits``.
|
|
3198
|
+
"""
|
|
3199
|
+
if qubits is None:
|
|
3200
|
+
qubits = range(self.N)
|
|
3201
|
+
if order is None:
|
|
3202
|
+
order = self.calc_qubit_ordering(qubits)
|
|
3203
|
+
elif set(qubits) != set(order):
|
|
3204
|
+
raise ValueError("``order`` must be a permutation of ``qubits``.")
|
|
3205
|
+
|
|
3206
|
+
return qubits, order
|
|
3207
|
+
|
|
3208
|
+
def _group_order(self, order, group_size=1):
|
|
3209
|
+
"""Take the qubit ordering ``order`` and batch it in groups of size
|
|
3210
|
+
``group_size``, sorting the qubits (for caching reasons) within each
|
|
3211
|
+
group.
|
|
3212
|
+
"""
|
|
3213
|
+
return tuple(
|
|
3214
|
+
tuple(sorted(g)) for g in partition_all(group_size, order)
|
|
3215
|
+
)
|
|
3216
|
+
|
|
3217
|
+
def get_qubit_distances(self, method="dijkstra", alpha=2):
|
|
3218
|
+
"""Get a nested dictionary of qubit distances. This is computed from a
|
|
3219
|
+
graph representing qubit interactions. The graph has an edge between
|
|
3220
|
+
qubits if they are acted on by the same gate, and the distance-weight
|
|
3221
|
+
of the edge is exponentially small in the number of gates between them.
|
|
3222
|
+
|
|
3223
|
+
Parameters
|
|
3224
|
+
----------
|
|
3225
|
+
method : {'dijkstra', 'resistance'}, optional
|
|
3226
|
+
The method to use to compute the qubit distances. See
|
|
3227
|
+
:func:`networkx.all_pairs_dijkstra_path_length` and
|
|
3228
|
+
:func:`networkx.resistance_distance`.
|
|
3229
|
+
alpha : float, optional
|
|
3230
|
+
The distance weight between qubits is ``alpha**(num_gates - 1 )``.
|
|
3231
|
+
|
|
3232
|
+
Returns
|
|
3233
|
+
-------
|
|
3234
|
+
dict[int, dict[int, float]]
|
|
3235
|
+
The distance between each pair of qubits, accessed like
|
|
3236
|
+
``distances[q1][q2]``. If two qubits are not connected, the
|
|
3237
|
+
distance is missing.
|
|
3238
|
+
"""
|
|
3239
|
+
import networkx as nx
|
|
3240
|
+
|
|
3241
|
+
G = nx.Graph()
|
|
3242
|
+
for g in self.gates:
|
|
3243
|
+
for q1, q2 in itertools.combinations(g.qubits, 2):
|
|
3244
|
+
if G.has_edge(q1, q2):
|
|
3245
|
+
G[q1][q2]["weight"] /= alpha
|
|
3246
|
+
else:
|
|
3247
|
+
G.add_edge(q1, q2, weight=1)
|
|
3248
|
+
|
|
3249
|
+
if method == "dijkstra":
|
|
3250
|
+
distances = dict(
|
|
3251
|
+
nx.all_pairs_dijkstra_path_length(G, weight="weight")
|
|
3252
|
+
)
|
|
3253
|
+
elif method == "resistance":
|
|
3254
|
+
distances = nx.resistance_distance(G, weight="weight")
|
|
3255
|
+
else:
|
|
3256
|
+
raise ValueError(f"Unknown method {method}.")
|
|
3257
|
+
|
|
3258
|
+
return distances
|
|
3259
|
+
|
|
3260
|
+
def reordered_gates_dfs_clustered(self):
|
|
3261
|
+
"""Get the gates reordered by a depth first search traversal of the
|
|
3262
|
+
multi-qubit gate graph that greedily selects successive gates which
|
|
3263
|
+
are 'close' in graph distance, and shifts single qubit gates to be
|
|
3264
|
+
adjacent to multi-qubit gates where possible.
|
|
3265
|
+
"""
|
|
3266
|
+
# first we make a directed graph of the multi-qubit gates
|
|
3267
|
+
successors = {}
|
|
3268
|
+
predecessors = {}
|
|
3269
|
+
single_qubit_stacks = {}
|
|
3270
|
+
single_qubit_predecessors = {}
|
|
3271
|
+
last_gates = {}
|
|
3272
|
+
queue = []
|
|
3273
|
+
|
|
3274
|
+
for i, g in enumerate(self.gates):
|
|
3275
|
+
if g.total_qubit_count == 1:
|
|
3276
|
+
# lazily accumulate single qubit gates
|
|
3277
|
+
(q,) = g.qubits
|
|
3278
|
+
single_qubit_stacks.setdefault(q, []).append(i)
|
|
3279
|
+
|
|
3280
|
+
else:
|
|
3281
|
+
pi = predecessors[i] = []
|
|
3282
|
+
sqpi = single_qubit_predecessors[i] = []
|
|
3283
|
+
|
|
3284
|
+
for q in g.qubits:
|
|
3285
|
+
# collect any single qubit gates acting on this qubit
|
|
3286
|
+
sqpi.extend(single_qubit_stacks.pop(q, []))
|
|
3287
|
+
|
|
3288
|
+
if q in last_gates:
|
|
3289
|
+
# qubit has already been acted on -> have an edge
|
|
3290
|
+
h = last_gates[q]
|
|
3291
|
+
# mark h as a predecessor of i
|
|
3292
|
+
pi.append(h)
|
|
3293
|
+
# mark i as a successor of h
|
|
3294
|
+
successors.setdefault(h, []).append(i)
|
|
3295
|
+
|
|
3296
|
+
# mark qubit as acted on
|
|
3297
|
+
last_gates[q] = i
|
|
3298
|
+
|
|
3299
|
+
if len(pi) == 0:
|
|
3300
|
+
# no predecessors -> is possible starting multiqubit gate
|
|
3301
|
+
queue.append(i)
|
|
3302
|
+
|
|
3303
|
+
# then we traverse the multi-qubit gates in a depth first, topological
|
|
3304
|
+
# order, breaking ties by minimizing the distance between active qubits
|
|
3305
|
+
distances = self.get_qubit_distances()
|
|
3306
|
+
|
|
3307
|
+
def gate_distance(i, j):
|
|
3308
|
+
qis = self.gates[i].qubits
|
|
3309
|
+
qjs = self.gates[j].qubits
|
|
3310
|
+
return min(
|
|
3311
|
+
distances[q1].get(q2, float("inf")) for q1 in qis for q2 in qjs
|
|
3312
|
+
)
|
|
3313
|
+
|
|
3314
|
+
# sort initial queue by qubit with smallest index
|
|
3315
|
+
queue.sort(key=lambda i: min(self.gates[i].qubits))
|
|
3316
|
+
new_gates = []
|
|
3317
|
+
|
|
3318
|
+
while queue:
|
|
3319
|
+
i = queue.pop(0)
|
|
3320
|
+
|
|
3321
|
+
# first flush any single qubit gates acting on the qubits of gate i
|
|
3322
|
+
new_gates.extend(
|
|
3323
|
+
self.gates[j] for j in single_qubit_predecessors.pop(i, [])
|
|
3324
|
+
)
|
|
3325
|
+
# then add the gate itself
|
|
3326
|
+
new_gates.append(self.gates[i])
|
|
3327
|
+
|
|
3328
|
+
# then remove i as a predecessor of its successors
|
|
3329
|
+
for j in successors.pop(i, []):
|
|
3330
|
+
pj = predecessors[j]
|
|
3331
|
+
pj.remove(i)
|
|
3332
|
+
if not pj:
|
|
3333
|
+
# j has no more predecessors -> can be added to queue
|
|
3334
|
+
queue.append(j)
|
|
3335
|
+
|
|
3336
|
+
# check if this is the last time q is acted on,
|
|
3337
|
+
# if so flush any remaining single qubit gates
|
|
3338
|
+
for q in self.gates[i].qubits:
|
|
3339
|
+
if last_gates[q] == i:
|
|
3340
|
+
# qubit has been acted on for the last time
|
|
3341
|
+
new_gates.extend(
|
|
3342
|
+
self.gates[j] for j in single_qubit_stacks.pop(q, [])
|
|
3343
|
+
)
|
|
3344
|
+
|
|
3345
|
+
# sort the queue of possible next gates
|
|
3346
|
+
queue.sort(key=lambda k: gate_distance(i, k))
|
|
3347
|
+
|
|
3348
|
+
# flush any remaining single qubit gates
|
|
3349
|
+
for q in sorted(single_qubit_stacks):
|
|
3350
|
+
new_gates.extend(self.gates[j] for j in single_qubit_stacks.pop(q))
|
|
3351
|
+
|
|
3352
|
+
return new_gates
|
|
3353
|
+
|
|
3354
|
+
def sample(
|
|
3355
|
+
self,
|
|
3356
|
+
C,
|
|
3357
|
+
qubits=None,
|
|
3358
|
+
order=None,
|
|
3359
|
+
group_size=10,
|
|
3360
|
+
max_marginal_storage=2**20,
|
|
3361
|
+
seed=None,
|
|
3362
|
+
optimize="auto-hq",
|
|
3363
|
+
backend=None,
|
|
3364
|
+
dtype="complex64",
|
|
3365
|
+
simplify_sequence="ADCRS",
|
|
3366
|
+
simplify_atol=1e-6,
|
|
3367
|
+
simplify_equalize_norms=True,
|
|
3368
|
+
):
|
|
3369
|
+
r"""Sample the circuit given by ``gates``, ``C`` times, using lightcone
|
|
3370
|
+
cancelling and caching marginal distribution results. This is a
|
|
3371
|
+
generator. This proceeds as a chain of marginal computations.
|
|
3372
|
+
|
|
3373
|
+
Assuming we have ``group_size=1``, and some ordering of the qubits,
|
|
3374
|
+
:math:`\{q_0, q_1, q_2, q_3, \ldots\}` we first compute:
|
|
3375
|
+
|
|
3376
|
+
.. math::
|
|
3377
|
+
|
|
3378
|
+
p(q_0) = \mathrm{diag} \mathrm{Tr}_{1, 2, 3,\ldots}
|
|
3379
|
+
| \psi_{0} \rangle \langle \psi_{0} |
|
|
3380
|
+
|
|
3381
|
+
I.e. simply the probability distribution on a single qubit, conditioned
|
|
3382
|
+
on nothing. The subscript on :math:`\psi` refers to the fact that we
|
|
3383
|
+
only need gates from the causal cone of qubit 0.
|
|
3384
|
+
From this we can sample an outcome, either 0 or 1, if we
|
|
3385
|
+
call this :math:`r_0` we can then move on to the next marginal:
|
|
3386
|
+
|
|
3387
|
+
.. math::
|
|
3388
|
+
|
|
3389
|
+
p(q_1 | r_0) = \mathrm{diag} \mathrm{Tr}_{2, 3,\ldots}
|
|
3390
|
+
\langle r_0
|
|
3391
|
+
| \psi_{0, 1} \rangle \langle \psi_{0, 1} |
|
|
3392
|
+
r_0 \rangle
|
|
3393
|
+
|
|
3394
|
+
I.e. the probability distribution of the next qubit, given our prior
|
|
3395
|
+
result. We can sample from this to get :math:`r_1`. Then we compute:
|
|
3396
|
+
|
|
3397
|
+
.. math::
|
|
3398
|
+
|
|
3399
|
+
p(q_2 | r_0 r_1) = \mathrm{diag} \mathrm{Tr}_{3,\ldots}
|
|
3400
|
+
\langle r_0 r_1
|
|
3401
|
+
| \psi_{0, 1, 2} \rangle \langle \psi_{0, 1, 2} |
|
|
3402
|
+
r_0 r_1 \rangle
|
|
3403
|
+
|
|
3404
|
+
Eventually we will reach the 'final marginal', which we can compute as
|
|
3405
|
+
|
|
3406
|
+
.. math::
|
|
3407
|
+
|
|
3408
|
+
|\langle r_0 r_1 r_2 r_3 \ldots | \psi \rangle|^2
|
|
3409
|
+
|
|
3410
|
+
since there is nothing left to trace out.
|
|
3411
|
+
|
|
3412
|
+
Parameters
|
|
3413
|
+
----------
|
|
3414
|
+
C : int
|
|
3415
|
+
The number of times to sample.
|
|
3416
|
+
qubits : None or sequence of int, optional
|
|
3417
|
+
Which qubits to measure, defaults (``None``) to all qubits.
|
|
3418
|
+
order : None or sequence of int, optional
|
|
3419
|
+
Which order to measure the qubits in, defaults (``None``) to an
|
|
3420
|
+
order based on greedily expanding the smallest reverse lightcone.
|
|
3421
|
+
If specified it should be a permutation of ``qubits``.
|
|
3422
|
+
group_size : int, optional
|
|
3423
|
+
How many qubits to group together into marginals, the larger this
|
|
3424
|
+
is the fewer marginals need to be computed, which can be faster at
|
|
3425
|
+
the cost of higher memory. The marginal themselves will each be
|
|
3426
|
+
of size ``2**group_size``.
|
|
3427
|
+
max_marginal_storage : int, optional
|
|
3428
|
+
The total cumulative number of marginal probabilites to cache, once
|
|
3429
|
+
this is exceeded caching will be turned off.
|
|
3430
|
+
seed : None or int, optional
|
|
3431
|
+
A random seed, passed to ``numpy.random.seed`` if given.
|
|
3432
|
+
optimize : str, optional
|
|
3433
|
+
Contraction path optimizer to use for the marginals, shouldn't be
|
|
3434
|
+
a non-reusable path optimizer as called on many different TNs.
|
|
3435
|
+
Passed to :func:`cotengra.array_contract_tree`.
|
|
3436
|
+
backend : str, optional
|
|
3437
|
+
Backend to perform the marginal contraction with, e.g. ``'numpy'``,
|
|
3438
|
+
``'cupy'`` or ``'jax'``. Passed to ``cotengra``.
|
|
3439
|
+
dtype : str, optional
|
|
3440
|
+
Data type to cast the TN to before contraction.
|
|
3441
|
+
simplify_sequence : str, optional
|
|
3442
|
+
Which local tensor network simplifications to perform and in which
|
|
3443
|
+
order, see
|
|
3444
|
+
:meth:`~quimb.tensor.tensor_core.TensorNetwork.full_simplify`.
|
|
3445
|
+
simplify_atol : float, optional
|
|
3446
|
+
The tolerance with which to compare to zero when applying
|
|
3447
|
+
:meth:`~quimb.tensor.tensor_core.TensorNetwork.full_simplify`.
|
|
3448
|
+
simplify_equalize_norms : bool, optional
|
|
3449
|
+
Actively renormalize tensor norms during simplification.
|
|
3450
|
+
|
|
3451
|
+
Yields
|
|
3452
|
+
------
|
|
3453
|
+
bitstrings : sequence of str
|
|
3454
|
+
"""
|
|
3455
|
+
# init TN norms, contraction trees, and marginals
|
|
3456
|
+
self._maybe_init_storage()
|
|
3457
|
+
|
|
3458
|
+
rng = np.random.default_rng(seed)
|
|
3459
|
+
|
|
3460
|
+
# which qubits and an ordering e.g. (2, 3, 4, 5), (5, 3, 4, 2)
|
|
3461
|
+
qubits, order = self._parse_qubits_order(qubits, order)
|
|
3462
|
+
|
|
3463
|
+
# group the ordering e.g. ((5, 3), (4, 2))
|
|
3464
|
+
groups = self._group_order(order, group_size)
|
|
3465
|
+
|
|
3466
|
+
result = dict()
|
|
3467
|
+
for _ in range(C):
|
|
3468
|
+
for where in groups:
|
|
3469
|
+
# key - (tuple[int] where, tuple[tuple[int q, str b])
|
|
3470
|
+
# value - marginal probability distribution of `where` given
|
|
3471
|
+
# prior results, as an ndarray
|
|
3472
|
+
# e.g. ((2,), ((0, '0'), (1, '0'))): array([1., 0.]), means
|
|
3473
|
+
# prob(qubit2='0')=1 given qubit0='0' and qubit1='0'
|
|
3474
|
+
# prob(qubit2='1')=0 given qubit0='0' and qubit1='0'
|
|
3475
|
+
key = (where, tuple(sorted(result.items())))
|
|
3476
|
+
if key not in self._sampled_conditionals:
|
|
3477
|
+
# compute p(qs=x | current bitstring)
|
|
3478
|
+
p = self.compute_marginal(
|
|
3479
|
+
where=where,
|
|
3480
|
+
fix=result,
|
|
3481
|
+
optimize=optimize,
|
|
3482
|
+
backend=backend,
|
|
3483
|
+
dtype=dtype,
|
|
3484
|
+
simplify_sequence=simplify_sequence,
|
|
3485
|
+
simplify_atol=simplify_atol,
|
|
3486
|
+
simplify_equalize_norms=simplify_equalize_norms,
|
|
3487
|
+
)
|
|
3488
|
+
p = do("to_numpy", p).astype("float64")
|
|
3489
|
+
p /= p.sum()
|
|
3490
|
+
|
|
3491
|
+
if self._marginal_storage_size <= max_marginal_storage:
|
|
3492
|
+
self._sampled_conditionals[key] = p
|
|
3493
|
+
self._marginal_storage_size += p.size
|
|
3494
|
+
else:
|
|
3495
|
+
p = self._sampled_conditionals[key]
|
|
3496
|
+
|
|
3497
|
+
# the sampled bitstring e.g. '1' or '001010101'
|
|
3498
|
+
b_where = sample_bitstring_from_prob_ndarray(p, seed=rng)
|
|
3499
|
+
|
|
3500
|
+
# split back into individual qubit results
|
|
3501
|
+
for q, b in zip(where, b_where):
|
|
3502
|
+
result[q] = b
|
|
3503
|
+
|
|
3504
|
+
yield "".join(result[i] for i in qubits)
|
|
3505
|
+
result.clear()
|
|
3506
|
+
|
|
3507
|
+
def sample_rehearse(
|
|
3508
|
+
self,
|
|
3509
|
+
qubits=None,
|
|
3510
|
+
order=None,
|
|
3511
|
+
group_size=10,
|
|
3512
|
+
result=None,
|
|
3513
|
+
optimize="auto-hq",
|
|
3514
|
+
simplify_sequence="ADCRS",
|
|
3515
|
+
simplify_atol=1e-6,
|
|
3516
|
+
simplify_equalize_norms=True,
|
|
3517
|
+
rehearse=True,
|
|
3518
|
+
progbar=False,
|
|
3519
|
+
):
|
|
3520
|
+
"""Perform the preparations and contraction tree findings for
|
|
3521
|
+
:meth:`~quimb.tensor.circuit.Circuit.sample`, caching various
|
|
3522
|
+
intermedidate objects, but don't perform the main contractions.
|
|
3523
|
+
|
|
3524
|
+
Parameters
|
|
3525
|
+
----------
|
|
3526
|
+
qubits : None or sequence of int, optional
|
|
3527
|
+
Which qubits to measure, defaults (``None``) to all qubits.
|
|
3528
|
+
order : None or sequence of int, optional
|
|
3529
|
+
Which order to measure the qubits in, defaults (``None``) to an
|
|
3530
|
+
order based on greedily expanding the smallest reverse lightcone.
|
|
3531
|
+
group_size : int, optional
|
|
3532
|
+
How many qubits to group together into marginals, the larger this
|
|
3533
|
+
is the fewer marginals need to be computed, which can be faster at
|
|
3534
|
+
the cost of higher memory. The marginal's size itself is
|
|
3535
|
+
exponential in ``group_size``.
|
|
3536
|
+
result : None or dict[int, str], optional
|
|
3537
|
+
Explicitly check the computational cost of this result, assumed to
|
|
3538
|
+
be all zeros if not given.
|
|
3539
|
+
optimize : str, optional
|
|
3540
|
+
Contraction path optimizer to use for the marginals, shouldn't be
|
|
3541
|
+
a non-reusable path optimizer as called on many different TNs.
|
|
3542
|
+
Passed to :func:`cotengra.array_contract_tree`.
|
|
3543
|
+
simplify_sequence : str, optional
|
|
3544
|
+
Which local tensor network simplifications to perform and in which
|
|
3545
|
+
order, see
|
|
3546
|
+
:meth:`~quimb.tensor.tensor_core.TensorNetwork.full_simplify`.
|
|
3547
|
+
simplify_atol : float, optional
|
|
3548
|
+
The tolerance with which to compare to zero when applying
|
|
3549
|
+
:meth:`~quimb.tensor.tensor_core.TensorNetwork.full_simplify`.
|
|
3550
|
+
simplify_equalize_norms : bool, optional
|
|
3551
|
+
Actively renormalize tensor norms during simplification.
|
|
3552
|
+
progbar : bool, optional
|
|
3553
|
+
Whether to show the progress of finding each contraction tree.
|
|
3554
|
+
|
|
3555
|
+
Returns
|
|
3556
|
+
-------
|
|
3557
|
+
dict[tuple[int], dict]
|
|
3558
|
+
One contraction tree object per grouped marginal computation.
|
|
3559
|
+
The keys of the dict are the qubits the marginal is computed for,
|
|
3560
|
+
the values are a dict containing a representative simplified tensor
|
|
3561
|
+
network (key: 'tn') and the main contraction tree (key: 'tree').
|
|
3562
|
+
"""
|
|
3563
|
+
# init TN norms, contraction trees, and marginals
|
|
3564
|
+
self._maybe_init_storage()
|
|
3565
|
+
qubits, order = self._parse_qubits_order(qubits, order)
|
|
3566
|
+
groups = self._group_order(order, group_size)
|
|
3567
|
+
|
|
3568
|
+
if result is None:
|
|
3569
|
+
result = {q: "r" for q in qubits}
|
|
3570
|
+
|
|
3571
|
+
fix = {}
|
|
3572
|
+
tns_and_trees = {}
|
|
3573
|
+
|
|
3574
|
+
for where in _progbar(groups, disable=not progbar):
|
|
3575
|
+
tns_and_trees[where] = self.compute_marginal(
|
|
3576
|
+
where=where,
|
|
3577
|
+
fix=fix,
|
|
3578
|
+
optimize=optimize,
|
|
3579
|
+
simplify_sequence=simplify_sequence,
|
|
3580
|
+
simplify_atol=simplify_atol,
|
|
3581
|
+
simplify_equalize_norms=simplify_equalize_norms,
|
|
3582
|
+
rehearse=rehearse,
|
|
3583
|
+
)
|
|
3584
|
+
|
|
3585
|
+
# set the result of qubit ``q`` arbitrarily
|
|
3586
|
+
for q in where:
|
|
3587
|
+
fix[q] = result[q]
|
|
3588
|
+
|
|
3589
|
+
return tns_and_trees
|
|
3590
|
+
|
|
3591
|
+
sample_tns = functools.partialmethod(sample_rehearse, rehearse="tn")
|
|
3592
|
+
|
|
3593
|
+
def sample_chaotic(
|
|
3594
|
+
self,
|
|
3595
|
+
C,
|
|
3596
|
+
marginal_qubits,
|
|
3597
|
+
fix=None,
|
|
3598
|
+
max_marginal_storage=2**20,
|
|
3599
|
+
seed=None,
|
|
3600
|
+
optimize="auto-hq",
|
|
3601
|
+
backend=None,
|
|
3602
|
+
dtype="complex64",
|
|
3603
|
+
simplify_sequence="ADCRS",
|
|
3604
|
+
simplify_atol=1e-6,
|
|
3605
|
+
simplify_equalize_norms=True,
|
|
3606
|
+
):
|
|
3607
|
+
r"""Sample from this circuit, *assuming* it to be chaotic. Which is to
|
|
3608
|
+
say, only compute and sample correctly from the final marginal,
|
|
3609
|
+
assuming that the distribution on the other qubits is uniform.
|
|
3610
|
+
Given ``marginal_qubits=5`` for instance, for each sample a random
|
|
3611
|
+
bit-string :math:`r_0 r_1 r_2 \ldots r_{N - 6}` for the remaining
|
|
3612
|
+
:math:`N - 5` qubits will be chosen, then the final marginal will be
|
|
3613
|
+
computed as
|
|
3614
|
+
|
|
3615
|
+
.. math::
|
|
3616
|
+
|
|
3617
|
+
p(q_{N-5}q_{N-4}q_{N-3}q_{N-2}q_{N-1}
|
|
3618
|
+
| r_0 r_1 r_2 \ldots r_{N-6})
|
|
3619
|
+
=
|
|
3620
|
+
|\langle r_0 r_1 r_2 \ldots r_{N - 6} | \psi \rangle|^2
|
|
3621
|
+
|
|
3622
|
+
and then sampled from. Note the expression on the right hand side has
|
|
3623
|
+
5 open indices here and so is a tensor, however if ``marginal_qubits``
|
|
3624
|
+
is not too big then the cost of contracting this is very similar to
|
|
3625
|
+
a single amplitude.
|
|
3626
|
+
|
|
3627
|
+
.. note::
|
|
3628
|
+
|
|
3629
|
+
This method *assumes* the circuit is chaotic, if its not, then the
|
|
3630
|
+
samples produced will not be an accurate representation of the
|
|
3631
|
+
probability distribution.
|
|
3632
|
+
|
|
3633
|
+
Parameters
|
|
3634
|
+
----------
|
|
3635
|
+
C : int
|
|
3636
|
+
The number of times to sample.
|
|
3637
|
+
marginal_qubits : int or sequence of int
|
|
3638
|
+
The number of qubits to treat as marginal, or the actual qubits. If
|
|
3639
|
+
an int is given then the qubits treated as marginal will be
|
|
3640
|
+
``circuit.calc_qubit_ordering()[:marginal_qubits]``.
|
|
3641
|
+
fix : None or dict[int, str], optional
|
|
3642
|
+
Measurement results on other qubits to fix. These will be randomly
|
|
3643
|
+
sampled if ``fix`` is not given or a qubit is missing.
|
|
3644
|
+
seed : None or int, optional
|
|
3645
|
+
A random seed, passed to ``numpy.random.seed`` if given.
|
|
3646
|
+
optimize : str, optional
|
|
3647
|
+
Contraction path optimizer to use for the marginal, can be a
|
|
3648
|
+
non-reusable path optimizer as only called once (though path won't
|
|
3649
|
+
be cached for later use in that case).
|
|
3650
|
+
backend : str, optional
|
|
3651
|
+
Backend to perform the marginal contraction with, e.g. ``'numpy'``,
|
|
3652
|
+
``'cupy'`` or ``'jax'``. Passed to ``cotengra``.
|
|
3653
|
+
dtype : str, optional
|
|
3654
|
+
Data type to cast the TN to before contraction.
|
|
3655
|
+
simplify_sequence : str, optional
|
|
3656
|
+
Which local tensor network simplifications to perform and in which
|
|
3657
|
+
order, see
|
|
3658
|
+
:meth:`~quimb.tensor.tensor_core.TensorNetwork.full_simplify`.
|
|
3659
|
+
simplify_atol : float, optional
|
|
3660
|
+
The tolerance with which to compare to zero when applying
|
|
3661
|
+
:meth:`~quimb.tensor.tensor_core.TensorNetwork.full_simplify`.
|
|
3662
|
+
simplify_equalize_norms : bool, optional
|
|
3663
|
+
Actively renormalize tensor norms during simplification.
|
|
3664
|
+
|
|
3665
|
+
Yields
|
|
3666
|
+
------
|
|
3667
|
+
str
|
|
3668
|
+
"""
|
|
3669
|
+
# init TN norms, contraction trees, and marginals
|
|
3670
|
+
self._maybe_init_storage()
|
|
3671
|
+
qubits = tuple(range(self.N))
|
|
3672
|
+
|
|
3673
|
+
rng = np.random.default_rng(seed)
|
|
3674
|
+
|
|
3675
|
+
# choose which qubits to treat as marginal - ideally 'towards one side'
|
|
3676
|
+
# to increase contraction efficiency
|
|
3677
|
+
if isinstance(marginal_qubits, numbers.Integral):
|
|
3678
|
+
marginal_qubits = self.calc_qubit_ordering()[:marginal_qubits]
|
|
3679
|
+
where = tuple(sorted(marginal_qubits))
|
|
3680
|
+
|
|
3681
|
+
# we will uniformly sample, and post-select on, the remaining qubits
|
|
3682
|
+
fix_qubits = tuple(q for q in qubits if q not in where)
|
|
3683
|
+
|
|
3684
|
+
result = dict()
|
|
3685
|
+
for _ in range(C):
|
|
3686
|
+
# generate a random bit-string for the fixed qubits
|
|
3687
|
+
for q in fix_qubits:
|
|
3688
|
+
if (fix is None) or (q not in fix):
|
|
3689
|
+
result[q] = rng.choice(("0", "1"))
|
|
3690
|
+
else:
|
|
3691
|
+
result[q] = fix[q]
|
|
3692
|
+
|
|
3693
|
+
# compute the remaining marginal
|
|
3694
|
+
key = (where, tuple(sorted(result.items())))
|
|
3695
|
+
if key not in self._sampled_conditionals:
|
|
3696
|
+
p = self.compute_marginal(
|
|
3697
|
+
where=where,
|
|
3698
|
+
fix=result,
|
|
3699
|
+
optimize=optimize,
|
|
3700
|
+
backend=backend,
|
|
3701
|
+
dtype=dtype,
|
|
3702
|
+
simplify_sequence=simplify_sequence,
|
|
3703
|
+
simplify_atol=simplify_atol,
|
|
3704
|
+
simplify_equalize_norms=simplify_equalize_norms,
|
|
3705
|
+
)
|
|
3706
|
+
p = do("to_numpy", p).astype("float64")
|
|
3707
|
+
p /= p.sum()
|
|
3708
|
+
|
|
3709
|
+
if self._marginal_storage_size <= max_marginal_storage:
|
|
3710
|
+
self._sampled_conditionals[key] = p
|
|
3711
|
+
self._marginal_storage_size += p.size
|
|
3712
|
+
else:
|
|
3713
|
+
p = self._sampled_conditionals[key]
|
|
3714
|
+
|
|
3715
|
+
# sample a bit-string for the marginal qubits
|
|
3716
|
+
b_where = sample_bitstring_from_prob_ndarray(p)
|
|
3717
|
+
|
|
3718
|
+
# split back into individual qubit results
|
|
3719
|
+
for q, b in zip(where, b_where):
|
|
3720
|
+
result[q] = b
|
|
3721
|
+
|
|
3722
|
+
yield "".join(result[i] for i in qubits)
|
|
3723
|
+
result.clear()
|
|
3724
|
+
|
|
3725
|
+
def sample_chaotic_rehearse(
|
|
3726
|
+
self,
|
|
3727
|
+
marginal_qubits,
|
|
3728
|
+
result=None,
|
|
3729
|
+
optimize="auto-hq",
|
|
3730
|
+
simplify_sequence="ADCRS",
|
|
3731
|
+
simplify_atol=1e-6,
|
|
3732
|
+
simplify_equalize_norms=True,
|
|
3733
|
+
dtype="complex64",
|
|
3734
|
+
rehearse=True,
|
|
3735
|
+
):
|
|
3736
|
+
"""Rehearse chaotic sampling (perform just the TN simplifications and
|
|
3737
|
+
contraction tree finding).
|
|
3738
|
+
|
|
3739
|
+
Parameters
|
|
3740
|
+
----------
|
|
3741
|
+
marginal_qubits : int or sequence of int
|
|
3742
|
+
The number of qubits to treat as marginal, or the actual qubits. If
|
|
3743
|
+
an int is given then the qubits treated as marginal will be
|
|
3744
|
+
``circuit.calc_qubit_ordering()[:marginal_qubits]``.
|
|
3745
|
+
result : None or dict[int, str], optional
|
|
3746
|
+
Explicitly check the computational cost of this result, assumed to
|
|
3747
|
+
be all zeros if not given.
|
|
3748
|
+
optimize : str, optional
|
|
3749
|
+
Contraction path optimizer to use for the marginal, can be a
|
|
3750
|
+
non-reusable path optimizer as only called once (though path won't
|
|
3751
|
+
be cached for later use in that case).
|
|
3752
|
+
simplify_sequence : str, optional
|
|
3753
|
+
Which local tensor network simplifications to perform and in which
|
|
3754
|
+
order, see
|
|
3755
|
+
:meth:`~quimb.tensor.tensor_core.TensorNetwork.full_simplify`.
|
|
3756
|
+
simplify_atol : float, optional
|
|
3757
|
+
The tolerance with which to compare to zero when applying
|
|
3758
|
+
:meth:`~quimb.tensor.tensor_core.TensorNetwork.full_simplify`.
|
|
3759
|
+
simplify_equalize_norms : bool, optional
|
|
3760
|
+
Actively renormalize tensor norms during simplification.
|
|
3761
|
+
dtype : str, optional
|
|
3762
|
+
Data type to cast the TN to before contraction.
|
|
3763
|
+
|
|
3764
|
+
Returns
|
|
3765
|
+
-------
|
|
3766
|
+
dict[tuple[int], dict]
|
|
3767
|
+
The contraction path information for the main computation, the key
|
|
3768
|
+
is the qubits that formed the final marginal. The value is itself a
|
|
3769
|
+
dict with keys ``'tn'`` - a representative tensor network - and
|
|
3770
|
+
``'tree'`` - the contraction tree.
|
|
3771
|
+
"""
|
|
3772
|
+
|
|
3773
|
+
# init TN norms, contraction trees, and marginals
|
|
3774
|
+
self._maybe_init_storage()
|
|
3775
|
+
qubits = tuple(range(self.N))
|
|
3776
|
+
|
|
3777
|
+
if isinstance(marginal_qubits, numbers.Integral):
|
|
3778
|
+
marginal_qubits = self.calc_qubit_ordering()[:marginal_qubits]
|
|
3779
|
+
where = tuple(sorted(marginal_qubits))
|
|
3780
|
+
|
|
3781
|
+
fix_qubits = tuple(q for q in qubits if q not in where)
|
|
3782
|
+
|
|
3783
|
+
if result is None:
|
|
3784
|
+
fix = {q: "0" for q in fix_qubits}
|
|
3785
|
+
else:
|
|
3786
|
+
fix = {q: result[q] for q in fix_qubits}
|
|
3787
|
+
|
|
3788
|
+
rehs = self.compute_marginal(
|
|
3789
|
+
where=where,
|
|
3790
|
+
fix=fix,
|
|
3791
|
+
optimize=optimize,
|
|
3792
|
+
simplify_sequence=simplify_sequence,
|
|
3793
|
+
simplify_atol=simplify_atol,
|
|
3794
|
+
simplify_equalize_norms=simplify_equalize_norms,
|
|
3795
|
+
dtype=dtype,
|
|
3796
|
+
rehearse=rehearse,
|
|
3797
|
+
)
|
|
3798
|
+
|
|
3799
|
+
if rehearse == "tn":
|
|
3800
|
+
return rehs
|
|
3801
|
+
|
|
3802
|
+
return {where: rehs}
|
|
3803
|
+
|
|
3804
|
+
sample_chaotic_tn = functools.partialmethod(
|
|
3805
|
+
sample_chaotic_rehearse, rehearse="tn"
|
|
3806
|
+
)
|
|
3807
|
+
|
|
3808
|
+
def get_gate_by_gate_circuits(self, group_size=10):
|
|
3809
|
+
"""Get a sequence of circuits by partitioning the gates into groups
|
|
3810
|
+
such circuit `i + 1` acts on at most ``group_size`` new qubits compared
|
|
3811
|
+
to circuit `i`.
|
|
3812
|
+
|
|
3813
|
+
Parameters
|
|
3814
|
+
----------
|
|
3815
|
+
group_size : int, optional
|
|
3816
|
+
The maximum number of new qubits that can be acted on by a circuit
|
|
3817
|
+
compared to its predecessor.
|
|
3818
|
+
|
|
3819
|
+
Returns
|
|
3820
|
+
-------
|
|
3821
|
+
Sequence[dict]
|
|
3822
|
+
A sequence of dicts, each with keys ``'circuit'`` and ``'where'``,
|
|
3823
|
+
where the former is a :class:`~quimb.tensor.circuit.Circuit` and
|
|
3824
|
+
the latter the tuple of new qubits that it acts on comparaed to
|
|
3825
|
+
the previous circuit.
|
|
3826
|
+
"""
|
|
3827
|
+
circs = [self.__class__(self.N)]
|
|
3828
|
+
groups = []
|
|
3829
|
+
current_group = set()
|
|
3830
|
+
|
|
3831
|
+
# this ensures that single qubit gates are always adjacent to
|
|
3832
|
+
# multi-qubit gates and will thus always be included in the same group
|
|
3833
|
+
gates = self.reordered_gates_dfs_clustered()
|
|
3834
|
+
|
|
3835
|
+
for gate in gates:
|
|
3836
|
+
# if we were to add next gate, how many new qubits would we have?
|
|
3837
|
+
next_group = current_group.union(gate.qubits)
|
|
3838
|
+
if len(next_group) > group_size:
|
|
3839
|
+
# over the limit: flush a copy of the current circuit and group
|
|
3840
|
+
groups.append(tuple(sorted(current_group)))
|
|
3841
|
+
circs.append(circs[-1].copy())
|
|
3842
|
+
# start a new group
|
|
3843
|
+
current_group = set(gate.qubits)
|
|
3844
|
+
else:
|
|
3845
|
+
# add the gate to the current group
|
|
3846
|
+
current_group = next_group
|
|
3847
|
+
circs[-1].apply_gate(gate)
|
|
3848
|
+
|
|
3849
|
+
# add the final group corresponding to circs[-1]
|
|
3850
|
+
groups.append(tuple(sorted(current_group)))
|
|
3851
|
+
|
|
3852
|
+
return tuple({"circuit": c, "where": g} for c, g in zip(circs, groups))
|
|
3853
|
+
|
|
3854
|
+
def sample_gate_by_gate(
|
|
3855
|
+
self,
|
|
3856
|
+
C,
|
|
3857
|
+
group_size=10,
|
|
3858
|
+
seed=None,
|
|
3859
|
+
max_marginal_storage=2**20,
|
|
3860
|
+
optimize="auto-hq",
|
|
3861
|
+
backend=None,
|
|
3862
|
+
dtype="complex64",
|
|
3863
|
+
simplify_sequence="ADCRS",
|
|
3864
|
+
simplify_atol=1e-6,
|
|
3865
|
+
simplify_equalize_norms=True,
|
|
3866
|
+
):
|
|
3867
|
+
"""Sample this circuit using the gate-by-gate method, where we 'evolve'
|
|
3868
|
+
a result bitstring by sequentially including more and more gates, at
|
|
3869
|
+
each step updating the result by computing a full conditional marginal.
|
|
3870
|
+
See "How to simulate quantum measurement without computing marginals"
|
|
3871
|
+
by Sergey Bravyi, David Gosset, Yinchen Liu
|
|
3872
|
+
(https://arxiv.org/abs/2112.08499). The overall complexity of this is
|
|
3873
|
+
guaranteed to be similar to that of computing a single amplitude which
|
|
3874
|
+
can be much better than the naive "qubit-by-qubit" (`.sample`) method.
|
|
3875
|
+
However, it requires evaluting a number of tensor networks that scales
|
|
3876
|
+
linearly with the number of gates which can offset any practical
|
|
3877
|
+
advantages for shallow circuits for example.
|
|
3878
|
+
|
|
3879
|
+
Parameters
|
|
3880
|
+
----------
|
|
3881
|
+
C : int
|
|
3882
|
+
The number of samples to generate.
|
|
3883
|
+
group_size : int, optional
|
|
3884
|
+
The maximum number of qubits that can be acted on by a circuit
|
|
3885
|
+
compared to its predecessor. This will be the dimension of the
|
|
3886
|
+
marginal computed at each step.
|
|
3887
|
+
seed : None or int, optional
|
|
3888
|
+
A random seed, passed to ``numpy.random.seed`` if given.
|
|
3889
|
+
max_marginal_storage : int, optional
|
|
3890
|
+
The total cumulative number of marginal probabilites to cache, once
|
|
3891
|
+
this is exceeded caching will be turned off.
|
|
3892
|
+
optimize : str, optional
|
|
3893
|
+
Contraction path optimizer to use for the marginals, shouldn't be
|
|
3894
|
+
a non-reusable path optimizer as called on many different TNs.
|
|
3895
|
+
Passed to :func:`cotengra.array_contract_tree`.
|
|
3896
|
+
backend : str, optional
|
|
3897
|
+
Backend to perform the marginal contraction with, e.g. ``'numpy'``,
|
|
3898
|
+
``'cupy'`` or ``'jax'``. Passed to ``cotengra``.
|
|
3899
|
+
dtype : str, optional
|
|
3900
|
+
Data type to cast the TN to before contraction.
|
|
3901
|
+
simplify_sequence : str, optional
|
|
3902
|
+
Which local tensor network simplifications to perform and in which
|
|
3903
|
+
order, see
|
|
3904
|
+
:meth:`~quimb.tensor.tensor_core.TensorNetwork.full_simplify`.
|
|
3905
|
+
simplify_atol : float, optional
|
|
3906
|
+
The tolerance with which to compare to zero when applying
|
|
3907
|
+
:meth:`~quimb.tensor.tensor_core.TensorNetwork.full_simplify`.
|
|
3908
|
+
simplify_equalize_norms : bool, optional
|
|
3909
|
+
Actively renormalize tensor norms during simplification.
|
|
3910
|
+
rehearse : bool, optional
|
|
3911
|
+
If ``True``, generate and cache the simplified tensor network and
|
|
3912
|
+
contraction tree but don't actually perform the contraction.
|
|
3913
|
+
Returns a dict with keys ``'tn'`` and ``'tree'`` with the tensor
|
|
3914
|
+
network that will be contracted and the corresponding contraction
|
|
3915
|
+
tree if so.
|
|
3916
|
+
|
|
3917
|
+
Yields
|
|
3918
|
+
------
|
|
3919
|
+
str
|
|
3920
|
+
"""
|
|
3921
|
+
self._maybe_init_storage()
|
|
3922
|
+
|
|
3923
|
+
rng = np.random.default_rng(seed)
|
|
3924
|
+
|
|
3925
|
+
key = ("gate_by_gate_circuits", group_size)
|
|
3926
|
+
try:
|
|
3927
|
+
circs_wheres = self._storage[key]
|
|
3928
|
+
except KeyError:
|
|
3929
|
+
circs_wheres = self.get_gate_by_gate_circuits(group_size)
|
|
3930
|
+
self._storage[key] = circs_wheres
|
|
3931
|
+
|
|
3932
|
+
for _ in range(C):
|
|
3933
|
+
# start with all qubits in the |0> state
|
|
3934
|
+
result = {q: "0" for q in range(self.N)}
|
|
3935
|
+
|
|
3936
|
+
for circ_where in circs_wheres:
|
|
3937
|
+
# get the next circuit and the new group of qubits
|
|
3938
|
+
circ_g = circ_where["circuit"]
|
|
3939
|
+
where = circ_where["where"]
|
|
3940
|
+
|
|
3941
|
+
# remove the new group of qubits from our current result
|
|
3942
|
+
for q in where:
|
|
3943
|
+
result.pop(q)
|
|
3944
|
+
|
|
3945
|
+
# check if we have already computed the conditional
|
|
3946
|
+
key = (where, tuple(sorted(result.items())))
|
|
3947
|
+
|
|
3948
|
+
if key not in circ_g._sampled_conditionals:
|
|
3949
|
+
p = circ_g.compute_marginal(
|
|
3950
|
+
where,
|
|
3951
|
+
fix=result,
|
|
3952
|
+
optimize=optimize,
|
|
3953
|
+
backend=backend,
|
|
3954
|
+
dtype=dtype,
|
|
3955
|
+
simplify_sequence=simplify_sequence,
|
|
3956
|
+
simplify_atol=simplify_atol,
|
|
3957
|
+
simplify_equalize_norms=simplify_equalize_norms,
|
|
3958
|
+
)
|
|
3959
|
+
p /= p.sum()
|
|
3960
|
+
|
|
3961
|
+
if circ_g._marginal_storage_size <= max_marginal_storage:
|
|
3962
|
+
circ_g._sampled_conditionals[key] = p
|
|
3963
|
+
circ_g._marginal_storage_size += p.size
|
|
3964
|
+
else:
|
|
3965
|
+
p = circ_g._sampled_conditionals[key]
|
|
3966
|
+
|
|
3967
|
+
# sample a configuration for our new group
|
|
3968
|
+
b_where = sample_bitstring_from_prob_ndarray(p, seed=rng)
|
|
3969
|
+
|
|
3970
|
+
# update the fixed qubits given new group result
|
|
3971
|
+
for q, qx in zip(where, b_where):
|
|
3972
|
+
result[q] = qx
|
|
3973
|
+
|
|
3974
|
+
yield "".join(result[i] for i in range(self.N))
|
|
3975
|
+
|
|
3976
|
+
def sample_gate_by_gate_rehearse(
|
|
3977
|
+
self,
|
|
3978
|
+
group_size=10,
|
|
3979
|
+
optimize="auto-hq",
|
|
3980
|
+
dtype="complex64",
|
|
3981
|
+
simplify_sequence="ADCRS",
|
|
3982
|
+
simplify_atol=1e-6,
|
|
3983
|
+
simplify_equalize_norms=True,
|
|
3984
|
+
rehearse=True,
|
|
3985
|
+
progbar=False,
|
|
3986
|
+
):
|
|
3987
|
+
"""Perform the preparations and contraction tree findings for
|
|
3988
|
+
:meth:`~quimb.tensor.circuit.Circuit.sample_gate_by_gate`, caching
|
|
3989
|
+
various intermedidate objects, but don't perform the main contractions.
|
|
3990
|
+
|
|
3991
|
+
Parameters
|
|
3992
|
+
----------
|
|
3993
|
+
group_size : int, optional
|
|
3994
|
+
The maximum number of qubits that can be acted on by a circuit
|
|
3995
|
+
compared to its predecessor. This will be the dimension of the
|
|
3996
|
+
marginal computed at each step.
|
|
3997
|
+
optimize : str, optional
|
|
3998
|
+
Contraction path optimizer to use for the marginals, shouldn't be
|
|
3999
|
+
a non-reusable path optimizer as called on many different TNs.
|
|
4000
|
+
Passed to :func:`cotengra.array_contract_tree`.
|
|
4001
|
+
dtype : str, optional
|
|
4002
|
+
Data type to cast the TN to before contraction.
|
|
4003
|
+
simplify_sequence : str, optional
|
|
4004
|
+
Which local tensor network simplifications to perform and in which
|
|
4005
|
+
order, see
|
|
4006
|
+
:meth:`~quimb.tensor.tensor_core.TensorNetwork.full_simplify`.
|
|
4007
|
+
simplify_atol : float, optional
|
|
4008
|
+
The tolerance with which to compare to zero when applying
|
|
4009
|
+
:meth:`~quimb.tensor.tensor_core.TensorNetwork.full_simplify`.
|
|
4010
|
+
simplify_equalize_norms : bool, optional
|
|
4011
|
+
Actively renormalize tensor norms during simplification.
|
|
4012
|
+
rehearse : True or "tn", optional
|
|
4013
|
+
If ``True``, generate and cache the simplified tensor network and
|
|
4014
|
+
contraction tree but don't actually perform the contraction. If
|
|
4015
|
+
"tn", only generate the simplified tensor networks.
|
|
4016
|
+
|
|
4017
|
+
Returns
|
|
4018
|
+
-------
|
|
4019
|
+
Sequence[dict] or Sequence[TensorNetwork]
|
|
4020
|
+
"""
|
|
4021
|
+
self._maybe_init_storage()
|
|
4022
|
+
|
|
4023
|
+
key = ("gate_by_gate_circuits", group_size)
|
|
4024
|
+
try:
|
|
4025
|
+
circs_wheres = self._storage[key]
|
|
4026
|
+
except KeyError:
|
|
4027
|
+
circs_wheres = self.get_gate_by_gate_circuits(group_size)
|
|
4028
|
+
self._storage[key] = circs_wheres
|
|
4029
|
+
|
|
4030
|
+
rehs = []
|
|
4031
|
+
result = {q: "0" for q in range(self.N)}
|
|
4032
|
+
|
|
4033
|
+
for circs_wheres in _progbar(circs_wheres, disable=not progbar):
|
|
4034
|
+
# get the next circuit and the new group of qubits
|
|
4035
|
+
circ_g = circs_wheres["circuit"]
|
|
4036
|
+
where = circs_wheres["where"]
|
|
4037
|
+
|
|
4038
|
+
# remove the new group of qubits from our current result
|
|
4039
|
+
for q in where:
|
|
4040
|
+
result.pop(q)
|
|
4041
|
+
|
|
4042
|
+
r = circ_g.compute_marginal(
|
|
4043
|
+
where,
|
|
4044
|
+
fix=result,
|
|
4045
|
+
optimize=optimize,
|
|
4046
|
+
dtype=dtype,
|
|
4047
|
+
simplify_sequence=simplify_sequence,
|
|
4048
|
+
simplify_atol=simplify_atol,
|
|
4049
|
+
simplify_equalize_norms=simplify_equalize_norms,
|
|
4050
|
+
rehearse=rehearse,
|
|
4051
|
+
)
|
|
4052
|
+
|
|
4053
|
+
if rehearse != "tn":
|
|
4054
|
+
r["where"] = where
|
|
4055
|
+
r["circuit"] = circ_g
|
|
4056
|
+
|
|
4057
|
+
rehs.append(r)
|
|
4058
|
+
|
|
4059
|
+
# update the fixed qubits with randomly rotated results so we
|
|
4060
|
+
# don't get zero probability networks when simplifying
|
|
4061
|
+
for q in where:
|
|
4062
|
+
result[q] = "r"
|
|
4063
|
+
|
|
4064
|
+
return rehs
|
|
4065
|
+
|
|
4066
|
+
sample_gate_by_gate_tns = functools.partialmethod(
|
|
4067
|
+
sample_gate_by_gate_rehearse, rehearse="tn"
|
|
4068
|
+
)
|
|
4069
|
+
|
|
4070
|
+
def to_dense(
|
|
4071
|
+
self,
|
|
4072
|
+
reverse=False,
|
|
4073
|
+
optimize="auto-hq",
|
|
4074
|
+
simplify_sequence="R",
|
|
4075
|
+
simplify_atol=1e-12,
|
|
4076
|
+
simplify_equalize_norms=True,
|
|
4077
|
+
backend=None,
|
|
4078
|
+
dtype=None,
|
|
4079
|
+
rehearse=False,
|
|
4080
|
+
):
|
|
4081
|
+
"""Generate the dense representation of the final wavefunction.
|
|
4082
|
+
|
|
4083
|
+
Parameters
|
|
4084
|
+
----------
|
|
4085
|
+
reverse : bool, optional
|
|
4086
|
+
Whether to reverse the order of the subsystems, to match the
|
|
4087
|
+
convention of qiskit for example.
|
|
4088
|
+
optimize : str, optional
|
|
4089
|
+
Contraction path optimizer to use for the contraction, can be a
|
|
4090
|
+
non-reusable path optimizer as only called once (though path won't
|
|
4091
|
+
be cached for later use in that case).
|
|
4092
|
+
dtype : dtype or str, optional
|
|
4093
|
+
If given, convert the tensors to this dtype prior to contraction.
|
|
4094
|
+
simplify_sequence : str, optional
|
|
4095
|
+
Which local tensor network simplifications to perform and in which
|
|
4096
|
+
order, see
|
|
4097
|
+
:meth:`~quimb.tensor.tensor_core.TensorNetwork.full_simplify`.
|
|
4098
|
+
simplify_atol : float, optional
|
|
4099
|
+
The tolerance with which to compare to zero when applying
|
|
4100
|
+
:meth:`~quimb.tensor.tensor_core.TensorNetwork.full_simplify`.
|
|
4101
|
+
simplify_equalize_norms : bool, optional
|
|
4102
|
+
Actively renormalize tensor norms during simplification.
|
|
4103
|
+
backend : str, optional
|
|
4104
|
+
Backend to perform the contraction with, e.g. ``'numpy'``,
|
|
4105
|
+
``'cupy'`` or ``'jax'``. Passed to ``cotengra``.
|
|
4106
|
+
dtype : str, optional
|
|
4107
|
+
Data type to cast the TN to before contraction.
|
|
4108
|
+
rehearse : bool, optional
|
|
4109
|
+
If ``True``, generate and cache the simplified tensor network and
|
|
4110
|
+
contraction tree but don't actually perform the contraction.
|
|
4111
|
+
Returns a dict with keys ``'tn'`` and ``'tree'`` with the tensor
|
|
4112
|
+
network that will be contracted and the corresponding contraction
|
|
4113
|
+
tree if so.
|
|
4114
|
+
|
|
4115
|
+
Returns
|
|
4116
|
+
-------
|
|
4117
|
+
psi : qarray
|
|
4118
|
+
The densely represented wavefunction with ``dtype`` data.
|
|
4119
|
+
"""
|
|
4120
|
+
psi = self.get_psi_simplified(
|
|
4121
|
+
seq=simplify_sequence,
|
|
4122
|
+
atol=simplify_atol,
|
|
4123
|
+
equalize_norms=simplify_equalize_norms,
|
|
4124
|
+
)
|
|
4125
|
+
|
|
4126
|
+
if dtype is not None:
|
|
4127
|
+
psi.astype_(dtype)
|
|
4128
|
+
|
|
4129
|
+
if rehearse == "tn":
|
|
4130
|
+
return psi
|
|
4131
|
+
|
|
4132
|
+
output_inds = tuple(map(psi.site_ind, range(self.N)))
|
|
4133
|
+
if reverse:
|
|
4134
|
+
output_inds = output_inds[::-1]
|
|
4135
|
+
|
|
4136
|
+
tree = psi.contraction_tree(output_inds=output_inds, optimize=optimize)
|
|
4137
|
+
|
|
4138
|
+
if rehearse:
|
|
4139
|
+
return rehearsal_dict(psi, tree)
|
|
4140
|
+
|
|
4141
|
+
# perform the full contraction with the path found
|
|
4142
|
+
psi_tensor = psi.contract(
|
|
4143
|
+
all,
|
|
4144
|
+
output_inds=output_inds,
|
|
4145
|
+
optimize=tree,
|
|
4146
|
+
backend=backend,
|
|
4147
|
+
).data
|
|
4148
|
+
|
|
4149
|
+
k = ops.reshape(psi_tensor, (-1, 1))
|
|
4150
|
+
|
|
4151
|
+
if isinstance(k, np.ndarray):
|
|
4152
|
+
k = qu.qarray(k)
|
|
4153
|
+
|
|
4154
|
+
return k
|
|
4155
|
+
|
|
4156
|
+
to_dense_rehearse = functools.partialmethod(to_dense, rehearse=True)
|
|
4157
|
+
to_dense_tn = functools.partialmethod(to_dense, rehearse="tn")
|
|
4158
|
+
|
|
4159
|
+
def simulate_counts(self, C, seed=None, reverse=False, **to_dense_opts):
|
|
4160
|
+
"""Simulate measuring all qubits in the computational basis many times.
|
|
4161
|
+
Unlike :meth:`~quimb.tensor.circuit.Circuit.sample`, this generates all
|
|
4162
|
+
the samples simultaneously using the full wavefunction constructed from
|
|
4163
|
+
:meth:`~quimb.tensor.circuit.Circuit.to_dense`, then calling
|
|
4164
|
+
:func:`~quimb.calc.simulate_counts`.
|
|
4165
|
+
|
|
4166
|
+
.. warning::
|
|
4167
|
+
|
|
4168
|
+
Because this constructs the full wavefunction it always requires
|
|
4169
|
+
exponential memory in the number of qubits, regardless of circuit
|
|
4170
|
+
depth and structure.
|
|
4171
|
+
|
|
4172
|
+
Parameters
|
|
4173
|
+
----------
|
|
4174
|
+
C : int
|
|
4175
|
+
The number of 'experimental runs', i.e. total counts.
|
|
4176
|
+
seed : int, optional
|
|
4177
|
+
A seed for reproducibility.
|
|
4178
|
+
reverse : bool, optional
|
|
4179
|
+
Whether to reverse the order of the subsystems, to match the
|
|
4180
|
+
convention of qiskit for example.
|
|
4181
|
+
to_dense_opts
|
|
4182
|
+
Suppled to :meth:`~quimb.tensor.circuit.Circuit.to_dense`.
|
|
4183
|
+
|
|
4184
|
+
Returns
|
|
4185
|
+
-------
|
|
4186
|
+
results : dict[str, int]
|
|
4187
|
+
The number of recorded counts for each
|
|
4188
|
+
"""
|
|
4189
|
+
p_dense = self.to_dense(reverse=reverse, **to_dense_opts)
|
|
4190
|
+
return qu.simulate_counts(p_dense, C=C, seed=seed)
|
|
4191
|
+
|
|
4192
|
+
def schrodinger_contract(self, *args, **contract_opts):
|
|
4193
|
+
ntensor = self._psi.num_tensors
|
|
4194
|
+
path = [(0, 1)] + [(0, i) for i in reversed(range(1, ntensor - 1))]
|
|
4195
|
+
return self.psi.contract(*args, optimize=path, **contract_opts)
|
|
4196
|
+
|
|
4197
|
+
def xeb(
|
|
4198
|
+
self,
|
|
4199
|
+
samples_or_counts,
|
|
4200
|
+
cache=None,
|
|
4201
|
+
cache_maxsize=2**20,
|
|
4202
|
+
progbar=False,
|
|
4203
|
+
**amplitude_opts,
|
|
4204
|
+
):
|
|
4205
|
+
"""Compute the linear cross entropy benchmark (XEB) for samples or
|
|
4206
|
+
counts, amplitude per amplitude.
|
|
4207
|
+
|
|
4208
|
+
Parameters
|
|
4209
|
+
----------
|
|
4210
|
+
samples_or_counts : Iterable[str] or Dict[str, int]
|
|
4211
|
+
Either the raw bitstring samples or a dict mapping bitstrings to
|
|
4212
|
+
the number of counts observed.
|
|
4213
|
+
cache : dict, optional
|
|
4214
|
+
A dictionary to store the probabilities in, if not supplied
|
|
4215
|
+
``quimb.utils.LRU(cache_maxsize)`` will be used.
|
|
4216
|
+
cache_maxsize, optional
|
|
4217
|
+
The maximum size of the cache to be used.
|
|
4218
|
+
progbar, optional
|
|
4219
|
+
Whether to show progress as the bitstrings are iterated over.
|
|
4220
|
+
amplitude_opts
|
|
4221
|
+
Supplied to :meth:`~quimb.tensor.circuit.Circuit.amplitude`.
|
|
4222
|
+
"""
|
|
4223
|
+
try:
|
|
4224
|
+
it = samples_or_counts.items()
|
|
4225
|
+
except AttributeError:
|
|
4226
|
+
it = zip(samples_or_counts, itertools.repeat(1))
|
|
4227
|
+
|
|
4228
|
+
if progbar:
|
|
4229
|
+
it = _progbar(it)
|
|
4230
|
+
|
|
4231
|
+
M = 0
|
|
4232
|
+
psum = 0.0
|
|
4233
|
+
|
|
4234
|
+
if cache is None:
|
|
4235
|
+
cache = LRU(cache_maxsize)
|
|
4236
|
+
|
|
4237
|
+
for b, cnt in it:
|
|
4238
|
+
try:
|
|
4239
|
+
p = cache[b]
|
|
4240
|
+
except KeyError:
|
|
4241
|
+
p = cache[b] = abs(self.amplitude(b, **amplitude_opts)) ** 2
|
|
4242
|
+
psum += cnt * p
|
|
4243
|
+
M += cnt
|
|
4244
|
+
|
|
4245
|
+
return (2**self.N) / M * psum - 1
|
|
4246
|
+
|
|
4247
|
+
def xeb_ex(
|
|
4248
|
+
self,
|
|
4249
|
+
optimize="auto-hq",
|
|
4250
|
+
simplify_sequence="R",
|
|
4251
|
+
simplify_atol=1e-12,
|
|
4252
|
+
simplify_equalize_norms=True,
|
|
4253
|
+
dtype=None,
|
|
4254
|
+
backend=None,
|
|
4255
|
+
autojit=False,
|
|
4256
|
+
progbar=False,
|
|
4257
|
+
**contract_opts,
|
|
4258
|
+
):
|
|
4259
|
+
"""Compute the exactly expected XEB for this circuit. The main feature
|
|
4260
|
+
here is that if you supply a cotengra optimizer that searches for
|
|
4261
|
+
sliced indices then the XEB will be computed without constructing the
|
|
4262
|
+
full wavefunction.
|
|
4263
|
+
|
|
4264
|
+
Parameters
|
|
4265
|
+
----------
|
|
4266
|
+
optimize : str or PathOptimizer, optional
|
|
4267
|
+
Contraction path optimizer.
|
|
4268
|
+
simplify_sequence : str, optional
|
|
4269
|
+
Simplifications to apply to tensor network prior to contraction.
|
|
4270
|
+
simplify_sequence : str, optional
|
|
4271
|
+
Which local tensor network simplifications to perform and in which
|
|
4272
|
+
order, see
|
|
4273
|
+
:meth:`~quimb.tensor.tensor_core.TensorNetwork.full_simplify`.
|
|
4274
|
+
simplify_atol : float, optional
|
|
4275
|
+
The tolerance with which to compare to zero when applying
|
|
4276
|
+
:meth:`~quimb.tensor.tensor_core.TensorNetwork.full_simplify`.
|
|
4277
|
+
dtype : str, optional
|
|
4278
|
+
Data type to cast the TN to before contraction.
|
|
4279
|
+
backend : str, optional
|
|
4280
|
+
Convert tensors to, and then use contractions from, this library.
|
|
4281
|
+
autojit : bool, optional
|
|
4282
|
+
Apply ``autoray.autojit`` to the contraciton and map-reduce.
|
|
4283
|
+
progbar : bool, optional
|
|
4284
|
+
Show progress in terms of number of wavefunction chunks processed.
|
|
4285
|
+
"""
|
|
4286
|
+
# get potentially simplified TN of full wavefunction
|
|
4287
|
+
psi = self.to_dense_tn(
|
|
4288
|
+
simplify_sequence=simplify_sequence,
|
|
4289
|
+
simplify_atol=simplify_atol,
|
|
4290
|
+
simplify_equalize_norms=simplify_equalize_norms,
|
|
4291
|
+
dtype=dtype,
|
|
4292
|
+
)
|
|
4293
|
+
|
|
4294
|
+
# find a possibly sliced contraction tree
|
|
4295
|
+
output_inds = tuple(map(psi.site_ind, range(self.N)))
|
|
4296
|
+
tree = psi.contraction_tree(optimize=optimize, output_inds=output_inds)
|
|
4297
|
+
|
|
4298
|
+
arrays = psi.arrays
|
|
4299
|
+
if backend is not None:
|
|
4300
|
+
arrays = [do("array", x, like=backend) for x in arrays]
|
|
4301
|
+
|
|
4302
|
+
# perform map-reduce style computation over output wavefunction chunks
|
|
4303
|
+
# so we don't need entire wavefunction in memory at same time
|
|
4304
|
+
chunks = tree.gen_output_chunks(
|
|
4305
|
+
arrays, autojit=autojit, **contract_opts
|
|
4306
|
+
)
|
|
4307
|
+
if progbar:
|
|
4308
|
+
chunks = _progbar(chunks, total=tree.nchunks)
|
|
4309
|
+
|
|
4310
|
+
def f(chunk):
|
|
4311
|
+
return do("sum", do("abs", chunk) ** 4)
|
|
4312
|
+
|
|
4313
|
+
if autojit:
|
|
4314
|
+
# since we convert the arrays above, the jit backend is
|
|
4315
|
+
# automatically inferred
|
|
4316
|
+
from autoray import autojit
|
|
4317
|
+
|
|
4318
|
+
f = autojit(f)
|
|
4319
|
+
|
|
4320
|
+
p2sum = functools.reduce(operator.add, map(f, chunks))
|
|
4321
|
+
return 2**self.N * p2sum - 1
|
|
4322
|
+
|
|
4323
|
+
def update_params_from(self, tn):
|
|
4324
|
+
"""Assuming ``tn`` is a tensor network with tensors tagged ``GATE_{i}``
|
|
4325
|
+
corresponding to this circuit (e.g. from ``circ.psi`` or ``circ.uni``)
|
|
4326
|
+
but with updated parameters, update the current circuit parameters and
|
|
4327
|
+
tensors with those values.
|
|
4328
|
+
|
|
4329
|
+
This is an inplace modification of the ``Circuit``.
|
|
4330
|
+
|
|
4331
|
+
Parameters
|
|
4332
|
+
----------
|
|
4333
|
+
tn : TensorNetwork
|
|
4334
|
+
The tensor network to find the updated parameters from.
|
|
4335
|
+
"""
|
|
4336
|
+
for i, gate in enumerate(self._gates):
|
|
4337
|
+
tag = self.gate_tag(i)
|
|
4338
|
+
t = tn[tag]
|
|
4339
|
+
|
|
4340
|
+
# sanity check that tensor(s) `t` correspond to the correct gate
|
|
4341
|
+
if gate.tag not in get_tags(t):
|
|
4342
|
+
raise ValueError(
|
|
4343
|
+
f"The tensor(s) correponding to gate {i} "
|
|
4344
|
+
f"should be tagged with '{gate.tag}', got {t}."
|
|
4345
|
+
)
|
|
4346
|
+
|
|
4347
|
+
# only update gates and tensors if they are parametrizable
|
|
4348
|
+
if isinstance(t, PTensor):
|
|
4349
|
+
# update the actual tensor
|
|
4350
|
+
self._psi[tag].params = t.params
|
|
4351
|
+
|
|
4352
|
+
# update the circuit's gate record
|
|
4353
|
+
self._gates[i] = Gate(
|
|
4354
|
+
label=gate.label,
|
|
4355
|
+
params=t.params,
|
|
4356
|
+
qubits=gate.qubits,
|
|
4357
|
+
round=gate.round,
|
|
4358
|
+
parametrize=True,
|
|
4359
|
+
)
|
|
4360
|
+
|
|
4361
|
+
self.clear_storage()
|
|
4362
|
+
|
|
4363
|
+
def draw(
|
|
4364
|
+
self,
|
|
4365
|
+
figsize=None,
|
|
4366
|
+
radius=1 / 3,
|
|
4367
|
+
drawcolor=(0.5, 0.5, 0.5),
|
|
4368
|
+
linewidth=1,
|
|
4369
|
+
):
|
|
4370
|
+
"""Draw a simple linear schematic of the circuit.
|
|
4371
|
+
|
|
4372
|
+
Parameters
|
|
4373
|
+
----------
|
|
4374
|
+
figsize : tuple, optional
|
|
4375
|
+
The size of the figure, if not given will be set based on the
|
|
4376
|
+
number of gates and qubits.
|
|
4377
|
+
radius : float, optional
|
|
4378
|
+
The radius of the gates.
|
|
4379
|
+
drawcolor : tuple, optional
|
|
4380
|
+
The color of the wires.
|
|
4381
|
+
linewidth : float, optional
|
|
4382
|
+
The linewidth of the wires.
|
|
4383
|
+
|
|
4384
|
+
Returns
|
|
4385
|
+
-------
|
|
4386
|
+
fig : matplotlib.Figure
|
|
4387
|
+
The figure object.
|
|
4388
|
+
ax : matplotlib.Axes
|
|
4389
|
+
The axis object.
|
|
4390
|
+
"""
|
|
4391
|
+
from quimb.schematic import Drawing, hash_to_color
|
|
4392
|
+
|
|
4393
|
+
if figsize is None:
|
|
4394
|
+
figsize = (self.num_gates / 6, self.N / 6)
|
|
4395
|
+
|
|
4396
|
+
d = Drawing(
|
|
4397
|
+
figsize=figsize,
|
|
4398
|
+
presets=dict(
|
|
4399
|
+
wire=dict(
|
|
4400
|
+
color=drawcolor,
|
|
4401
|
+
linewidth=linewidth,
|
|
4402
|
+
),
|
|
4403
|
+
gate=dict(
|
|
4404
|
+
radius=radius,
|
|
4405
|
+
),
|
|
4406
|
+
),
|
|
4407
|
+
)
|
|
4408
|
+
|
|
4409
|
+
depths = {}
|
|
4410
|
+
for i, g in enumerate(self.gates):
|
|
4411
|
+
# level = max(depths.get(q, 0) for q in g.qubits) + 1
|
|
4412
|
+
level = i
|
|
4413
|
+
|
|
4414
|
+
if len(g.qubits) == 1:
|
|
4415
|
+
(q,) = g.qubits
|
|
4416
|
+
# draw line from previous gate to this one
|
|
4417
|
+
d.line(
|
|
4418
|
+
(depths.get(q, -1) + radius, q),
|
|
4419
|
+
(level - radius, q),
|
|
4420
|
+
preset="wire",
|
|
4421
|
+
zorder=level,
|
|
4422
|
+
)
|
|
4423
|
+
# draw the gate
|
|
4424
|
+
d.marker(
|
|
4425
|
+
(level, q),
|
|
4426
|
+
color=hash_to_color(g.label),
|
|
4427
|
+
zorder=0,
|
|
4428
|
+
preset="gate",
|
|
4429
|
+
)
|
|
4430
|
+
# record last gate on this qubit
|
|
4431
|
+
depths[q] = level
|
|
4432
|
+
else:
|
|
4433
|
+
# stretch a box over all qubits
|
|
4434
|
+
qmin = min(g.qubits)
|
|
4435
|
+
qmax = max(g.qubits)
|
|
4436
|
+
d.rectangle(
|
|
4437
|
+
(level, qmin),
|
|
4438
|
+
(level, qmax),
|
|
4439
|
+
color=hash_to_color(g.label),
|
|
4440
|
+
zorder=0,
|
|
4441
|
+
alpha=1 / 3,
|
|
4442
|
+
preset="gate",
|
|
4443
|
+
)
|
|
4444
|
+
for q in g.qubits:
|
|
4445
|
+
# draw markers on each qubit acted on
|
|
4446
|
+
d.marker(
|
|
4447
|
+
(level, q),
|
|
4448
|
+
color=hash_to_color(g.label),
|
|
4449
|
+
zorder=0,
|
|
4450
|
+
preset="gate",
|
|
4451
|
+
)
|
|
4452
|
+
# draw lines from previous gate to this one
|
|
4453
|
+
d.line(
|
|
4454
|
+
(depths.get(q, -1) + radius, q),
|
|
4455
|
+
(level - radius, q),
|
|
4456
|
+
preset="wire",
|
|
4457
|
+
zorder=level,
|
|
4458
|
+
)
|
|
4459
|
+
# record last gate on this qubit
|
|
4460
|
+
depths[q] = level
|
|
4461
|
+
|
|
4462
|
+
# draw final lines to the right
|
|
4463
|
+
level = max(depths.values(), default=0) + 1
|
|
4464
|
+
for q in depths:
|
|
4465
|
+
d.line((depths.get(q, -1), q), (level, q), preset="wire")
|
|
4466
|
+
|
|
4467
|
+
return d.fig, d.ax
|
|
4468
|
+
|
|
4469
|
+
def __repr__(self):
|
|
4470
|
+
r = "<Circuit(n={}, num_gates={}, gate_opts={})>"
|
|
4471
|
+
return r.format(self.N, self.num_gates, self.gate_opts)
|
|
4472
|
+
|
|
4473
|
+
|
|
4474
|
+
class CircuitMPS(Circuit):
|
|
4475
|
+
"""Quantum circuit simulation keeping the state always in a MPS form. If
|
|
4476
|
+
you think the circuit will not build up much entanglement, or you just want
|
|
4477
|
+
to keep a rigorous handle on how much entanglement is present, this can
|
|
4478
|
+
be useful.
|
|
4479
|
+
|
|
4480
|
+
Parameters
|
|
4481
|
+
----------
|
|
4482
|
+
N : int, optional
|
|
4483
|
+
The number of qubits in the circuit.
|
|
4484
|
+
psi0 : TensorNetwork1DVector, optional
|
|
4485
|
+
The initial state, assumed to be ``|00000....0>`` if not given. The
|
|
4486
|
+
state is always copied and the tag ``PSI0`` added.
|
|
4487
|
+
max_bond : int, optional
|
|
4488
|
+
The maximum bond dimension to truncate to when applying gates, if any.
|
|
4489
|
+
This is simply a shortcut for setting ``gate_opts['max_bond']``.
|
|
4490
|
+
cutoff : float, optional
|
|
4491
|
+
The singular value cutoff to use when truncating the state.
|
|
4492
|
+
This is simply a shortcut for setting ``gate_opts['cutoff']``.
|
|
4493
|
+
gate_opts : dict, optional
|
|
4494
|
+
Default options to pass to each gate, for example, "max_bond" and
|
|
4495
|
+
"cutoff" etc.
|
|
4496
|
+
gate_contract : str, optional
|
|
4497
|
+
The default method for applying gates. Relevant MPS options are:
|
|
4498
|
+
|
|
4499
|
+
- ``'auto-mps'``: automatically choose a method that maintains the
|
|
4500
|
+
MPS form (default). This uses ``'swap+split'`` for 2-qubit gates
|
|
4501
|
+
and ``'nonlocal'`` for 3+ qubit gates.
|
|
4502
|
+
- ``'swap+split'``: swap nonlocal qubits to be next to each other,
|
|
4503
|
+
before applying the gate, then swapping them back
|
|
4504
|
+
- ``'nonlocal'``: turn the gate into a potentially nonlocal (sub) MPO
|
|
4505
|
+
and apply it directly. See :func:`tensor_network_1d_compress`.
|
|
4506
|
+
|
|
4507
|
+
circuit_opts
|
|
4508
|
+
Supplied to :class:`~quimb.tensor.circuit.Circuit`.
|
|
4509
|
+
|
|
4510
|
+
Attributes
|
|
4511
|
+
----------
|
|
4512
|
+
psi : MatrixProductState
|
|
4513
|
+
The current state of the circuit, always in MPS form.
|
|
4514
|
+
|
|
4515
|
+
Examples
|
|
4516
|
+
--------
|
|
4517
|
+
|
|
4518
|
+
Create a circuit object that always uses the "nonlocal" method for
|
|
4519
|
+
contracting in gates, and the "dm" compression method within that, using
|
|
4520
|
+
a large cutoff and maximum bond dimension::
|
|
4521
|
+
|
|
4522
|
+
circ = qtn.CircuitMPS(
|
|
4523
|
+
N=56,
|
|
4524
|
+
gate_opts=dict(
|
|
4525
|
+
contract="nonlocal",
|
|
4526
|
+
method="dm",
|
|
4527
|
+
max_bond=1024,
|
|
4528
|
+
cutoff=1e-3,
|
|
4529
|
+
)
|
|
4530
|
+
)
|
|
4531
|
+
|
|
4532
|
+
"""
|
|
4533
|
+
|
|
4534
|
+
def __init__(
|
|
4535
|
+
self,
|
|
4536
|
+
N=None,
|
|
4537
|
+
*,
|
|
4538
|
+
psi0=None,
|
|
4539
|
+
max_bond=None,
|
|
4540
|
+
cutoff=1e-10,
|
|
4541
|
+
gate_opts=None,
|
|
4542
|
+
gate_contract="auto-mps",
|
|
4543
|
+
**circuit_opts,
|
|
4544
|
+
):
|
|
4545
|
+
gate_opts = ensure_dict(gate_opts)
|
|
4546
|
+
gate_opts.setdefault("contract", gate_contract)
|
|
4547
|
+
gate_opts.setdefault("propagate_tags", False)
|
|
4548
|
+
gate_opts.setdefault("max_bond", max_bond)
|
|
4549
|
+
gate_opts.setdefault("cutoff", cutoff)
|
|
4550
|
+
# this is used to pass around the canonical form
|
|
4551
|
+
gate_opts.setdefault("info", {})
|
|
4552
|
+
|
|
4553
|
+
circuit_opts.setdefault("tag_gate_numbers", False)
|
|
4554
|
+
circuit_opts.setdefault("tag_gate_rounds", False)
|
|
4555
|
+
circuit_opts.setdefault("tag_gate_labels", False)
|
|
4556
|
+
|
|
4557
|
+
super().__init__(N, psi0, gate_opts, **circuit_opts)
|
|
4558
|
+
|
|
4559
|
+
def _init_state(self, N, dtype="complex128"):
|
|
4560
|
+
return MPS_computational_state("0" * N, dtype=dtype)
|
|
4561
|
+
|
|
4562
|
+
def apply_gates(self, gates, progbar=False, **gate_opts):
|
|
4563
|
+
if progbar:
|
|
4564
|
+
from ..utils import progbar as _progbar
|
|
4565
|
+
|
|
4566
|
+
gates = tuple(gates)
|
|
4567
|
+
gates = _progbar(gates, total=len(gates))
|
|
4568
|
+
gates.set_description(
|
|
4569
|
+
f"max_bond={self._psi.max_bond()}, "
|
|
4570
|
+
f"error~={self.error_estimate():.3g}"
|
|
4571
|
+
)
|
|
4572
|
+
|
|
4573
|
+
for gate in gates:
|
|
4574
|
+
if isinstance(gate, Gate):
|
|
4575
|
+
self._apply_gate(gate, **gate_opts)
|
|
4576
|
+
else:
|
|
4577
|
+
self.apply_gate(*gate, **gate_opts)
|
|
4578
|
+
|
|
4579
|
+
if progbar and (gate.total_qubit_count >= 2):
|
|
4580
|
+
# these don't change for single qubit gates
|
|
4581
|
+
gates.set_description(
|
|
4582
|
+
f"max_bond={self._psi.max_bond()}, "
|
|
4583
|
+
f"error~={self.error_estimate():.3g}"
|
|
4584
|
+
)
|
|
4585
|
+
|
|
4586
|
+
@property
|
|
4587
|
+
def psi(self):
|
|
4588
|
+
# no squeeze so that bond dims of 1 preserved
|
|
4589
|
+
return self._psi.copy()
|
|
4590
|
+
|
|
4591
|
+
@property
|
|
4592
|
+
def uni(self):
|
|
4593
|
+
raise ValueError(
|
|
4594
|
+
"You can't extract the circuit unitary TN from a ``CircuitMPS``."
|
|
4595
|
+
)
|
|
4596
|
+
|
|
4597
|
+
def calc_qubit_ordering(self, qubits=None):
|
|
4598
|
+
"""MPS already has a natural ordering."""
|
|
4599
|
+
if qubits is None:
|
|
4600
|
+
return tuple(range(self.N))
|
|
4601
|
+
else:
|
|
4602
|
+
return tuple(sorted(qubits))
|
|
4603
|
+
|
|
4604
|
+
def get_psi_reverse_lightcone(self, where, keep_psi0=False):
|
|
4605
|
+
"""Override ``get_psi_reverse_lightcone`` as for an MPS the lightcone
|
|
4606
|
+
is not meaningful.
|
|
4607
|
+
"""
|
|
4608
|
+
return self.psi
|
|
4609
|
+
|
|
4610
|
+
def sample(
|
|
4611
|
+
self,
|
|
4612
|
+
C,
|
|
4613
|
+
seed=None,
|
|
4614
|
+
):
|
|
4615
|
+
"""Sample the MPS circuit ``C`` times.
|
|
4616
|
+
|
|
4617
|
+
Parameters
|
|
4618
|
+
----------
|
|
4619
|
+
C : int
|
|
4620
|
+
The number of samples to generate.
|
|
4621
|
+
seed : None, int, or generator, optional
|
|
4622
|
+
A random seed or generator to use for reproducibility.
|
|
4623
|
+
"""
|
|
4624
|
+
for config, _ in self._psi.sample(C, seed=seed):
|
|
4625
|
+
yield "".join(map(str, config))
|
|
4626
|
+
|
|
4627
|
+
def fidelity_estimate(self):
|
|
4628
|
+
r"""Estimate the fidelity of the current state based on its norm, which
|
|
4629
|
+
tracks how much the state has been truncated:
|
|
4630
|
+
|
|
4631
|
+
.. math::
|
|
4632
|
+
|
|
4633
|
+
\tilde{F} =
|
|
4634
|
+
\left| \langle \psi | \psi \rangle \right|^2
|
|
4635
|
+
\approx
|
|
4636
|
+
\left|\langle \psi_\mathrm{ideal} | \psi \rangle\right|^2
|
|
4637
|
+
|
|
4638
|
+
See Also
|
|
4639
|
+
--------
|
|
4640
|
+
error_estimate
|
|
4641
|
+
"""
|
|
4642
|
+
cur_orthog = self.gate_opts["info"].get("cur_orthog", None)
|
|
4643
|
+
|
|
4644
|
+
if cur_orthog is None:
|
|
4645
|
+
return abs(self._psi.norm()) ** 2
|
|
4646
|
+
|
|
4647
|
+
cmin, cmax = cur_orthog
|
|
4648
|
+
return abs(self._psi[cmin : cmax + 1].norm(tags=all)) ** 2
|
|
4649
|
+
|
|
4650
|
+
def error_estimate(self):
|
|
4651
|
+
r"""Estimate the error in the current state based on the norm of the
|
|
4652
|
+
discarded part of the state:
|
|
4653
|
+
|
|
4654
|
+
.. math::
|
|
4655
|
+
|
|
4656
|
+
\epsilon = 1 - \tilde{F}
|
|
4657
|
+
|
|
4658
|
+
See Also
|
|
4659
|
+
--------
|
|
4660
|
+
fidelity_estimate
|
|
4661
|
+
"""
|
|
4662
|
+
return 1 - self.fidelity_estimate()
|
|
4663
|
+
|
|
4664
|
+
def local_expectation(
|
|
4665
|
+
self,
|
|
4666
|
+
G,
|
|
4667
|
+
where,
|
|
4668
|
+
normalized=False,
|
|
4669
|
+
**contract_opts,
|
|
4670
|
+
):
|
|
4671
|
+
"""Compute the local expectation value of a local operator at ``where``
|
|
4672
|
+
(via forming the reduced density matrix). Note this moves the
|
|
4673
|
+
orthogonality around inplace, and records it in `info`.
|
|
4674
|
+
|
|
4675
|
+
Parameters
|
|
4676
|
+
----------
|
|
4677
|
+
G : Tensor
|
|
4678
|
+
The local operator tensor.
|
|
4679
|
+
where : int
|
|
4680
|
+
The qubit to compute the expectation value at.
|
|
4681
|
+
|
|
4682
|
+
Returns
|
|
4683
|
+
-------
|
|
4684
|
+
float
|
|
4685
|
+
"""
|
|
4686
|
+
return self._psi.local_expectation_canonical(
|
|
4687
|
+
G,
|
|
4688
|
+
where,
|
|
4689
|
+
normalized=normalized,
|
|
4690
|
+
info=self.gate_opts["info"],
|
|
4691
|
+
**contract_opts,
|
|
4692
|
+
)
|
|
4693
|
+
|
|
4694
|
+
|
|
4695
|
+
class CircuitPermMPS(CircuitMPS):
|
|
4696
|
+
"""Quantum circuit simulation keeping the state always in an MPS form, but
|
|
4697
|
+
lazily tracking the qubit ordering rather than 'swapping back' qubits after
|
|
4698
|
+
applying non-local gates. This can be useful for circuits with no
|
|
4699
|
+
expectation of locality. The qubit ordering is always tracked in the
|
|
4700
|
+
attribute ``qubits``. The ``psi`` attribute returns the TN with the sites
|
|
4701
|
+
reindexed and retagged according to the current qubit ordering, meaning it
|
|
4702
|
+
is no longer an MPS. Use `circ.get_psi_unordered()` to get the unpermuted
|
|
4703
|
+
MPS and use `circ.qubits` to get the current qubit ordering if you prefer.
|
|
4704
|
+
"""
|
|
4705
|
+
|
|
4706
|
+
def __init__(
|
|
4707
|
+
self,
|
|
4708
|
+
N=None,
|
|
4709
|
+
psi0=None,
|
|
4710
|
+
gate_opts=None,
|
|
4711
|
+
gate_contract="swap+split",
|
|
4712
|
+
**circuit_opts,
|
|
4713
|
+
):
|
|
4714
|
+
gate_opts = ensure_dict(gate_opts)
|
|
4715
|
+
gate_opts.setdefault("contract", gate_contract)
|
|
4716
|
+
# this is used to pass around the canonical form
|
|
4717
|
+
gate_opts.setdefault("info", {})
|
|
4718
|
+
super().__init__(N, psi0=psi0, gate_opts=gate_opts, **circuit_opts)
|
|
4719
|
+
# keep track of the current qubit ordering
|
|
4720
|
+
self.qubits = list(range(self.N))
|
|
4721
|
+
|
|
4722
|
+
def _apply_gate(self, gate, tags=None, **gate_opts):
|
|
4723
|
+
# first translate gate qubits to their current 'physical' location
|
|
4724
|
+
qubits = gate.qubits
|
|
4725
|
+
phys_sites = [self.qubits.index(q) for q in qubits]
|
|
4726
|
+
gate = gate.copy_with(qubits=phys_sites)
|
|
4727
|
+
|
|
4728
|
+
# if the gate is non-local, account for swap (without swap back)
|
|
4729
|
+
if len(phys_sites) == 2:
|
|
4730
|
+
i, j = sorted(phys_sites)
|
|
4731
|
+
q = self.qubits.pop(j)
|
|
4732
|
+
self.qubits.insert(i + 1, q)
|
|
4733
|
+
gate_opts["swap_back"] = False
|
|
4734
|
+
|
|
4735
|
+
super()._apply_gate(gate, tags=tags, **gate_opts)
|
|
4736
|
+
|
|
4737
|
+
def calc_qubit_ordering(self, qubits=None):
|
|
4738
|
+
"""Given by the current qubit permutation."""
|
|
4739
|
+
if qubits is None:
|
|
4740
|
+
return tuple(self.qubits)
|
|
4741
|
+
else:
|
|
4742
|
+
return tuple(sorted(qubits, key=self.qubits.index))
|
|
4743
|
+
|
|
4744
|
+
def get_psi_unordered(self):
|
|
4745
|
+
"""Return the MPS representing the state but without reordering the
|
|
4746
|
+
sites.
|
|
4747
|
+
"""
|
|
4748
|
+
return self._psi.copy()
|
|
4749
|
+
|
|
4750
|
+
def sample(self, C, seed=None):
|
|
4751
|
+
"""Sample the PermMPS circuit ``C`` times.
|
|
4752
|
+
|
|
4753
|
+
Parameters
|
|
4754
|
+
----------
|
|
4755
|
+
C : int
|
|
4756
|
+
The number of samples to generate.
|
|
4757
|
+
seed : None, int, or generator, optional
|
|
4758
|
+
A random seed or generator to use for reproducibility.
|
|
4759
|
+
|
|
4760
|
+
Yields
|
|
4761
|
+
------
|
|
4762
|
+
str
|
|
4763
|
+
The next sample bitstring.
|
|
4764
|
+
"""
|
|
4765
|
+
# configuring is in physical order, so need to reorder for sampling
|
|
4766
|
+
ordering = self.calc_qubit_ordering()
|
|
4767
|
+
for config, _ in self._psi.sample(C, seed=seed):
|
|
4768
|
+
yield "".join(str(config[i]) for i in ordering)
|
|
4769
|
+
|
|
4770
|
+
@property
|
|
4771
|
+
def psi(self):
|
|
4772
|
+
# need to reindex and retag the MPS
|
|
4773
|
+
psi = self._psi.copy()
|
|
4774
|
+
psi.view_as_(TensorNetworkGenVector)
|
|
4775
|
+
psi.reindex_(
|
|
4776
|
+
{
|
|
4777
|
+
psi.site_ind(i): psi.site_ind(q)
|
|
4778
|
+
for i, q in enumerate(self.qubits)
|
|
4779
|
+
}
|
|
4780
|
+
)
|
|
4781
|
+
psi.retag_(
|
|
4782
|
+
{
|
|
4783
|
+
psi.site_tag(i): psi.site_tag(q)
|
|
4784
|
+
for i, q in enumerate(self.qubits)
|
|
4785
|
+
}
|
|
4786
|
+
)
|
|
4787
|
+
return psi
|
|
4788
|
+
|
|
4789
|
+
|
|
4790
|
+
class CircuitDense(Circuit):
|
|
4791
|
+
"""Quantum circuit simulation keeping the state in full dense form."""
|
|
4792
|
+
|
|
4793
|
+
def __init__(
|
|
4794
|
+
self, N=None, psi0=None, gate_opts=None, gate_contract=True, tags=None
|
|
4795
|
+
):
|
|
4796
|
+
gate_opts = ensure_dict(gate_opts)
|
|
4797
|
+
gate_opts.setdefault("contract", gate_contract)
|
|
4798
|
+
super().__init__(N, psi0, gate_opts, tags)
|
|
4799
|
+
|
|
4800
|
+
@property
|
|
4801
|
+
def psi(self):
|
|
4802
|
+
t = self._psi ^ ...
|
|
4803
|
+
psi = t.as_network()
|
|
4804
|
+
psi.view_as_(Dense1D, like=self._psi)
|
|
4805
|
+
return psi
|
|
4806
|
+
|
|
4807
|
+
@property
|
|
4808
|
+
def uni(self):
|
|
4809
|
+
raise ValueError(
|
|
4810
|
+
"You can't extract the circuit unitary TN from a ``CircuitDense``."
|
|
4811
|
+
)
|
|
4812
|
+
|
|
4813
|
+
def calc_qubit_ordering(self, qubits=None):
|
|
4814
|
+
"""Qubit ordering doesn't matter for a dense wavefunction."""
|
|
4815
|
+
if qubits is None:
|
|
4816
|
+
return tuple(range(self.N))
|
|
4817
|
+
else:
|
|
4818
|
+
return tuple(sorted(qubits))
|
|
4819
|
+
|
|
4820
|
+
def get_psi_reverse_lightcone(self, where, keep_psi0=False):
|
|
4821
|
+
"""Override ``get_psi_reverse_lightcone`` as for a dense wavefunction
|
|
4822
|
+
the lightcone is not meaningful.
|
|
4823
|
+
"""
|
|
4824
|
+
return self.psi
|