Repository: rigetticomputing/pyquil
Pull request #517 (2018-07-30 23:32:17)

get_qc
Following up #504 


Patch:
diff --git a/DEVELOPMENT.md b/DEVELOPMENT.md
index 9a7dfa7..10aaa32 100644
--- a/DEVELOPMENT.md
+++ b/DEVELOPMENT.md
@@ -2,9 +2,10 @@
 
 We use pytest (version > 3.0) and mock for testing. Tests can be run from the top-level directory using:
 ```
-python setup.py test
+pytest
 ```
-If you want to test in multiple enviornments, such as Python 3.5 and Python 3.6, then you can use `tox`. This is done with:
+
+To run the complete suite of tests in their own environment, you can use `tox`. This is done with:
 ```
 pip install tox
 tox
@@ -15,7 +16,7 @@ tox
 We use sphinx to build the documentation. To do this, navigate into pyQuil's top-level directory and run:
 
 ```
-sphinx-build -b html docs/source docs/_build
+make html
 ```
 To view the docs navigate to the newly-created `docs/_build` directory and open
 the `index.html` file in a browser. Note that we use the Read the Docs theme for
diff --git a/MANIFEST b/MANIFEST
new file mode 100644
index 0000000..4bf4483
--- /dev/null
+++ b/MANIFEST
@@ -0,0 +1,1 @@
+include README.md
\ No newline at end of file
diff --git a/Makefile b/Makefile
deleted file mode 100644
index 39f2743..0000000
--- a/Makefile
+++ /dev/null
@@ -1,36 +0,0 @@
-# top-level pyquil Makefile
-
-PACKAGENAME = pyquil
-FILES = pyquil/*.py pyquil/api/*.py pyquil/latex/*.py pyquil/_parser/*.py
-# Kudos: Adapted from Auto-documenting default target 
-# https://marmelab.com/blog/2016/02/29/auto-documented-makefile.html
-help:
-	@grep -E '^[a-zA-Z_-]+:.*?## .*$$' $(MAKEFILE_LIST) | awk 'BEGIN {FS = ":.*?## "}; {printf "\033[36m%-12s\033[0m %s\n", $$1, $$2}'
-
-test:		## Run unittests with current enviroment
-	@pytest $(PACKAGENAME)/tests
-
-testall:	## Run full test suite
-	@tox
-
-coverage:	## Report test coverage
-	@pytest --cov=$(PACKAGENAME) --cov-report term-missing $(PACKAGENAME)/tests
-
-lint:		## Delint python source
-	@flake8 $(PACKAGENAME)
-
-typecheck:	## Static typechecking 
-	@mypy $(FILES)  --ignore-missing-imports --follow-imports=skip
-
-untyped:	## Report type errors and untyped functions
-	@mypy $(FILES) --ignore-missing-imports --follow-imports=skip --disallow-untyped-defs
-
-docs:		## Build documentation
-	$(MAKE) -C docs html
-
-antlr: 		## Rebuild antlr parser (Run after modification to antlr grammar ./pyquil/_parser/Quil.g4)
-	(cd pyquil/_parser && antlr4 -Dlanguage=Python3 -o gen3 Quil.g4)
-
-
-.PHONY: help
-.PHONY: docs
diff --git a/README.md b/README.md
index af0170b..6f775c7 100644
--- a/README.md
+++ b/README.md
@@ -12,7 +12,14 @@ Documentation is hosted at [http://pyquil.readthedocs.io/en/latest/](http://pyqu
 
 ## Installation
 
-You can install pyQuil directly from the Python package manager `pip` using:
+You can install pyQuil as a conda package (recommended):
+
+```bash
+conda install -c rigetti pyquil
+```
+
+or using `pip`:
+
 ```
 pip install pyquil
 ```
@@ -22,7 +29,7 @@ To instead install pyQuil from source, clone this repository, `cd` into it, and
 pip install -e .
 ```
 
-## Connecting to the Rigetti Forest
+## Connecting to Rigetti Forest
 
 pyQuil can be used to build and manipulate Quil programs without restriction. However, to run
 programs (e.g., to get wavefunctions, get multishot experiment data), you will need an API key
