Skip to content

Commit

Permalink
circuit openqasm2: support custom gate defns
Browse files Browse the repository at this point in the history
  • Loading branch information
jcmgray committed Dec 6, 2023
1 parent a8d4dca commit a8577a1
Show file tree
Hide file tree
Showing 3 changed files with 178 additions and 25 deletions.
7 changes: 7 additions & 0 deletions quimb/experimental/schematic.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
import warnings

from quimb.schematic import *

warnings.warn(
"The 'quimb.experimental.schematic' has been moved to `quimb.schematic`.",
)
128 changes: 103 additions & 25 deletions quimb/tensor/circuit.py
Original file line number Diff line number Diff line change
Expand Up @@ -128,36 +128,56 @@ def parse_qsim_url(url, **kwargs):
return parse_qsim_str(request.urlopen(url).read().decode(), **kwargs)


def parse_openqasm2_str(contents):
"""Parse the string contents of an OpenQASM 2.0 file. This parser only
supports basic gate definitions, and is not guaranteed to check the full
openqasm grammar.
"""
# define regular expressions for parsing
rgxs = {
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*(\w+\[\d+\]\s*(,\s*\w+\[\d+\])*);"
),
"error": re.compile(r"^(gate|if)"),
"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"),
"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
for line in contents.split("\n"):
line = line.strip()

lines = contents.split("\n")
while lines:
line = lines.pop(0).strip()
if not line:
# blank line
continue
Expand Down Expand Up @@ -200,19 +220,81 @@ def parse_openqasm2_str(contents):
f"Custom gate definitions are 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("(.*)\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(2),
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.strip("()").split(",")
eval(param, {"pi": math.pi}) for param in params.split(",")
)
else:
params = ()
Expand Down Expand Up @@ -878,7 +960,6 @@ def apply_swap(psi, i, j, **gate_opts):
register_special_gate("IDEN", lambda *_, **__: None, 1, array=qu.identity(2))



def build_controlled_gate_htn(
ncontrol,
gate,
Expand Down Expand Up @@ -1446,9 +1527,7 @@ def _apply_gate(self, gate, tags=None, **gate_opts):
opts = {**self.gate_opts, **gate_opts}

if gate.controls:
apply_controlled_gate(
self._psi, gate, tags=tags, **opts
)
apply_controlled_gate(self._psi, gate, tags=tags, **opts)
elif gate.special:
# these are specified as a general function
SPECIAL_GATES[gate.label](
Expand Down Expand Up @@ -2645,7 +2724,8 @@ def compute_marginal(
# 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,
output_inds=output_inds,
optimize=optimize,
)

if rehearse:
Expand Down Expand Up @@ -3272,9 +3352,7 @@ def to_dense(
if reverse:
output_inds = output_inds[::-1]

tree = psi.contraction_tree(
output_inds=output_inds, optimize=optimize
)
tree = psi.contraction_tree(output_inds=output_inds, optimize=optimize)

if rehearse:
return rehearsal_dict(psi, tree)
Expand Down
68 changes: 68 additions & 0 deletions tests/test_tensor/test_circuit.py
Original file line number Diff line number Diff line change
Expand Up @@ -162,6 +162,74 @@ def test_from_openqasm2(self):
qc = qtn.Circuit.from_openqasm2_str(example_openqasm2_qft())
assert (qc.psi.H & qc.psi) ^ all == pytest.approx(1.0)

def test_openqasm2_custom_gates(self):
circ = qtn.Circuit.from_openqasm2_str(
"""
OPENQASM 2.0;
include "qelib1.inc";
qreg q[3];
gate hello a, b {
h a;
cx a, b;
u3(0.1, 0.2, 0.3) b;
}
gate world(param1, θ) q
{
u2(θ / 2, param1) q;
u2(param1, θ / 2) q;
}
hello q[0], q[1];
world(0.1, 0.2) q[2];
hello q[2], q[1];
"""
)
assert [g.label for g in circ.gates] == [
"H",
"CX",
"U3",
"U2",
"U2",
"H",
"CX",
"U3",
]

def test_openqasm2_custom_nested_gates(self):
circ = qtn.Circuit.from_openqasm2_str(
"""
OPENQASM 2.0;
include "qelib1.inc";
qreg q[3];
gate cphase(θ) a, b
{
U3(0, 0, θ / 2) a;
CX a, b;
U3(0, 0, -θ / 2) b;
CX a, b;
U3(0, 0, θ / 2) b;
}
gate doublecphase(θ) a, b, c {
cphase(θ) a, b;
cphase(θ) b, c;
}
doublecphase(0.1) q[0], q[1], q[2];
doublecphase(0.2) q[2], q[0], q[1];
"""
)
assert [g.label for g in circ.gates] == [
"U3",
"CX",
"U3",
"CX",
"U3",
] * 4

@pytest.mark.parametrize(
"Circ", [qtn.Circuit, qtn.CircuitMPS, qtn.CircuitDense]
)
Expand Down

0 comments on commit a8577a1

Please sign in to comment.