Files
qibotn/.venv/lib/python3.12/site-packages/quimb/tensor/circuit.py
jaunatisblue f93c95b3a1
Some checks failed
Build wheels / build (ubuntu-latest, 3.11) (push) Has been cancelled
Build wheels / build (ubuntu-latest, 3.12) (push) Has been cancelled
Build wheels / build (ubuntu-latest, 3.13) (push) Has been cancelled
Tests / check (push) Has been cancelled
Tests / build (ubuntu-latest, 3.11) (push) Has been cancelled
Tests / build (ubuntu-latest, 3.12) (push) Has been cancelled
Tests / build (ubuntu-latest, 3.13) (push) Has been cancelled
代码封装
2026-05-18 22:58:57 +08:00

5411 lines
172 KiB
Python

"""Tools for quantum circuit simulation using tensor networks."""
import cmath
import functools
import itertools
import math
import numbers
import operator
import re
import warnings
import numpy as np
from autoray import astype, backend_like, do, get_dtype_name, reshape
import quimb as qu
from ..utils import (
LRU,
concatv,
deprecated,
ensure_dict,
partition_all,
partitionby,
tree_map,
)
from ..utils import progbar as _progbar
from . import array_ops as ops
from .tensor_builder import (
HTN_CP_operator_from_products,
MPO_identity_like,
MPS_computational_state,
TN_from_sites_computational_state,
)
from .tensor_core import (
PTensor,
Tensor,
TensorNetwork,
get_tags,
oset_union,
rand_uuid,
tags_to_oset,
tensor_contract,
)
from .tn1d.core import Dense1D, MatrixProductOperator
from .tnag.core import TensorNetworkGenOperator, TensorNetworkGenVector
def recursive_stack(x):
if not isinstance(x, (list, tuple)):
return x
return do("stack", tuple(map(recursive_stack, x)))
def _convert_ints_and_floats(x):
if isinstance(x, str):
try:
return int(x)
except ValueError:
pass
try:
return float(x)
except ValueError:
pass
return x
def _put_registers_last(x):
# no need to do anything unless parameter (i.e. float) is found last
if not isinstance(x[-1], float):
return x
# swap this last group of floats with the penultimate group of integers
parts = tuple(partitionby(type, x))
return tuple(concatv(*parts[:-2], parts[-1], parts[-2]))
def parse_qsim_str(contents):
"""Parse a 'qsim' input format string into circuit information.
The format is described here: https://quantumai.google/qsim/input_format.
Parameters
----------
contents : str
The full string of the qsim file.
Returns
-------
circuit_info : dict
Information about the circuit:
- circuit_info['n']: the number of qubits
- circuit_info['n_gates']: the number of gates in total
- circuit_info['gates']: list[list[str]], list of gates, each of which
is a list of strings read from a line of the qsim file.
"""
lines = contents.split("\n")
n = int(lines[0])
# turn into tuples of python types
gates = [
tuple(map(_convert_ints_and_floats, line.strip().split(" ")))
for line in lines[1:]
if line
]
# put registers/parameters in standard order and detect if gate round used
gates = tuple(map(_put_registers_last, gates))
round_specified = isinstance(gates[0][0], numbers.Integral)
return {
"n": n,
"gates": gates,
"n_gates": len(gates),
"round_specified": round_specified,
}
def parse_qsim_file(fname, **kwargs):
"""Parse a qsim file."""
with open(fname) as f:
return parse_qsim_str(f.read(), **kwargs)
def parse_qsim_url(url, **kwargs):
"""Parse a qsim url."""
from urllib import request
return parse_qsim_str(request.urlopen(url).read().decode(), **kwargs)
def to_clean_list(s, delimiter):
"""Split, strip and filter a string by a given character into a list."""
if s is None:
return []
return list(filter(None, (w.strip() for w in s.split(delimiter))))
def multi_replace(s, replacements):
"""Replace multiple substrings in a string."""
for w, r in replacements.items():
s = s.replace(w, r)
return s
@functools.lru_cache(None)
def get_openqasm2_regexes():
return {
"header": re.compile(r"(OPENQASM\s+2.0;)|(include\s+\"qelib1.inc\";)"),
"comment": re.compile(r"^//"),
"comment_start": re.compile(r"/\*"),
"comment_end": re.compile(r"\*/"),
"qreg": re.compile(r"qreg\s+(\w+)\s*\[(\d+)\];"),
"gate": re.compile(r"(\w+)\s*(\((.+)\))?\s*(.*);"),
"error": re.compile(r"^(if|for)"),
"ignore": re.compile(r"^(creg|measure|barrier)"),
"gate_def": re.compile(r"^gate\s+"),
"gate_sig": re.compile(r"^gate\s+(\w+)\s*(\((.+)\))?\s*(.*)"),
}
def parse_openqasm2_str(contents):
"""Parse the string contents of an OpenQASM 2.0 file. This parser does not
support classical control flow is not guaranteed to check the full openqasm
grammar.
"""
# define regular expressions for parsing
rgxs = get_openqasm2_regexes()
# initialise number of qubits to zero and an empty list for gates
sitemap = {}
gates = []
custom_gates = {}
# only want to warn once about each ignored instruction
warned = {}
# Process each line
in_comment = False
lines = contents.split("\n")
while lines:
line = lines.pop(0).strip()
if not line:
# blank line
continue
if rgxs["comment"].match(line):
# single comment
continue
if rgxs["comment_start"].match(line):
# start of multiline comments
in_comment = True
if in_comment:
# in multiline comment, check if its ending
in_comment = not bool(rgxs["comment_end"].match(line))
continue
if rgxs["header"].match(line):
# ignore standard header lines
continue
match = rgxs["qreg"].match(line)
if match:
# quantum register -> extend sites
name, nq = match.groups()
for i in range(int(nq)):
sitemap[f"{name}[{i}]"] = len(sitemap)
continue
match = rgxs["ignore"].match(line)
if match:
# certain operations we can just ignore and warn about
(op,) = match.groups()
if not warned.get(op, False):
warnings.warn(
f"Unsupported operation ignored: {op}", SyntaxWarning
)
warned[op] = True
continue
if rgxs["error"].match(line):
# raise hard error for custom tate defns etc
raise NotImplementedError(
f"The following instruction is not supported: {line}"
)
if rgxs["gate_def"].match(line):
# custom gate definition:
# first gather all lines involved in the gate definition
gate_lines = [line]
while True:
if "}" in line:
# finished -> break
break
else:
# not finished -> need next line
line = lines.pop(0)
gate_lines.append(line)
# then combine this full gate definition, without newlines
gate_body = "".join(gate_lines)
# separate the signature and body
gate_sig, gate_body = re.match(
r"(.*)\s*{(.*)}", gate_body
).groups()
# parse the signature
match = rgxs["gate_sig"].match(gate_sig)
label = match[1]
sig_params = to_clean_list(match[3], ",")
sig_qubits = to_clean_list(match[4], ",")
# break body only back into individual lines, include semicolons
gate_body = to_clean_list(gate_body, ";")
# insert formatters, (using simple `replace` on the whole line will
# scramble the label if parameters or qubits are letters etc)
for i, gate_line in enumerate(gate_body):
gm = rgxs["gate"].match(gate_line + ";")
glabel = gm[1]
gqubits = multi_replace(
gm[4], {q: f"{{{q}}}" for q in sig_qubits}
)
if gm[3]:
# sub gate line is parametrized gate
gparams = multi_replace(
gm[3], {p: f"{{{p}}}" for p in sig_params}
)
gate_body[i] = f"{glabel}({gparams}) {gqubits};"
else:
# sub gate line is standard gate
gate_body[i] = f"{glabel} {gqubits};"
custom_gates[label] = sig_params, sig_qubits, gate_body
continue
match = rgxs["gate"].search(line)
if match:
# apply a gate
label, params, qubits = (
match.group(1),
match.group(3),
match.group(4),
)
if label in custom_gates:
# custom gate -> resolve parameters and qubits and prepend
# the constituent gate lines to the main list
sig_params, sig_qubits, gate_body = custom_gates[label]
replacer = {
**dict(zip(sig_params, to_clean_list(params, ","))),
**dict(zip(sig_qubits, to_clean_list(qubits, ","))),
}
# recurse by prepending the translated gate body
for gl in reversed(gate_body):
lines.insert(0, gl.format(**replacer))
continue
# standard gate -> add to list directly
if params:
params = tuple(
eval(param, {"pi": math.pi}) for param in params.split(",")
)
else:
params = ()
qubits = tuple(
sitemap[qubit.strip()] for qubit in qubits.split(",")
)
gates.append(Gate(label, params, qubits))
continue
# if not covered by previous checks, simply raise
raise SyntaxError(f"{line}")
return {
"n": len(sitemap),
"sitemap": sitemap,
"gates": gates,
"n_gates": len(gates),
}
def parse_openqasm2_file(fname, **kwargs):
"""Parse an OpenQASM 2.0 file."""
with open(fname) as f:
return parse_openqasm2_str(f.read(), **kwargs)
def parse_openqasm2_url(url, **kwargs):
"""Parse an OpenQASM 2.0 url."""
from urllib import request
return parse_openqasm2_str(request.urlopen(url).read().decode(), **kwargs)
# -------------------------- core gate functions ---------------------------- #
ALL_GATES = set()
ONE_QUBIT_GATES = set()
TWO_QUBIT_GATES = set()
ALL_PARAM_GATES = set()
ONE_QUBIT_PARAM_GATES = set()
TWO_QUBIT_PARAM_GATES = set()
# the tensor tags to use for each gate (defaults to label)
GATE_TAGS = {}
# the number of qubits a gate acts on
GATE_SIZE = {}
# gates which just require a constant array
CONSTANT_GATES = {}
# gates which are parametrized
PARAM_GATES = {}
# gates which involve a non-array operation such as reindexing only
SPECIAL_GATES = {}
def register_constant_gate(name, G, num_qubits, tag=None):
if tag is None:
tag = name
GATE_TAGS[name] = tag
CONSTANT_GATES[name] = G
GATE_SIZE[name] = num_qubits
if num_qubits == 1:
ONE_QUBIT_GATES.add(name)
elif num_qubits == 2:
TWO_QUBIT_GATES.add(name)
ALL_GATES.add(name)
def register_param_gate(name, param_fn, num_qubits, tag=None):
if tag is None:
tag = name
GATE_TAGS[name] = tag
PARAM_GATES[name] = param_fn
GATE_SIZE[name] = num_qubits
if num_qubits == 1:
ONE_QUBIT_GATES.add(name)
ONE_QUBIT_PARAM_GATES.add(name)
elif num_qubits == 2:
TWO_QUBIT_GATES.add(name)
TWO_QUBIT_PARAM_GATES.add(name)
ALL_GATES.add(name)
ALL_PARAM_GATES.add(name)
def register_special_gate(name, fn, num_qubits, tag=None, array=None):
if tag is None:
tag = name
GATE_TAGS[name] = tag
GATE_SIZE[name] = num_qubits
if num_qubits == 1:
ONE_QUBIT_GATES.add(name)
elif num_qubits == 2:
TWO_QUBIT_GATES.add(name)
SPECIAL_GATES[name] = fn
ALL_GATES.add(name)
if array is not None:
CONSTANT_GATES[name] = array
# constant single qubit gates
register_constant_gate("H", qu.hadamard(), 1)
register_constant_gate("X", qu.pauli("X"), 1)
register_constant_gate("Y", qu.pauli("Y"), 1)
register_constant_gate("Z", qu.pauli("Z"), 1)
register_constant_gate("S", qu.S_gate(), 1)
register_constant_gate("SDG", qu.S_gate().H, 1)
register_constant_gate("T", qu.T_gate(), 1)
register_constant_gate("TDG", qu.T_gate().H, 1)
register_constant_gate("SX", cmath.rect(1, 0.25 * math.pi) * qu.Xsqrt(), 1)
register_constant_gate(
"SXDG", cmath.rect(1, -0.25 * math.pi) * qu.Xsqrt().H, 1
)
register_constant_gate("X_1_2", qu.Xsqrt(), 1, "X_1/2")
register_constant_gate("Y_1_2", qu.Ysqrt(), 1, "Y_1/2")
register_constant_gate("Z_1_2", qu.Zsqrt(), 1, "Z_1/2")
register_constant_gate("W_1_2", qu.Wsqrt(), 1, "W_1/2")
register_constant_gate("HZ_1_2", qu.Wsqrt(), 1, "W_1/2")
# constant two qubit gates
register_constant_gate("CX", qu.cX(), 2)
register_constant_gate("CNOT", qu.CNOT(), 2, "CX")
register_constant_gate("CY", qu.cY(), 2)
register_constant_gate("CZ", qu.cZ(), 2)
register_constant_gate("ISWAP", qu.iswap(), 2)
register_constant_gate("IS", qu.iswap(), 2, "ISWAP")
# constant three qubit gates
register_constant_gate("CCX", qu.ccX(), 3)
register_constant_gate("CCNOT", qu.ccX(), 3, "CCX")
register_constant_gate("TOFFOLI", qu.ccX(), 3, "CCX")
register_constant_gate("CCY", qu.ccY(), 3)
register_constant_gate("CCZ", qu.ccZ(), 3)
register_constant_gate("CSWAP", qu.cswap(), 3)
register_constant_gate("FREDKIN", qu.cswap(), 3, "CSWAP")
# single parametrizable gates
def rx_gate_param_gen(params):
phi = params[0]
with backend_like(phi):
# get a real backend zero
zero = phi * 0.0
c = do("complex", do("cos", phi / 2), zero)
s = do("complex", zero, -do("sin", phi / 2))
return recursive_stack(((c, s), (s, c)))
register_param_gate("RX", rx_gate_param_gen, 1)
def ry_gate_param_gen(params):
phi = params[0]
with backend_like(phi):
# get a real backend zero
zero = phi * 0.0
c = do("complex", do("cos", phi / 2), zero)
s = do("complex", do("sin", phi / 2), zero)
return recursive_stack(((c, -s), (s, c)))
register_param_gate("RY", ry_gate_param_gen, 1)
def rz_gate_param_gen(params):
phi = params[0]
with backend_like(phi):
# get a real backend zero
zero = phi * 0.0
c = do("complex", do("cos", phi / 2), zero)
s = do("complex", zero, -do("sin", phi / 2))
# get a complex backend zero
zero = do("complex", zero, zero)
return recursive_stack(((c + s, zero), (zero, c - s)))
register_param_gate("RZ", rz_gate_param_gen, 1)
def u3_gate_param_gen(params):
theta, phi, lamda = params[0], params[1], params[2]
with backend_like(theta):
# get a real backend zero
zero = theta * 0.0
theta_2 = theta / 2
c2 = do("complex", do("cos", theta_2), zero)
s2 = do("complex", do("sin", theta_2), zero)
el = do("exp", do("complex", zero, lamda))
ep = do("exp", do("complex", zero, phi))
elp = do("exp", do("complex", zero, lamda + phi))
return recursive_stack(((c2, -el * s2), (ep * s2, elp * c2)))
register_param_gate("U3", u3_gate_param_gen, 1)
def u2_gate_param_gen(params):
phi, lamda = params[0], params[1]
with backend_like(phi):
# get a real backend zero
zero = phi * 0.0
c01 = -do("exp", do("complex", zero, lamda))
c10 = do("exp", do("complex", zero, phi))
c11 = do("exp", do("complex", zero, phi + lamda))
# get a complex backend zero and backend one
zero = do("complex", zero, zero)
one = zero + 1.0
return recursive_stack(((one, c01), (c10, c11))) / 2**0.5
register_param_gate("U2", u2_gate_param_gen, 1)
def u1_gate_param_gen(params):
lamda = params[0]
with backend_like(lamda):
# get a real backend zero
zero = lamda * 0.0
c11 = do("exp", do("complex", zero, lamda))
# get a complex backend zero and backend one
zero = do("complex", zero, zero)
one = zero + 1.0
return recursive_stack(((one, zero), (zero, c11)))
register_param_gate("U1", u1_gate_param_gen, 1)
register_param_gate("PHASE", u1_gate_param_gen, 1)
# two qubit parametrizable gates
def cu3_param_gen(params):
U3 = u3_gate_param_gen(params)
with backend_like(U3):
# get a 'backend zero'
zero = 0.0 * U3[0, 0]
# get a 'backend one'
one = zero + 1.0
data = (
(((one, zero), (zero, zero)), ((zero, one), (zero, zero))),
(
((zero, zero), (U3[0, 0], U3[0, 1])),
((zero, zero), (U3[1, 0], U3[1, 1])),
),
)
return recursive_stack(data)
register_param_gate("CU3", cu3_param_gen, 2)
def cu2_param_gen(params):
U2 = u2_gate_param_gen(params)
with backend_like(U2):
# get a 'backend zero'
zero = 0.0 * U2[0, 0]
# get a 'backend one'
one = zero + 1.0
data = (
(((one, zero), (zero, zero)), ((zero, one), (zero, zero))),
(
((zero, zero), (U2[0, 0], U2[0, 1])),
((zero, zero), (U2[1, 0], U2[1, 1])),
),
)
return recursive_stack(data)
register_param_gate("CU2", cu2_param_gen, 2)
def cu1_param_gen(params):
lamda = params[0]
with backend_like(lamda):
# get a real backend zero
zero = 0.0 * lamda
c11 = do("exp", do("complex", zero, lamda))
# get a complex backend zero and backend one
zero = do("complex", zero, zero)
one = zero + 1.0
data = (
(((one, zero), (zero, zero)), ((zero, one), (zero, zero))),
(((zero, zero), (one, zero)), ((zero, zero), (zero, c11))),
)
return recursive_stack(data)
register_param_gate("CU1", cu1_param_gen, 2)
register_param_gate("CPHASE", cu1_param_gen, 2)
def crx_param_gen(params):
"""Parametrized controlled X-rotation."""
theta = params[0]
with backend_like(theta):
# get a real backend zero
zero = 0.0 * theta
ccos = do("complex", do("cos", theta / 2), zero)
csin = do("complex", zero, -do("sin", theta / 2))
# get a complex backend zero and backend one
zero = do("complex", zero, zero)
one = zero + 1.0
data = (
(((one, zero), (zero, zero)), ((zero, one), (zero, zero))),
(((zero, zero), (ccos, csin)), ((zero, zero), (csin, ccos))),
)
return recursive_stack(data)
register_param_gate("CRX", crx_param_gen, 2)
def cry_param_gen(params):
"""Parametrized controlled Y-rotation."""
theta = params[0]
with backend_like(theta):
# get a real backend zero
zero = 0.0 * theta
ccos = do("complex", do("cos", theta / 2), zero)
csin = do("complex", do("sin", theta / 2), zero)
# get a complex backend zero and backend one
zero = do("complex", zero, zero)
one = zero + 1.0
data = (
(((one, zero), (zero, zero)), ((zero, one), (zero, zero))),
(((zero, zero), (ccos, -csin)), ((zero, zero), (csin, ccos))),
)
return recursive_stack(data)
register_param_gate("CRY", cry_param_gen, 2)
def crz_param_gen(params):
"""Parametrized controlled Z-rotation."""
theta = params[0]
with backend_like(theta):
# get a real backend zero
zero = 0.0 * theta
theta_2 = theta / 2
c = do("complex", do("cos", theta_2), zero)
s = do("complex", zero, -do("sin", theta_2))
# get a complex backend zero and backend one
zero = do("complex", zero, zero)
one = zero + 1.0
data = (
(((one, zero), (zero, zero)), ((zero, one), (zero, zero))),
(((zero, zero), (c + s, zero)), ((zero, zero), (zero, c - s))),
)
return recursive_stack(data)
register_param_gate("CRZ", crz_param_gen, 2)
def fsim_param_gen(params):
theta, phi = params[0], params[1]
with backend_like(theta):
# get a real backend zero
zero = theta * 0.0
a = do("complex", do("cos", theta), zero)
b = do("complex", zero, -do("sin", theta))
c = do("exp", do("complex", zero, -phi))
# get a complex backend zero and backend one
zero = do("complex", zero, zero)
one = zero + 1.0
data = (
(((one, zero), (zero, zero)), ((zero, a), (b, zero))),
(((zero, b), (a, zero)), ((zero, zero), (zero, c))),
)
return recursive_stack(data)
register_param_gate("FSIM", fsim_param_gen, 2)
register_param_gate("FS", fsim_param_gen, 2, "FSIM")
def fsimg_param_gen(params):
theta, zeta, chi, gamma, phi = (
params[0],
params[1],
params[2],
params[3],
params[4],
)
"""Parametrized, most general number conserving two qubit gate.
"""
with backend_like(theta):
# get a real backend zero
zero = 0.0 * theta
cos = do("cos", theta)
sin = do("sin", theta)
c11 = do("exp", do("complex", zero, -(gamma + zeta))) * do(
"complex", cos, zero
)
c12 = do("exp", do("complex", zero, -(gamma - chi))) * do(
"complex", zero, -sin
)
c21 = do("exp", do("complex", zero, -(gamma + chi))) * do(
"complex", zero, -sin
)
c22 = do("exp", do("complex", zero, -(gamma - zeta))) * do(
"complex", cos, zero
)
c33 = do("exp", do("complex", zero, -(2 * gamma + phi)))
# get a complex backend zero and backend one
zero = do("complex", zero, zero)
one = zero + 1.0
data = (
(((one, zero), (zero, zero)), ((zero, c11), (c12, zero))),
(((zero, c21), (c22, zero)), ((zero, zero), (zero, c33))),
)
return recursive_stack(data)
register_param_gate("FSIMG", fsimg_param_gen, 2)
def givens_param_gen(params):
theta = params[0]
with backend_like(theta):
# get a real backend zero
zero = 0.0 * theta
a = do("complex", do("cos", theta), zero)
b = do("complex", do("sin", theta), zero)
# get a complex backend zero and backend one
zero = do("complex", zero, zero)
one = zero + 1.0
data = (
(((one, zero), (zero, zero)), ((zero, a), (-b, zero))),
(((zero, b), (a, zero)), ((zero, zero), (zero, one))),
)
return recursive_stack(data)
register_param_gate("GIVENS", givens_param_gen, num_qubits=2)
def givens2_param_gen(params):
theta, phi = params[0], params[1]
with backend_like(theta):
# get a real backend zero
zero = 0.0 * theta
a = do("complex", do("cos", theta), zero)
b = do("exp", do("complex", zero, phi)) * do(
"complex", do("sin", theta), zero
)
b_conj = do("exp", do("complex", zero, -phi)) * do(
"complex", do("sin", theta), zero
)
# get a complex backend zero and backend one
zero = do("complex", zero, zero)
one = zero + 1.0
data = (
(((one, zero), (zero, zero)), ((zero, a), (-b, zero))),
(((zero, b_conj), (a, zero)), ((zero, zero), (zero, one))),
)
return recursive_stack(data)
register_param_gate("GIVENS2", givens2_param_gen, num_qubits=2)
def xx_plus_yy_param_gen(params):
theta, beta = params[0], params[1]
with backend_like(theta):
# get a real backend zero
zero = 0.0 * theta
half_theta = 0.5 * theta
a = do("complex", do("cos", half_theta), zero)
b = do("exp", do("complex", zero, beta)) * do(
"complex", do("sin", half_theta), zero
)
b_conj = do("exp", do("complex", zero, -beta)) * do(
"complex", do("sin", half_theta), zero
)
# get a complex backend zero and backend one
zero = do("complex", zero, zero)
one = zero + 1.0
data = (
(((one, zero), (zero, zero)), ((zero, a), (-1j * b, zero))),
(((zero, -1j * b_conj), (a, zero)), ((zero, zero), (zero, one))),
)
return recursive_stack(data)
register_param_gate("XXPLUSYY", xx_plus_yy_param_gen, num_qubits=2)
def xx_minus_yy_param_gen(params):
theta, beta = params[0], params[1]
with backend_like(theta):
# get a real backend zero
zero = 0.0 * theta
half_theta = 0.5 * theta
a = do("complex", do("cos", half_theta), zero)
b = do("exp", do("complex", zero, beta)) * do(
"complex", do("sin", half_theta), zero
)
b_conj = do("exp", do("complex", zero, -beta)) * do(
"complex", do("sin", half_theta), zero
)
# get a complex backend zero and backend one
zero = do("complex", zero, zero)
one = zero + 1.0
data = (
(((a, zero), (zero, -1j * b_conj)), ((zero, one), (zero, zero))),
(((zero, zero), (one, zero)), ((-1j * b, zero), (zero, a))),
)
return recursive_stack(data)
register_param_gate("XXMINUSYY", xx_minus_yy_param_gen, num_qubits=2)
def rxx_param_gen(params):
r"""Parametrized two qubit XX-rotation.
.. math::
\mathrm{RXX}(\theta) = \exp(-i \frac{\theta}{2} X_i X_j)
"""
theta = params[0]
with backend_like(theta):
# get a real 'backend zero'
zero = 0.0 * theta
theta_2 = theta / 2
ccos = do("complex", do("cos", theta_2), zero)
csin = do("complex", zero, -do("sin", theta_2))
# get a complex backend zero
zero = do("complex", zero, zero)
data = (
(((ccos, zero), (zero, csin)), ((zero, ccos), (csin, zero))),
(((zero, csin), (ccos, zero)), ((csin, zero), (zero, ccos))),
)
return recursive_stack(data)
register_param_gate("RXX", rxx_param_gen, 2)
def ryy_param_gen(params):
r"""Parametrized two qubit YY-rotation.
.. math::
\mathrm{RYY}(\theta) = \exp(-i \frac{\theta}{2} Y_i Y_j)
"""
theta = params[0]
with backend_like(theta):
# get a real 'backend zero'
zero = 0.0 * theta
theta_2 = theta / 2
ccos = do("complex", do("cos", theta_2), zero)
csin = do("complex", zero, do("sin", theta_2))
# get a complex backend zero
zero = do("complex", zero, zero)
data = (
(((ccos, zero), (zero, csin)), ((zero, ccos), (-csin, zero))),
(((zero, -csin), (ccos, zero)), ((csin, zero), (zero, ccos))),
)
return recursive_stack(data)
register_param_gate("RYY", ryy_param_gen, 2)
def rzz_param_gen(params):
r"""Parametrized two qubit ZZ-rotation.
.. math::
\mathrm{RZZ}(\theta) = \exp(-i \frac{\theta}{2} Z_i Z_j)
"""
theta = params[0]
with backend_like(theta):
# get a real 'backend zero'
zero = 0.0 * theta
theta_2 = theta / 2
c00 = c11 = do("complex", do("cos", theta_2), do("sin", -theta_2))
c01 = c10 = do("complex", do("cos", theta_2), do("sin", theta_2))
# get a complex backend zero
zero = do("complex", zero, zero)
data = (
(((c00, zero), (zero, zero)), ((zero, c01), (zero, zero))),
(((zero, zero), (c10, zero)), ((zero, zero), (zero, c11))),
)
return recursive_stack(data)
register_param_gate("RZZ", rzz_param_gen, 2)
def su4_gate_param_gen(params):
"""See https://arxiv.org/abs/quant-ph/0308006 - Fig. 7.
params:
# theta1, phi1, lamda1,
# theta2, phi2, lamda2,
# theta3, phi3, lamda3,
# theta4, phi4, lamda4,
# t1, t2, t3,
"""
TA1 = Tensor(u3_gate_param_gen(params[0:3]), ["a1", "a0"])
TA2 = Tensor(u3_gate_param_gen(params[3:6]), ["b1", "b0"])
cnot = do(
"array",
qu.CNOT().reshape(2, 2, 2, 2),
like=params,
dtype=TA1.data.dtype,
)
TNOTC1 = Tensor(cnot, ["b2", "a2", "b1", "a1"])
TRz1 = Tensor(rz_gate_param_gen(params[12:13]), inds=["a3", "a2"])
TRy2 = Tensor(ry_gate_param_gen(params[13:14]), inds=["b3", "b2"])
TCNOT2 = Tensor(cnot, ["a5", "b4", "a3", "b3"])
TRy3 = Tensor(ry_gate_param_gen(params[14:15]), inds=["b5", "b4"])
TNOTC3 = Tensor(cnot, ["b6", "a6", "b5", "a5"])
TA3 = Tensor(u3_gate_param_gen(params[6:9]), ["a7", "a6"])
TA4 = Tensor(u3_gate_param_gen(params[9:12]), ["b7", "b6"])
return tensor_contract(
TA1,
TA2,
TNOTC1,
TRz1,
TRy2,
TCNOT2,
TRy3,
TNOTC3,
TA3,
TA4,
output_inds=["a7", "b7"] + ["a0", "b0"],
optimize="auto-hq",
).data
register_param_gate("SU4", su4_gate_param_gen, 2)
# special non-tensor gates
_MPS_METHODS = {
"auto-mps",
"nonlocal",
"swap+split",
}
def apply_swap(psi, i, j, **gate_opts):
contract = gate_opts.pop("contract", None)
if contract not in _MPS_METHODS:
# just do swap by lazily reindexing
iind, jind = map(psi.site_ind, (int(i), int(j)))
psi.reindex_({iind: jind, jind: iind})
else:
# tensors are absorbed so propagate_tags is not needed
gate_opts.pop("propagate_tags", None)
if contract == "nonlocal":
psi.gate_nonlocal_(qu.swap(2), (i, j), **gate_opts)
else: # {"swap+split", "auto-mps"}:
psi.swap_sites_with_compress_(i, j, **gate_opts)
register_special_gate("SWAP", apply_swap, 2, array=qu.swap(2))
register_special_gate("IDEN", lambda *_, **__: None, 1, array=qu.identity(2))
def build_controlled_gate_htn(
ncontrol,
gate,
upper_inds,
lower_inds,
tags_each=None,
tags_all=None,
bond_ind=None,
):
"""Build a low rank hyper tensor network (CP-decomp like) representation of
a multi controlled gate.
"""
ngate = len(gate.qubits)
gate_shape = (2,) * (2 * ngate)
array = gate.array.reshape(gate_shape)
I2 = qu.identity(2, dtype=array.dtype)
IG = qu.identity(2**ngate, dtype=array.dtype).reshape(gate_shape)
p1 = qu.down(qtype="dop", dtype=array.dtype) # |1><1|
array_seqs = [[I2] * ncontrol + [IG], [p1] * ncontrol + [array - IG]]
# might need to group indices and tags on the target gate if multi-qubit
if ngate > 1:
upper_inds = (*upper_inds[:ncontrol], upper_inds[ncontrol:])
lower_inds = (*lower_inds[:ncontrol], lower_inds[ncontrol:])
tags_each = (*tags_each[:ncontrol], tags_each[ncontrol:])
htn = HTN_CP_operator_from_products(
array_seqs,
upper_inds=upper_inds,
lower_inds=lower_inds,
tags_each=tags_each,
tags_all=tags_all,
bond_ind=bond_ind,
)
return htn
def _apply_controlled_gate_mps(psi, gate, tags=None, **gate_opts):
"""Apply a multi-controlled gate to a state represented as an MPS."""
submpo = gate.build_mpo()
where = sorted((*gate.controls, *gate.qubits))
psi.gate_with_submpo_(submpo, where, **gate_opts)
def _apply_controlled_gate_htn(
psi, gate, tags=None, propagate_tags="register", **gate_opts
):
assert propagate_tags == "register"
all_qubits = (*gate.controls, *gate.qubits)
ncontrol = len(gate.controls)
ngate = len(gate.qubits)
ntotal = ncontrol + ngate
upper_inds = [rand_uuid() for _ in range(ntotal)]
lower_inds = [rand_uuid() for _ in range(ntotal)]
tags_sequence = [psi.site_tag(i) for i in all_qubits]
htn = build_controlled_gate_htn(
ncontrol,
gate,
upper_inds=upper_inds,
lower_inds=lower_inds,
tags_each=tags_sequence,
tags_all=tags,
)
psi.gate_inds_with_tn_(
[psi.site_ind(i) for i in all_qubits],
htn,
lower_inds,
upper_inds,
**gate_opts,
)
def apply_controlled_gate(
psi,
gate,
tags=None,
contract="auto-split-gate",
propagate_tags="register",
**gate_opts,
):
if contract in ("auto-mps", "nonlocal"):
_apply_controlled_gate_mps(psi, gate, tags=tags, **gate_opts)
elif contract in (
"auto-split-gate",
"split-gate",
):
_apply_controlled_gate_htn(
psi, gate, tags=tags, propagate_tags=propagate_tags, **gate_opts
)
else:
raise ValueError(
f"Contract method '{contract}' not "
"supported for multi-controlled gates."
)
@functools.lru_cache(2**15)
def _cached_param_gate_build(fn, params):
return fn(params)
class Gate:
"""A simple class for storing the details of a quantum circuit gate.
Parameters
----------
label : str
The name or 'identifier' of the gate.
params : Iterable[float]
The parameters of the gate.
qubits : Iterable[int], optional
Which qubits the gate acts on.
controls : Iterable[int], optional
Which qubits are the controls.
round : int, optional
If given, which round or layer the gate is part of.
parametrize : bool, optional
Whether the gate will correspond to a parametrized tensor.
"""
__slots__ = (
"_label",
"_params",
"_qubits",
"_controls",
"_round",
"_parametrize",
"_tag",
"_special",
"_constant",
"_array",
)
def __init__(
self,
label,
params,
qubits=None,
controls=None,
round=None,
parametrize=False,
):
self._label = label.upper()
if self._label not in ALL_GATES:
raise ValueError(f"Unknown gate: {self._label}.")
self._params = ops.asarray(params)
if qubits is None:
self._qubits = None
else:
self._qubits = tuple(qubits)
if controls is None:
self._controls = None
else:
self._controls = tuple(controls)
self._round = int(round) if round is not None else round
self._parametrize = bool(parametrize)
self._tag = GATE_TAGS[self._label]
self._special = self._label in SPECIAL_GATES
self._constant = self._label in CONSTANT_GATES
if (self._special or self._constant) and self._parametrize:
raise ValueError(f"Cannot parametrize the gate: {self._label}.")
self._array = None
@classmethod
def from_raw(cls, U, qubits=None, controls=None, round=None):
new = object.__new__(cls)
new._label = f"RAW{id(U)}"
new._params = "raw"
if qubits is None:
new._qubits = None
else:
new._qubits = tuple(qubits)
if controls is None:
new._controls = None
else:
new._controls = tuple(controls)
new._round = int(round) if round is not None else round
new._special = False
new._parametrize = isinstance(U, ops.PArray)
new._tag = None
new._array = U
return new
def copy(self):
new = object.__new__(self.__class__)
new._label = self._label
new._params = self._params
new._qubits = self._qubits
new._controls = self._controls
new._round = self._round
new._parametrize = self._parametrize
new._tag = self._tag
new._special = self._special
new._constant = self._constant
new._array = self._array
return new
@property
def label(self):
return self._label
@property
def params(self):
return self._params
@property
def qubits(self):
return self._qubits
@qubits.setter
def qubits(self, qubits):
if qubits is None:
self._qubits = None
else:
self._qubits = tuple(qubits)
@property
def total_qubit_count(self):
nq = len(self._qubits)
if self._controls:
nq += len(self._controls)
return nq
@property
def controls(self):
return self._controls
@property
def round(self):
return self._round
@property
def special(self):
return self._special
@property
def parametrize(self):
return self._parametrize
@property
def tag(self):
return self._tag
def copy_with(self, **kwargs):
"""Take a copy of this gate but with some attributes changed."""
label = kwargs.get("label", self._label)
params = kwargs.get("params", self._params)
qubits = kwargs.get("qubits", self._qubits)
controls = kwargs.get("controls", self._controls)
round = kwargs.get("round", self._round)
parametrize = kwargs.get("parametrize", self._parametrize)
if isinstance(params, str) and (params == "raw"):
return self.from_raw(
U=self._array,
qubits=qubits,
controls=controls,
round=round,
)
else:
return self.__class__(
label=label,
params=params,
qubits=qubits,
controls=controls,
round=round,
parametrize=parametrize,
)
def build_array(self):
"""Build the array representation of the gate. For controlled gates
this *excludes* the control qubits.
"""
if self._special and (self._label not in CONSTANT_GATES):
# these don't have an array representation
raise ValueError(f"{self.label} gates have no array to build.")
if self._constant:
# simply return the constant array
return CONSTANT_GATES[self._label]
# build the array
param_fn = PARAM_GATES[self._label]
if self._parametrize:
# either lazily, as tensor will be parametrized
return ops.PArray(param_fn, self._params)
# or cached directly into array
try:
return _cached_param_gate_build(param_fn, self._params)
except TypeError:
return param_fn(self._params)
@property
def array(self):
if self._array is None:
self._array = self.build_array()
return self._array
def build_mpo(self, L=None, **kwargs):
"""Build an MPO representation of this gate."""
G = self.array
if L is None:
L = max((*self.qubits, *self.controls), default=0) + 1
if not self.controls:
return MatrixProductOperator.from_dense(
G, sites=self.qubits, L=L, **kwargs
)
IG = qu.identity(2 ** len(self.qubits))
IG = reshape(IG, G.shape)
p1 = qu.down(qtype="dop")
# form (G - 1) on target qubits
mpo = MatrixProductOperator.from_dense(
G - IG, sites=self.qubits, L=L, **kwargs
)
# take tensor product with |11...><11...| on controls
mpo.fill_empty_sites_(mode=self.controls, fill_array=p1)
# add with identity on all qubits
mpo_I = MPO_identity_like(
mpo, sites=sorted((*self.qubits, *self.controls))
)
return mpo.add_MPO_(mpo_I)
def __repr__(self):
return (
f"<{self.__class__.__name__}("
+ f"label={self._label}, "
+ f"params={self._params}, "
+ f"qubits={self._qubits}"
+ (f", controls={self._controls})" if self._controls else "")
+ (f", round={self._round}" if self._round is not None else "")
+ (
f", parametrize={self._parametrize})"
if self._parametrize
else ""
)
+ ")>"
)
def sample_bitstring_from_prob_ndarray(p, seed=None):
"""Sample a bitstring from n-dimensional tensor ``p`` of probabilities.
Examples
--------
>>> import numpy as np
>>> p = np.zeros(shape=(2, 2, 2, 2, 2))
>>> p[0, 1, 0, 1, 1] = 1.0
>>> sample_bitstring_from_prob_ndarray(p)
'01011'
"""
rng = np.random.default_rng(seed)
b = rng.choice(p.size, p=p.ravel())
return f"{b:0>{p.ndim}b}"
def rehearsal_dict(tn, tree):
return {
"tn": tn,
"tree": tree,
"W": tree.contraction_width(),
"C": math.log10(max(tree.contraction_cost(), 1)),
}
def parse_to_gate(
gate_id,
*gate_args,
params=None,
qubits=None,
controls=None,
gate_round=None,
parametrize=None,
):
"""Map all types of gate specification into a `Gate` object."""
if isinstance(gate_id, Gate):
# already a gate
if gate_args:
raise ValueError(
"You cannot specify ``gate_args`` for an already "
"encapsulated `Gate` object."
)
if any((params, qubits, controls, gate_round, parametrize)):
raise ValueError(
"You cannot specify ``controls`` or ``gate_round`` for an "
"already encapsulated gate - supply directly to the `Gate` "
"constructor instead."
)
return gate_id
if isinstance(gate_id, tuple):
# if given a tuple just unpack it
if gate_args:
raise ValueError(
"You cannot specify ``gate_args`` when supplying a tuple."
)
gate_id, gate_args = gate_id[0], gate_id[1:]
if hasattr(gate_id, "shape") and not isinstance(gate_id, str):
# raw gate (numpy strings have a shape - ignore those)
if parametrize is not None:
raise ValueError(
"You cannot specify ``parametrize`` for raw gate, supply a "
"``PArray`` instead."
)
return Gate.from_raw(
U=gate_id,
qubits=gate_args,
controls=controls,
round=gate_round,
)
# else gate is specified as a tuple or kwargs
if isinstance(gate_id, numbers.Integral) or gate_id.isdigit():
# gate round given as first entry of tuple
if gate_round is None:
# explicilty specified ``gate_round`` takes precedence
gate_round = gate_id
gate_id, gate_args = gate_args[0], gate_args[1:]
if parametrize is None:
parametrize = False
if gate_args:
if any((params, qubits)):
raise ValueError(
"You cannot specify ``params`` or ``qubits`` "
"when supplying ``gate_args``."
)
nq = GATE_SIZE[gate_id.upper()]
(
params,
qubits,
) = (
gate_args[:-nq],
gate_args[-nq:],
)
else:
# qubits and params specified directly
if params is None:
params = ()
return Gate(
label=gate_id,
params=params,
qubits=qubits,
controls=controls,
round=gate_round,
parametrize=parametrize,
)
def _combine_1q_gate_run(gates, array_fn=None):
"""Combine a run of single qubit gates in application order."""
gates = tuple(gate for _, gate in gates)
G = gates[0].array
if array_fn is not None:
G = array_fn(G)
for gate in gates[1:]:
Gi = gate.array
if array_fn is not None:
Gi = array_fn(Gi)
G = Gi @ G
return Gate.from_raw(G, gates[0].qubits)
def _combine_2q_gate_run(gates, array_fn=None):
"""Combine a run of two qubit gates in application order."""
gates = tuple(gate for _, gate in gates)
G = gates[0].array
if array_fn is not None:
G = array_fn(G)
G = reshape(G, (4, 4))
for gate in gates[1:]:
Gi = gate.array
if array_fn is not None:
Gi = array_fn(Gi)
G = reshape(Gi, (4, 4)) @ G
return Gate.from_raw(reshape(G, (2, 2, 2, 2)), gates[0].qubits)
def _can_merge_1q_gate(gate):
return (
(gate.controls is None)
and (not gate.special)
and (not gate.parametrize)
and (gate.qubits is not None)
and (len(gate.qubits) == 1)
)
def _can_merge_2q_gate(gate):
return (
(gate.controls is None)
and (not gate.special)
and (not gate.parametrize)
and (gate.qubits is not None)
and (len(gate.qubits) == 2)
)
def _iter_gates_with_merged_runs(gates, merge_1q=True, merge_2q=True):
"""Yield ``(gate_to_apply, gates_to_record)``, merging adjacent runs of
local gates that are not interrupted by any operation touching the same
qubits.
"""
pending_1q = {}
pending_2q = {}
def flush_qubit(q):
run = pending_1q.pop(q, None)
if run is None:
return
if len(run) == 1:
return run[0][1], run
return None, run
def flush_pair(pair):
run = pending_2q.pop(pair, None)
if run is None:
return
if len(run) == 1:
return run[0][1], run
return None, run
def flush_touched(touched, keep_qubit=None, keep_pair=None):
for q in tuple(pending_1q):
if q == keep_qubit:
continue
if q in touched:
item = flush_qubit(q)
if item is not None:
yield item
for pair in tuple(pending_2q):
if pair == keep_pair:
continue
if touched.intersection(pair):
item = flush_pair(pair)
if item is not None:
yield item
def flush_all():
for q in tuple(pending_1q):
item = flush_qubit(q)
if item is not None:
yield item
for pair in tuple(pending_2q):
item = flush_pair(pair)
if item is not None:
yield item
for i, gate in enumerate(gates):
if merge_1q and _can_merge_1q_gate(gate):
(q,) = gate.qubits
yield from flush_touched({q}, keep_qubit=q)
pending_1q.setdefault(q, []).append((i, gate))
continue
if merge_2q and _can_merge_2q_gate(gate):
pair = gate.qubits
yield from flush_touched(set(pair), keep_pair=pair)
pending_2q.setdefault(pair, []).append((i, gate))
continue
touched = set(gate.qubits or ())
if gate.controls:
touched.update(gate.controls)
yield from flush_touched(touched)
yield gate, ((i, gate),)
yield from flush_all()
_iter_gates_with_merged_1q_runs = functools.partial(
_iter_gates_with_merged_runs, merge_1q=True, merge_2q=False
)
# --------------------------- main circuit class ---------------------------- #
class Circuit:
"""Class for simulating quantum circuits using tensor networks. The class
keeps a list of :class:`Gate` objects in sync with a tensor network
representing the current state of the circuit.
Parameters
----------
N : int, optional
The number of qubits.
psi0 : TensorNetwork1DVector, optional
The initial state, assumed to be ``|00000....0>`` if not given. The
state is always copied and the tag ``PSI0`` added.
gate_opts : dict_like, optional
Default keyword arguments to supply to each
:func:`~quimb.tensor.tn1d.core.gate_TN_1D` call during the circuit.
gate_contract : str, optional
Shortcut for setting the default `'contract'` option in `gate_opts`.
gate_propagate_tags : str, optional
Shortcut for setting the default `'propagate_tags'` option in
`gate_opts`.
tags : str or sequence of str, optional
Tag(s) to add to the initial wavefunction tensors (whether these are
propagated to the rest of the circuit's tensors depends on
``gate_opts``).
psi0_dtype : str, optional
Ensure the initial state has this dtype.
psi0_tag : str, optional
Ensure the initial state has this tag.
tag_gate_numbers : bool, optional
Whether to tag each gate tensor with its number in the circuit, like
``"GATE_{g}"``. This is required for updating the circuit parameters.
gate_tag_id : str, optional
The format string for tagging each gate tensor, by default e.g.
``"GATE_{g}"``.
tag_gate_rounds : bool, optional
Whether to tag each gate tensor with its number in the circuit, like
``"ROUND_{r}"``.
round_tag_id : str, optional
The format string for tagging each round of gates, by default e.g.
``"ROUND_{r}"``.
tag_gate_labels : bool, optional
Whether to tag each gate tensor with its gate type label, e.g.
``{"X_1/2", "ISWAP", "CCX", ...}``..
bra_site_ind_id : str, optional
Use this to label 'bra' site indices when creating certain (mostly
internal) intermediate tensor networks.
dtype : str, optional
A default dtype to perform calculations in. Depending on
`convert_eager`, this is enforced *after* circuit construction
and simplification (the default for exact simulation), or eagerly to
the initial state and as gates are applied (the default for MPS
simulation).
to_backend : callable, optional
If given, apply this function to both the initial state arrays and to
every gate as it is applied.
convert_eager : bool, optional
Whether to eagerly perform dtype casting and application of
`to_backend` as gates are supplied, or wait until after the necessary
TNs for a particular task such as sampling are formed and simplified.
Deferred conversion (`convert_eager=False`) is the default mode for
full contraction.
Attributes
----------
psi : TensorNetwork1DVector
The current circuit wavefunction as a tensor network.
uni : TensorNetwork1DOperator
The current circuit unitary operator as a tensor network.
gates : tuple[Gate]
The gates in the circuit.
Examples
--------
Create 3-qubit GHZ-state:
>>> qc = qtn.Circuit(3)
>>> gates = [
('H', 0),
('H', 1),
('CNOT', 1, 2),
('CNOT', 0, 2),
('H', 0),
('H', 1),
('H', 2),
]
>>> qc.apply_gates(gates)
>>> qc.psi
<TensorNetwork1DVector(tensors=12, indices=14, L=3, max_bond=2)>
>>> qc.psi.to_dense().round(4)
qarray([[ 0.7071+0.j],
[ 0. +0.j],
[ 0. +0.j],
[-0. +0.j],
[-0. +0.j],
[ 0. +0.j],
[ 0. +0.j],
[ 0.7071+0.j]])
>>> for b in qc.sample(10):
... print(b)
000
000
111
000
111
111
000
111
000
000
See Also
--------
Gate
"""
def __init__(
self,
N=None,
psi0=None,
gate_opts=None,
gate_contract="auto-split-gate",
gate_propagate_tags="register",
tags=None,
psi0_dtype="complex128",
psi0_tag="PSI0",
tag_gate_numbers=True,
gate_tag_id="GATE_{}",
tag_gate_rounds=True,
round_tag_id="ROUND_{}",
tag_gate_labels=True,
bra_site_ind_id="b{}",
dtype=None,
to_backend=None,
convert_eager=False,
):
if (N is None) and (psi0 is None):
raise ValueError("You must supply one of `N` or `psi0`.")
elif psi0 is None:
self.N = N
self._psi = self._init_state(N, dtype=psi0_dtype)
elif N is None:
self._psi = psi0.copy()
self.N = psi0.nsites
else:
if N != psi0.nsites:
raise ValueError("`N` doesn't match `psi0`.")
self.N = N
self._psi = psi0.copy()
self._psi.add_tag(psi0_tag)
if tags is not None:
if isinstance(tags, str):
tags = (tags,)
for tag in tags:
self._psi.add_tag(tag)
self.tag_gate_numbers = tag_gate_numbers
self.tag_gate_rounds = tag_gate_rounds
self.tag_gate_labels = tag_gate_labels
self.dtype = dtype
self.to_backend = to_backend
self.convert_eager = convert_eager
if self.convert_eager:
self._maybe_convert(self._psi)
self._backend_gate_cache = {}
self.gate_opts = ensure_dict(gate_opts)
self.gate_opts.setdefault("contract", gate_contract)
self.gate_opts.setdefault("propagate_tags", gate_propagate_tags)
self._gates = []
self._ket_site_ind_id = self._psi.site_ind_id
self._bra_site_ind_id = bra_site_ind_id
self._gate_tag_id = gate_tag_id
self._round_tag_id = round_tag_id
if self._ket_site_ind_id == self._bra_site_ind_id:
raise ValueError(
"The 'ket' and 'bra' site ind ids clash : '{}' and '{}".format(
self._ket_site_ind_id, self._bra_site_ind_id
)
)
self._sample_n_gates = -1
self._storage = dict()
self._sampled_conditionals = dict()
def copy(self):
"""Copy the circuit and its state."""
new = object.__new__(self.__class__)
new.N = self.N
new._psi = self._psi.copy()
new.gate_opts = tree_map(lambda x: x, self.gate_opts)
new.tag_gate_numbers = self.tag_gate_numbers
new.tag_gate_rounds = self.tag_gate_rounds
new.tag_gate_labels = self.tag_gate_labels
new.to_backend = self.to_backend
new.dtype = self.dtype
new.convert_eager = self.convert_eager
new._backend_gate_cache = self._backend_gate_cache
new._gates = self._gates.copy()
new._ket_site_ind_id = self._ket_site_ind_id
new._bra_site_ind_id = self._bra_site_ind_id
new._gate_tag_id = self._gate_tag_id
new._round_tag_id = self._round_tag_id
new._sample_n_gates = self._sample_n_gates
new._storage = self._storage.copy()
new._sampled_conditionals = self._sampled_conditionals.copy()
return new
def _maybe_convert(self, obj, dtype=None):
istn = isinstance(obj, TensorNetwork)
if dtype is None:
# use default dtype
dtype = self.dtype
if dtype is not None:
# cast array or TN to dtype
if istn:
obj.astype_(dtype)
else:
if get_dtype_name(obj) != dtype:
obj = astype(obj, dtype)
if self.to_backend is not None:
# once dtype is enforced, apply to_backend
# for e.g. gpu transfer etc
if istn:
obj.apply_to_arrays(self.to_backend)
else:
obj = self.to_backend(obj)
return obj
def _maybe_convert_gate_array(self, G):
key = id(G)
if key not in self._backend_gate_cache:
self._backend_gate_cache[key] = self._maybe_convert(G)
return self._backend_gate_cache[key]
def apply_to_arrays(self, fn):
"""Apply a function to all the arrays in the circuit."""
self._psi.apply_to_arrays(fn)
def get_params(self):
"""Get a pytree - in this case a dict - of all the parameters in the
circuit.
Returns
-------
dict[int, tuple]
A dictionary mapping gate numbers to their parameters.
"""
return {
i: self._psi[self.gate_tag(i)].params
for i, gate in enumerate(self._gates)
if gate.parametrize
}
def set_params(self, params):
"""Set the parameters of the circuit.
Parameters
----------
params : dict`
A dictionary mapping gate numbers to the new parameters.
"""
for i, p in params.items():
self._psi[self.gate_tag(i)].params = p
self._gates[i] = self._gates[i].copy_with(params=ops.asarray(p))
self.clear_storage()
@classmethod
def from_qsim_str(cls, contents, progbar=False, **circuit_opts):
"""Generate a ``Circuit`` instance from a 'qsim' string."""
info = parse_qsim_str(contents)
qc = cls(info["n"], **circuit_opts)
qc.apply_gates(info["gates"], progbar=progbar)
return qc
@classmethod
def from_qsim_file(cls, fname, progbar=False, **circuit_opts):
"""Generate a ``Circuit`` instance from a 'qsim' file.
The qsim file format is described here:
https://quantumai.google/qsim/input_format.
"""
info = parse_qsim_file(fname)
qc = cls(info["n"], **circuit_opts)
qc.apply_gates(info["gates"], progbar=progbar)
return qc
@classmethod
def from_qsim_url(cls, url, progbar=False, **circuit_opts):
"""Generate a ``Circuit`` instance from a 'qsim' url."""
info = parse_qsim_url(url)
qc = cls(info["n"], **circuit_opts)
qc.apply_gates(info["gates"], progbar=progbar)
return qc
from_qasm = deprecated(from_qsim_str, "from_qasm", "from_qsim_str")
from_qasm_file = deprecated(
from_qsim_file, "from_qasm_file", "from_qsim_file"
)
from_qasm_url = deprecated(from_qsim_url, "from_qasm_url", "from_qsim_url")
@classmethod
def from_openqasm2_str(cls, contents, progbar=False, **circuit_opts):
"""Generate a ``Circuit`` instance from an OpenQASM 2.0 string."""
info = parse_openqasm2_str(contents)
qc = cls(info["n"], **circuit_opts)
qc.apply_gates(info["gates"], progbar)
return qc
@classmethod
def from_openqasm2_file(cls, fname, progbar=False, **circuit_opts):
"""Generate a ``Circuit`` instance from an OpenQASM 2.0 file."""
info = parse_openqasm2_file(fname)
qc = cls(info["n"], **circuit_opts)
qc.apply_gates(info["gates"], progbar=progbar)
return qc
@classmethod
def from_openqasm2_url(cls, url, progbar=False, **circuit_opts):
"""Generate a ``Circuit`` instance from an OpenQASM 2.0 url."""
info = parse_openqasm2_url(url)
qc = cls(info["n"], **circuit_opts)
qc.apply_gates(info["gates"], progbar=progbar)
return qc
@classmethod
def from_gates(cls, gates, N=None, progbar=False, **kwargs):
"""Generate a ``Circuit`` instance from a sequence of gates.
Parameters
----------
gates : sequence[Gate] or sequence[tuple]
The sequence of gates to apply.
N : int, optional
The number of qubits. If not given, will be inferred from the
gates.
progbar : bool, optional
Whether to show a progress bar.
kwargs
Supplied to the ``Circuit`` constructor.
"""
if N is None:
gates = tuple(gates)
N = 0
for gate in gates:
gate = parse_to_gate(gate)
if gate.qubits:
N = max(N, max(gate.qubits) + 1)
if gate.controls:
N = max(N, max(gate.controls) + 1)
qc = cls(N, **kwargs)
qc.apply_gates(gates, progbar=progbar)
return qc
@property
def gates(self):
return tuple(self._gates)
@property
def num_gates(self):
return len(self._gates)
def ket_site_ind(self, i):
"""Get the site index for the given qubit."""
return self._ket_site_ind_id.format(i)
def bra_site_ind(self, i):
"""Get the 'bra' site index for the given qubit, if forming an operator."""
return self._bra_site_ind_id.format(i)
def gate_tag(self, g):
"""Get the tag for the given gate, indexed linearly."""
return self._gate_tag_id.format(g)
def round_tag(self, r):
"""Get the tag for the given round (/layer)."""
return self._round_tag_id.format(r)
def _init_state(self, N, dtype="complex128"):
return TN_from_sites_computational_state(
site_map={i: "0" for i in range(N)}, dtype=dtype
)
def _apply_gate(self, gate, tags=None, **gate_opts):
"""Apply a ``Gate`` to this ``Circuit``. This is the main method that
all calls to apply a gate should go through.
Parameters
----------
gate : Gate
The gate to apply.
tags : str or sequence of str, optional
Tags to add to the gate tensor(s).
"""
tags = tags_to_oset(tags)
gate_number = gate_opts.pop("_gate_number", None)
record_gate = gate_opts.pop("_record_gate", True)
tags |= self._gate_tags_for_record(gate, gate_number)
# overide any default gate opts
opts = {**self.gate_opts, **gate_opts}
if gate.controls:
# handle extra (low-rank) control structure
apply_controlled_gate(self._psi, gate, tags=tags, **opts)
elif gate.special:
# these are specified as a general function
SPECIAL_GATES[gate.label](
self._psi, *gate.params, *gate.qubits, **opts
)
else:
# gate supplied as a matrix/tensor
G = gate.array
if self.convert_eager:
G = self._maybe_convert_gate_array(G)
# apply the gate to the TN!
self._psi.gate_(G, gate.qubits, tags=tags, **opts)
# keep track of the gates applied
if record_gate:
self._gates.append(gate)
def _gate_tags_for_record(self, gate, gate_number=None):
tags = tags_to_oset(None)
if gate_number is None:
gate_number = self.num_gates
if self.tag_gate_numbers:
tags.add(self.gate_tag(gate_number))
if self.tag_gate_rounds and (gate.round is not None):
tags.add(self.round_tag(gate.round))
if self.tag_gate_labels and (gate.tag is not None):
tags.add(gate.tag)
return tags
def _apply_merged_1q_gate_run(self, gates, gate_number_offset=0, **gate_opts):
tags = tags_to_oset(gate_opts.pop("tags", None))
for i, gate in gates:
tags |= self._gate_tags_for_record(
gate, gate_number=gate_number_offset + i
)
opts = {**self.gate_opts, **gate_opts}
if self.convert_eager:
G = _combine_1q_gate_run(
gates, array_fn=self._maybe_convert_gate_array
).array
else:
G = _combine_1q_gate_run(gates).array
self._psi.gate_(G, gates[0][1].qubits, tags=tags, **opts)
def _apply_merged_2q_gate_run(self, gates, gate_number_offset=0, **gate_opts):
tags = tags_to_oset(gate_opts.pop("tags", None))
for i, gate in gates:
tags |= self._gate_tags_for_record(
gate, gate_number=gate_number_offset + i
)
opts = {**self.gate_opts, **gate_opts}
if self.convert_eager:
G = _combine_2q_gate_run(
gates, array_fn=self._maybe_convert_gate_array
).array
else:
G = _combine_2q_gate_run(gates).array
self._psi.gate_(G, gates[0][1].qubits, tags=tags, **opts)
def apply_gate(
self,
gate_id,
*gate_args,
params=None,
qubits=None,
controls=None,
gate_round=None,
parametrize=None,
**gate_opts,
):
"""Apply a single gate to this tensor network quantum circuit. If
``gate_round`` is supplied the tensor(s) added will be tagged with
``'ROUND_{gate_round}'``. Alternatively, putting an integer first like
so::
circuit.apply_gate(10, 'H', 7)
Is automatically translated to::
circuit.apply_gate('H', 7, gate_round=10)
Parameters
----------
gate_id : Gate, str, or array_like
Which gate to apply. This can be:
- A ``Gate`` instance, i.e. with parameters and qubits already
specified.
- A string, e.g. ``'H'``, ``'U3'``, etc. in which case
``gate_args`` should be supplied with ``(*params, *qubits)``.
- A raw array, in which case ``gate_args`` should be supplied
with ``(*qubits,)``.
gate_args : list[str]
The arguments to supply to it.
gate_round : int, optional
The gate round. If ``gate_id`` is integer-like, will also be taken
from here, with then ``gate_id, gate_args = gate_args[0],
gate_args[1:]``.
gate_opts
Supplied to the gate function, options here will override the
default ``gate_opts``.
"""
gate = parse_to_gate(
gate_id,
*gate_args,
params=params,
qubits=qubits,
controls=controls,
gate_round=gate_round,
parametrize=parametrize,
)
self._apply_gate(gate, **gate_opts)
def apply_gate_raw(
self, U, where, controls=None, gate_round=None, **gate_opts
):
"""Apply the raw array ``U`` as a gate on qubits in ``where``. It will
be assumed to be unitary for the sake of computing reverse lightcones.
"""
gate = Gate.from_raw(U, where, controls=controls, round=gate_round)
self._apply_gate(gate, **gate_opts)
def apply_gates(self, gates, progbar=False, **gate_opts):
"""Apply a sequence of gates to this tensor network quantum circuit.
Parameters
----------
gates : Sequence[Gate] or Sequence[Tuple]
The sequence of gates to apply.
gate_opts
Supplied to :meth:`~quimb.tensor.circuit.Circuit.apply_gate`.
"""
merge_1q = gate_opts.pop("merge_1q", "auto")
merge_2q = gate_opts.pop("merge_2q", "auto")
if merge_1q == "auto":
merge_1q = True
if merge_2q == "auto":
merge_2q = True
if merge_1q or merge_2q:
gates = tuple(
gate if isinstance(gate, Gate) else parse_to_gate(gate)
for gate in gates
)
pbar = None
if progbar:
from ..utils import progbar as _progbar
pbar = _progbar(total=len(gates))
gate_number_offset = len(self._gates)
for gate, gates_to_record in _iter_gates_with_merged_runs(
gates, merge_1q=merge_1q, merge_2q=merge_2q
):
if gate is None:
if len(gates_to_record[0][1].qubits) == 1:
self._apply_merged_1q_gate_run(
gates_to_record,
gate_number_offset=gate_number_offset,
**gate_opts,
)
else:
self._apply_merged_2q_gate_run(
gates_to_record,
gate_number_offset=gate_number_offset,
**gate_opts,
)
else:
self._apply_gate(
gate,
_gate_number=(
gate_number_offset + gates_to_record[0][0]
),
_record_gate=False,
**gate_opts,
)
if pbar is not None:
pbar.update(len(gates_to_record))
if pbar is not None:
pbar.close()
self._gates.extend(gates)
else:
if progbar:
from ..utils import progbar as _progbar
gates = tuple(gates)
gates = _progbar(gates, total=len(gates))
for gate in gates:
gate = (
gate if isinstance(gate, Gate) else parse_to_gate(gate)
)
self._apply_gate(gate, **gate_opts)
self._psi.squeeze_()
def h(self, i, gate_round=None, **kwargs):
self.apply_gate("H", i, gate_round=gate_round, **kwargs)
def x(self, i, gate_round=None, **kwargs):
self.apply_gate("X", i, gate_round=gate_round, **kwargs)
def y(self, i, gate_round=None, **kwargs):
self.apply_gate("Y", i, gate_round=gate_round, **kwargs)
def z(self, i, gate_round=None, **kwargs):
self.apply_gate("Z", i, gate_round=gate_round, **kwargs)
def s(self, i, gate_round=None, **kwargs):
self.apply_gate("S", i, gate_round=gate_round, **kwargs)
def sdg(self, i, gate_round=None, **kwargs):
self.apply_gate("SDG", i, gate_round=gate_round, **kwargs)
def t(self, i, gate_round=None, **kwargs):
self.apply_gate("T", i, gate_round=gate_round, **kwargs)
def tdg(self, i, gate_round=None, **kwargs):
self.apply_gate("TDG", i, gate_round=gate_round, **kwargs)
def sx(self, i, gate_round=None, **kwargs):
self.apply_gate("SX", i, gate_round=gate_round, **kwargs)
def sxdg(self, i, gate_round=None, **kwargs):
self.apply_gate("SXDG", i, gate_round=gate_round, **kwargs)
def x_1_2(self, i, gate_round=None, **kwargs):
self.apply_gate("X_1_2", i, gate_round=gate_round, **kwargs)
def y_1_2(self, i, gate_round=None, **kwargs):
self.apply_gate("Y_1_2", i, gate_round=gate_round, **kwargs)
def z_1_2(self, i, gate_round=None, **kwargs):
self.apply_gate("Z_1_2", i, gate_round=gate_round, **kwargs)
def w_1_2(self, i, gate_round=None, **kwargs):
self.apply_gate("W_1_2", i, gate_round=gate_round, **kwargs)
def hz_1_2(self, i, gate_round=None, **kwargs):
self.apply_gate("HZ_1_2", i, gate_round=gate_round, **kwargs)
# constant two qubit gates
def cnot(self, i, j, gate_round=None, **kwargs):
self.apply_gate("CNOT", i, j, gate_round=gate_round, **kwargs)
def cx(self, i, j, gate_round=None, **kwargs):
self.apply_gate("CX", i, j, gate_round=gate_round, **kwargs)
def cy(self, i, j, gate_round=None, **kwargs):
self.apply_gate("CY", i, j, gate_round=gate_round, **kwargs)
def cz(self, i, j, gate_round=None, **kwargs):
self.apply_gate("CZ", i, j, gate_round=gate_round, **kwargs)
def iswap(self, i, j, gate_round=None, **kwargs):
self.apply_gate("ISWAP", i, j, **kwargs)
# special non-tensor gates
def iden(self, i, gate_round=None):
pass
def swap(self, i, j, gate_round=None, **kwargs):
self.apply_gate("SWAP", i, j, **kwargs)
# parametrizable gates
def rx(self, theta, i, gate_round=None, parametrize=False, **kwargs):
self.apply_gate(
"RX",
theta,
i,
gate_round=gate_round,
parametrize=parametrize,
**kwargs,
)
def ry(self, theta, i, gate_round=None, parametrize=False, **kwargs):
self.apply_gate(
"RY",
theta,
i,
gate_round=gate_round,
parametrize=parametrize,
**kwargs,
)
def rz(self, theta, i, gate_round=None, parametrize=False, **kwargs):
self.apply_gate(
"RZ",
theta,
i,
gate_round=gate_round,
parametrize=parametrize,
**kwargs,
)
def u3(
self,
theta,
phi,
lamda,
i,
gate_round=None,
parametrize=False,
**kwargs,
):
self.apply_gate(
"U3",
theta,
phi,
lamda,
i,
gate_round=gate_round,
parametrize=parametrize,
**kwargs,
)
def u2(self, phi, lamda, i, gate_round=None, parametrize=False, **kwargs):
self.apply_gate(
"U2",
phi,
lamda,
i,
gate_round=gate_round,
parametrize=parametrize,
**kwargs,
)
def u1(self, lamda, i, gate_round=None, parametrize=False, **kwargs):
self.apply_gate(
"U1",
lamda,
i,
gate_round=gate_round,
parametrize=parametrize,
**kwargs,
)
def phase(self, lamda, i, gate_round=None, parametrize=False, **kwargs):
self.apply_gate(
"PHASE",
lamda,
i,
gate_round=gate_round,
parametrize=parametrize,
**kwargs,
)
def cu3(
self,
theta,
phi,
lamda,
i,
j,
gate_round=None,
parametrize=False,
**kwargs,
):
self.apply_gate(
"CU3",
theta,
phi,
lamda,
i,
j,
gate_round=gate_round,
parametrize=parametrize,
**kwargs,
)
def cu2(
self, phi, lamda, i, j, gate_round=None, parametrize=False, **kwargs
):
self.apply_gate(
"CU2",
phi,
lamda,
i,
j,
gate_round=gate_round,
parametrize=parametrize,
**kwargs,
)
def cu1(self, lamda, i, j, gate_round=None, parametrize=False, **kwargs):
self.apply_gate(
"CU1",
lamda,
i,
j,
gate_round=gate_round,
parametrize=parametrize,
**kwargs,
)
def cphase(
self, lamda, i, j, gate_round=None, parametrize=False, **kwargs
):
self.apply_gate(
"CPHASE",
lamda,
i,
j,
gate_round=gate_round,
parametrize=parametrize,
**kwargs,
)
def fsim(
self, theta, phi, i, j, gate_round=None, parametrize=False, **kwargs
):
self.apply_gate(
"FSIM",
theta,
phi,
i,
j,
gate_round=gate_round,
parametrize=parametrize,
**kwargs,
)
def fsimg(
self,
theta,
zeta,
chi,
gamma,
phi,
i,
j,
gate_round=None,
parametrize=False,
**kwargs,
):
self.apply_gate(
"FSIMG",
theta,
zeta,
chi,
gamma,
phi,
i,
j,
gate_round=gate_round,
parametrize=parametrize,
**kwargs,
)
def givens(
self, theta, i, j, gate_round=None, parametrize=False, **kwargs
):
self.apply_gate(
"GIVENS",
theta,
i,
j,
gate_round=gate_round,
parametrize=parametrize,
**kwargs,
)
def givens2(
self, theta, phi, i, j, gate_round=None, parametrize=False, **kwargs
):
self.apply_gate(
"GIVENS2",
theta,
phi,
i,
j,
gate_round=gate_round,
parametrize=parametrize,
**kwargs,
)
def xx_plus_yy(
self, theta, beta, i, j, gate_round=None, parametrize=False, **kwargs
):
self.apply_gate(
"XXPLUSYY",
theta,
beta,
i,
j,
gate_round=gate_round,
parametrize=parametrize,
**kwargs,
)
def xx_minus_yy(
self, theta, beta, i, j, gate_round=None, parametrize=False, **kwargs
):
self.apply_gate(
"XXMINUSYY",
theta,
beta,
i,
j,
gate_round=gate_round,
parametrize=parametrize,
**kwargs,
)
def rxx(self, theta, i, j, gate_round=None, parametrize=False, **kwargs):
self.apply_gate(
"RXX",
theta,
i,
j,
gate_round=gate_round,
parametrize=parametrize,
**kwargs,
)
def ryy(self, theta, i, j, gate_round=None, parametrize=False, **kwargs):
self.apply_gate(
"RYY",
theta,
i,
j,
gate_round=gate_round,
parametrize=parametrize,
**kwargs,
)
def rzz(self, theta, i, j, gate_round=None, parametrize=False, **kwargs):
self.apply_gate(
"RZZ",
theta,
i,
j,
gate_round=gate_round,
parametrize=parametrize,
**kwargs,
)
def crx(self, theta, i, j, gate_round=None, parametrize=False, **kwargs):
self.apply_gate(
"CRX",
theta,
i,
j,
gate_round=gate_round,
parametrize=parametrize,
**kwargs,
)
def cry(self, theta, i, j, gate_round=None, parametrize=False, **kwargs):
self.apply_gate(
"CRY",
theta,
i,
j,
gate_round=gate_round,
parametrize=parametrize,
**kwargs,
)
def crz(self, theta, i, j, gate_round=None, parametrize=False, **kwargs):
self.apply_gate(
"CRZ",
theta,
i,
j,
gate_round=gate_round,
parametrize=parametrize,
**kwargs,
)
def su4(
self,
theta1,
phi1,
lamda1,
theta2,
phi2,
lamda2,
theta3,
phi3,
lamda3,
theta4,
phi4,
lamda4,
t1,
t2,
t3,
i,
j,
gate_round=None,
parametrize=False,
**kwargs,
):
self.apply_gate(
"SU4",
theta1,
phi1,
lamda1,
theta2,
phi2,
lamda2,
theta3,
phi3,
lamda3,
theta4,
phi4,
lamda4,
t1,
t2,
t3,
i,
j,
gate_round=gate_round,
parametrize=parametrize,
**kwargs,
)
def ccx(self, i, j, k, gate_round=None, **kwargs):
self.apply_gate("CCX", i, j, k, gate_round=gate_round, **kwargs)
def ccnot(self, i, j, k, gate_round=None, **kwargs):
self.apply_gate("CCNOT", i, j, k, gate_round=gate_round, **kwargs)
def toffoli(self, i, j, k, gate_round=None, **kwargs):
self.apply_gate("TOFFOLI", i, j, k, gate_round=gate_round, **kwargs)
def ccy(self, i, j, k, gate_round=None, **kwargs):
self.apply_gate("CCY", i, j, k, gate_round=gate_round, **kwargs)
def ccz(self, i, j, k, gate_round=None, **kwargs):
self.apply_gate("CCZ", i, j, k, gate_round=gate_round, **kwargs)
def cswap(self, i, j, k, gate_round=None, **kwargs):
self.apply_gate("CSWAP", i, j, k, gate_round=gate_round, **kwargs)
def fredkin(self, i, j, k, gate_round=None, **kwargs):
self.apply_gate("FREDKIN", i, j, k, gate_round=gate_round, **kwargs)
@property
def psi(self):
"""Tensor network representation of the wavefunction."""
# make sure all same dtype and drop singlet dimensions
psi = self._psi.copy()
psi.squeeze_()
if not self.convert_eager:
# not converted yet
self._maybe_convert(psi)
return psi
def get_uni(self, transposed=False):
"""Tensor network representation of the unitary operator (i.e. with
the initial state removed).
"""
U = self.psi
if transposed:
# rename the initial state rand_uuid bonds to 1D site inds
ixmap = {
self.ket_site_ind(i): self.bra_site_ind(i)
for i in range(self.N)
}
else:
ixmap = {}
# the first `N` tensors should be the tensors of input state
tids = tuple(U.tensor_map)[: self.N]
for i, tid in enumerate(tids):
t = U.pop_tensor(tid)
(old_ix,) = t.inds
if transposed:
ixmap[old_ix] = f"k{i}"
else:
ixmap[old_ix] = f"b{i}"
U.reindex_(ixmap)
U.view_as_(
TensorNetworkGenOperator,
upper_ind_id=self._ket_site_ind_id,
lower_ind_id=self._bra_site_ind_id,
)
return U
@property
def uni(self):
import warnings
warnings.warn(
"In future the tensor network returned by ``circ.uni`` will not "
"be transposed as it is currently, to match the expectation from "
"``U = circ.uni.to_dense()`` behaving like ``U @ psi``. You can "
"retain this behaviour with ``circ.get_uni(transposed=True)``.",
FutureWarning,
)
return self.get_uni(transposed=True)
def get_reverse_lightcone_tags(self, where):
"""Get the tags of gates in this circuit corresponding to the 'reverse'
lightcone propagating backwards from registers in ``where``.
Parameters
----------
where : int or sequence of int
The register or register to get the reverse lightcone of.
Returns
-------
tuple[str]
The sequence of gate tags (``GATE_{i}``, ...) corresponding to the
lightcone.
"""
if isinstance(where, numbers.Integral):
cone = {where}
else:
cone = set(where)
lightcone_tags = []
for i, gate in reversed(tuple(enumerate(self._gates))):
if gate.label == "IDEN":
continue
elif gate.controls:
# TODO: only add if any *targets* in cone, requires changes
# elsewhere to make sure tensors aren't then missing
regs = {*gate.controls, *gate.qubits}
if regs & cone:
lightcone_tags.append(self.gate_tag(i))
cone |= regs
elif gate.label == "SWAP":
i, j = gate.qubits
i_in_cone = i in cone
j_in_cone = j in cone
if i_in_cone:
cone.add(j)
else:
cone.discard(j)
if j_in_cone:
cone.add(i)
else:
cone.discard(i)
else:
regs = set(gate.qubits)
if regs & cone:
lightcone_tags.append(self.gate_tag(i))
cone |= regs
# initial state is always part of the lightcone
lightcone_tags.append("PSI0")
lightcone_tags.reverse()
return tuple(lightcone_tags)
def get_psi_reverse_lightcone(self, where, keep_psi0=False):
"""Get just the bit of the wavefunction in the reverse lightcone of
sites in ``where`` - i.e. causally linked.
Parameters
----------
where : int, or sequence of int
The sites to propagate the the lightcone back from, supplied to
:meth:`~quimb.tensor.circuit.Circuit.get_reverse_lightcone_tags`.
keep_psi0 : bool, optional
Keep the tensors corresponding to the initial wavefunction
regardless of whether they are outside of the lightcone.
Returns
-------
psi_lc : TensorNetwork1DVector
"""
if isinstance(where, numbers.Integral):
where = (where,)
psi = self.psi
lightcone_tags = self.get_reverse_lightcone_tags(where)
psi_lc = psi.select_any(lightcone_tags).view_like_(psi)
if not keep_psi0:
# these sites are in the lightcone regardless of being alone
site_inds = set(map(psi.site_ind, where))
for tid, t in tuple(psi_lc.tensor_map.items()):
# get all tensors connected to this tensor (incld itself)
neighbors = oset_union(psi_lc.ind_map[ix] for ix in t.inds)
# lone tensor not attached to anything - drop it
# but only if it isn't directly in the ``where`` region
if (len(neighbors) == 1) and set(t.inds).isdisjoint(site_inds):
psi_lc.pop_tensor(tid)
return psi_lc
def clear_storage(self):
"""Clear all cached data."""
self._storage.clear()
self._sampled_conditionals.clear()
self._marginal_storage_size = 0
self._sample_n_gates = self.num_gates
def _maybe_init_storage(self):
# clear/create the cache if circuit has changed
if self._sample_n_gates != self.num_gates:
self.clear_storage()
def get_psi_simplified(
self, seq="ADCRS", atol=1e-12, equalize_norms=False
):
"""Get the full wavefunction post local tensor network simplification.
Parameters
----------
seq : str, optional
Which local tensor network simplifications to perform and in which
order, see
:meth:`~quimb.tensor.tensor_core.TensorNetwork.full_simplify`.
atol : float, optional
The tolerance with which to compare to zero when applying
:meth:`~quimb.tensor.tensor_core.TensorNetwork.full_simplify`.
equalize_norms : bool, optional
Actively renormalize tensor norms during simplification.
Returns
-------
psi : TensorNetwork1DVector
"""
self._maybe_init_storage()
key = ("psi_simplified", seq, atol)
if key in self._storage:
return self._storage[key].copy()
# we simplify and store a copy
psi = self._psi.copy()
psi.squeeze_()
# make sure to keep all outer indices
output_inds = tuple(map(psi.site_ind, range(self.N)))
# simplify the state and cache it
psi.full_simplify_(
seq=seq,
atol=atol,
output_inds=output_inds,
equalize_norms=equalize_norms,
)
self._storage[key] = psi
# return a copy so we can modify it inplace
return psi.copy()
def get_rdm_lightcone_simplified(
self,
where,
seq="ADCRS",
atol=1e-12,
equalize_norms=False,
):
"""Get a simplified TN of the norm of the wavefunction, with
gates outside reverse lightcone of ``where`` cancelled, and physical
indices within ``where`` preserved so that they can be fixed (sliced)
or used as output indices.
Parameters
----------
where : int or sequence of int
The region assumed to be the target density matrix essentially.
Supplied to
:meth:`~quimb.tensor.circuit.Circuit.get_reverse_lightcone_tags`.
seq : str, optional
Which local tensor network simplifications to perform and in which
order, see
:meth:`~quimb.tensor.tensor_core.TensorNetwork.full_simplify`.
atol : float, optional
The tolerance with which to compare to zero when applying
:meth:`~quimb.tensor.tensor_core.TensorNetwork.full_simplify`.
equalize_norms : bool, optional
Actively renormalize tensor norms during simplification.
Returns
-------
TensorNetwork
"""
key = ("rdm_lightcone_simplified", tuple(sorted(where)), seq, atol)
if key in self._storage:
return self._storage[key].copy()
ket_lc = self.get_psi_reverse_lightcone(where)
k_inds = tuple(map(self.ket_site_ind, where))
b_inds = tuple(map(self.bra_site_ind, where))
bra_lc = ket_lc.conj().reindex(dict(zip(k_inds, b_inds)))
rho_lc = bra_lc | ket_lc
# don't want to simplify site indices in region away
output_inds = b_inds + k_inds
# # simplify the norm and cache it
rho_lc.full_simplify_(
seq=seq,
atol=atol,
output_inds=output_inds,
equalize_norms=equalize_norms,
)
self._storage[key] = rho_lc
# return a copy so we can modify it inplace
return rho_lc.copy()
def amplitude(
self,
b,
optimize="auto-hq",
simplify_sequence="ADCRS",
simplify_atol=1e-12,
simplify_equalize_norms=True,
backend=None,
dtype=None,
rehearse=False,
):
r"""Get the amplitude coefficient of bitstring ``b``.
.. math::
c_b = \langle b | \psi \rangle
Parameters
----------
b : str or sequence of int
The bitstring to compute the transition amplitude for.
optimize : str, optional
Contraction path optimizer to use for the amplitude, can be a
non-reusable path optimizer as only called once (though path won't
be cached for later use in that case).
simplify_sequence : str, optional
Which local tensor network simplifications to perform and in which
order, see
:meth:`~quimb.tensor.tensor_core.TensorNetwork.full_simplify`.
simplify_atol : float, optional
The tolerance with which to compare to zero when applying
:meth:`~quimb.tensor.tensor_core.TensorNetwork.full_simplify`.
simplify_equalize_norms : bool, optional
Actively renormalize tensor norms during simplification.
backend : str, optional
Backend to perform the contraction with, e.g. ``'numpy'``,
``'cupy'`` or ``'jax'``. Passed to ``cotengra``.
dtype : str, optional
Data type to cast the TN to before contraction.
rehearse : bool or "tn", optional
If ``True``, generate and cache the simplified tensor network and
contraction tree but don't actually perform the contraction.
Returns a dict with keys ``"tn"`` and ``'tree'`` with the tensor
network that will be contracted and the corresponding contraction
tree if so.
"""
self._maybe_init_storage()
if len(b) != self.N:
raise ValueError(
f"Bit-string {b} length does not "
f"match number of qubits {self.N}."
)
fs_opts = {
"seq": simplify_sequence,
"atol": simplify_atol,
"equalize_norms": simplify_equalize_norms,
}
# get the full wavefunction simplified
psi_b = self.get_psi_simplified(**fs_opts)
# fix the output indices to the correct bitstring
for i, x in zip(range(self.N), b):
psi_b.isel_({psi_b.site_ind(i): x})
# perform a final simplification and cast
psi_b.full_simplify_(**fs_opts)
self._maybe_convert(psi_b, dtype)
if rehearse == "tn":
return psi_b
tree = psi_b.contraction_tree(output_inds=(), optimize=optimize)
if rehearse:
return rehearsal_dict(psi_b, tree)
# perform the full contraction with the tree found
c_b = psi_b.contract(
all, output_inds=(), optimize=tree, backend=backend
)
return c_b
def amplitude_rehearse(
self,
b="random",
simplify_sequence="ADCRS",
simplify_atol=1e-12,
simplify_equalize_norms=True,
optimize="auto-hq",
dtype=None,
rehearse=True,
):
"""Perform just the tensor network simplifications and contraction tree
finding associated with computing a single amplitude (caching the
results) but don't perform the actual contraction.
Parameters
----------
b : 'random', str or sequence of int
The bitstring to rehearse computing the transition amplitude for,
if ``'random'`` (the default) a random bitstring will be used.
optimize : str, optional
Contraction path optimizer to use for the marginal, can be a
non-reusable path optimizer as only called once (though path won't
be cached for later use in that case).
simplify_sequence : str, optional
Which local tensor network simplifications to perform and in which
order, see
:meth:`~quimb.tensor.tensor_core.TensorNetwork.full_simplify`.
simplify_atol : float, optional
The tolerance with which to compare to zero when applying
:meth:`~quimb.tensor.tensor_core.TensorNetwork.full_simplify`.
simplify_equalize_norms : bool, optional
Actively renormalize tensor norms during simplification.
backend : str, optional
Backend to perform the marginal contraction with, e.g. ``'numpy'``,
``'cupy'`` or ``'jax'``. Passed to ``cotengra``.
dtype : str, optional
Data type to cast the TN to before contraction.
Returns
-------
dict
"""
if b == "random":
b = "r" * self.N
return self.amplitude(
b=b,
optimize=optimize,
dtype=dtype,
rehearse=rehearse,
simplify_sequence=simplify_sequence,
simplify_atol=simplify_atol,
simplify_equalize_norms=simplify_equalize_norms,
)
amplitude_tn = functools.partialmethod(amplitude_rehearse, rehearse="tn")
def partial_trace(
self,
keep,
optimize="auto-hq",
simplify_sequence="ADCRS",
simplify_atol=1e-12,
simplify_equalize_norms=True,
backend=None,
dtype=None,
rehearse=False,
):
r"""Perform the partial trace on the circuit wavefunction, retaining
only qubits in ``keep``, and making use of reverse lightcone
cancellation:
.. math::
\rho_{\bar{q}} = Tr_{\bar{p}}
|\psi_{\bar{q}} \rangle \langle \psi_{\bar{q}}|
Where :math:`\bar{q}` is the set of qubits to keep,
:math:`\psi_{\bar{q}}` is the circuit wavefunction only with gates in
the causal cone of this set, and :math:`\bar{p}` is the remaining
qubits.
Parameters
----------
keep : int or sequence of int
The qubit(s) to keep as we trace out the rest.
optimize : str, optional
Contraction path optimizer to use for the reduced density matrix,
can be a non-reusable path optimizer as only called once (though
path won't be cached for later use in that case).
simplify_sequence : str, optional
Which local tensor network simplifications to perform and in which
order, see
:meth:`~quimb.tensor.tensor_core.TensorNetwork.full_simplify`.
simplify_atol : float, optional
The tolerance with which to compare to zero when applying
:meth:`~quimb.tensor.tensor_core.TensorNetwork.full_simplify`.
simplify_equalize_norms : bool, optional
Actively renormalize tensor norms during simplification.
backend : str, optional
Backend to perform the marginal contraction with, e.g. ``'numpy'``,
``'cupy'`` or ``'jax'``. Passed to ``cotengra``.
dtype : str, optional
Data type to cast the TN to before contraction.
rehearse : bool or "tn", optional
If ``True``, generate and cache the simplified tensor network and
contraction tree but don't actually perform the contraction.
Returns a dict with keys ``"tn"`` and ``'tree'`` with the tensor
network that will be contracted and the corresponding contraction
tree if so.
Returns
-------
array or dict
"""
if isinstance(keep, numbers.Integral):
keep = (keep,)
output_inds = tuple(map(self.ket_site_ind, keep)) + tuple(
map(self.bra_site_ind, keep)
)
rho = self.get_rdm_lightcone_simplified(
where=keep,
seq=simplify_sequence,
atol=simplify_atol,
equalize_norms=simplify_equalize_norms,
)
self._maybe_convert(rho, dtype)
if rehearse == "tn":
return rho
tree = rho.contraction_tree(output_inds=output_inds, optimize=optimize)
if rehearse:
return rehearsal_dict(rho, tree)
# perform the full contraction with the tree found
rho_dense = rho.contract(
all,
output_inds=output_inds,
optimize=tree,
backend=backend,
).data
return ops.reshape(rho_dense, [2 ** len(keep), 2 ** len(keep)])
partial_trace_rehearse = functools.partialmethod(
partial_trace, rehearse=True
)
partial_trace_tn = functools.partialmethod(partial_trace, rehearse="tn")
def local_expectation(
self,
G,
where,
optimize="auto-hq",
simplify_sequence="ADCRS",
simplify_atol=1e-12,
simplify_equalize_norms=True,
backend=None,
dtype=None,
rehearse=False,
):
r"""Compute the a single expectation value of operator ``G``, acting on
sites ``where``, making use of reverse lightcone cancellation.
.. math::
\langle \psi_{\bar{q}} | G_{\bar{q}} | \psi_{\bar{q}} \rangle
where :math:`\bar{q}` is the set of qubits :math:`G` acts one and
:math:`\psi_{\bar{q}}` is the circuit wavefunction only with gates in
the causal cone of this set. If you supply a tuple or list of gates
then the expectations will be computed simultaneously.
Parameters
----------
G : array or sequence[array]
The raw operator(s) to find the expectation of.
where : int or sequence of int
Which qubits the operator acts on.
optimize : str, optional
Contraction path optimizer to use for the local expectation,
can be a non-reusable path optimizer as only called once (though
path won't be cached for later use in that case).
simplify_sequence : str, optional
Which local tensor network simplifications to perform and in which
order, see
:meth:`~quimb.tensor.tensor_core.TensorNetwork.full_simplify`.
simplify_atol : float, optional
The tolerance with which to compare to zero when applying
:meth:`~quimb.tensor.tensor_core.TensorNetwork.full_simplify`.
simplify_equalize_norms : bool, optional
Actively renormalize tensor norms during simplification.
backend : str, optional
Backend to perform the marginal contraction with, e.g. ``'numpy'``,
``'cupy'`` or ``'jax'``. Passed to ``cotengra``.
dtype : str, optional
Data type to cast the TN to before contraction.
gate_opts : None or dict_like
Options to use when applying ``G`` to the wavefunction.
rehearse : bool or "tn", optional
If ``True``, generate and cache the simplified tensor network and
contraction tree but don't actually perform the contraction.
Returns a dict with keys ``'tn'`` and ``'tree'`` with the tensor
network that will be contracted and the corresponding contraction
tree if so.
Returns
-------
scalar, tuple[scalar] or dict
"""
if isinstance(where, numbers.Integral):
where = (where,)
fs_opts = {
"seq": simplify_sequence,
"atol": simplify_atol,
"equalize_norms": simplify_equalize_norms,
}
rho = self.get_rdm_lightcone_simplified(where=where, **fs_opts)
k_inds = tuple(self.ket_site_ind(i) for i in where)
b_inds = tuple(self.bra_site_ind(i) for i in where)
if isinstance(G, (list, tuple)):
# if we have multiple expectations create an extra indexed stack
nG = len(G)
G_data = do("stack", G)
G_data = reshape(G_data, (nG,) + (2,) * 2 * len(where))
output_inds = (rand_uuid(),)
else:
G_data = reshape(G, (2,) * 2 * len(where))
output_inds = ()
TG = Tensor(data=G_data, inds=output_inds + b_inds + k_inds)
rhoG = rho | TG
rhoG.full_simplify_(output_inds=output_inds, **fs_opts)
self._maybe_convert(rhoG, dtype)
if rehearse == "tn":
return rhoG
tree = rhoG.contraction_tree(
output_inds=output_inds, optimize=optimize
)
if rehearse:
return rehearsal_dict(rhoG, tree)
g_ex = rhoG.contract(
all,
output_inds=output_inds,
optimize=tree,
backend=backend,
)
if isinstance(g_ex, Tensor):
g_ex = tuple(g_ex.data)
return g_ex
local_expectation_rehearse = functools.partialmethod(
local_expectation, rehearse=True
)
local_expectation_tn = functools.partialmethod(
local_expectation, rehearse="tn"
)
def compute_marginal(
self,
where,
fix=None,
optimize="auto-hq",
backend=None,
dtype="complex64",
simplify_sequence="ADCRS",
simplify_atol=1e-6,
simplify_equalize_norms=True,
rehearse=False,
):
"""Compute the probability tensor of qubits in ``where``, given
possibly fixed qubits in ``fix`` and tracing everything else having
removed redundant unitary gates.
Parameters
----------
where : sequence of int
The qubits to compute the marginal probability distribution of.
fix : None or dict[int, str], optional
Measurement results on other qubits to fix.
optimize : str, optional
Contraction path optimizer to use for the marginal, can be a
non-reusable path optimizer as only called once (though path won't
be cached for later use in that case).
backend : str, optional
Backend to perform the marginal contraction with, e.g. ``'numpy'``,
``'cupy'`` or ``'jax'``. Passed to ``cotengra``.
dtype : str, optional
Data type to cast the TN to before contraction.
simplify_sequence : str, optional
Which local tensor network simplifications to perform and in which
order, see
:meth:`~quimb.tensor.tensor_core.TensorNetwork.full_simplify`.
simplify_atol : float, optional
The tolerance with which to compare to zero when applying
:meth:`~quimb.tensor.tensor_core.TensorNetwork.full_simplify`.
simplify_equalize_norms : bool, optional
Actively renormalize tensor norms during simplification.
rehearse : bool or "tn", optional
Whether to perform the marginal contraction or just return the
associated TN and contraction tree.
"""
self._maybe_init_storage()
# index trick to contract straight to reduced density matrix diagonal
# rho_ii -> p_i (i.e. insert a COPY tensor into the norm)
output_inds = [self.ket_site_ind(i) for i in where]
fs_opts = {
"seq": simplify_sequence,
"atol": simplify_atol,
"equalize_norms": simplify_equalize_norms,
}
# lightcone region is target qubit plus fixed qubits
region = set(where)
if fix is not None:
region |= set(fix)
region = tuple(sorted(region))
# have we fixed or are measuring all qubits?
final_marginal = len(region) == self.N
# these both are cached and produce TN copies
if final_marginal:
# won't need to partially trace anything -> just need ket
nm_lc = self.get_psi_simplified(**fs_opts)
else:
# can use lightcone cancellation on partially traced qubits
nm_lc = self.get_rdm_lightcone_simplified(region, **fs_opts)
# re-connect the ket and bra indices as taking diagonal
nm_lc.reindex_(
{self.bra_site_ind(i): self.ket_site_ind(i) for i in region}
)
if fix:
# project (slice) fixed tensors with bitstring
# this severs the indices connecting bra and ket on fixed sites
nm_lc.isel_({self.ket_site_ind(i): b for i, b in fix.items()})
# having sliced we can do a final simplify
nm_lc.full_simplify_(output_inds=output_inds, **fs_opts)
# for stability with very small probabilities, scale by average prob
if fix is not None:
nfact = 2 ** len(fix)
if final_marginal:
nm_lc.multiply_(nfact**0.5, spread_over="all")
else:
nm_lc.multiply_(nfact, spread_over="all")
# cast to desired data type
self._maybe_convert(nm_lc, dtype)
if rehearse == "tn":
return nm_lc
# NB. the tree isn't *neccesarily* the same each time due to the post
# projection full simplify, however there is also the lower level
# contraction path cache if the structure generated *is* the same
# so still pretty efficient to just overwrite
tree = nm_lc.contraction_tree(
output_inds=output_inds,
optimize=optimize,
)
if rehearse:
return rehearsal_dict(nm_lc, tree)
# perform the full contraction with the tree found
p_marginal = abs(
nm_lc.contract(
all,
output_inds=output_inds,
optimize=tree,
backend=backend,
).data
)
if final_marginal:
# we only did half the ket contraction so need to square
p_marginal = p_marginal**2
if fix is not None:
p_marginal = p_marginal / nfact
return p_marginal
compute_marginal_rehearse = functools.partialmethod(
compute_marginal, rehearse=True
)
compute_marginal_tn = functools.partialmethod(
compute_marginal, rehearse="tn"
)
def calc_qubit_ordering(self, qubits=None, method="greedy-lightcone"):
"""Get a order to measure ``qubits`` in, by greedily choosing whichever
has the smallest reverse lightcone followed by whichever expands this
lightcone *least*.
Parameters
----------
qubits : None or sequence of int
The qubits to generate a lightcone ordering for, if ``None``,
assume all qubits.
Returns
-------
tuple[int]
The order to 'measure' qubits in.
"""
self._maybe_init_storage()
if qubits is None:
qubits = tuple(range(self.N))
else:
qubits = tuple(sorted(qubits))
key = ("lightcone_ordering", method, qubits)
# check the cache first
if key in self._storage:
return self._storage[key]
if method == "greedy-lightcone":
cone = set()
lctgs = {
i: set(self.get_reverse_lightcone_tags(i)) for i in qubits
}
order = []
while lctgs:
# get the next qubit which adds least num gates to lightcone
next_qubit = min(lctgs, key=lambda i: len(lctgs[i] - cone))
cone |= lctgs.pop(next_qubit)
order.append(next_qubit)
else:
# use graph distance based hierachical clustering
psi = self.get_psi_simplified("R")
qubit_inds = tuple(map(psi.site_ind, qubits))
tids = psi._get_tids_from_inds(qubit_inds, "any")
matcher = re.compile(psi.site_ind_id.format(r"(\d+)"))
order = []
for tid in psi.compute_hierarchical_ordering(tids, method=method):
t = psi.tensor_map[tid]
for ind in t.inds:
for sq in matcher.findall(ind):
order.append(int(sq))
order = self._storage[key] = tuple(order)
return order
def _parse_qubits_order(self, qubits=None, order=None):
"""Simply initializes the default of measuring all qubits, and the
default order, or checks that ``order`` is a permutation of ``qubits``.
"""
if qubits is None:
qubits = range(self.N)
if order is None:
order = self.calc_qubit_ordering(qubits)
elif set(qubits) != set(order):
raise ValueError("``order`` must be a permutation of ``qubits``.")
return qubits, order
def _group_order(self, order, group_size=1):
"""Take the qubit ordering ``order`` and batch it in groups of size
``group_size``, sorting the qubits (for caching reasons) within each
group.
"""
return tuple(
tuple(sorted(g)) for g in partition_all(group_size, order)
)
def get_qubit_distances(self, method="dijkstra", alpha=2):
"""Get a nested dictionary of qubit distances. This is computed from a
graph representing qubit interactions. The graph has an edge between
qubits if they are acted on by the same gate, and the distance-weight
of the edge is exponentially small in the number of gates between them.
Parameters
----------
method : {'dijkstra', 'resistance'}, optional
The method to use to compute the qubit distances. See
:func:`networkx.all_pairs_dijkstra_path_length` and
:func:`networkx.resistance_distance`.
alpha : float, optional
The distance weight between qubits is ``alpha**(num_gates - 1 )``.
Returns
-------
dict[int, dict[int, float]]
The distance between each pair of qubits, accessed like
``distances[q1][q2]``. If two qubits are not connected, the
distance is missing.
"""
import networkx as nx
G = nx.Graph()
for g in self.gates:
for q1, q2 in itertools.combinations(g.qubits, 2):
if G.has_edge(q1, q2):
G[q1][q2]["weight"] /= alpha
else:
G.add_edge(q1, q2, weight=1)
if method == "dijkstra":
distances = dict(
nx.all_pairs_dijkstra_path_length(G, weight="weight")
)
elif method == "resistance":
distances = nx.resistance_distance(G, weight="weight")
else:
raise ValueError(f"Unknown method {method}.")
return distances
def reordered_gates_dfs_clustered(self):
"""Get the gates reordered by a depth first search traversal of the
multi-qubit gate graph that greedily selects successive gates which
are 'close' in graph distance, and shifts single qubit gates to be
adjacent to multi-qubit gates where possible.
"""
# first we make a directed graph of the multi-qubit gates
successors = {}
predecessors = {}
single_qubit_stacks = {}
single_qubit_predecessors = {}
last_gates = {}
queue = []
for i, g in enumerate(self.gates):
if g.total_qubit_count == 1:
# lazily accumulate single qubit gates
(q,) = g.qubits
single_qubit_stacks.setdefault(q, []).append(i)
else:
pi = predecessors[i] = []
sqpi = single_qubit_predecessors[i] = []
for q in g.qubits:
# collect any single qubit gates acting on this qubit
sqpi.extend(single_qubit_stacks.pop(q, []))
if q in last_gates:
# qubit has already been acted on -> have an edge
h = last_gates[q]
# mark h as a predecessor of i
pi.append(h)
# mark i as a successor of h
successors.setdefault(h, []).append(i)
# mark qubit as acted on
last_gates[q] = i
if len(pi) == 0:
# no predecessors -> is possible starting multiqubit gate
queue.append(i)
# then we traverse the multi-qubit gates in a depth first, topological
# order, breaking ties by minimizing the distance between active qubits
distances = self.get_qubit_distances()
def gate_distance(i, j):
qis = self.gates[i].qubits
qjs = self.gates[j].qubits
return min(
distances[q1].get(q2, float("inf")) for q1 in qis for q2 in qjs
)
# sort initial queue by qubit with smallest index
queue.sort(key=lambda i: min(self.gates[i].qubits))
new_gates = []
while queue:
i = queue.pop(0)
# first flush any single qubit gates acting on the qubits of gate i
new_gates.extend(
self.gates[j] for j in single_qubit_predecessors.pop(i, [])
)
# then add the gate itself
new_gates.append(self.gates[i])
# then remove i as a predecessor of its successors
for j in successors.pop(i, []):
pj = predecessors[j]
pj.remove(i)
if not pj:
# j has no more predecessors -> can be added to queue
queue.append(j)
# check if this is the last time q is acted on,
# if so flush any remaining single qubit gates
for q in self.gates[i].qubits:
if last_gates[q] == i:
# qubit has been acted on for the last time
new_gates.extend(
self.gates[j] for j in single_qubit_stacks.pop(q, [])
)
# sort the queue of possible next gates
queue.sort(key=lambda k: gate_distance(i, k))
# flush any remaining single qubit gates
for q in sorted(single_qubit_stacks):
new_gates.extend(self.gates[j] for j in single_qubit_stacks.pop(q))
return new_gates
def sample(
self,
C,
qubits=None,
order=None,
group_size=10,
max_marginal_storage=2**20,
seed=None,
optimize="auto-hq",
backend=None,
dtype="complex64",
simplify_sequence="ADCRS",
simplify_atol=1e-6,
simplify_equalize_norms=True,
):
r"""Sample the circuit given by ``gates``, ``C`` times, using lightcone
cancelling and caching marginal distribution results. This is a
generator. This proceeds as a chain of marginal computations.
Assuming we have ``group_size=1``, and some ordering of the qubits,
:math:`\{q_0, q_1, q_2, q_3, \ldots\}` we first compute:
.. math::
p(q_0) = \mathrm{diag} \mathrm{Tr}_{1, 2, 3,\ldots}
| \psi_{0} \rangle \langle \psi_{0} |
I.e. simply the probability distribution on a single qubit, conditioned
on nothing. The subscript on :math:`\psi` refers to the fact that we
only need gates from the causal cone of qubit 0.
From this we can sample an outcome, either 0 or 1, if we
call this :math:`r_0` we can then move on to the next marginal:
.. math::
p(q_1 | r_0) = \mathrm{diag} \mathrm{Tr}_{2, 3,\ldots}
\langle r_0
| \psi_{0, 1} \rangle \langle \psi_{0, 1} |
r_0 \rangle
I.e. the probability distribution of the next qubit, given our prior
result. We can sample from this to get :math:`r_1`. Then we compute:
.. math::
p(q_2 | r_0 r_1) = \mathrm{diag} \mathrm{Tr}_{3,\ldots}
\langle r_0 r_1
| \psi_{0, 1, 2} \rangle \langle \psi_{0, 1, 2} |
r_0 r_1 \rangle
Eventually we will reach the 'final marginal', which we can compute as
.. math::
|\langle r_0 r_1 r_2 r_3 \ldots | \psi \rangle|^2
since there is nothing left to trace out.
Parameters
----------
C : int
The number of times to sample.
qubits : None or sequence of int, optional
Which qubits to measure, defaults (``None``) to all qubits.
order : None or sequence of int, optional
Which order to measure the qubits in, defaults (``None``) to an
order based on greedily expanding the smallest reverse lightcone.
If specified it should be a permutation of ``qubits``.
group_size : int, optional
How many qubits to group together into marginals, the larger this
is the fewer marginals need to be computed, which can be faster at
the cost of higher memory. The marginal themselves will each be
of size ``2**group_size``.
max_marginal_storage : int, optional
The total cumulative number of marginal probabilites to cache, once
this is exceeded caching will be turned off.
seed : None or int, optional
A random seed, passed to ``numpy.random.seed`` if given.
optimize : str, optional
Contraction path optimizer to use for the marginals, shouldn't be
a non-reusable path optimizer as called on many different TNs.
Passed to :func:`cotengra.array_contract_tree`.
backend : str, optional
Backend to perform the marginal contraction with, e.g. ``'numpy'``,
``'cupy'`` or ``'jax'``. Passed to ``cotengra``.
dtype : str, optional
Data type to cast the TN to before contraction.
simplify_sequence : str, optional
Which local tensor network simplifications to perform and in which
order, see
:meth:`~quimb.tensor.tensor_core.TensorNetwork.full_simplify`.
simplify_atol : float, optional
The tolerance with which to compare to zero when applying
:meth:`~quimb.tensor.tensor_core.TensorNetwork.full_simplify`.
simplify_equalize_norms : bool, optional
Actively renormalize tensor norms during simplification.
Yields
------
bitstrings : sequence of str
"""
# init TN norms, contraction trees, and marginals
self._maybe_init_storage()
rng = np.random.default_rng(seed)
# which qubits and an ordering e.g. (2, 3, 4, 5), (5, 3, 4, 2)
qubits, order = self._parse_qubits_order(qubits, order)
# group the ordering e.g. ((5, 3), (4, 2))
groups = self._group_order(order, group_size)
result = dict()
for _ in range(C):
for where in groups:
# key - (tuple[int] where, tuple[tuple[int q, str b])
# value - marginal probability distribution of `where` given
# prior results, as an ndarray
# e.g. ((2,), ((0, '0'), (1, '0'))): array([1., 0.]), means
# prob(qubit2='0')=1 given qubit0='0' and qubit1='0'
# prob(qubit2='1')=0 given qubit0='0' and qubit1='0'
key = (where, tuple(sorted(result.items())))
if key not in self._sampled_conditionals:
# compute p(qs=x | current bitstring)
p = self.compute_marginal(
where=where,
fix=result,
optimize=optimize,
backend=backend,
dtype=dtype,
simplify_sequence=simplify_sequence,
simplify_atol=simplify_atol,
simplify_equalize_norms=simplify_equalize_norms,
)
p = do("to_numpy", p).astype("float64")
p /= p.sum()
if self._marginal_storage_size <= max_marginal_storage:
self._sampled_conditionals[key] = p
self._marginal_storage_size += p.size
else:
p = self._sampled_conditionals[key]
# the sampled bitstring e.g. '1' or '001010101'
b_where = sample_bitstring_from_prob_ndarray(p, seed=rng)
# split back into individual qubit results
for q, b in zip(where, b_where):
result[q] = b
yield "".join(result[i] for i in qubits)
result.clear()
def sample_rehearse(
self,
qubits=None,
order=None,
group_size=10,
result=None,
optimize="auto-hq",
simplify_sequence="ADCRS",
simplify_atol=1e-6,
simplify_equalize_norms=True,
rehearse=True,
progbar=False,
):
"""Perform the preparations and contraction tree findings for
:meth:`~quimb.tensor.circuit.Circuit.sample`, caching various
intermedidate objects, but don't perform the main contractions.
Parameters
----------
qubits : None or sequence of int, optional
Which qubits to measure, defaults (``None``) to all qubits.
order : None or sequence of int, optional
Which order to measure the qubits in, defaults (``None``) to an
order based on greedily expanding the smallest reverse lightcone.
group_size : int, optional
How many qubits to group together into marginals, the larger this
is the fewer marginals need to be computed, which can be faster at
the cost of higher memory. The marginal's size itself is
exponential in ``group_size``.
result : None or dict[int, str], optional
Explicitly check the computational cost of this result, assumed to
be all zeros if not given.
optimize : str, optional
Contraction path optimizer to use for the marginals, shouldn't be
a non-reusable path optimizer as called on many different TNs.
Passed to :func:`cotengra.array_contract_tree`.
simplify_sequence : str, optional
Which local tensor network simplifications to perform and in which
order, see
:meth:`~quimb.tensor.tensor_core.TensorNetwork.full_simplify`.
simplify_atol : float, optional
The tolerance with which to compare to zero when applying
:meth:`~quimb.tensor.tensor_core.TensorNetwork.full_simplify`.
simplify_equalize_norms : bool, optional
Actively renormalize tensor norms during simplification.
progbar : bool, optional
Whether to show the progress of finding each contraction tree.
Returns
-------
dict[tuple[int], dict]
One contraction tree object per grouped marginal computation.
The keys of the dict are the qubits the marginal is computed for,
the values are a dict containing a representative simplified tensor
network (key: 'tn') and the main contraction tree (key: 'tree').
"""
# init TN norms, contraction trees, and marginals
self._maybe_init_storage()
qubits, order = self._parse_qubits_order(qubits, order)
groups = self._group_order(order, group_size)
if result is None:
result = {q: "r" for q in qubits}
fix = {}
tns_and_trees = {}
for where in _progbar(groups, disable=not progbar):
tns_and_trees[where] = self.compute_marginal(
where=where,
fix=fix,
optimize=optimize,
simplify_sequence=simplify_sequence,
simplify_atol=simplify_atol,
simplify_equalize_norms=simplify_equalize_norms,
rehearse=rehearse,
)
# set the result of qubit ``q`` arbitrarily
for q in where:
fix[q] = result[q]
return tns_and_trees
sample_tns = functools.partialmethod(sample_rehearse, rehearse="tn")
def sample_chaotic(
self,
C,
marginal_qubits,
fix=None,
max_marginal_storage=2**20,
seed=None,
optimize="auto-hq",
backend=None,
dtype="complex64",
simplify_sequence="ADCRS",
simplify_atol=1e-6,
simplify_equalize_norms=True,
):
r"""Sample from this circuit, *assuming* it to be chaotic. Which is to
say, only compute and sample correctly from the final marginal,
assuming that the distribution on the other qubits is uniform.
Given ``marginal_qubits=5`` for instance, for each sample a random
bit-string :math:`r_0 r_1 r_2 \ldots r_{N - 6}` for the remaining
:math:`N - 5` qubits will be chosen, then the final marginal will be
computed as
.. math::
p(q_{N-5}q_{N-4}q_{N-3}q_{N-2}q_{N-1}
| r_0 r_1 r_2 \ldots r_{N-6})
=
|\langle r_0 r_1 r_2 \ldots r_{N - 6} | \psi \rangle|^2
and then sampled from. Note the expression on the right hand side has
5 open indices here and so is a tensor, however if ``marginal_qubits``
is not too big then the cost of contracting this is very similar to
a single amplitude.
.. note::
This method *assumes* the circuit is chaotic, if its not, then the
samples produced will not be an accurate representation of the
probability distribution.
Parameters
----------
C : int
The number of times to sample.
marginal_qubits : int or sequence of int
The number of qubits to treat as marginal, or the actual qubits. If
an int is given then the qubits treated as marginal will be
``circuit.calc_qubit_ordering()[:marginal_qubits]``.
fix : None or dict[int, str], optional
Measurement results on other qubits to fix. These will be randomly
sampled if ``fix`` is not given or a qubit is missing.
seed : None or int, optional
A random seed, passed to ``numpy.random.seed`` if given.
optimize : str, optional
Contraction path optimizer to use for the marginal, can be a
non-reusable path optimizer as only called once (though path won't
be cached for later use in that case).
backend : str, optional
Backend to perform the marginal contraction with, e.g. ``'numpy'``,
``'cupy'`` or ``'jax'``. Passed to ``cotengra``.
dtype : str, optional
Data type to cast the TN to before contraction.
simplify_sequence : str, optional
Which local tensor network simplifications to perform and in which
order, see
:meth:`~quimb.tensor.tensor_core.TensorNetwork.full_simplify`.
simplify_atol : float, optional
The tolerance with which to compare to zero when applying
:meth:`~quimb.tensor.tensor_core.TensorNetwork.full_simplify`.
simplify_equalize_norms : bool, optional
Actively renormalize tensor norms during simplification.
Yields
------
str
"""
# init TN norms, contraction trees, and marginals
self._maybe_init_storage()
qubits = tuple(range(self.N))
rng = np.random.default_rng(seed)
# choose which qubits to treat as marginal - ideally 'towards one side'
# to increase contraction efficiency
if isinstance(marginal_qubits, numbers.Integral):
marginal_qubits = self.calc_qubit_ordering()[:marginal_qubits]
where = tuple(sorted(marginal_qubits))
# we will uniformly sample, and post-select on, the remaining qubits
fix_qubits = tuple(q for q in qubits if q not in where)
result = dict()
for _ in range(C):
# generate a random bit-string for the fixed qubits
for q in fix_qubits:
if (fix is None) or (q not in fix):
result[q] = rng.choice(("0", "1"))
else:
result[q] = fix[q]
# compute the remaining marginal
key = (where, tuple(sorted(result.items())))
if key not in self._sampled_conditionals:
p = self.compute_marginal(
where=where,
fix=result,
optimize=optimize,
backend=backend,
dtype=dtype,
simplify_sequence=simplify_sequence,
simplify_atol=simplify_atol,
simplify_equalize_norms=simplify_equalize_norms,
)
p = do("to_numpy", p).astype("float64")
p /= p.sum()
if self._marginal_storage_size <= max_marginal_storage:
self._sampled_conditionals[key] = p
self._marginal_storage_size += p.size
else:
p = self._sampled_conditionals[key]
# sample a bit-string for the marginal qubits
b_where = sample_bitstring_from_prob_ndarray(p)
# split back into individual qubit results
for q, b in zip(where, b_where):
result[q] = b
yield "".join(result[i] for i in qubits)
result.clear()
def sample_chaotic_rehearse(
self,
marginal_qubits,
result=None,
optimize="auto-hq",
simplify_sequence="ADCRS",
simplify_atol=1e-6,
simplify_equalize_norms=True,
dtype="complex64",
rehearse=True,
):
"""Rehearse chaotic sampling (perform just the TN simplifications and
contraction tree finding).
Parameters
----------
marginal_qubits : int or sequence of int
The number of qubits to treat as marginal, or the actual qubits. If
an int is given then the qubits treated as marginal will be
``circuit.calc_qubit_ordering()[:marginal_qubits]``.
result : None or dict[int, str], optional
Explicitly check the computational cost of this result, assumed to
be all zeros if not given.
optimize : str, optional
Contraction path optimizer to use for the marginal, can be a
non-reusable path optimizer as only called once (though path won't
be cached for later use in that case).
simplify_sequence : str, optional
Which local tensor network simplifications to perform and in which
order, see
:meth:`~quimb.tensor.tensor_core.TensorNetwork.full_simplify`.
simplify_atol : float, optional
The tolerance with which to compare to zero when applying
:meth:`~quimb.tensor.tensor_core.TensorNetwork.full_simplify`.
simplify_equalize_norms : bool, optional
Actively renormalize tensor norms during simplification.
dtype : str, optional
Data type to cast the TN to before contraction.
Returns
-------
dict[tuple[int], dict]
The contraction path information for the main computation, the key
is the qubits that formed the final marginal. The value is itself a
dict with keys ``'tn'`` - a representative tensor network - and
``'tree'`` - the contraction tree.
"""
# init TN norms, contraction trees, and marginals
self._maybe_init_storage()
qubits = tuple(range(self.N))
if isinstance(marginal_qubits, numbers.Integral):
marginal_qubits = self.calc_qubit_ordering()[:marginal_qubits]
where = tuple(sorted(marginal_qubits))
fix_qubits = tuple(q for q in qubits if q not in where)
if result is None:
fix = {q: "0" for q in fix_qubits}
else:
fix = {q: result[q] for q in fix_qubits}
rehs = self.compute_marginal(
where=where,
fix=fix,
optimize=optimize,
simplify_sequence=simplify_sequence,
simplify_atol=simplify_atol,
simplify_equalize_norms=simplify_equalize_norms,
dtype=dtype,
rehearse=rehearse,
)
if rehearse == "tn":
return rehs
return {where: rehs}
sample_chaotic_tn = functools.partialmethod(
sample_chaotic_rehearse, rehearse="tn"
)
def get_gate_by_gate_circuits(self, group_size=10):
"""Get a sequence of circuits by partitioning the gates into groups
such circuit `i + 1` acts on at most ``group_size`` new qubits compared
to circuit `i`.
Parameters
----------
group_size : int, optional
The maximum number of new qubits that can be acted on by a circuit
compared to its predecessor.
Returns
-------
Sequence[dict]
A sequence of dicts, each with keys ``'circuit'`` and ``'where'``,
where the former is a :class:`~quimb.tensor.circuit.Circuit` and
the latter the tuple of new qubits that it acts on comparaed to
the previous circuit.
"""
circs = [self.__class__(self.N)]
groups = []
current_group = set()
# this ensures that single qubit gates are always adjacent to
# multi-qubit gates and will thus always be included in the same group
gates = self.reordered_gates_dfs_clustered()
for gate in gates:
# if we were to add next gate, how many new qubits would we have?
next_group = current_group.union(gate.qubits)
if len(next_group) > group_size:
# over the limit: flush a copy of the current circuit and group
groups.append(tuple(sorted(current_group)))
circs.append(circs[-1].copy())
# start a new group
current_group = set(gate.qubits)
else:
# add the gate to the current group
current_group = next_group
circs[-1].apply_gate(gate)
# add the final group corresponding to circs[-1]
groups.append(tuple(sorted(current_group)))
return tuple({"circuit": c, "where": g} for c, g in zip(circs, groups))
def sample_gate_by_gate(
self,
C,
group_size=10,
seed=None,
max_marginal_storage=2**20,
optimize="auto-hq",
backend=None,
dtype="complex64",
simplify_sequence="ADCRS",
simplify_atol=1e-6,
simplify_equalize_norms=True,
):
"""Sample this circuit using the gate-by-gate method, where we 'evolve'
a result bitstring by sequentially including more and more gates, at
each step updating the result by computing a full conditional marginal.
See "How to simulate quantum measurement without computing marginals"
by Sergey Bravyi, David Gosset, Yinchen Liu
(https://arxiv.org/abs/2112.08499). The overall complexity of this is
guaranteed to be similar to that of computing a single amplitude which
can be much better than the naive "qubit-by-qubit" (`.sample`) method.
However, it requires evaluting a number of tensor networks that scales
linearly with the number of gates which can offset any practical
advantages for shallow circuits for example.
Parameters
----------
C : int
The number of samples to generate.
group_size : int, optional
The maximum number of qubits that can be acted on by a circuit
compared to its predecessor. This will be the dimension of the
marginal computed at each step.
seed : None or int, optional
A random seed, passed to ``numpy.random.seed`` if given.
max_marginal_storage : int, optional
The total cumulative number of marginal probabilites to cache, once
this is exceeded caching will be turned off.
optimize : str, optional
Contraction path optimizer to use for the marginals, shouldn't be
a non-reusable path optimizer as called on many different TNs.
Passed to :func:`cotengra.array_contract_tree`.
backend : str, optional
Backend to perform the marginal contraction with, e.g. ``'numpy'``,
``'cupy'`` or ``'jax'``. Passed to ``cotengra``.
dtype : str, optional
Data type to cast the TN to before contraction.
simplify_sequence : str, optional
Which local tensor network simplifications to perform and in which
order, see
:meth:`~quimb.tensor.tensor_core.TensorNetwork.full_simplify`.
simplify_atol : float, optional
The tolerance with which to compare to zero when applying
:meth:`~quimb.tensor.tensor_core.TensorNetwork.full_simplify`.
simplify_equalize_norms : bool, optional
Actively renormalize tensor norms during simplification.
rehearse : bool, optional
If ``True``, generate and cache the simplified tensor network and
contraction tree but don't actually perform the contraction.
Returns a dict with keys ``'tn'`` and ``'tree'`` with the tensor
network that will be contracted and the corresponding contraction
tree if so.
Yields
------
str
"""
self._maybe_init_storage()
rng = np.random.default_rng(seed)
key = ("gate_by_gate_circuits", group_size)
try:
circs_wheres = self._storage[key]
except KeyError:
circs_wheres = self.get_gate_by_gate_circuits(group_size)
self._storage[key] = circs_wheres
for _ in range(C):
# start with all qubits in the |0> state
result = {q: "0" for q in range(self.N)}
for circ_where in circs_wheres:
# get the next circuit and the new group of qubits
circ_g = circ_where["circuit"]
where = circ_where["where"]
# remove the new group of qubits from our current result
for q in where:
result.pop(q)
# check if we have already computed the conditional
key = (where, tuple(sorted(result.items())))
if key not in circ_g._sampled_conditionals:
p = circ_g.compute_marginal(
where,
fix=result,
optimize=optimize,
backend=backend,
dtype=dtype,
simplify_sequence=simplify_sequence,
simplify_atol=simplify_atol,
simplify_equalize_norms=simplify_equalize_norms,
)
p /= p.sum()
if circ_g._marginal_storage_size <= max_marginal_storage:
circ_g._sampled_conditionals[key] = p
circ_g._marginal_storage_size += p.size
else:
p = circ_g._sampled_conditionals[key]
# sample a configuration for our new group
b_where = sample_bitstring_from_prob_ndarray(p, seed=rng)
# update the fixed qubits given new group result
for q, qx in zip(where, b_where):
result[q] = qx
yield "".join(result[i] for i in range(self.N))
def sample_gate_by_gate_rehearse(
self,
group_size=10,
optimize="auto-hq",
dtype="complex64",
simplify_sequence="ADCRS",
simplify_atol=1e-6,
simplify_equalize_norms=True,
rehearse=True,
progbar=False,
):
"""Perform the preparations and contraction tree findings for
:meth:`~quimb.tensor.circuit.Circuit.sample_gate_by_gate`, caching
various intermedidate objects, but don't perform the main contractions.
Parameters
----------
group_size : int, optional
The maximum number of qubits that can be acted on by a circuit
compared to its predecessor. This will be the dimension of the
marginal computed at each step.
optimize : str, optional
Contraction path optimizer to use for the marginals, shouldn't be
a non-reusable path optimizer as called on many different TNs.
Passed to :func:`cotengra.array_contract_tree`.
dtype : str, optional
Data type to cast the TN to before contraction.
simplify_sequence : str, optional
Which local tensor network simplifications to perform and in which
order, see
:meth:`~quimb.tensor.tensor_core.TensorNetwork.full_simplify`.
simplify_atol : float, optional
The tolerance with which to compare to zero when applying
:meth:`~quimb.tensor.tensor_core.TensorNetwork.full_simplify`.
simplify_equalize_norms : bool, optional
Actively renormalize tensor norms during simplification.
rehearse : True or "tn", optional
If ``True``, generate and cache the simplified tensor network and
contraction tree but don't actually perform the contraction. If
"tn", only generate the simplified tensor networks.
Returns
-------
Sequence[dict] or Sequence[TensorNetwork]
"""
self._maybe_init_storage()
key = ("gate_by_gate_circuits", group_size)
try:
circs_wheres = self._storage[key]
except KeyError:
circs_wheres = self.get_gate_by_gate_circuits(group_size)
self._storage[key] = circs_wheres
rehs = []
result = {q: "0" for q in range(self.N)}
for circs_wheres in _progbar(circs_wheres, disable=not progbar):
# get the next circuit and the new group of qubits
circ_g = circs_wheres["circuit"]
where = circs_wheres["where"]
# remove the new group of qubits from our current result
for q in where:
result.pop(q)
r = circ_g.compute_marginal(
where,
fix=result,
optimize=optimize,
dtype=dtype,
simplify_sequence=simplify_sequence,
simplify_atol=simplify_atol,
simplify_equalize_norms=simplify_equalize_norms,
rehearse=rehearse,
)
if rehearse != "tn":
r["where"] = where
r["circuit"] = circ_g
rehs.append(r)
# update the fixed qubits with randomly rotated results so we
# don't get zero probability networks when simplifying
for q in where:
result[q] = "r"
return rehs
sample_gate_by_gate_tns = functools.partialmethod(
sample_gate_by_gate_rehearse, rehearse="tn"
)
def to_dense(
self,
reverse=False,
optimize="auto-hq",
simplify_sequence="R",
simplify_atol=1e-12,
simplify_equalize_norms=True,
backend=None,
dtype=None,
rehearse=False,
):
"""Generate the dense representation of the final wavefunction.
Parameters
----------
reverse : bool, optional
Whether to reverse the order of the subsystems, to match the
convention of qiskit for example.
optimize : str, optional
Contraction path optimizer to use for the contraction, can be a
non-reusable path optimizer as only called once (though path won't
be cached for later use in that case).
dtype : dtype or str, optional
If given, convert the tensors to this dtype prior to contraction.
simplify_sequence : str, optional
Which local tensor network simplifications to perform and in which
order, see
:meth:`~quimb.tensor.tensor_core.TensorNetwork.full_simplify`.
simplify_atol : float, optional
The tolerance with which to compare to zero when applying
:meth:`~quimb.tensor.tensor_core.TensorNetwork.full_simplify`.
simplify_equalize_norms : bool, optional
Actively renormalize tensor norms during simplification.
backend : str, optional
Backend to perform the contraction with, e.g. ``'numpy'``,
``'cupy'`` or ``'jax'``. Passed to ``cotengra``.
dtype : str, optional
Data type to cast the TN to before contraction.
rehearse : bool, optional
If ``True``, generate and cache the simplified tensor network and
contraction tree but don't actually perform the contraction.
Returns a dict with keys ``'tn'`` and ``'tree'`` with the tensor
network that will be contracted and the corresponding contraction
tree if so.
Returns
-------
psi : qarray
The densely represented wavefunction with ``dtype`` data.
"""
psi = self.get_psi_simplified(
seq=simplify_sequence,
atol=simplify_atol,
equalize_norms=simplify_equalize_norms,
)
self._maybe_convert(psi, dtype)
if rehearse == "tn":
return psi
output_inds = tuple(map(psi.site_ind, range(self.N)))
if reverse:
output_inds = output_inds[::-1]
tree = psi.contraction_tree(output_inds=output_inds, optimize=optimize)
if rehearse:
return rehearsal_dict(psi, tree)
# perform the full contraction with the path found
psi_tensor = psi.contract(
all,
output_inds=output_inds,
optimize=tree,
backend=backend,
).data
k = ops.reshape(psi_tensor, (-1, 1))
if isinstance(k, np.ndarray):
k = qu.qarray(k)
return k
to_dense_rehearse = functools.partialmethod(to_dense, rehearse=True)
to_dense_tn = functools.partialmethod(to_dense, rehearse="tn")
def simulate_counts(self, C, seed=None, reverse=False, **to_dense_opts):
"""Simulate measuring all qubits in the computational basis many times.
Unlike :meth:`~quimb.tensor.circuit.Circuit.sample`, this generates all
the samples simultaneously using the full wavefunction constructed from
:meth:`~quimb.tensor.circuit.Circuit.to_dense`, then calling
:func:`~quimb.calc.simulate_counts`.
.. warning::
Because this constructs the full wavefunction it always requires
exponential memory in the number of qubits, regardless of circuit
depth and structure.
Parameters
----------
C : int
The number of 'experimental runs', i.e. total counts.
seed : int, optional
A seed for reproducibility.
reverse : bool, optional
Whether to reverse the order of the subsystems, to match the
convention of qiskit for example.
to_dense_opts
Suppled to :meth:`~quimb.tensor.circuit.Circuit.to_dense`.
Returns
-------
results : dict[str, int]
The number of recorded counts for each
"""
p_dense = self.to_dense(reverse=reverse, **to_dense_opts)
return qu.simulate_counts(p_dense, C=C, seed=seed)
def schrodinger_contract(self, *args, **contract_opts):
ntensor = self._psi.num_tensors
path = [(0, 1)] + [(0, i) for i in reversed(range(1, ntensor - 1))]
return self.psi.contract(*args, optimize=path, **contract_opts)
def xeb(
self,
samples_or_counts,
cache=None,
cache_maxsize=2**20,
progbar=False,
**amplitude_opts,
):
"""Compute the linear cross entropy benchmark (XEB) for samples or
counts, amplitude per amplitude.
Parameters
----------
samples_or_counts : Iterable[str] or Dict[str, int]
Either the raw bitstring samples or a dict mapping bitstrings to
the number of counts observed.
cache : dict, optional
A dictionary to store the probabilities in, if not supplied
``quimb.utils.LRU(cache_maxsize)`` will be used.
cache_maxsize, optional
The maximum size of the cache to be used.
progbar, optional
Whether to show progress as the bitstrings are iterated over.
amplitude_opts
Supplied to :meth:`~quimb.tensor.circuit.Circuit.amplitude`.
"""
try:
it = samples_or_counts.items()
except AttributeError:
it = zip(samples_or_counts, itertools.repeat(1))
if progbar:
it = _progbar(it)
M = 0
psum = 0.0
if cache is None:
cache = LRU(cache_maxsize)
for b, cnt in it:
try:
p = cache[b]
except KeyError:
p = cache[b] = abs(self.amplitude(b, **amplitude_opts)) ** 2
psum += cnt * p
M += cnt
return (2**self.N) / M * psum - 1
def xeb_ex(
self,
optimize="auto-hq",
simplify_sequence="R",
simplify_atol=1e-12,
simplify_equalize_norms=True,
dtype=None,
backend=None,
autojit=False,
progbar=False,
**contract_opts,
):
"""Compute the exactly expected XEB for this circuit. The main feature
here is that if you supply a cotengra optimizer that searches for
sliced indices then the XEB will be computed without constructing the
full wavefunction.
Parameters
----------
optimize : str or PathOptimizer, optional
Contraction path optimizer.
simplify_sequence : str, optional
Simplifications to apply to tensor network prior to contraction.
simplify_sequence : str, optional
Which local tensor network simplifications to perform and in which
order, see
:meth:`~quimb.tensor.tensor_core.TensorNetwork.full_simplify`.
simplify_atol : float, optional
The tolerance with which to compare to zero when applying
:meth:`~quimb.tensor.tensor_core.TensorNetwork.full_simplify`.
dtype : str, optional
Data type to cast the TN to before contraction.
backend : str, optional
Convert tensors to, and then use contractions from, this library.
autojit : bool, optional
Apply ``autoray.autojit`` to the contraciton and map-reduce.
progbar : bool, optional
Show progress in terms of number of wavefunction chunks processed.
"""
# get potentially simplified TN of full wavefunction
psi = self.to_dense_tn(
simplify_sequence=simplify_sequence,
simplify_atol=simplify_atol,
simplify_equalize_norms=simplify_equalize_norms,
dtype=dtype,
)
# find a possibly sliced contraction tree
output_inds = tuple(map(psi.site_ind, range(self.N)))
tree = psi.contraction_tree(optimize=optimize, output_inds=output_inds)
arrays = psi.arrays
if backend is not None:
arrays = [do("array", x, like=backend) for x in arrays]
# perform map-reduce style computation over output wavefunction chunks
# so we don't need entire wavefunction in memory at same time
chunks = tree.gen_output_chunks(
arrays, autojit=autojit, **contract_opts
)
if progbar:
chunks = _progbar(chunks, total=tree.nchunks)
def f(chunk):
return do("sum", do("abs", chunk) ** 4)
if autojit:
# since we convert the arrays above, the jit backend is
# automatically inferred
from autoray import autojit
f = autojit(f)
p2sum = functools.reduce(operator.add, map(f, chunks))
return 2**self.N * p2sum - 1
def update_params_from(self, tn):
"""Assuming ``tn`` is a tensor network with tensors tagged ``GATE_{i}``
corresponding to this circuit (e.g. from ``circ.psi`` or ``circ.uni``)
but with updated parameters, update the current circuit parameters and
tensors with those values.
This is an inplace modification of the ``Circuit``.
Parameters
----------
tn : TensorNetwork
The tensor network to find the updated parameters from.
"""
for i, gate in enumerate(self._gates):
tag = self.gate_tag(i)
t = tn[tag]
# sanity check that tensor(s) `t` correspond to the correct gate
if gate.tag not in get_tags(t):
raise ValueError(
f"The tensor(s) correponding to gate {i} "
f"should be tagged with '{gate.tag}', got {t}."
)
# only update gates and tensors if they are parametrizable
if isinstance(t, PTensor):
# update the actual tensor
self._psi[tag].params = t.params
# update the circuit's gate record
self._gates[i] = Gate(
label=gate.label,
params=t.params,
qubits=gate.qubits,
round=gate.round,
parametrize=True,
)
self.clear_storage()
def draw(
self,
figsize=None,
radius=1 / 3,
drawcolor=(0.5, 0.5, 0.5),
linewidth=1,
):
"""Draw a simple linear schematic of the circuit.
Parameters
----------
figsize : tuple, optional
The size of the figure, if not given will be set based on the
number of gates and qubits.
radius : float, optional
The radius of the gates.
drawcolor : tuple, optional
The color of the wires.
linewidth : float, optional
The linewidth of the wires.
Returns
-------
fig : matplotlib.Figure
The figure object.
ax : matplotlib.Axes
The axis object.
"""
from quimb.schematic import Drawing, hash_to_color
if figsize is None:
figsize = (self.num_gates / 6, self.N / 6)
d = Drawing(
figsize=figsize,
presets=dict(
wire=dict(
color=drawcolor,
linewidth=linewidth,
),
gate=dict(
radius=radius,
),
),
)
depths = {}
for i, g in enumerate(self.gates):
# level = max(depths.get(q, 0) for q in g.qubits) + 1
level = i
if len(g.qubits) == 1:
(q,) = g.qubits
# draw line from previous gate to this one
d.line(
(depths.get(q, -1) + radius, q),
(level - radius, q),
preset="wire",
zorder=level,
)
# draw the gate
d.marker(
(level, q),
color=hash_to_color(g.label),
zorder=0,
preset="gate",
)
# record last gate on this qubit
depths[q] = level
else:
# stretch a box over all qubits
qmin = min(g.qubits)
qmax = max(g.qubits)
d.rectangle(
(level, qmin),
(level, qmax),
color=hash_to_color(g.label),
zorder=0,
alpha=1 / 3,
preset="gate",
)
for q in g.qubits:
# draw markers on each qubit acted on
d.marker(
(level, q),
color=hash_to_color(g.label),
zorder=0,
preset="gate",
)
# draw lines from previous gate to this one
d.line(
(depths.get(q, -1) + radius, q),
(level - radius, q),
preset="wire",
zorder=level,
)
# record last gate on this qubit
depths[q] = level
# draw final lines to the right
level = max(depths.values(), default=0) + 1
for q in depths:
d.line((depths.get(q, -1), q), (level, q), preset="wire")
return d.fig, d.ax
def __repr__(self):
r = "<Circuit(n={}, num_gates={}, gate_opts={})>"
return r.format(self.N, self.num_gates, self.gate_opts)
class CircuitMPS(Circuit):
"""Quantum circuit simulation keeping the state always in a MPS form. If
you think the circuit will not build up much entanglement, or you just want
to keep a rigorous handle on how much entanglement is present, this can
be useful.
Parameters
----------
N : int, optional
The number of qubits in the circuit.
psi0 : TensorNetwork1DVector, optional
The initial state, assumed to be ``|00000....0>`` if not given. The
state is always copied and the tag ``PSI0`` added.
max_bond : int, optional
The maximum bond dimension to truncate to when applying gates, if any.
This is simply a shortcut for setting ``gate_opts['max_bond']``.
cutoff : float, optional
The singular value cutoff to use when truncating the state.
This is simply a shortcut for setting ``gate_opts['cutoff']``.
gate_opts : dict, optional
Default options to pass to each gate, for example, "max_bond" and
"cutoff" etc.
gate_contract : str, optional
The default method for applying gates. Relevant MPS options are:
- ``'auto-mps'``: automatically choose a method that maintains the
MPS form (default). This uses ``'swap+split'`` for 2-qubit gates
and ``'nonlocal'`` for 3+ qubit gates.
- ``'swap+split'``: swap nonlocal qubits to be next to each other,
before applying the gate, then swapping them back
- ``'nonlocal'``: turn the gate into a potentially nonlocal (sub) MPO
and apply it directly. See :func:`tensor_network_1d_compress`.
dtype : str, optional
The data type to use for the state tensor.
to_backend : callable, optional
A function to convert tensor data to a particular backend.
convert_eager : bool, optional
Whether to eagerly perform dtype casting and application of
`to_backend` as gates are supplied, or wait until after the necessary
TNs for a particular task such as sampling are formed and simplified.
Eager conversion (`convert_eager=True`) is the default mode for
MPS simulation, unlike full contraction.
circuit_opts
Supplied to :class:`~quimb.tensor.circuit.Circuit`.
Attributes
----------
psi : MatrixProductState
The current state of the circuit, always in MPS form.
Examples
--------
Create a circuit object that always uses the "nonlocal" method for
contracting in gates, and the "dm" compression method within that, using
a large cutoff and maximum bond dimension::
circ = qtn.CircuitMPS(
N=56,
gate_opts=dict(
contract="nonlocal",
method="dm",
max_bond=1024,
cutoff=1e-3,
)
)
"""
def __init__(
self,
N=None,
*,
psi0=None,
max_bond=None,
cutoff=1e-10,
gate_opts=None,
gate_contract="auto-mps",
dtype=None,
to_backend=None,
convert_eager=True,
**circuit_opts,
):
gate_opts = ensure_dict(gate_opts)
gate_opts.setdefault("contract", gate_contract)
gate_opts.setdefault("propagate_tags", False)
gate_opts.setdefault("max_bond", max_bond)
gate_opts.setdefault("cutoff", cutoff)
# this is used to pass around the canonical form
gate_opts.setdefault("info", {})
circuit_opts.setdefault("tag_gate_numbers", False)
circuit_opts.setdefault("tag_gate_rounds", False)
circuit_opts.setdefault("tag_gate_labels", False)
circuit_opts.setdefault("dtype", dtype)
circuit_opts.setdefault("to_backend", to_backend)
circuit_opts.setdefault("convert_eager", convert_eager)
super().__init__(N, psi0, gate_opts, **circuit_opts)
def _init_state(self, N, dtype="complex128"):
return MPS_computational_state("0" * N, dtype=dtype)
def apply_gates(self, gates, progbar=False, **gate_opts):
merge_1q = gate_opts.pop("merge_1q", "auto")
merge_2q = gate_opts.pop("merge_2q", "auto")
if merge_1q == "auto":
merge_1q = True
if merge_2q == "auto":
# MPS truncation semantics are sensitive to when a 2q gate is
# materialized, so keep the default conservative here.
merge_2q = False
if merge_1q or merge_2q:
gates = tuple(
gate if isinstance(gate, Gate) else parse_to_gate(gate)
for gate in gates
)
pbar = None
if progbar:
from ..utils import progbar as _progbar
pbar = _progbar(total=len(gates))
pbar.set_description(
f"max_bond={self._psi.max_bond()}, "
f"error~={self.error_estimate():.3g}"
)
gate_number_offset = len(self._gates)
for gate, gates_to_record in _iter_gates_with_merged_runs(
gates, merge_1q=merge_1q, merge_2q=merge_2q
):
if gate is None:
if len(gates_to_record[0][1].qubits) == 1:
self._apply_merged_1q_gate_run(
gates_to_record,
gate_number_offset=gate_number_offset,
**gate_opts,
)
else:
self._apply_merged_2q_gate_run(
gates_to_record,
gate_number_offset=gate_number_offset,
**gate_opts,
)
gate_for_progress = gates_to_record[-1][1]
else:
self._apply_gate(
gate,
_gate_number=(
gate_number_offset + gates_to_record[0][0]
),
_record_gate=False,
**gate_opts,
)
gate_for_progress = gate
if pbar is not None:
pbar.update(len(gates_to_record))
if gate_for_progress.total_qubit_count >= 2:
# these don't change for single qubit gates
pbar.set_description(
f"max_bond={self._psi.max_bond()}, "
f"error~={self.error_estimate():.3g}"
)
if pbar is not None:
pbar.close()
self._gates.extend(gates)
else:
if progbar:
from ..utils import progbar as _progbar
gates = tuple(gates)
gates = _progbar(gates, total=len(gates))
gates.set_description(
f"max_bond={self._psi.max_bond()}, "
f"error~={self.error_estimate():.3g}"
)
for gate in gates:
gate = gate if isinstance(gate, Gate) else parse_to_gate(gate)
self._apply_gate(gate, **gate_opts)
if progbar and (gate.total_qubit_count >= 2):
# these don't change for single qubit gates
gates.set_description(
f"max_bond={self._psi.max_bond()}, "
f"error~={self.error_estimate():.3g}"
)
@property
def psi(self):
# no squeeze so that bond dims of 1 preserved
psi = self._psi.copy()
if not self.convert_eager:
self._maybe_convert(psi)
return psi
@property
def uni(self):
raise ValueError(
"You can't extract the circuit unitary TN from a ``CircuitMPS``."
)
def calc_qubit_ordering(self, qubits=None):
"""MPS already has a natural ordering."""
if qubits is None:
return tuple(range(self.N))
else:
return tuple(sorted(qubits))
def get_psi_reverse_lightcone(self, where, keep_psi0=False):
"""Override ``get_psi_reverse_lightcone`` as for an MPS the lightcone
is not meaningful.
"""
return self.psi
def sample(
self,
C,
seed=None,
dtype=None,
*,
qubits=None,
order=None,
group_size=None,
max_marginal_storage=None,
optimize=None,
backend=None,
simplify_sequence=None,
simplify_atol=None,
simplify_equalize_norms=None,
):
"""Sample the MPS circuit ``C`` times.
Parameters
----------
C : int
The number of samples to generate.
seed : None, int, or generator, optional
A random seed or generator to use for reproducibility.
"""
unsupported = (
qubits,
order,
group_size,
max_marginal_storage,
optimize,
backend,
simplify_sequence,
simplify_atol,
simplify_equalize_norms,
)
if any(x is not None for x in unsupported):
warnings.warn(
"Unsupported options for sampling an MPS circuit supplied, "
"ignoring: " + ", ".join(map(str, unsupported))
)
if dtype is not None or not self.convert_eager:
psi = self._psi.copy()
self._maybe_convert(psi, dtype)
else:
psi = self._psi
for config, _ in psi.sample(C, seed=seed):
yield "".join(map(str, config))
def fidelity_estimate(self):
r"""Estimate the fidelity of the current state based on its norm, which
tracks how much the state has been truncated:
.. math::
\tilde{F} =
\left| \langle \psi | \psi \rangle \right|^2
\approx
\left|\langle \psi_\mathrm{ideal} | \psi \rangle\right|^2
See Also
--------
error_estimate
"""
cur_orthog = self.gate_opts["info"].get("cur_orthog", None)
if cur_orthog is None:
return abs(self._psi.norm()) ** 2
cmin, cmax = cur_orthog
return abs(self._psi[cmin : cmax + 1].norm(tags=all)) ** 2
def error_estimate(self):
r"""Estimate the error in the current state based on the norm of the
discarded part of the state:
.. math::
\epsilon = 1 - \tilde{F}
See Also
--------
fidelity_estimate
"""
return 1 - self.fidelity_estimate()
def local_expectation(
self,
G,
where,
normalized=False,
dtype=None,
*,
simplify_sequence=None,
simplify_atol=None,
simplify_equalize_norms=None,
backend=None,
rehearse=None,
**contract_opts,
):
"""Compute the local expectation value of a local operator at ``where``
(via forming the reduced density matrix). Note this moves the
orthogonality around inplace, and records it in `info`.
Parameters
----------
G : Tensor
The local operator tensor.
where : int
The qubit to compute the expectation value at.
normalized : bool, optional
Whether to normalize the expectation value by the norm of the
state.
dtype : dtype, optional
If given, ensure the TN is cast to this dtype before contracting.
Returns
-------
float
"""
unsupported = (
simplify_sequence,
simplify_atol,
simplify_equalize_norms,
backend,
rehearse,
)
if any(x is not None for x in unsupported):
warnings.warn(
"Unsupported options for computing local_expectation with an "
"MPS circuit supplied, ignoring: "
+ ", ".join(map(str, unsupported))
)
if dtype is not None or not self.convert_eager:
psi = self._psi.copy()
self._maybe_convert(psi, dtype)
else:
psi = self._psi
return psi.local_expectation_canonical(
G,
where,
normalized=normalized,
info=self.gate_opts["info"],
**contract_opts,
)
class CircuitPermMPS(CircuitMPS):
"""Quantum circuit simulation keeping the state always in an MPS form, but
lazily tracking the qubit ordering rather than 'swapping back' qubits after
applying non-local gates. This can be useful for circuits with no
expectation of locality. The qubit ordering is always tracked in the
attribute ``qubits``. The ``psi`` attribute returns the TN with the sites
reindexed and retagged according to the current qubit ordering, meaning it
is no longer an MPS. Use `circ.get_psi_unordered()` to get the unpermuted
MPS and use `circ.qubits` to get the current qubit ordering if you prefer.
"""
def __init__(
self,
N=None,
psi0=None,
gate_opts=None,
gate_contract="swap+split",
**circuit_opts,
):
gate_opts = ensure_dict(gate_opts)
gate_opts.setdefault("contract", gate_contract)
# this is used to pass around the canonical form
gate_opts.setdefault("info", {})
super().__init__(N, psi0=psi0, gate_opts=gate_opts, **circuit_opts)
# keep track of the current qubit ordering
self.qubits = list(range(self.N))
def _apply_gate(self, gate, tags=None, **gate_opts):
# first translate gate qubits to their current 'physical' location
qubits = gate.qubits
phys_sites = [self.qubits.index(q) for q in qubits]
gate = gate.copy_with(qubits=phys_sites)
# if the gate is non-local, account for swap (without swap back)
if len(phys_sites) == 2:
i, j = sorted(phys_sites)
q = self.qubits.pop(j)
self.qubits.insert(i + 1, q)
gate_opts["swap_back"] = False
super()._apply_gate(gate, tags=tags, **gate_opts)
def calc_qubit_ordering(self, qubits=None):
"""Given by the current qubit permutation."""
if qubits is None:
return tuple(self.qubits)
else:
return tuple(sorted(qubits, key=self.qubits.index))
def get_psi_unordered(self):
"""Return the MPS representing the state but without reordering the
sites.
"""
return self._psi.copy()
def sample(
self,
C,
seed=None,
dtype=None,
):
"""Sample the PermMPS circuit ``C`` times.
Parameters
----------
C : int
The number of samples to generate.
seed : None, int, or generator, optional
A random seed or generator to use for reproducibility.
Yields
------
str
The next sample bitstring.
"""
if dtype is not None or not self.convert_eager:
psi = self._psi.copy()
self._maybe_convert(psi, dtype)
else:
psi = self._psi
# configuring is in physical order, so need to reorder for sampling
ordering = self.calc_qubit_ordering()
for config, _ in psi.sample(C, seed=seed):
yield "".join(str(config[i]) for i in ordering)
@property
def psi(self):
# need to reindex and retag the MPS
psi = self._psi.copy()
psi.view_as_(TensorNetworkGenVector)
psi.reindex_(
{
psi.site_ind(i): psi.site_ind(q)
for i, q in enumerate(self.qubits)
}
)
psi.retag_(
{
psi.site_tag(i): psi.site_tag(q)
for i, q in enumerate(self.qubits)
}
)
if not self.convert_eager:
self._maybe_convert(psi)
return psi
class CircuitDense(Circuit):
"""Quantum circuit simulation keeping the state in full dense form."""
def __init__(
self,
N=None,
psi0=None,
gate_opts=None,
gate_contract=True,
tags=None,
convert_eager=True,
**circuit_opts,
):
gate_opts = ensure_dict(gate_opts)
gate_opts.setdefault("contract", gate_contract)
gate_opts.setdefault("convert_eager", convert_eager)
super().__init__(N, psi0, gate_opts, tags, **circuit_opts)
@property
def psi(self):
t = self._psi ^ ...
psi = t.as_network()
psi.view_as_(Dense1D, like=self._psi)
return psi
@property
def uni(self):
raise ValueError(
"You can't extract the circuit unitary TN from a ``CircuitDense``."
)
def calc_qubit_ordering(self, qubits=None):
"""Qubit ordering doesn't matter for a dense wavefunction."""
if qubits is None:
return tuple(range(self.N))
else:
return tuple(sorted(qubits))
def get_psi_reverse_lightcone(self, where, keep_psi0=False):
"""Override ``get_psi_reverse_lightcone`` as for a dense wavefunction
the lightcone is not meaningful.
"""
return self.psi