diff --git a/pyquil/__init__.py b/pyquil/__init__.py
index 9ff2970..cee9d1a 100644
--- a/pyquil/__init__.py
+++ b/pyquil/__init__.py
@@ -1,4 +1,4 @@
 __version__ = "2.0.0.dev0"
 
 from pyquil.quil import Program
-from pyquil.api import QVMConnection, QPUConnection
+from pyquil.api import QVMConnection, QPUConnection, list_quantum_computers, get_qc
diff --git a/pyquil/_parser/PyQuilListener.py b/pyquil/_parser/PyQuilListener.py
index 7c74a78..ac1b142 100644
--- a/pyquil/_parser/PyQuilListener.py
+++ b/pyquil/_parser/PyQuilListener.py
@@ -17,7 +17,6 @@
 import operator
 from typing import Any, List, Iterator, Callable
 from numbers import Number
-import sys
 
 import numpy as np
 from antlr4 import InputStream, CommonTokenStream, ParseTreeWalker
@@ -34,14 +33,9 @@ from pyquil.quilbase import Gate, DefGate, Measurement, Addr, JumpTarget, Label,
     Reset, Wait, ClassicalTrue, ClassicalFalse, ClassicalNot, ClassicalAnd, ClassicalOr, ClassicalMove, \
     ClassicalExchange, Nop, RawInstr, Qubit, Pragma, AbstractInstruction
 
-if sys.version_info.major == 2:
-    from .gen2.QuilLexer import QuilLexer
-    from .gen2.QuilListener import QuilListener
-    from .gen2 .QuilParser import QuilParser
-elif sys.version_info.major == 3:
-    from .gen3.QuilLexer import QuilLexer
-    from .gen3.QuilListener import QuilListener
-    from .gen3.QuilParser import QuilParser
+from .gen3.QuilLexer import QuilLexer
+from .gen3.QuilListener import QuilListener
+from .gen3.QuilParser import QuilParser
 
 
 def run_parser(quil):
