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.
Files changed (122) hide show
  1. trajectree/__init__.py +3 -0
  2. trajectree/fock_optics/devices.py +1 -1
  3. trajectree/fock_optics/light_sources.py +2 -2
  4. trajectree/fock_optics/measurement.py +3 -3
  5. trajectree/fock_optics/utils.py +6 -6
  6. trajectree/quimb/docs/_pygments/_pygments_dark.py +118 -0
  7. trajectree/quimb/docs/_pygments/_pygments_light.py +118 -0
  8. trajectree/quimb/docs/conf.py +158 -0
  9. trajectree/quimb/docs/examples/ex_mpi_expm_evo.py +62 -0
  10. trajectree/quimb/quimb/__init__.py +507 -0
  11. trajectree/quimb/quimb/calc.py +1491 -0
  12. trajectree/quimb/quimb/core.py +2279 -0
  13. trajectree/quimb/quimb/evo.py +712 -0
  14. trajectree/quimb/quimb/experimental/__init__.py +0 -0
  15. trajectree/quimb/quimb/experimental/autojittn.py +129 -0
  16. trajectree/quimb/quimb/experimental/belief_propagation/__init__.py +109 -0
  17. trajectree/quimb/quimb/experimental/belief_propagation/bp_common.py +397 -0
  18. trajectree/quimb/quimb/experimental/belief_propagation/d1bp.py +316 -0
  19. trajectree/quimb/quimb/experimental/belief_propagation/d2bp.py +653 -0
  20. trajectree/quimb/quimb/experimental/belief_propagation/hd1bp.py +571 -0
  21. trajectree/quimb/quimb/experimental/belief_propagation/hv1bp.py +775 -0
  22. trajectree/quimb/quimb/experimental/belief_propagation/l1bp.py +316 -0
  23. trajectree/quimb/quimb/experimental/belief_propagation/l2bp.py +537 -0
  24. trajectree/quimb/quimb/experimental/belief_propagation/regions.py +194 -0
  25. trajectree/quimb/quimb/experimental/cluster_update.py +286 -0
  26. trajectree/quimb/quimb/experimental/merabuilder.py +865 -0
  27. trajectree/quimb/quimb/experimental/operatorbuilder/__init__.py +15 -0
  28. trajectree/quimb/quimb/experimental/operatorbuilder/operatorbuilder.py +1631 -0
  29. trajectree/quimb/quimb/experimental/schematic.py +7 -0
  30. trajectree/quimb/quimb/experimental/tn_marginals.py +130 -0
  31. trajectree/quimb/quimb/experimental/tnvmc.py +1483 -0
  32. trajectree/quimb/quimb/gates.py +36 -0
  33. trajectree/quimb/quimb/gen/__init__.py +2 -0
  34. trajectree/quimb/quimb/gen/operators.py +1167 -0
  35. trajectree/quimb/quimb/gen/rand.py +713 -0
  36. trajectree/quimb/quimb/gen/states.py +479 -0
  37. trajectree/quimb/quimb/linalg/__init__.py +6 -0
  38. trajectree/quimb/quimb/linalg/approx_spectral.py +1109 -0
  39. trajectree/quimb/quimb/linalg/autoblock.py +258 -0
  40. trajectree/quimb/quimb/linalg/base_linalg.py +719 -0
  41. trajectree/quimb/quimb/linalg/mpi_launcher.py +397 -0
  42. trajectree/quimb/quimb/linalg/numpy_linalg.py +244 -0
  43. trajectree/quimb/quimb/linalg/rand_linalg.py +514 -0
  44. trajectree/quimb/quimb/linalg/scipy_linalg.py +293 -0
  45. trajectree/quimb/quimb/linalg/slepc_linalg.py +892 -0
  46. trajectree/quimb/quimb/schematic.py +1518 -0
  47. trajectree/quimb/quimb/tensor/__init__.py +401 -0
  48. trajectree/quimb/quimb/tensor/array_ops.py +610 -0
  49. trajectree/quimb/quimb/tensor/circuit.py +4824 -0
  50. trajectree/quimb/quimb/tensor/circuit_gen.py +411 -0
  51. trajectree/quimb/quimb/tensor/contraction.py +336 -0
  52. trajectree/quimb/quimb/tensor/decomp.py +1255 -0
  53. trajectree/quimb/quimb/tensor/drawing.py +1646 -0
  54. trajectree/quimb/quimb/tensor/fitting.py +385 -0
  55. trajectree/quimb/quimb/tensor/geometry.py +583 -0
  56. trajectree/quimb/quimb/tensor/interface.py +114 -0
  57. trajectree/quimb/quimb/tensor/networking.py +1058 -0
  58. trajectree/quimb/quimb/tensor/optimize.py +1818 -0
  59. trajectree/quimb/quimb/tensor/tensor_1d.py +4778 -0
  60. trajectree/quimb/quimb/tensor/tensor_1d_compress.py +1854 -0
  61. trajectree/quimb/quimb/tensor/tensor_1d_tebd.py +662 -0
  62. trajectree/quimb/quimb/tensor/tensor_2d.py +5954 -0
  63. trajectree/quimb/quimb/tensor/tensor_2d_compress.py +96 -0
  64. trajectree/quimb/quimb/tensor/tensor_2d_tebd.py +1230 -0
  65. trajectree/quimb/quimb/tensor/tensor_3d.py +2869 -0
  66. trajectree/quimb/quimb/tensor/tensor_3d_tebd.py +46 -0
  67. trajectree/quimb/quimb/tensor/tensor_approx_spectral.py +60 -0
  68. trajectree/quimb/quimb/tensor/tensor_arbgeom.py +3237 -0
  69. trajectree/quimb/quimb/tensor/tensor_arbgeom_compress.py +565 -0
  70. trajectree/quimb/quimb/tensor/tensor_arbgeom_tebd.py +1138 -0
  71. trajectree/quimb/quimb/tensor/tensor_builder.py +5411 -0
  72. trajectree/quimb/quimb/tensor/tensor_core.py +11179 -0
  73. trajectree/quimb/quimb/tensor/tensor_dmrg.py +1472 -0
  74. trajectree/quimb/quimb/tensor/tensor_mera.py +204 -0
  75. trajectree/quimb/quimb/utils.py +892 -0
  76. trajectree/quimb/tests/__init__.py +0 -0
  77. trajectree/quimb/tests/test_accel.py +501 -0
  78. trajectree/quimb/tests/test_calc.py +788 -0
  79. trajectree/quimb/tests/test_core.py +847 -0
  80. trajectree/quimb/tests/test_evo.py +565 -0
  81. trajectree/quimb/tests/test_gen/__init__.py +0 -0
  82. trajectree/quimb/tests/test_gen/test_operators.py +361 -0
  83. trajectree/quimb/tests/test_gen/test_rand.py +296 -0
  84. trajectree/quimb/tests/test_gen/test_states.py +261 -0
  85. trajectree/quimb/tests/test_linalg/__init__.py +0 -0
  86. trajectree/quimb/tests/test_linalg/test_approx_spectral.py +368 -0
  87. trajectree/quimb/tests/test_linalg/test_base_linalg.py +351 -0
  88. trajectree/quimb/tests/test_linalg/test_mpi_linalg.py +127 -0
  89. trajectree/quimb/tests/test_linalg/test_numpy_linalg.py +84 -0
  90. trajectree/quimb/tests/test_linalg/test_rand_linalg.py +134 -0
  91. trajectree/quimb/tests/test_linalg/test_slepc_linalg.py +283 -0
  92. trajectree/quimb/tests/test_tensor/__init__.py +0 -0
  93. trajectree/quimb/tests/test_tensor/test_belief_propagation/__init__.py +0 -0
  94. trajectree/quimb/tests/test_tensor/test_belief_propagation/test_d1bp.py +39 -0
  95. trajectree/quimb/tests/test_tensor/test_belief_propagation/test_d2bp.py +67 -0
  96. trajectree/quimb/tests/test_tensor/test_belief_propagation/test_hd1bp.py +64 -0
  97. trajectree/quimb/tests/test_tensor/test_belief_propagation/test_hv1bp.py +51 -0
  98. trajectree/quimb/tests/test_tensor/test_belief_propagation/test_l1bp.py +142 -0
  99. trajectree/quimb/tests/test_tensor/test_belief_propagation/test_l2bp.py +101 -0
  100. trajectree/quimb/tests/test_tensor/test_circuit.py +816 -0
  101. trajectree/quimb/tests/test_tensor/test_contract.py +67 -0
  102. trajectree/quimb/tests/test_tensor/test_decomp.py +40 -0
  103. trajectree/quimb/tests/test_tensor/test_mera.py +52 -0
  104. trajectree/quimb/tests/test_tensor/test_optimizers.py +488 -0
  105. trajectree/quimb/tests/test_tensor/test_tensor_1d.py +1171 -0
  106. trajectree/quimb/tests/test_tensor/test_tensor_2d.py +606 -0
  107. trajectree/quimb/tests/test_tensor/test_tensor_2d_tebd.py +144 -0
  108. trajectree/quimb/tests/test_tensor/test_tensor_3d.py +123 -0
  109. trajectree/quimb/tests/test_tensor/test_tensor_arbgeom.py +226 -0
  110. trajectree/quimb/tests/test_tensor/test_tensor_builder.py +441 -0
  111. trajectree/quimb/tests/test_tensor/test_tensor_core.py +2066 -0
  112. trajectree/quimb/tests/test_tensor/test_tensor_dmrg.py +388 -0
  113. trajectree/quimb/tests/test_tensor/test_tensor_spectral_approx.py +63 -0
  114. trajectree/quimb/tests/test_tensor/test_tensor_tebd.py +270 -0
  115. trajectree/quimb/tests/test_utils.py +85 -0
  116. trajectree/trajectory.py +2 -2
  117. {trajectree-0.0.0.dist-info → trajectree-0.0.1.dist-info}/METADATA +2 -2
  118. trajectree-0.0.1.dist-info/RECORD +126 -0
  119. trajectree-0.0.0.dist-info/RECORD +0 -16
  120. {trajectree-0.0.0.dist-info → trajectree-0.0.1.dist-info}/WHEEL +0 -0
  121. {trajectree-0.0.0.dist-info → trajectree-0.0.1.dist-info}/licenses/LICENSE +0 -0
  122. {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