Skip to content

Commit

Permalink
add OpenQASM 2.0 parser, rename qsam -> qsim
Browse files Browse the repository at this point in the history
  • Loading branch information
jcmgray committed Sep 10, 2023
1 parent 6f24494 commit 7ed075f
Show file tree
Hide file tree
Showing 2 changed files with 231 additions and 37 deletions.
220 changes: 190 additions & 30 deletions quimb/tensor/circuit.py
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,14 @@

import quimb as qu
from ..utils import progbar as _progbar
from ..utils import partitionby, concatv, partition_all, ensure_dict, LRU
from ..utils import (
concatv,
deprecated,
ensure_dict,
LRU,
partition_all,
partitionby,
)
from .tensor_core import (
get_tags,
oset_union,
Expand Down Expand Up @@ -66,13 +73,15 @@ def _put_registers_last(x):
return tuple(concatv(*parts[:-2], parts[-1], parts[-2]))


def parse_qasm(contents):
"""Parse qasm from a string.
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 qasm file.
The full string of the qsim file.
Returns
-------
Expand All @@ -82,7 +91,7 @@ def parse_qasm(contents):
- 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 qasm file.
is a list of strings read from a line of the qsim file.
"""

lines = contents.split("\n")
Expand All @@ -107,16 +116,140 @@ def parse_qasm(contents):
}


def parse_qasm_file(fname, **kwargs):
"""Parse a qasm file."""
return parse_qasm(open(fname).read(), **kwargs)
def parse_qsim_file(fname, **kwargs):
"""Parse a qsim file."""
with open(fname) as f:
return parse_qsim_str(f.read(), **kwargs)


def parse_qasm_url(url, **kwargs):
"""Parse a qasm url."""
def parse_qsim_url(url, **kwargs):
"""Parse a qsim url."""
from urllib import request

return parse_qasm(request.urlopen(url).read().decode(), **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 = {
"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)"),
"ignore": re.compile(r"^(creg|measure|barrier)"),
}

# initialise number of qubits to zero and an empty list for gates
sitemap = {}
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()

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"Custom gate definitions are not supported: {line}"
)

match = rgxs["gate"].search(line)
if match:
# apply a gate
label, params, qubits = (
match.group(1),
match.group(2),
match.group(3),
)

if label.upper() not in ALL_GATES:
# not implemented by quimb yet
raise NotImplementedError(f"unsupported gate: {line}")

if params:
params = tuple(
eval(param, {"pi": math.pi})
for param in params.strip("()").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 ---------------------------- #
Expand Down Expand Up @@ -1047,26 +1180,60 @@ def __init__(
self._sampled_conditionals = dict()

@classmethod
def from_qasm(cls, qasm, **quantum_circuit_opts):
"""Generate a ``Circuit`` instance from a qasm string."""
info = parse_qasm(qasm)
qc = cls(info["n"], **quantum_circuit_opts)
def from_qsim_str(cls, contents, **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"])
return qc

@classmethod
def from_qsim_file(cls, fname, **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"])
return qc

@classmethod
def from_qsim_url(cls, url, **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"])
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, **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"])
return qc

@classmethod
def from_qasm_file(cls, fname, **quantum_circuit_opts):
"""Generate a ``Circuit`` instance from a qasm file."""
info = parse_qasm_file(fname)
qc = cls(info["n"], **quantum_circuit_opts)
def from_openqasm2_file(cls, fname, **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"])
return qc

@classmethod
def from_qasm_url(cls, url, **quantum_circuit_opts):
"""Generate a ``Circuit`` instance from a qasm url."""
info = parse_qasm_url(url)
qc = cls(info["n"], **quantum_circuit_opts)
def from_openqasm2_url(cls, url, **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"])
return qc

Expand Down Expand Up @@ -1185,13 +1352,6 @@ def apply_gates(self, gates, **gate_opts):

self._psi.squeeze_()

def apply_circuit(self, gates): # pragma: no cover
import warnings

msg = "``apply_circuit`` is deprecated in favour of ``apply_gates``."
warnings.warn(msg, DeprecationWarning)
self.apply_gates(gates)

def h(self, i, gate_round=None, **kwargs):
self.apply_gate("H", i, gate_round=gate_round, **kwargs)

Expand Down
48 changes: 41 additions & 7 deletions tests/test_tensor/test_circuit.py
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,7 @@ def rand_reg_graph(reg, n, seed=None):
return G


def graph_to_qasm(G, gamma0=-0.743043, beta0=0.754082):
def graph_to_qsim(G, gamma0=-0.743043, beta0=0.754082):
n = G.number_of_nodes()

# add all the gates
Expand Down Expand Up @@ -95,6 +95,36 @@ def swappy_circ(n, depth):
return circ


def example_openqasm2_qft():
return """
// quantum Fourier transform
OPENQASM 2.0;
include "qelib1.inc";
qreg q[4];
creg c[4];
x q[0];
x q[2];
barrier q;
h q[0];
cu1(pi/2) q[1],q[0];
h q[1];
cu1(pi/4) q[2],q[0];
cu1(pi/2) q[2],q[1];
/*
This is a multi line comment.
*/
h q[2];
cu1(pi/8) q[3],q[0];
cu1(pi/4) q[3],q[1];
cu1(pi/2) q[3],q[2];
h q[3];
measure q -> c;
"""


class TestCircuit:

def test_prepare_GHZ(self):
Expand All @@ -116,19 +146,23 @@ def test_prepare_GHZ(self):
assert '111' in counts
assert counts['000'] + counts['111'] == 1024

def test_from_qasm(self):
def test_from_qsim(self):
G = rand_reg_graph(reg=3, n=18, seed=42)
qasm = graph_to_qasm(G)
qc = qtn.Circuit.from_qasm(qasm)
qsim = graph_to_qsim(G)
qc = qtn.Circuit.from_qsim_str(qsim)
assert (qc.psi.H & qc.psi) ^ all == pytest.approx(1.0)

def test_from_qasm_mps_swapsplit(self):
def test_from_qsim_mps_swapsplit(self):
G = rand_reg_graph(reg=3, n=18, seed=42)
qasm = graph_to_qasm(G)
qc = qtn.CircuitMPS.from_qasm(qasm)
qsim = graph_to_qsim(G)
qc = qtn.CircuitMPS.from_qsim_str(qsim)
assert len(qc.psi.tensors) == 18
assert (qc.psi.H & qc.psi) ^ all == pytest.approx(1.0)

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)

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

0 comments on commit 7ed075f

Please sign in to comment.