@@ -102,22 +96,42 @@ class PyQuilListener(QuilListener):
     Functions are invoked when the parser reaches the various different constructs in Quil.
     """
     def __init__(self):
-        # type: () -> None
-        self.result = []    # type: List[AbstractInstruction]
+        self.result = []  # type: List[AbstractInstruction]
+        self.previous_result = None  # type: List[AbstractInstruction]
 
-    def exitDefGate(self, ctx):
-        # type: (QuilParser.DefGateContext) -> None
+    def exitDefGate(self, ctx: QuilParser.DefGateContext):
         gate_name = ctx.name().getText()
         matrix = _matrix(ctx.matrix())
         parameters = list(map(_variable, ctx.variable()))
         self.result.append(DefGate(gate_name, matrix, parameters))
 
-    def exitDefCircuit(self, ctx):
-        # type: (QuilParser.DefCircuitContext) -> None
-        self.result.append(RawInstr(ctx.getText()))
+    # DEFCIRCUIT parsing:
+    # When we enter a circuit definition we create a backup of the instructions seen up to that point. Then, when the
+    # listener continues walking through the circuit instructions it will add to an empty list. Once we leave the
+    # circuit we then take all those instructions, shove them into a RawInstr (since PyQuil has no support for circuit
+    # definitions yet), recover the backup, and then continue on our way.
 
-    def exitGate(self, ctx):
-        # type: (QuilParser.GateContext) -> None
+    def enterDefCircuit(self, ctx: QuilParser.DefCircuitContext) -> None:
+        self.previous_result = self.result
+        self.result = []
+
+    def exitDefCircuit(self, ctx: QuilParser.DefCircuitContext):
+        circuit_name = ctx.name().getText()
+        variables = [variable.getText() for variable in ctx.variable()]
+        qubitVariables = [qubitVariable.getText() for qubitVariable in ctx.qubitVariable()]
+
+        if variables:
+            raw_defcircuit = 'DEFCIRCUIT {}({}) {}:'.format(circuit_name, ', '.join(variables), ' '.join(qubitVariables))
+        else:
+            raw_defcircuit = 'DEFCIRCUIT {} {}:'.format(circuit_name, ' '.join(qubitVariables))
+
+        raw_defcircuit += '\n    '.join([''] + [instr.out() for instr in self.result])
+        self.previous_result.append(RawInstr(raw_defcircuit))
+
+        self.result = self.previous_result
+        self.previous_result = None
+
+    def exitGate(self, ctx: QuilParser.GateContext):
         gate_name = ctx.name().getText()
         params = list(map(_param, ctx.param()))
         qubits = list(map(_qubit, ctx.qubit()))
@@ -130,8 +144,20 @@ class PyQuilListener(QuilListener):
         else:
             self.result.append(Gate(gate_name, params, qubits))
 
-    def exitMeasure(self, ctx):
-        # type: (QuilParser.MeasureContext) -> None
+    def exitCircuitGate(self, ctx: QuilParser.CircuitGateContext):
+        """
+        PyQuil has no constructs yet for representing gate instructions within a DEFCIRCUIT (ie. gates where the qubits
+        are inputs to the call to the circuit). Therefore we parse them as a raw instructions.
+        """
+        gate_name = ctx.name().getText()
+        params = [param.getText() for param in ctx.param()]
+        qubits = [qubit.getText() for qubit in ctx.circuitQubit()]
+        if params:
+            self.result.append(RawInstr('{}({}) {}'.format(gate_name, ', '.join(params), ' '.join(qubits))))
+        else:
+            self.result.append(RawInstr('{} {}'.format(gate_name, ' '.join(qubits))))
+
+    def exitMeasure(self, ctx: QuilParser.MeasureContext):
         qubit = _qubit(ctx.qubit())
         classical = None
         if ctx.addr():
diff --git a/pyquil/api/__init__.py b/pyquil/api/__init__.py
index 666267c..8b5cd09 100644
--- a/pyquil/api/__init__.py
+++ b/pyquil/api/__init__.py
@@ -29,7 +29,7 @@ from pyquil.api.qpu import QPUConnection, get_devices, QPU
 from pyquil.device import Device
 from pyquil.api.wavefunction_simulator import WavefunctionSimulator
 from pyquil.api._base_connection import ForestConnection
-from pyquil.api.quantum_computer import QuantumComputer
+from pyquil.api.quantum_computer import QuantumComputer, list_quantum_computers, get_qc
 from pyquil.api._qam import QAM
 
 
diff --git a/pyquil/api/quantum_computer.py b/pyquil/api/quantum_computer.py
index bca2edd..81900d9 100644
--- a/pyquil/api/quantum_computer.py
+++ b/pyquil/api/quantum_computer.py
@@ -1,11 +1,15 @@
+import warnings
 from math import pi
 from typing import List
 
+import networkx as nx
 import numpy as np
 
+from pyquil.api import get_devices, QPU, ForestConnection, QVM
 from pyquil.api._qam import QAM
-from pyquil.device import AbstractDevice
+from pyquil.device import AbstractDevice, NxDevice
 from pyquil.gates import MEASURE, RX
+from pyquil.noise import decoherance_noise_with_asymmetric_ro
 from pyquil.quil import Program, get_classical_addresses_from_program
 from pyquil.quilbase import Measurement, Pragma
 
@@ -185,3 +189,168 @@ class QuantumComputer:
 
     def __str__(self):
         return self.name
+
+
+def list_quantum_computers(connection: ForestConnection = None, qpus=True, qvms=True) -> List[str]:
+    """
+    List the names of available quantum computers
+
+    :param connection: An optional :py:class:ForestConnection` object. If not specified,
+        the default values for URL endpoints will be used, and your API key
+        will be read from ~/.pyquil_config. If you deign to change any
+        of these parameters, pass your own :py:class:`ForestConnection` object.
+    :param qpus: Whether to include QPU's in the list.
+    :param qvms: Whether to include QVM's in the list.
+    """
+    if connection is None:
+        # TODO: Use this to list devices?
+        connection = ForestConnection()
+
+    qc_names = []
+    if qpus:
+        for qpu_name in ['8Q-Agave', '19Q-Acorn']:
+            qc_names += [qpu_name]
+            if qvms:
+                qc_names += ["{}-qvm".format(qpu_name), "{}-noisy-qvm".format(qpu_name)]
+
+    if qvms:
+        qc_names += ['9q-generic-qvm', '9q-generic-noisy-qvm']
+
+    return qc_names
+
+
+def _parse_name(name, as_qvm, noisy):
+    """
+    Try to figure out whether we're getting a (noisy) qvm, and the associated qpu name.
+
+    See :py:func:`get_qc` for examples of valid names + flags.
+    """
+    if name.endswith('-noisy-qvm'):
+        if as_qvm is not None and (not as_qvm):
+            raise ValueError("The provided qc name indicates you are getting a noisy QVM, "
+                             "but you have specified `as_qvm=False`")
+
+        if noisy is not None and (not noisy):
+            raise ValueError("The provided qc name indicates you are getting a noisy QVM, "
+                             "but you have specified `noisy=False`")
+
+        as_qvm = True
+        noisy = True
+        prefix = name[:-len('-noisy-qvm')]
+        return prefix, as_qvm, noisy
+
+    if name.endswith('-qvm'):
+        if as_qvm is not None and (not as_qvm):
+            raise ValueError("The provided qc name indicates you are getting a QVM, "
+                             "but you have specified `as_qvm=False`")
+        as_qvm = True
+        if noisy is not None:
+            noisy = False
+        prefix = name[:-len('-qvm')]
+        return prefix, as_qvm, noisy
+
+    if as_qvm is None:
+        as_qvm = False
+
+    if noisy is None:
+        noisy = False
+
+    return name, as_qvm, noisy
+
+
+def get_qc(name: str, *, as_qvm: bool = None, noisy: bool = None,
+           connection: ForestConnection = None):
+    """
+    Get a quantum computer.
+
+    A quantum computer is an object of type :py:class:`QuantumComputer` and can be backed
+    either by a QVM simulator ("Quantum/Quil Virtual Machine") or a physical Rigetti QPU ("Quantum
+    Processing Unit") made of superconducting qubits.
+
+    You can choose the quantum computer to target through a combination of its name and optional
+    flags. There are multiple ways to get the same quantum computer. The following are equivalent::
+
+        >>> qc = get_qc("8Q-Agave-noisy-qvm")
+        >>> qc = get_qc("8Q-Agave", as_qvm=True, noisy=True)
+
+    and will construct a simulator of the 8q-agave chip with a noise model based on device
+    characteristics. We also provide a means for constructing generic quantum simulators that
+    are not related to a given piece of Rigetti hardware::
+
+        >>> qc = get_qc("9q-generic-qvm")
+        >>> qc = get_qc("9q-generic", as_qvm=True)
+
+    Redundant flags are acceptable, but conflicting flags will raise an exception::
+
+        >>> qc = get_qc("9q-generic-qvm") # qc is fully specified by its name
+        >>> qc = get_qc("9q-generic-qvm", as_qvm=True) # redundant, but ok
+        >>> qc = get_qc("9q-generic-qvm", as_qvm=False) # Error!
+
+    Use :py:func:`list_quantum_computers` to retrieve a list of known qc names.
+
+    This method is provided as a convenience to quickly construct and use QVM's and QPU's.
+    Power users may wish to have more control over the specification of a quantum computer
+    (e.g. custom noise models, bespoke topologies, etc.). This is possible by constructing
+    a :py:class:`QuantumComputer` object by hand. Please refer to the documentation on
+    :py:class:`QuantumComputer` for more information.
+
+    :param name: The name of the desired quantum computer. This should correspond to a name
+        returned by :py:func:`list_quantum_computers`. Names ending in "-qvm" will return
+        a QVM. Names ending in "-noisy-qvm" will return a QVM with a noise model. Otherwise,
+        we will return a QPU with the given name.
+    :param as_qvm: An optional flag to force construction of a QVM (instead of a QPU). If
+        specified and set to ``True``, a QVM-backed quantum computer will be returned regardless
+        of the name's suffix
+    :param noisy: An optional flag to force inclusion of a noise model. If
+        specified and set to ``True``, a quantum computer with a noise model will be returned
+        regardless of the name's suffix. The noise model for QVM's based on a real QPU
+        is an empirically parameterized model based on real device noise characteristics.
+        The generic QVM noise model is simple T1 and T2 noise plus readout error. See
+        :py:func:`decoherance_noise_with_asymmetric_ro`.
+    :param connection: An optional :py:class:ForestConnection` object. If not specified,
+        the default values for URL endpoints, ping time, and status time will be used. Your
+        user id and API key will be read from ~/.pyquil_config. If you deign to change any
+        of these parameters, pass your own :py:class:`ForestConnection` object.
+    :return:
+    """
+    if connection is None:
+        connection = ForestConnection()
+
+    name, as_qvm, noisy = _parse_name(name, as_qvm, noisy)
+
+    if name == '9q-generic':
+        if not as_qvm:
+            raise ValueError("The device '9q-generic' is only available as a QVM")
+
+        nineq_square = nx.convert_node_labels_to_integers(nx.grid_2d_graph(3, 3))
+        nineq_device = NxDevice(topology=nineq_square)
+        if noisy:
+            noise_model = decoherance_noise_with_asymmetric_ro(nineq_device.get_isa())
+        else:
+            noise_model = None
+
+        return QuantumComputer(name='9q-generic-qvm',
+                               qam=QVM(connection=connection, noise_model=noise_model),
+                               device=nineq_device)
+
+    # At least based off a real device.
+    device = get_devices(as_dict=True)[name]
+
+    if not as_qvm:
+        if noisy is not None and noisy:
+            warnings.warn("You have specified `noisy=True`, but you're getting a QPU. This flag "
+                          "is meant for controling noise models on QVMs.")
+        return QuantumComputer(name=name,
+                               qam=QPU(device_name=name, connection=connection),
+                               device=device)
+
+    if noisy:
+        noise_model = device.noise_model
+        name = "{name}-noisy-qvm".format(name=name)
+    else:
+        noise_model = None
+        name = "{name}-qvm".format(name=name)
+
+    return QuantumComputer(name=name,
+                           qam=QVM(connection=connection, noise_model=noise_model),
+                           device=device)
diff --git a/pyquil/noise.py b/pyquil/noise.py
index 24f2485..53a3685 100644
--- a/pyquil/noise.py
+++ b/pyquil/noise.py
@@ -22,6 +22,7 @@ from collections import namedtuple
 import numpy as np
 import sys
 
+from pyquil.device import ISA, gates_in_isa
 from pyquil.gates import I, MEASURE, X
 from pyquil.parameters import format_parameter
 from pyquil.quilbase import Pragma, Gate
@@ -441,6 +442,20 @@ def _decoherence_noise_model(gates, T1=30e-6, T2=30e-6, gate_time_1q=50e-9,
     return NoiseModel(kraus_maps, aprobs)
 
 
+def decoherance_noise_with_asymmetric_ro(isa: ISA, p00=0.975, p11=0.911):
+    """Similar to :py:func`_decoherance_noise_model`, but with asymmetric readout.
+
+    For simplicity, we use the default values for T1, T2, gate times, et al. and only allow
+    the specification of readout fidelities.
+    """
+    gates = gates_in_isa(isa)
+    noise_model = _decoherence_noise_model(gates)
+    aprobs = np.array([[p00, 1 - p00],
+                       [1 - p11, p11]])
+    aprobs = {q: aprobs for q in noise_model.assignment_probs.keys()}
+    return NoiseModel(noise_model.gates, aprobs)
+
+
 def _noise_model_program_header(noise_model):
     """
     Generate the header for a pyquil Program that uses ``noise_model`` to overload noisy gates.
diff --git a/pyquil/quilbase.py b/pyquil/quilbase.py
index caddadc..c4eed71 100644
--- a/pyquil/quilbase.py
+++ b/pyquil/quilbase.py
@@ -466,4 +466,4 @@ class RawInstr(AbstractInstruction):
         return self.instr
 
     def __repr__(self):
-        return '<RawInstr>'
+        return '<RawInstr {}>'.format(self.instr)
diff --git a/requirements.txt b/requirements.txt
index d9da9cf..4ba45b1 100644
--- a/requirements.txt
+++ b/requirements.txt
@@ -10,6 +10,7 @@ networkx
 pytest
 requests-mock
 flake8
+tox
 
 # docs
 sphinx
diff --git a/setup.cfg b/setup.cfg
deleted file mode 100644
index 74a03d9..0000000
--- a/setup.cfg
+++ /dev/null
@@ -1,5 +0,0 @@
-[metadata]
-description-file = README.md
-
-[aliases]
-test = pytest


Test patch:
diff --git a/pyquil/tests/test_parser.py b/pyquil/tests/test_parser.py
index 828d429..75a3ded 100644
--- a/pyquil/tests/test_parser.py
+++ b/pyquil/tests/test_parser.py
@@ -20,7 +20,7 @@ from pyquil.gates import *
 from pyquil.parameters import Parameter, quil_sin, quil_cos, Segment, Add
 from pyquil.parser import parse
 from pyquil.quilatom import Addr
-from pyquil.quilbase import Label, JumpTarget, Jump, JumpWhen, JumpUnless, DefGate, Qubit, Pragma
+from pyquil.quilbase import Label, JumpTarget, Jump, JumpWhen, JumpUnless, DefGate, Qubit, Pragma, RawInstr
 from pyquil.tests.utils import parse_equals
 
 
@@ -182,3 +182,12 @@ def test_invalid():
 
 def test_empty_program():
     parse_equals("")
+
+
+def test_def_circuit():
+    defcircuit = """
+DEFCIRCUIT bell a b:
+    H a
+    CNOT a b
+""".strip()
+    parse_equals(defcircuit, RawInstr(defcircuit))
diff --git a/pyquil/tests/test_quantum_computer.py b/pyquil/tests/test_quantum_computer.py
index 138284e..507887f 100644
--- a/pyquil/tests/test_quantum_computer.py
+++ b/pyquil/tests/test_quantum_computer.py
@@ -1,13 +1,13 @@
 import networkx as nx
 import numpy as np
+import pytest
 
-from pyquil import Program
+from pyquil import Program, get_qc, list_quantum_computers
 from pyquil.api import QVM
-from pyquil.device import NxDevice, ISA, gates_in_isa
+from pyquil.api.quantum_computer import _get_flipped_protoquil_program, QuantumComputer, _parse_name
+from pyquil.device import NxDevice
 from pyquil.gates import *
-
-from pyquil.api.quantum_computer import _get_flipped_protoquil_program, QuantumComputer
-from pyquil.noise import _decoherence_noise_model, NoiseModel
+from pyquil.noise import decoherance_noise_with_asymmetric_ro
 
 
 def test_get_flipped_program():
@@ -81,25 +81,9 @@ def test_run(forest):
     assert 0 < np.mean(parity) < 0.15
 
 
-def decoherance_noise_with_asymettric_ro(isa: ISA):
-    """Reimplementation of `add_decoherance_noise` with asymmetric readout.
-
-    For simplicity, we use the default values for T1, T2, gate times, et al. and hard-code
-    readout fidelities here.
-    """
-    gates = gates_in_isa(isa)
-    noise_model = _decoherence_noise_model(gates)
-    p00 = 0.975
-    p11 = 0.911
-    aprobs = np.array([[p00, 1 - p00],
-                       [1 - p11, p11]])
-    aprobs = {q: aprobs for q in noise_model.assignment_probs.keys()}
-    return NoiseModel(noise_model.gates, aprobs)
-
-
 def test_readout_symmetrization(forest):
     device = NxDevice(nx.complete_graph(3))
-    noise_model = decoherance_noise_with_asymettric_ro(device.get_isa())
+    noise_model = decoherance_noise_with_asymmetric_ro(device.get_isa())
     qc = QuantumComputer(
         name='testy!',
         qam=QVM(connection=forest, noise_model=noise_model),
@@ -114,12 +98,102 @@ def test_readout_symmetrization(forest):
     avg0_us = np.mean(bs1[:, 0])
     avg1_us = 1 - np.mean(bs1[:, 1])
     diff_us = avg1_us - avg0_us
-    print(avg0_us, avg1_us, diff_us)
     assert diff_us > 0.03
 
     bs2 = qc.run_symmetrized_readout(prog, [0, 1], 1000)
     avg0_s = np.mean(bs2[:, 0])
     avg1_s = 1 - np.mean(bs2[:, 1])
     diff_s = avg1_s - avg0_s
-    print(avg0_s, avg1_s, diff_s)
     assert diff_s < 0.05
+
+
+def test_list_qc():
+    qc_names = list_quantum_computers()
+    assert qc_names == ['8Q-Agave', '8Q-Agave-qvm', '8Q-Agave-noisy-qvm',
+                        '19Q-Acorn', '19Q-Acorn-qvm', '19Q-Acorn-noisy-qvm',
+                        '9q-generic-qvm', '9q-generic-noisy-qvm']
+
+
+def test_parse_qc_name():
+    name, as_qvm, noisy = _parse_name('9q-generic', None, None)
+    assert name == '9q-generic'
+    assert not as_qvm
+    assert not noisy
+
+    name, as_qvm, noisy = _parse_name('9q-generic-qvm', None, None)
+    assert name == '9q-generic'
+    assert as_qvm
+    assert not noisy
+
+    name, as_qvm, noisy = _parse_name('9q-generic-noisy-qvm', None, None)
+    assert name == '9q-generic'
+    assert as_qvm
+    assert noisy
+
+
+def test_parse_qc_flags():
+    name, as_qvm, noisy = _parse_name('9q-generic', False, False)
+    assert name == '9q-generic'
+    assert not as_qvm
+    assert not noisy
+
+    name, as_qvm, noisy = _parse_name('9q-generic', True, None)
+    assert name == '9q-generic'
+    assert as_qvm
+    assert not noisy
+
+    name, as_qvm, noisy = _parse_name('9q-generic', True, True)
+    assert name == '9q-generic'
+    assert as_qvm
+    assert noisy
+
+
+def test_parse_qc_redundant():
+    name, as_qvm, noisy = _parse_name('9q-generic', False, False)
+    assert name == '9q-generic'
+    assert not as_qvm
+    assert not noisy
+
+    name, as_qvm, noisy = _parse_name('9q-generic-qvm', True, False)
+    assert name == '9q-generic'
+    assert as_qvm
+    assert not noisy
+
+    name, as_qvm, noisy = _parse_name('9q-generic-noisy-qvm', True, True)
+    assert name == '9q-generic'
+    assert as_qvm
+    assert noisy
+
+
+def test_parse_qc_conflicting():
+    with pytest.raises(ValueError) as e:
+        name, as_qvm, noisy = _parse_name('9q-generic-qvm', False, False)
+
+    assert e.match(r'.*but you have specified `as_qvm=False`')
+
+    with pytest.raises(ValueError) as e:
+        name, as_qvm, noisy = _parse_name('9q-generic-noisy-qvm', True, False)
+    assert e.match(r'.*but you have specified `noisy=False`')
+
+
+def test_parse_qc_strip():
+    # Originally used `str.strip` to remove the suffixes. This is not correct!
+    name, _, _ = _parse_name("mvq-qvm", None, None)
+    assert name == 'mvq'
+
+    name, _, _ = _parse_name("mvq-noisy-qvm", None, None)
+    assert name == 'mvq'
+
+
+def test_qc(forest):
+    qc = get_qc('9q-generic-noisy-qvm', connection=forest)
+    assert isinstance(qc, QuantumComputer)
+    assert isinstance(qc.qam, QVM)
+    assert qc.qam.noise_model is not None
+    assert qc.qubit_topology().number_of_nodes() == 9
+    assert qc.qubit_topology().degree[0] == 2
+    assert qc.qubit_topology().degree[4] == 4
+
+    # TODO: have `qubits` default to all device qubits?
+    bs = qc.run_and_measure(Program(X(0)), qubits=[0], trials=1)
+    np.testing.assert_array_equal(bs, [[1]])
diff --git a/pyquil/tests/utils.py b/pyquil/tests/utils.py
index 84c70ff..0fdde9c 100644
--- a/pyquil/tests/utils.py
+++ b/pyquil/tests/utils.py
@@ -2,4 +2,6 @@ from pyquil.parser import parse
 
 
 def parse_equals(quil_string, *instructions):
-    assert list(instructions) == parse(quil_string)
+    expected = list(instructions)
+    actual = parse(quil_string)
+    assert expected == actual