Repository: BQSKit/bqskit (language: python)

Issue:
Added maximal_matching function to graph, added supermarq stats to ci…
circuit.py updated to include SupermarQ metrics (https://arxiv.org/abs/2202.11045).
graph.py updated to include method to find maximal matchings in coupling graphs.


Discussion / hints:

Fix patch:
diff --git a/.github/workflows/pre-commit.yml b/.github/workflows/pre-commit.yml
index 3fa4ed41d..b2f3b7447 100644
--- a/.github/workflows/pre-commit.yml
+++ b/.github/workflows/pre-commit.yml
@@ -12,7 +12,7 @@ jobs:
     - uses: actions/checkout@v1
     - uses: actions/setup-python@v1
       with:
-        python-version: '3.10'
+        python-version: '3.10.6' # TODO: Remove .6 when mypy fixes issue 13627
     - name: set PY
       run: echo "name=PY::$(python -c 'import hashlib, sys;print(hashlib.sha256(sys.version.encode()+sys.executable.encode()).hexdigest())')" >> $GITHUB_ENV
     - uses: actions/cache@v1
diff --git a/.github/workflows/tox.yml b/.github/workflows/tox.yml
index 509ad9de6..ee22bcd3e 100644
--- a/.github/workflows/tox.yml
+++ b/.github/workflows/tox.yml
@@ -9,7 +9,7 @@ jobs:
   tox:
     strategy:
       matrix:
-        python-version: ['3.7', '3.8', '3.9', '3.10']
+        python-version: ['3.8', '3.9', '3.10']
         os: [macOS-latest, windows-latest, ubuntu-latest]
 
     runs-on: ${{ matrix.os }}
diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml
index 588c2b0ae..3e41c9d8b 100644
--- a/.pre-commit-config.yaml
+++ b/.pre-commit-config.yaml
@@ -7,6 +7,7 @@ repos:
     -   id: trailing-whitespace
     -   id: end-of-file-fixer
     -   id: check-docstring-first
+        exclude: 'bqskit/ext/rigetti.py'
     -   id: debug-statements
     -   id: double-quote-string-fixer
     -   id: name-tests-test
@@ -23,7 +24,7 @@ repos:
     -   id: no-commit-to-branch
         args: [--branch, master]
 -   repo: https://github.com/myint/docformatter
-    rev: v1.5.0-rc1
+    rev: v1.5.0
     hooks:
     -   id: docformatter
         args:
@@ -37,15 +38,14 @@ repos:
     -   id: autopep8
         args:
             - --in-place
-            - -a
             - --max-line-length=80
+            - --ignore=E731
 -   repo: https://github.com/asottile/pyupgrade
-    rev: v2.37.3
+    rev: v2.38.0
     hooks:
     -   id: pyupgrade
         args:
-            - --py36-plus
-            - --keep-percent-format
+            - --py38-plus
 -   repo: https://github.com/asottile/reorder_python_imports
     rev: v3.8.2
     hooks:
@@ -53,7 +53,7 @@ repos:
         args:
             - --add-import
             - from __future__ import annotations
-            - --py3-plus
+            - --py37-plus
 -   repo: https://github.com/asottile/add-trailing-comma
     rev: v2.2.3
     hooks:
@@ -65,7 +65,7 @@ repos:
     hooks:
     -   id: setup-cfg-fmt
 - repo: https://github.com/myint/autoflake
-  rev: v1.4
+  rev: v1.5.3
   hooks:
     - id: autoflake
       args:
@@ -81,6 +81,6 @@ repos:
     hooks:
     -   id: flake8
         args:
-            - --ignore=E741,E241,E226,F405,F403,W503,F901
+            - --ignore=E741,E241,E226,F405,F403,W503,F901,E731,TYP006
             - --max-line-length=80
-        additional_dependencies: [flake8-typing-imports==1.7.0]
+        additional_dependencies: [flake8-typing-imports==1.13.0]
diff --git a/.readthedocs.yaml b/.readthedocs.yaml
index d827a47ba..85435d797 100644
--- a/.readthedocs.yaml
+++ b/.readthedocs.yaml
@@ -8,7 +8,7 @@ version: 2
 build:
   os: ubuntu-20.04
   tools:
-    python: "3.9"
+    python: "3.10"
 
 # Build documentation in the docs/ directory with Sphinx
 sphinx:
diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md
index 91e244da2..d0e68be9b 100644
--- a/CONTRIBUTING.md
+++ b/CONTRIBUTING.md
@@ -23,14 +23,7 @@ You will want to install BQSKit from source, by cloning the repository from gith
 ```
 git clone https://github.com/BQSKit/BQSKit.git
 cd bqskit
-pip install -e .
-```
-
-Once you clone the repository, you will want to install `tox`. The easiest
-way is with pip:
-
-```sh
-pip install tox
+pip install -e '.[dev]'
 ```
 
 You can run `tox` to install all development packages, setup virtual
@@ -62,7 +55,7 @@ top level package's `__init__.py`, e.g. `bqskit.ir.__init__`.
 early with static code analysis tools like [Mypy](http://mypy-lang.org/).
 You can see [PEP 484: Type Annotations](https://www.python.org/dev/peps/pep-0484/)
 for more information. Please do annotate your contribution with types.
-Sometimes this can be tricky, if you need help, feel free to ask.
+Sometimes this can be tricky, if you need help, please don't hesitate to ask.
 
 
 ## Testing
diff --git a/LICENSE b/LICENSE
index 26147c219..a27bf6b01 100644
--- a/LICENSE
+++ b/LICENSE
@@ -20,7 +20,6 @@ Technology (MIT) nor the names of its contributors may be used to
 endorse or promote products derived from this software without specific
 prior written permission.
 
-
 THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
 AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
 IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE
diff --git a/README.md b/README.md
index 321a517b9..08bd6ea8c 100644
--- a/README.md
+++ b/README.md
@@ -1,110 +1,51 @@
 # Berkeley Quantum Synthesis Toolkit (BQSKit)
 
-BQSKit allows you to use high-quality synthesis algorithms with ease to
-compile quantum circuits. It is designed to be highly customizable to find
-a compiler-flow that suites your needs well.
+The Berkeley Quantum Synthesis Toolkit (BQSKit) \[bis • kit\] is a powerful and portable quantum compiler framework.
+It can be used with ease to compile quantum programs to efficient physical circuits for any QPU.
 
 ## Installation
 
-BQSKit is available for Python 3.7+ on Linux, MacOS, and Windows. BQSKit
+BQSKit is available for Python 3.8+ on Linux, macOS, and Windows. BQSKit
 and its dependencies are listed on the [Python Package Index](https://pypi.org),
-and as such, can be installed with pip:
+and as such, pip can easily install it:
 
 ```sh
 pip install bqskit
 ```
 
-## Basic Usage
-
-BQSKit can be used as a quantum compiler and so much more. Below we have
-examples for the common use cases in BQSKit.
-
-### Circuit Optimization Example
-
-The most obvious use for an optimizing quantum compiler is to optimize
-a quantum program. Here we can optimize a program given as a circuit in
-qasm:
+An optional extension includes pre-built support for many quantum devices and modules for working with other quantum frameworks. Pip can install this extension by appending `[ext]` to the previous command:
 
-```python
-from bqskit.compiler import Compiler
-from bqskit.compiler import CompilationTask
-from bqskit.ir import Circuit
-
-# Load a circuit from QASM
-circuit = Circuit.from_file("tfim.qasm")
+```sh
+pip install 'bqskit[ext]'
+```
 
-# Create a standard CompilationTask to optimize the circuit
-task = CompilationTask.optimize(circuit)
+**Note**: If you are using a device with Apple Silicon, you will need to install BQSKit with Anaconda. See the [instructions here](https://github.com/BQSKit/bqskit-tutorial/blob/master/README.md) for more information. We are currently in the process of implementing native ARM support. When that is ready, we will update this note.
 
-# Spawn a compiler and compile the task
-if __name__ == '__main__':
-    with Compiler() as compiler:
-        optimized_circuit = compiler.compile(task)
-```
 
-We first load the qasm program into a `Circuit` object. Then we create
-a `CompilationTask` object, which encapsulates an entire compilation flow.
-This includes the circuit to be compiled, as well as, the configured
-algorithms that are desired to be used to compile the program.
+## Basic Usage
 
-We provide some default constructors for the common cases, such as
-`CompilationTask.optimize` and the ones you will see in the following
-sections. These these default configurations aim to be widely applicable,
-and as a result, may be far from the best. The best compilation
-flow for your use case will likely require some experimentation. Once you are
-familiar with the basics, we recommend you explore how these defaults are
-built and toy with them.
+A standard workflow utilizing BQSKit consists of loading a program into the framework, modeling the target QPU, compiling the program, and exporting the resulting circuit. The below example uses BQSKit to optimize an input circuit provided by a qasm file:
 
-The last block of the program spawns a `Compiler` object which is responsible
-for executing the `CompilationTask`. This will handle the execution efficiently.
+```python
+from bqskit import compile, Circuit
 
-### Unitary Synthesis Example
+# Load a circuit from QASM
+circuit = Circuit.from_file("input.qasm")
 
-A compiler built around the concept of synthesis should definitely
-support synthesis. In fact, the BQSKit compiler does support a variety
-of ways to perform synthesis. The following example uses the default
-flow for synthesizing a toffoli unitary:
+# Compile the circuit
+compiled_circuit = compile(circuit)
 
-```python
-import numpy as np
-from bqskit.compiler import Compiler
-from bqskit.compiler import CompilationTask
-
-# Construct the unitary as an NumPy array
-toffoli = np.array([
-    [1, 0, 0, 0, 0, 0, 0, 0],
-    [0, 1, 0, 0, 0, 0, 0, 0],
-    [0, 0, 1, 0, 0, 0, 0, 0],
-    [0, 0, 0, 1, 0, 0, 0, 0],
-    [0, 0, 0, 0, 1, 0, 0, 0],
-    [0, 0, 0, 0, 0, 1, 0, 0],
-    [0, 0, 0, 0, 0, 0, 0, 1],
-    [0, 0, 0, 0, 0, 0, 1, 0],
-])
-
-# Create a standard synthesis CompilationTask
-task = CompilationTask.synthesize(toffoli)
-
-# Spawn a compiler and compile the task
-if __name__ == '__main__':
-    with Compiler() as compiler:
-        synthesized_circuit = compiler.compile(task)
+# Save output as QASM
+compiled_circuit.save("output.qasm")
 ```
 
-## Verifying Results
+To learn more about BQSKit, follow the [tutorial series](https://github.com/BQSKit/bqskit-tutorial/blob/master/1_comping_with_bqskit.ipynb) or refer to the [documentation](https://bqskit.readthedocs.io/en/latest/).
 
-All of BQSKit's synthesis algorithms are approximate, which mean the
-compiled results might be slightly different than what was inputted.
-For most cases, these differences are close to floating point thresholds,
-but can be greater in magnitude. Most algorithms will offer some sort
-of control over this error, but it is important to be able to measure it.
+## How to Cite
 
-For simulatable circuits, we can compare the unitaries directly:
+You can use the [software disclosure](https://www.osti.gov/biblio/1785933) to cite the BQSKit package.
 
-```python
-dist = synthesized_circuit.get_unitary().get_distance_from(toffoli)
-assert dist < 1e-10
-```
+Additionally, if you used or extended a specific algorithm, you can cite that individually. BQSKit passes will include a relevant reference in their documentation.
 
 ## License
 
diff --git a/bqskit/__init__.py b/bqskit/__init__.py
index 98b3324e3..09a2fccd9 100644
--- a/bqskit/__init__.py
+++ b/bqskit/__init__.py
@@ -2,21 +2,21 @@
 from __future__ import annotations
 
 import logging
+from sys import stdout as _stdout
 
-from bqskit.compiler.compiler import Compiler
-from bqskit.compiler.task import CompilationTask
+from bqskit.compiler.compile import compile
+from bqskit.compiler.machine import MachineModel
 from bqskit.ir.circuit import Circuit
-from bqskit.ir.lang import register_language
-from bqskit.ir.lang.qasm2 import OPENQASM2Language
-from bqskit.qis.unitary.unitarymatrix import UnitaryMatrix
-
+from bqskit.ir.lang import register_language as _register_language
+from bqskit.ir.lang.qasm2 import OPENQASM2Language as _qasm
 
 # Initialize Logging
 _logger = logging.getLogger('bqskit')
-_logger.setLevel(logging.CRITICAL)
-_handler = logging.StreamHandler()
-_handler.setLevel(logging.DEBUG)
-_fmt = '%(asctime)s.%(msecs)03d - %(levelname)-8s | %(name)s: %(message)s'
+_handler = logging.StreamHandler(_stdout)
+_handler.setLevel(0)
+_fmt_header = '(%(thread)d): %(asctime)s.%(msecs)03d - %(levelname)-8s |'
+_fmt_message = ' %(name)s: %(message)s'
+_fmt = _fmt_header + _fmt_message
 _formatter = logging.Formatter(_fmt, '%H:%M:%S')
 _handler.setFormatter(_formatter)
 _logger.addHandler(_handler)
@@ -34,14 +34,46 @@ def enable_logging(verbose: bool = False) -> None:
     logging.getLogger('bqskit').setLevel(level)
 
 
-# Register supported languages
-register_language('qasm', OPENQASM2Language())
+def disable_parallelism() -> None:
+    """
+    Disables the default parallelism in BQSKit.
+
+    If parallelism is disabled, everything will run single threaded. Performance
+    will degrade drastically but will free up computer resources. This setting
+    is ignored if connecting to an external cluster.
+    """
+    from bqskit.compiler import Compiler
+    Compiler._parallelism_enabled = False
+
+
+def enable_parallelism() -> None:
+    """
+    Enables parallelism; it is enabled by default.
+
+    See `disable_parallelism` for more info.
+    """
+    from bqskit.compiler import Compiler
+    Compiler._parallelism_enabled = True
+
+
+def enable_dashboard() -> None:
+    """Enables the dask dashboard."""
+    from bqskit.compiler import Compiler
+    Compiler._dashboard = True
+
+
+def disable_dashboard() -> None:
+    """Enables the dask dashboard."""
+    from bqskit.compiler import Compiler
+    Compiler._dashboard = False
 
 
 __all__ = [
-    'CompilationTask',
-    'Compiler',
+    'compile',
+    'MachineModel',
     'Circuit',
-    'UnitaryMatrix',
     'enable_logging',
 ]
+
+# Register supported languages
+_register_language('qasm', _qasm())
diff --git a/bqskit/compiler/__init__.py b/bqskit/compiler/__init__.py
index 7f0557f0d..ccac308d1 100644
--- a/bqskit/compiler/__init__.py
+++ b/bqskit/compiler/__init__.py
@@ -5,7 +5,19 @@
 
 The `bqskit.compiler` package implements the BQSKit's compiler framework.
 This includes classes for defining, combining, executing, and managing
-compilation algorithms.
+compilation algorithms. It also includes the standard :func:`compile`
+function definition.
+
+.. rubric:: Standard BQSKit Compile Function
+
+.. autosummary::
+    :toctree: autogen
+    :recursive:
+    :nosignatures:
+
+    compile
+
+.. rubric:: Compiler Infrastructure
 
 .. autosummary::
     :toctree: autogen
@@ -20,6 +32,7 @@
 from __future__ import annotations
 
 from bqskit.compiler.basepass import BasePass
+from bqskit.compiler.compile import compile
 from bqskit.compiler.compiler import Compiler
 from bqskit.compiler.executor import Executor
 from bqskit.compiler.machine import MachineModel
@@ -27,8 +40,9 @@
 
 __all__ = [
     'BasePass',
+    'compile',
     'Compiler',
     'Executor',
-    'CompilationTask',
     'MachineModel',
+    'CompilationTask',
 ]
diff --git a/bqskit/compiler/basepass.py b/bqskit/compiler/basepass.py
index 363b7f66f..44da2a0bb 100644
--- a/bqskit/compiler/basepass.py
+++ b/bqskit/compiler/basepass.py
@@ -13,8 +13,10 @@
 
 from bqskit.compiler.machine import MachineModel
 from bqskit.ir.circuit import Circuit
+from bqskit.qis.graph import CouplingGraph
 from bqskit.qis.unitary.unitarymatrix import UnitaryMatrix
 from bqskit.utils.typing import is_iterable
+from bqskit.utils.typing import is_sequence
 
 _logger = logging.getLogger(__name__)
 
@@ -60,7 +62,10 @@ def run(self, circuit: Circuit, data: dict[str, Any] = {}) -> None:
         """
 
     @staticmethod
-    def get_model(circuit: Circuit, data: dict[str, Any]) -> MachineModel:
+    def get_model(
+        circuit: Circuit | UnitaryMatrix,
+        data: dict[str, Any],
+    ) -> MachineModel:
         """
         Retrieve the machine model from the data dictionary.
 
@@ -89,6 +94,71 @@ def get_model(circuit: Circuit, data: dict[str, Any]) -> MachineModel:
 
         return data['machine_model']
 
+    @staticmethod
+    def get_placement(
+        circuit: Circuit | UnitaryMatrix,
+        data: dict[str, Any],
+    ) -> list[int]:
+        """
+        Retrieve the logical to physical qubit map from the data dictionary.
+
+        Args:
+            circuit (Circuit | UnitaryMatrix): The pass circuit.
+
+            data (dict[str, Any]): The data dictionary.
+
+        Returns:
+            (list[int]): A list with length equal to circuit.num_qudits.
+            Logical qubit i is mapped to the physical qubit described
+            by the i-th element in the list.
+        """
+        # Default placement is trivial map: i -> i
+        default_placement = list(range(circuit.num_qudits))
+        model = BasePass.get_model(circuit, data)
+        sg = model.coupling_graph.get_subgraph(default_placement)
+
+        # Check in data for existing placement
+        if 'placement' in data:
+            p = data['placement']
+
+            # Check it's valid
+            if is_sequence(p) and len(p) == circuit.num_qudits:
+                return data['placement']
+
+        # If none found try to return default placement
+        if sg.is_fully_connected():
+            if len(data) != 0:
+                data['placement'] = default_placement
+            return default_placement
+
+        raise RuntimeError(
+            "No valid placement found and trivial one doesn't work."
+            '\nConsider running a placement pass before the current pass.',
+        )
+
+    @staticmethod
+    def get_connectivity(
+        circuit: Circuit | UnitaryMatrix,
+        data: dict[str, Any],
+    ) -> CouplingGraph:
+        """
+        Retrieve the current connectivity of the circuit.
+
+        Args:
+            circuit (Circuit | UnitaryMatrix): The pass circuit.
+
+            data (dict[str, Any]): The data dictionary.
+
+        Returns:
+            (list[int]): A coupling graph of the connectivity.
+        """
+        model = BasePass.get_model(circuit, data)
+        try:
+            placement = BasePass.get_placement(circuit, data)
+        except RuntimeError:
+            placement = list(range(circuit.num_qudits))
+        return model.coupling_graph.get_subgraph(placement)
+
     @staticmethod
     def get_target(circuit: Circuit, data: dict[str, Any]) -> UnitaryMatrix:
         """
@@ -133,9 +203,16 @@ def execute(
         *args: Any,
         **kwargs: Any,
     ) -> list[T]:
-        """Execute a function potentially in parallel."""
+        """Map a function over iterable arguments in parallel."""
 
-        if BasePass.in_parallel(data):
+        if not all(is_iterable(arg) for arg in args):
+            raise ValueError('Each argument must be a sized iterable')
+
+        if not all(len(arg) == len(args[0]) for arg in args):
+            raise ValueError('Each iterable argument must have same length.')
+
+        # Execute using dask if dask is started and there are multiple tasks
+        if BasePass.in_parallel(data) and len(args[0]) > 1:
             if fn == Circuit.instantiate:
                 kwargs['parallel'] = True
 
@@ -146,12 +223,13 @@ def execute(
             rejoin()
             return results
 
+        # Otherwise execute in current process
         else:
-            if all(is_iterable(arg) for arg in args):
-                if all(len(arg) == len(args[0]) for arg in args):
-                    results = []
-                    for subargs in zip(*args):
-                        results.append(fn(*subargs, **kwargs))
-                    return results
-
-            return [fn(*args, **kwargs)]
+            results = []
+            if len(args) == 1:
+                for arg in args[0]:
+                    results.append(fn(arg, **kwargs))
+            else:
+                for subargs in zip(*args):
+                    results.append(fn(*subargs, **kwargs))
+            return results
diff --git a/bqskit/compiler/compile.py b/bqskit/compiler/compile.py
new file mode 100644
index 000000000..41679806e
--- /dev/null
+++ b/bqskit/compiler/compile.py
@@ -0,0 +1,939 @@
+"""This module defines a standard `compile` function using BQSKit."""
+from __future__ import annotations
+
+import logging
+import warnings
+from typing import Any
+from typing import Callable
+from typing import TYPE_CHECKING
+
+import numpy as np
+
+from bqskit.compiler.basepass import BasePass
+from bqskit.compiler.compiler import Compiler
+from bqskit.compiler.machine import MachineModel
+from bqskit.compiler.task import CompilationTask
+from bqskit.ir.circuit import Circuit
+from bqskit.ir.gates import CNOTGate
+from bqskit.ir.gates import RZGate
+from bqskit.ir.gates import SqrtXGate
+from bqskit.ir.gates import SwapGate
+from bqskit.ir.gates.circuitgate import CircuitGate
+from bqskit.ir.gates.measure import MeasurementPlaceholder
+from bqskit.ir.gates.parameterized.u3 import U3Gate
+from bqskit.ir.operation import Operation
+from bqskit.ir.opt import HilbertSchmidtCostGenerator
+from bqskit.ir.opt import ScipyMinimizer
+from bqskit.passes import *
+from bqskit.qis.state.state import StateVector
+from bqskit.qis.unitary.unitarymatrix import UnitaryMatrix
+from bqskit.utils.typing import is_integer
+from bqskit.utils.typing import is_real_number
+_logger = logging.getLogger(__name__)
+if TYPE_CHECKING:
+    from bqskit.qis.unitary import UnitaryLike
+    from bqskit.qis.state import StateLike
+
+
+def compile(
+    input: Circuit | UnitaryLike | StateLike,
+    model: MachineModel | None = None,
+    optimization_level: int = 1,
+    max_synthesis_size: int = 3,
+    synthesis_epsilon: float = 1e-10,
+    error_threshold: float | None = None,
+    error_sim_size: int = 8,
+    compiler: Compiler | None = None,
+    *compiler_args: Any,
+    **compiler_kwargs: Any,
+) -> Circuit:
+    """
+    Compile a circuit, unitary, or state with a standard workflow.
+
+    Args:
+        input (Circuit | UnitaryLike | StateLike): The input to compile.
+
+        model (MachineModel | None): A model of the target machine.
+            Defaults to an all-to-all connected hardware with CNOTs and U3s
+            as native gates. See :class:`MachineModel` for information
+            on loading a preset model or creating a custom one.
+
+        optimization_level (int): The degree of optimization in the workflow.
+            The workflow will produce better circuits at the cost of
+            performance with a higher number.  An optimization_level
+            of 0 is not supported due to inherit optimization in any
+            workflow containing synthesis. Valid inputs are 1, 2, 3, or 4.
+            (Default: 1)
+
+        max_synthesis_size (int): The maximum size of a unitary to
+            synthesize or instantiate. Larger circuits will be partitioned.
+            Increasing this will most likely lead to better results with
+            an exponential time trade-off. (Default: 3)
+
+        synthesis_epsilon (float): The maximum distance between target
+            and circuit unitary during any instantiation or synthesis
+            algorithms. (Default: 1e-10)
+
+        error_threshold (float | None): This parameter controls the
+            verification mechanism in this compile function. By default,
+            it is set to None, so no verification is done. If you set
+            this to a float, the upper bound on compilation error
+            is calculated. If the upper bound is larger than this number,
+            a warning will be logged. (Default: None)
+
+        error_sim_size (int): If an `error_threshold` is set, the error
+            upper bound is calculated by simulating blocks of this size.
+            As you increase `error_sim_size`, the upper bound on error
+            becomes more accurate. (Default: 8)
+
+        compiler (Compiler | None): Pass a :class:`Compiler` to prevent
+            creating one. Save on startup time by passing a compiler in
+            when calling `compile` multiple times. (Default: None)
+
+        compiler_args (Any): Passed directly to BQSKit compiler construction.
+            Arguments for connecting to a dask cluster can go here.
+            See :class:`Compiler` for more info.
+
+        compiler_kwargs (Any): Passed directly to BQSKit compiler construction.
+            Arguments for connecting to a dask cluster can go here.
+            See :class:`Compiler` for more info.
+
+    Returns:
+        (Circuit): The compiled circuit.
+
+    Examples:
+        >>> from bqskit import Circuit, compile
+        >>> circuit = Circuit.from_file('input.qasm')
+        >>> compiled_circuit = compile(circuit)
+        >>> compiled_circuit.save('output.qasm')
+    """
+    # Check `input`
+    try:
+        if isinstance(input, Circuit):
+            pass
+
+        elif UnitaryMatrix.is_unitary(input):
+            input = UnitaryMatrix(input)
+
+        elif StateVector.is_pure_state(input):
+            input = StateVector(input)
+
+        else:
+            raise TypeError(
+                'Input is neither a circuit, a unitary, nor a state.'
+                f' Got {type(input)}.',
+            )
+    except Exception as e:
+        raise TypeError(
+            'Unable to determine type of input.'
+            ' Ensure that you are trying to compile a valid'
+            ' circuit, unitary, or state.',
+        ) from e
+
+    assert isinstance(input, (Circuit, UnitaryMatrix, StateVector))
+
+    if not all(r == 2 for r in input.radixes):
+        raise ValueError(
+            'Currently, can only automatically build a workflow '
+            'for qubit-only systems.',
+        )  # TODO
+
+    # Check `model`
+    if model is None:
+        model = MachineModel(input.num_qudits)
+
+    if not isinstance(model, MachineModel):
+        raise TypeError(f'Expected MachineModel for model, got {type(model)}.')
+
+    if model.num_qudits < input.num_qudits:
+        raise ValueError('Machine is too small for circuit.')
+
+    if not all(r == 2 for r in model.radixes):
+        raise ValueError(
+            'Currently, can only automatically build a workflow '
+            'for qubit-only systems.',
+        )  # TODO
+
+    model_mq_gates = [g for g in model.gate_set if g.num_qudits >= 2]
+
+    if len(model_mq_gates) == 0 and input.num_qudits > 1:
+        raise ValueError('No entangling gates in native gate set.')
+
+    if all(g.num_qudits > input.num_qudits for g in model.gate_set):
+        raise ValueError(
+            'Model gate set does not contain any entangling gates'
+            ' that is less than or equal to input size.'
+            f' Cannot compile {input.num_qudits}-qudit input without'
+            f' {input.num_qudits}-qudit or smaller gates.',
+        )
+
+    # Check `optimization_level`
+    if not is_integer(optimization_level):
+        raise TypeError(
+            'Expected integer for optimization_level'
+            f', got{type(optimization_level)}.',
+        )
+
+    if optimization_level < 1 or optimization_level > 4:
+        raise ValueError(
+            'The optimization level should be either 1, 2, 3, or 4.'
+            f' Got {optimization_level}.',
+        )
+
+    if len(model_mq_gates) >= 2 and optimization_level > 2:
+        _logger.warning(
+            'Multiple entangling gates in native gate set.\n'
+            'Expect longer compile times.',
+        )
+
+    # Check `max_synthesis_size`
+    if not is_integer(max_synthesis_size):
+        raise TypeError(
+            'Expected integer for max_synthesis_size'
+            f', got{type(max_synthesis_size)}.',
+        )
+
+    if max_synthesis_size < max(g.num_qudits for g in model.gate_set):
+        value = max(g.num_qudits for g in model.gate_set)
+        raise ValueError(
+            'The maximum synthesis size needs to be greater than or equal to'
+            f' the largest native gate size: {max_synthesis_size} < {value}.',
+        )
+
+    if max_synthesis_size > 6:
+        _logger.warning('Large max synthesis size; expect long compile times.')
+
+    # Check `synthesis_epsilon`
+    if not is_real_number(synthesis_epsilon):
+        raise TypeError(
+            'Expected float for synthesis_epsilon'
+            f', got {type(synthesis_epsilon)}.',
+        )
+
+    if synthesis_epsilon < 0 or synthesis_epsilon > 1:
+        raise ValueError(
+            'Out-of-bounds synthesis_epsilon, it must be between 0 and 1,'
+            f' got {synthesis_epsilon}.',
+        )
+
+    # Check `error_threshold`
+    if error_threshold is not None:
+        if not is_real_number(error_threshold):
+            raise TypeError(
+                'Expected float for error_threshold'
+                f', got {type(error_threshold)}.',
+            )
+
+        if error_threshold < 0 or error_threshold > 1:
+            raise ValueError(
+                'Out-of-bounds error_threshold, it must be between 0 and 1,'
+                f' got {error_threshold}.',
+            )
+
+    # Check `error_sim_size`
+    if not is_integer(error_sim_size):
+        raise TypeError(
+            'Expected integer for error_sim_size'
+            f', got{type(error_sim_size)}.',
+        )
+
+    if error_sim_size < max_synthesis_size:
+        raise ValueError(
+            'Simulation size for error calculation cannot be less than the'
+            f'maximum synthesis size: {error_sim_size} < {max_synthesis_size}.',
+        )
+
+    # Build workflow
+    if isinstance(input, Circuit):
+        if input.num_qudits > max_synthesis_size:
+            if any(
+                g.num_qudits > max_synthesis_size
+                and not isinstance(g, MeasurementPlaceholder)
+                for g in input.gate_set
+            ):
+                raise ValueError(
+                    'Unable to compile circuit with gate larger than'
+                    ' max_synthesis_size.\nConsider adjusting it.',
+                )
+
+        task = _circuit_workflow(
+            input,
+            model,
+            optimization_level,
+            synthesis_epsilon,
+            max_synthesis_size,
+            error_threshold,
+            error_sim_size,
+        )
+
+    elif isinstance(input, UnitaryMatrix):
+        utry = input
+
+        if utry.num_qudits > max_synthesis_size:
+            raise ValueError(
+                'Cannot synthesize unitary larger than max_synthesis_size.\n'
+                'Consider adjusting max_synthesis_size or checking the input.',
+            )
+
+        task = _circuit_workflow(
+            Circuit.from_unitary(utry),
+            model,
+            optimization_level,
+            synthesis_epsilon,
+            max_synthesis_size,
+            error_threshold,
+            error_sim_size,
+        )
+
+    elif isinstance(input, StateVector):
+        state = input
+
+        if state.num_qudits > max_synthesis_size:
+            raise ValueError(
+                'Cannot synthesize state larger than max_synthesis_size.\n'
+                'Consider adjusting max_synthesis_size or checking the input.',
+            )
+
+        task = _stateprep_workflow(
+            state,
+            model,
+            optimization_level,
+            synthesis_epsilon,
+            max_synthesis_size,
+            error_threshold,
+            error_sim_size,
+        )
+
+    # Connect to or construct a Compiler
+    managed_compiler = compiler is None
+
+    if managed_compiler:
+        compiler = Compiler(*compiler_args, **compiler_kwargs)
+
+    elif not isinstance(compiler, Compiler):
+        raise TypeError(
+            'Expected Compiler object for compiler parameter'
+            f', got {type(compiler)}.',
+        )
+
+    # Perform the compilation
+    out = compiler.compile(task)
+
+    # Log error if necessary
+    if error_threshold is not None:
+        error = compiler.analyze(task, 'error')
+        nonsq_error = 1 - np.sqrt(max(1 - (error * error), 0))
+        if nonsq_error > error_threshold:
+            warnings.warn(
+                'Upper bound on error is greater than set threshold:'
+                f' {nonsq_error} > {error_threshold}.',
+            )
+
+    # Close managed compiler
+    if managed_compiler:
+        compiler.close()
+
+    return out
+
+
+def _circuit_workflow(
+    circuit: Circuit,
+    model: MachineModel,
+    optimization_level: int = 1,
+    synthesis_epsilon: float = 1e-10,
+    max_synthesis_size: int = 4,
+    error_threshold: float | None = None,
+    error_sim_size: int = 8,
+) -> CompilationTask:
+    """Build standard workflow for circuit compilation."""
+    workflow_builders = [
+        _opt1_workflow,
+        _opt2_workflow,
+        _opt3_workflow,
+        _opt4_workflow,
+    ]
+    workflow_builder = workflow_builders[optimization_level - 1]
+    workflow = [UnfoldPass(), ExtractMeasurements()]
+    workflow += workflow_builder(
+        circuit,
+        model,
+        synthesis_epsilon,
+        max_synthesis_size,
+        error_threshold,
+        error_sim_size,
+    )
+    workflow += [RestoreMeasurements()]
+    return CompilationTask(circuit, workflow)
+
+
+def _opt1_workflow(
+    circuit: Circuit,
+    model: MachineModel,
+    synthesis_epsilon: float = 1e-10,
+    max_synthesis_size: int = 4,
+    error_threshold: float | None = None,
+    error_sim_size: int = 8,
+) -> list[BasePass]:
+    """Build Optimization Level 1 workflow for circuit compilation."""
+    layer_gen = _get_layer_gen(model)
+    scan_mq = ScanningGateRemovalPass(
+        success_threshold=synthesis_epsilon,
+        collection_filter=_mq_gate_collection_filter,
+    )
+    qsearch = QSearchSynthesisPass(
+        success_threshold=synthesis_epsilon,
+        layer_generator=layer_gen,
+    )
+    leap = LEAPSynthesisPass(
+        success_threshold=synthesis_epsilon,
+        layer_generator=layer_gen,
+        min_prefix_size=3,
+    )
+    direct_synthesis = IfThenElsePass(WidthPredicate(3), qsearch, leap)
+    single_qudit_gate_rebase = _get_single_qudit_gate_rebase_pass(model)
+    if circuit.num_qudits > 1:
+        smallest_entangler_size = min(
+            g.num_qudits for g in model.gate_set
+            if g.num_qudits != 1
+        )
+        non_native_gates = [
+            g for g in circuit.gate_set
+            if g not in model.gate_set
+        ]
+        non_native_tq_gates = [
+            g for g in non_native_gates
+            if g.num_qudits == 2
+        ]
+        if SwapGate() not in model.gate_set:
+            non_native_tq_gates.append(SwapGate())
+        native_tq_gates = [g for g in model.gate_set if g.num_qudits == 2]
+
+        all_gates = model.gate_set.union(circuit.gate_set)
+        if any(g.num_qudits > 2 for g in all_gates):
+            multi_qudit_gate_rebase: BasePass = direct_synthesis
+        else:
+            multi_qudit_gate_rebase = Rebase2QuditGatePass(
+                non_native_tq_gates,
+                native_tq_gates,
+                max_depth=3,
+                max_retries=5,
+            )
+    else:
+        smallest_entangler_size = 1
+        multi_qudit_gate_rebase = NOOPPass()
+
+    return [
+        SetModelPass(model),
+
+        # Multi-qudit gate retargeting
+        IfThenElsePass(
+            NotPredicate(WidthPredicate(2)),
+            [
+                LogPass('Retargeting multi-qudit gates.'),
+                QuickPartitioner(max_synthesis_size),
+                ExtendBlockSizePass(smallest_entangler_size),
+                QuickPartitioner(error_sim_size),
+                ForEachBlockPass(
+                    ForEachBlockPass(
+                        [
+                            FillSingleQuditGatesPass(synthesis_epsilon),
+                            IfThenElsePass(
+                                NotPredicate(MultiPhysicalPredicate()),
+                                multi_qudit_gate_rebase,
+                                scan_mq,
+                            ),
+                        ],
+                        replace_filter=_gen_replace_filter(model),
+                    ),
+                    calculate_error_bound=(error_threshold is not None),
+                ),
+                UnfoldPass(),
+            ],
+        ),
+
+        # Mapping via Sabre
+        LogPass('Mapping circuit.'),
+        GreedyPlacementPass(),
+        GeneralizedSabreLayoutPass(),
+        GeneralizedSabreRoutingPass(),
+
+        # Swap gate retargeting
+        IfThenElsePass(
+            NotPredicate(WidthPredicate(2)),
+            [
+                LogPass('Retargeting swap gates.'),
+                QuickPartitioner(max_synthesis_size),
+                ExtendBlockSizePass(smallest_entangler_size),
+                QuickPartitioner(error_sim_size),
+                ForEachBlockPass(
+                    ForEachBlockPass(
+                        [
+                            FillSingleQuditGatesPass(synthesis_epsilon),
+                            IfThenElsePass(
+                                NotPredicate(MultiPhysicalPredicate()),
+                                multi_qudit_gate_rebase,
+                            ),
+                        ],
+                        replace_filter=_gen_replace_filter(model),
+                    ),
+                    calculate_error_bound=(error_threshold is not None),
+                ),
+                UnfoldPass(),
+            ],
+        ),
+
+        # Single-qudit gate retargeting
+        LogPass('Retargeting single-qudit gates.'),
+        single_qudit_gate_rebase,
+
+        # Finalizing
+        LogErrorPass(),
+        ApplyPlacement(),
+    ]
+
+
+def _opt2_workflow(
+    circuit: Circuit,
+    model: MachineModel,
+    synthesis_epsilon: float = 1e-10,
+    max_synthesis_size: int = 3,
+    error_threshold: float | None = None,
+    error_sim_size: int = 8,
+) -> list[BasePass]:
+    """Build Optimization Level 2 workflow for circuit compilation."""
+    inst_ops = {'multistarts': 4, 'ftol': 5e-12, 'gtol': 1e-14}
+    layer_gen = _get_layer_gen(model)
+    scan_mq = ScanningGateRemovalPass(
+        success_threshold=synthesis_epsilon,
+        collection_filter=_mq_gate_collection_filter,
+    )
+    scan = ScanningGateRemovalPass(success_threshold=synthesis_epsilon)
+    qsearch = QSearchSynthesisPass(
+        success_threshold=synthesis_epsilon,
+        layer_generator=layer_gen,
+        instantiate_options=inst_ops,
+    )
+    leap = LEAPSynthesisPass(
+        success_threshold=synthesis_epsilon,
+        layer_generator=layer_gen,
+        instantiate_options=inst_ops,
+        min_prefix_size=5,
+    )
+    direct_synthesis = IfThenElsePass(WidthPredicate(3), qsearch, leap)
+    single_qudit_gate_rebase = _get_single_qudit_gate_rebase_pass(model)
+    if circuit.num_qudits > 1:
+        smallest_entangler_size = min(
+            g.num_qudits for g in model.gate_set
+            if g.num_qudits != 1
+        )
+        non_native_gates = [
+            g for g in circuit.gate_set
+            if g not in model.gate_set
+        ]
+        non_native_tq_gates = [
+            g for g in non_native_gates
+            if g.num_qudits == 2
+        ]
+        if SwapGate() not in model.gate_set:
+            non_native_tq_gates.append(SwapGate())
+        native_tq_gates = [g for g in model.gate_set if g.num_qudits == 2]
+
+        all_gates = model.gate_set.union(circuit.gate_set)
+        if any(g.num_qudits > 2 for g in all_gates):
+            multi_qudit_gate_rebase: BasePass = direct_synthesis
+        else:
+            multi_qudit_gate_rebase = Rebase2QuditGatePass(
+                non_native_tq_gates,
+                native_tq_gates,
+                max_depth=3,
+                max_retries=5,
+            )
+    else:
+        smallest_entangler_size = 1
+        multi_qudit_gate_rebase = NOOPPass()
+
+    return [
+        SetModelPass(model),
+
+        # Multi-qudit gate retargeting
+        IfThenElsePass(
+            NotPredicate(WidthPredicate(2)),
+            [
+                LogPass('Retargeting multi-qudit gates.'),
+                QuickPartitioner(max_synthesis_size),
+                ExtendBlockSizePass(smallest_entangler_size),
+                QuickPartitioner(error_sim_size),
+                ForEachBlockPass(
+                    ForEachBlockPass(
+                        [
+                            FillSingleQuditGatesPass(synthesis_epsilon),
+                            IfThenElsePass(
+                                NotPredicate(MultiPhysicalPredicate()),
+                                multi_qudit_gate_rebase,
+                                scan_mq,
+                            ),
+                        ],
+                        replace_filter=_gen_replace_filter(model),
+                    ),
+                    calculate_error_bound=(error_threshold is not None),
+                ),
+                UnfoldPass(),
+            ],
+        ),
+
+        # Mapping via Sabre
+        LogPass('Mapping circuit.'),
+        GreedyPlacementPass(),
+        GeneralizedSabreLayoutPass(),
+        GeneralizedSabreRoutingPass(),
+
+        # Swap gate retargeting
+        IfThenElsePass(
+            NotPredicate(WidthPredicate(2)),
+            [
+                LogPass('Retargeting swap gates.'),
+                QuickPartitioner(max_synthesis_size),
+                ExtendBlockSizePass(smallest_entangler_size),
+                QuickPartitioner(error_sim_size),
+                ForEachBlockPass(
+                    ForEachBlockPass(
+                        [
+                            FillSingleQuditGatesPass(synthesis_epsilon),
+                            IfThenElsePass(
+                                NotPredicate(MultiPhysicalPredicate()),
+                                multi_qudit_gate_rebase,
+                            ),
+                        ],
+                        replace_filter=_gen_replace_filter(model),
+                    ),
+                    calculate_error_bound=(error_threshold is not None),
+                ),
+                UnfoldPass(),
+            ],
+        ),
+
+        # Single-qudit gate retargeting
+        LogPass('Retargeting single-qudit gates.'),
+        single_qudit_gate_rebase,
+
+        # Optimization: Scanning gate deletion on blocks
+        LogPass('Attempting to delete gates.'),
+        QuickPartitioner(max_synthesis_size),
+        ExtendBlockSizePass(smallest_entangler_size),
+        QuickPartitioner(error_sim_size),
+        ForEachBlockPass(
+            ForEachBlockPass(scan),
+            calculate_error_bound=(error_threshold is not None),
+        ),
+        UnfoldPass(),
+
+        # Finalizing
+        LogErrorPass(),
+        ApplyPlacement(),
+    ]
+
+
+def _opt3_workflow(
+    circuit: Circuit,
+    model: MachineModel,
+    synthesis_epsilon: float = 1e-10,
+    max_synthesis_size: int = 4,
+    error_threshold: float | None = None,
+    error_sim_size: int = 8,
+) -> list[BasePass]:
+    """Build Optimization Level 3 workflow for circuit compilation."""
+    inst_ops = {
+        'multistarts': 8,
+        'method': 'minimization',
+        'ftol': 5e-16,
+        'gtol': 1e-15,
+    }
+    layer_gen = _get_layer_gen(model)
+    scan = ScanningGateRemovalPass(success_threshold=synthesis_epsilon)
+    scan_mq = ScanningGateRemovalPass(
+        success_threshold=synthesis_epsilon,
+        collection_filter=_mq_gate_collection_filter,
+    )
+    qsearch = QSearchSynthesisPass(
+        success_threshold=synthesis_epsilon,
+        layer_generator=layer_gen,
+        instantiate_options=inst_ops,
+    )
+    leap = LEAPSynthesisPass(
+        success_threshold=synthesis_epsilon,
+        layer_generator=layer_gen,
+        instantiate_options=inst_ops,
+        min_prefix_size=7,
+    )
+    direct_synthesis = IfThenElsePass(WidthPredicate(4), qsearch, leap)
+    single_qudit_gate_rebase = _get_single_qudit_gate_rebase_pass(model)
+    if circuit.num_qudits > 1:
+        smallest_entangler_size = min(
+            g.num_qudits for g in model.gate_set
+            if g.num_qudits != 1
+        )
+        non_native_gates = [
+            g for g in circuit.gate_set
+            if g not in model.gate_set
+        ]
+        non_native_tq_gates = [
+            g for g in non_native_gates
+            if g.num_qudits == 2
+        ]
+        if SwapGate() not in model.gate_set:
+            non_native_tq_gates.append(SwapGate())
+        native_tq_gates = [g for g in model.gate_set if g.num_qudits == 2]
+        native_mq_gates = [g for g in model.gate_set if g.num_qudits >= 2]
+
+        all_gates = model.gate_set.union(circuit.gate_set)
+        if any(g.num_qudits > 2 for g in all_gates):
+            multi_qudit_gate_rebase: BasePass = direct_synthesis
+        else:
+            multi_qudit_gate_rebase = Rebase2QuditGatePass(
+                non_native_tq_gates,
+                native_tq_gates,
+                max_depth=3,
+                max_retries=5,
+            )
+    else:
+        smallest_entangler_size = 1
+        multi_qudit_gate_rebase = NOOPPass()
+        native_mq_gates = []
+
+    return [
+        SetModelPass(model),
+
+        # Multi-qudit gate retargeting
+        IfThenElsePass(
+            NotPredicate(WidthPredicate(2)),
+            [
+                LogPass('Retargeting multi-qudit gates.'),
+                QuickPartitioner(max_synthesis_size),
+                ExtendBlockSizePass(smallest_entangler_size),
+                QuickPartitioner(error_sim_size),
+                ForEachBlockPass(
+                    ForEachBlockPass(
+                        [
+                            FillSingleQuditGatesPass(synthesis_epsilon),
+                            IfThenElsePass(
+                                NotPredicate(MultiPhysicalPredicate()),
+                                multi_qudit_gate_rebase,
+                                scan_mq,
+                            ),
+                        ],
+                        replace_filter=_gen_replace_filter(model),
+                    ),
+                    calculate_error_bound=(error_threshold is not None),
+                ),
+                UnfoldPass(),
+            ],
+        ),
+
+        # Mapping via Sabre
+        LogPass('Mapping circuit.'),
+        GreedyPlacementPass(),
+        GeneralizedSabreLayoutPass(),
+        GeneralizedSabreRoutingPass(),
+
+        # Swap gate retargeting
+        IfThenElsePass(
+            NotPredicate(WidthPredicate(2)),
+            [
+                LogPass('Retargeting swap gates.'),
+                QuickPartitioner(max_synthesis_size),
+                ExtendBlockSizePass(smallest_entangler_size),
+                QuickPartitioner(error_sim_size),
+                ForEachBlockPass(
+                    ForEachBlockPass(
+                        [
+                            FillSingleQuditGatesPass(synthesis_epsilon),
+                            IfThenElsePass(
+                                NotPredicate(MultiPhysicalPredicate()),
+                                multi_qudit_gate_rebase,
+                            ),
+                        ],
+                        replace_filter=_gen_replace_filter(model),
+                    ),
+                    calculate_error_bound=(error_threshold is not None),
+                ),
+                UnfoldPass(),
+            ],
+        ),
+
+        # Optimization: Iterative Resynthesis
+        IfThenElsePass(
+            NotPredicate(WidthPredicate(2)),
+            [
+                WhileLoopPass(
+                    GateCountPredicate(native_mq_gates),
+                    [
+                        LogPass('Resynthesizing blocks.'),
+                        QuickPartitioner(max_synthesis_size),
+                        ExtendBlockSizePass(smallest_entangler_size),
+                        QuickPartitioner(error_sim_size),
+                        ForEachBlockPass(
+                            ForEachBlockPass(
+                                direct_synthesis,
+                                replace_filter=_gen_replace_filter(model),
+                            ),
+                            calculate_error_bound=(error_threshold is not None),
+                        ),
+                        UnfoldPass(),
+                    ],
+                ),
+            ],
+        ),
+
+        # Single-qudit gate retargeting
+        LogPass('Retargeting single-qudit gates.'),
+        single_qudit_gate_rebase,
+
+        # Optimization: Iterative gate deletion on blocks
+        WhileLoopPass(
+            ChangePredicate(),
+            [
+                LogPass('Attempting to delete gates.'),
+                QuickPartitioner(max_synthesis_size),
+                QuickPartitioner(error_sim_size),
+                ForEachBlockPass(
+                    ForEachBlockPass(scan),
+                    calculate_error_bound=(error_threshold is not None),
+                ),
+                UnfoldPass(),
+            ],
+        ),
+
+        # Finalizing
+        LogErrorPass(),
+        ApplyPlacement(),
+    ]
+
+
+def _opt4_workflow(
+    circuit: Circuit,
+    model: MachineModel,
+    synthesis_epsilon: float = 1e-10,
+    max_synthesis_size: int = 4,
+    error_threshold: float | None = None,
+    error_sim_size: int = 8,
+) -> list[BasePass]:
+    """Build optimization Level 4 workflow for circuit compilation."""
+    # TODO
+    raise NotImplementedError('Optimization level 4 is not yet ready.')
+
+
+def _stateprep_workflow(
+    state: StateVector,
+    model: MachineModel,
+    optimization_level: int = 1,
+    synthesis_epsilon: float = 1e-10,
+    max_synthesis_size: int = 4,
+    error_threshold: float | None = None,
+    error_sim_size: int = 8,
+) -> CompilationTask:
+    """Build a workflow for state preparation."""
+    # TODO
+    raise NotImplementedError('State preparation is not yet implemented.')
+
+
+def _get_layer_gen(model: MachineModel) -> LayerGenerator:
+    """Build a `model`-compliant layer generator."""
+    tq_gates = [gate for gate in model.gate_set if gate.num_qudits == 2]
+    mq_gates = [gate for gate in model.gate_set if gate.num_qudits > 2]
+
+    if len(tq_gates) == 1 and len(mq_gates) == 0:
+        if CNOTGate() in tq_gates:
+            return FourParamGenerator()
+        else:
+            return SimpleLayerGenerator(tq_gates[0])
+
+    return WideLayerGenerator(tq_gates + mq_gates)
+
+
+def _get_single_qudit_gate_rebase_pass(model: MachineModel) -> BasePass:
+    """Build a pass to convert single-qudit-gates to the native gate set."""
+    sq_gates = [g for g in model.gate_set if g.num_qudits == 1]
+
+    if len(sq_gates) == 0:
+        return NOOPPass()
+
+    if all(g.is_constant() for g in sq_gates):
+        _logger.warn(
+            'The standard workflow with BQSKit may have trouble'
+            ' targeting gate sets containing no parameterized'
+            ' single-qudit gates.',
+        )
+        # TODO: Implement better retargeting techniques for constant gate sets
+
+    instantiate_options = {
+        'method': 'minimization',
+        'minimizer': ScipyMinimizer(),
+        'cost_fn_gen': HilbertSchmidtCostGenerator(),
+    }
+    layer_generator = SingleQuditLayerGenerator(sq_gates, True)
+    core_sq_rebase: BasePass = NOOPPass()
+    if len(sq_gates) == 1:
+        if sq_gates[0] == U3Gate():
+            core_sq_rebase = U3Decomposition()
+
+    elif len(sq_gates) >= 2:
+        if RZGate() in sq_gates and SqrtXGate() in sq_gates:
+            core_sq_rebase = ZXZXZDecomposition()
+
+    if isinstance(core_sq_rebase, NOOPPass):
+        core_sq_rebase = QSearchSynthesisPass(
+            layer_generator=layer_generator,
+            heuristic_function=DijkstraHeuristic(),
+            instantiate_options=instantiate_options,
+        )
+
+    return PassGroup([
+        IfThenElsePass(
+            NotPredicate(SinglePhysicalPredicate()),
+            [
+                GroupSingleQuditGatePass(),
+                ForEachBlockPass([
+                    IfThenElsePass(
+                        NotPredicate(SinglePhysicalPredicate()),
+                        core_sq_rebase,
+                    ),
+                ]),
+                UnfoldPass(),
+            ],
+        ),
+    ])
+
+
+def _gen_replace_filter(
+    model: MachineModel,
+) -> Callable[[Circuit, Operation], bool]:
+    """Generate a replace filter for use during the standard workflow."""
+    def _replace_filter(new: Circuit, old: Operation) -> bool:
+        # return true if old doesn't satisfy model
+        if not isinstance(old.gate, CircuitGate):
+            return True
+
+        org = old.gate._circuit
+        org_mq_gates = [g for g in org.gate_set if g.num_qudits > 1]
+
+        if any(g not in model.gate_set for g in org_mq_gates):
+            return True
+
+        if any(
+            (old.location[e[0]], old.location[e[1]]) not in model.coupling_graph
+            for e in org.coupling_graph
+        ):
+            return True
+
+        # else pick shortest circuit
+        org_sq_n = sum(org.count(g) for g in org.gate_set if g.num_qudits == 1)
+        org_mq_n = sum(org.count(g) for g in org.gate_set if g.num_qudits >= 2)
+        new_sq_n = sum(new.count(g) for g in new.gate_set if g.num_qudits == 1)
+        new_mq_n = sum(new.count(g) for g in new.gate_set if g.num_qudits >= 2)
+        return (new_mq_n, new_sq_n) < (org_mq_n, org_sq_n)
+    return _replace_filter
+
+
+def _mq_gate_collection_filter(op: Operation) -> bool:
+    """Return true if `op` is a multi-qudit operation."""
+    return op.num_qudits > 1
diff --git a/bqskit/compiler/compiler.py b/bqskit/compiler/compiler.py
index b1542bfd3..560f90871 100644
--- a/bqskit/compiler/compiler.py
+++ b/bqskit/compiler/compiler.py
@@ -1,47 +1,63 @@
 """This module implements the Compiler class."""
 from __future__ import annotations
 
-import inspect
 import logging
+import sys
+import time
 import uuid
-from typing import Any
-from typing import Callable
+from subprocess import PIPE
+from subprocess import Popen
+from typing import TYPE_CHECKING
+
+if TYPE_CHECKING:
+    from typing import Any
+    from dask.distributed import Future
+    from bqskit.compiler.task import CompilationTask
+    from bqskit.ir.circuit import Circuit
 
 from dask.distributed import Client
-from dask.distributed import Future
 from threadpoolctl import threadpool_limits
 
 from bqskit.compiler.executor import Executor
-from bqskit.compiler.task import CompilationTask
-from bqskit.ir.circuit import Circuit
 
 _logger = logging.getLogger(__name__)
 
 
 class Compiler:
     """
-    The BQSKit compiler class.
-
     A compiler is responsible for accepting and managing compilation tasks.
-    The compiler class spins up a Dask execution environment, which
-    compilation tasks can then access to parallelize their operations.
+
+    The compiler class spins up or connects to a Dask execution environment,
+    which compilation tasks can then access to parallelize their operations.
     The compiler is implemented as a context manager and it is recommended
     to use it as one. If the compiler is not used in a context manager, it
     is the responsibility of the user to call `close()`.
 
-    You will need to wrap construction and all uses of a compiler object
-    in a `if __name__ == '__main__':` clause when calling from a script.
-
     Examples:
-        >>> if __name__ == '__main__':
-        ...     with Compiler() as compiler:
-        ...         circuit = compiler.compile(task)
+        1. Use in a context manager:
+        >>> with Compiler() as compiler:
+        ...     circuit = compiler.compile(task)
 
+        2. Use compiler without context manager:
         >>> compiler = Compiler()
         >>> circuit = compiler.compile(task)
         >>> compiler.close()
+
+        3. Connect to an already running dask cluster:
+        >>> with Compiler('localhost:8786') as compiler:
+        ...     circuit = compiler.compile(task)
+
+        4. Connect to a dask cluster with a scheduler file:
+        >>> with Compiler(scheduler_file='/path/to/file/') as compiler:
+        ...     circuit = compiler.compile(task)
     """
 
+    _parallelism_enabled = True
+    """Used to to limit default parallelism; see bqskit.disable_parallelism."""
+
+    _dashboard = False
+    """Used to enable the dask dashboard; see bqskit.enable_dashboard."""
+
     def __init__(self, *args: Any, **kwargs: Any) -> None:
         """
         Construct a Compiler object.
@@ -50,34 +66,25 @@ def __init__(self, *args: Any, **kwargs: Any) -> None:
             All arguments are passed directly to Dask. You can use
             these to connect to and configure a Dask cluster.
         """
-        # Determine if being run from a dask worker
-        for frame in inspect.stack():
-            if 'spawn.py' in frame[1]:
-                self.is_worker = True
-                raise RuntimeError(
-                    '\n \n'
-                    'Fatal Error: Creating compiler object outside of a '
-                    "\"if __name__ == '__main__':\" clause."
-                    '\n \n',
-                    # TODO: Link to documentation explaining more
-                )
-
         # Connect to or start a Dask cluster
-        self.client = Client(*args, **kwargs)
+        self.managed = len(args) == 0 and len(kwargs) == 0
+        if self.managed:
+            self.processes = start_dask_cluster()
+            self.client = Client('localhost:8786')
+        else:
+            self.client = Client(*args, **kwargs)
 
         # Initialize workers for logging support
-        def get_setup_fn() -> Callable[[], None]:
-            x = logging.getLogger('bqskit').level
+        def set_logging_level(level: int) -> None:
+            import bqskit  # noqa
+            logging.getLogger('bqskit').setLevel(level)
 
-            def setup_fn() -> None:
-                logging.getLogger('bqskit').setLevel(x)
-            return setup_fn
-        self.client.register_worker_callbacks(get_setup_fn())
+        self.client.run(set_logging_level, logging.getLogger('bqskit').level)
 
         # Limit threads for low level math calls on workers
         def limit_threads() -> None:
             threadpool_limits(limits=1)
-        self.client.register_worker_callbacks(limit_threads)
+        self.client.run(limit_threads)
 
         # Keep track of submitted compilation tasks
         self.tasks: dict[uuid.UUID, Future] = {}
@@ -93,12 +100,19 @@ def __exit__(self, type: Any, value: Any, traceback: Any) -> None:
 
     def close(self) -> None:
         """Shutdown compiler."""
-        try:
-            self.client.close()
-            self.tasks = {}
-            _logger.info('Stopped compiler process.')
-        except (AttributeError, TypeError):
-            pass
+        self.client.close()
+        self.tasks = {}
+        if self.managed:
+            for proc in self.processes:
+                proc.terminate()  # type: ignore
+                proc.wait()  # type: ignore
+        _logger.info('Stopped compiler process.')
+
+    def __del__(self) -> None:
+        if self.managed and hasattr(self, 'processes'):
+            for proc in self.processes:
+                proc.terminate()  # type: ignore
+                proc.wait()  # type: ignore
 
     def submit(self, task: CompilationTask) -> None:
         """Submit a CompilationTask to the Compiler."""
@@ -133,3 +147,60 @@ def analyze(self, task: CompilationTask, key: str) -> Any:
         if task.task_id not in self.tasks:
             self.submit(task)
         return self.tasks[task.task_id].result()[1][key]
+
+
+def start_dask_cluster() -> tuple[Popen[bytes], Popen[str]]:
+    """Start a dask cluster and return the scheduler and worker processes."""
+    scheduler_proc: Any = None
+    worker_proc: Any = None
+
+    # Start a dask scheduler in another process
+    try:
+        dashboard = '--dashboard' if Compiler._dashboard else '--no-dashboard'
+        scheduler_proc = Popen(
+            [
+                'dask-scheduler',
+                '--host', 'localhost',
+                '--port', '8786',
+                dashboard,
+            ],
+            bufsize=1,
+            universal_newlines=True,
+            stdout=sys.stdout,
+            stderr=PIPE,
+        )
+        for line in iter(scheduler_proc.stderr.readline, ''):
+            if 'Scheduler at:' in line:
+                break
+        scheduler_proc.stderr.close()
+    except Exception as e:
+        raise RuntimeError('Failed to start dask scheduler') from e
+
+    # Start dask workers in another process
+    try:
+        workers = []
+        if Compiler._parallelism_enabled:
+            workers.append('auto')
+        else:
+            workers.append('1')
+            workers.append('--nthreads')
+            workers.append('1')
+
+        worker_proc = Popen(
+            ['dask-worker', '--nworkers', *workers, 'localhost:8786'],
+            bufsize=1,
+            universal_newlines=True,
+            stdout=sys.stdout,
+            stderr=PIPE,
+        )
+        for line in iter(worker_proc.stderr.readline, ''):
+            if 'Starting established connection' in line:
+                break
+        worker_proc.stderr.close()
+    except Exception as e:
+        scheduler_proc.terminate()
+        scheduler_proc.wait()
+        raise RuntimeError('Failed to start dask worker') from e
+
+    time.sleep(1)
+    return worker_proc, scheduler_proc
diff --git a/bqskit/compiler/executor.py b/bqskit/compiler/executor.py
index 4f5ef8f36..7e726bffb 100644
--- a/bqskit/compiler/executor.py
+++ b/bqskit/compiler/executor.py
@@ -1,10 +1,12 @@
 """This module implements the Executor class."""
 from __future__ import annotations
 
-from typing import Any
+from typing import TYPE_CHECKING
 
 from bqskit.compiler.task import CompilationTask
-from bqskit.ir.circuit import Circuit
+if TYPE_CHECKING:
+    from typing import Any
+    from bqskit.ir.circuit import Circuit
 
 
 class Executor:
diff --git a/bqskit/compiler/machine.py b/bqskit/compiler/machine.py
index 0f13b0eb8..7bf84ef75 100644
--- a/bqskit/compiler/machine.py
+++ b/bqskit/compiler/machine.py
@@ -1,19 +1,34 @@
 """This module implements the MachineModel class."""
 from __future__ import annotations
 
+from typing import Sequence
+
+from bqskit.ir.circuit import Circuit
+from bqskit.ir.gate import Gate
+from bqskit.ir.gates.constant.cx import CNOTGate
+from bqskit.ir.gates.parameterized.u3 import U3Gate
 from bqskit.ir.location import CircuitLocation
 from bqskit.qis.graph import CouplingGraph
 from bqskit.qis.graph import CouplingGraphLike
 from bqskit.utils.typing import is_integer
+from bqskit.utils.typing import is_valid_radixes
+
+
+default_gate_set: set[Gate] = {
+    CNOTGate(),
+    U3Gate(),
+}
 
 
 class MachineModel:
-    """A model of a quantum processing unit's connectivity."""
+    """A model of a quantum processing unit."""
 
     def __init__(
         self,
         num_qudits: int,
         coupling_graph: CouplingGraphLike | None = None,
+        gate_set: set[Gate] = default_gate_set,
+        radixes: Sequence[int] = [],
     ) -> None:
         """
         MachineModel Constructor.
@@ -27,8 +42,19 @@ def __init__(
                 an all-to-all coupling graph is used as a default.
                 (Default: None)
 
+            gate_set (set[Gate]): The native gate set available on the
+                machine.
+
+            radixes (Sequence[int]): A sequence with its length equal
+                to `num_qudits`. Each element specifies the base of a
+                qudit. Defaults to qubits.
+
         Raises:
             ValueError: If `num_qudits` is nonpositive.
+
+        Note:
+            Pre-built models for many active QPUs exist in the `bqskit.ext`
+            package.
         """
 
         if not is_integer(num_qudits):
@@ -39,17 +65,51 @@ def __init__(
         if num_qudits <= 0:
             raise ValueError(f'Expected positive num_qudits, got {num_qudits}.')
 
+        self.radixes = tuple(radixes if len(radixes) > 0 else [2] * num_qudits)
+
+        if not is_valid_radixes(self.radixes):
+            raise TypeError('Invalid qudit radixes.')
+
+        if len(self.radixes) != num_qudits:
+            raise ValueError(
+                'Expected length of radixes to be equal to num_qudits:'
+                ' %d != %d' % (len(self.radixes), num_qudits),
+            )
+
         if coupling_graph is None:
             coupling_graph = CouplingGraph.all_to_all(num_qudits)
 
         if not CouplingGraph.is_valid_coupling_graph(
                 coupling_graph, num_qudits,
         ):
-            raise TypeError('Invalid coupling graph.')
+            raise TypeError('Invalid coupling graph, expected list of tuples')
+
+        if not isinstance(gate_set, set):
+            raise TypeError(f'Expected set of gates, got {type(gate_set)}.')
 
+        if not all(isinstance(g, Gate) for g in gate_set):
+            raise TypeError(f'Expected set of gates, got {type(gate_set)}.')
+
+        self.gate_set = gate_set
         self.coupling_graph = CouplingGraph(coupling_graph)
         self.num_qudits = num_qudits
 
     def get_locations(self, block_size: int) -> list[CircuitLocation]:
         """Return all `block_size` connected blocks of qudit indicies."""
         return self.coupling_graph.get_subgraphs_of_size(block_size)
+
+    def is_compatible(self, circuit: Circuit) -> bool:
+        """Check if a circuit is compatible with this model."""
+        if circuit.num_qudits > self.num_qudits:
+            return False
+
+        if any(g not in self.gate_set for g in circuit.gate_set):
+            return False
+
+        if any(e not in self.coupling_graph for e in circuit.coupling_graph):
+            return False
+
+        if any(r != self.radixes[i] for i, r in enumerate(circuit.radixes)):
+            return False
+
+        return True
diff --git a/bqskit/compiler/task.py b/bqskit/compiler/task.py
index 93600eecc..06da28720 100644
--- a/bqskit/compiler/task.py
+++ b/bqskit/compiler/task.py
@@ -3,12 +3,13 @@
 
 import logging
 import uuid
+import warnings
 from typing import Sequence
 
 from bqskit.compiler.basepass import BasePass
 from bqskit.ir.circuit import Circuit
-from bqskit.ir.gates import CircuitGate
 from bqskit.ir.gates import CNOTGate
+from bqskit.ir.gates.circuitgate import CircuitGate
 from bqskit.ir.operation import Operation
 from bqskit.passes.control import ForEachBlockPass
 from bqskit.passes.control.predicates.count import GateCountPredicate
@@ -28,10 +29,8 @@ class CompilationTask():
     """
     A complete description of a quantum compilation task.
 
-    The CompilationTask class describes a compilation workflow completely.
-    These can be submitted to a BQSKit compiler to be efficiently executed.
-
-    There are static constructors for the most common use cases.
+    The CompilationTask class describes a compilation workflow completely. These
+    can be submitted to a BQSKit compiler to be efficiently executed.
     """
 
     def __init__(self, input: Circuit, passes: Sequence[BasePass]) -> None:
@@ -51,6 +50,10 @@ def __init__(self, input: Circuit, passes: Sequence[BasePass]) -> None:
     @staticmethod
     def synthesize(utry: UnitaryLike) -> CompilationTask:
         """Produces a standard synthesis task for the given unitary."""
+        warnings.warn(
+            'Default task creation is deprecated and will soon be removed.\n'
+            'Instead, use the new compile function.',
+        )
         circuit = Circuit.from_unitary(utry)
         num_qudits = circuit.num_qudits
 
@@ -80,6 +83,10 @@ def synthesize(utry: UnitaryLike) -> CompilationTask:
     @staticmethod
     def optimize(circuit: Circuit) -> CompilationTask:
         """Produces a standard optimization task for the given circuit."""
+        warnings.warn(
+            'Default task creation is deprecated and will soon be removed.\n'
+            'Instead, use the new compile function.',
+        )
         num_qudits = circuit.num_qudits
 
         if num_qudits <= 3:
diff --git a/bqskit/exec/runners/quest.py b/bqskit/exec/runners/quest.py
index 79840d4dd..af1f8e6e4 100644
--- a/bqskit/exec/runners/quest.py
+++ b/bqskit/exec/runners/quest.py
@@ -20,7 +20,7 @@
 from bqskit.ir.operation import Operation
 from bqskit.ir.point import CircuitPoint
 from bqskit.passes.control import ForEachBlockPass
-from bqskit.passes.layout import SimpleLayoutPass
+from bqskit.passes.mapping.setmodel import SetModelPass
 from bqskit.passes.partitioning import QuickPartitioner
 from bqskit.passes.synthesis import LEAPSynthesisPass
 
@@ -102,7 +102,7 @@ def run(self, circuit: Circuit) -> RunnerResults:
             ForEachBlockPass(synthesis_pass),
         ]
         if self.model is not None:
-            pass_list.insert(0, SimpleLayoutPass(self.model))
+            pass_list.insert(0, SetModelPass(self.model))
 
         task = CompilationTask(circuit.copy(), pass_list)
 
@@ -296,7 +296,7 @@ def annealing_objective(x: npt.NDArray[np.float64], *args: Any) -> float:
     distances = [0] * len(psol_configs)
 
     for index, config in enumerate(psol_configs):
-        distances[index] = sum(  # type: ignore
+        distances[index] = sum(
             utrys[i][config[i]].get_distance_from(utrys[i][b])
             < max(dists[i][config[i]], dists[i][b])
             for i, b in enumerate(blocks)
@@ -356,7 +356,7 @@ def gen_approximate_circuits(
         ForEachBlockPass(synthesis_pass),
     ]
     if model is not None:
-        pass_list.insert(0, SimpleLayoutPass(model))
+        pass_list.insert(0, SetModelPass(model))
 
     task = CompilationTask(circuit.copy(), pass_list)
 
diff --git a/bqskit/exec/runners/sim.py b/bqskit/exec/runners/sim.py
index 14c589cec..4671708b5 100644
--- a/bqskit/exec/runners/sim.py
+++ b/bqskit/exec/runners/sim.py
@@ -1,5 +1,8 @@
 from __future__ import annotations
 
+from typing import cast
+from typing import Sequence
+
 import numpy as np
 
 from bqskit.exec.results import RunnerResults
@@ -14,4 +17,5 @@ def run(self, circuit: Circuit) -> RunnerResults:
         """Execute the circuit, see CircuitRunner.run for more info."""
         state = np.reshape(circuit.get_unitary()[:, 0], (-1))
         probs = np.square(np.abs(state), dtype=np.float64)
-        return RunnerResults(circuit.num_qudits, circuit.radixes, probs)  # type: ignore # noqa
+        probs = cast(Sequence[float], probs)
+        return RunnerResults(circuit.num_qudits, circuit.radixes, probs)
diff --git a/bqskit/ext/__init__.py b/bqskit/ext/__init__.py
new file mode 100644
index 000000000..038b3d1c6
--- /dev/null
+++ b/bqskit/ext/__init__.py
@@ -0,0 +1,68 @@
+"""
+=============================================================
+BQSKit Extensions (:mod:`bqskit.ext`)
+=============================================================
+
+.. currentmodule:: bqskit.ext
+
+This subpackage provides integrations with other popular frameworks
+and pre-built MachineModels for many QPUs.
+"""
+from __future__ import annotations
+
+try:
+    import cirq  # noqa
+    import pytket  # noqa
+    import qiskit  # noqa
+    import qutip  # noqa
+except ImportError as e:
+    raise ImportError(
+        '\n\nUnable to import bqskit.ext package.\n'
+        'Ensure that bqskit was installed with extension support.\n'
+        'To install bqskit with extension support try:\n'
+        '\tpip install bqskit[ext]\n',
+    ) from e
+
+from bqskit.ext.cirq.translate import bqskit_to_cirq
+from bqskit.ext.cirq.translate import cirq_to_bqskit
+from bqskit.ext.pytket.translate import bqskit_to_pytket
+from bqskit.ext.pytket.translate import pytket_to_bqskit
+from bqskit.ext.qiskit.models import model_from_backend
+from bqskit.ext.qiskit.translate import bqskit_to_qiskit
+from bqskit.ext.qiskit.translate import qiskit_to_bqskit
+from bqskit.ext.qutip.translate import bqskit_to_qutip
+from bqskit.ext.qutip.translate import qutip_to_bqskit
+from bqskit.ext.supermarq import supermarq_program_communication
+from bqskit.ext.supermarq import supermarq_critical_depth
+from bqskit.ext.supermarq import supermarq_entanglement_ratio
+from bqskit.ext.supermarq import supermarq_parallelism
+from bqskit.ext.supermarq import supermarq_liveness
+from bqskit.ext.rigetti import Aspen11Model
+from bqskit.ext.rigetti import AspenM2Model
+from bqskit.ext.honeywell import H1_1Model
+from bqskit.ext.honeywell import H1_2Model
+from bqskit.ext.cirq.models import Sycamore23Model, SycamoreModel
+
+
+__all__ = [
+    'bqskit_to_cirq',
+    'cirq_to_bqskit',
+    'bqskit_to_pytket',
+    'pytket_to_bqskit',
+    'model_from_backend',
+    'bqskit_to_qiskit',
+    'qiskit_to_bqskit',
+    'bqskit_to_qutip',
+    'qutip_to_bqskit',
+    'supermarq_program_communication',
+    'supermarq_critical_depth',
+    'supermarq_entanglement_ratio',
+    'supermarq_parallelism',
+    'supermarq_liveness',
+    'Aspen11Model',
+    'AspenM2Model',
+    'H1_1Model',
+    'H1_2Model',
+    'Sycamore23Model',
+    'SycamoreModel',
+]
diff --git a/bqskit/ext/cirq/__init__.py b/bqskit/ext/cirq/__init__.py
new file mode 100644
index 000000000..aad787dba
--- /dev/null
+++ b/bqskit/ext/cirq/__init__.py
@@ -0,0 +1,7 @@
+"""This package contains integrations with the Cirq framework."""
+from __future__ import annotations
+
+from bqskit.ext.cirq.translate import bqskit_to_cirq
+from bqskit.ext.cirq.translate import cirq_to_bqskit
+
+__all__ = ['bqskit_to_cirq', 'cirq_to_bqskit']
diff --git a/bqskit/ext/cirq/models.py b/bqskit/ext/cirq/models.py
new file mode 100644
index 000000000..995431136
--- /dev/null
+++ b/bqskit/ext/cirq/models.py
@@ -0,0 +1,39 @@
+"""This module implements pre-built models for Google's QPUs."""
+from __future__ import annotations
+
+import cirq_google
+
+from bqskit.compiler.machine import MachineModel
+from bqskit.ir.gate import Gate
+from bqskit.ir.gates import CZGate
+from bqskit.ir.gates import PhasedXZGate
+from bqskit.ir.gates import RZGate
+from bqskit.ir.gates import SqrtISwapGate
+from bqskit.ir.gates import SycamoreGate
+from bqskit.qis.graph import CouplingGraph
+
+google_gate_set: set[Gate] = {
+    PhasedXZGate(),
+    RZGate(),
+    SycamoreGate(),
+    CZGate(),
+    SqrtISwapGate(),
+}
+
+# Sycamore Device
+_qubits = cirq_google.Sycamore.metadata.qubit_set
+_qubit_map = {q: i for i, q in enumerate(list(_qubits))}
+_pairs = cirq_google.Sycamore.metadata.qubit_pairs
+_edges = [(_qubit_map[q1], _qubit_map[q2]) for q1, q2 in _pairs]
+_sycamore_coupling_graph = CouplingGraph(_edges)
+SycamoreModel = MachineModel(54, _sycamore_coupling_graph, google_gate_set)
+
+# Sycamore23 Device
+_qubits = cirq_google.Sycamore23.metadata.qubit_set
+_qubit_map = {q: i for i, q in enumerate(list(_qubits))}
+_pairs = cirq_google.Sycamore23.metadata.qubit_pairs
+_edges = [(_qubit_map[q1], _qubit_map[q2]) for q1, q2 in _pairs]
+_sycamore23_coupling_graph = CouplingGraph(_edges)
+Sycamore23Model = MachineModel(23, _sycamore23_coupling_graph, google_gate_set)
+
+__all__ = ['SycamoreModel', 'Sycamore23Model']
diff --git a/bqskit/ext/cirq/translate.py b/bqskit/ext/cirq/translate.py
new file mode 100644
index 000000000..a249c77e2
--- /dev/null
+++ b/bqskit/ext/cirq/translate.py
@@ -0,0 +1,19 @@
+"""This module implements functions for translating to and from Cirq."""
+from __future__ import annotations
+
+import cirq
+from cirq.contrib.qasm_import import circuit_from_qasm
+
+from bqskit.ir.circuit import Circuit
+from bqskit.ir.lang.qasm2 import OPENQASM2Language
+
+
+def cirq_to_bqskit(cc: cirq.Circuit) -> Circuit:
+    """Convert Cirq's Circuit `cc` to a BQSKit Circuit."""
+    circuit = OPENQASM2Language().decode(cirq.qasm(cc))
+    return circuit
+
+
+def bqskit_to_cirq(circuit: Circuit) -> cirq.Circuit:
+    """Convert a BQSKit Circuit to Cirq's Circuit."""
+    return circuit_from_qasm(OPENQASM2Language().encode(circuit))
diff --git a/bqskit/ext/honeywell.py b/bqskit/ext/honeywell.py
new file mode 100644
index 000000000..b5ad28ac4
--- /dev/null
+++ b/bqskit/ext/honeywell.py
@@ -0,0 +1,14 @@
+"""This module implemenets Honeywell QPU models."""
+from __future__ import annotations
+
+from bqskit.compiler.machine import MachineModel
+from bqskit.ir.gate import Gate
+from bqskit.ir.gates import RZGate
+from bqskit.ir.gates import ZZGate
+from bqskit.ir.gates.parameterized.u1q import U1qPi2Gate
+from bqskit.ir.gates.parameterized.u1q import U1qPiGate
+
+honeywell_gate_set: set[Gate] = {U1qPiGate, U1qPi2Gate, RZGate(), ZZGate()}
+
+H1_1Model = MachineModel(20, None, honeywell_gate_set)
+H1_2Model = MachineModel(12, None, honeywell_gate_set)
diff --git a/bqskit/ext/pytket/__init__.py b/bqskit/ext/pytket/__init__.py
new file mode 100644
index 000000000..573c7587f
--- /dev/null
+++ b/bqskit/ext/pytket/__init__.py
@@ -0,0 +1,7 @@
+"""This package contains integrations with the PyTKet framework."""
+from __future__ import annotations
+
+from bqskit.ext.pytket.translate import bqskit_to_pytket
+from bqskit.ext.pytket.translate import pytket_to_bqskit
+
+__all__ = ['bqskit_to_pytket', 'pytket_to_bqskit']
diff --git a/bqskit/ext/pytket/translate.py b/bqskit/ext/pytket/translate.py
new file mode 100644
index 000000000..a86cc3021
--- /dev/null
+++ b/bqskit/ext/pytket/translate.py
@@ -0,0 +1,20 @@
+"""This module implements functions for translating to and from PyTKet."""
+from __future__ import annotations
+
+import pytket
+from pytket.qasm import circuit_from_qasm_str
+from pytket.qasm import circuit_to_qasm_str
+
+from bqskit.ir.circuit import Circuit
+from bqskit.ir.lang.qasm2 import OPENQASM2Language
+
+
+def pytket_to_bqskit(qc: pytket.Circuit) -> Circuit:
+    """Convert PyTKet's Circuit `cc` to a BQSKit Circuit."""
+    circuit = OPENQASM2Language().decode(circuit_to_qasm_str(qc))
+    return circuit
+
+
+def bqskit_to_pytket(circuit: Circuit) -> pytket.Circuit:
+    """Convert a BQSKit Circuit to a PyTKet Circuit."""
+    return circuit_from_qasm_str(OPENQASM2Language().encode(circuit))
diff --git a/bqskit/ext/qiskit/__init__.py b/bqskit/ext/qiskit/__init__.py
new file mode 100644
index 000000000..4f778cd11
--- /dev/null
+++ b/bqskit/ext/qiskit/__init__.py
@@ -0,0 +1,8 @@
+"""This package contains integrations with the Qiskit framework."""
+from __future__ import annotations
+
+from bqskit.ext.qiskit.models import model_from_backend
+from bqskit.ext.qiskit.translate import bqskit_to_qiskit
+from bqskit.ext.qiskit.translate import qiskit_to_bqskit
+
+__all__ = ['bqskit_to_qiskit', 'qiskit_to_bqskit', 'model_from_backend']
diff --git a/bqskit/ext/qiskit/models.py b/bqskit/ext/qiskit/models.py
new file mode 100644
index 000000000..13c30a081
--- /dev/null
+++ b/bqskit/ext/qiskit/models.py
@@ -0,0 +1,50 @@
+"""This module implements functions for working with IBM Backends."""
+from __future__ import annotations
+
+from qiskit.providers import BackendV1
+
+from bqskit.compiler import MachineModel
+from bqskit.ir.gate import Gate
+from bqskit.ir.gates.constant.cx import CNOTGate
+from bqskit.ir.gates.constant.cz import CZGate
+from bqskit.ir.gates.constant.sx import SXGate
+from bqskit.ir.gates.constant.x import XGate
+from bqskit.ir.gates.parameterized import RZGate
+from bqskit.ir.gates.parameterized.u1 import U1Gate
+from bqskit.ir.gates.parameterized.u2 import U2Gate
+from bqskit.ir.gates.parameterized.u3 import U3Gate
+
+
+def model_from_backend(backend: BackendV1) -> MachineModel:
+    """Create a machine model for a IBM Backend."""
+    config = backend.configuration()
+    num_qudits = config.n_qubits
+    gate_set = _basis_gate_str_to_bqskit_gate(config.basis_gates)
+    coupling_map = list({tuple(sorted(e)) for e in config.coupling_map})
+    return MachineModel(num_qudits, coupling_map, gate_set)  # type: ignore
+
+
+def _basis_gate_str_to_bqskit_gate(basis_gates: list[str]) -> set[Gate]:
+    gate_set: set[Gate] = set()
+    if len(basis_gates) > 10:
+        return {CNOTGate(), RZGate(), SXGate()}
+    for basis_gate in basis_gates:
+        if basis_gate == 'cx':
+            gate_set.add(CNOTGate())
+        elif basis_gate == 'cz':
+            gate_set.add(CZGate())
+        elif basis_gate == 'u3':
+            gate_set.add(U3Gate())
+        elif basis_gate == 'u2':
+            gate_set.add(U2Gate())
+        elif basis_gate == 'u1':
+            gate_set.add(U1Gate())
+        elif basis_gate == 'rz':
+            gate_set.add(RZGate())
+        elif basis_gate == 'x':
+            gate_set.add(XGate())
+        elif basis_gate == 'sx':
+            gate_set.add(SXGate())
+        elif basis_gate == 'p':
+            gate_set.add(RZGate())
+    return gate_set
diff --git a/bqskit/ext/qiskit/translate.py b/bqskit/ext/qiskit/translate.py
new file mode 100644
index 000000000..c076038fd
--- /dev/null
+++ b/bqskit/ext/qiskit/translate.py
@@ -0,0 +1,22 @@
+"""This module implements functions for translating to and from Qiskit."""
+from __future__ import annotations
+
+from qiskit import QuantumCircuit
+
+from bqskit.ir.circuit import Circuit
+from bqskit.ir.lang.qasm2 import OPENQASM2Language
+
+
+def qiskit_to_bqskit(qc: QuantumCircuit) -> Circuit:
+    """Convert Qiskit's QuantumCircuit `qc` to a BQSKit Circuit."""
+    circuit = OPENQASM2Language().decode(qc.qasm())
+    # circuit.renumber_qudits(list(reversed(range(circuit.num_qudits))))
+    return circuit
+    # TODO: support gates not captured by qasm
+
+
+def bqskit_to_qiskit(circuit: Circuit) -> QuantumCircuit:
+    """Convert a BQSKit Circuit to Qiskit's QuantumCircuit."""
+    # circuit.renumber_qudits(list(reversed(range(circuit.num_qudits))))
+    return QuantumCircuit.from_qasm_str(OPENQASM2Language().encode(circuit))
+    # TODO: support gates not captured by qasm
diff --git a/bqskit/ext/qutip/__init__.py b/bqskit/ext/qutip/__init__.py
new file mode 100644
index 000000000..382498249
--- /dev/null
+++ b/bqskit/ext/qutip/__init__.py
@@ -0,0 +1,7 @@
+"""This package contains integrations with the QuTiP framework."""
+from __future__ import annotations
+
+from bqskit.ext.qutip.translate import bqskit_to_qutip
+from bqskit.ext.qutip.translate import qutip_to_bqskit
+
+__all__ = ['bqskit_to_qutip', 'qutip_to_bqskit']
diff --git a/bqskit/ext/qutip/translate.py b/bqskit/ext/qutip/translate.py
new file mode 100644
index 000000000..c9c1506b3
--- /dev/null
+++ b/bqskit/ext/qutip/translate.py
@@ -0,0 +1,20 @@
+"""This module implements functions for translating to and from QuTiP."""
+from __future__ import annotations
+
+from qutip import QubitCircuit
+from qutip.qip.qasm import circuit_to_qasm_str
+from qutip.qip.qasm import read_qasm
+
+from bqskit.ir.circuit import Circuit
+from bqskit.ir.lang.qasm2 import OPENQASM2Language
+
+
+def qutip_to_bqskit(qc: QubitCircuit) -> Circuit:
+    """Convert QuTiP's QubitCircuit `qc` to a BQSKit Circuit."""
+    circuit = OPENQASM2Language().decode(circuit_to_qasm_str(qc))
+    return circuit
+
+
+def bqskit_to_qutip(circuit: Circuit) -> QubitCircuit:
+    """Convert a BQSKit Circuit to QuTiP's QubitCircuit."""
+    return read_qasm(OPENQASM2Language().encode(circuit), strmode=True)
diff --git a/bqskit/ext/rigetti.py b/bqskit/ext/rigetti.py
new file mode 100644
index 000000000..02daf9c99
--- /dev/null
+++ b/bqskit/ext/rigetti.py
@@ -0,0 +1,88 @@
+"""This module implements MachineModels for Rigetti's QPUs."""
+from __future__ import annotations
+
+from bqskit.compiler.machine import MachineModel
+from bqskit.ir.gates import CZGate
+from bqskit.ir.gates import RZGate
+from bqskit.ir.gates import SXGate
+from bqskit.ir.gates import XGate
+from bqskit.qis.graph import CouplingGraph
+
+rigetti_gate_set = {SXGate(), XGate(), RZGate(), CZGate()}
+
+_aspen_11_coupling_graph = CouplingGraph([
+    # Ring 1
+    (0, 1), (1, 2), (2, 3), (3, 4),
+    (4, 5), (5, 6), (6, 7), (7, 0),
+
+    # Ring 2
+    (8, 9), (9, 10), (10, 11), (11, 12),
+    (12, 13), (13, 14), (14, 15), (15, 8),
+
+    # Ring 3
+    (16, 17), (17, 18), (19, 20),
+    (20, 21), (21, 22), (22, 23), (23, 16),
+
+    # Ring 4
+    (24, 25), (25, 26), (26, 27), (27, 28),
+    (28, 29), (29, 30), (30, 31), (31, 24),
+
+    # Ring 5
+    (34, 35), (35, 36),
+    (36, 37), (37, 38), (38, 39),
+
+    # Ring 1-2
+    (1, 14), (2, 13),
+
+    # Ring 2-3
+    (9, 22), (10, 21),
+
+    # Ring 3-4
+    (17, 30),
+
+    # Ring 4-5
+    (25, 38), (26, 37),
+])
+"""Retrieved August 31, 2022: https://qcs.rigetti.com/qpus"""
+
+_octo_rings = [
+    [
+        (i + 0, i + 1), (i + 1, i + 2), (i + 2, i + 3), (i + 3, i + 4),
+        (i + 4, i + 5), (i + 5, i + 6), (i + 6, i + 7), (i + 7, i + 0),
+    ]
+    for i in range(0, 10 * 8, 8)
+]
+
+_horizontal_connections = [
+    [
+        (i + 1, j + 7),
+        (i + 2, j + 6),
+    ]
+    for i, j in [(8 * r, 8 * (r + 1)) for r in [0, 1, 2, 3, 5, 6, 7, 8]]
+]
+
+_vertical_connections = [
+    [
+        (i + 0, j + 3),
+        (i + 7, j + 4),
+    ]
+    for i, j in [(8 * r, 8 * (r + 5)) for r in range(5)]
+]
+_links = []
+for l in _octo_rings:
+    _links.extend(l)
+for l in _horizontal_connections:
+    _links.extend(l)
+for l in _vertical_connections:
+    _links.extend(l)
+
+_aspen_m2_coupling_graph = CouplingGraph(_links)
+"""Retrieved August 31, 2022: https://qcs.rigetti.com/qpus"""
+
+Aspen11Model = MachineModel(40, _aspen_11_coupling_graph, rigetti_gate_set)
+"""A BQSKit MachineModel for Rigetti's Aspen-11 quantum processor."""
+
+AspenM2Model = MachineModel(80, _aspen_m2_coupling_graph, rigetti_gate_set)
+"""A BQSKit MachineModel for Rigetti's Aspen-M-2 quantum processor."""
+
+__all__ = ['Aspen11Model', 'AspenM2Model']
diff --git a/bqskit/ext/supermarq.py b/bqskit/ext/supermarq.py
new file mode 100644
index 000000000..2eb3862be
--- /dev/null
+++ b/bqskit/ext/supermarq.py
@@ -0,0 +1,60 @@
+"""
+This module implements circuit metrics used in the Supermarq benchmark suite.
+
+References:
+    Tomesh, Teague, et al. "Supermarq: A scalable quantum benchmark suite."
+    2022 IEEE International Symposium on High-Performance Computer
+    Architecture (HPCA). IEEE, 2022.
+"""
+from __future__ import annotations
+
+import numpy as np
+
+from bqskit.ir.circuit import Circuit
+
+
+def supermarq_program_communication(circuit: Circuit) -> float:
+    """SupermarQ program communication metric."""
+    n = circuit.num_qudits
+    degrees_sum = sum(circuit.coupling_graph.get_qudit_degrees())
+    return degrees_sum / (n * (n - 1))
+
+
+def supermarq_critical_depth(circuit: Circuit) -> float:
+    """SupermarQ critical depth metric."""
+    qudit_depths = np.zeros(circuit.num_qudits, dtype=int)
+    num_multi_qubit_gates = 0
+    for op in circuit:
+        if len(op.location) > 1:
+            new_depth = max(qudit_depths[list(op.location)]) + 1
+            qudit_depths[list(op.location)] = new_depth
+            num_multi_qubit_gates += 1
+    return int(max(qudit_depths)) / num_multi_qubit_gates
+
+
+def supermarq_entanglement_ratio(circuit: Circuit) -> float:
+    """SupermarQ entanglement-ratio metric."""
+    num_multi_qubit_gates = 0
+    num_gates = 0
+    for op in circuit:
+        num_gates += 1
+        if len(op.location) > 1:
+            num_multi_qubit_gates += 1
+    return num_multi_qubit_gates / num_gates
+
+
+def supermarq_parallelism(circuit: Circuit) -> float:
+    """SupermarQ parallelism metric."""
+    d = circuit.depth
+    ng = circuit.num_operations
+    return (ng / d - 1) / (circuit.num_qudits - 1)
+
+
+def supermarq_liveness(circuit: Circuit) -> float:
+    """SupermarQ liveness metric."""
+    liveness_sum = 0
+    for i in range(circuit.num_qudits):
+        for j in range(circuit.num_cycles):
+            if circuit._circuit[j][i] is not None:
+                liveness_sum += 1
+    return liveness_sum / (circuit.num_qudits * circuit.depth)
diff --git a/bqskit/ir/__init__.py b/bqskit/ir/__init__.py
index 36ffd7c91..10d0e4342 100644
--- a/bqskit/ir/__init__.py
+++ b/bqskit/ir/__init__.py
@@ -10,9 +10,9 @@
 when operations are executed, and the qudit index determines which
 qudits an operation operates on.
 
-Every component of the IR is also a function from a vector
-real numbers to a unitary matrix. This is done to facilitate circuit
-instantiatation, which is a very important primitive in quantum synthesis.
+Every component of the IR is also a function from a vector of
+real numbers to a unitary matrix. This is to facilitate circuit
+instantiation, a core primitive in quantum synthesis.
 
 .. rubric:: Core Classes
 
@@ -48,12 +48,12 @@
     CircuitPointLike
     CircuitRegionLike
 
+.. rubric:: Gate Library
+
 .. automodule:: bqskit.ir.gates
    :no-members:
    :no-inherited-members:
    :no-special-members:
-
-TODO: More description
 """
 from __future__ import annotations
 
diff --git a/bqskit/ir/circuit.py b/bqskit/ir/circuit.py
index 52835ee82..293474b8e 100644
--- a/bqskit/ir/circuit.py
+++ b/bqskit/ir/circuit.py
@@ -6,9 +6,11 @@
 from typing import Any
 from typing import cast
 from typing import Collection
+from typing import Dict
 from typing import Iterable
 from typing import Iterator
 from typing import List
+from typing import Optional
 from typing import overload
 from typing import Sequence
 from typing import Set
@@ -23,8 +25,8 @@
 from bqskit.ir.gate import Gate
 from bqskit.ir.gates.circuitgate import CircuitGate
 from bqskit.ir.gates.composed.daggergate import DaggerGate
-from bqskit.ir.gates.composed.tagged import TaggedGate
 from bqskit.ir.gates.constant.unitary import ConstantUnitaryGate
+from bqskit.ir.gates.measure import MeasurementPlaceholder
 from bqskit.ir.iterator import CircuitIterator
 from bqskit.ir.lang import get_language
 from bqskit.ir.location import CircuitLocation
@@ -52,6 +54,7 @@
 from bqskit.qis.unitary.unitarymatrix import UnitaryLike
 from bqskit.qis.unitary.unitarymatrix import UnitaryMatrix
 from bqskit.utils.random import seed_random_sources
+from bqskit.utils.typing import is_bool
 from bqskit.utils.typing import is_integer
 from bqskit.utils.typing import is_iterable
 from bqskit.utils.typing import is_sequence_of_int
@@ -67,8 +70,6 @@
 
 class Circuit(DifferentiableUnitary, StateVectorMap, Collection[Operation]):
     """
-    Circuit class.
-
     A Circuit is a quantum program composed of operation objects.
 
     The operations are organized in 2-dimensions, and are indexed by
@@ -162,6 +163,12 @@ def __init__(self, num_qudits: int, radixes: Sequence[int] = []) -> None:
 
         self._circuit: list[list[Operation | None]] = []
         self._gate_info: dict[Gate, int] = {}
+        self._graph_info: dict[tuple[int, int], int] = {}
+
+        _NodePtrs = Dict[int, Optional[CircuitPoint]]
+        self._front: _NodePtrs = {i: None for i in range(self.num_qudits)}
+        self._rear: _NodePtrs = {i: None for i in range(self.num_qudits)}
+        self._dag: dict[CircuitPoint, tuple[_NodePtrs, _NodePtrs]] = {}
 
     # region Circuit Properties
 
@@ -221,37 +228,35 @@ def coupling_graph(self) -> CouplingGraph:
         The qudit connectivity in the circuit.
 
         Returns:
-            set[tuple[int, int]]: The coupling graph required by
-            the circuit. The graph is returned as an edge list.
+            (CouplingGraph): The coupling graph required by the circuit.
 
         Notes:
-            - Multi-qudit gates set participating qudits to have
-              all-to-all connectivity.
-
-            - The graph is undirected.
+            - Logical multi-qudit gates require participating qudits to
+                have all-to-all connectivity.
         """
-        coupling_graph = set()
-        for op in self:
-            for q1 in op.location:
-                for q2 in op.location:
-                    if q1 == q2:
-                        continue
-                    coupling_graph.add((min(q1, q2), max(q1, q2)))
-        return CouplingGraph(coupling_graph)
+        return CouplingGraph(self._graph_info.keys(), self.num_qudits)
 
     @property
     def gate_set(self) -> set[Gate]:
         """The set of gates in the circuit."""
         return set(self._gate_info.keys())
 
+    @property
+    def gate_counts(self) -> dict[Gate, int]:
+        """The count of each type of gate in the circuit."""
+        return {gate: self.count(gate) for gate in self.gate_set}
+
     @property
     def active_qudits(self) -> list[int]:
         """The qudits involved in at least one operation."""
-        return [
-            qudit
-            for qudit in range(self.num_qudits)
-            if not self.is_qudit_idle(qudit)
-        ]
+        active_qudits = set()  # TODO: add test case for single-qudit gates
+        for edge in self._graph_info.keys():
+            active_qudits.add(edge[0])
+            active_qudits.add(edge[1])
+        for qudit, point in self._front.items():
+            if point is not None:
+                active_qudits.add(qudit)
+        return list(sorted(active_qudits))
 
     def is_differentiable(self) -> bool:
         """Check if all gates are differentiable."""
@@ -287,6 +292,9 @@ def append_qudit(self, radix: int = 2) -> None:
         for cycle in self._circuit:
             cycle.append(None)
 
+        self._rear[self.num_qudits - 1] = None
+        self._front[self.num_qudits - 1] = None
+
     def extend_qudits(self, radixes: Iterable[int]) -> None:
         """
         Append many qudits to the circuit.
@@ -340,6 +348,12 @@ def insert_qudit(self, qudit_index: int, radix: int = 2) -> None:
         self._radixes = tuple(radix_list)
 
         # Insert qudit
+        shift_index = lambda q: q if q < qudit_index else q + 1
+        inc_point = lambda p: CircuitPoint(p.cycle, p.qudit + 1)
+        shift_point = lambda p: p if p.qudit < qudit_index else inc_point(p)
+        shift_point_or_none = lambda p: None if p is None else shift_point(p)
+        shift_edge = lambda e: (shift_index(e[0]), shift_index(e[1]))
+
         for cycle in self._circuit:
             cycle.insert(qudit_index, None)
 
@@ -348,12 +362,41 @@ def insert_qudit(self, qudit_index: int, radix: int = 2) -> None:
             for i, op in enumerate(cycle[qudit_index:]):
                 if op is None or i + qudit_index in qudits_to_skip:
                     continue
-                op._location = CircuitLocation([
-                    index if index < qudit_index else index + 1
-                    for index in op.location
-                ])
+                shifted_location = [shift_index(i) for i in op.location]
+                op._location = CircuitLocation(shifted_location)
                 qudits_to_skip.extend(op.location)
 
+        # Shift _front, _rear, _graph_info, _dag
+        self._front = {
+            shift_index(q): shift_point_or_none(p)
+            for q, p in self._front.items()
+        }
+        self._front[qudit_index] = None
+        self._rear = {
+            shift_index(q): shift_point_or_none(p)
+            for q, p in self._rear.items()
+        }
+        self._rear[qudit_index] = None
+        self._graph_info = {
+            shift_edge(e): i
+            for e, i in self._graph_info.items()
+        }
+        self._dag = {
+            shift_point(p): (
+                # Prev
+                {
+                    shift_index(q): shift_point_or_none(p2)
+                    for q, p2 in ptr_map[0].items()
+                },
+                # Next
+                {
+                    shift_index(q): shift_point_or_none(p2)
+                    for q, p2 in ptr_map[1].items()
+                },
+            )
+            for p, ptr_map in self._dag.items()
+        }
+
     def pop_qudit(self, qudit_index: int) -> None:
         """
         Pop a qudit from the circuit and all gates attached to it.
@@ -395,6 +438,12 @@ def pop_qudit(self, qudit_index: int) -> None:
         self._radixes = tuple(radix_list)
 
         # Remove qudit
+        shift_index = lambda q: q if q < qudit_index else q - 1
+        dec_point = lambda p: CircuitPoint(p.cycle, p.qudit - 1)
+        shift_point = lambda p: p if p.qudit < qudit_index else dec_point(p)
+        shift_point_or_none = lambda p: None if p is None else shift_point(p)
+        shift_edge = lambda e: (shift_index(e[0]), shift_index(e[1]))
+
         for cycle_index, cycle in enumerate(self._circuit):
             cycle.pop(qudit_index)
 
@@ -404,12 +453,41 @@ def pop_qudit(self, qudit_index: int) -> None:
             for i, op in enumerate(cycle[qudit_index:]):
                 if op is None or i + qudit_index in qudits_to_skip:
                     continue
-                op._location = CircuitLocation([
-                    index if index < qudit_index else index - 1
-                    for index in op.location
-                ])
+                shifted_location = [shift_index(i) for i in op.location]
+                op._location = CircuitLocation(shifted_location)
                 qudits_to_skip.extend(op.location)
 
+        # Shift _front, _rear, _graph_info, _dag
+        self._front = {
+            shift_index(q): shift_point_or_none(p)
+            for q, p in self._front.items()
+            if q != qudit_index
+        }
+        self._rear = {
+            shift_index(q): shift_point_or_none(p)
+            for q, p in self._rear.items()
+            if q != qudit_index
+        }
+        self._graph_info = {
+            shift_edge(e): i
+            for e, i in self._graph_info.items()
+        }
+        self._dag = {
+            shift_point(p): (
+                # Prev
+                {
+                    shift_index(q): shift_point_or_none(p2)
+                    for q, p2 in ptr_map[0].items()
+                },
+                # Next
+                {
+                    shift_index(q): shift_point_or_none(p2)
+                    for q, p2 in ptr_map[1].items()
+                },
+            )
+            for p, ptr_map in self._dag.items()
+        }
+
     def is_qudit_in_range(self, qudit_index: int) -> bool:
         """Return true if `qudit_index` is in-range for the circuit."""
 
@@ -431,7 +509,88 @@ def is_qudit_idle(self, qudit_index: int) -> bool:
                 f'Expected integer for qudit_index, got: {type(qudit_index)}',
             )
 
-        return all(cycle[qudit_index] is None for cycle in self._circuit)
+        return self._front[qudit_index] is None
+
+    def renumber_qudits(self, qudit_permutation: Sequence[int]) -> None:
+        """
+        Permute the qudits in the circuit.
+
+        Args:
+            qudit_permutation (Sequence[int]): A map from qudit indices
+                to qudit indices.
+
+        Raises:
+            IndexError: If any of the indices are out of range.
+
+            ValueError: If the `qudit_permutation` is not the same size
+                as the circuit.
+
+            ValueError: If the `qudit_permutation` is not a valid permutation.
+        """
+        if not is_sequence_of_int(qudit_permutation):
+            raise TypeError(
+                'Expected sequence of integers'
+                ', got %s' % type(qudit_permutation),
+            )
+
+        if len(qudit_permutation) != self.num_qudits:
+            raise ValueError(
+                'Expected qudit_permutation length equal to circuit num_qudits:'
+                '%d, got %d' % (self.num_qudits, len(qudit_permutation)),
+            )
+
+        if len(qudit_permutation) != len(set(qudit_permutation)):
+            raise ValueError('Invalid permutation.')
+
+        perm = [int(q) for q in qudit_permutation]
+
+        perm_point = lambda p: CircuitPoint(p.cycle, perm[p.qudit])
+        perm_point_or_none = lambda p: perm_point(p) if p is not None else p
+
+        self._graph_info = {
+            (perm[e[0]], perm[e[1]]): i
+            for e, i in self._graph_info.items()
+        }
+        self._front = {
+            perm[i]: perm_point_or_none(p)
+            for i, p in self._front.items()
+        }
+        self._rear = {
+            perm[i]: perm_point_or_none(p)
+            for i, p in self._rear.items()
+        }
+        self._dag = {
+            perm_point(p): (
+                # Prev
+                {
+                    perm[q]: perm_point_or_none(p2)
+                    for q, p2 in ptr_map[0].items()
+                },
+                # Next
+                {
+                    perm[q]: perm_point_or_none(p2)
+                    for q, p2 in ptr_map[1].items()
+                },
+            )
+            for p, ptr_map in self._dag.items()
+        }
+
+        for i in range(self.num_cycles):
+            self._circuit[i] = [
+                self._circuit[i][perm.index(q)]
+                for q in range(self.num_qudits)
+            ]
+
+            qudits_to_skip: list[int] = []
+            for j in range(self.num_qudits):
+                if j in qudits_to_skip:
+                    continue
+
+                op = self._circuit[i][j]
+                if op is not None:
+                    loc = CircuitLocation([perm[q] for q in op.location])
+                    op._location = loc
+                    qudits_to_skip.extend(loc)
 
     # endregion
 
@@ -444,6 +603,32 @@ def _append_cycle(self) -> None:
     def _insert_cycle(self, cycle_index: int) -> None:
         """Inserts an idle cycle in the circuit."""
         self._circuit.insert(cycle_index, [None] * self.num_qudits)
+        inc_point = lambda p: CircuitPoint(p.cycle + 1, p.qudit)
+        shift_point = lambda p: p if p.cycle < cycle_index else inc_point(p)
+        shift_point_or_none = lambda p: None if p is None else shift_point(p)
+        self._front = {
+            q: shift_point_or_none(p)
+            for q, p in self._front.items()
+        }
+        self._rear = {
+            q: shift_point_or_none(p)
+            for q, p in self._rear.items()
+        }
+        self._dag = {
+            shift_point(p): (
+                # Prev
+                {
+                    q: shift_point_or_none(p2)
+                    for q, p2 in prevs.items()
+                },
+                # Next
+                {
+                    q: shift_point_or_none(p2)
+                    for q, p2 in nexts.items()
+                },
+            )
+            for p, (prevs, nexts) in self._dag.items()
+        }
 
     def pop_cycle(self, cycle_index: int) -> None:
         """
@@ -464,15 +649,41 @@ def pop_cycle(self, cycle_index: int) -> None:
         if not self.is_cycle_in_range(cycle_index):
             raise IndexError(f'Cycle index ({cycle_index}) is out-of-range.')
 
-        qudits_to_skip: list[int] = []
-        for qudit_index, op in enumerate(self._circuit[cycle_index]):
-            if op is not None and qudit_index not in qudits_to_skip:
-                qudits_to_skip.extend(op.location)
-                self._gate_info[op.gate] -= 1
-                if self._gate_info[op.gate] <= 0:
-                    del self._gate_info[op.gate]
-
-        self._circuit.pop(cycle_index)
+        ops = {op for op in self._circuit[cycle_index] if op is not None}
+        points = [(cycle_index, op.location[0]) for op in ops]
+        if len(points) != 0:
+            for point in points:
+                self.pop(point)
+            return
+        else:
+            self._circuit.pop(cycle_index)
+
+        dec_point = lambda p: CircuitPoint(p.cycle - 1, p.qudit)
+        shift_point = lambda p: p if p.cycle < cycle_index else dec_point(p)
+        shift_point_or_none = lambda p: None if p is None else shift_point(p)
+        self._front = {
+            q: shift_point_or_none(p)
+            for q, p in self._front.items()
+        }
+        self._rear = {
+            q: shift_point_or_none(p)
+            for q, p in self._rear.items()
+        }
+        self._dag = {
+            shift_point(p): (
+                # Prev
+                {
+                    q: shift_point_or_none(p2)
+                    for q, p2 in ptr_map[0].items()
+                },
+                # Next
+                {
+                    q: shift_point_or_none(p2)
+                    for q, p2 in ptr_map[1].items()
+                },
+            )
+            for p, ptr_map in self._dag.items()
+        }
 
     def _is_cycle_idle(self, cycle_index: int) -> bool:
         """Return true if the cycle is idle, that is it contains no gates."""
@@ -576,20 +787,14 @@ def find_available_cycle(self, location: CircuitLocationLike) -> int:
 
         # No available cycle
         if self.num_cycles == 0:
-            return -1
-
-        # Iterate through cycles in reverse order
-        for cycle in range(self.num_cycles - 1, -1, -1):
-            # Find the first occupied cycle
-            if not self.is_cycle_unoccupied(cycle, location):
-                # The first available cycle is the previous one
-                if cycle == self.num_cycles - 1:
-                    return -1
-                return cycle + 1
+            return 0
 
-        # If we didn't find an occupied cycle,
-        # then they are all unoccupied.
-        return 0
+        cycle = 0
+        for q in location:
+            rear = self._rear[q]
+            if rear is not None:
+                cycle = max(cycle, rear.cycle + 1)
+        return cycle
 
     def _find_available_or_append_cycle(
         self,
@@ -600,7 +805,7 @@ def _find_available_or_append_cycle(
         available_cycle = self.find_available_cycle(location)
 
         # If no available cycle
-        if available_cycle == -1:
+        if available_cycle == self.num_cycles:
             self._append_cycle()
             return self.num_cycles - 1
 
@@ -638,6 +843,36 @@ def normalize_point(self, point: CircuitPointLike) -> CircuitPoint:
 
     # endregion
 
+    # region DAG Methods
+
+    @property
+    def front(self) -> set[CircuitPoint]:
+        """The positions of operations with no dependencies."""
+        front_set = set()
+        for point in self._front.values():
+            if point is not None and len(self.prev(point)) == 0:
+                front_set.add(point)
+        return front_set
+
+    @property
+    def rear(self) -> set[CircuitPoint]:
+        """The positions of operations with nothing after them."""
+        rear_set = set()
+        for point in self._rear.values():
+            if point is not None and len(self.next(point)) == 0:
+                rear_set.add(point)
+        return rear_set
+
+    def next(self, point: CircuitPoint) -> set[CircuitPoint]:
+        """Return the points of operations dependent on the one at `point`."""
+        return {p for p in self._dag[point][1].values() if p is not None}
+
+    def prev(self, point: CircuitPoint) -> set[CircuitPoint]:
+        """Return the points of operations the one at `point` depends on."""
+        return {p for p in self._dag[point][0].values() if p is not None}
+
+    # endregion
+
     # region Operation/Gate/Circuit Methods
 
     def check_valid_operation(self, op: Operation) -> None:
@@ -776,20 +1011,45 @@ def append(self, op: Operation) -> None:
 
         Examples:
             >>> circ = Circuit(1)
-            >>> op = Operation(H(), [0])
+            >>> op = Operation(HGate(), [0])
             >>> circ.append(op) # Appends a Hadamard gate to qudit 0.
         """
         self.check_valid_operation(op)
-
-        if op.gate not in self._gate_info:
-            self._gate_info[op.gate] = 0
-        self._gate_info[op.gate] += 1
-
         cycle_index = self._find_available_or_append_cycle(op.location)
+        point = CircuitPoint(cycle_index, op.location[0])
 
+        prevs: dict[int, CircuitPoint | None] = {i: None for i in op.location}
         for qudit_index in op.location:
+            # Add op to the circuit structure
             self._circuit[cycle_index][qudit_index] = op
 
+            # Update necessary gates already in _dag to point to this one
+            rear = self._rear[qudit_index]
+            if rear is not None:
+                prevs[qudit_index] = rear
+                self._dag[rear][1][qudit_index] = point
+
+            # Update rear pointers
+            self._rear[qudit_index] = point
+
+            # Update front pointers
+            if self._front[qudit_index] is None:
+                self._front[qudit_index] = point
+
+        # Add new op to the dag
+        self._dag[point] = (prevs, {i: None for i in op.location})
+
+        # Update _graph_info
+        for pair in op.location.pairs:
+            if pair not in self._graph_info:
+                self._graph_info[pair] = 0
+            self._graph_info[pair] += 1
+
+        # Update _gate_info
+        if op.gate not in self._gate_info:
+            self._gate_info[op.gate] = 0
+        self._gate_info[op.gate] += 1
+
     def append_gate(
         self,
         gate: Gate,
@@ -815,15 +1075,14 @@ def append_gate(
         See Also:
             :func:`append`
         """
-        if not isinstance(gate, Gate):
-            raise TypeError('Expected gate, got %s.' % type(gate))
-
         self.append(Operation(gate, location, params))
 
     def append_circuit(
         self,
         circuit: Circuit,
         location: CircuitLocationLike,
+        as_circuit_gate: bool = False,
+        move: bool = False,
     ) -> None:
         """
         Append `circuit` at the qudit location specified.
@@ -833,6 +1092,13 @@ def append_circuit(
 
             location (CircuitLocationLike): Apply the circuit to these qudits.
 
+            as_circuit_gate (bool): If true, append `circuit` as a unit
+                block (CircuitGate) rather than each operation in `circuit`
+                individually. (Default: False)
+
+            move (bool): Move circuit into circuit gate rather than copy.
+                (Default: False)
+
         Raises:
             ValueError: If `circuit` is not the same size as `location`.
 
@@ -847,9 +1113,17 @@ def append_circuit(
 
         location = CircuitLocation(location)
 
+        if not is_bool(as_circuit_gate):
+            raise TypeError(f'Expected bool, got: {type(as_circuit_gate)}.')
+
         if circuit.num_qudits != len(location):
             raise ValueError('Circuit and location size mismatch.')
 
+        if as_circuit_gate:
+            op = Operation(CircuitGate(circuit, move), location, circuit.params)
+            self.append(op)
+            return
+
         for op in circuit:
             mapped_location = [location[q] for q in op.location]
             self.append(Operation(op.gate, mapped_location, op.params))
@@ -908,26 +1182,98 @@ def insert(self, cycle_index: int, op: Operation) -> None:
         self.check_valid_operation(op)
 
         if self.num_cycles == 0:
-            self._append_cycle()
+            self.append(op)
+            return
 
         if not self.is_cycle_in_range(cycle_index):
             if cycle_index < -self.num_cycles:
                 cycle_index = 0
             else:
-                cycle_index = self.num_cycles
-                self._append_cycle()
+                self.append(op)
+                return
 
-        if op.gate not in self._gate_info:
-            self._gate_info[op.gate] = 0
-        self._gate_info[op.gate] += 1
+        if cycle_index < 0:
+            cycle_index = self.num_cycles + cycle_index
 
         if not self.is_cycle_unoccupied(cycle_index, op.location):
             self._insert_cycle(cycle_index)
-            cycle_index -= 1 if cycle_index < 0 else 0
 
+        point = CircuitPoint(cycle_index, op.location[0])
+
+        prevs: dict[int, CircuitPoint | None] = {i: None for i in op.location}
+        nexts: dict[int, CircuitPoint | None] = {i: None for i in op.location}
+        for qudit_index in op.location:
+
+            # Update front pointers if necessary
+            if self._front[qudit_index] is None:
+                self._front[qudit_index] = point
+
+            # Update rear pointers if necessary
+            if self._rear[qudit_index] is None:
+                self._rear[qudit_index] = point
+
+            # Search to left of cycle_index on qudit_index
+            for c in reversed(range(cycle_index)):
+                prev_op = self._circuit[c][qudit_index]
+
+                # Find first op on qudit_index
+                if prev_op is not None:
+                    prev_point = CircuitPoint(c, prev_op.location[0])
+
+                    # Update its next to be this
+                    self._dag[prev_point][1][qudit_index] = point
+
+                    # Add it to this prev
+                    prevs[qudit_index] = prev_point
+
+                    # Stop searching to left
+                    break
+
+            # Search to right of cycle_index on qudit_index
+            for c in range(cycle_index, self.num_cycles):
+                next_op = self._circuit[c][qudit_index]
+
+                # Find first op on qudit_index
+                if next_op is not None:
+                    next_point = CircuitPoint(c, next_op.location[0])
+
+                    # Update its prev to be this
+                    self._dag[next_point][0][qudit_index] = point
+
+                    # Add it to this next
+                    nexts[qudit_index] = next_point
+
+                    # Stop searching to right
+                    break
+
+        # Update _front if this is new front
+        for qudit_index, _prev_point in prevs.items():
+            if _prev_point is None:
+                self._front[qudit_index] = point
+
+        # Update _rear if this is new rear
+        for qudit_index, _next_point in nexts.items():
+            if _next_point is None:
+                self._rear[qudit_index] = point
+
+        # Add this to _dag
+        self._dag[point] = (prevs, nexts)
+
+        # Add op to the circuit structure
         for qudit_index in op.location:
             self._circuit[cycle_index][qudit_index] = op
 
+        # Update _graph_info
+        for pair in op.location.pairs:
+            if pair not in self._graph_info:
+                self._graph_info[pair] = 0
+            self._graph_info[pair] += 1
+
+        # Update _gate_info
+        if op.gate not in self._gate_info:
+            self._gate_info[op.gate] = 0
+        self._gate_info[op.gate] += 1
+
     def insert_gate(
         self,
         cycle_index: int,
@@ -967,6 +1313,8 @@ def insert_circuit(
         cycle_index: int,
         circuit: Circuit,
         location: CircuitLocationLike,
+        as_circuit_gate: bool = False,
+        move: bool = False,
     ) -> None:
         """
         Insert `circuit` at the cycle and location specified.
@@ -979,6 +1327,13 @@ def insert_circuit(
             location (CircuitLocationLike): Apply the circuit to these
                 qudits.
 
+            as_circuit_gate (bool): If true, append `circuit` as a unit
+                block (CircuitGate) rather than each operation in `circuit`
+                individually. (Default: False)
+
+            move (bool): Move circuit into circuit gate rather than copy.
+                (Default: False)
+
         Raises:
             ValueError: If `circuit` is not the same size as `location`.
 
@@ -991,6 +1346,9 @@ def insert_circuit(
                 f'Expected integer cycle index, got: {cycle_index}.',
             )
 
+        if not is_bool(as_circuit_gate):
+            raise TypeError(f'Expected bool, got: {type(as_circuit_gate)}.')
+
         if not CircuitLocation.is_location(location):
             raise TypeError('Invalid location.')
 
@@ -999,6 +1357,11 @@ def insert_circuit(
         if circuit.num_qudits != len(location):
             raise ValueError('Circuit and location size mismatch.')
 
+        if as_circuit_gate:
+            op = Operation(CircuitGate(circuit, move), location, circuit.params)
+            self.insert(cycle_index, op)
+            return
+
         for op in reversed(circuit):
             mapped_location = [location[q] for q in op.location]
             self.insert(
@@ -1114,7 +1477,7 @@ def pop(self, point: CircuitPointLike | None = None) -> Operation:
 
         Examples:
             >>> circ = Circuit(1)
-            >>> circ.append_gate(H(), [0])
+            >>> circ.append_gate(HGate(), [0])
             >>> circ.get_num_gates()
             1
             >>> circ.pop(0, 0)
@@ -1139,25 +1502,52 @@ def pop(self, point: CircuitPointLike | None = None) -> Operation:
             raise IndexError('Pop from empty circuit.')
 
         op = self[point]
+        point = self.normalize_point(point)
+        point = CircuitPoint(point[0], op.location[0])
+
+        # Remove it from _dag
+        prevs, nexts = self._dag[point]
+        for q in op.location:
+            prev = prevs[q]
+            if prev is not None:
+                self._dag[prev][1][q] = nexts[q]
+
+            next = nexts[q]
+            if next is not None:
+                self._dag[next][0][q] = prevs[q]
+
+            if self._front[q] == point:
+                self._front[q] = nexts[q]
 
+            if self._rear[q] == point:
+                self._rear[q] = prevs[q]
+        self._dag.pop(point)
+
+        # Remove it from _circuit
         for qudit_index in op.location:
             self._circuit[point[0]][qudit_index] = None
 
+        if self._is_cycle_idle(point.cycle):
+            self.pop_cycle(point.cycle)
+
+        # Update gate and graph counts
         self._gate_info[op.gate] -= 1
         if self._gate_info[op.gate] <= 0:
-            del self._gate_info[op.gate]
+            self._gate_info.pop(op.gate)
 
-        if self._is_cycle_idle(point[0]):
-            self.pop_cycle(point[0])
+        for pair in op.location.pairs:
+            self._graph_info[pair] -= 1
+            if self._graph_info[pair] <= 0:
+                self._graph_info.pop(pair)
 
         return op
 
-    def batch_pop(self, points: Sequence[CircuitPointLike]) -> Circuit:
+    def batch_pop(self, points: Iterable[CircuitPointLike]) -> Circuit:
         """
         Pop all operatons at `points` at once.
 
         Args:
-            points (Sequence[CircuitPointLike]): Remove operations
+            points (Iterable[CircuitPointLike]): Remove operations
                 at these points all at the same time.
 
         Returns:
@@ -1172,42 +1562,20 @@ def batch_pop(self, points: Sequence[CircuitPointLike]) -> Circuit:
             raise IndexError('Out-of-range point.')
 
         # Sort points
-        points = sorted(points)
-
-        # Collect operations avoiding duplicates
-        points_to_skip: list[CircuitPointLike] = []
-        ops_and_cycles: list[tuple[Operation, int]] = []
-        for point in points:
-            if point in points_to_skip:
-                continue
-            if self.is_point_idle(point):
-                continue
-
-            op = self[point]
-            ops_and_cycles.append((op, point[0]))
-            points_to_skip.extend((point[0], q) for q in op.location)
-
-        ops = [op for op, _ in ops_and_cycles]
-        points = [(cycle, op.location[0]) for op, cycle in ops_and_cycles]
+        points = [self.normalize_point(point) for point in points]
+        points = [p for p in points if not self.is_point_idle(p)]
+        ops_and_cycles = {(self[point], point[0]) for point in points}
+        ops_and_cycles = sorted(list(ops_and_cycles), key=lambda x: x[1])
 
         if len(points) == 0:
-            raise IndexError('No operations exists at any of the points.')
+            raise IndexError('Must batch pop at least one point.')
 
-        # Pop gates, tracking if the circuit popped a cycle
-        num_cycles = self.num_cycles
-        for i, point in enumerate(points):
-
-            try:
-                self.pop(point)
-            except IndexError:  # Silently discard multi-points-one-op errors
-                pass
-
-            if num_cycles != self.num_cycles:
-                num_cycles = self.num_cycles
-                points[i + 1:] = [(point[0] - 1, point[1])
-                                  for point in points[i + 1:]]
+        # Pop from circuit
+        for op, cycle in reversed(ops_and_cycles):
+            self.pop((cycle, op.location[0]))
 
         # Form new circuit and return
+        ops = [op for op, _ in ops_and_cycles]
         qudits = list(set(sum((tuple(op.location) for op in ops), ())))
         qudits = sorted(qudits)
         radixes = [self.radixes[q] for q in qudits]
@@ -1238,6 +1606,39 @@ def replace(self, point: CircuitPointLike, op: Operation) -> None:
         if len(self[point].location.intersection(op.location)) == 0:
             raise ValueError("Point's qudit is not in operation's location.")
 
+        old_op = self._circuit[point[0]][point[1]]
+        if old_op is not None and set(old_op.location) == set(op.location):
+            if old_op.location[0] != op.location[0]:
+                old_point = CircuitPoint(point[0], old_op.location[0])
+                new_point = CircuitPoint(point[0], op.location[0])
+                prevs, nexts = self._dag[old_point]
+                for q in old_op.location:
+                    if self._front[q] == old_point:
+                        self._front[q] = new_point
+                    if self._rear[q] == old_point:
+                        self._rear[q] = new_point
+                    prev = prevs[q]
+                    if prev is not None:
+                        self._dag[prev][1][q] = new_point
+                    next = nexts[q]
+                    if next is not None:
+                        self._dag[next][0][q] = new_point
+                self._dag[new_point] = self._dag[old_point]
+                self._dag.pop(old_point)
+
+            self._gate_info[old_op.gate] -= 1
+            if self._gate_info[old_op.gate] <= 0:
+                self._gate_info.pop(old_op.gate)
+
+            if op.gate not in self._gate_info:
+                self._gate_info[op.gate] = 0
+            self._gate_info[op.gate] += 1
+
+            for q in old_op.location:
+                self._circuit[point[0]][q] = op
+
+            return
+
         self.pop(point)
         self.insert(point[0], op)
 
@@ -1290,6 +1691,8 @@ def replace_with_circuit(
         self,
         point: CircuitPointLike,
         circuit: Circuit,
+        as_circuit_gate: bool = False,
+        move: bool = False,
     ) -> None:
         """Replace the operation at 'point' with `circuit`."""
         op = self.pop(point)
@@ -1300,13 +1703,23 @@ def replace_with_circuit(
         if circuit.radixes != tuple(self.radixes[x] for x in op.location):
             raise ValueError('Cannot replace operation with circuit.')
 
-        self.insert_circuit(point[0], circuit, op.location)
+        self.insert_circuit(
+            point[0],
+            circuit,
+            op.location,
+            as_circuit_gate,
+            move,
+        )
 
     def copy(self) -> Circuit:
         """Return a deep copy of this circuit."""
         circuit = Circuit(self.num_qudits, self.radixes)
         circuit._circuit = copy.deepcopy(self._circuit)
         circuit._gate_info = copy.deepcopy(self._gate_info)
+        circuit._graph_info = copy.deepcopy(self._graph_info)
+        circuit._front = copy.deepcopy(self._front)
+        circuit._rear = copy.deepcopy(self._rear)
+        circuit._dag = copy.deepcopy(self._dag)
         return circuit
 
     def become(self, circuit: Circuit, deepcopy: bool = True) -> None:
@@ -1316,16 +1729,35 @@ def become(self, circuit: Circuit, deepcopy: bool = True) -> None:
             self._radixes = circuit.radixes
             self._circuit = copy.deepcopy(circuit._circuit)
             self._gate_info = copy.deepcopy(circuit._gate_info)
+            self._graph_info = copy.deepcopy(circuit._graph_info)
+            self._front = copy.deepcopy(circuit._front)
+            self._rear = copy.deepcopy(circuit._rear)
+            self._dag = copy.deepcopy(circuit._dag)
         else:
             self._num_qudits = circuit.num_qudits
             self._radixes = circuit.radixes
             self._circuit = copy.copy(circuit._circuit)
             self._gate_info = copy.copy(circuit._gate_info)
+            self._graph_info = copy.copy(circuit._graph_info)
+            self._front = copy.copy(circuit._front)
+            self._rear = copy.copy(circuit._rear)
+            self._dag = copy.copy(circuit._dag)
 
     def clear(self) -> None:
         """Clear the circuit."""
         self._circuit = []
         self._gate_info = {}
+        self._graph_info = {}
+        self._front = {i: None for i in range(self.num_qudits)}
+        self._rear = {i: None for i in range(self.num_qudits)}
+        self._dag = {}
+
+    def compress(self) -> None:
+        """Compress the circuit's cycles."""
+        compressed_circuit = Circuit(self.num_qudits, self.radixes)
+        for op in self:
+            compressed_circuit.append(op)
+        self.become(compressed_circuit)
 
     # endregion
 
@@ -1482,9 +1914,25 @@ def straighten(
                     op = self._circuit[old_cycle_index][qudit_index]
                     if op is not None:
                         gate_moved = True
+                        s_point = CircuitPoint(old_cycle_index, op.location[0])
+                        d_point = CircuitPoint(new_cycle_index, op.location[0])
+
+                        prevs, nexts = self._dag[s_point]
                         for qudit in op.location:
                             self._circuit[new_cycle_index][qudit] = op
                             self._circuit[old_cycle_index][qudit] = None
+                            prev = prevs[qudit]
+                            if prev is not None:
+                                self._dag[prev][1][qudit] = d_point
+                            next = nexts[qudit]
+                            if next is not None:
+                                self._dag[next][0][qudit] = d_point
+                            if self._front[qudit] == s_point:
+                                self._front[qudit] = d_point
+                            if self._rear[qudit] == s_point:
+                                self._rear[qudit] = d_point
+                        self._dag.pop(s_point)
+                        self._dag[d_point] = (prevs, nexts)
                         qudits_to_add_to_shadow.extend(op.location)
 
             shadow_qudits.update(qudits_to_add_to_shadow)
@@ -1533,22 +1981,12 @@ def fold(self, region: CircuitRegionLike) -> CircuitPoint:
         region = self.straighten(region)[0]
         circuit = self.batch_pop(region.points)
 
-        # Remove placeholders if being called from batch_fold
-        placeholder_gate_points = [
-            (cycle, op.location[0])
-            for cycle, op in circuit.operations_with_cycles()
-            if isinstance(op.gate, TaggedGate)
-            and op.gate.tag == '__fold_placeholder__'
-        ]
-        if len(placeholder_gate_points) > 0:
-            circuit.batch_pop(placeholder_gate_points)
-
-        # Form and insert CircuitGate
-        self.insert_gate(
+        # Insert popped circuit as a CircuitGate
+        self.insert_circuit(
             region.min_cycle,
-            CircuitGate(circuit, True),
+            circuit,
             sorted(list(region.keys())),
-            list(circuit.params),
+            True,
         )
 
         return CircuitPoint(region.min_cycle, region.min_qudit)
@@ -1563,22 +2001,24 @@ def unfold(self, point: CircuitPointLike) -> None:
         circuit.set_params(op.params)
         self.replace_with_circuit(point, circuit)
 
+    def batch_unfold(self, points: Sequence[CircuitPointLike]) -> None:
+        """Unfold the CircuitGates at `points` into the circuit."""
+        points = {(point[0], self[point].location[0]) for point in points}
+        for point in reversed(sorted(points)):
+            self.unfold(point)
+
     def unfold_all(self) -> None:
         """Unfold all CircuitGates in the circuit."""
-        cycle_index = 0
-        qudit_index = 0
-        while cycle_index < len(self._circuit):
-            op = self._circuit[cycle_index][qudit_index]
-
-            if op is not None and isinstance(op.gate, CircuitGate):
-                self.unfold((cycle_index, op.location[0]))
-                qudit_index = sorted(op.location)[0]
-                continue
-
-            qudit_index += 1
-            if qudit_index >= self.num_qudits:
-                qudit_index = 0
-                cycle_index += 1
+        while any(isinstance(gate, CircuitGate) for gate in self.gate_set):
+            unfolded_circuit = Circuit(self.num_qudits, self.radixes)
+            for op in self:
+                if isinstance(op.gate, CircuitGate):
+                    circuit = op.gate._circuit
+                    circuit.set_params(op.params)
+                    unfolded_circuit.append_circuit(circuit, op.location)
+                else:
+                    unfolded_circuit.append(op)
+            self.become(unfolded_circuit)
 
     def surround(
         self,
@@ -1938,29 +2378,23 @@ def get_operations(
 
     def get_slice(self, points: Sequence[CircuitPointLike]) -> Circuit:
         """Return a copy of a slice of this circuit."""
-        if not all(self.is_point_in_range(point) for point in points):
-            raise IndexError('Out-of-range point.')
-
         # Sort points
         points = sorted(points)
 
         # Collect operations avoiding duplicates
-        points_to_skip: list[CircuitPointLike] = []
-        ops_and_cycles: list[tuple[Operation, int]] = []
-        for point in points:
-            if point in points_to_skip:
-                continue
-            if self.is_point_idle(point):
-                continue
+        ops_and_cycles: list[tuple[Operation, int]] = list({
+            (self[point], point[0])
+            for point in points
+            if not self.is_point_idle(point)
+        })
 
-            op = self[point]
-            ops_and_cycles.append((op, point[0]))
-            points_to_skip.extend((point[0], q) for q in op.location)
+        if len(ops_and_cycles) == 0:
+            raise IndexError('No operations exists at any of the points.')
 
+        ops_and_cycles.sort(key=lambda x: (x[1], *x[0].location))
         ops = [op for op, _ in ops_and_cycles]
-        points = [(cycle, op.location[0]) for op, cycle in ops_and_cycles]
 
-        if len(points) == 0:
+        if len(ops_and_cycles) == 0:
             raise IndexError('No operations exists at any of the points.')
 
         # Form new circuit and return
@@ -2061,63 +2495,6 @@ def get_inverse(self) -> Circuit:
             )
         return circuit
 
-    def compress(self) -> None:
-        """Compress the circuit's cycles."""
-        compressed_circuit = Circuit(self.num_qudits, self.radixes)
-        for op in self:
-            compressed_circuit.append(op)
-        self.become(compressed_circuit)
-
-    def renumber_qudits(self, qudit_permutation: Sequence[int]) -> None:
-        """
-        Permute the qudits in the circuit.
-
-        Args:
-            qudit_permutation (Sequence[int]): A map from qudit indices
-                to qudit indices.
-
-        Raises:
-            IndexError: If any of the indices are out of range.
-
-            ValueError: If the `qudit_permutation` is not the same size
-                as the circuit.
-
-            ValueError: If the `qudit_permutation` is not a valid permutation.
-        """
-        if not is_sequence_of_int(qudit_permutation):
-            raise TypeError(
-                'Expected sequence of integers'
-                ', got %s' % type(qudit_permutation),
-            )
-
-        if len(qudit_permutation) != self.num_qudits:
-            raise ValueError(
-                'Expected qudit_permutation length equal to circuit num_qudits:'
-                '%d, got %d' % (self.num_qudits, len(qudit_permutation)),
-            )
-
-        if len(qudit_permutation) != len(set(qudit_permutation)):
-            raise ValueError('Invalid permutation.')
-
-        perm = [int(q) for q in qudit_permutation]
-
-        for i in range(self.num_cycles):
-            self._circuit[i] = [
-                self._circuit[i][perm.index(q)]
-                for q in range(self.num_qudits)
-            ]
-
-            qudits_to_skip: list[int] = []
-            for j in range(self.num_qudits):
-                if j in qudits_to_skip:
-                    continue
-
-                op = self._circuit[i][j]
-                if op is not None:
-                    loc = CircuitLocation([perm[q] for q in op.location])
-                    op._location = loc
-                    qudits_to_skip.extend(loc)
-
     def get_unitary(self, params: RealVector = []) -> UnitaryMatrix:
         """
         Return the unitary matrix of the circuit.
@@ -2213,7 +2590,7 @@ def get_unitary_and_grad(
                 # Should work fine with non unitary gradients
                 # TODO: Fix for non qubits
                 full_grad = np.kron(grad, iden)
-                full_grad = perm @ full_grad @ permT
+                full_grad = permT @ full_grad @ perm
                 full_gards.append(right_utry @ full_grad @ left_utry)
             left.apply_right(M, loc)
 
@@ -2323,6 +2700,7 @@ def instantiate(
 
         # If method is specified by name; match it
         elif isinstance(method, str):
+            instantiater = None
             for inst in instantiater_order:
                 inst_t = cast(Instantiater, inst)
                 if inst_t.get_method_name().lower() == method.lower():
@@ -2455,6 +2833,17 @@ def minimize(self, cost: CostFunction, **kwargs: Any) -> None:
 
     # endregion
 
+    # region Measurement Methods
+
+    def remove_all_measurements(self) -> None:
+        """Remove all measurement placeholders from the circuit."""
+        while any(isinstance(g, MeasurementPlaceholder) for g in self.gate_set):
+            for g in self.gate_set:
+                if isinstance(g, MeasurementPlaceholder):
+                    self.remove(g)
+
+    # endregion
+
     # region Collection and Iteration Methods
 
     @overload
diff --git a/bqskit/ir/gate.py b/bqskit/ir/gate.py
index 23e69d53e..6bbee7596 100644
--- a/bqskit/ir/gate.py
+++ b/bqskit/ir/gate.py
@@ -9,6 +9,8 @@
 from typing import Callable
 from typing import TYPE_CHECKING
 
+from bqskit.ir.location import CircuitLocation
+from bqskit.qis.unitary.unitary import RealVector
 from bqskit.qis.unitary.unitary import Unitary
 
 if TYPE_CHECKING:
@@ -41,6 +43,14 @@ def get_qasm_gate_def(self) -> str:
 
         return ''
 
+    def get_qasm(self, params: RealVector, location: CircuitLocation) -> str:
+        """Returns the qasm string for this gate."""
+        return '{}({}) q[{}];\n'.format(
+            self.qasm_name,
+            ', '.join([str(p) for p in params]),
+            '], q['.join([str(q) for q in location]),
+        ).replace('()', '')
+
     with_frozen_params: Callable[[Gate, dict[int, float]], FrozenParameterGate]
     with_all_frozen_params: Callable[[Gate, list[float]], FrozenParameterGate]
 
diff --git a/bqskit/ir/gates/__init__.py b/bqskit/ir/gates/__init__.py
index 550730864..44883eb11 100644
--- a/bqskit/ir/gates/__init__.py
+++ b/bqskit/ir/gates/__init__.py
@@ -24,9 +24,12 @@
     :template: autosummary/gate.rst
 
     CCXGate
+    ToffoliGate
     CHGate
     CPIGate
+    CSGate
     CSUMGate
+    CTGate
     CNOTGate
     CXGate
     CYGate
@@ -39,14 +42,13 @@
     SGate
     SdgGate
     SqrtCNOTGate
-    SqrtISwapGate
     SwapGate
     SqrtXGate
+    SqrtISwapGate
     SXGate
     SycamoreGate
     TGate
     TdgGate
-    ToffoliGate
     ConstantUnitaryGate
     XGate
     XXGate
@@ -63,11 +65,13 @@
     :nosignatures:
     :template: autosummary/gate.rst
 
+    CPGate
     CRXGate
     CRYGate
     CRZGate
     FSIMGate
     PauliGate
+    PhasedXZGate
     RXGate
     RXXGate
     RYGate
@@ -75,6 +79,9 @@
     RZGate
     RZZGate
     U1Gate
+    U1qGate
+    U1qPiGate
+    U1qPi2Gate
     U2Gate
     U3Gate
     U8Gate
@@ -102,6 +109,7 @@
     :nosignatures:
 
     CircuitGate
+    MeasurementPlaceholder
 """
 from __future__ import annotations
 
@@ -112,6 +120,7 @@
 from bqskit.ir.gates.constant import *  # noqa
 from bqskit.ir.gates.constant import __all__ as constant_all
 from bqskit.ir.gates.constantgate import ConstantGate
+from bqskit.ir.gates.measure import MeasurementPlaceholder
 from bqskit.ir.gates.parameterized import *  # noqa
 from bqskit.ir.gates.parameterized import __all__ as parameterized_all
 from bqskit.ir.gates.qubitgate import QubitGate
@@ -119,4 +128,4 @@
 
 __all__ = composed_all + constant_all + parameterized_all
 __all__ += ['ComposedGate', 'QubitGate', 'QutritGate', 'ConstantGate']
-__all__ += ['CircuitGate']
+__all__ += ['CircuitGate', 'MeasurementPlaceholder']
diff --git a/bqskit/ir/gates/circuitgate.py b/bqskit/ir/gates/circuitgate.py
index 5bbc747b3..8c33c4b52 100644
--- a/bqskit/ir/gates/circuitgate.py
+++ b/bqskit/ir/gates/circuitgate.py
@@ -6,6 +6,7 @@
 import numpy as np
 import numpy.typing as npt
 
+from bqskit.ir.location import CircuitLocation
 from bqskit.qis.unitary.unitary import RealVector
 from bqskit.qis.unitary.unitarymatrix import UnitaryMatrix
 
@@ -94,3 +95,60 @@ def __eq__(self, other: object) -> bool:
             op1.gate == op2.gate and op1.location == op2.location
             for op1, op2 in zip(self._circuit, other._circuit)
         )
+
+    def get_qasm_gate_def(self) -> str:
+        """Returns a qasm gate definition block for this gate."""
+        ret = ''
+        for gate in self._circuit.gate_set:
+            ret += gate.get_qasm_gate_def()
+
+        # TODO: replace use of hash
+        id = hash(self)
+        if id < 0:
+            id = -id
+        ret += f'gate circuitgate_{id} '
+
+        if self.num_params > 0:
+            ret += '(p'
+            ret += ', p'.join([str(i) for i in range(self.num_params)])
+            ret += ') '
+
+        ret += 'q' + ', q'.join([str(i) for i in range(self.num_qudits)])
+
+        ret += ' {\n'
+        param_index = 0
+        for op in self._circuit:
+            params = [
+                f'p{i}'
+                for i in range(param_index, param_index + op.num_params)
+            ]
+            if isinstance(op.gate, CircuitGate):
+                op_id = hash(op.gate)
+                if op_id < 0:
+                    op_id = -op_id
+                ret += '\t{}({}) q{};\n'.format(
+                    f'circuitgate_{op_id}',
+                    ', '.join([str(p) for p in params]),
+                    ', q'.join([str(q) for q in op.location]),
+                ).replace('()', '')
+            else:
+                ret += '\t{}({}) q{};\n'.format(
+                    op.gate.qasm_name,
+                    ', '.join([str(p) for p in params]),
+                    ', q'.join([str(q) for q in op.location]),
+                ).replace('()', '')
+        ret += '}\n'
+        return ret
+
+    def get_qasm(self, params: RealVector, location: CircuitLocation) -> str:
+        """Returns the qasm string for this gate."""
+        # TODO: replace use of hash
+        id = hash(self)
+        if id < 0:
+            id = -id
+
+        return '{}({}) q[{}];\n'.format(
+            f'circuitgate_{id}',
+            ', '.join([str(p) for p in params]),
+            '], q['.join([str(q) for q in location]),
+        ).replace('()', '')
diff --git a/bqskit/ir/gates/composed/frozenparam.py b/bqskit/ir/gates/composed/frozenparam.py
index 53dd7a8a6..73b07cd3d 100644
--- a/bqskit/ir/gates/composed/frozenparam.py
+++ b/bqskit/ir/gates/composed/frozenparam.py
@@ -83,7 +83,7 @@ def __init__(self, gate: Gate, frozen_params: dict[int, float]) -> None:
             i for i in range(gate.num_params)
             if i not in self.frozen_params.keys()
         ]
-        self._name = '%s(%s, %s)' % (
+        self._name = '{}({}, {})'.format(
             self.__class__.__name__,
             self.gate.name,
             str(self.frozen_params),
diff --git a/bqskit/ir/gates/composed/tagged.py b/bqskit/ir/gates/composed/tagged.py
index b862e3a8d..0bae08a71 100644
--- a/bqskit/ir/gates/composed/tagged.py
+++ b/bqskit/ir/gates/composed/tagged.py
@@ -40,7 +40,7 @@ def __init__(self, gate: Gate, tag: Any) -> None:
 
         self.gate = gate
         self.tag = tag
-        self._name = 'Tagged(%s:%s)' % (gate.name, str(tag))
+        self._name = f'Tagged({gate.name}:{str(tag)})'
         self._num_params = gate.num_params
         self._num_qudits = gate.num_qudits
         self._radixes = gate.radixes
diff --git a/bqskit/ir/gates/composed/vlg.py b/bqskit/ir/gates/composed/vlg.py
index e692e407d..3b158ccd5 100644
--- a/bqskit/ir/gates/composed/vlg.py
+++ b/bqskit/ir/gates/composed/vlg.py
@@ -144,7 +144,7 @@ def get_unitary(self, params: RealVector = []) -> UnitaryMatrix:
 
         P = np.sum([a * s for a, s in zip(l, self.perms)], 0)
         G = self.gate.get_unitary(a)
-        PGPT = P @ np.kron(G, self.I) @ P.T
+        PGPT = P.T @ np.kron(G, self.I) @ P
         return UnitaryMatrix.closest_to(PGPT, self.radixes)
 
     def get_grad(self, params: RealVector = []) -> npt.NDArray[np.complex128]:
diff --git a/bqskit/ir/gates/constant/__init__.py b/bqskit/ir/gates/constant/__init__.py
index 1c84716b3..dd8f74cd9 100644
--- a/bqskit/ir/gates/constant/__init__.py
+++ b/bqskit/ir/gates/constant/__init__.py
@@ -5,7 +5,9 @@
 from bqskit.ir.gates.constant.ccx import ToffoliGate
 from bqskit.ir.gates.constant.ch import CHGate
 from bqskit.ir.gates.constant.cpi import CPIGate
+from bqskit.ir.gates.constant.cs import CSGate
 from bqskit.ir.gates.constant.csum import CSUMGate
+from bqskit.ir.gates.constant.ct import CTGate
 from bqskit.ir.gates.constant.cx import CNOTGate
 from bqskit.ir.gates.constant.cx import CXGate
 from bqskit.ir.gates.constant.cy import CYGate
@@ -38,7 +40,9 @@
     'ToffoliGate',
     'CHGate',
     'CPIGate',
+    'CSGate',
     'CSUMGate',
+    'CTGate',
     'CNOTGate',
     'CXGate',
     'CYGate',
diff --git a/bqskit/ir/gates/constant/cs.py b/bqskit/ir/gates/constant/cs.py
new file mode 100644
index 000000000..6908c6891
--- /dev/null
+++ b/bqskit/ir/gates/constant/cs.py
@@ -0,0 +1,34 @@
+"""This module implements the CSGate."""
+from __future__ import annotations
+
+from bqskit.ir.gates.constantgate import ConstantGate
+from bqskit.ir.gates.qubitgate import QubitGate
+from bqskit.qis.unitary.unitarymatrix import UnitaryMatrix
+
+
+class CSGate(ConstantGate, QubitGate):
+    """
+    The Controlled-S gate.
+
+    The CS gate is given by the following unitary:
+
+    .. math::
+
+        \\begin{pmatrix}
+        1 & 0 & 0 & 0 \\\\
+        0 & 1 & 0 & 0 \\\\
+        0 & 0 & 1 & 0 \\\\
+        0 & 0 & 0 & i \\\\
+        \\end{pmatrix}
+    """
+
+    _num_qudits = 2
+    _qasm_name = 'cs'
+    _utry = UnitaryMatrix(
+        [
+            [1, 0, 0, 0],
+            [0, 1, 0, 0],
+            [0, 0, 1, 0],
+            [0, 0, 0, 1j],
+        ],
+    )
diff --git a/bqskit/ir/gates/constant/ct.py b/bqskit/ir/gates/constant/ct.py
new file mode 100644
index 000000000..64e72cffc
--- /dev/null
+++ b/bqskit/ir/gates/constant/ct.py
@@ -0,0 +1,36 @@
+"""This module implements the CTGate."""
+from __future__ import annotations
+
+import cmath
+
+from bqskit.ir.gates.constantgate import ConstantGate
+from bqskit.ir.gates.qubitgate import QubitGate
+from bqskit.qis.unitary.unitarymatrix import UnitaryMatrix
+
+
+class CTGate(ConstantGate, QubitGate):
+    """
+    The Controlled-T gate.
+
+    The CT gate is given by the following unitary:
+
+    .. math::
+
+        \\begin{pmatrix}
+        1 & 0 & 0 & 0 \\\\
+        0 & 1 & 0 & 0 \\\\
+        0 & 0 & 1 & 0 \\\\
+        0 & 0 & 0 & \\exp({i\\frac{\\pi}{4}}) \\\\
+        \\end{pmatrix}
+    """
+
+    _num_qudits = 2
+    _qasm_name = 'ct'
+    _utry = UnitaryMatrix(
+        [
+            [1, 0, 0, 0],
+            [0, 1, 0, 0],
+            [0, 0, 1, 0],
+            [0, 0, 0, cmath.exp(1j * cmath.pi / 4)],
+        ],
+    )
diff --git a/bqskit/ir/gates/constant/identity.py b/bqskit/ir/gates/constant/identity.py
index 162408fd9..504724fbf 100644
--- a/bqskit/ir/gates/constant/identity.py
+++ b/bqskit/ir/gates/constant/identity.py
@@ -60,3 +60,9 @@ def __eq__(self, other: object) -> bool:
 
     def __hash__(self) -> int:
         return hash((self.num_qudits, self.radixes))
+
+    def __str__(self) -> str:
+        if self.is_qubit_only():
+            return f'IdentityGate({self.num_qudits})'
+        else:
+            return f'IdentityGate({self.num_qudits}, {self.radixes})'
diff --git a/bqskit/ir/gates/constant/itoffoli.py b/bqskit/ir/gates/constant/itoffoli.py
index dad5e5d10..3b5fbafb1 100644
--- a/bqskit/ir/gates/constant/itoffoli.py
+++ b/bqskit/ir/gates/constant/itoffoli.py
@@ -32,6 +32,7 @@ class IToffoliGate(ConstantGate, QubitGate):
     """
 
     _num_qudits = 3
+    _qasm_name = 'iccx'
     _utry = UnitaryMatrix(
         [
             [1, 0, 0, 0, 0, 0, 0, 0],
diff --git a/bqskit/ir/gates/constant/permutation.py b/bqskit/ir/gates/constant/permutation.py
index cb8a78db3..bdbdc94c8 100644
--- a/bqskit/ir/gates/constant/permutation.py
+++ b/bqskit/ir/gates/constant/permutation.py
@@ -34,4 +34,19 @@ def __init__(
             raise ValueError('Expected positive integer, got %d' % num_qudits)
 
         self._num_qudits = num_qudits
-        self._utry = PermutationMatrix.from_qubit_location(num_qudits, location)
+        self.location = tuple(location)
+        self._utry = PermutationMatrix.from_qubit_location(
+            num_qudits, self.location,
+        )
+
+    def __str__(self) -> str:
+        return f'PermutationGate({self.location})'
+
+    def __eq__(self, other: object) -> bool:
+        return (
+            isinstance(other, PermutationGate)
+            and self._utry == other._utry
+        )
+
+    def __hash__(self) -> int:
+        return hash(self._utry)
diff --git a/bqskit/ir/gates/constant/sqrtiswap.py b/bqskit/ir/gates/constant/sqrtiswap.py
index 62e688e0f..be60d1292 100644
--- a/bqskit/ir/gates/constant/sqrtiswap.py
+++ b/bqskit/ir/gates/constant/sqrtiswap.py
@@ -25,6 +25,7 @@ class SqrtISwapGate(ConstantGate, QubitGate):
     """
 
     _num_qudits = 2
+    _qasm_name = 'sqisw'
     _utry = UnitaryMatrix(
         [
             [1, 0, 0, 0],
diff --git a/bqskit/ir/gates/constant/sycamore.py b/bqskit/ir/gates/constant/sycamore.py
index f1e38c202..2400679b6 100644
--- a/bqskit/ir/gates/constant/sycamore.py
+++ b/bqskit/ir/gates/constant/sycamore.py
@@ -21,11 +21,12 @@ class SycamoreGate(ConstantGate, QubitGate):
         1 & 0 & 0 & 0 \\\\
         0 & 0 & -i & 0 \\\\
         0 & -i & 0 & 0 \\\\
-        0 & 0 & 0 & e^{-i\frac{\\pi}{6}} \\\\
+        0 & 0 & 0 & e^{-i\\frac{\\pi}{6}} \\\\
         \\end{pmatrix}
     """
 
     _num_qudits = 2
+    _qasm_name = 'syc'
     _utry = UnitaryMatrix(
         [
             [1, 0, 0, 0],
diff --git a/bqskit/ir/gates/measure.py b/bqskit/ir/gates/measure.py
new file mode 100644
index 000000000..f17d53adf
--- /dev/null
+++ b/bqskit/ir/gates/measure.py
@@ -0,0 +1,66 @@
+"""This module implements the MeasurementPlaceholder class."""
+from __future__ import annotations
+
+from bqskit.ir.gate import Gate
+from bqskit.ir.location import CircuitLocation
+from bqskit.qis.unitary.unitary import RealVector
+from bqskit.qis.unitary.unitarymatrix import UnitaryMatrix
+
+
+class MeasurementPlaceholder(Gate):
+    """Pseudogate to hold measurement information."""
+
+    def __init__(
+        self,
+        classical_regs: list[tuple[str, int]],
+        measurements: dict[int, tuple[str, int]],
+    ) -> None:
+        """
+        Construct a MeasurementPlaceholder.
+
+        Args:
+            classical_regs (list[tuple[str, int]]): A list of classical
+                register descriptors. Each one is given as a tuple containing
+                its name and size.
+
+            measurements (dict[int, tuple[str, int]]): A list of measurements
+                made. Measurements are given as a map of qudit index to
+                a tuple containing the classical register's name and index.
+        """
+        self._name = 'measurement'
+        self._num_qudits = len(measurements)
+        self._radixes = tuple([2] * self._num_qudits)
+        self._num_params = 0
+        self.classical_regs = classical_regs
+        self.measurements = measurements
+
+    def get_unitary(self, params: RealVector = []) -> UnitaryMatrix:
+        raise RuntimeError(
+            'Cannot compute unitary for a measurement placeholder.'
+            ' Consider removing measurements either by calling'
+            ' `circuit.remove_measurements()` or by using the'
+            ' ExtractMeasurements and RestoreMeasurements passes.',
+        )
+
+    def get_qasm_gate_def(self) -> str:
+        """Declares the classical registers."""
+        ret = ''
+        for name, size in self.classical_regs:
+            ret += f'creg {name}[{size}];\n'
+        return ret
+
+    def get_qasm(self, params: RealVector, location: CircuitLocation) -> str:
+        ret = ''
+        for qudit, (name, index) in self.measurements.items():
+            ret += f'measure q[{qudit}] -> {name}[{index}];\n'
+        return ret
+
+    def __eq__(self, other: object) -> bool:
+        return (
+            isinstance(other, MeasurementPlaceholder)
+            and other.measurements == self.measurements
+            and other.classical_regs == self.classical_regs
+        )
+
+    def __hash__(self) -> int:
+        return hash(tuple(self.classical_regs))
diff --git a/bqskit/ir/gates/parameterized/__init__.py b/bqskit/ir/gates/parameterized/__init__.py
index 8919332d3..06a56f520 100644
--- a/bqskit/ir/gates/parameterized/__init__.py
+++ b/bqskit/ir/gates/parameterized/__init__.py
@@ -1,11 +1,13 @@
 """This package contains parameterized gates."""
 from __future__ import annotations
 
+from bqskit.ir.gates.parameterized.cp import CPGate
 from bqskit.ir.gates.parameterized.crx import CRXGate
 from bqskit.ir.gates.parameterized.cry import CRYGate
 from bqskit.ir.gates.parameterized.crz import CRZGate
 from bqskit.ir.gates.parameterized.fsim import FSIMGate
 from bqskit.ir.gates.parameterized.pauli import PauliGate
+from bqskit.ir.gates.parameterized.phasedxz import PhasedXZGate
 from bqskit.ir.gates.parameterized.rx import RXGate
 from bqskit.ir.gates.parameterized.rxx import RXXGate
 from bqskit.ir.gates.parameterized.ry import RYGate
@@ -13,17 +15,22 @@
 from bqskit.ir.gates.parameterized.rz import RZGate
 from bqskit.ir.gates.parameterized.rzz import RZZGate
 from bqskit.ir.gates.parameterized.u1 import U1Gate
+from bqskit.ir.gates.parameterized.u1q import U1qGate
+from bqskit.ir.gates.parameterized.u1q import U1qPi2Gate
+from bqskit.ir.gates.parameterized.u1q import U1qPiGate
 from bqskit.ir.gates.parameterized.u2 import U2Gate
 from bqskit.ir.gates.parameterized.u3 import U3Gate
 from bqskit.ir.gates.parameterized.u8 import U8Gate
 from bqskit.ir.gates.parameterized.unitary import VariableUnitaryGate
 
 __all__ = [
+    'CPGate',
     'CRXGate',
     'CRYGate',
     'CRZGate',
     'FSIMGate',
     'PauliGate',
+    'PhasedXZGate',
     'RXGate',
     'RXXGate',
     'RYGate',
@@ -31,6 +38,9 @@
     'RZGate',
     'RZZGate',
     'U1Gate',
+    'U1qGate',
+    'U1qPiGate',
+    'U1qPi2Gate',
     'U2Gate',
     'U3Gate',
     'U8Gate',
diff --git a/bqskit/ir/gates/parameterized/cp.py b/bqskit/ir/gates/parameterized/cp.py
new file mode 100644
index 000000000..58830f442
--- /dev/null
+++ b/bqskit/ir/gates/parameterized/cp.py
@@ -0,0 +1,72 @@
+"""This module implements the CPGate."""
+from __future__ import annotations
+
+import numpy as np
+import numpy.typing as npt
+
+from bqskit.ir.gates.qubitgate import QubitGate
+from bqskit.qis.unitary.differentiable import DifferentiableUnitary
+from bqskit.qis.unitary.unitary import RealVector
+from bqskit.qis.unitary.unitarymatrix import UnitaryMatrix
+from bqskit.utils.cachedclass import CachedClass
+
+
+class CPGate(
+    QubitGate,
+    DifferentiableUnitary,
+    CachedClass,
+):
+    """
+    A gate representing a controlled phase rotation.
+
+    It is given by the following parameterized unitary:
+
+    .. math::
+
+        \\begin{pmatrix}
+        1 & 0 & 0 & 0 \\\\
+        0 & 1 & 0 & 0 \\\\
+        0 & 0 & 1 & 0 \\\\
+        0 & 0 & 0 & \\exp({i\\theta}) \\\\
+        \\end{pmatrix}
+    """
+
+    _num_qudits = 2
+    _num_params = 1
+    _qasm_name = 'cp'
+
+    def get_unitary(self, params: RealVector = []) -> UnitaryMatrix:
+        """Return the unitary for this gate, see :class:`Unitary` for more."""
+        self.check_parameters(params)
+
+        exp = np.exp(1j * params[0])
+
+        return UnitaryMatrix(
+            [
+                [1, 0, 0, 0],
+                [0, 1, 0, 0],
+                [0, 0, 1, 0],
+                [0, 0, 0, exp],
+            ],
+        )
+
+    def get_grad(self, params: RealVector = []) -> npt.NDArray[np.complex128]:
+        """
+        Return the gradient for this gate.
+
+        See :class:`DifferentiableUnitary` for more info.
+        """
+        self.check_parameters(params)
+
+        dexp = 1j * np.exp(1j * params[0])
+
+        return np.array(
+            [
+                [
+                    [0, 0, 0, 0],
+                    [0, 0, 0, 0],
+                    [0, 0, 0, 0],
+                    [0, 0, 0, dexp],
+                ],
+            ], dtype=np.complex128,
+        )
diff --git a/bqskit/ir/gates/parameterized/fsim.py b/bqskit/ir/gates/parameterized/fsim.py
index c94815233..4e056f139 100644
--- a/bqskit/ir/gates/parameterized/fsim.py
+++ b/bqskit/ir/gates/parameterized/fsim.py
@@ -40,6 +40,7 @@ class FSIMGate(
 
     _num_qudits = 2
     _num_params = 2
+    _qasm_name = 'fsim'
 
     def get_unitary(self, params: RealVector = []) -> UnitaryMatrix:
         """Return the unitary for this gate, see :class:`Unitary` for more."""
diff --git a/bqskit/ir/gates/parameterized/phasedxz.py b/bqskit/ir/gates/parameterized/phasedxz.py
new file mode 100644
index 000000000..7b7ee4392
--- /dev/null
+++ b/bqskit/ir/gates/parameterized/phasedxz.py
@@ -0,0 +1,88 @@
+"""This module implements the PhasedXZGate."""
+from __future__ import annotations
+
+import numpy as np
+import numpy.typing as npt
+
+from bqskit.ir.gates.qubitgate import QubitGate
+from bqskit.qis.unitary.differentiable import DifferentiableUnitary
+from bqskit.qis.unitary.unitary import RealVector
+from bqskit.qis.unitary.unitarymatrix import UnitaryMatrix
+from bqskit.utils.cachedclass import CachedClass
+
+
+class PhasedXZGate(QubitGate, DifferentiableUnitary, CachedClass):
+    """
+    A gate representing an Google's PhasedXZGate.
+
+    References:
+        https://quantumai.google/reference/python/cirq/PhasedXZGate
+    """
+
+    _num_qudits = 1
+    _num_params = 3
+    _qasm_name = 'pxz'
+
+    def get_unitary(self, params: RealVector = []) -> UnitaryMatrix:
+        """Return the unitary for this gate, see :class:`Unitary` for more."""
+        self.check_parameters(params)
+
+        x = params[0]
+        z = params[1]
+        a = params[2]
+        cos = np.cos(np.pi * x / 2)
+        sin = -1j * np.sin(np.pi * x / 2)
+        e1 = np.exp(1j * np.pi * x / 2)
+        e2 = np.exp(1j * np.pi * (x / 2 - a))
+        e3 = np.exp(1j * np.pi * (x / 2 + z + a))
+        e4 = np.exp(1j * np.pi * (x / 2 + z))
+
+        return UnitaryMatrix(
+            [
+                [e1 * cos, e2 * sin],
+                [e3 * sin, e4 * cos],
+            ],
+        )
+
+    def get_grad(self, params: RealVector = []) -> npt.NDArray[np.complex128]:
+        """
+        Return the gradient for this gate.
+
+        See :class:`DifferentiableUnitary` for more info.
+        """
+        self.check_parameters(params)
+
+        x = params[0]
+        z = params[1]
+        a = params[2]
+        cos = np.cos(np.pi * x / 2)
+        sin = -1j * np.sin(np.pi * x / 2)
+        dcos = -np.pi * np.sin(np.pi * x / 2) / 2
+        dsin = -1j * np.pi * np.cos(np.pi * x / 2) / 2
+        e1 = np.exp(1j * np.pi * x / 2)
+        e2 = np.exp(1j * np.pi * (x / 2 - a))
+        e3 = np.exp(1j * np.pi * (x / 2 + z + a))
+        e4 = np.exp(1j * np.pi * (x / 2 + z))
+
+        return np.array(
+            [
+                [
+                    [
+                        (e1 * dcos) + (1j * np.pi / 2 * e1 * cos),
+                        (e2 * dsin) + (1j * np.pi / 2 * e2 * sin),
+                    ],
+                    [
+                        (e3 * dsin) + (1j * np.pi / 2 * e3 * sin),
+                        (e4 * dcos) + (1j * np.pi / 2 * e4 * cos),
+                    ],
+                ],
+                [
+                    [0, 0],
+                    [1j * np.pi * e3 * sin, 1j * np.pi * e4 * cos],
+                ],
+                [
+                    [0, -1j * np.pi * e2 * sin],
+                    [1j * np.pi * e3 * sin, 0],
+                ],
+            ], dtype=np.complex128,
+        )
diff --git a/bqskit/ir/gates/parameterized/u1q.py b/bqskit/ir/gates/parameterized/u1q.py
new file mode 100644
index 000000000..c7d84b986
--- /dev/null
+++ b/bqskit/ir/gates/parameterized/u1q.py
@@ -0,0 +1,81 @@
+"""This module implements the U1qGate."""
+from __future__ import annotations
+
+import math
+
+import numpy as np
+import numpy.typing as npt
+
+from bqskit.ir.gates.qubitgate import QubitGate
+from bqskit.qis.unitary.differentiable import DifferentiableUnitary
+from bqskit.qis.unitary.unitary import RealVector
+from bqskit.qis.unitary.unitarymatrix import UnitaryMatrix
+from bqskit.utils.cachedclass import CachedClass
+
+
+class U1qGate(QubitGate, DifferentiableUnitary, CachedClass):
+    """
+    The Honeywell U1q single qubit gate.
+
+    It is given by the following parameterized unitary:
+
+    .. math::
+
+        \\begin{pmatrix}
+        \\cos{\\frac{\\theta_0}{2}} &
+         -i\\exp{-i\\theta_1}\\sin{\\frac{\\theta_0}{2}} \\\\
+        -i\\exp{i\\theta_1}\\sin{\\frac{\\theta_0}{2}} &
+         \\cos{\\frac{\\theta_0}{2}} \\\\
+        \\end{pmatrix}
+    """
+
+    _num_qudits = 1
+    _num_params = 2
+    _qasm_name = 'U1q'
+
+    def get_unitary(self, params: RealVector = []) -> UnitaryMatrix:
+        """Return the unitary for this gate, see :class:`Unitary` for more."""
+        self.check_parameters(params)
+
+        ct = np.cos(params[0] / 2)
+        st = np.sin(params[0] / 2)
+        enp = np.exp(-1j * params[1])
+        epp = np.exp(1j * params[1])
+
+        return UnitaryMatrix(
+            [
+                [ct, -1j * enp * st],
+                [-1j * epp * st, ct],
+            ],
+        )
+
+    def get_grad(self, params: RealVector = []) -> npt.NDArray[np.complex128]:
+        """
+        Return the gradient for this gate.
+
+        See :class:`DifferentiableUnitary` for more info.
+        """
+        self.check_parameters(params)
+
+        ct = np.cos(params[0] / 2)
+        st = np.sin(params[0] / 2)
+        enp = np.exp(-1j * params[1])
+        epp = np.exp(1j * params[1])
+
+        return np.array(
+            [
+                [  # wrt params[0]
+                    [-0.5 * st, -0.5j * enp * ct],
+                    [-0.5j * epp * ct, -0.5 * st],
+                ],
+
+                [  # wrt params[1]
+                    [0, -1 * enp * st],
+                    [epp * st, 0],
+                ],
+            ], dtype=np.complex128,
+        )
+
+
+U1qPiGate = U1qGate().with_frozen_params({0: math.pi})
+U1qPi2Gate = U1qGate().with_frozen_params({0: math.pi / 2})
diff --git a/bqskit/ir/interval.py b/bqskit/ir/interval.py
index 53b061fd9..5375955a6 100644
--- a/bqskit/ir/interval.py
+++ b/bqskit/ir/interval.py
@@ -178,25 +178,28 @@ def is_interval(interval: Any) -> TypeGuard[IntervalLike]:
             return True
 
         if not isinstance(interval, tuple):
-            _logger.debug('Bounds is not a tuple.')
+            _logger.log(0, 'Bounds is not a tuple.')
             return False
 
         if len(interval) != 2:
-            _logger.debug(
+            _logger.log(
+                0,
                 'Expected interval to contain two values'
                 f', got {len(interval)}.',
             )
             return False
 
         if not is_integer(interval[0]):
-            _logger.debug(
+            _logger.log(
+                0,
                 'Expected integer values in interval'
                 f', got {type(interval[0])}.',
             )
             return False
 
         if not is_integer(interval[1]):
-            _logger.debug(
+            _logger.log(
+                0,
                 'Expected integer values in interval'
                 f', got {type(interval[1])}.',
             )
diff --git a/bqskit/ir/iterator.py b/bqskit/ir/iterator.py
index 6e6659215..87e9a3bd2 100644
--- a/bqskit/ir/iterator.py
+++ b/bqskit/ir/iterator.py
@@ -1,6 +1,7 @@
 """This module implements the CircuitIterator class."""
 from __future__ import annotations
 
+import heapq
 from typing import Iterator
 from typing import Sequence
 from typing import Tuple
@@ -19,16 +20,125 @@
     from bqskit.ir.circuit import Circuit
 
 
-class CircuitIterator(
-    Iterator[
-        Union[
-            Operation,
-            Tuple[int, Operation],  # if and_cycles == True
-        ]
-    ],
-):
+class CircuitIterator(Iterator[Union[Operation, Tuple[int, Operation]]]):
+    """A CircuitIterator iterates through a circuit in a simulation order."""
+
+    def __init__(
+            self,
+            circuit: Circuit,
+            start: CircuitPointLike = CircuitPoint(0, 0),
+            end: CircuitPointLike | None = None,
+            qudits_or_region: CircuitRegionLike | Sequence[int] | None = None,
+            exclude: bool = False,
+            reverse: bool = False,
+            and_cycles: bool = False,
+    ) -> None:
+        """
+        Construct a CircuitIterator.
+
+        Args:
+            circuit (Circuit): The circuit to iterate through.
+
+            start (CircuitPointLike): Only iterate through points greater
+                than or equal to `start`. Defaults to start at the beginning
+                of the circuit. (Default: (0, 0))
+
+            end (CircuitPointLike | None): Only iterate through points
+                less than or equal to this. If left as None, iterates
+                until the end of the circuit. (Default: None)
+
+            qudits_or_region (CircuitRegionLike | Sequence[int] | None):
+                Determines the way the circuit is iterated. If a region
+                is given, then iterate through operations in the region.
+                If a sequence of qudit indices is given, then only iterate
+                the operations touching those qudits. If left as None,
+                then iterate through the entire circuit in simulation order.
+                (Default: None)
+
+            exclude (bool): If iterating through a region or only some
+                qudits and `exclude` is true, then do not yield operations
+                that are only partially in the region or on the desired
+                qudits. This may result in a sequence of operations that
+                does not occur in simulation order in the circuit.
+                (Default: False)
+
+            reverse (bool): Reverse the ordering. If true, then end acts
+                as start and vice versa. (Default: False)
+
+            and_cycles (bool): If true, in addition to the operation,
+                return the cycle index where it was found. (Default: False)
+        """
+        if (
+            start == (0, 0)
+            and end is None
+            and qudits_or_region is None
+            and not exclude
+            and not reverse
+        ):
+            # Default iteration can be done faster with the DAG Iterator
+            self.iter: CircuitIterator = CircuitDagIterator(circuit, and_cycles)
+
+        else:
+            self.iter = CircuitGridIterator(
+                circuit,
+                start,
+                end,
+                qudits_or_region,
+                exclude,
+                reverse,
+                and_cycles,
+            )
+
+    def __next__(self) -> Operation | tuple[int, Operation]:
+        return self.iter.__next__()
+
+    def __iter__(self) -> CircuitIterator:
+        return self.iter.__iter__()
+
+
+class CircuitDagIterator(CircuitIterator):
+    """Fast and simple iteration through circuit."""
+
+    def __init__(self, circuit: Circuit, and_cycles: bool = False):
+        """Set a CircuitDagIterator to iterate through `circuit`."""
+        self.circuit = circuit
+        self.and_cycles = and_cycles
+
+        # Track frontier for topological traversal
+        self.frontier = list(circuit.front)
+        heapq.heapify(self.frontier)
+
+        # Tracks how many of a gate's dependencies have been partitioned
+        self.prev_binned_counts = {n: 0 for n in circuit.front}
+
+    def __next__(self) -> Operation | tuple[int, Operation]:
+        if len(self.frontier) == 0:
+            raise StopIteration
+
+        point = heapq.heappop(self.frontier)
+        self.prev_binned_counts.pop(point)
+
+        # Update frontier
+        for successor in self.circuit.next(point):
+            if successor not in self.prev_binned_counts:
+                self.prev_binned_counts[successor] = 1
+            else:
+                self.prev_binned_counts[successor] += 1
+            num_prev_binned = self.prev_binned_counts[successor]
+            total_num_prev = len(self.circuit.prev(successor))
+            if num_prev_binned == total_num_prev:
+                heapq.heappush(self.frontier, successor)
+
+        op = self.circuit[point]
+        return (point.cycle, op) if self.and_cycles else op
+
+    def __iter__(self) -> CircuitIterator:
+        return self
+
+
+class CircuitGridIterator(CircuitIterator):
     """
-    The CircuitIterator Class.
+    The CircuitGridIterator Class.
 
     A CircuitIterator can iterate through a circuit in a few different ways. By
     default it can iterate through all operations in the circuit in simulation
@@ -49,7 +159,7 @@ def __init__(
             and_cycles: bool = False,
     ) -> None:
         """
-        Construct a CircuitIterator.
+        Construct a CircuitGridIterator.
 
         Args:
             circuit (Circuit): The circuit to iterate through.
diff --git a/bqskit/ir/lang/qasm2/parser.py b/bqskit/ir/lang/qasm2/parser.py
index ab15cc623..c5478682d 100644
--- a/bqskit/ir/lang/qasm2/parser.py
+++ b/bqskit/ir/lang/qasm2/parser.py
@@ -16,28 +16,29 @@
 EXP: "EXP"
 LN: "ln"
 SQRT: "sqrt"
+COMMENT: /\/\/+.*/
 mainprogram: "OPENQASM" REAL ";" program
 program: statement | program statement
 statement: decl
-            | gatedecl goplist "}"
-            | gatedecl "}"
+            | gatedecl goplist rbracket
+            | gatedecl rbracket
             | "opaque" ID idlist ";"
-            | "opaque" ID "( )" idlist ";"
+            | "opaque" ID "(" ")" idlist ";"
             | "opaque" ID "(" idlist ")" idlist ";"
             | qop
-            | "if (" ID "==" NNINTEGER ")" qop
+            | "if" "(" ID "==" NNINTEGER ")" qop
             | "barrier" anylist ";"
-            | "include" ESCAPED_STRING ";"
-            | /\/\/+.*/
+            | incstmt
+incstmt: "include" ESCAPED_STRING ";"
 decl: qreg | creg
 creg: "creg" ID "[" NNINTEGER "]" ";"
 qreg: "qreg" ID "[" NNINTEGER "]" ";"
 gatedecl: "gate" ID idlist "{"
-            | "gate" ID "( )" idlist "{"
+            | "gate" ID "(" ")" idlist "{"
             | "gate" ID "(" idlist ")" idlist "{"
-goplist: uop
+goplist: uopp
             | "barrier" idlist ";"
-            | goplist uop
+            | goplist uopp
             | goplist "barrier" idlist ";"
 qop: uop
         | measure
@@ -48,10 +49,18 @@
         | cxgate
         | gate
 gate: ID anylist ";"
-        | ID "( )" anylist ";"
+        | ID "(" ")" anylist ";"
         | ID "(" explist ")" anylist ";"
-ugate: "U (" explist ")" argument ";"
+ugate: "U" "(" explist ")" argument ";"
 cxgate: "CX" argument "," argument ";"
+uopp: ugatep
+        | cxgatep
+        | gatep
+gatep: ID anylist ";"
+        | ID "(" ")" anylist ";"
+        | ID "(" explist ")" anylist ";"
+ugatep: "U" "(" explist ")" argument ";"
+cxgatep: "CX" argument "," argument ";"
 anylist: idlist | mixedlist
 idlist: ID | idlist "," ID
 mixedlist: ID "[" NNINTEGER "]" | mixedlist "," ID
@@ -72,10 +81,12 @@
         | EXP
         | LN
         | SQRT
+rbracket: "}"
 
 %import common.ESCAPED_STRING
 %import common.WS
 %ignore WS
+%ignore COMMENT
 """,
     parser='lalr',
     start='mainprogram',
diff --git a/bqskit/ir/lang/qasm2/qasm2.py b/bqskit/ir/lang/qasm2/qasm2.py
index b468bf894..bd86a2bc1 100644
--- a/bqskit/ir/lang/qasm2/qasm2.py
+++ b/bqskit/ir/lang/qasm2/qasm2.py
@@ -6,7 +6,7 @@
 from bqskit.ir.lang.language import LangException
 from bqskit.ir.lang.language import Language
 from bqskit.ir.lang.qasm2.parser import parse
-from bqskit.ir.lang.qasm2.visitor import OPENQASMVisitor  # type: ignore
+from bqskit.ir.lang.qasm2.visitor import OPENQASMVisitor
 
 if TYPE_CHECKING:
     from bqskit.ir.circuit import Circuit
@@ -34,5 +34,5 @@ def decode(self, source: str) -> Circuit:
         """Parse `source` into a circuit."""
         tree = parse(source)
         visitor = OPENQASMVisitor()
-        visitor.visit(tree)
+        visitor.visit_topdown(tree)
         return visitor.get_circuit()
diff --git a/bqskit/ir/lang/qasm2/visitor.py b/bqskit/ir/lang/qasm2/visitor.py
index 41c4507f5..598d879db 100644
--- a/bqskit/ir/lang/qasm2/visitor.py
+++ b/bqskit/ir/lang/qasm2/visitor.py
@@ -1,10 +1,14 @@
-# type: ignore
 """This module implements a OPENQASM parse tree visitor."""
 from __future__ import annotations
 
 import logging
+import os
+from dataclasses import dataclass
 from typing import Any
+from typing import cast
+from typing import List
 from typing import NamedTuple
+from typing import Tuple
 
 import lark
 import numpy as np
@@ -12,23 +16,42 @@
 
 from bqskit.ir.circuit import Circuit
 from bqskit.ir.gate import Gate
+from bqskit.ir.gates.circuitgate import CircuitGate
 from bqskit.ir.gates.composed.controlled import ControlledGate
 from bqskit.ir.gates.composed.daggergate import DaggerGate
+from bqskit.ir.gates.constant.ccx import CCXGate
 from bqskit.ir.gates.constant.ch import CHGate
+from bqskit.ir.gates.constant.cs import CSGate
+from bqskit.ir.gates.constant.ct import CTGate
 from bqskit.ir.gates.constant.cx import CXGate
 from bqskit.ir.gates.constant.cy import CYGate
 from bqskit.ir.gates.constant.cz import CZGate
 from bqskit.ir.gates.constant.h import HGate
 from bqskit.ir.gates.constant.identity import IdentityGate
+from bqskit.ir.gates.constant.iswap import ISwapGate
+from bqskit.ir.gates.constant.itoffoli import IToffoliGate
 from bqskit.ir.gates.constant.s import SGate
 from bqskit.ir.gates.constant.sdg import SdgGate
+from bqskit.ir.gates.constant.sqrtcnot import SqrtCNOTGate
+from bqskit.ir.gates.constant.sqrtiswap import SqrtISwapGate
 from bqskit.ir.gates.constant.swap import SwapGate
 from bqskit.ir.gates.constant.sx import SXGate
+from bqskit.ir.gates.constant.sycamore import SycamoreGate
 from bqskit.ir.gates.constant.t import TGate
 from bqskit.ir.gates.constant.tdg import TdgGate
 from bqskit.ir.gates.constant.x import XGate
+from bqskit.ir.gates.constant.xx import XXGate
 from bqskit.ir.gates.constant.y import YGate
+from bqskit.ir.gates.constant.yy import YYGate
 from bqskit.ir.gates.constant.z import ZGate
+from bqskit.ir.gates.constant.zz import ZZGate
+from bqskit.ir.gates.measure import MeasurementPlaceholder
+from bqskit.ir.gates.parameterized.cp import CPGate
+from bqskit.ir.gates.parameterized.crx import CRXGate
+from bqskit.ir.gates.parameterized.cry import CRYGate
+from bqskit.ir.gates.parameterized.crz import CRZGate
+from bqskit.ir.gates.parameterized.fsim import FSIMGate
+from bqskit.ir.gates.parameterized.phasedxz import PhasedXZGate
 from bqskit.ir.gates.parameterized.rx import RXGate
 from bqskit.ir.gates.parameterized.rxx import RXXGate
 from bqskit.ir.gates.parameterized.ry import RYGate
@@ -36,11 +59,13 @@
 from bqskit.ir.gates.parameterized.rz import RZGate
 from bqskit.ir.gates.parameterized.rzz import RZZGate
 from bqskit.ir.gates.parameterized.u1 import U1Gate
+from bqskit.ir.gates.parameterized.u1q import U1qGate
 from bqskit.ir.gates.parameterized.u2 import U2Gate
 from bqskit.ir.gates.parameterized.u3 import U3Gate
 from bqskit.ir.lang.language import LangException
+from bqskit.ir.lang.qasm2.parser import parse
+from bqskit.ir.location import CircuitLocation
 from bqskit.ir.operation import Operation
-from bqskit.utils.typing import is_iterable
 
 
 _logger = logging.getLogger(__name__)
@@ -52,13 +77,75 @@ class QubitReg(NamedTuple):
     size: int
 
 
-class GateDef(NamedTuple):
+class ClassicalReg(NamedTuple):
+    """Definition of a Classical Bit Register."""
+    name: str
+    size: int
+
+
+@dataclass
+class GateDef:
     """Definition of a QASM Gate."""
     qasm_name: str
     num_params: int
     num_vars: int
     gate: Gate
 
+    def build_op(self, loc: CircuitLocation, params: list[float]) -> Operation:
+        """Build an operation for this gate."""
+        return Operation(self.gate, loc, params)
+
+
+@dataclass
+class CustomGateDef:
+    """Definition of a Custom QASM Gate."""
+    qasm_name: str
+    num_params: int
+    num_vars: int
+    gate_def_list: list[GateDef | CustomGateDef]
+    loc_list: list[CircuitLocation]
+    param_exp_list: list[list[lark.Tree | float]]
+
+    def build_op(self, loc: CircuitLocation, params: list[float]) -> Operation:
+        """Build an operation for this gate."""
+        cgc = Circuit(self.num_vars)
+        for i in range(len(self.gate_def_list)):
+            sublocation = self.loc_list[i]
+            subparams = self.evaluate_param_exps(self.param_exp_list[i], params)
+            cgc.append(self.gate_def_list[i].build_op(sublocation, subparams))
+        gate = CircuitGate(cgc, True)
+        return Operation(gate, loc, gate._circuit.params)
+
+    def evaluate_param_exps(
+        self,
+        param_exps: list[lark.Tree | float],
+        params: list[float],
+    ) -> list[float]:
+        """Substitute params into the place holders in param_exp and
+        evaluate."""
+        eval_params = []
+        for exp in param_exps:
+            if isinstance(exp, float):
+                eval_params.append(exp)
+            else:
+                subbed_exp = self.replace_param_indices(exp, params)
+                eval_params.append(float(eval_exp(subbed_exp)))
+        return eval_params
+
+    def replace_param_indices(
+        self,
+        exp: lark.Tree,
+        params: list[float],
+    ) -> lark.Tree:
+        """Return a new tree with parameter indices replaced with values."""
+        if isinstance(exp, lark.Token):
+            if exp.type == 'PARAM_IDX':
+                return lark.Token('REAL', params[int(exp)])
+            else:
+                return exp
+        children = [self.replace_param_indices(c, params) for c in exp.children]
+        return lark.Tree(exp.data, children)
+
 
 class OPENQASMVisitor(Visitor):
     """
@@ -69,11 +156,15 @@ class OPENQASMVisitor(Visitor):
     """
 
     def __init__(self) -> None:
+        """Initialize state for the OPENQASMVisitor."""
         self.op_list: list[Operation] = []
         self.qubit_regs: list[QubitReg] = []
         self.gate_defs: dict[str, GateDef] = {}
+        self.classical_regs: list[ClassicalReg] = []
+        self.gate_def_parsing_obj: Any = None
+        self.custom_gate_defs: dict[str, CustomGateDef] = {}
+        self.measurements: dict[int, tuple[str, int]] = {}
         self.fill_gate_defs()
-        self.gate_def_parsing_obj = None
 
     def get_circuit(self) -> Circuit:
         """Retrieve the circuit generated after walking a parse tree."""
@@ -82,71 +173,119 @@ def get_circuit(self) -> Circuit:
             raise LangException('No qubit registers defined.')
         circuit = Circuit(num_qubits)
         circuit.extend(self.op_list)
+
+        # Add measurements
+        if len(self.measurements) > 0:
+            cregs = cast(List[Tuple[str, int]], self.classical_regs)
+            mph = MeasurementPlaceholder(cregs, self.measurements)
+            circuit.append_gate(mph, list(self.measurements.keys()))
+
         return circuit
 
     def fill_gate_defs(self) -> None:
         """Prefills gate definitions with built-in gates."""
+        # Parameterized Gates
+        self.gate_defs['p'] = GateDef('p', 1, 1, RZGate())
+        self.gate_defs['cp'] = GateDef('cp', 1, 2, CPGate())
+        self.gate_defs['crx'] = GateDef('crx', 1, 2, CRXGate())
+        self.gate_defs['cry'] = GateDef('cry', 1, 2, CRYGate())
+        self.gate_defs['crz'] = GateDef('crz', 1, 2, CRZGate())
+        self.gate_defs['fsim'] = GateDef('fsim', 2, 2, FSIMGate())
+        self.gate_defs['rx'] = GateDef('rx', 1, 1, RXGate())
+        self.gate_defs['pxz'] = GateDef('pxz', 1, 3, PhasedXZGate())
+        self.gate_defs['rxx'] = GateDef('rxx', 1, 2, RXXGate())
+        self.gate_defs['ry'] = GateDef('ry', 1, 1, RYGate())
+        self.gate_defs['ryy'] = GateDef('ryy', 1, 2, RYYGate())
+        self.gate_defs['rz'] = GateDef('rz', 1, 1, RZGate())
+        self.gate_defs['rzz'] = GateDef('rzz', 1, 2, RZZGate())
+        self.gate_defs['u1'] = GateDef('u1', 1, 1, U1Gate())
+        self.gate_defs['u1q'] = GateDef('u1q', 1, 2, U1qGate())
+        self.gate_defs['U1q'] = GateDef('u1q', 1, 2, U1qGate())
+        self.gate_defs['u2'] = GateDef('u2', 2, 1, U2Gate())
+        self.gate_defs['u3'] = GateDef('u3', 3, 1, U3Gate())
         self.gate_defs['U'] = GateDef('U', 3, 1, U3Gate())
         self.gate_defs['u'] = GateDef('u', 3, 1, U3Gate())
-        self.gate_defs['u3'] = GateDef('u3', 3, 1, U3Gate())
-        self.gate_defs['u2'] = GateDef('u2', 2, 1, U2Gate())
-        self.gate_defs['u1'] = GateDef('u1', 1, 1, U1Gate())
+        self.gate_defs['cu1'] = GateDef('cu1', 1, 2, ControlledGate(U1Gate()))
+        self.gate_defs['cu2'] = GateDef('cu2', 2, 2, ControlledGate(U2Gate()))
+        self.gate_defs['cu3'] = GateDef('cu3', 3, 2, ControlledGate(U3Gate()))
+
+        # Constant Gates
+        self.gate_defs['ccx'] = GateDef('ccx', 0, 3, CCXGate())
+        self.gate_defs['ch'] = GateDef('ch', 0, 2, CHGate())
+        self.gate_defs['cs'] = GateDef('cs', 0, 2, CSGate())
+        self.gate_defs['ct'] = GateDef('ct', 0, 2, CTGate())
         self.gate_defs['cx'] = GateDef('cx', 0, 2, CXGate())
+        self.gate_defs['CX'] = GateDef('CX', 0, 2, CXGate())
         self.gate_defs['cy'] = GateDef('cy', 0, 2, CYGate())
         self.gate_defs['cz'] = GateDef('cz', 0, 2, CZGate())
-        self.gate_defs['ch'] = GateDef('ch', 0, 2, CHGate())
-        self.gate_defs['swap'] = GateDef('swap', 0, 2, SwapGate())
-        self.gate_defs['id'] = GateDef('id', 0, 1, IdentityGate(1))
-        self.gate_defs['x'] = GateDef('x', 0, 1, XGate())
-        self.gate_defs['y'] = GateDef('y', 0, 1, YGate())
-        self.gate_defs['z'] = GateDef('z', 0, 1, ZGate())
         self.gate_defs['h'] = GateDef('h', 0, 1, HGate())
+        self.gate_defs['id'] = GateDef('id', 0, 1, IdentityGate(1))
+        self.gate_defs['iswap'] = GateDef('iswap', 0, 2, ISwapGate())
+        self.gate_defs['iccx'] = GateDef('iccx', 0, 3, IToffoliGate())
         self.gate_defs['s'] = GateDef('s', 0, 1, SGate())
         self.gate_defs['sdg'] = GateDef('sdg', 0, 1, SdgGate())
+        self.gate_defs['csx'] = GateDef('csx', 0, 2, SqrtCNOTGate())
+        self.gate_defs['cv'] = GateDef('cv', 0, 2, SqrtCNOTGate())
+        self.gate_defs['sqisw'] = GateDef('sqisw', 0, 2, SqrtISwapGate())
+        self.gate_defs['swap'] = GateDef('swap', 0, 2, SwapGate())
+        self.gate_defs['sx'] = GateDef('sx', 0, 1, SXGate())
+        self.gate_defs['v'] = GateDef('v', 0, 1, SXGate())
+        self.gate_defs['syc'] = GateDef('syc', 0, 2, SycamoreGate())
         self.gate_defs['t'] = GateDef('t', 0, 1, TGate())
         self.gate_defs['tdg'] = GateDef('tdg', 0, 1, TdgGate())
-        self.gate_defs['rx'] = GateDef('rx', 1, 1, RXGate())
-        self.gate_defs['ry'] = GateDef('ry', 1, 1, RYGate())
-        self.gate_defs['rz'] = GateDef('rz', 1, 1, RZGate())
-        self.gate_defs['p'] = GateDef('p', 1, 1, RZGate())
-        self.gate_defs['sx'] = GateDef('sx', 0, 1, SXGate())
+        self.gate_defs['x'] = GateDef('x', 0, 1, XGate())
+        self.gate_defs['xx'] = GateDef('xx', 0, 2, XXGate())
+        self.gate_defs['y'] = GateDef('y', 0, 1, YGate())
+        self.gate_defs['yy'] = GateDef('yy', 0, 2, YYGate())
+        self.gate_defs['z'] = GateDef('z', 0, 1, ZGate())
+        self.gate_defs['zz'] = GateDef('zz', 0, 2, ZZGate())
         self.gate_defs['sxdg'] = GateDef('sxdg', 0, 1, DaggerGate(SXGate()))
-        self.gate_defs['rxx'] = GateDef('rxx', 1, 2, RXXGate())
-        self.gate_defs['ryy'] = GateDef('ryy', 1, 2, RYYGate())
-        self.gate_defs['rzz'] = GateDef('rzz', 1, 2, RZZGate())
-        self.gate_defs['cu1'] = GateDef('cu1', 1, 2, ControlledGate(U1Gate()))
-        self.gate_defs['cu2'] = GateDef('cu2', 2, 2, ControlledGate(U2Gate()))
-        self.gate_defs['cu3'] = GateDef('cu3', 3, 2, ControlledGate(U3Gate()))
 
-    def qreg(self, args: Any) -> None:
-        reg_name = args.children[0]
+    def qreg(self, tree: lark.Tree) -> None:
+        """Qubit register node visitor."""
+        reg_name = tree.children[0]
         if any(reg_name == reg.name for reg in self.qubit_regs):
             raise LangException('Qubit register redeclared: %s.' % reg_name)
-        reg_size = int(args.children[1])
+        reg_size = int(tree.children[1])
         reg = QubitReg(reg_name, reg_size)
         _logger.debug('Qubit register %s declared with size %d.' % reg)
         self.qubit_regs.append(reg)
 
-    def gate(self, args: Any) -> None:
-        gate_name = args.children[0]
-        param_list = None if len(args.children) == 2 else args.children[1]
-        if len(args.children) == 2:
-            var_list = args.children[1]
+    def gate(self, tree: lark.Tree) -> None:
+        """Apply a normal gate statement to the circuit."""
+        # Parse parameters
+        if len(tree.children) == 3:
+            exp_list = self.flatten_exps(tree.children[1])
+            params = [float(eval_exp(exp)) for exp in exp_list]
         else:
-            var_list = args.children[2]
+            params = []
 
-        if gate_name not in self.gate_defs:
-            raise LangException('Unrecognized gate: %s.' % gate_name)
+        # Parse location
+        location = []
+        for reg_name, reg_idx in qubits_from_list(tree.children[-1]):
+            outer_idx = 0
+            for reg in self.qubit_regs:
+                if reg.name == reg_name:
+                    location.append(outer_idx + reg_idx)
+                    break
+                outer_idx += reg.size
+        location = CircuitLocation(location)
 
-        gate_def = self.gate_defs[gate_name]
+        if any(q in self.measurements for q in location):
+            raise LangException(
+                'BQSKit currently does not support mid-circuit measurements.'
+                ' Unable to apply a gate on the same qubit where a measurement'
+                ' has been previously made.',
+            )
 
-        # Calculate params
-        params = []
-        if param_list is not None:
-            if is_iterable(eval_explist(param_list)):
-                params = [float(i) for i in eval_explist(param_list)]
-            else:
-                params = [float(eval_explist(param_list))]
+        # Parse gate object
+        gate_name = str(tree.children[0])
+        if gate_name in self.gate_defs:
+            gate_def: GateDef | CustomGateDef = self.gate_defs[gate_name]
+        elif gate_name in self.custom_gate_defs:
+            gate_def = self.custom_gate_defs[gate_name]
+        else:
+            raise LangException('Unrecognized gate: %s.' % gate_name)
 
         if len(params) != gate_def.num_params:
             raise LangException(
@@ -154,15 +293,104 @@ def gate(self, args: Any) -> None:
                 % (gate_def.num_params, len(params), gate_name),
             )
 
-        # Calculate location
-        location = []
-        for reg_name, reg_idx in qubits_from_list(var_list):
-            outer_idx = 0
-            for reg in self.qubit_regs:
-                if reg.name == reg_name:
-                    location.append(outer_idx + reg_idx)
-                    break
-                outer_idx += reg.num_qudits
+        if len(location) != gate_def.num_vars:
+            raise LangException(
+                'Gate acts on %d qubits, got %d qubit variables.'
+                % (gate_def.num_vars, len(location)),
+            )
+
+        # Build operation and add to circuit
+        self.op_list.append(gate_def.build_op(location, params))
+
+    def cxgate(self, tree: lark.Tree) -> None:
+        """CX gate node visitor."""
+        control = tree.children[0].children
+        target = tree.children[1].children
+        cname, cidx = str(control[0]), int(control[1])
+        tname, tidx = str(target[0]), int(target[1])
+
+        outer_idx = 0
+        cloc: int | None = None
+        tloc: int | None = None
+        for reg in self.qubit_regs:
+            if reg.name == cname:
+                cloc = outer_idx + cidx
+            if reg.name == tname:
+                tloc = outer_idx + tidx
+            outer_idx += reg.size
+
+        if cloc is None:
+            raise LangException(f'Qubit register not found: {cname}')
+
+        if tloc is None:
+            raise LangException(f'Qubit register not found: {tname}')
+
+        if cloc == tloc:
+            raise LangException('CX control and target qubit cannot be equal.')
+
+        loc = CircuitLocation([cloc, tloc])
+        self.op_list.append(Operation(CXGate(), loc))
+
+    def ugate(self, tree: lark.Tree) -> None:
+        """U gate node visitor."""
+        exp_list = self.flatten_exps(tree.children[0])
+        params = [float(eval_exp(exp)) for exp in exp_list]
+        qubit = tree.children[1].children
+        name, idx = str(qubit[0]), int(qubit[1])
+
+        outer_idx = 0
+        location: int | None = None
+        for reg in self.qubit_regs:
+            if reg.name == name:
+                location = outer_idx + idx
+                break
+            outer_idx += reg.size
+
+        if location is None:
+            raise LangException(f'Qubit register not found: {name}')
+
+        location = CircuitLocation(location)
+        self.op_list.append(self.gate_defs['U'].build_op(location, params))
+
+    def gatep(self, tree: lark.Tree) -> None:
+        """Apply a gate to a currently-being-built gate declaration."""
+        # Parse parameters or store their expression
+        param_exps = []
+        if len(tree.children) == 3:
+            exp_list = self.flatten_exps(tree.children[1])
+            for exp in exp_list:
+                if self.has_param_variable(exp):
+                    param_exps.append(self.replace_param_ids(exp))
+                else:
+                    param_exps.append(float(eval_exp(exp)))
+
+        self.gate_def_parsing_obj['param_exp_list'].append(param_exps)
+
+        # Parse location
+        location: list[int] = []
+        iter = tree.children[-1].children[0]
+        while iter:
+            qname = str(iter.children[-1])
+            qid = self.gate_def_parsing_obj['qubits'].index(qname)
+            location.insert(0, qid)
+            iter = None if len(iter.children) != 2 else iter.children[0]
+
+        self.gate_def_parsing_obj['loc_list'].append(location)
+
+        # Parse gate object
+        gate_name = str(tree.children[0])
+        if gate_name in self.gate_defs:
+            gate_def: GateDef | CustomGateDef = self.gate_defs[gate_name]
+        elif gate_name in self.custom_gate_defs:
+            gate_def = self.custom_gate_defs[gate_name]
+        else:
+            raise LangException('Unrecognized gate: %s.' % gate_name)
+
+        if len(param_exps) != gate_def.num_params:
+            raise LangException(
+                'Expected %d params got %d params for gate %s.'
+                % (gate_def.num_params, len(param_exps), gate_name),
+            )
 
         if len(location) != gate_def.num_vars:
             raise LangException(
@@ -170,26 +398,235 @@ def gate(self, args: Any) -> None:
                 % (gate_def.num_vars, len(location)),
             )
 
-        # Calculate operation
-        self.op_list.append(Operation(gate_def.gate, location, params))
+        self.gate_def_parsing_obj['gate_def_list'].append(gate_def)
+
+    def cxgatep(self, tree: lark.Tree) -> None:
+        """CX gate node visitor (while building custom gate)."""
+        control = tree.children[0].children
+        target = tree.children[1].children
+        cid = self.gate_def_parsing_obj['qubits'].index(str(control[0]))
+        tid = self.gate_def_parsing_obj['qubits'].index(str(target[0]))
+
+        if cid == tid:
+            raise LangException('CX control and target qubit cannot be equal.')
+
+        loc = CircuitLocation([cid, tid])
+        self.gate_def_parsing_obj['param_exp_list'].append([])
+        self.gate_def_parsing_obj['loc_list'].append(loc)
+        self.gate_def_parsing_obj['gate_def_list'].append(self.gate_defs['CX'])
+
+    def ugatep(self, tree: lark.Tree) -> None:
+        """U gate node visitor (while building custom gate)."""
+        param_exps = []
+        exp_list = self.flatten_exps(tree.children[0])
+        for exp in exp_list:
+            if self.has_param_variable(exp):
+                param_exps.append(self.replace_param_ids(exp))
+            else:
+                param_exps.append(float(eval_exp(exp)))
+
+        qubit = tree.children[1].children
+        qid = self.gate_def_parsing_obj['qubits'].index(str(qubit[0]))
+
+        location = CircuitLocation(qid)
+        self.gate_def_parsing_obj['param_exp_list'].append(param_exps)
+        self.gate_def_parsing_obj['loc_list'].append(location)
+        self.gate_def_parsing_obj['gate_def_list'].append(self.gate_defs['U'])
+
+    def rbracket(self, tree: lark.Tree) -> None:
+        """Finish a gate declaration block."""
+        qasm_name = self.gate_def_parsing_obj['name']
+        num_params = self.gate_def_parsing_obj['num_params']
+        num_vars = self.gate_def_parsing_obj['num_qubits']
+        gate_def_list = self.gate_def_parsing_obj['gate_def_list']
+        loc_list = self.gate_def_parsing_obj['loc_list']
+        param_exp_list = self.gate_def_parsing_obj['param_exp_list']
+        self.custom_gate_defs[qasm_name] = CustomGateDef(
+            qasm_name,
+            num_params,
+            num_vars,
+            gate_def_list,
+            loc_list,
+            param_exp_list,
+        )
+        self.gate_def_parsing_obj = None
 
-    def cxgate(self, args: Any) -> None:
-        self.gate(args)
+    def gatedecl(self, tree: lark.Tree) -> None:
+        """Start a gate declaration block."""
+        # parse gate name
+        self.gate_def_parsing_obj = {'name': str(tree.children[0])}
 
-    def ugate(self, args: Any) -> None:
-        self.gate(args)
+        # parse parameters
+        if len(tree.children) == 2:
+            self.gate_def_parsing_obj['num_params'] = 0
+            self.gate_def_parsing_obj['params'] = []
+        else:
+            params: list[str] = []
+            iter = tree.children[1]
+            while iter:
+                if isinstance(iter, lark.Tree):
+                    if len(iter.children) == 2:
+                        params.insert(0, str(iter.children[1]))
+                        iter = iter.children[0]
+                    else:
+                        params.insert(0, str(iter.children[0]))
+                        iter = None
+                else:
+                    params.insert(0, str(iter))
+                    iter = None
+
+            self.gate_def_parsing_obj['num_params'] = len(params)
+            self.gate_def_parsing_obj['params'] = params
+
+        # parse qubits
+        qubits: list[str] = []
+        iter = tree.children[-1]
+        while iter:
+            if isinstance(iter, lark.Tree):
+                if len(iter.children) == 2:
+                    qubits.insert(0, str(iter.children[1]))
+                    iter = iter.children[0]
+                else:
+                    qubits.insert(0, str(iter.children[0]))
+                    iter = None
+            else:
+                qubits.insert(0, str(iter))
+                iter = None
+
+        self.gate_def_parsing_obj['num_qubits'] = len(qubits)
+        self.gate_def_parsing_obj['qubits'] = qubits
+        self.gate_def_parsing_obj['gate_def_list'] = []
+        self.gate_def_parsing_obj['loc_list'] = []
+        self.gate_def_parsing_obj['param_exp_list'] = []
+
+    def flatten_exps(self, explist: lark.Tree) -> list[lark.Tree]:
+        """Flatten an explist tree into a list of exp subtrees."""
+        exp_list: list[lark.Tree] = []
+        iter = explist
+        while iter:
+            exp_list.insert(0, iter.children[-1])
+            iter = None if len(iter.children) == 1 else iter.children[0]
+        return exp_list
+
+    def has_param_variable(self, exp: lark.Tree) -> bool:
+        """Return true is exp is bound to gatedecl param variable."""
+        if isinstance(exp, lark.Token):
+            return str(exp) in self.gate_def_parsing_obj['params']
+        return any(self.has_param_variable(c) for c in exp.children)
+
+    def replace_param_ids(self, exp: lark.Tree) -> lark.Tree:
+        """Return a new tree with parameter ids replaces with indices."""
+        if isinstance(exp, lark.Token):
+            if str(exp) in self.gate_def_parsing_obj['params']:
+                index = self.gate_def_parsing_obj['params'].index(str(exp))
+                return lark.Token('PARAM_IDX', index)
+            else:
+                return exp
+        children = [self.replace_param_ids(c) for c in exp.children]
+        return lark.Tree(exp.data, children)
+
+    def incstmt(self, tree: lark.Tree) -> None:
+        """Include statement node."""
+        file_name = str(tree.children[0])
+        if file_name[0] == "\"":
+            file_name = file_name[1:]
+        if file_name[-1] == "\"":
+            file_name = file_name[:-1]
+
+        if not os.path.isfile(file_name):
+            if file_name != 'qelib1.inc':
+                # Suppress warnings over qelib1.inc due to high frequency
+                _logger.warning(
+                    f'Unable to find {file_name} used in a QASM include'
+                    ' statement. Ignoring include file and continuing.',
+                )
+            return
+
+        with open(file_name) as f:
+            filedata = f.read()
+            if not filedata.startswith('OPENQASM'):
+                filedata = 'OPENQASM 2.0;\n' + filedata
+            tree = parse(filedata)
+            visitor = OPENQASMVisitor()
+            visitor.visit_topdown(tree)
+            self.op_list.extend(visitor.op_list)
+            self.qubit_regs.extend(visitor.qubit_regs)
+            self.custom_gate_defs.update(visitor.custom_gate_defs)
+
+    def creg(self, tree: lark.Tree) -> None:
+        """Classical bit register node visitor."""
+        reg_name = tree.children[0]
+        if any(reg_name == reg.name for reg in self.classical_regs):
+            raise LangException('Classical register redeclared: %s.' % reg_name)
+        reg_size = int(tree.children[1])
+        reg = ClassicalReg(reg_name, reg_size)
+        _logger.debug('Classical register %s declared with size %d.' % reg)
+        self.classical_regs.append(reg)
+
+    def measure(self, tree: lark.Tree) -> None:
+        """Measure statement node visitor."""
+        qubit_childs = tree.children[0].children
+        class_childs = tree.children[1].children
+        qubit_reg_name = str(qubit_childs[0])
+        class_reg_name = str(class_childs[0])
+        if not any(r.name == qubit_reg_name for r in self.qubit_regs):
+            raise LangException(
+                f'Measuring undefined qubit register: {qubit_reg_name}',
+            )
 
-    def gatedecl(self, args: Any) -> None:
-        _logger.warning('Ignoring unsupported qasm feature: gate declaration.')
+        if not any(r.name == class_reg_name for r in self.classical_regs):
+            raise LangException(
+                f'Measuring undefined classical register: {class_reg_name}',
+            )
 
-    def creg(self, args: Any) -> None:
-        _logger.warning('Ignoring unsupported qasm feature: classic register.')
+        if len(qubit_childs) == 1 and len(class_childs) == 1:
+            for name, size in self.qubit_regs:
+                if qubit_reg_name == name:
+                    qubit_size = size
+                    break
+            for name, size in self.classical_regs:
+                if class_reg_name == name:
+                    class_size = size
+                    break
+            if qubit_size != class_size:
+                raise LangException(
+                    'Size mismatch between qubit and classical register'
+                    f' during measure operation: {qubit_size} != {class_size}.',
+                )
+
+            outer_idx = 0
+            for name, size in self.qubit_regs:
+                if name == qubit_reg_name:
+                    break
+                outer_idx += size
 
-    def measure(self, args: Any) -> None:
-        _logger.warning('Ignoring unsupported qasm feature: measure.')
+            for i in range(qubit_size):
+                self.measurements[outer_idx + i] = (class_reg_name, i)
+
+        elif len(qubit_childs) == 2 and len(class_childs) == 2:
+            qubit_index = int(qubit_childs[1])
+            class_index = int(class_childs[1])
+
+            # Convert qubit_index to global index
+            outer_idx = 0
+            for name, size in self.qubit_regs:
+                if name == qubit_reg_name:
+                    qubit_index = outer_idx + qubit_index
+                    break
+                outer_idx += size
+
+            self.measurements[qubit_index] = (class_reg_name, class_index)
+
+        else:
+            raise LangException(
+                'Invalid measurement: either a single qubit is being measured '
+                'to a full classical register or a qubit register is being '
+                'measured to a single classical bit.',
+            )
 
-    def reset(self, args: Any) -> None:
-        _logger.warning('Ignoring unsupported qasm feature: reset.')
+    def reset(self, tree: lark.Tree) -> None:
+        """Reset statement node visitor."""
+        raise LangException('BQSKit currently does not support resets.')
 
 
 eval_locals = {
@@ -255,5 +692,7 @@ def qubits_from_list(tree: lark.Tree) -> list[tuple[str, int]]:
             q1 = qubits_from_list(tree.children[0])
             return [*q1, q2]
         return [q2]
+    elif tree.data == 'argument':
+        return [(str(tree.children[0]), int(tree.children[1]))]
     else:
         raise Exception()
diff --git a/bqskit/ir/location.py b/bqskit/ir/location.py
index e114d0573..347d9c045 100644
--- a/bqskit/ir/location.py
+++ b/bqskit/ir/location.py
@@ -111,6 +111,19 @@ def intersection(self, other: CircuitLocationLike) -> CircuitLocation:
             return CircuitLocation([other] if other in self else [])
         return CircuitLocation([x for x in self if x in other])  # type: ignore
 
+    @property
+    def pairs(self) -> set[tuple[int, int]]:
+        """Return all pairs of unique elements in the location."""
+        _pairs = set()
+        for q1 in self:
+            for q2 in self:
+                if q1 == q2:
+                    continue
+                a = min(q1, q2)
+                b = max(q1, q2)
+                _pairs.add((a, b))
+        return _pairs
+
     @staticmethod
     def is_location(
         location: Any,
@@ -146,14 +159,16 @@ def is_location(
             location = list(location)
 
         else:
-            _logger.debug(
+            _logger.log(
+                0,
                 f'Expected integer(s) for location, got {type(location)}.',
             )
             return False
 
         if not is_sequence_of_int(location):
             checks = [is_integer(q) for q in location]
-            _logger.debug(
+            _logger.log(
+                0,
                 'Expected iterable of positive integers for location'
                 f', got atleast one {type(location[checks.index(False)])}.',
             )
@@ -161,19 +176,20 @@ def is_location(
 
         if not all(qudit_index >= 0 for qudit_index in location):
             checks = [qudit_index >= 0 for qudit_index in location]
-            _logger.debug(
+            _logger.log(
+                0,
                 'Expected iterable of positive integers for location'
                 f', got atleast one {location[checks.index(False)]}.',
             )
             return False
 
         if len(set(location)) != len(location):
-            _logger.debug('Location has duplicate qudit indices.')
+            _logger.log(0, 'Location has duplicate qudit indices.')
             return False
 
         if num_qudits is not None:
             if not all([qudit < num_qudits for qudit in location]):
-                _logger.debug('Location has an erroneously large qudit.')
+                _logger.log(0, 'Location has an erroneously large qudit.')
                 return False
 
         return True
diff --git a/bqskit/ir/operation.py b/bqskit/ir/operation.py
index 38592512e..ecbedbb19 100644
--- a/bqskit/ir/operation.py
+++ b/bqskit/ir/operation.py
@@ -98,11 +98,7 @@ def get_qasm(self) -> str:
         else:
             full_params = self.params
 
-        return '{}({}) q[{}];\n'.format(
-            self.gate.qasm_name,
-            ', '.join([str(p) for p in full_params]),
-            '], q['.join([str(q) for q in self.location]),
-        ).replace('()', '')
+        return self.gate.get_qasm(full_params, self.location)
 
     def get_unitary(self, params: RealVector = []) -> UnitaryMatrix:
         """Return the unitary for this gate, see :class:`Unitary` for more."""
diff --git a/bqskit/ir/opt/__init__.py b/bqskit/ir/opt/__init__.py
index 2d0aeee79..ad915823f 100644
--- a/bqskit/ir/opt/__init__.py
+++ b/bqskit/ir/opt/__init__.py
@@ -12,7 +12,9 @@
 from bqskit.ir.opt.instantiaters import Minimization
 from bqskit.ir.opt.instantiaters import QFactor
 from bqskit.ir.opt.minimizer import Minimizer
+from bqskit.ir.opt.minimizers import CeresMinimizer
 from bqskit.ir.opt.minimizers import LBFGSMinimizer
+from bqskit.ir.opt.minimizers import ScipyMinimizer
 from bqskit.ir.opt.multistartgen import MultiStartGenerator
 from bqskit.ir.opt.multistartgens import DiagonalStartGenerator
 from bqskit.ir.opt.multistartgens import RandomStartGenerator
@@ -32,4 +34,6 @@
     'MultiStartGenerator',
     'DiagonalStartGenerator',
     'RandomStartGenerator',
+    'CeresMinimizer',
+    'ScipyMinimizer',
 ]
diff --git a/bqskit/ir/opt/minimizers/__init__.py b/bqskit/ir/opt/minimizers/__init__.py
index 79e43aaac..e4605d596 100644
--- a/bqskit/ir/opt/minimizers/__init__.py
+++ b/bqskit/ir/opt/minimizers/__init__.py
@@ -1,6 +1,8 @@
 """This package includes circuit minimizer implementations."""
 from __future__ import annotations
 
+from bqskit.ir.opt.minimizers.ceres import CeresMinimizer
 from bqskit.ir.opt.minimizers.lbfgs import LBFGSMinimizer
+from bqskit.ir.opt.minimizers.scipy import ScipyMinimizer
 
-__all__ = ['LBFGSMinimizer']
+__all__ = ['LBFGSMinimizer', 'CeresMinimizer', 'ScipyMinimizer']
diff --git a/bqskit/ir/opt/minimizers/scipy.py b/bqskit/ir/opt/minimizers/scipy.py
new file mode 100644
index 000000000..f6f125901
--- /dev/null
+++ b/bqskit/ir/opt/minimizers/scipy.py
@@ -0,0 +1,37 @@
+"""This module implements the ScipyMinimizer class."""
+from __future__ import annotations
+
+from typing import TYPE_CHECKING
+
+import numpy as np
+from scipy.optimize import minimize
+
+from bqskit.ir.opt.minimizer import Minimizer
+
+if TYPE_CHECKING:
+    from bqskit.ir.opt.cost import CostFunction
+    from bqskit.qis import RealVector
+    import numpy.typing as npt
+
+
+class ScipyMinimizer(Minimizer):
+    """A minimizer that uses scipy's 'minimize' function."""
+
+    def __init__(self, tol: float = 1e-10) -> None:
+        """
+        Construct a ScipyMinimizer by passing all arguments to scipy.
+
+        Args:
+            tol (float): Set the tolerance used during scipy's minimize.
+        """
+        self.tol = tol
+
+    def minimize(
+        self,
+        cost: CostFunction,
+        x0: RealVector,
+    ) -> npt.NDArray[np.float64]:
+        """Solve the minimization problem by using scipy."""
+        if len(x0) == 0:
+            return np.array([])
+        return minimize(cost, x0, tol=self.tol).x
diff --git a/bqskit/ir/point.py b/bqskit/ir/point.py
index 42f558e1b..a44d1b1f5 100644
--- a/bqskit/ir/point.py
+++ b/bqskit/ir/point.py
@@ -85,24 +85,33 @@ def is_point(point: Any) -> TypeGuard[CircuitPointLike]:
             return True
 
         if not isinstance(point, tuple):
-            _logger.debug('Point is not a tuple.')
+            _logger.log(0, 'Point is not a tuple.')
             return False
 
         if len(point) != 2:
-            _logger.debug(
-                'Expected point to contain two values, got %d.' % len(point),
+            _logger.log(
+                0,
+                'Expected point to contain two values, got %d.' % len(
+                    point,
+                ),
             )
             return False
 
         if not is_integer(point[0]):
-            _logger.debug(
-                'Expected integer values in point, got %s.' % type(point[0]),
+            _logger.log(
+                0,
+                'Expected integer values in point, got %s.' % type(
+                    point[0],
+                ),
             )
             return False
 
         if not is_integer(point[1]):
-            _logger.debug(
-                'Expected integer values in point, got %s.' % type(point[1]),
+            _logger.log(
+                0,
+                'Expected integer values in point, got %s.' % type(
+                    point[1],
+                ),
             )
             return False
 
diff --git a/bqskit/ir/region.py b/bqskit/ir/region.py
index 01584816b..78197e973 100644
--- a/bqskit/ir/region.py
+++ b/bqskit/ir/region.py
@@ -466,15 +466,15 @@ def is_region(region: Any) -> TypeGuard[CircuitRegionLike]:
             return True
 
         if not is_mapping(region):
-            _logger.debug('Region is not a mapping.')
+            _logger.log(0, 'Region is not a mapping.')
             return False
 
         if not all(is_integer(key) for key in region.keys()):
-            _logger.debug('Region does not have integer keys.')
+            _logger.log(0, 'Region does not have integer keys.')
             return False
 
         if not all(CycleInterval.is_interval(val) for val in region.values()):
-            _logger.debug('Region does not have IntervalLike values.')
+            _logger.log(0, 'Region does not have IntervalLike values.')
             return False
 
         return True
diff --git a/bqskit/passes/__init__.py b/bqskit/passes/__init__.py
index 2d5dafdbf..abd05aaed 100644
--- a/bqskit/passes/__init__.py
+++ b/bqskit/passes/__init__.py
@@ -32,6 +32,7 @@
     QSearchSynthesisPass
     QFASTDecompositionPass
     QPredictDecompositionPass
+    SynthesisPass
 
 .. rubric:: Processing Passes
 
@@ -41,14 +42,14 @@
 
     ExhaustiveGateRemovalPass
     IterativeScanningGateRemovalPass
+    Rebase2QuditGatePass
     ScanningGateRemovalPass
-    WindowOptimizationPass
     SubstitutePass
+    WindowOptimizationPass
 
 .. rubric:: Control Passes
 
-This are passes that are used to manage control flow inside of a
-compilation task.
+This are passes that are used to manage control flow inside of a workflow.
 
 .. autosummary::
     :toctree: autogen
@@ -74,23 +75,42 @@
     GateCountPredicate
     NotPredicate
     WidthPredicate
+    PhysicalPredicate
+    SinglePhysicalPredicate
+    MultiPhysicalPredicate
 
 .. rubric:: Rule-based Passes
 
+These passes apply fixed rules to a circuit to transform it.
+
 .. autosummary::
     :toctree: autogen
     :recursive:
 
+    CHToCNOTPass
     CNOTToCZPass
+    CNOTToCHPass
+    CNOTToCYPass
+    CYToCNOTPass
+    SwapToCNOTPass
+    U3Decomposition
     ZXZXZDecomposition
 
-.. rubric:: Layout Passes
+.. rubric:: Mapping Passes
+
+These passes either perform qubit placement, layout, routing or otherwise
+are involved the qubit mapping process.
 
 .. autosummary::
     :toctree: autogen
     :recursive:
 
-    SimpleLayoutPass
+    GeneralizedSabreLayoutPass
+    GreedyPlacementPass
+    TrivialPlacementPass
+    GeneralizedSabreRoutingPass
+    SetModelPass
+    ApplyPlacement
 
 .. rubric:: Utility Passes
 
@@ -105,6 +125,10 @@
     UpdateDataPass
     ToU3Pass
     BlockConversionPass
+    LogPass
+    ExtendBlockSizePass
+    LogErrorPass
+    FillSingleQuditGatesPass
 
 .. rubric:: IO Passes
 
@@ -130,19 +154,24 @@
 
 .. rubric:: Search Layer Generators
 
+Layer generators can be used to modify how search based synthesis
+algorithms extend circuit templates.
+
 .. autosummary::
     :toctree: autogen
     :recursive:
 
-    LayerGenerator
-    SimpleLayerGenerator
+    FourParamGenerator
+    MiddleOutLayerGenerator
     SeedLayerGenerator
-    StairLayerGenerator
+    SimpleLayerGenerator
     SingleQuditLayerGenerator
-    MiddleOutLayerGenerator
+    StairLayerGenerator
+    WideLayerGenerator
 """
 from __future__ import annotations
 
+from bqskit.passes.alias import PassAlias
 from bqskit.passes.control.dothendecide import DoThenDecide
 from bqskit.passes.control.dowhileloop import DoWhileLoopPass
 from bqskit.passes.control.foreach import ForEachBlockPass
@@ -151,14 +180,26 @@
 from bqskit.passes.control.predicate import PassPredicate
 from bqskit.passes.control.predicates.change import ChangePredicate
 from bqskit.passes.control.predicates.count import GateCountPredicate
+from bqskit.passes.control.predicates.multi import MultiPhysicalPredicate
 from bqskit.passes.control.predicates.notpredicate import NotPredicate
+from bqskit.passes.control.predicates.physical import PhysicalPredicate
+from bqskit.passes.control.predicates.single import SinglePhysicalPredicate
 from bqskit.passes.control.predicates.width import WidthPredicate
 from bqskit.passes.control.whileloop import WhileLoopPass
+from bqskit.passes.group import PassGroup
 from bqskit.passes.io.checkpoint import LoadCheckpointPass
 from bqskit.passes.io.checkpoint import SaveCheckpointPass
 from bqskit.passes.io.intermediate import RestoreIntermediatePass
 from bqskit.passes.io.intermediate import SaveIntermediatePass
-from bqskit.passes.layout.simple import SimpleLayoutPass
+from bqskit.passes.mapping.apply import ApplyPlacement
+from bqskit.passes.mapping.layout.sabre import GeneralizedSabreLayoutPass
+from bqskit.passes.mapping.placement.greedy import GreedyPlacementPass
+from bqskit.passes.mapping.placement.trivial import TrivialPlacementPass
+from bqskit.passes.mapping.routing.sabre import GeneralizedSabreRoutingPass
+from bqskit.passes.mapping.setmodel import SetModelPass
+from bqskit.passes.measure import ExtractMeasurements
+from bqskit.passes.measure import RestoreMeasurements
+from bqskit.passes.noop import NOOPPass
 from bqskit.passes.partitioning.cluster import ClusteringPartitioner
 from bqskit.passes.partitioning.greedy import GreedyPartitioner
 from bqskit.passes.partitioning.quick import QuickPartitioner
@@ -166,18 +207,23 @@
 from bqskit.passes.partitioning.single import GroupSingleQuditGatePass
 from bqskit.passes.processing.exhaustive import ExhaustiveGateRemovalPass
 from bqskit.passes.processing.iterative import IterativeScanningGateRemovalPass
+from bqskit.passes.processing.rebase import Rebase2QuditGatePass
 from bqskit.passes.processing.scan import ScanningGateRemovalPass
 from bqskit.passes.processing.substitute import SubstitutePass
 from bqskit.passes.processing.window import WindowOptimizationPass
 from bqskit.passes.rules.cnot2cz import CNOTToCZPass
+from bqskit.passes.rules.swap2cnot import SwapToCNOTPass
+from bqskit.passes.rules.u3 import U3Decomposition
 from bqskit.passes.rules.zxzxz import ZXZXZDecomposition
 from bqskit.passes.search.frontier import Frontier
 from bqskit.passes.search.generator import LayerGenerator
+from bqskit.passes.search.generators.fourparam import FourParamGenerator
 from bqskit.passes.search.generators.middleout import MiddleOutLayerGenerator
 from bqskit.passes.search.generators.seed import SeedLayerGenerator
 from bqskit.passes.search.generators.simple import SimpleLayerGenerator
 from bqskit.passes.search.generators.single import SingleQuditLayerGenerator
 from bqskit.passes.search.generators.stair import StairLayerGenerator
+from bqskit.passes.search.generators.wide import WideLayerGenerator
 from bqskit.passes.search.heuristic import HeuristicFunction
 from bqskit.passes.search.heuristics.astar import AStarHeuristic
 from bqskit.passes.search.heuristics.dijkstra import DijkstraHeuristic
@@ -190,11 +236,16 @@
 from bqskit.passes.util.compress import CompressPass
 from bqskit.passes.util.conversion import BlockConversionPass
 from bqskit.passes.util.converttou3 import ToU3Pass
+from bqskit.passes.util.extend import ExtendBlockSizePass
+from bqskit.passes.util.fill import FillSingleQuditGatesPass
+from bqskit.passes.util.log import LogErrorPass
+from bqskit.passes.util.log import LogPass
 from bqskit.passes.util.random import SetRandomSeedPass
 from bqskit.passes.util.record import RecordStatsPass
 from bqskit.passes.util.unfold import UnfoldPass
 from bqskit.passes.util.update import UpdateDataPass
 
+
 __all__ = [
     'DoWhileLoopPass',
     'ForEachBlockPass',
@@ -219,7 +270,6 @@
     'UnfoldPass',
     'UpdateDataPass',
     'ToU3Pass',
-    'SimpleLayoutPass',
     'ScanningGateRemovalPass',
     'WindowOptimizationPass',
     'SimpleLayerGenerator',
@@ -247,4 +297,28 @@
     'ExhaustiveGateRemovalPass',
     'WidthPredicate',
     'IterativeScanningGateRemovalPass',
+    'SetModelPass',
+    'PassAlias',
+    'PassGroup',
+    'NOOPPass',
+    'FourParamGenerator',
+    'WideLayerGenerator',
+    'SwapToCNOTPass',
+    'GeneralizedSabreLayoutPass',
+    'GreedyPlacementPass',
+    'TrivialPlacementPass',
+    'GeneralizedSabreRoutingPass',
+    'SetModelPass',
+    'U3Decomposition',
+    'PhysicalPredicate',
+    'SinglePhysicalPredicate',
+    'Rebase2QuditGatePass',
+    'LogPass',
+    'ExtractMeasurements',
+    'RestoreMeasurements',
+    'ExtendBlockSizePass',
+    'ApplyPlacement',
+    'MultiPhysicalPredicate',
+    'LogErrorPass',
+    'FillSingleQuditGatesPass',
 ]
diff --git a/bqskit/passes/control/__init__.py b/bqskit/passes/control/__init__.py
index ccf01ccaf..013a0e790 100644
--- a/bqskit/passes/control/__init__.py
+++ b/bqskit/passes/control/__init__.py
@@ -9,7 +9,9 @@
 from bqskit.passes.control.predicate import PassPredicate
 from bqskit.passes.control.predicates.change import ChangePredicate
 from bqskit.passes.control.predicates.count import GateCountPredicate
+from bqskit.passes.control.predicates.multi import MultiPhysicalPredicate
 from bqskit.passes.control.predicates.notpredicate import NotPredicate
+from bqskit.passes.control.predicates.physical import PhysicalPredicate
 from bqskit.passes.control.predicates.width import WidthPredicate
 from bqskit.passes.control.whileloop import WhileLoopPass
 
@@ -25,4 +27,6 @@
     'DoThenDecide',
     'ParallelDo',
     'WidthPredicate',
+    'PhysicalPredicate',
+    'MultiPhysicalPredicate',
 ]
diff --git a/bqskit/passes/control/foreach.py b/bqskit/passes/control/foreach.py
index f1506296a..9df8f20e1 100644
--- a/bqskit/passes/control/foreach.py
+++ b/bqskit/passes/control/foreach.py
@@ -1,16 +1,11 @@
 """This module implements the ForEachBlockPass class."""
 from __future__ import annotations
 
-import copy
 import logging
 from typing import Any
 from typing import Callable
 from typing import Sequence
 
-from distributed import get_client
-from distributed import rejoin
-from distributed import secede
-
 from bqskit.compiler.basepass import BasePass
 from bqskit.compiler.machine import MachineModel
 from bqskit.ir.circuit import Circuit
@@ -20,6 +15,7 @@
 from bqskit.ir.gates.parameterized.unitary import VariableUnitaryGate
 from bqskit.ir.operation import Operation
 from bqskit.ir.point import CircuitPoint
+from bqskit.utils.typing import is_integer
 from bqskit.utils.typing import is_sequence
 
 _logger = logging.getLogger(__name__)
@@ -27,11 +23,10 @@
 
 class ForEachBlockPass(BasePass):
     """
-    The ForEachBlockPass class.
+    A pass that executes other passes on each block in the circuit.
 
     This is a control pass that executes another pass or passes on every block
-    in the circuit. This will be done in parallel if executed in the compiler
-    framework.
+    in the circuit. This will be done in parallel if run from a compiler.
     """
 
     key = 'ForEachBlockPass_data'
@@ -43,6 +38,7 @@ def __init__(
         calculate_error_bound: bool = False,
         collection_filter: Callable[[Operation], bool] | None = None,
         replace_filter: Callable[[Circuit, Operation], bool] | None = None,
+        batch_size: int = 1,
     ) -> None:
         """
         Construct a ForEachBlockPass.
@@ -72,6 +68,9 @@ def __init__(
                 operation will be replaced with the new circuit.
                 Defaults to always replace.
 
+            batch_size (int): The amount of blocks to batch in one job.
+                If zero, batch all blocks in one job. (Default: 1).
+
         Raises:
             ValueError: If a Sequence[BasePass] is given, but it is empty.
         """
@@ -92,10 +91,23 @@ def __init__(
             if len(loop_body) == 0:
                 raise ValueError('Expected at least one pass.')
 
+        if not is_integer(batch_size):
+            raise TypeError(
+                'Expected integer for batch_size'
+                f', got {type(batch_size)}',
+            )
+
+        if batch_size < 0:
+            raise ValueError(
+                'Expected nonnegative integer for batch_size'
+                f', got {batch_size}',
+            )
+
         self.loop_body = loop_body if is_sequence(loop_body) else [loop_body]
         self.calculate_error_bound = calculate_error_bound
         self.collection_filter = collection_filter or default_collection_filter
         self.replace_filter = replace_filter or default_replace_filter
+        self.batch_size = batch_size
 
         if not callable(self.collection_filter):
             raise TypeError(
@@ -115,7 +127,6 @@ def run(self, circuit: Circuit, data: dict[str, Any] = {}) -> None:
         # Make room in data for block data
         if self.key not in data:
             data[self.key] = []
-        data[self.key].append([])
 
         # Collect blocks
         blocks: list[tuple[int, Operation]] = []
@@ -124,11 +135,10 @@ def run(self, circuit: Circuit, data: dict[str, Any] = {}) -> None:
                 blocks.append((cycle, op))
 
         # Get the machine model
-        model = BasePass.get_model(circuit, data)
+        model = self.get_model(circuit, data)
+        coupling_graph = self.get_connectivity(circuit, data)
 
-        # Go through the blocks
-        points: list[CircuitPoint] = []
-        ops: list[Operation] = []
+        # Preprocess blocks
         subcircuits: list[Circuit] = []
         block_datas: list[dict[str, Any]] = []
         for i, (cycle, op) in enumerate(blocks):
@@ -142,72 +152,65 @@ def run(self, circuit: Circuit, data: dict[str, Any] = {}) -> None:
                 subcircuit = Circuit.from_operation(op)
 
             # Form Subtopology
+            subradixes = [circuit.radixes[q] for q in op.location]
             subnumbering = {op.location[i]: i for i in range(len(op.location))}
             submodel = MachineModel(
                 len(op.location),
-                model.coupling_graph.get_subgraph(op.location, subnumbering),
+                coupling_graph.get_subgraph(op.location, subnumbering),
+                model.gate_set,
+                subradixes,
             )
 
-            # Record Data Part 1
-            block_data['op'] = copy.deepcopy(op)
-            block_data['subcircuit_pre'] = subcircuit.copy()
+            # Form subdata
             block_data['subnumbering'] = subnumbering
             block_data['machine_model'] = submodel
-            if 'parallel' in data:
-                block_data['parallel'] = data['parallel']
+            block_data['parallel'] = self.in_parallel(data)
+            block_data['point'] = CircuitPoint(cycle, op.location[0])
+            block_data['calculate_error_bound'] = self.calculate_error_bound
 
             subcircuits.append(subcircuit)
             block_datas.append(block_data)
 
-        # Perform Work
-        if 'parallel' in data:  # In Parallel
-            client = get_client()
-            completed_subcircuits = []
-            completed_block_datas = []
-            subc_futures = client.scatter(subcircuits)
-            data_futures = client.scatter(block_datas)
-            futures = client.map(
-                _sub_do_work,
-                [self.loop_body] * len(subc_futures),
-                subc_futures,
-                data_futures,
-            )
-            secede()
-            client.gather(futures)
-            rejoin()
-            for future in futures:
-                x, y = future.result()
-                completed_subcircuits.append(x)
-                completed_block_datas.append(y)
-
-        else:  # Sequentially
-            completed_subcircuits = []
-            completed_block_datas = []
-            for subcircuit, block_data in zip(subcircuits, block_datas):
-                x, y = _sub_do_work(self.loop_body, subcircuit, block_data)
-                completed_subcircuits.append(x)
-                completed_block_datas.append(y)
-
-        # Process work
+        # Group them into batches
+        if self.batch_size == 0:
+            batched_subcircuits = [subcircuits]
+            batched_block_datas = [block_datas]
+        else:
+            batched_subcircuits = [
+                subcircuits[i:i + self.batch_size]
+                for i in range(0, len(blocks), self.batch_size)
+            ]
+            batched_block_datas = [
+                block_datas[i:i + self.batch_size]
+                for i in range(0, len(blocks), self.batch_size)
+            ]
+
+        # Do the work
+        results = self.execute(
+            data,
+            _sub_do_work,
+            [self.loop_body] * len(batched_subcircuits),
+            batched_subcircuits,
+            batched_block_datas,
+        )
+
+        # Unpack results
+        completed_subcircuits, completed_block_datas = [], []
+        for batch in results:
+            completed_subcircuits.extend(list(zip(*batch))[0])
+            completed_block_datas.extend(list(zip(*batch))[1])
+
+        # Postprocess blocks
+        points: list[CircuitPoint] = []
+        ops: list[Operation] = []
+        error_sum = 0.0
         for i, (cycle, op) in enumerate(blocks):
             subcircuit = completed_subcircuits[i]
             block_data = completed_block_datas[i]
-            # Record Data Part 2
-            block_data['subcircuit_post'] = subcircuit.copy()
-            block_data['loop_body_data'] = block_data
-            block_data['point'] = CircuitPoint(cycle, op.location[0])
-
-            # Calculate Errors
-            if self.calculate_error_bound:
-                new_utry = subcircuit.get_unitary()
-                old_utry = op.get_unitary()
-                error = new_utry.get_distance_from(old_utry)
-                block_data['error'] = error
-                _logger.info(f'Block {i} has error {error}.')
 
             # Mark Blocks to be Replaced
             if self.replace_filter(subcircuit, op):
-                _logger.info(f'Replacing block {i}.')
+                _logger.debug(f'Replacing block {i}.')
                 points.append(CircuitPoint(cycle, op.location[0]))
                 ops.append(
                     Operation(
@@ -217,15 +220,27 @@ def run(self, circuit: Circuit, data: dict[str, Any] = {}) -> None:
                     ),
                 )
                 block_data['replaced'] = True
+
+                # Calculate Error
+                if self.calculate_error_bound:
+                    error_sum += block_data['error']
             else:
                 block_data['replaced'] = False
 
-            # Record block data into pass data
-            data[self.key][-1].append(block_data)
-
         # Replace blocks
         circuit.batch_replace(points, ops)
 
+        # Record block data into pass data
+        data[self.key].append(completed_block_datas)
+
+        # Record error
+        if self.calculate_error_bound:
+            if 'error' in data:
+                data['error'] += error_sum
+            else:
+                data['error'] = error_sum
+            _logger.debug(f"New circuit error is {data['error']}.")
+
 
 def default_collection_filter(op: Operation) -> bool:
     return isinstance(
@@ -244,9 +259,21 @@ def default_replace_filter(circuit: Circuit, op: Operation) -> bool:
 
 def _sub_do_work(
     loop_body: Sequence[BasePass],
-    subcircuit: Circuit,
-    subdata: dict[str, Any],
-) -> tuple[Circuit, dict[str, Any]]:
-    for loop_pass in loop_body:
-        loop_pass.run(subcircuit, subdata)
-    return subcircuit, subdata
+    subcircuits: list[Circuit],
+    subdatas: list[dict[str, Any]],
+) -> list[tuple[Circuit, dict[str, Any]]]:
+    results = []
+    for subcircuit, subdata in zip(subcircuits, subdatas):
+        if subdata['calculate_error_bound']:
+            old_utry = subcircuit.get_unitary()
+
+        for loop_pass in loop_body:
+            loop_pass.run(subcircuit, subdata)
+
+        if subdata['calculate_error_bound']:
+            new_utry = subcircuit.get_unitary()
+            error = new_utry.get_distance_from(old_utry)
+            subdata['error'] = error
+
+        results.append((subcircuit, subdata))
+    return results
diff --git a/bqskit/passes/control/ifthenelse.py b/bqskit/passes/control/ifthenelse.py
index 738e3cd10..f3e98e93c 100644
--- a/bqskit/passes/control/ifthenelse.py
+++ b/bqskit/passes/control/ifthenelse.py
@@ -79,6 +79,9 @@ def __init__(
                         'Expected at least one pass for false branch.',
                     )
 
+        if on_false is None:
+            on_false = []
+
         self.condition = condition
         self.on_true = on_true if is_sequence(on_true) else [on_true]
         self.on_false = on_false if is_sequence(on_false) else [on_false]
diff --git a/bqskit/passes/control/predicates/__init__.py b/bqskit/passes/control/predicates/__init__.py
index 35400138f..8438a7e9f 100644
--- a/bqskit/passes/control/predicates/__init__.py
+++ b/bqskit/passes/control/predicates/__init__.py
@@ -3,7 +3,10 @@
 
 from bqskit.passes.control.predicates.change import ChangePredicate
 from bqskit.passes.control.predicates.count import GateCountPredicate
+from bqskit.passes.control.predicates.multi import MultiPhysicalPredicate
 from bqskit.passes.control.predicates.notpredicate import NotPredicate
+from bqskit.passes.control.predicates.physical import PhysicalPredicate
+from bqskit.passes.control.predicates.single import SinglePhysicalPredicate
 from bqskit.passes.control.predicates.width import WidthPredicate
 
 __all__ = [
@@ -11,4 +14,7 @@
     'GateCountPredicate',
     'NotPredicate',
     'WidthPredicate',
+    'PhysicalPredicate',
+    'SinglePhysicalPredicate',
+    'MultiPhysicalPredicate',
 ]
diff --git a/bqskit/passes/control/predicates/count.py b/bqskit/passes/control/predicates/count.py
index bf494dcff..bc19b6bdf 100644
--- a/bqskit/passes/control/predicates/count.py
+++ b/bqskit/passes/control/predicates/count.py
@@ -3,10 +3,12 @@
 
 import logging
 from typing import Any
+from typing import Sequence
 
 from bqskit.ir.circuit import Circuit
 from bqskit.ir.gate import Gate
 from bqskit.passes.control.predicate import PassPredicate
+from bqskit.utils.typing import is_sequence
 
 _logger = logging.getLogger(__name__)
 
@@ -21,17 +23,20 @@ class GateCountPredicate(PassPredicate):
 
     key = 'GateCountPredicate_circuit_count'
 
-    def __init__(self, gate: Gate) -> None:
+    def __init__(self, gate: Gate | Sequence[Gate]) -> None:
         """Construct a GateCountPredicate."""
 
-        if not isinstance(gate, Gate):
+        if isinstance(gate, Gate):
+            gate = [gate]
+
+        if not is_sequence(gate) or not all(isinstance(g, Gate) for g in gate):
             raise TypeError(f'Expected gate, got {type(gate)}')
 
         self.gate = gate
 
     def get_truth_value(self, circuit: Circuit, data: dict[str, Any]) -> bool:
         """Call this predicate, see :class:`PassPredicate` for more info."""
-        gate_count = circuit.count(self.gate)
+        gate_count = sum(circuit.count(g) for g in self.gate)
 
         # If first call, record data and return true
         if self.key not in data:
diff --git a/bqskit/passes/control/predicates/multi.py b/bqskit/passes/control/predicates/multi.py
new file mode 100644
index 000000000..c841b07e3
--- /dev/null
+++ b/bqskit/passes/control/predicates/multi.py
@@ -0,0 +1,32 @@
+"""This module implements the MultiPhysicalPredicate class."""
+from __future__ import annotations
+
+import logging
+from typing import Any
+
+from bqskit.compiler.basepass import BasePass
+from bqskit.ir.circuit import Circuit
+from bqskit.passes.control.predicate import PassPredicate
+
+_logger = logging.getLogger(__name__)
+
+
+class MultiPhysicalPredicate(PassPredicate):
+    """
+    The MultiPhysicalPredicate class.
+
+    The MultiPhysicalPredicate returns true if circuit's multi-qudit gates are
+    in the native gate set.
+    """
+
+    def get_truth_value(self, circuit: Circuit, data: dict[str, Any]) -> bool:
+        """Call this predicate, see :class:`PassPredicate` for more info."""
+        model = BasePass.get_model(circuit, data)
+        for gate in circuit.gate_set:
+            if gate.num_qudits < 2:
+                continue
+            if gate not in model.gate_set:
+                _logger.debug(f'{gate} not in native set: {model.gate_set}.')
+                return False
+
+        return True
diff --git a/bqskit/passes/control/predicates/physical.py b/bqskit/passes/control/predicates/physical.py
new file mode 100644
index 000000000..fe62eb5d5
--- /dev/null
+++ b/bqskit/passes/control/predicates/physical.py
@@ -0,0 +1,44 @@
+"""This module implements the PhysicalPredicate class."""
+from __future__ import annotations
+
+import logging
+from typing import Any
+
+from bqskit.compiler.basepass import BasePass
+from bqskit.ir.circuit import Circuit
+from bqskit.passes.control.predicate import PassPredicate
+
+_logger = logging.getLogger(__name__)
+
+
+class PhysicalPredicate(PassPredicate):
+    """
+    The PhysicalPredicate class.
+
+    The PhysicalPredicate returns true if circuit's coupling graph and gate set
+    matches the workflow's machine model.
+    """
+
+    def get_truth_value(self, circuit: Circuit, data: dict[str, Any]) -> bool:
+        """Call this predicate, see :class:`PassPredicate` for more info."""
+        model = BasePass.get_model(circuit, data)
+        for gate in circuit.gate_set:
+            if gate not in model.gate_set:
+                _logger.debug(f'{gate} not in native set: {model.gate_set}.')
+                return False
+
+        if circuit.num_qudits == 1:
+            return True
+
+        try:
+            placement = BasePass.get_placement(circuit, data)
+            subgraph = model.coupling_graph.get_subgraph(placement)
+            if not subgraph.is_fully_connected():
+                _logger.debug('Qudits are disconnected on the machine model.')
+                return False
+
+        except RuntimeError:
+            _logger.debug('Qudits are disconnected on the machine model.')
+            return False
+
+        return True
diff --git a/bqskit/passes/control/predicates/single.py b/bqskit/passes/control/predicates/single.py
new file mode 100644
index 000000000..0c38af083
--- /dev/null
+++ b/bqskit/passes/control/predicates/single.py
@@ -0,0 +1,32 @@
+"""This module implements the PhysicalPredicate class."""
+from __future__ import annotations
+
+import logging
+from typing import Any
+
+from bqskit.compiler.basepass import BasePass
+from bqskit.ir.circuit import Circuit
+from bqskit.passes.control.predicate import PassPredicate
+
+_logger = logging.getLogger(__name__)
+
+
+class SinglePhysicalPredicate(PassPredicate):
+    """
+    The SinglePhysicalPredicate class.
+
+    The SinglePhysicalPredicate returns true if circuit's single-qudit gates are
+    in the native gate set.
+    """
+
+    def get_truth_value(self, circuit: Circuit, data: dict[str, Any]) -> bool:
+        """Call this predicate, see :class:`PassPredicate` for more info."""
+        model = BasePass.get_model(circuit, data)
+        for gate in circuit.gate_set:
+            if gate.num_qudits > 1:
+                continue
+            if gate not in model.gate_set:
+                _logger.debug(f'{gate} not in native set: {model.gate_set}.')
+                return False
+
+        return True
diff --git a/bqskit/passes/group.py b/bqskit/passes/group.py
index a5a9d44dc..51bae81dd 100644
--- a/bqskit/passes/group.py
+++ b/bqskit/passes/group.py
@@ -3,7 +3,7 @@
 from typing import cast
 from typing import Sequence
 
-from bqskit.compiler import BasePass
+from bqskit.compiler.basepass import BasePass
 from bqskit.passes.alias import PassAlias
 from bqskit.utils.typing import is_sequence
 
diff --git a/bqskit/passes/layout/__init__.py b/bqskit/passes/layout/__init__.py
deleted file mode 100644
index 7f3c6326e..000000000
--- a/bqskit/passes/layout/__init__.py
+++ /dev/null
@@ -1,6 +0,0 @@
-"""This package implements passes that perform qudit assignment."""
-from __future__ import annotations
-
-from bqskit.passes.layout.simple import SimpleLayoutPass
-
-__all__ = ['SimpleLayoutPass']
diff --git a/bqskit/passes/layout/simple.py b/bqskit/passes/layout/simple.py
deleted file mode 100644
index f60cf14df..000000000
--- a/bqskit/passes/layout/simple.py
+++ /dev/null
@@ -1,47 +0,0 @@
-"""This module implements the SimpleLayoutPass class."""
-from __future__ import annotations
-
-from typing import Any
-
-from bqskit.compiler.basepass import BasePass
-from bqskit.compiler.machine import MachineModel
-from bqskit.ir.circuit import Circuit
-
-
-class SimpleLayoutPass(BasePass):
-    """
-    Assigns logical qudits to physical qudits.
-
-    Layout also encodes a machine model into the compilation workflow.
-    """
-
-    def __init__(self, model: MachineModel | None = None) -> None:
-        """
-        Construct a SimpleLayoutPass.
-
-        Args:
-            model (MachineModel | None): The machine model to use for
-                layout and to encode into the compilation workflow.
-                Defaults to an all-to-all model matching the size of the
-                circuit.
-        """
-        if model is not None and not isinstance(model, MachineModel):
-            raise TypeError(f'Expected MachineModel, got: {type(model)}.')
-
-        self.model = model
-
-    def run(self, circuit: Circuit, data: dict[str, Any] = {}) -> None:
-        """Perform the pass's operation, see :class:`BasePass` for more."""
-        if self.model is None:
-            data['machine_model'] = MachineModel(circuit.num_qudits)
-            return
-
-        if self.model.num_qudits < circuit.num_qudits:
-            raise RuntimeError('Machine model is too small for circuit.')
-
-        qudits = list(range(circuit.num_qudits))
-        graph = self.model.coupling_graph.get_subgraph(qudits)
-        if not graph.is_fully_connected():
-            raise RuntimeError('Simple layout creates a disconnected graph.')
-
-        data['machine_model'] = MachineModel(circuit.num_qudits, graph)
diff --git a/bqskit/passes/mapping/__init__.py b/bqskit/passes/mapping/__init__.py
new file mode 100644
index 000000000..6d30aa3e2
--- /dev/null
+++ b/bqskit/passes/mapping/__init__.py
@@ -0,0 +1,18 @@
+"""This package implements passes that perform qudit assignment."""
+from __future__ import annotations
+
+from bqskit.passes.mapping.apply import ApplyPlacement
+from bqskit.passes.mapping.layout.sabre import GeneralizedSabreLayoutPass
+from bqskit.passes.mapping.placement.greedy import GreedyPlacementPass
+from bqskit.passes.mapping.placement.trivial import TrivialPlacementPass
+from bqskit.passes.mapping.routing.sabre import GeneralizedSabreRoutingPass
+from bqskit.passes.mapping.setmodel import SetModelPass
+
+__all__ = [
+    'GeneralizedSabreLayoutPass',
+    'GreedyPlacementPass',
+    'TrivialPlacementPass',
+    'GeneralizedSabreRoutingPass',
+    'SetModelPass',
+    'ApplyPlacement',
+]
diff --git a/bqskit/passes/mapping/apply.py b/bqskit/passes/mapping/apply.py
new file mode 100644
index 000000000..060786778
--- /dev/null
+++ b/bqskit/passes/mapping/apply.py
@@ -0,0 +1,26 @@
+"""This module implements the ApplyPlacement class."""
+from __future__ import annotations
+
+import logging
+from typing import Any
+
+from bqskit.compiler.basepass import BasePass
+from bqskit.ir.circuit import Circuit
+
+_logger = logging.getLogger(__name__)
+
+
+class ApplyPlacement(BasePass):
+    """Place the circuit on the machine model."""
+
+    def run(self, circuit: Circuit, data: dict[str, Any] = {}) -> None:
+        """Perform the pass's operation, see :class:`BasePass` for more."""
+        model = self.get_model(circuit, data)
+        placement = self.get_placement(circuit, data)
+        physical_circuit = Circuit(model.num_qudits, model.radixes)
+        physical_circuit.append_circuit(circuit, placement)
+        circuit.become(physical_circuit)
+        if 'final_mapping' in data:
+            pi = data['final_mapping']
+            data['final_mapping'] = [placement[p] for p in pi]
+        data['placement'] = list(i for i in range(model.num_qudits))
diff --git a/bqskit/passes/mapping/layout/__init__.py b/bqskit/passes/mapping/layout/__init__.py
new file mode 100644
index 000000000..c053a05a8
--- /dev/null
+++ b/bqskit/passes/mapping/layout/__init__.py
@@ -0,0 +1,11 @@
+"""
+This subpackage implements Layout algorithms.
+
+Layout algorithms are responsible for finding the best initial mapping of
+logical to physical qudits.
+"""
+from __future__ import annotations
+
+from bqskit.passes.mapping.layout.sabre import GeneralizedSabreLayoutPass
+
+__all__ = ['GeneralizedSabreLayoutPass']
diff --git a/bqskit/passes/mapping/layout/sabre.py b/bqskit/passes/mapping/layout/sabre.py
new file mode 100644
index 000000000..c2587bc8a
--- /dev/null
+++ b/bqskit/passes/mapping/layout/sabre.py
@@ -0,0 +1,83 @@
+"""This module implements the GeneralizedSabreLayoutPass class."""
+from __future__ import annotations
+
+import logging
+from typing import Any
+
+from bqskit.compiler.basepass import BasePass
+from bqskit.ir.circuit import Circuit
+from bqskit.passes.mapping.sabre import GeneralizedSabreAlgorithm
+
+_logger = logging.getLogger(__name__)
+
+
+class GeneralizedSabreLayoutPass(BasePass, GeneralizedSabreAlgorithm):
+    """
+    Uses the Sabre algorithm to layout the circuit.
+
+    See :class:`GeneralizedSabreAlgorithm` for more info.
+    """
+
+    def __init__(
+        self,
+        total_passes: int = 1,
+        decay_delta: float = 0.001,
+        decay_reset_interval: int = 5,
+        decay_reset_on_gate: bool = True,
+        extended_set_size: int = 20,
+        extended_set_weight: float = 0.5,
+    ) -> None:
+        """
+        Construct a GeneralizedSabreLayoutPass.
+
+        Args:
+            total_passes (int): The amount of forward and backward passes
+                to apply before finalizing the layout.
+
+            decay_delta (float): See :class:`GeneralizedSabreAlgorithm`
+                for info. (Default: 0.001)
+
+            decay_reset_interval (int): See :class:`GeneralizedSabreAlgorithm`
+                for info. (Default: 5)
+
+            decay_reset_on_gate (bool): See :class:`GeneralizedSabreAlgorithm`
+                for info. (Default: True)
+
+            extended_set_size (int): See :class:`GeneralizedSabreAlgorithm`
+                for info. (Default: 20)
+
+            extended_set_weight (float): See :class:`GeneralizedSabreAlgorithm`
+                for info. (Default: 0.5)
+        """
+        if not isinstance(total_passes, int):
+            raise TypeError(
+                'Expected int for total_passes'
+                f', got {type(total_passes)}',
+            )
+
+        if total_passes < 1:
+            raise ValueError('Total passes must be a positive integer.')
+
+        self.total_passes = total_passes
+        super().__init__(
+            decay_delta,
+            decay_reset_interval,
+            decay_reset_on_gate,
+            extended_set_size,
+            extended_set_weight,
+        )
+
+    def run(self, circuit: Circuit, data: dict[str, Any] = {}) -> None:
+        """Perform the pass's operation, see :class:`BasePass` for more."""
+        subgraph = self.get_connectivity(circuit, data)
+        if not subgraph.is_fully_connected():
+            raise RuntimeError('Cannot layout circuit on disconnected qudits.')
+
+        pi = [i for i in range(circuit.num_qudits)]
+        for _ in range(self.total_passes):
+            self.forward_pass(circuit, pi, subgraph)
+            self.backward_pass(circuit, pi, subgraph)
+
+        # select qubits
+        _logger.info(f'Found layout: {str(pi)}')
+        data['initial_mapping'] = pi
diff --git a/bqskit/passes/mapping/placement/__init__.py b/bqskit/passes/mapping/placement/__init__.py
new file mode 100644
index 000000000..bad954a86
--- /dev/null
+++ b/bqskit/passes/mapping/placement/__init__.py
@@ -0,0 +1,12 @@
+"""
+This subpackage implements Placement algorithms.
+
+Placement algorithms are responsible for selecting which set of physical qubits
+to start the circuit on.
+"""
+from __future__ import annotations
+
+from bqskit.passes.mapping.placement.greedy import GreedyPlacementPass
+from bqskit.passes.mapping.placement.trivial import TrivialPlacementPass
+
+__all__ = ['GreedyPlacementPass', 'TrivialPlacementPass']
diff --git a/bqskit/passes/mapping/placement/greedy.py b/bqskit/passes/mapping/placement/greedy.py
new file mode 100644
index 000000000..7e91a13e6
--- /dev/null
+++ b/bqskit/passes/mapping/placement/greedy.py
@@ -0,0 +1,55 @@
+"""This module implements the GreedyPlacementPass class."""
+from __future__ import annotations
+
+import logging
+from typing import Any
+
+import numpy as np
+
+from bqskit.compiler.basepass import BasePass
+from bqskit.ir.circuit import Circuit
+_logger = logging.getLogger(__name__)
+
+
+class GreedyPlacementPass(BasePass):
+    """Find a placement by starting with the most connected qudit."""
+
+    def run(self, circuit: Circuit, data: dict[str, Any] = {}) -> None:
+        """Perform the pass's operation, see :class:`BasePass` for more."""
+
+        # Find physical qudit with highest degree
+        graph = self.get_model(circuit, data).coupling_graph
+        degrees = np.array(graph.get_qudit_degrees())
+        highest_degree_qudit = int(np.argmax(degrees))
+
+        # Search for placement by greedily adding best neighbor
+        placement = [highest_degree_qudit]
+        neighbors = graph.get_neighbors_of(highest_degree_qudit)
+        while len(placement) < circuit.num_qudits:
+            # Score each neighbor of current placement set
+            best_score = None
+            best_neighbor = None
+            for q in neighbors:
+                inter = [n for n in graph.get_neighbors_of(q) if n in placement]
+                lookahead = sum(degrees[n] for n in graph.get_neighbors_of(q))
+                score = (len(inter), degrees[q], lookahead)
+                if best_score is None or score > best_score:
+                    best_score = score
+                    best_neighbor = q
+
+            # Add best scoring neighbor to placement
+            assert best_neighbor is not None
+            neighbors.remove(best_neighbor)
+            placement.append(best_neighbor)
+            for n in graph.get_neighbors_of(best_neighbor):
+                if n not in placement and n not in neighbors:
+                    neighbors.append(n)
+
+        data['placement'] = sorted(placement)
+
+        _logger.info(f'Placed qudits on {data["placement"]}')
+
+        # Raise an error if this is not a valid placement
+        sg = graph.get_subgraph(data['placement'])
+        if not sg.is_fully_connected():
+            raise RuntimeError('No valid placement found.')
diff --git a/bqskit/passes/mapping/placement/trivial.py b/bqskit/passes/mapping/placement/trivial.py
new file mode 100644
index 000000000..cbea3fb9e
--- /dev/null
+++ b/bqskit/passes/mapping/placement/trivial.py
@@ -0,0 +1,28 @@
+"""This module implements the TrivialPlacementPass class."""
+from __future__ import annotations
+
+import logging
+from typing import Any
+
+from bqskit.compiler.basepass import BasePass
+from bqskit.ir.circuit import Circuit
+_logger = logging.getLogger(__name__)
+
+
+class TrivialPlacementPass(BasePass):
+    """Place the logical qubits on the first n physical qubits."""
+
+    def run(self, circuit: Circuit, data: dict[str, Any] = {}) -> None:
+        """Perform the pass's operation, see :class:`BasePass` for more."""
+
+        # Select the first n physical qubits for placement
+        trivial_placement = list(range(circuit.num_qudits))
+        model = BasePass.get_model(circuit, data)
+        data['placement'] = trivial_placement
+
+        _logger.info(f'Placed qudits on {data["placement"]}')
+
+        # Raise an error if this is not a valid placement
+        sg = model.coupling_graph.get_subgraph(data['placement'])
+        if not sg.is_fully_connected():
+            raise RuntimeError('The trivial placement is not valid.')
diff --git a/bqskit/passes/mapping/routing/__init__.py b/bqskit/passes/mapping/routing/__init__.py
new file mode 100644
index 000000000..56a55b87b
--- /dev/null
+++ b/bqskit/passes/mapping/routing/__init__.py
@@ -0,0 +1,10 @@
+"""
+This subpackage implements Routing algorithms.
+
+Routing algorithms are responsible routing logical gates via physical gates.
+"""
+from __future__ import annotations
+
+from bqskit.passes.mapping.routing.sabre import GeneralizedSabreRoutingPass
+
+__all__ = ['GeneralizedSabreRoutingPass']
diff --git a/bqskit/passes/mapping/routing/sabre.py b/bqskit/passes/mapping/routing/sabre.py
new file mode 100644
index 000000000..bb930b6d3
--- /dev/null
+++ b/bqskit/passes/mapping/routing/sabre.py
@@ -0,0 +1,36 @@
+"""This module implements the GeneralizedSabreRoutingPass class."""
+from __future__ import annotations
+
+import copy
+import logging
+from typing import Any
+
+from bqskit.compiler.basepass import BasePass
+from bqskit.ir.circuit import Circuit
+from bqskit.passes.mapping.sabre import GeneralizedSabreAlgorithm
+
+
+_logger = logging.getLogger(__name__)
+
+
+class GeneralizedSabreRoutingPass(BasePass, GeneralizedSabreAlgorithm):
+    """
+    Uses the Sabre algorithm to route the circuit.
+
+    See :class:`GeneralizedSabreAlgorithm` for more info.
+    """
+
+    def run(self, circuit: Circuit, data: dict[str, Any] = {}) -> None:
+        """Perform the pass's operation, see :class:`BasePass` for more."""
+        subgraph = self.get_connectivity(circuit, data)
+        if not subgraph.is_fully_connected():
+            raise RuntimeError('Cannot route circuit on disconnected qudits.')
+
+        if 'initial_mapping' in data:
+            pi = copy.deepcopy(data['initial_mapping'])
+        else:
+            pi = [i for i in range(circuit.num_qudits)]
+
+        self.forward_pass(circuit, pi, subgraph, modify_circuit=True)
+        data['final_mapping'] = pi
+        _logger.info(f'Finished routing with layout: {str(pi)}')
diff --git a/bqskit/passes/mapping/sabre.py b/bqskit/passes/mapping/sabre.py
new file mode 100644
index 000000000..9d31fedbb
--- /dev/null
+++ b/bqskit/passes/mapping/sabre.py
@@ -0,0 +1,437 @@
+"""This module implements the GeneralizedSabreAlgorithm class."""
+from __future__ import annotations
+
+import copy
+import logging
+
+import numpy as np
+
+from bqskit.ir.circuit import Circuit
+from bqskit.ir.gates import SwapGate
+from bqskit.ir.operation import Operation
+from bqskit.ir.point import CircuitPoint
+from bqskit.qis.graph import CouplingGraph
+
+
+_logger = logging.getLogger(__name__)
+
+
+class GeneralizedSabreAlgorithm():
+    """
+    Implements methods for Sabre-based layout and routing algorithms using a
+    modified heuristic to accommodate larger than 2-qudit gates.
+
+    References:
+        Gushu Li, Yufei Ding, and Yuan Xie. 2019. Tackling the Qubit
+        Mapping Problem for NISQ-Era Quantum Devices. In Proceedings of
+        the 24th ACM International Conference on Architectural
+        Support for Programming Languages and Operating Systems
+        (ASPLOS 2019). Association for Computing Machinery, New York, NY,
+        USA, 1001-1014. https://doi.org/10.1145/3297858.3304023
+
+        Casey Duckering, Jonathan M. Baker, Andrew Litteken, and Frederic
+        T. Chong. 2021. Orchestrated trios: compiling for efficient
+        communication in Quantum programs with 3-Qubit gates. In Proceedings
+        of the 26th ACM International Conference on Architectural Support
+        for Programming Languages and Operating Systems (ASPLOS 2021).
+        Association for Computing Machinery, New York, NY, USA, 375-385.
+        https://doi.org/10.1145/3445814.3446718
+    """
+
+    def __init__(
+        self,
+        decay_delta: float = 0.001,
+        decay_reset_interval: int = 5,
+        decay_reset_on_gate: bool = True,
+        extended_set_size: int = 20,
+        extended_set_weight: float = 0.5,
+    ) -> None:
+        """
+        Construct a GeneralizedSabreAlgorithm.
+
+        Args:
+            decay_delta (float): The amount to adjust the decay factor by
+                each time a swap is applied. Set to zero to disable decay.
+                (Default: 0.001)
+
+            decay_reset_interval (int): The amount of swaps to apply before
+                reseting the decay factors. (Default: 5)
+
+            decay_reset_on_gate (bool): If true, reset decay factors when
+                a logical gate is applied. (Default: True)
+
+            extended_set_size (int): The size of the look-ahead or extended
+                set. Set to zero to disable look ahead. (Default: 20)
+
+            extended_set_weight (float): The weight on the extended set
+                term when scoring potential swaps. (Default: 0.5)
+        """
+        if not isinstance(decay_delta, float):
+            raise TypeError(
+                'Expected float for decay_delta'
+                f', got {type(decay_delta)}',
+            )
+
+        if not isinstance(decay_reset_interval, int):
+            raise TypeError(
+                'Expected int for decay_reset_interval'
+                f', got {type(decay_reset_interval)}',
+            )
+
+        if not isinstance(decay_reset_on_gate, bool):
+            raise TypeError(
+                'Expected bool for decay_reset_on_gate'
+                f', got {type(decay_reset_on_gate)}',
+            )
+
+        if not isinstance(extended_set_size, int):
+            raise TypeError(
+                'Expected int for extended_set_size'
+                f', got {type(extended_set_size)}',
+            )
+
+        if not isinstance(extended_set_weight, float):
+            raise TypeError(
+                'Expected float for extended_set_weight'
+                f', got {type(extended_set_weight)}',
+            )
+
+        if decay_reset_interval < 1:
+            raise ValueError('Decay reset interval must be a positive integer.')
+
+        if extended_set_size < 0:
+            raise ValueError('Extended set size must be a nonnegative integer.')
+
+        self.decay_delta = decay_delta
+        self.decay_reset_interval = decay_reset_interval
+        self.decay_reset_on_gate = decay_reset_on_gate
+        self.extended_set_size = extended_set_size
+        self.extended_set_weight = extended_set_weight
+
+    def forward_pass(
+        self,
+        circuit: Circuit,
+        pi: list[int],
+        cg: CouplingGraph,
+        modify_circuit: bool = False,
+    ) -> None:
+        """
+        Apply a forward pass of the Sabre algorithm to `pi`.
+
+        Args:
+            circuit (Circuit): The circuit to pass over.
+
+            pi (list[int]): The input logical-to-physical mapping. This
+                maps logical qudits to physical qudits. So, `pi[l] == p`
+                implies logical qudit `l` is sitting on physical qudit `p`.
+
+            cg (CouplingGraph): The connectivity of the hardware.
+
+            modfiy_circuit (bool): Whether to modify the circuit as the
+                pass is applied or not. (Default: False)
+        """
+        # Preprocessing
+        D = cg.all_pairs_shortest_path()
+        F = circuit.front
+        decay = [1.0 for i in range(circuit.num_qudits)]
+        iter_count = 0
+        prev_swap = (-1, -1)
+        prev_executed_counts: dict[CircuitPoint, int] = {n: 0 for n in F}
+        _logger.debug(f'Starting forward sabre pass with pi: {pi}.')
+
+        if modify_circuit:
+            mapped_circuit = Circuit(circuit.num_qudits, circuit.radixes)
+
+        # Main Loop
+        while len(F) > 0:
+
+            # Retrieve executable gates giving the current mapping `pi`
+            execute_list = [n for n in F if self._can_exe(circuit[n], pi, cg)]
+
+            # Execute the gates and update F
+            if len(execute_list) > 0:
+                for n in execute_list:
+                    F.remove(n)
+                    prev_executed_counts.pop(n)
+                    _logger.debug(f'Executing gate at point {n}.')
+
+                    if modify_circuit:
+                        op = circuit[n]
+                        physical_location = [pi[q] for q in op.location]
+                        mapped_circuit.append_gate(
+                            op.gate,
+                            physical_location,
+                            op.params,
+                        )
+
+                    # Reset previous swap if executed gate overlaps it
+                    if any(pi[i] in prev_swap for i in circuit[n].location):
+                        prev_swap = (-1, -1)
+
+                    for successor in circuit.next(n):
+                        if successor not in prev_executed_counts:
+                            prev_executed_counts[successor] = 1
+                        else:
+                            prev_executed_counts[successor] += 1
+                        num_prev_executed = prev_executed_counts[successor]
+                        total_num_prev = len(circuit.prev(successor))
+                        if num_prev_executed == total_num_prev:
+                            F.add(successor)
+
+                # Reset decay if necessary
+                if self.decay_reset_on_gate:
+                    iter_count = 0
+                    for i in range(circuit.num_qudits):
+                        decay[i] = 1.0
+
+                continue  # Restart main loop if we executed at least one gate
+
+            # Pick and apply a swap
+            E = self._calc_extended_set(circuit, F)
+            best_swap = self._get_best_swap(
+                circuit, F, E, D, cg, pi, decay, prev_swap,
+            )
+            self._apply_swap(best_swap, pi, decay)
+            prev_swap = best_swap
+            if modify_circuit:
+                mapped_circuit.append_gate(SwapGate(), best_swap)
+
+            # Update loop counter and reset decay if necessary
+            iter_count += 1
+            if iter_count % self.decay_reset_interval == 0:
+                for i in range(circuit.num_qudits):
+                    decay[i] = 1.0
+
+        if modify_circuit:
+            circuit.become(mapped_circuit)
+
+    def backward_pass(
+        self,
+        circuit: Circuit,
+        pi: list[int],
+        cg: CouplingGraph,
+    ) -> None:
+        """
+        Apply a backward pass of the Sabre algorithm to `pi`.
+
+        Args:
+            circuit (Circuit): The circuit to pass over.
+
+            pi (list[int]): The input logical-to-physical mapping. This
+                maps logical qudits to physical qudits. So, `pi[l] == p`
+                implies logical qudit `l` is sitting on physical qudit `p`.
+
+            cg (CouplingGraph): The connectivity of the hardware.
+        """
+        # Preprocessing
+        D = cg.all_pairs_shortest_path()
+        F = circuit.rear
+        decay = [1.0 for i in range(circuit.num_qudits)]
+        iter_count = 0
+        prev_swap = (-1, -1)
+        next_executed_counts: dict[CircuitPoint, int] = {n: 0 for n in F}
+        _logger.debug(f'Starting backward sabre pass with pi: {pi}.')
+
+        # Main Loop
+        while len(F) > 0:
+
+            # Retrieve executable gates giving the current mapping: pi
+            execute_list = [n for n in F if self._can_exe(circuit[n], pi, cg)]
+
+            # Execute the gates and update F
+            if len(execute_list) > 0:
+                for n in execute_list:
+                    F.remove(n)
+                    next_executed_counts.pop(n)
+                    _logger.debug(f'Executing gate at point {n}.')
+
+                    # Reset previous swap if executed gate overlaps it
+                    if any(pi[i] in prev_swap for i in circuit[n].location):
+                        prev_swap = (-1, -1)
+
+                    for predessor in circuit.prev(n):
+                        if predessor not in next_executed_counts:
+                            next_executed_counts[predessor] = 1
+                        else:
+                            next_executed_counts[predessor] += 1
+                        num_next_executed = next_executed_counts[predessor]
+                        total_num_next = len(circuit.next(predessor))
+                        if num_next_executed == total_num_next:
+                            F.add(predessor)
+
+                # Reset decay if necessary
+                if self.decay_reset_on_gate:
+                    iter_count = 0
+                    for i in range(circuit.num_qudits):
+                        decay[i] = 1.0
+
+                continue  # Restart main loop if we executed at least one gate
+
+            # Pick and apply a swap
+            E = self._calc_extended_set(circuit, F)
+            best_swap = self._get_best_swap(
+                circuit, F, E, D, cg, pi, decay, prev_swap,
+            )
+            self._apply_swap(best_swap, pi, decay)
+            prev_swap = best_swap
+
+            # Update loop counter and reset decay if necessary
+            iter_count += 1
+            if iter_count % self.decay_reset_interval == 0:
+                for i in range(circuit.num_qudits):
+                    decay[i] = 1.0
+
+    def _can_exe(self, op: Operation, pi: list[int], cg: CouplingGraph) -> bool:
+        """Return true if `op` is executable given the current mapping `pi`."""
+        # TODO: check if circuitgate of only 1-qubit gates
+        if op.num_qudits == 1:
+            return True
+        physical_qudits = [pi[i] for i in op.location]
+        return cg.get_subgraph(physical_qudits).is_fully_connected()
+
+    def _calc_extended_set(
+        self,
+        circuit: Circuit,
+        F: set[CircuitPoint],
+    ) -> set[CircuitPoint]:
+        """Calculate the Extended Set for look-ahead capabilities."""
+        extended_set: set[CircuitPoint] = set()
+        frontier = list(copy.copy(F))
+        while len(frontier) > 0 and len(extended_set) < self.extended_set_size:
+            n = frontier.pop(0)
+            extended_set.update(circuit.next(n))
+            frontier.extend(circuit.next(n))
+        return extended_set
+
+    def _get_best_swap(
+        self,
+        circuit: Circuit,
+        F: set[CircuitPoint],
+        E: set[CircuitPoint],
+        D: list[list[int]],
+        cg: CouplingGraph,
+        pi: list[int],
+        decay: list[float],
+        prev_swap: tuple[int, int],
+    ) -> tuple[int, int]:
+        """Return the best swap given the current algorithm state."""
+        # Track best one
+        best_score = np.inf
+        best_swap = None
+
+        # Gather all considerable swaps
+        swap_candidate_list = self._obtain_swaps(circuit, F, pi, cg)
+        if prev_swap in swap_candidate_list:
+            swap_candidate_list.remove(prev_swap)
+
+        # Score them, tracking the best one
+        for swap in swap_candidate_list:
+            score = self._score_swap(circuit, F, pi, D, swap, decay, E)
+            if score < best_score:
+                best_score = score
+                best_swap = swap
+
+        if best_swap is None:
+            raise RuntimeError('Unable to find best swap.')
+
+        return best_swap
+
+    def _obtain_swaps(
+        self,
+        circuit: Circuit,
+        F: set[CircuitPoint],
+        pi: list[int],
+        cg: CouplingGraph,
+    ) -> set[tuple[int, int]]:
+        """Produce all physical swaps with at least one qudit in F."""
+        all_qudits: set[int] = set()
+        for n in F:
+            all_qudits.update(circuit[n].location)
+        physical_qudits = [pi[i] for i in all_qudits]
+
+        swaps = set()
+        for physical_qudit in physical_qudits:
+            neighbors = cg.get_neighbors_of(physical_qudit)
+            for neighbor in neighbors:
+                a = min(neighbor, physical_qudit)
+                b = max(neighbor, physical_qudit)
+                swaps.add((a, b))
+
+        return swaps
+
+    def _score_swap(
+        self,
+        circuit: Circuit,
+        F: set[CircuitPoint],
+        pi: list[int],
+        D: list[list[int]],
+        swap: tuple[int, int],
+        decay: list[float],
+        E: set[CircuitPoint],
+    ) -> float:
+        """Score the candidate swap given the current algorithm state."""
+        # Apply potential swap
+        l1, l2 = pi.index(swap[0]), pi.index(swap[1])
+        pi[l1], pi[l2] = pi[l2], pi[l1]
+
+        # Calculate front set term
+        front = 0.0
+        for n in F:
+            logical_qudits = circuit[n].location
+
+            # Disallow meaningless swaps
+            physical_qudits = [pi[i] for i in logical_qudits]
+            if swap[0] in physical_qudits and swap[1] in physical_qudits:
+                pi[l1], pi[l2] = pi[l2], pi[l1]
+                return np.inf
+
+            min_term = np.inf
+            for q in logical_qudits:
+                term = 0.0
+                for p in logical_qudits:
+                    if p == q:
+                        continue
+                    term += D[pi[q]][pi[p]]
+                min_term = min(term, min_term)
+            front += min_term
+        front /= len(F)
+
+        # Calculate extended set term
+        extend = 0.0
+        if len(E) > 0:
+            for n in E:
+                logical_qudits = circuit[n].location
+                min_term = np.inf
+                for q in logical_qudits:
+                    term = 0.0
+                    for p in logical_qudits:
+                        if p == q:
+                            continue
+                        term += D[pi[q]][pi[p]]
+                    min_term = min(term, min_term)
+                extend += min_term
+            extend /= len(E)
+            extend *= self.extended_set_weight
+
+        # Calculate decay factor
+        decay_factor = max(decay[swap[0]], decay[swap[1]])
+
+        # Undo potential swap
+        pi[l1], pi[l2] = pi[l2], pi[l1]
+
+        # Return final score
+        return decay_factor * (front + extend)
+
+    def _apply_swap(
+        self,
+        swap: tuple[int, int],
+        pi: list[int],
+        decay: list[float],
+    ) -> None:
+        """Apply the swap to `pi` and update `decay`."""
+        _logger.debug('applying swap %s' % str(swap))
+        l1, l2 = pi.index(swap[0]), pi.index(swap[1])
+        pi[l1], pi[l2] = pi[l2], pi[l1]
+
+        decay[swap[0]] += self.decay_delta
+        decay[swap[1]] += self.decay_delta
diff --git a/bqskit/passes/mapping/setmodel.py b/bqskit/passes/mapping/setmodel.py
new file mode 100644
index 000000000..9a95aaaae
--- /dev/null
+++ b/bqskit/passes/mapping/setmodel.py
@@ -0,0 +1,38 @@
+"""This module implements the SetModelPass class."""
+from __future__ import annotations
+
+import logging
+from typing import Any
+
+from bqskit.compiler.basepass import BasePass
+from bqskit.compiler.machine import MachineModel
+from bqskit.ir.circuit import Circuit
+
+_logger = logging.getLogger(__name__)
+
+
+class SetModelPass(BasePass):
+    """Sets a target machine model for future passes to target."""
+
+    def __init__(self, model: MachineModel) -> None:
+        """
+        Construct a SetModelPass.
+
+        Args:
+            model (MachineModel | None): The machine model to encode
+                into the compilation workflow.
+        """
+        if not isinstance(model, MachineModel):
+            raise TypeError(f'Expected MachineModel, got: {type(model)}.')
+
+        self.model = model
+
+    def run(self, circuit: Circuit, data: dict[str, Any] = {}) -> None:
+        """Perform the pass's operation, see :class:`BasePass` for more."""
+        if 'machine_model' in data:
+            _logger.warning('Overriding existing machine model.')
+
+        if self.model.num_qudits < circuit.num_qudits:
+            raise RuntimeError('Machine model is too small for circuit.')
+
+        data['machine_model'] = self.model
diff --git a/bqskit/passes/measure.py b/bqskit/passes/measure.py
new file mode 100644
index 000000000..d0a218fe7
--- /dev/null
+++ b/bqskit/passes/measure.py
@@ -0,0 +1,56 @@
+"""This module implements the ExtractMeasurements and RestoreMeasurements
+passes."""
+from __future__ import annotations
+
+import logging
+from typing import Any
+
+from bqskit.compiler.basepass import BasePass
+from bqskit.ir.circuit import Circuit
+from bqskit.ir.gates.measure import MeasurementPlaceholder
+from bqskit.ir.point import CircuitPoint
+_logger = logging.getLogger(__name__)
+
+
+class ExtractMeasurements(BasePass):
+    """A pass that stores and removes the measurements in a circuit."""
+
+    key = '__measurement_data__'
+
+    def run(self, circuit: Circuit, data: dict[str, Any] = {}) -> None:
+        """Perform the pass's operation, see :class:`BasePass` for more."""
+        _logger.info('Extracting measurements from circuit.')
+        points_to_remove: list[CircuitPoint] = []
+        cregs: dict[str, int] = {}
+        measurements: dict[int, tuple[str, int]] = {}
+        for cycle, op in circuit.operations_with_cycles():
+            if isinstance(op.gate, MeasurementPlaceholder):
+                points_to_remove.append(CircuitPoint(cycle, op.location[0]))
+                measurements.update(op.gate.measurements)
+                for name, size in op.gate.classical_regs:
+                    if name not in cregs:
+                        cregs[name] = size
+
+        if len(cregs) > 0:
+            circuit.batch_pop(points_to_remove)
+            data[self.key] = (cregs, measurements)
+            _logger.debug(f'Extracted classical registers: {cregs}.')
+            _logger.debug(f'Extracted measurements: {measurements}.')
+
+
+class RestoreMeasurements(BasePass):
+    """A pass that restores the measurements in a circuit."""
+
+    def run(self, circuit: Circuit, data: dict[str, Any] = {}) -> None:
+        """Perform the pass's operation, see :class:`BasePass` for more."""
+        _logger.info('Restoring measurements to circuit.')
+        if ExtractMeasurements.key in data:
+            cregs, measurements = data[ExtractMeasurements.key]
+
+            # permute for final layout
+            if 'final_mapping' in data:
+                pi = data['final_mapping']
+                measurements = {pi[q]: c for q, c in measurements.items()}
+
+            mph = MeasurementPlaceholder(list(cregs.items()), measurements)
+            circuit.append_gate(mph, list(measurements.keys()))
diff --git a/bqskit/passes/partitioning/__init__.py b/bqskit/passes/partitioning/__init__.py
index beaf7e551..17de705fd 100644
--- a/bqskit/passes/partitioning/__init__.py
+++ b/bqskit/passes/partitioning/__init__.py
@@ -1,4 +1,8 @@
-"""This package implements partitioning passes."""
+"""
+This package implements partitioning passes.
+
+Partitioning passes group together adjacent gates into CircuitGates objects.
+"""
 from __future__ import annotations
 
 from bqskit.passes.partitioning.cluster import ClusteringPartitioner
diff --git a/bqskit/passes/partitioning/greedy.py b/bqskit/passes/partitioning/greedy.py
index 1f9b9952e..3ede6b2f4 100644
--- a/bqskit/passes/partitioning/greedy.py
+++ b/bqskit/passes/partitioning/greedy.py
@@ -3,12 +3,14 @@
 
 import bisect
 import logging
+import warnings
 from typing import Any
 
 from bqskit.compiler.basepass import BasePass
 from bqskit.ir.circuit import Circuit
 from bqskit.ir.gates.circuitgate import CircuitGate
 from bqskit.ir.interval import CycleInterval
+from bqskit.ir.point import CircuitPoint
 from bqskit.ir.region import CircuitRegion
 from bqskit.utils.typing import is_integer
 
@@ -32,7 +34,18 @@ def __init__(self, block_size: int = 3) -> None:
 
         Raises:
             ValueError: If `block_size` is less than 2.
+
+        Note:
+            GreedyPartitioner has been deprecated and will be removed in
+            a future update. See :class:`QuickPartitioner` for a
+            replacement partitioner.
         """
+        warnings.warn(
+            'GreedyPartitioner has been deprecated and will be '
+            'removed in a future update. See QuickPartitioner '
+            'for a replacement partitioner.',
+            DeprecationWarning,
+        )
 
         if not is_integer(block_size):
             raise TypeError(
@@ -68,7 +81,7 @@ def run(self, circuit: Circuit, data: dict[str, Any] = {}) -> None:
         ]
         potential_regions = {}
         for cycle, op in circuit.operations_with_cycles():
-            point = (cycle, op.location[0])
+            point = CircuitPoint(cycle, op.location[0])
             if len(op.location) > self.block_size:
                 regions.append(circuit.get_region([point]))
                 for qudit, bounds in circuit.get_region([point]).items():
diff --git a/bqskit/passes/partitioning/quick.py b/bqskit/passes/partitioning/quick.py
index 58aafbe52..02e98a07f 100644
--- a/bqskit/passes/partitioning/quick.py
+++ b/bqskit/passes/partitioning/quick.py
@@ -1,14 +1,16 @@
 """This module defines the QuickPartitioner pass."""
 from __future__ import annotations
 
-import heapq
 import logging
 from typing import Any
+from typing import cast
+from typing import Sequence
 
 from bqskit.compiler.basepass import BasePass
 from bqskit.ir.circuit import Circuit
-from bqskit.ir.gates.circuitgate import CircuitGate
-from bqskit.ir.region import CircuitRegion
+from bqskit.ir.gates import CircuitGate
+from bqskit.ir.location import CircuitLocation
+from bqskit.ir.point import CircuitPoint
 from bqskit.utils.typing import is_integer
 
 _logger = logging.getLogger(__name__)
@@ -16,16 +18,13 @@
 
 class QuickPartitioner(BasePass):
     """
-    The QuickPartitioner Pass.
+    A partitioner that iterates over circuit gates only once.
 
     This pass forms partitions in the circuit by iterating over the operations
     in a topological order and binning them into blocks.
     """
 
-    def __init__(
-        self,
-        block_size: int = 3,
-    ) -> None:
+    def __init__(self, block_size: int = 3) -> None:
         """
         Construct a QuickPartitioner.
 
@@ -58,323 +57,279 @@ def run(self, circuit: Circuit, data: dict[str, Any] = {}) -> None:
 
             data (dict[str,Any]): Optional data unique to specific run.
         """
+        # The partitioned circuit that will be built and returned
+        partitioned_circuit = Circuit(circuit.num_qudits, circuit.radixes)
+
+        # If block size >= circuit size, return the circuit as a block
+        if self.block_size >= circuit.num_qudits:
+            _logger.debug(
+                'Configured block size is greater than or equal to'
+                'circuit size; blocking entire circuit.',
+            )
+            partitioned_circuit.append_circuit(
+                circuit,
+                list(range(circuit.num_qudits)),
+                True,
+            )
+            circuit.become(partitioned_circuit)
+            return
 
-        # Number of qudits in the circuit
-        num_qudits = circuit.num_qudits
+        # Tracks bins with at least one active_qudit
+        active_bins: list[Bin | None] = [
+            None for _ in range(circuit.num_qudits)
+        ]
+        # Tracks the first cycle in circuit not included in partitioned_circuit
+        dividing_line: dict[int, int] = {
+            i: 0 if circuit._front[i] is None else circuit._front[i].cycle  # type: ignore  # noqa
+            for i in range(circuit.num_qudits)
+        }
+
+        # Inactive bins that cannot yet be put on partitioned_circuit
+        pending_bins: list[Bin] = []
+
+        # Track how many bins have been closed without processing
+        num_closed = 0
+
+        def close_bin_qudits(bin: Bin, loc: Sequence[int], cycle: int) -> bool:
+            """Deactivate `qudits` in `bin`; return True if bin is inactive."""
+            to_return = False
+            for q in loc:
+                if q in bin.active_qudits:
+                    bin.active_qudits.remove(q)
+                    bin.ends[q] = cycle - 1
+
+                if active_bins[q] == bin:
+                    active_bins[q] = None
+
+            # Check if the bin is completely inactive now
+            if len(bin.active_qudits) == 0:
+                pending_bins.append(bin)
+                to_return = True
+            return to_return
+
+        def process_pending_bins() -> None:
+            """Add pending bins that can be added to the partitioned circuit."""
+            need_to_reprocess = True
+            while need_to_reprocess:
+                need_to_reprocess = False
+                to_remove = []
+
+                for bin in pending_bins:
+                    if all(
+                        dividing_line[qudit] == start
+                        for qudit, start in bin.starts.items()
+                    ):
+                        to_remove.append(bin)
+                        subc = circuit.get_slice(bin.op_list)
+                        loc = list(sorted(bin.qudits))
+
+                        # Merge previously placed blocks if possible
+                        merging = True
+                        while merging:
+                            merging = False
+                            for p in partitioned_circuit.rear:
+                                qudits = partitioned_circuit[p].location
+
+                                # if qudits is subset of bin.qudits
+                                if all(q in bin.qudits for q in qudits):
+                                    prev_op = partitioned_circuit.pop(p)
+                                    pg = cast(CircuitGate, prev_op.gate)
+                                    prev_circ = pg._circuit
+                                    local_loc = [loc.index(q) for q in qudits]
+                                    subc.insert_circuit(0, prev_circ, local_loc)
+
+                                    # retry merging
+                                    merging = True
+                                    break
+
+                                # if bin.qudits is a subset of qudits
+                                if all(q in qudits for q in bin.qudits):
+                                    prev_op = partitioned_circuit.pop(p)
+                                    pg = cast(CircuitGate, prev_op.gate)
+                                    prev_circ = pg._circuit
+                                    lloc = [qudits.index(q) for q in bin.qudits]
+                                    prev_circ.append_circuit(subc, lloc)
+                                    subc.become(prev_circ)
+
+                                    # retry merging
+                                    merging = True
+                                    break
+
+                        # Place circuit
+                        partitioned_circuit.append_circuit(
+                            subc,
+                            loc,
+                            True,
+                            True,
+                        )
+                        for qudit in bin.qudits:
+                            dividing_line[qudit] = bin.ends[qudit] + 1  # type: ignore  # noqa
+                        need_to_reprocess = True
 
-        # If block size > circuit size, return the circuit as a block
-        if self.block_size > num_qudits:
-            _logger.warning(
-                'Configured block size is greater than circuit size; '
-                'blocking entire circuit.',
-            )
-            circuit.fold({
-                qudit_index: (0, circuit.num_cycles)
-                for qudit_index in range(circuit.num_qudits)
+                for bin in to_remove:
+                    pending_bins.remove(bin)
+
+        # Main loop
+        for cycle, op in circuit.operations_with_cycles():
+            point = CircuitPoint(cycle, op.location[0])
+            location = op.location
+
+            # Get all currently active bins that share atleast one qudit
+            overlapping_bins: list[Bin] = list({
+                active_bins[q] for q in location  # type: ignore
+                if active_bins[q] is not None
             })
-            return
 
-        # List to hold the active blocks
-        active_blocks: Any = []
+            # Get all the currently active bins that can have op added to them
+            admissible_bins = [
+                b for b in overlapping_bins
+                if b.can_accommodate(location, self.block_size)
+            ]
+
+            # Close location on inadmissible overlapping bins
+            for bin in overlapping_bins:
+                if bin not in admissible_bins:
+                    if close_bin_qudits(bin, location, cycle):
+                        num_closed += 1
+
+            # If we cannot add this op to any bin, make a new one
+            if len(admissible_bins) == 0:
+                assert all(active_bins[q] is None for q in location)
+                new_bin = Bin(point, location)
+                for q in location:
+                    active_bins[q] = new_bin
+
+                # Block qudits to prevent circular dependencies
+                for bin in overlapping_bins:
+                    bin.blocked_qudits.update(new_bin.qudits)
+
+                    for active_bin in active_bins:
+                        if active_bin is None or active_bin == bin:
+                            continue
+
+                        indirect = active_bin.blocked_qudits
+                        indirect = indirect.intersection(bin.qudits)
+                        if len(indirect) != 0:
+                            active_bin.blocked_qudits.update(new_bin.qudits)
+
+            else:
+                # Add to first admissible bin
+                selected_bin = admissible_bins[0]
+
+                # Deactivate the rest
+                for bin in admissible_bins[1:]:
+                    if close_bin_qudits(bin, location, cycle):
+                        num_closed += 1
+
+                # Add op to selected_bin
+                selected_bin.add_op(point, location)
+                for q in location:
+                    if active_bins[q] is None:
+                        active_bins[q] = selected_bin
+                    else:
+                        assert active_bins[q] == selected_bin
+
+                # Block qudits to prevent circular dependencies
+                for bin in overlapping_bins:
+                    if bin == selected_bin:
+                        continue
+                    bin.blocked_qudits.update(selected_bin.qudits)
+
+                    for active_bin in active_bins:
+                        if active_bin is None or active_bin == bin:
+                            continue
+
+                        indirect = active_bin.blocked_qudits
+                        indirect = indirect.intersection(bin.qudits)
+                        if len(indirect) != 0:
+                            active_bin.blocked_qudits.update(new_bin.qudits)
+
+            # If a new bin was finalized, reprocess pending bins
+            if num_closed >= 5:
+                process_pending_bins()
+                num_closed = 0
+
+        # Close remaining active bins
+        for b in active_bins:
+            if b is not None:
+                close_bin_qudits(b, b.qudits, circuit.num_cycles)
+
+        # Process remaining bins
+        process_pending_bins()
+
+        # Become partitioned circuit
+        circuit.become(partitioned_circuit, False)
+
+
+class Bin:
+    """A Bin is where gates go as the QuickPartitioner sweeps a circuit."""
+
+    id: int = 0
+    """Unique ID counter for Bin instances."""
 
-        # List to hold the finished blocks
-        finished_blocks: Any = {}
-        block_id = 0
+    def __init__(
+        self,
+        point: CircuitPoint,
+        location: CircuitLocation,
+    ) -> None:
+        """Can start a new bin from an operation."""
 
-        # Active qudit cycles and block-qudit dependencies
-        qudit_actives: Any = [{} for _ in range(num_qudits)]
-        qudit_dependencies: Any = [{} for _ in range(num_qudits)]
+        # The qudits in the bin
+        self.qudits: list[int] = list(location)
 
-        # The partitioned circuit
-        partitioned_circuit = Circuit(num_qudits, circuit.radixes)
+        # The starting cycles for each qudit (inclusive)
+        self.starts: dict[int, int] = {q: point.cycle for q in location}
 
-        # For each cycle, operation in topological order
-        for cycle, op in circuit.operations_with_cycles():
+        # The ending cycles for each qudit (inclusive)
+        self.ends: dict[int, int | None] = {q: None for q in location}
 
-            # Get the qudits of the operation
-            qudits = op.location._location
-
-            # Update the active locations of the qudits
-            for qudit in qudits:
-                qudit_actives[qudit][cycle] = None
-
-            # Compile a list of admissible blocks out of the
-            # active blocks for the operation
-            admissible_blocks = []
-            for index, block in enumerate(active_blocks):
-                if all([qudit not in block[-1] for qudit in qudits]):
-                    admissible_blocks.append(index)
-
-            # Boolean indicator to capture if an active block
-            # has been found for the operation
-            found = False
-
-            # For all admissible blocks, check if all operation
-            # qudits are in the block. If such a block is found,
-            # update the upper region bound for the corresponding
-            # qudits, and raise the found boolean
-            for block in [active_blocks[index] for index in admissible_blocks]:
-                if all([qudit in block for qudit in qudits]):
-                    for qudit in qudits:
-                        block[qudit][1] = cycle
-                    found = True
-                    break
-
-            updated_qudits = set()
-
-            # If such a block is not found
-            if not found:
-
-                # For all admissible blocks, check if the operation
-                # qudits can be added to the block without breaching
-                # the size limit. If such a block is found, add the
-                # new qudits, update the region bounds, check if any
-                # blocks are finished, and raise the found boolean
-                for block in [
-                    active_blocks[index]
-                    for index in admissible_blocks
-                ]:
-                    if len(set(list(qudits) + list(block.keys()))) - \
-                            1 <= self.block_size:
-                        for qudit in qudits:
-                            if qudit not in block:
-                                block[qudit] = [cycle, cycle]
-                            else:
-                                block[qudit][1] = cycle
-                        block_id, updated_qudits = self.compute_finished_blocks(
-                            block, qudits, active_blocks,
-                            finished_blocks, block_id, qudit_dependencies, cycle, num_qudits,  # noqa
-                        )
-                        found = True
-                        break
-
-            # If a block is still not found, check if any blocks are finished
-            # with the new operation qudits, create a new block, and add it
-            # to the list of active blocks
-            if not found:
-                block_id, updated_qudits = self.compute_finished_blocks(
-                    None, qudits, active_blocks,
-                    finished_blocks, block_id, qudit_dependencies, cycle, num_qudits,  # noqa
-                )
-                block = {qudit: [cycle, cycle] for qudit in qudits}
-                block[-1] = set()
-                active_blocks.append(block)
-
-            # Where the active qudit cycles keep getting updated
-            while updated_qudits:
-
-                # Check if any blocks corresponding to updated qudits
-                # are eligible to be added to the circuit. If eligible,
-                # update actives, dependencies, and updated qudits.
-                final_regions = []
-                new_updated_qudits = set()
-                for qudit in updated_qudits:
-                    blk_ids = list(qudit_dependencies[qudit].keys())
-                    for blk_id in blk_ids:
-                        num_passed = 0
-                        for qdt, bounds in finished_blocks[blk_id].items():
-                            if len(qudit_actives[qdt]) == 0:
-                                num_passed += 1
-                            elif next(iter(qudit_actives[qdt])) == bounds[0]:
-                                num_passed += 1
-                        if num_passed == len(finished_blocks[blk_id]):
-                            for qdt, bounds in finished_blocks[blk_id].items():
-                                for cycle in range(bounds[0], bounds[1] + 1):
-                                    if cycle in qudit_actives[qdt]:
-                                        del qudit_actives[qdt][cycle]
-                                del qudit_dependencies[qdt][blk_id]
-                                new_updated_qudits.add(qdt)
-                            final_regions.append(
-                                CircuitRegion(
-                                {qdt: (bounds[0], bounds[1]) for qdt, bounds in finished_blocks[blk_id].items()},  # noqa
-                                ),
-                            )
-                            del finished_blocks[blk_id]
-
-                # If there are any regions
-                if final_regions:
-
-                    # Sort the regions if multiple exist
-                    if len(final_regions) > 1:
-                        final_regions = self.topo_sort(final_regions)
-
-                    # Fold the final regions into a partitioned circuit
-                    for region in final_regions:
-                        region = circuit.downsize_region(region)
-                        cgc = circuit.get_slice(region.points)
-                        partitioned_circuit.append_gate(
-                            CircuitGate(
-                                cgc, True,
-                            ), sorted(
-                                list(
-                                    region.keys(),
-                                ),
-                            ), list(
-                                cgc.params,
-                            ),
-                        )
+        # The qudits that can still accept new gates
+        self.active_qudits: list[int] = list(location)
 
-                updated_qudits = new_updated_qudits
+        # Qudits that cannot be added to the bin
+        self.blocked_qudits: set[int] = set()
 
-        # Convert all remaining finished blocks and active blocks
-        # into circuit regions
-        final_regions = []
-        for block in finished_blocks.values():
-            final_regions.append(
-                CircuitRegion(
-                {qdt: (bounds[0], bounds[1]) for qdt, bounds in block.items()},  # noqa
-                ),
-            )
-        for block in active_blocks:
-            del block[-1]
-            final_regions.append(
-                CircuitRegion(
-                {qdt: (bounds[0], bounds[1]) for qdt, bounds in block.items()},  # noqa
-                ),
-            )
+        # Points for each operation in this bin
+        self.op_list: list[CircuitPoint] = [point]
+
+        self.id = Bin.id
+        Bin.id += 1
+
+    def __hash__(self) -> int:
+        return hash(self.id)
+
+    def __eq__(self, other: object) -> bool:
+        return isinstance(other, Bin) and self.id == other.id
+
+    def add_op(
+        self,
+        point: CircuitPoint,
+        location: CircuitLocation,
+    ) -> None:
+        """Add an operation the bin."""
+        for q in location:
+            if q not in self.qudits:
+                self.qudits.append(q)
+                self.active_qudits.append(q)
+                self.starts[q] = point.cycle
+        self.op_list.append(point)
+
+    def can_accommodate(self, loc: CircuitLocation, block_size: int) -> bool:
+        """
+        Return true if the op can be added to this bin.
+
+        An op can be added to the bin if all overlapping qudits are active in
+        the bin and if the new bin won't be too large.
+        """
+        if any(q in loc for q in self.blocked_qudits):
+            return False
+
+        overlapping_qudits_are_active = all(
+            q not in self.qudits or q in self.active_qudits
+            for q in loc
+        )
+
+        size_limit = max(block_size, len(self.qudits))
+        too_big = len(set(self.qudits + list(loc))) > size_limit
 
-        # If there are any regions
-        if final_regions:
-
-            # Sort the regions if multiple exist
-            if len(final_regions) > 1:
-                final_regions = self.topo_sort(final_regions)
-
-            # Fold the final regions into a partitioned circuit
-            for region in final_regions:
-                region = circuit.downsize_region(region)
-                cgc = circuit.get_slice(region.points)
-                partitioned_circuit.append_gate(
-                    CircuitGate(
-                        cgc, True,
-                    ), sorted(
-                        list(
-                            region.keys(),
-                        ),
-                    ), list(
-                        cgc.params,
-                    ),
-                )
-
-        # Copy the partitioned circuit to the original circuit
-        circuit.become(partitioned_circuit)
-
-    def compute_finished_blocks(  # type: ignore
-        self, block, qudits, active_blocks, finished_blocks,
-        block_id, qudit_dependencies, cycle, num_qudits,
-    ):
-        """Add blocks with all inactive qudits to the finished_blocks list and
-        remove them from the active_blocks list."""
-
-        # Compile the qudits from the new operation,
-        # the active qudits of the block being updated,
-        # and the qudits in the block's inadmissible list
-        qudits = set(qudits)
-        if block:
-            qudits.update([qudit for qudit in block if qudit != -1])
-            qudits.update(block[-1])
-
-        remove_blocks = []
-
-        # For all active blocks
-        for active_block in active_blocks:
-
-            # If the active block is different than the block being updated
-            if active_block != block:
-
-                # If any of the qudits are in the active block or its
-                # inadmissible list, then add those qudits to the
-                # inadmissible list of the active block
-                if any([
-                    qudit in active_block or qudit in active_block[-1]
-                    for qudit in qudits
-                ]):
-                    active_block[-1].update(qudits)
-
-                # If the active block has reached its maximum size
-                # and/or all of its qudits are inadmissible,
-                # then add it to the remove list
-                if (
-                    len(active_block) - 1 == self.block_size and  # noqa
-                    all([
-                        qudit in active_block[-1]
-                        for qudit in active_block if qudit != -1
-                    ])
-                ) or (
-                    cycle - max(
-                        active_block[qudit][1]
-                        for qudit in active_block
-                        if qudit != -1
-                    ) > 200
-                ) or len(active_block[-1]) == num_qudits:
-                    remove_blocks.append(active_block)
-
-        # Remove all blocks in the remove list from the active list
-        # and add them to the finished blocks list after deleting
-        # their inadmissible list and update qudit dependencies
-        updated_qudits = set()
-        for remove_block in remove_blocks:
-            del remove_block[-1]
-            finished_blocks[block_id] = remove_block
-            for qudit in remove_block:
-                qudit_dependencies[qudit][block_id] = None
-                updated_qudits.add(qudit)
-            active_blocks.remove(remove_block)
-            block_id += 1
-
-        return block_id, updated_qudits
-
-    def topo_sort(self, regions):  # type: ignore
-        """Topologically sort circuit regions."""
-
-        # Number of regions in the circuit
-        num_regions = len(regions)
-
-        # For each region, generate the number of in edges
-        # and the list of all out edges
-        in_edges = [0] * num_regions
-        out_edges: Any = [[] for _ in range(num_regions)]
-        for i in range(num_regions - 1):
-            for j in range(i + 1, num_regions):
-                dependency = regions[i].dependency(regions[j])
-                if dependency == 1:
-                    in_edges[i] += 1
-                    out_edges[j].append(i)
-                elif dependency == -1:
-                    in_edges[j] += 1
-                    out_edges[i].append(j)
-
-        # Convert the list of number of in edges in to a min-heap
-        in_edges = [[num_edges, i] for i, num_edges in enumerate(in_edges)]
-        heapq.heapify(in_edges)
-
-        index = 0
-        sorted_regions = []
-
-        # While there are regions remaining to be sorted
-        while index < num_regions:
-
-            # Select the regions with zero remaining in edges
-            selections = []
-            while in_edges and not in_edges[0][0]:
-                selections.append(heapq.heappop(in_edges))
-
-            if not selections:
-                raise RuntimeError('Unable to topologically sort regions.')
-
-            # Add the regions to the sorted list
-            for region in selections:
-                sorted_regions.append(regions[region[1]])
-                index += 1
-
-            # Remove the regions from all other regions' in edges counts
-            for i in range(len(in_edges)):
-                in_edges[i][0] -= sum(
-                    in_edges[i][1] in out_edges[region[1]]
-                    for region in selections
-                )
-
-            # Convert in edges into a min-heap
-            heapq.heapify(in_edges)
-
-        return sorted_regions
+        return overlapping_qudits_are_active and not too_big
diff --git a/bqskit/passes/partitioning/single.py b/bqskit/passes/partitioning/single.py
index bf64cb949..a59645c36 100644
--- a/bqskit/passes/partitioning/single.py
+++ b/bqskit/passes/partitioning/single.py
@@ -3,7 +3,7 @@
 
 from typing import Any
 
-from bqskit.compiler import BasePass
+from bqskit.compiler.basepass import BasePass
 from bqskit.ir.circuit import Circuit
 from bqskit.ir.region import CircuitRegion
 
diff --git a/bqskit/passes/processing/__init__.py b/bqskit/passes/processing/__init__.py
index 6742606ce..5cd7c4104 100644
--- a/bqskit/passes/processing/__init__.py
+++ b/bqskit/passes/processing/__init__.py
@@ -3,6 +3,7 @@
 
 from bqskit.passes.processing.exhaustive import ExhaustiveGateRemovalPass
 from bqskit.passes.processing.iterative import IterativeScanningGateRemovalPass
+from bqskit.passes.processing.rebase import Rebase2QuditGatePass
 from bqskit.passes.processing.scan import ScanningGateRemovalPass
 from bqskit.passes.processing.substitute import SubstitutePass
 from bqskit.passes.processing.window import WindowOptimizationPass
@@ -13,4 +14,5 @@
     'ScanningGateRemovalPass',
     'SubstitutePass',
     'WindowOptimizationPass',
+    'Rebase2QuditGatePass',
 ]
diff --git a/bqskit/passes/processing/rebase.py b/bqskit/passes/processing/rebase.py
index a3e5cf320..d5f49b5f2 100644
--- a/bqskit/passes/processing/rebase.py
+++ b/bqskit/passes/processing/rebase.py
@@ -160,7 +160,6 @@ def run(self, circuit: Circuit, data: dict[str, Any] = {}) -> None:
 
                 # Group together a 2-qubit block composed of gates from old set
                 point = self.group_near_gates(circuit, circuit.point(g))
-                print(point)
                 circuits_with_new_gate = []
                 for circ in self.circs:
                     circuit_copy = circuit.copy()
@@ -169,7 +168,7 @@ def run(self, circuit: Circuit, data: dict[str, Any] = {}) -> None:
 
                 # If we have exceeded the number of retries, up the max depth
                 if self.max_retries >= 0 and num_retries > self.max_retries:
-                    _logger.info('Exceeded max retries, increasing depth.')
+                    _logger.debug('Exceeded max retries, increasing depth.')
                     circuit_copy = circuit.copy()
                     circuit_copy.replace_with_circuit(point, self.overdrive)
                     circuits_with_new_gate.append(circuit_copy)
@@ -197,7 +196,7 @@ def run(self, circuit: Circuit, data: dict[str, Any] = {}) -> None:
                     circuit.unfold(point)
                     continue
 
-                _logger.info(self.replaced_log_messages[best_index])
+                _logger.debug(self.replaced_log_messages[best_index])
                 circuit.become(instantiated_circuits[best_index])
 
     def group_near_gates(self, circuit: Circuit, center: Point) -> Point:
@@ -257,7 +256,7 @@ def group_near_gates(self, circuit: Circuit, center: Point) -> Point:
             f'{int(c)} {g}' + ('s' if c > 1 else '')
             for g, c in counts.items()
         ])
-        _logger.info(f'Grouped together {grouped_gate_str}.')
+        _logger.debug(f'Grouped together {grouped_gate_str}.')
         return circuit.fold(region)
 
     def generate_new_gate_templates(self) -> None:
diff --git a/bqskit/passes/processing/scan.py b/bqskit/passes/processing/scan.py
index bdbb06a00..61cc89bc6 100644
--- a/bqskit/passes/processing/scan.py
+++ b/bqskit/passes/processing/scan.py
@@ -96,7 +96,7 @@ def __init__(
     def run(self, circuit: Circuit, data: dict[str, Any] = {}) -> None:
         """Perform the pass's operation, see :class:`BasePass` for more."""
         start = 'left' if self.start_from_left else 'right'
-        _logger.debug(f'Starting scan gate removal from {start}.')
+        _logger.debug(f'Starting scanning gate removal on the {start}.')
 
         target = self.get_target(circuit, data)
 
@@ -108,7 +108,7 @@ def run(self, circuit: Circuit, data: dict[str, Any] = {}) -> None:
                 _logger.debug(f'Skipping operation {op} at cycle {cycle}.')
                 continue
 
-            _logger.info(f'Attempting removal of operation at cycle {cycle}.')
+            _logger.debug(f'Attempting removal of operation at cycle {cycle}.')
             _logger.debug(f'Operation: {op}')
 
             working_copy = circuit_copy.copy()
@@ -120,10 +120,16 @@ def run(self, circuit: Circuit, data: dict[str, Any] = {}) -> None:
                 cycle -= idx_shift
 
             working_copy.pop((cycle, op.location[0]))
-            working_copy.instantiate(target, **self.instantiate_options)
+            working_copy = self.execute(
+                data,
+                Circuit.instantiate,
+                [working_copy],
+                target=target,
+                **self.instantiate_options,
+            )[0]
 
             if self.cost(working_copy, target) < self.success_threshold:
-                _logger.info('Successfully removed operation.')
+                _logger.debug('Successfully removed operation.')
                 circuit_copy = working_copy
 
         circuit.become(circuit_copy)
diff --git a/bqskit/passes/processing/substitute.py b/bqskit/passes/processing/substitute.py
index 6971108f0..dbce50344 100644
--- a/bqskit/passes/processing/substitute.py
+++ b/bqskit/passes/processing/substitute.py
@@ -125,7 +125,13 @@ def run(self, circuit: Circuit, data: dict[str, Any] = {}) -> None:
                 _logger.debug(f'Trying location: {loc}')
                 circuit_copy = circuit.copy()
                 circuit_copy.replace_gate(point, self.gate, loc)
-                circuit_copy.instantiate(target, **self.instantiate_options)
+                circuit_copy = self.execute(
+                    data,
+                    Circuit.instantiate,
+                    [circuit_copy],
+                    target=target,
+                    **self.instantiate_options,
+                )[0]
 
                 if self.cost(circuit_copy, target) < self.success_threshold:
                     _logger.info('Successfully substituted operation.')
diff --git a/bqskit/passes/processing/window.py b/bqskit/passes/processing/window.py
index 57d491e41..43efac627 100644
--- a/bqskit/passes/processing/window.py
+++ b/bqskit/passes/processing/window.py
@@ -2,6 +2,7 @@
 from __future__ import annotations
 
 import logging
+import warnings
 from typing import Any
 from typing import Callable
 
@@ -51,7 +52,18 @@ def __init__(
 
         Raises:
             ValueError: If `window_size` is <= 1.
+
+        Note:
+            This has been deprecated and will removed in a future update.
+            See :class:`ScanningGateRemovalPass` for a replacement
+            post-synthesis optimization pass.
         """
+        warnings.warn(
+            'WindowOptimizationPass has been deprecated and will be '
+            'removed in a future update. See ScanningGateRemovalpass '
+            'for a replacement post-synthesis optimization pass.',
+            DeprecationWarning,
+        )
 
         if not is_integer(window_size):
             raise TypeError(
diff --git a/bqskit/passes/rules/__init__.py b/bqskit/passes/rules/__init__.py
index 7cb637764..61f7dc9e1 100644
--- a/bqskit/passes/rules/__init__.py
+++ b/bqskit/passes/rules/__init__.py
@@ -1,7 +1,22 @@
 """This package implements BQSKit's rule-based passes."""
 from __future__ import annotations
 
+from bqskit.passes.rules.ch2cnot import CHToCNOTPass
+from bqskit.passes.rules.cnot2ch import CNOTToCHPass
+from bqskit.passes.rules.cnot2cy import CNOTToCYPass
 from bqskit.passes.rules.cnot2cz import CNOTToCZPass
+from bqskit.passes.rules.cy2cnot import CYToCNOTPass
+from bqskit.passes.rules.swap2cnot import SwapToCNOTPass
+from bqskit.passes.rules.u3 import U3Decomposition
 from bqskit.passes.rules.zxzxz import ZXZXZDecomposition
 
-__all__ = ['CNOTToCZPass', 'ZXZXZDecomposition']
+__all__ = [
+    'CHToCNOTPass',
+    'CNOTToCZPass',
+    'CNOTToCHPass',
+    'CNOTToCYPass',
+    'CYToCNOTPass',
+    'SwapToCNOTPass',
+    'U3Decomposition',
+    'ZXZXZDecomposition',
+]
diff --git a/bqskit/passes/rules/u3.py b/bqskit/passes/rules/u3.py
new file mode 100644
index 000000000..ea2d8173a
--- /dev/null
+++ b/bqskit/passes/rules/u3.py
@@ -0,0 +1,34 @@
+"""This module implements the U3Decomposition."""
+from __future__ import annotations
+
+from typing import Any
+
+from bqskit.compiler.basepass import BasePass
+from bqskit.ir.circuit import Circuit
+from bqskit.ir.gates import U3Gate
+
+
+class U3Decomposition(BasePass):
+    """
+    The U3Decomposition class.
+
+    Convert a single-qubit circuit to U3 gate.
+    """
+
+    def run(self, circuit: Circuit, data: dict[str, Any] = {}) -> None:
+        """Perform the pass's operation, see :class:`BasePass` for more."""
+
+        if circuit.num_qudits != 1:
+            raise ValueError(
+                'Cannot convert multi-qudit circuit into ZXZXZ sequence.',
+            )
+
+        if circuit.radixes[0] != 2:
+            raise ValueError(
+                'Cannot convert non-qubit circuit into ZXZXZ sequence.',
+            )
+
+        utry = circuit.get_unitary()
+        new_circuit = Circuit(1)
+        new_circuit.append_gate(U3Gate(), 0, U3Gate.calc_params(utry))
+        circuit.become(new_circuit)
diff --git a/bqskit/passes/search/generators/__init__.py b/bqskit/passes/search/generators/__init__.py
index d9aac6a59..aec7fab2e 100644
--- a/bqskit/passes/search/generators/__init__.py
+++ b/bqskit/passes/search/generators/__init__.py
@@ -7,12 +7,14 @@
 from bqskit.passes.search.generators.simple import SimpleLayerGenerator
 from bqskit.passes.search.generators.single import SingleQuditLayerGenerator
 from bqskit.passes.search.generators.stair import StairLayerGenerator
+from bqskit.passes.search.generators.wide import WideLayerGenerator
 
 __all__ = [
-    'SimpleLayerGenerator',
-    'SeedLayerGenerator',
-    'StairLayerGenerator',
-    'MiddleOutLayerGenerator',
     'FourParamGenerator',
+    'MiddleOutLayerGenerator',
+    'SeedLayerGenerator',
+    'SimpleLayerGenerator',
     'SingleQuditLayerGenerator',
+    'StairLayerGenerator',
+    'WideLayerGenerator',
 ]
diff --git a/bqskit/passes/search/generators/fourparam.py b/bqskit/passes/search/generators/fourparam.py
index 6891944ef..b1a36d6b1 100644
--- a/bqskit/passes/search/generators/fourparam.py
+++ b/bqskit/passes/search/generators/fourparam.py
@@ -75,11 +75,11 @@ def gen_successors(
             raise ValueError('Cannot expand a single-qudit circuit.')
 
         # Get the machine model
-        model = BasePass.get_model(circuit, data)
+        coupling_graph = BasePass.get_connectivity(circuit, data)
 
         # Generate successors
         successors = []
-        for edge in model.coupling_graph:
+        for edge in coupling_graph:
             successor = circuit.copy()
             successor.append_gate(CNOTGate(), [edge[0], edge[1]])
             successor.append_gate(RYGate(), edge[0])
diff --git a/bqskit/passes/search/generators/middleout.py b/bqskit/passes/search/generators/middleout.py
index e65466093..ce88fc2c5 100644
--- a/bqskit/passes/search/generators/middleout.py
+++ b/bqskit/passes/search/generators/middleout.py
@@ -42,12 +42,12 @@ def gen_successors(
             raise ValueError('Cannot expand a single-qudit circuit.')
 
         # Get the machine model
-        model = BasePass.get_model(circuit, data)
+        coupling_graph = BasePass.get_connectivity(circuit, data)
 
         # Generate successors
         successors = []
         for (edge, cycle) in it.product(
-                model.coupling_graph, range(circuit.num_cycles),
+                coupling_graph, range(circuit.num_cycles),
         ):
             successor = circuit.copy()
             successor.insert_gate(
@@ -63,7 +63,7 @@ def gen_successors(
                 self.generated_circuits.add(hash)
                 successors.append(successor)
 
-        for edge in model.coupling_graph:
+        for edge in coupling_graph:
             successor = circuit.copy()
             successor.append_gate(self.two_qudit_gate, [edge[0], edge[1]])
             successor.append_gate(self.single_qudit_gate_1, edge[0])
diff --git a/bqskit/passes/search/generators/simple.py b/bqskit/passes/search/generators/simple.py
index 535a2adc0..83a062838 100644
--- a/bqskit/passes/search/generators/simple.py
+++ b/bqskit/passes/search/generators/simple.py
@@ -188,12 +188,12 @@ def gen_successors(
         if circuit.num_qudits < 2:
             raise ValueError('Cannot expand a single-qudit circuit.')
 
-        # Get the machine model
-        model = BasePass.get_model(circuit, data)
+        # Get the coupling graph
+        coupling_graph = BasePass.get_connectivity(circuit, data)
 
         # Generate successors
         successors = []
-        for edge in model.coupling_graph:
+        for edge in coupling_graph:
             successor = circuit.copy()
             successor.append_gate(self.two_qudit_gate, [edge[0], edge[1]])
             successor.append_gate(self.single_qudit_gate_1, edge[0])
diff --git a/bqskit/passes/search/generators/wide.py b/bqskit/passes/search/generators/wide.py
new file mode 100644
index 000000000..16307c551
--- /dev/null
+++ b/bqskit/passes/search/generators/wide.py
@@ -0,0 +1,145 @@
+"""This module implements the WideLayerGenerator class."""
+from __future__ import annotations
+
+import logging
+from typing import Any
+from typing import Sequence
+
+from bqskit.compiler.basepass import BasePass
+from bqskit.ir.circuit import Circuit
+from bqskit.ir.gate import Gate
+from bqskit.ir.gates import IToffoliGate
+from bqskit.ir.gates import U3Gate
+from bqskit.passes.search.generator import LayerGenerator
+from bqskit.qis.state.state import StateVector
+from bqskit.qis.unitary.unitarymatrix import UnitaryMatrix
+_logger = logging.getLogger(__name__)
+
+
+class WideLayerGenerator(LayerGenerator):
+    """
+    A layer generator for use with wide gates like an iToffoli.
+
+    Very similar to the simple layer generator. Each additional block adds one
+    multi-qudit gate followed by single-qudit gates on each qudit.
+    """
+
+    def __init__(
+        self,
+        multi_qudit_gates: Gate | Sequence[Gate] = IToffoliGate(),
+        single_qudit_gate: Gate = U3Gate(),
+    ) -> None:
+        """
+        Construct a WideLayerGenerator.
+
+        Args:
+            multi_qudit_gate (Gate | Sequence[Gate]): A multi-qudit gate
+                or sequence of gates that starts this layer generator's
+                building block. If multiple gates are given as a sequence,
+                then a successive layer is built for each gate
+                and each location. (Default: IToffoliGate())
+
+            single_qudit_gate (Gate): A single-qudit gate that follows
+                `multi_qudit_gate` in the building block. (Default: U3Gate())
+
+        Raises:
+            ValueError: If the single-qudit gate's size is not 1.
+
+            ValueError: If there is any radix mismatch between the gates.
+        """
+        if isinstance(multi_qudit_gates, Gate):
+            multi_qudit_gates = [multi_qudit_gates]
+
+        if not all(isinstance(g, Gate) for g in multi_qudit_gates):
+            raise TypeError(
+                'Expected gate for multi_qudit_gates, got %s.'
+                % [type(g) for g in multi_qudit_gates],
+            )
+
+        if not isinstance(single_qudit_gate, Gate):
+            raise TypeError(
+                'Expected gate for single_qudit_gate, got %s.'
+                % type(single_qudit_gate),
+            )
+
+        if single_qudit_gate.num_qudits != 1:
+            raise ValueError(
+                'Expected single-qudit gate'
+                ', got a gate that acts on %d qudits.'
+                % single_qudit_gate.num_qudits,
+            )
+
+        sr = single_qudit_gate.radixes[0]
+        for mg in multi_qudit_gates:
+            if any(r != sr for r in mg.radixes):
+                raise ValueError(
+                    'Radix mismatch between gates'
+                    f': {mg.radixes} !~ {single_qudit_gate.radixes}.',
+                )
+
+        self.multi_qudit_gates: list[Gate] = list(multi_qudit_gates)
+        self.single_qudit_gate = single_qudit_gate
+
+    def gen_initial_layer(
+        self,
+        target: UnitaryMatrix | StateVector,
+        data: dict[str, Any],
+    ) -> Circuit:
+        """
+        Generate the initial layer, see LayerGenerator for more.
+
+        Raises:
+            ValueError: If `target` has a radix mismatch with
+                `self.initial_layer_gate`.
+        """
+
+        if not isinstance(target, (UnitaryMatrix, StateVector)):
+            raise TypeError(
+                'Expected unitary or state, got %s.' % type(target),
+            )
+
+        for radix in target.radixes:
+            if radix != self.single_qudit_gate.radixes[0]:
+                raise ValueError(
+                    'Radix mismatch between target and single_qudit_gate.',
+                )
+
+        init_circuit = Circuit(target.num_qudits, target.radixes)
+        for i in range(init_circuit.num_qudits):
+            init_circuit.append_gate(self.single_qudit_gate, [i])
+        return init_circuit
+
+    def gen_successors(
+        self,
+        circuit: Circuit,
+        data: dict[str, Any],
+    ) -> list[Circuit]:
+        """
+        Generate the successors of a circuit node.
+
+        Raises:
+            ValueError: If circuit is a single-qudit circuit.
+        """
+
+        if not isinstance(circuit, Circuit):
+            raise TypeError('Expected circuit, got %s.' % type(circuit))
+
+        if circuit.num_qudits < 2:
+            raise ValueError('Cannot expand a single-qudit circuit.')
+
+        # Get the coupling graph
+        cg = BasePass.get_connectivity(circuit, data)
+
+        # Generate successors
+        successors = []
+        for mg in self.multi_qudit_gates:
+            if mg.num_qudits > circuit.num_qudits:
+                continue
+            for loc in cg.get_subgraphs_of_size(mg.num_qudits):
+                successor = circuit.copy()
+                successor.append_gate(mg, loc)
+                for q in loc:
+                    successor.append_gate(self.single_qudit_gate, q)
+                successors.append(successor)
+
+        return successors
diff --git a/bqskit/passes/synthesis/leap.py b/bqskit/passes/synthesis/leap.py
index 95df1f1d6..42e3911b6 100644
--- a/bqskit/passes/synthesis/leap.py
+++ b/bqskit/passes/synthesis/leap.py
@@ -5,9 +5,6 @@
 from typing import Any
 
 import numpy as np
-from dask.distributed import get_client
-from dask.distributed import rejoin
-from dask.distributed import secede
 from scipy.stats import linregress
 
 from bqskit.ir.circuit import Circuit
@@ -28,12 +25,14 @@
 
 class LEAPSynthesisPass(SynthesisPass):
     """
-    The LEAPSynthesisPass class.
+    A pass implementing the LEAP search synthesis algorithm.
 
     References:
-        Smith, Ethan, et al. “LEAP: Scaling Numerical Optimization Based
-        Synthesis Using an Incremental Approach.” International Workshop
-        of Quantum Computing Software at Supercomputing (2020).
+        Ethan Smith, Marc G. Davis, Jeffrey M. Larson, Ed Younis,
+        Lindsay Bassman, Wim Lavrijsen, and Costin Iancu. 2022. LEAP:
+        Scaling Numerical Optimization Based Synthesis Using an
+        Incremental Approach. ACM Transactions on Quantum Computing
+        (June 2022). https://doi.org/10.1145/3548693
     """
 
     def __init__(
@@ -161,147 +160,104 @@ def synthesize(self, utry: UnitaryMatrix, data: dict[str, Any]) -> Circuit:
 
         # Seed the search with an initial layer
         initial_layer = self.layer_gen.gen_initial_layer(utry, data)
-        initial_layer.instantiate(utry, **self.instantiate_options)
+        initial_layer = self.execute(
+            data,
+            Circuit.instantiate,
+            [initial_layer],
+            target=utry,
+            **self.instantiate_options,
+        )[0]
         frontier.add(initial_layer, 0)
 
         # Track best circuit, initially the initial layer
-        leap_data: dict[str, Any] = {}
-        leap_data['best_dist'] = self.cost.calc_cost(initial_layer, utry)
-        leap_data['best_circ'] = initial_layer
-        leap_data['best_layer'] = 0
-        leap_data['best_dists'] = [leap_data['best_dist']]
-        leap_data['best_layers'] = [0]
-        leap_data['last_prefix_layer'] = 0
-        if self.store_partial_solutions:
-            leap_data['psols'] = {}
-        _logger.info(
-            'Search started, initial layer has cost: %e.' %
-            leap_data['best_dist'],
-        )
+        best_dist = self.cost.calc_cost(initial_layer, utry)
+        best_circ = initial_layer
+        best_layer = 0
+        best_dists = [best_dist]
+        best_layers = [0]
+        last_prefix_layer = 0
+
+        # Track partial solutions
+        psols: dict[int, list[tuple[Circuit, float]]] = {}
 
-        if leap_data['best_dist'] < self.success_threshold:
-            _logger.info('Successful synthesis.')
+        _logger.debug(f'Search started, initial layer has cost: {best_dist}.')
+
+        # Evalute initial layer
+        if best_dist < self.success_threshold:
+            _logger.debug('Successful synthesis.')
             return initial_layer
 
-        if 'parallel' in data:  # In Parallel
-            client = get_client()
-            while not frontier.empty():
-                top_circuit, layer = frontier.pop()
-
-                # Generate successors and evaluate each
-                successors = self.layer_gen.gen_successors(top_circuit, data)
-
-                # Submit instantiate jobs
-                futures = client.map(
-                    Circuit.instantiate,
-                    successors,
-                    target=utry,
-                    parallel=True,
-                    **self.instantiate_options,
-                    pure=False,
-                )
-
-                # Wait for and gather results
-                secede()
-                circuits = client.gather(futures)
-                rejoin()
-
-                for circuit in circuits:
-                    if self.evaluate_node(
-                        circuit,
-                        utry,
-                        data,
-                        frontier,
-                        layer,
-                        leap_data,
-                    ):
-                        if self.store_partial_solutions:
-                            data['psols'] = leap_data['psols']
-                        return circuit
-
-        else:  # Sequentially
-            while not frontier.empty():
-                top_circuit, layer = frontier.pop()
-                # Generate successors and evaluate each
-                successors = self.layer_gen.gen_successors(top_circuit, data)
-
-                for circuit in successors:
-                    circuit.instantiate(utry, **self.instantiate_options)
-                    if self.evaluate_node(
-                        circuit,
-                        utry,
-                        data,
-                        frontier,
-                        layer,
-                        leap_data,
+        # Main loop
+        while not frontier.empty():
+            top_circuit, layer = frontier.pop()
+
+            # Generate successors
+            successors = self.layer_gen.gen_successors(top_circuit, data)
+
+            # Instantiate successors
+            circuits = self.execute(
+                data,
+                Circuit.instantiate,
+                successors,
+                target=utry,
+                **self.instantiate_options,
+            )
+
+            # Evaluate successors
+            for circuit in circuits:
+                dist = self.cost.calc_cost(circuit, utry)
+
+                if dist < self.success_threshold:
+                    _logger.debug('Successful synthesis.')
+                    if self.store_partial_solutions:
+                        data['psols'] = psols
+                    return circuit
+
+                if self.check_new_best(layer + 1, dist, best_layer, best_dist):
+                    _logger.debug(
+                        'New best circuit found with %d layer%s and cost: %e.'
+                        % (layer + 1, '' if layer == 0 else 's', dist),
+                    )
+                    best_dist = dist
+                    best_circ = circuit
+                    best_layer = layer + 1
+
+                    if self.check_leap_condition(
+                        layer + 1,
+                        best_dist,
+                        best_layers,
+                        best_dists,
+                        last_prefix_layer,
                     ):
-                        if self.store_partial_solutions:
-                            data['psols'] = leap_data['psols']
-                        return circuit
+                        _logger.debug(f'Prefix formed at {layer + 1} layers.')
+                        last_prefix_layer = layer + 1
+                        frontier.clear()
+                        data['window_markers'].append(circuit.num_cycles)
+                        if self.max_layer is None or layer + 1 < self.max_layer:
+                            frontier.add(circuit, layer + 1)
 
-        _logger.info('Frontier emptied.')
-        _logger.info(
-            'Returning best known circuit with %d layer%s and cost: %e.'
-            % (
-                leap_data['best_layer'], '' if leap_data['best_layer'] == 1
-                else 's', leap_data['best_dist'],
-            ),
-        )
-        if self.store_partial_solutions:
-            data['psols'] = leap_data['psols']
+                if self.store_partial_solutions:
+                    if layer not in psols:
+                        psols[layer] = []
 
-        return leap_data['best_circ']
+                    psols[layer].append((circuit.copy(), dist))
+
+                    if len(psols[layer]) > self.partials_per_depth:
+                        psols[layer].sort(key=lambda x: x[1])
+                        del psols[layer][-1]
 
-    def evaluate_node(
-        self,
-        circuit: Circuit,
-        utry: UnitaryMatrix,
-        data: dict[str, Any],
-        frontier: Frontier,
-        layer: int,
-        leap_data: dict[str, Any],
-    ) -> bool:
-        dist = self.cost.calc_cost(circuit, utry)
-
-        if dist < self.success_threshold:
-            _logger.info('Successful synthesis.')
-            return True
-
-        if self.check_new_best(
-            layer + 1,
-            dist,
-            leap_data['best_layer'],
-            leap_data['best_dist'],
-        ):
-            _logger.info(
-                'New best circuit found with %d layer%s and cost: %e.'
-                % (layer + 1, '' if layer == 0 else 's', dist),
-            )
-            leap_data['best_dist'] = dist
-            leap_data['best_circ'] = circuit
-            leap_data['best_layer'] = layer + 1
-
-            if self.check_leap_condition(layer + 1, leap_data):
-                _logger.info('Prefix formed at %d layers.' % (layer + 1))
-                leap_data['last_prefix_layer'] = layer + 1
-                frontier.clear()
-                data['window_markers'].append(circuit.num_cycles)
                 if self.max_layer is None or layer + 1 < self.max_layer:
                     frontier.add(circuit, layer + 1)
 
+        _logger.warning('Frontier emptied.')
+        _logger.warning(
+            'Returning best known circuit with %d layer%s and cost: %e.'
+            % (best_layer, '' if best_layer == 1 else 's', best_dist),
+        )
         if self.store_partial_solutions:
-            if layer not in leap_data['psols']:
-                leap_data['psols'][layer] = []
-
-            leap_data['psols'][layer].append((circuit.copy(), dist))
-
-            if len(leap_data['psols'][layer]) > self.partials_per_depth:
-                leap_data['psols'][layer].sort(key=lambda x: x[1])
-                del leap_data['psols'][layer][-1]
+            data['psols'] = psols
 
-        if self.max_layer is None or layer + 1 < self.max_layer:
-            frontier.add(circuit, layer + 1)
-        return False
+        return best_circ
 
     def check_new_best(
         self,
@@ -337,7 +293,10 @@ def check_new_best(
     def check_leap_condition(
         self,
         new_layer: int,
-        leap_data: dict[str, Any],
+        best_dist: float,
+        best_layers: list[int],
+        best_dists: list[float],
+        last_prefix_layer: int,
     ) -> bool:
         """
         Return true if the leap condition is satisfied.
@@ -358,25 +317,24 @@ def check_leap_condition(
 
         with np.errstate(invalid='ignore', divide='ignore'):
             # Calculate predicted best value
-            m, y_int, _, _, _ = linregress(
-                leap_data['best_layers'], leap_data['best_dists'],
-            )
+            m, y_int, _, _, _ = linregress(best_layers, best_dists)
+
         predicted_best = m * (new_layer) + y_int
 
         # Track new values
-        leap_data['best_layers'].append(new_layer)
-        leap_data['best_dists'].append(leap_data['best_dist'])
+        best_layers.append(new_layer)
+        best_dists.append(best_dist)
 
         if np.isnan(predicted_best):
             return False
 
         # Compute difference between actual value
-        delta = predicted_best - leap_data['best_dist']
+        delta = predicted_best - best_dist
 
         _logger.debug(
             'Predicted best value %f for new best best with delta %f.'
             % (predicted_best, delta),
         )
 
-        layers_added = new_layer - leap_data['last_prefix_layer']
+        layers_added = new_layer - last_prefix_layer
         return delta < 0 and layers_added >= self.min_prefix_size
diff --git a/bqskit/passes/synthesis/qfast.py b/bqskit/passes/synthesis/qfast.py
index e906a2304..f0e47abc6 100644
--- a/bqskit/passes/synthesis/qfast.py
+++ b/bqskit/passes/synthesis/qfast.py
@@ -6,7 +6,6 @@
 from typing import Any
 from typing import Sequence
 
-from bqskit.compiler.machine import MachineModel
 from bqskit.ir.circuit import Circuit
 from bqskit.ir.gate import Gate
 from bqskit.ir.gates import PauliGate
@@ -25,19 +24,23 @@
 
 class QFASTDecompositionPass(SynthesisPass):
     """
-    The QFASTDecompositionPass class.
+    A pass performing one round of decomposition from the QFAST algorithm.
 
-    Performs one QFAST decomposition step breaking down a unitary into a
-    sequence of smaller operations.
+    References:
+        E. Younis, K. Sen, K. Yelick and C. Iancu, "QFAST: Conflating Search
+        and Numerical Optimization for Scalable Quantum Circuit Synthesis,"
+        2021 IEEE International Conference on Quantum Computing and
+        Engineering (QCE), 2021, pp. 232-243, doi: 10.1109/QCE52317.2021.00041.
     """
 
     def __init__(
         self,
         gate: Gate = PauliGate(2),
-        success_threshold: float = 1e-6,
+        success_threshold: float = 1e-10,
         progress_threshold: float = 5e-3,
         cost: CostFunctionGenerator = HilbertSchmidtResidualsGenerator(),
         max_depth: int | None = None,
+        instantiate_options: dict[str, Any] = {},
     ) -> None:
         """
         QFASTDecompositionPass Constructor.
@@ -68,6 +71,10 @@ def __init__(
                 success before termination. If left as None it will default
                 to unlimited. (Default: None)
 
+            instantiate_options (dict[str: Any]): Options passed directly
+                to circuit.instantiate when instantiating circuit
+                templates. (Default: {})
+
         Raises:
             ValueError: If max_depth is nonpositive.
         """
@@ -110,61 +117,55 @@ def __init__(
         self.progress_threshold = progress_threshold
         self.cost = cost
         self.max_depth = max_depth
+        self.instantiate_options: dict[str, Any] = {
+            'cost_fn_gen': self.cost,
+            'method': 'minimization',
+        }
+        self.instantiate_options.update(instantiate_options)
 
     def synthesize(self, utry: UnitaryMatrix, data: dict[str, Any]) -> Circuit:
         """Synthesize `utry`, see :class:`SynthesisPass` for more."""
 
-        # 0. Skip any unitaries too small for the configured gate.
+        # Skip any unitaries too small for the configured gate.
         if self.gate.num_qudits > utry.num_qudits:
             _logger.warning('Skipping unitary synthesis since gate is larger.')
             return Circuit.from_unitary(utry)
 
-        # 1. Create empty circuit with same size and radixes as `utry`.
+        # Create empty circuit with same size and radixes as `utry`.
         circuit = Circuit(utry.num_qudits, utry.radixes)
 
-        # 2. Calculate relevant coupling_graph and create the VLG head.
-        # If a MachineModel is provided in the data dict, it will be used.
-        # Otherwise all-to-all connectivity is assumed.
-        model = None
-
-        if 'machine_model' in data:
-            model = data['machine_model']
-
-        if (
-            not isinstance(model, MachineModel)
-            or model.num_qudits < circuit.num_qudits
-        ):
-            _logger.warning(
-                'MachineModel not specified or invalid;'
-                ' defaulting to all-to-all.',
-            )
-            model = MachineModel(circuit.num_qudits)
+        # Calculate relevant coupling_graph and create the VLG head.
+        model = self.get_model(utry, data)
         locations = model.get_locations(self.gate.num_qudits)
-        circuit.append_gate(
-            VariableLocationGate(self.gate, locations, circuit.radixes),
-            list(range(utry.num_qudits)),
-        )
+        vlg_head = VariableLocationGate(self.gate, locations, circuit.radixes)
+        circuit.append_gate(vlg_head, list(range(utry.num_qudits)))
 
-        # 3. Bottom-up synthesis: build circuit up one gate at a time
+        # Track depth and distances
         depth = 1
         last_dist = 1.0
         failed_locs: list[tuple[CircuitLocation, float]] = []
 
+        # Main loop
         while True:
-            circuit.instantiate(utry, cost_fn_gen=self.cost)
 
-            dist = self.cost.calc_cost(circuit, utry)
+            # Instantiate circuit
+            circuit = self.execute(
+                data,
+                Circuit.instantiate,
+                [circuit],
+                target=utry,
+                **self.instantiate_options,
+            )[0]
 
-            _logger.info(
-                'Finished optimizing depth %d at %e cost.' % (depth, dist),
-            )
+            dist = self.cost.calc_cost(circuit, utry)
+            _logger.info(f'Instantiated depth {depth} at {dist} cost.')
 
             if dist < self.success_threshold:
-                _logger.info('Circuit found with cost: %e.' % dist)
+                self.finalize(circuit, utry, data)
                 _logger.info('Successful synthesis.')
-                self.finalize(utry, circuit)
                 return circuit
 
+            # Expand or restrict head
             location = self.get_location_of_head(circuit)
 
             if last_dist - dist >= self.progress_threshold:
@@ -172,6 +173,7 @@ def synthesize(self, utry: UnitaryMatrix, data: dict[str, Any]) -> Circuit:
                 last_dist = dist
                 self.expand(circuit, location, locations)
                 depth += 1
+                failed_locs = []
 
             elif not self.can_restrict(circuit[-1, 0]):
                 _logger.info('Progress has not been made.')
@@ -193,6 +195,35 @@ def get_location_of_head(self, circuit: Circuit) -> CircuitLocation:
         head_gate: VariableLocationGate = circuit[-1, 0].gate  # type: ignore
         return CircuitLocation(head_gate.get_location(circuit[-1, 0].params))
 
+    def finalize(
+        self,
+        circuit: Circuit,
+        utry: UnitaryMatrix,
+        data: dict[str, Any],
+    ) -> None:
+        """Finalize the circuit by replacing the head with self.gate."""
+        # Replace Head with self.gate
+        location = self.get_location_of_head(circuit)
+        _logger.info(f'Final gate added at location {location}.')
+        circuit.pop()
+        circuit.append(Operation(self.gate, location))
+
+        # Reinstantiate
+        dist = self.cost.calc_cost(circuit, utry)
+        while dist > self.success_threshold:
+            circuit.become(
+                self.execute(
+                    data,
+                    Circuit.instantiate,
+                    [circuit],
+                    target=utry,
+                    **(self.instantiate_options),
+                )[0],
+            )
+            dist = self.cost.calc_cost(circuit, utry)
+
+        _logger.info(f'Final circuit found with cost: {dist}.')
+
     def expand(
         self,
         circuit: Circuit,
@@ -200,31 +231,11 @@ def expand(
         locations: Sequence[CircuitLocation],
     ) -> None:
         """Expand the circuit after a successful layer."""
-
-        _logger.info(
-            'Expanding circuit by adding a gate at location %s.' % str(
-                location,
-            ),
-        )
-
+        _logger.info(f'Expanding by adding a gate on qubits {location}.')
         circuit.insert(-1, Operation(self.gate, location))
         self.lift_head_restrictions(circuit, locations)
         self.restrict_head(circuit, location)
 
-    def finalize(self, utry: UnitaryMatrix, circuit: Circuit) -> None:
-        """Finalize the circuit."""
-        location = self.get_location_of_head(circuit)
-        _logger.info('Final gate added at location %s.' % str(location))
-
-        circuit.insert(-1, Operation(self.gate, location))
-        circuit.pop()
-
-        # TODO: reconsider method arg to instantiate call
-        circuit.instantiate(utry, cost_fn_gen=self.cost, method='minimization')
-
-        dist = self.cost.gen_cost(circuit, utry).get_cost(circuit.params)
-        _logger.info('Final circuit distance: %e.' % dist)
-
     def can_restrict(self, head: Operation) -> bool:
         """Return true if the VLG head can be restricted further."""
         head_gate: VariableLocationGate = head.gate  # type: ignore
@@ -244,18 +255,12 @@ def restrict_head(
             location (CircuitLocation): The location to remove from the
                 VLG head.
         """
-
-        _logger.debug(
-            'Removing location %s from the VLG head.' % str(location),
-        )
-
+        _logger.debug(f'Removing location {location} from the VLG head.')
         head_gate: VariableLocationGate = circuit[-1, 0].gate  # type: ignore
         locations = copy.deepcopy(head_gate.locations)
         locations.remove(location)
-        new_head = Operation(
-            VariableLocationGate(self.gate, locations, circuit.radixes),
-            list(range(circuit.num_qudits)),
-        )
+        new_vlg = VariableLocationGate(self.gate, locations, circuit.radixes)
+        new_head = Operation(new_vlg, list(range(circuit.num_qudits)))
         circuit.pop()
         circuit.append(new_head)
 
@@ -265,16 +270,9 @@ def lift_head_restrictions(
         locations: Sequence[CircuitLocation],
     ) -> None:
         """Set the `circuit`'s VLG head's valid locations to `locations`."""
-
-        _logger.debug(
-            'Lifting restrictions, setting VLG head location pool to %s.'
-            % str(locations),
-        )
-
-        new_head = Operation(
-            VariableLocationGate(self.gate, locations, circuit.radixes),
-            list(range(circuit.num_qudits)),
-        )
-
+        _logger.debug('Lifting restrictions.')
+        _logger.debug(f'Reset VLG head location pool to {locations}.')
+        vlg_gate = VariableLocationGate(self.gate, locations, circuit.radixes)
+        new_head = Operation(vlg_gate, list(range(circuit.num_qudits)))
         circuit.pop()
         circuit.append(new_head)
diff --git a/bqskit/passes/synthesis/qpredict.py b/bqskit/passes/synthesis/qpredict.py
index fee8e0443..dd2d65403 100644
--- a/bqskit/passes/synthesis/qpredict.py
+++ b/bqskit/passes/synthesis/qpredict.py
@@ -7,7 +7,6 @@
 
 import numpy as np
 
-from bqskit.compiler.machine import MachineModel
 from bqskit.ir.circuit import Circuit
 from bqskit.ir.gates import VariableUnitaryGate
 from bqskit.ir.location import CircuitLocation
@@ -35,9 +34,9 @@ def __init__(
         block_size_start: int = 2,
         block_size_limit: int | None = None,
         fail_limit: int = 3,
-        success_threshold: float = 1e-6,
+        success_threshold: float = 1e-8,
         progress_threshold_r: float = 5e-2,
-        progress_threshold_a: float = 1e-4,
+        progress_threshold_a: float = 1e-8,
         cost: CostFunctionGenerator = HilbertSchmidtResidualsGenerator(),
         max_depth: int | None = None,
         instantiate_options: dict[str, Any] = {},
@@ -185,9 +184,9 @@ def synthesize(self, utry: UnitaryMatrix, data: dict[str, Any]) -> Circuit:
         """Synthesize `utry`, see :class:`SynthesisPass` for more."""
 
         # 0. Skip any unitaries too small for the configured block.
-        if self.block_size_start > utry.num_qudits:
+        if self.block_size_start >= utry.num_qudits:
             _logger.warning(
-                'Skipping synthesis: block size is larger than input unitary.',
+                'Skipping synthesis: block size is too large.',
             )
             return Circuit.from_unitary(utry)
 
@@ -200,22 +199,7 @@ def synthesize(self, utry: UnitaryMatrix, data: dict[str, Any]) -> Circuit:
             block_size_end = self.block_size_limit
 
         # 3. Calculate relevant coupling_graphs
-        # If a MachineModel is provided in the data dict, it will be used.
-        # Otherwise all-to-all connectivity is assumed.
-        model = None
-
-        if 'machine_model' in data:
-            model = data['machine_model']
-
-        if (
-            not isinstance(model, MachineModel)
-            or model.num_qudits < circuit.num_qudits
-        ):
-            _logger.warning(
-                'MachineModel not specified or invalid;'
-                ' defaulting to all-to-all.',
-            )
-            model = MachineModel(circuit.num_qudits)
+        model = self.get_model(utry, data)
         locations = [
             model.get_locations(i)
             for i in range(self.block_size_start, block_size_end + 1)
@@ -281,7 +265,7 @@ def analyze_remainder(
                 based on remainder analysis.
         """
         _logger.info('Performing remainder analysis.')
-        pauli_coefs = pauli_expansion(unitary_log_no_i(R.numpy))
+        pauli_coefs = pauli_expansion(unitary_log_no_i(R))  # type: ignore
 
         locations_by_index: dict[int, set[CircuitLocation]] = {}
         for location_group in locations:
diff --git a/bqskit/passes/synthesis/qsearch.py b/bqskit/passes/synthesis/qsearch.py
index 024fa882c..f10ccc712 100644
--- a/bqskit/passes/synthesis/qsearch.py
+++ b/bqskit/passes/synthesis/qsearch.py
@@ -4,10 +4,6 @@
 import logging
 from typing import Any
 
-from dask.distributed import get_client
-from dask.distributed import rejoin
-from dask.distributed import secede
-
 from bqskit.ir.circuit import Circuit
 from bqskit.ir.opt.cost.functions import HilbertSchmidtResidualsGenerator
 from bqskit.ir.opt.cost.generator import CostFunctionGenerator
@@ -142,130 +138,84 @@ def synthesize(self, utry: UnitaryMatrix, data: dict[str, Any]) -> Circuit:
 
         # Seed the search with an initial layer
         initial_layer = self.layer_gen.gen_initial_layer(utry, data)
-        initial_layer.instantiate(utry, **self.instantiate_options)
+        initial_layer = self.execute(
+            data,
+            Circuit.instantiate,
+            [initial_layer],
+            target=utry,
+            **self.instantiate_options,
+        )[0]
         frontier.add(initial_layer, 0)
 
         # Track best circuit, initially the initial layer
-        search_data: dict[str, Any] = {}
-        search_data['best_dist'] = self.cost.calc_cost(initial_layer, utry)
-        search_data['best_circ'] = initial_layer
-        search_data['best_layer'] = 0
-        if self.store_partial_solutions:
-            search_data['psols'] = {}
-        _logger.info(
-            'Search started, initial layer has cost: %e.' %
-            search_data['best_dist'],
-        )
+        best_dist = self.cost.calc_cost(initial_layer, utry)
+        best_circ = initial_layer
+        best_layer = 0
+
+        # Track partial solutions
+        psols: dict[int, list[tuple[Circuit, float]]] = {}
+
+        _logger.debug(f'Search started, initial layer has cost: {best_dist}.')
 
-        if search_data['best_dist'] < self.success_threshold:
-            _logger.info('Successful synthesis.')
+        # Evalute initial layer
+        if best_dist < self.success_threshold:
+            _logger.debug('Successful synthesis.')
             return initial_layer
 
-        if 'parallel' in data:  # In Parallel
-            client = get_client()
-            while not frontier.empty():
-                top_circuit, layer = frontier.pop()
-
-                # Generate successors and evaluate each
-                successors = self.layer_gen.gen_successors(top_circuit, data)
-
-                # Submit instantiate jobs
-                futures = client.map(
-                    Circuit.instantiate,
-                    successors,
-                    target=utry,
-                    parallel=True,
-                    **self.instantiate_options,
-                    pure=False,
-                )
-
-                # Wait for and gather results
-                secede()
-                circuits = client.gather(futures)
-                rejoin()
-
-                for circuit in circuits:
-                    if self.evaluate_node(
-                        circuit,
-                        utry,
-                        data,
-                        frontier,
-                        layer,
-                        search_data,
-                    ):
-                        if self.store_partial_solutions:
-                            data['psols'] = search_data['psols']
-                        return circuit
-
-        else:  # Sequentially
-            while not frontier.empty():
-                top_circuit, layer = frontier.pop()
-
-                # Generate successors and evaluate each
-                successors = self.layer_gen.gen_successors(top_circuit, data)
-
-                for circuit in successors:
-                    circuit.instantiate(utry, **self.instantiate_options)
-                    if self.evaluate_node(
-                        circuit,
-                        utry,
-                        data,
-                        frontier,
-                        layer,
-                        search_data,
-                    ):
-                        if self.store_partial_solutions:
-                            data['psols'] = search_data['psols']
-                        return circuit
-
-        _logger.info('Frontier emptied.')
-        _logger.info(
-            'Returning best known circuit with %d layer%s and cost: %e.'
-            % (
-                search_data['best_layer'], '' if search_data['best_layer'] == 1
-                else 's', search_data['best_dist'],
-            ),
-        )
-        if self.store_partial_solutions:
-            data['psols'] = search_data['psols']
+        # Main loop
+        while not frontier.empty():
+            top_circuit, layer = frontier.pop()
 
-        return search_data['best_circ']
+            # Generate successors
+            successors = self.layer_gen.gen_successors(top_circuit, data)
 
-    def evaluate_node(
-        self,
-        circuit: Circuit,
-        utry: UnitaryMatrix,
-        data: dict[str, Any],
-        frontier: Frontier,
-        layer: int,
-        search_data: dict[str, Any],
-    ) -> bool:
-        dist = self.cost.calc_cost(circuit, utry)
-
-        if dist < self.success_threshold:
-            _logger.info('Successful synthesis.')
-            return True
-
-        if dist < search_data['best_dist']:
-            _logger.info(
-                'New best circuit found with %d layer%s and cost: %e.'
-                % (layer + 1, '' if layer == 0 else 's', dist),
+            # Instantiate successors
+            circuits = self.execute(
+                data,
+                Circuit.instantiate,
+                successors,
+                target=utry,
+                **self.instantiate_options,
             )
-            search_data['best_dist'] = dist
-            search_data['best_circ'] = circuit
-            search_data['best_layer'] = layer
 
-        if self.store_partial_solutions:
-            if layer not in search_data['psols']:
-                search_data['psols'][layer] = []
+            # Evaluate successors
+            for circuit in circuits:
+                dist = self.cost.calc_cost(circuit, utry)
+
+                if dist < self.success_threshold:
+                    _logger.debug('Successful synthesis.')
+                    if self.store_partial_solutions:
+                        data['psols'] = psols
+                    return circuit
+
+                if dist < best_dist:
+                    _logger.debug(
+                        'New best circuit found with %d layer%s and cost: %e.'
+                        % (layer + 1, '' if layer == 0 else 's', dist),
+                    )
+                    best_dist = dist
+                    best_circ = circuit
+                    best_layer = layer
+
+                if self.store_partial_solutions:
+                    if layer not in psols:
+                        psols[layer] = []
 
-            search_data['psols'][layer].append((circuit.copy(), dist))
+                    psols[layer].append((circuit.copy(), dist))
 
-            if len(search_data['psols'][layer]) > self.partials_per_depth:
-                search_data['psols'][layer].sort(key=lambda x: x[1])
-                del search_data['psols'][layer][-1]
+                    if len(psols[layer]) > self.partials_per_depth:
+                        psols[layer].sort(key=lambda x: x[1])
+                        del psols[layer][-1]
 
-        if self.max_layer is None or layer + 1 < self.max_layer:
-            frontier.add(circuit, layer + 1)
+                if self.max_layer is None or layer + 1 < self.max_layer:
+                    frontier.add(circuit, layer + 1)
+
+        _logger.warning('Frontier emptied.')
+        _logger.warning(
+            'Returning best known circuit with %d layer%s and cost: %e.'
+            % (best_layer, '' if best_layer == 1 else 's', best_dist),
+        )
+        if self.store_partial_solutions:
+            data['psols'] = psols
 
-        return False
+        return best_circ
diff --git a/bqskit/passes/synthesis/synthesis.py b/bqskit/passes/synthesis/synthesis.py
index b864b556a..03c370f63 100644
--- a/bqskit/passes/synthesis/synthesis.py
+++ b/bqskit/passes/synthesis/synthesis.py
@@ -1,20 +1,20 @@
 """This module implements the SynthesisPass abstract class."""
 from __future__ import annotations
 
-import logging
 from abc import abstractmethod
-from typing import Any
+from typing import TYPE_CHECKING
 
 from bqskit.compiler.basepass import BasePass
-from bqskit.ir.circuit import Circuit
-from bqskit.qis.unitary.unitarymatrix import UnitaryMatrix
 
-_logger = logging.getLogger(__name__)
+if TYPE_CHECKING:
+    from typing import Any
+    from bqskit.ir.circuit import Circuit
+    from bqskit.qis.unitary.unitarymatrix import UnitaryMatrix
 
 
 class SynthesisPass(BasePass):
     """
-    SynthesisPass class.
+    SynthesisPass abstract class.
 
     The SynthesisPass is a base class that exposes an abstract
     synthesize function. Inherit from this class and implement the
@@ -43,6 +43,8 @@ def synthesize(self, utry: UnitaryMatrix, data: dict[str, Any]) -> Circuit:
 
     def run(self, circuit: Circuit, data: dict[str, Any] = {}) -> None:
         """Perform the pass's operation, see :class:`BasePass` for more."""
+        if len(data) == 0:
+            data = dict()
 
         target_utry = self.get_target(circuit, data)
         circuit.become(self.synthesize(target_utry, data))
diff --git a/bqskit/passes/util/__init__.py b/bqskit/passes/util/__init__.py
index ea1776716..d8352fd83 100644
--- a/bqskit/passes/util/__init__.py
+++ b/bqskit/passes/util/__init__.py
@@ -4,6 +4,10 @@
 from bqskit.passes.util.compress import CompressPass
 from bqskit.passes.util.conversion import BlockConversionPass
 from bqskit.passes.util.converttou3 import ToU3Pass
+from bqskit.passes.util.extend import ExtendBlockSizePass
+from bqskit.passes.util.fill import FillSingleQuditGatesPass
+from bqskit.passes.util.log import LogErrorPass
+from bqskit.passes.util.log import LogPass
 from bqskit.passes.util.random import SetRandomSeedPass
 from bqskit.passes.util.record import RecordStatsPass
 from bqskit.passes.util.unfold import UnfoldPass
@@ -17,4 +21,8 @@
     'UpdateDataPass',
     'ToU3Pass',
     'BlockConversionPass',
+    'LogPass',
+    'ExtendBlockSizePass',
+    'LogErrorPass',
+    'FillSingleQuditGatesPass',
 ]
diff --git a/bqskit/passes/util/extend.py b/bqskit/passes/util/extend.py
new file mode 100644
index 000000000..7ba189621
--- /dev/null
+++ b/bqskit/passes/util/extend.py
@@ -0,0 +1,95 @@
+"""This module implements the ExtendBlockSizePass class."""
+from __future__ import annotations
+
+import logging
+from typing import Any
+from typing import Sequence
+
+from bqskit.compiler.basepass import BasePass
+from bqskit.ir.circuit import Circuit
+from bqskit.ir.gates.circuitgate import CircuitGate
+from bqskit.ir.operation import Operation
+from bqskit.qis.graph import CouplingGraph
+from bqskit.utils.typing import is_integer
+
+
+_logger = logging.getLogger(__name__)
+
+
+class ExtendBlockSizePass(BasePass):
+    """Ensure all blocks are at least a given size."""
+
+    def __init__(self, minimum_size: int) -> None:
+        """
+        Construct a ExtendBlockSizePass.
+
+        Args:
+            minimum_size (int): Extend all blocks to at least this size.
+        """
+        if not is_integer(minimum_size):
+            raise TypeError('Expected an integer for minimum size.')
+
+        self.minimum_size = minimum_size
+
+    def run(self, circuit: Circuit, data: dict[str, Any] = {}) -> None:
+        """Perform the pass's operation, see :class:`BasePass` for more."""
+        if circuit.num_qudits < self.minimum_size:
+            raise RuntimeError('Cannot extend block larger than circuit.')
+
+        cg = self.get_connectivity(circuit, data)
+
+        # Find all small blocks
+        small_blocks: list[tuple[int, int]] = []
+        for cycle, op in circuit.operations_with_cycles():
+            if isinstance(op.gate, CircuitGate):
+                if op.gate.num_qudits < self.minimum_size:
+                    small_blocks.append((cycle, op.location[0]))
+        small_blocks.sort()
+
+        # Select qubits to add
+        qudits_to_extend: list[tuple[int, ...]] = []
+        for block_point in small_blocks:
+            cycle = block_point[0]
+            op = circuit[block_point]
+
+            num_to_add = self.minimum_size - op.gate.num_qudits
+            qudits = list(op.location)
+            added = []
+            for _ in range(num_to_add):
+                neighbors = self.get_neighbors(qudits, cg)
+
+                if len(neighbors) == 0:
+                    raise RuntimeError('Coupling graph is not fully connected.')
+
+                if any(circuit.is_point_idle((cycle, n)) for n in neighbors):
+                    for n in neighbors:
+                        if circuit.is_point_idle((cycle, n)):
+                            added.append(n)
+                            qudits.append(n)
+                            break
+                else:
+                    added.append(neighbors[0])
+                    qudits.append(neighbors[0])
+            qudits_to_extend.append(tuple(added))
+
+        # Build extended operations
+        new_ops: list[Operation] = []
+        for block_point, to_add_qudits in zip(small_blocks, qudits_to_extend):
+            op = circuit[block_point]
+            op_circ: Circuit = op.gate._circuit.copy()  # type: ignore
+            for qudit in to_add_qudits:
+                op_circ.append_qudit(circuit.radixes[qudit])
+            gate = CircuitGate(op_circ, True)
+            location = tuple(op.location) + to_add_qudits
+            new_op = Operation(gate, location, op.params)
+            new_ops.append(new_op)
+
+        # Extend operations
+        circuit.batch_replace(small_blocks, new_ops)
+
+    def get_neighbors(self, l: Sequence[int], cg: CouplingGraph) -> list[int]:
+        """Return the neighbors of location `l` in `cg`."""
+        neighbors = set()
+        for q in l:
+            neighbors.update(cg.get_neighbors_of(q))
+        return list(neighbors.difference(l))
diff --git a/bqskit/passes/util/fill.py b/bqskit/passes/util/fill.py
new file mode 100644
index 000000000..5a384c1c8
--- /dev/null
+++ b/bqskit/passes/util/fill.py
@@ -0,0 +1,58 @@
+"""This module implements the FillSingleQuditGatesPass class."""
+from __future__ import annotations
+
+import logging
+from typing import Any
+
+from bqskit.compiler.basepass import BasePass
+from bqskit.ir.circuit import Circuit
+from bqskit.ir.gates.parameterized.u3 import U3Gate
+
+
+_logger = logging.getLogger(__name__)
+
+
+class FillSingleQuditGatesPass(BasePass):
+    """A pass that inserts single-qudit gates around multi-qudit gates."""
+
+    def __init__(self, success_threshold: float = 1e-10):
+        """
+        Construct a FillSingleQuditGatesPass.
+
+        Args:
+            success_threshold (bool): Reinstantiate the new filled circuit
+                to be within this distance from initial starting circuit.
+        """
+        self.success_threshold = success_threshold
+
+    def run(self, circuit: Circuit, data: dict[str, Any] = {}) -> None:
+        """Perform the pass's operation, see :class:`BasePass` for more."""
+        _logger.debug('Completing circuit with single-qudit gates.')
+        target = self.get_target(circuit, data)
+
+        complete_circuit = Circuit(circuit.num_qudits, circuit.radixes)
+
+        if target.num_qudits == 1:
+            params = U3Gate.calc_params(target)
+            complete_circuit.append_gate(U3Gate(), 0, params)
+            circuit.become(complete_circuit)
+            return
+
+        for q in range(circuit.num_qudits):
+            complete_circuit.append_gate(U3Gate(), q)
+
+        for op in circuit:
+            if op.num_qudits == 1:
+                continue
+
+            complete_circuit.append(op)
+            for q in op.location:
+                complete_circuit.append_gate(U3Gate(), q)
+
+        dist = 1.0
+        for i in range(10):
+            complete_circuit.instantiate(target)
+            dist = complete_circuit.get_unitary().get_distance_from(target, 1)
+
+        if dist <= self.success_threshold:
+            circuit.become(complete_circuit)
diff --git a/bqskit/passes/util/log.py b/bqskit/passes/util/log.py
new file mode 100644
index 000000000..0b8646b57
--- /dev/null
+++ b/bqskit/passes/util/log.py
@@ -0,0 +1,61 @@
+"""This module implements the LogPass and LogErrorPass classes."""
+from __future__ import annotations
+
+import logging
+from typing import Any
+
+import numpy as np
+
+from bqskit.compiler.basepass import BasePass
+from bqskit.ir.circuit import Circuit
+
+
+_logger = logging.getLogger(__name__)
+
+
+class LogPass(BasePass):
+    """A pass that logs a message to the BQSKit log."""
+
+    def __init__(self, msg: str, level: int = logging.INFO) -> None:
+        """
+        Construct a LogPass.
+
+        Args:
+            msg (str): The message to log.
+
+            level (int): The logging verbosity level.
+        """
+        self.msg = msg
+        self.level = level
+
+    def run(self, circuit: Circuit, data: dict[str, Any] = {}) -> None:
+        """Perform the pass's operation, see :class:`BasePass` for more."""
+        _logger.log(self.level, self.msg)
+
+
+class LogErrorPass(BasePass):
+    """A pass that logs the current error to the BQSKit log."""
+
+    def __init__(self, error_threshold: float | None = None) -> None:
+        """
+        Construct a LogErrorPass.
+
+        Args:
+            error_threshold (float | None): Logs a warning if the error
+                is above this threshold. If None, never log a warning.
+                (Default: None)
+        """
+        self.threshold = error_threshold
+
+    def run(self, circuit: Circuit, data: dict[str, Any] = {}) -> None:
+        """Perform the pass's operation, see :class:`BasePass` for more."""
+        if 'error' in data:
+            error = data['error']
+            nonsq_error = 1 - np.sqrt(max(1 - (error * error), 0))
+            if self.threshold is not None and nonsq_error > self.threshold:
+                _logger.warn(
+                    'Upper bound on error is greater than set threshold:'
+                    f' {nonsq_error} > {self.threshold}.',
+                )
+            else:
+                _logger.info(f'Upper bound on error is {nonsq_error}.')
diff --git a/bqskit/qis/__init__.py b/bqskit/qis/__init__.py
index d50f72a4b..d54b3c5a6 100644
--- a/bqskit/qis/__init__.py
+++ b/bqskit/qis/__init__.py
@@ -1,6 +1,6 @@
 """
 =======================================================
-Quantum Information Science Objects (:mod:`bqskit.qis`)
+Quantum Information Science Library (:mod:`bqskit.qis`)
 =======================================================
 
 .. currentmodule:: bqskit.qis
diff --git a/bqskit/qis/graph.py b/bqskit/qis/graph.py
index cba33c9a3..5bfca4003 100644
--- a/bqskit/qis/graph.py
+++ b/bqskit/qis/graph.py
@@ -1,17 +1,22 @@
 """This module implements the CouplingGraph class."""
 from __future__ import annotations
 
+import copy
 import itertools as it
 import logging
-from functools import lru_cache
+from random import shuffle
 from typing import Any
+from typing import cast
 from typing import Collection
 from typing import Iterable
 from typing import Iterator
+from typing import List
 from typing import Tuple
 from typing import TYPE_CHECKING
 from typing import Union
 
+import numpy as np
+
 if TYPE_CHECKING:
     from typing_extensions import TypeGuard
 
@@ -26,7 +31,11 @@
 class CouplingGraph(Collection[Tuple[int, int]]):
     """A graph representing connections in a qudit topology."""
 
-    def __init__(self, graph: Iterable[tuple[int, int]]) -> None:
+    def __init__(
+        self,
+        graph: Iterable[tuple[int, int]],
+        num_qudits: int | None = None,
+    ) -> None:
         if isinstance(graph, CouplingGraph):
             self.num_qudits: int = graph.num_qudits
             self._edges: set[tuple[int, int]] = graph._edges
@@ -36,27 +45,43 @@ def __init__(self, graph: Iterable[tuple[int, int]]) -> None:
         if not CouplingGraph.is_valid_coupling_graph(graph):
             raise TypeError('Invalid coupling graph.')
 
-        self._edges = set(graph)
+        self._edges = {g if g[0] <= g[1] else (g[1], g[0]) for g in graph}
 
-        self.num_qudits = 0
+        calced_num_qudits = 0
         for q1, q2 in self._edges:
-            self.num_qudits = max(self.num_qudits, max(q1, q2))
-        self.num_qudits += 1
+            calced_num_qudits = max(calced_num_qudits, max(q1, q2))
+        calced_num_qudits += 1
+
+        if num_qudits is None:
+            self.num_qudits = calced_num_qudits
+        elif calced_num_qudits > num_qudits:
+            raise ValueError('Edges between invalid qudits.')
+        else:
+            self.num_qudits = num_qudits
 
         self._adj = [[] for _ in range(self.num_qudits)]
         for q1, q2 in self._edges:
             self._adj[q1].append(q2)
             self._adj[q2].append(q1)
 
+        self._mat = [
+            [np.inf for _ in range(self.num_qudits)]
+            for _ in range(self.num_qudits)
+        ]
+        for q1, q2 in self._edges:
+            self._mat[q1][q2] = 1
+            self._mat[q2][q1] = 1
+
     def is_fully_connected(self) -> bool:
         """Return true if the graph is fully connected."""
-        frontier = {0}
-        qudits_seen = {0}
+        frontier: set[int] = {0}
+        qudits_seen: set[int] = set()
 
         while len(frontier) > 0:
             expanded_qudits = set()
 
             for qudit in frontier:
+                expanded_qudits.add(qudit)
                 expanded_qudits.update(self._adj[qudit])
 
             frontier = expanded_qudits - qudits_seen
@@ -75,13 +100,22 @@ def __contains__(self, __o: object) -> bool:
         return self._edges.__contains__(__o)
 
     def __eq__(self, __o: object) -> bool:
-        return self._edges.__eq__(__o)
+        if not isinstance(__o, CouplingGraph):
+            return False
+
+        if self.num_qudits != __o.num_qudits:
+            return False
+
+        if self._mat != __o._mat:
+            return False
+
+        return True
 
     def __iter__(self) -> Iterator[tuple[int, int]]:
         return self._edges.__iter__()
 
     def __hash__(self) -> int:
-        return hash(tuple(self._edges))
+        return hash((self.num_qudits, tuple(self._edges)))
 
     def __len__(self) -> int:
         return self._edges.__len__()
@@ -95,6 +129,53 @@ def __repr__(self) -> str:
     def get_qudit_degrees(self) -> list[int]:
         return [len(l) for l in self._adj]
 
+    def all_pairs_shortest_path(self) -> list[list[int]]:
+        """
+        Calculate all pairs shortest path matrix using Floyd-Warshall.
+
+        Returns:
+            D (list[list[int]]): D[i][j] is the length of the shortest
+                path from i to j.
+        """
+        D = copy.deepcopy(self._mat)
+        for k in range(self.num_qudits):
+            for i in range(self.num_qudits):
+                for j in range(self.num_qudits):
+                    D[i][j] = min(D[i][j], D[i][k] + D[k][j])
+        return cast(List[List[int]], D)
+
+    def get_shortest_path_tree(self, source: int) -> list[tuple[int, ...]]:
+        """Return shortest path from `source` to every node in `self`."""
+        # Dijkstra's algorithm to build shortest-path tree
+        unvisited_qudits = set(range(self.num_qudits))
+        distances = {i: np.inf for i in range(self.num_qudits)}
+        paths: list[tuple[int, ...]] = [tuple() for i in range(self.num_qudits)]
+        distances[source] = 0
+        paths[source] = (source,)
+
+        while len(unvisited_qudits) > 0:
+            # Pick next unvisited qudit with shortest distance
+            unvisited_distances = [
+                (x, y) for x, y in distances.items() if x in unvisited_qudits
+            ]
+            unvisited_distances.sort(key=lambda x: x[1])
+            current_qudit = unvisited_distances[0][0]
+
+            if distances[current_qudit] == np.inf:
+                raise RuntimeError(f'No path found to qudit: {current_qudit}.')
+
+            neighbors = self.get_neighbors_of(current_qudit)
+            unvisted_neighbors = unvisited_qudits.intersection(neighbors)
+
+            for other_qudit in unvisted_neighbors:
+                if distances[current_qudit] + 1 < distances[other_qudit]:
+                    distances[other_qudit] = distances[current_qudit] + 1
+                    paths[other_qudit] = paths[current_qudit] + (other_qudit,)
+
+            unvisited_qudits.remove(current_qudit)
+
+        return [paths[i] for i in range(self.num_qudits)]
+
     def get_subgraph(
         self,
         location: CircuitLocationLike,
@@ -106,15 +187,14 @@ def get_subgraph(
 
         location = CircuitLocation(location)
         if renumbering is None:
-            renumbering = {x: x for x in range(self.num_qudits)}
+            renumbering = {q: i for i, q in enumerate(location)}
 
         subgraph = []
         for q0, q1 in self._edges:
             if q0 in location and q1 in location:
                 subgraph.append((renumbering[q0], renumbering[q1]))
-        return CouplingGraph(subgraph)
+        return CouplingGraph(subgraph, len(location))
 
-    @lru_cache(maxsize=None)
     def get_subgraphs_of_size(self, size: int) -> list[CircuitLocation]:
         """
         Find all sets of indices that form connected subgraphs on their own.
@@ -146,7 +226,7 @@ def get_subgraphs_of_size(self, size: int) -> list[CircuitLocation]:
         if size > self.num_qudits:
             raise ValueError(
                 'The block_size is too large; '
-                f'expected <= {self.num_qudits}, got {size}.',
+                f'graph size is {self.num_qudits}, got {size}.',
             )
 
         if size <= 0:
@@ -259,11 +339,139 @@ def is_valid_coupling_graph(
 
     @staticmethod
     def all_to_all(num_qudits: int) -> CouplingGraph:
+        """Return a coupling graph with all qudits connected."""
         return CouplingGraph(set(it.combinations(range(num_qudits), 2)))
 
     @staticmethod
     def linear(num_qudits: int) -> CouplingGraph:
+        """Return a coupling graph with nearest-neighbor connectivity."""
         return CouplingGraph([(x, x + 1) for x in range(num_qudits - 1)])
 
+    @staticmethod
+    def ring(num_qudits: int) -> CouplingGraph:
+        """Return a coupling graph with ring connectivity."""
+        ext = [(0, num_qudits - 1)]
+        return CouplingGraph([(x, x + 1) for x in range(num_qudits - 1)] + ext)
+
+    @staticmethod
+    def star(num_qudits: int) -> CouplingGraph:
+        """Return a coupling graph with one qudit connected to all else."""
+        return CouplingGraph([(0, x) for x in range(1, num_qudits)])
+
+    @staticmethod
+    def grid(num_rows: int, num_cols: int) -> CouplingGraph:
+        """Return a coupling graph with a grid of qubits."""
+        num_qudits = num_rows * num_cols
+        edges = set()
+
+        for i in range(num_qudits):
+            if i % num_cols != num_cols - 1:
+                edges.add((i, i + 1))
+
+            if i < (num_rows - 1) * num_cols:
+                edges.add((i, i + num_cols))
+
+        return CouplingGraph(edges)
+
+    def maximal_matching(
+        self,
+        edges_to_ignore: list[tuple[int, int]] = [],
+        randomize: bool = False,
+    ) -> list[tuple[int, int]]:
+        """
+        Generate a random graph matching for the coupling graph. Edges in the
+        `ignored_edges` list will not be included.
+
+        Arguments:
+            edges_to_ignore (list[tuple[int]]): Edges not included in the
+                matching. (Default: [])
+
+            randomize (bool): Shuffle edges to create random matchings.
+                (Default: False)
+
+        Returns:
+            matching (list[tuple[int]]): A maximal list of edges that share
+                no common verticies.
+        """
+        matching: set[tuple[int, int]] = set()
+        vertices: set[int] = set()
+
+        edge_list = [
+            (u, v) for (u, v) in self._edges if (u, v) not in edges_to_ignore
+            and (v, u) not in edges_to_ignore
+        ]
+        if randomize:
+            shuffle(edge_list)
+
+        for edge in edge_list:
+            u, v = edge
+            if u not in vertices and v not in vertices and u != v:
+                matching.add(edge)
+                vertices.update(edge)
+        return list(matching)
+
+    def is_fully_connected_without(self, qudit: int) -> bool:
+        """Return true if the graph is fully connected without `qudit`."""
+        frontier = {0} if qudit != 0 else {1}
+        qudits_seen = {0} if qudit != 0 else {1}
+
+        while len(frontier) > 0:
+            expanded_qudits = set()
+
+            for q in frontier:
+                if q != qudit:
+                    neighbors = set(self.get_neighbors_of(q)) - {qudit}
+                    expanded_qudits.update(neighbors)
+
+            frontier = expanded_qudits - qudits_seen
+            qudits_seen.update(expanded_qudits)
+
+            if len(qudits_seen) == self.num_qudits - 1:
+                return True
+
+        return False
+
+    def get_rooted_minimum_span(self, root: int) -> list[tuple[int, int]]:
+        """
+        Connect `root` to every other node in the graph.
+
+        Args:
+            root (int): The qudit to start from.
+
+        Return:
+            (list[tuple[int, int]]): A list of minimal edges that
+                connects 'root' to every other qudit.
+        """
+        # Construct Minimum Spanning Tree using breadth-first search
+        mst = []
+        seen = {root}
+        frontier = [root]
+
+        while len(frontier) > 0 and len(seen) < self.num_qudits:
+            qudit = frontier.pop(0)
+            neighbors = set(self.get_neighbors_of(qudit))
+            unseen_neighbors = neighbors - seen
+            frontier.extend(unseen_neighbors)
+            seen.update(neighbors)
+            for neighbor in unseen_neighbors:
+                mst.append((qudit, neighbor))
+
+        mst = CouplingGraph(mst)
+
+        # Traverse MST depth-first to produce pairs of interacting qudits
+        interactions: list[tuple[int, int]] = []
+        frontier: list[tuple[int, tuple[int, int] | None]] = [(root, None)]
+
+        while len(frontier) > 0:
+            qudit, interaction = frontier.pop(0)
+            if interaction is not None:
+                interactions.append(interaction)
+
+            for neighbor in mst.get_neighbors_of(qudit):
+                if interaction is None or neighbor != interaction[0]:
+                    frontier.insert(0, (neighbor, (qudit, neighbor)))
+
+        return interactions
+
 
 CouplingGraphLike = Union[Iterable[Tuple[int, int]], CouplingGraph]
diff --git a/bqskit/qis/permutation.py b/bqskit/qis/permutation.py
index 2bb1d7230..ba5f07ed1 100644
--- a/bqskit/qis/permutation.py
+++ b/bqskit/qis/permutation.py
@@ -2,13 +2,11 @@
 from __future__ import annotations
 
 import logging
-from functools import lru_cache
 from typing import Sequence
 
 import numpy as np
-from bqskitrs import calc_permutation_matrix
 
-from bqskit.ir.location import CircuitLocation
+from bqskit.qis.unitary.unitarybuilder import UnitaryBuilder
 from bqskit.qis.unitary.unitarymatrix import UnitaryLike
 from bqskit.qis.unitary.unitarymatrix import UnitaryMatrix
 _logger = logging.getLogger(__name__)
@@ -33,70 +31,58 @@ def __init__(
 
         super().__init__(input, radixes, check_arguments)
 
+    swap_utry = UnitaryMatrix([
+        [1, 0, 0, 0],
+        [0, 0, 1, 0],
+        [0, 1, 0, 0],
+        [0, 0, 0, 1],
+    ])
+
     @staticmethod
-    @lru_cache(maxsize=None)
     def from_qubit_location(
         num_qubits: int,
-        location: tuple[int, ...],
+        location: Sequence[int],
     ) -> PermutationMatrix:
         """
         Creates the permutation matrix specified by arguments.
 
-        The resulting matrix will move the first len(location) qubits
-        into positions defined by location.
+        The resulting matrix will move the location's qubits into positions
+        defined by their index in location. For example:
+
+        P = Permutationmatrix.from_qubit_location(3, (1, 2, 0))
+
+                .-----.
+             0 -|     |- 1
+             1 -|  P  |- 2
+             2 -|     |- 0
+                '-----'
 
         Args:
             num_qubits (int): Total number of qubits
 
-            location (tuple[int, ...]): The desired locations to swap
-                the starting qubits to.
+            location (Sequence[int]): The desired qubits to swap
+                to their index.
 
         Returns:
-            PermutationMatrix: A 2**num_qubits by 2**num_qubits permutation
-            matrix.
-
-        Examples:
-            >>> calc_permutation_matrix( 2, (0, 1) )
-                [ [ 1, 0, 0, 0 ],
-                  [ 0, 1, 0, 0 ],
-                  [ 0, 0, 1, 0 ],
-                  [ 0, 0, 0, 1 ] ]
-
-            Here the 4x4 identity is returned because there are 2 total
-            qubits, specified by the first parameter, and the desired
-            permutation is [0, 1] -> [0, 1].
-
-            >>> calc_permutation_matrix( 2, (1,) ) = # Also equals
-            >>> calc_permutation_matrix( 2, (1, 0) ) =
-                [ [ 1, 0, 0, 0 ],
-                  [ 0, 0, 1, 0 ],
-                  [ 0, 1, 0, 0 ],
-                  [ 0, 0, 0, 1 ] ]
-
-            This is a more interesting example. The swap gate's unitary
-            is returned here since we are working with 2 qubits and want
-            the permutation that swaps the both qubits. This is given by
-            the permutation [0] -> [1] and [0, 1] -> [1, 0] in the second
-            case. Both calls produce identical permutations.
+            (PermutationMatrix): A 2**num_qubits by 2**num_qubits
+                permutation matrix.
         """
-
-        if not isinstance(num_qubits, int):
-            raise TypeError(
-                'Expected integer num_qudits'
-                ', got %s.' % type(num_qubits),
-            )
-
-        if num_qubits <= 0:
-            raise ValueError(
-                'Expected positive num_qudits'
-                ', got %d.' % num_qubits,
-            )
-
-        if not CircuitLocation.is_location(location, num_qubits):
-            raise TypeError('Invalid location.')
-
-        matrix = calc_permutation_matrix(num_qubits, location)
-        return PermutationMatrix(matrix)
+        current_perm = list(location)
+        for i in range(num_qubits):
+            if i not in current_perm:
+                current_perm.append(i)
+
+        perm_builder = UnitaryBuilder(num_qubits)
+        for index, qudit in enumerate(current_perm):
+            if index != qudit:
+                current_pos = current_perm.index(index)
+                perm_builder.apply_left(
+                    PermutationMatrix.swap_utry, (index, current_pos),
+                )
+                tmp = current_perm[index]
+                current_perm[index] = current_perm[current_pos]
+                current_perm[current_pos] = tmp
+        return PermutationMatrix(perm_builder.get_unitary())
 
     @staticmethod
     def is_permutation(P: np.typing.ArrayLike, tol: float = 1e-8) -> bool:
diff --git a/bqskit/qis/state/state.py b/bqskit/qis/state/state.py
index beb20597a..221e26a79 100644
--- a/bqskit/qis/state/state.py
+++ b/bqskit/qis/state/state.py
@@ -5,6 +5,7 @@
 from typing import Any
 from typing import Iterator
 from typing import Sequence
+from typing import TYPE_CHECKING
 from typing import Union
 
 import numpy as np
@@ -17,6 +18,10 @@
 from bqskit.utils.typing import is_vector
 
 
+if TYPE_CHECKING:
+    from typing import TypeGuard
+
+
 _logger = logging.getLogger(__name__)
 
 
@@ -162,12 +167,12 @@ def is_qudit_only(self, radix: int) -> bool:
         return all([r == radix for r in self.radixes])
 
     @staticmethod
-    def is_pure_state(V: np.typing.ArrayLike, tol: float = 1e-8) -> bool:
+    def is_pure_state(V: Any, tol: float = 1e-8) -> TypeGuard[StateLike]:
         """
         Check if V is a pure state vector.
 
         Args:
-            V (np.typing.ArrayLike): The vector to check.
+            V (Any): The vector to check.
 
             tol (float): The numerical precision of the check.
 
diff --git a/bqskit/qis/unitary/unitarymatrix.py b/bqskit/qis/unitary/unitarymatrix.py
index 8e40b8cc0..e77e1c6bb 100644
--- a/bqskit/qis/unitary/unitarymatrix.py
+++ b/bqskit/qis/unitary/unitarymatrix.py
@@ -4,6 +4,7 @@
 import logging
 from typing import Any
 from typing import Sequence
+from typing import TYPE_CHECKING
 from typing import Union
 
 import numpy as np
@@ -21,6 +22,9 @@
 from bqskit.utils.typing import is_square_matrix
 from bqskit.utils.typing import is_valid_radixes
 
+if TYPE_CHECKING:
+    from typing import TypeGuard
+
 if not building_docs():
     from numpy.lib.mixins import NDArrayOperatorsMixin
 else:
@@ -363,12 +367,12 @@ def from_file(filename: str) -> UnitaryMatrix:
         return UnitaryMatrix(np.loadtxt(filename, dtype=np.complex128))
 
     @staticmethod
-    def is_unitary(U: np.typing.ArrayLike, tol: float = 1e-8) -> bool:
+    def is_unitary(U: Any, tol: float = 1e-8) -> TypeGuard[UnitaryLike]:
         """
         Check if U is a unitary matrix.
 
         Args:
-            U (np.typing.ArrayLike): The matrix to check.
+            U (Any): The matrix to check.
 
             tol (float): The numerical precision of the check.
 
diff --git a/bqskit/utils/cachedclass.py b/bqskit/utils/cachedclass.py
index b4c0e4b56..751a361b6 100644
--- a/bqskit/utils/cachedclass.py
+++ b/bqskit/utils/cachedclass.py
@@ -6,9 +6,18 @@
 from typing import Hashable
 from typing import TypeVar
 
+from bqskit.utils.docs import building_docs
+
+if not building_docs():
+    from numpy.lib.mixins import NDArrayOperatorsMixin
+else:
+    class NDArrayOperatorsMixin:  # type: ignore
+        pass
+
 
 _logger = logging.getLogger(__name__)
 
+
 T = TypeVar('T')
 
 
@@ -33,11 +42,21 @@ class CachedClass:
     _instances: dict[Any, CachedClass] = {}
 
     def __new__(cls: type[T], *args: Any, **kwargs: Any) -> T:
-        hash_a = all(isinstance(arg, Hashable) for arg in args)
-        hash_kw = all(isinstance(arg, Hashable) for arg in kwargs.values())
+        hash_a = all(
+            isinstance(arg, Hashable)
+            and not isinstance(arg, NDArrayOperatorsMixin)
+            for arg in args
+        )
+        hash_kw = all(
+            isinstance(arg, Hashable)
+            and not isinstance(arg, NDArrayOperatorsMixin)
+            for arg in kwargs.values()
+        )
 
         if not hash_a or not hash_kw:
-            return object.__new__(cls)
+            obj = object.__new__(cls)
+            obj.__cache_key__ = (cls, args, kwargs)  # type: ignore
+            return obj
 
         key = (cls, args, tuple(kwargs.items()))
 
@@ -52,6 +71,7 @@ def __new__(cls: type[T], *args: Any, **kwargs: Any) -> T:
                 % (cls.__name__, args, kwargs),
             )
             _instances[key] = object.__new__(cls)
+            _instances[key].__cache_key__ = (cls, args, kwargs)
 
         return _instances[key]
 
@@ -60,3 +80,6 @@ def __copy__(self) -> CachedClass:
 
     def __deepcopy__(self, memo: Any) -> CachedClass:
         return self.__copy__()
+
+    def __getnewargs_ex__(self) -> tuple[Any, Any]:
+        return self.__cache_key__[1], self.__cache_key__[2]  # type: ignore
diff --git a/bqskit/utils/typing.py b/bqskit/utils/typing.py
index 3cbdbcd4b..4e2f935ce 100644
--- a/bqskit/utils/typing.py
+++ b/bqskit/utils/typing.py
@@ -62,7 +62,7 @@ def is_real_number(x: Any) -> TypeGuard[float]:
 
 def is_integer(x: Any) -> TypeGuard[int]:
     """Return true if x is an integer."""
-    return isinstance(x, numbers.Integral) and not isinstance(x, bool)
+    return isinstance(x, (int, np.int_))
 
 
 def is_bool(x: Any) -> TypeGuard[bool]:
diff --git a/docs/index.rst b/docs/index.rst
index 75ecf527f..8ca903aa5 100644
--- a/docs/index.rst
+++ b/docs/index.rst
@@ -1,12 +1,16 @@
 Welcome to BQSKit's documentation!
 ==================================
 
-BQSKit is a superoptimizing quantum compiler that aims to provide an
-easy-to-use and quick-to-extend software package around quantum synthesis.
-This is accomplished by first building a quantum circuit intermediate
-representation designed to work efficiently with numerical optimizer
-based synthesis algorithms, and second bundling a compiler infrastructure
-and algorithm framework that can run many algorithms efficiently.
+The Berkeley Quantum Synthesis Toolkit (BQSKit) \[bis • kit\] is a
+powerful and portable quantum compiler framework. It can be used with
+ease to compile quantum programs to efficient physical circuits for any
+QPU. BQSKit achieves its superior portability by utilizing a novel
+quantum circuit intermediate representation designed to facilitate
+numerical instantiation. On top of this IR, we built an algorithm
+framework and compiler infrastructure to efficiently accomplish any
+quantum compilation goal. You will find some guides and API documentation
+below, and you can learn how to use BQSKit to solve your problems with
+our `tutorial series. <https://github.com/BQSKit/bqskit-tutorial/>`_
 
 
 .. toctree::
@@ -14,19 +18,9 @@ and algorithm framework that can run many algorithms efficiently.
    :maxdepth: 1
 
    intro/start
-   intro/synthesis
    intro/CONTRIBUTING
    intro/license
 
-.. toctree::
-   :caption: BQSKit Tutorials
-   :maxdepth: 1
-
-   tutorials/Introduction to BQSKit IR.ipynb
-   tutorials/Search Synthesis.ipynb
-   tutorials/Searchless Synthesis.ipynb
-   tutorials/Partitioning and Optimization.ipynb
-
 .. toctree::
    :caption: API Reference
    :maxdepth: 1
diff --git a/docs/intro/CONTRIBUTING.md b/docs/intro/CONTRIBUTING.md
deleted file mode 120000
index f939e75f2..000000000
--- a/docs/intro/CONTRIBUTING.md
+++ /dev/null
@@ -1,1 +0,0 @@
-../../CONTRIBUTING.md
\ No newline at end of file
diff --git a/docs/intro/CONTRIBUTING.md b/docs/intro/CONTRIBUTING.md
new file mode 100644
index 000000000..d0e68be9b
--- /dev/null
+++ b/docs/intro/CONTRIBUTING.md
@@ -0,0 +1,86 @@
+# Contributing to BQSKit
+
+The Berkeley Quantum Synthesis Toolkit is an open-source superoptimizing
+compiler committed to making synthesis easy to use and quick to extend.
+As such, it is one our main goals to allow everyone to join our community
+and contribute to the BQSKit project. This page describes how to do just
+that.
+
+## Getting Started
+
+You will probably want to familiarize yourself with the codebase and
+documentation, and set up a development environment before making any
+contribution. We will welcome all contributions, but we envision there
+will be two common types of contributions to BQSKit: ones extending the
+IR with new gates, and ones implementing algorithms in a compiler pass.
+If you plan to make a similar contribution, you can find documentation
+for the IR and supported algorithms under the API Reference section.
+
+### Development Environment
+
+You will want to install BQSKit from source, by cloning the repository from github:
+
+```
+git clone https://github.com/BQSKit/BQSKit.git
+cd bqskit
+pip install -e '.[dev]'
+```
+
+You can run `tox` to install all development packages, setup virtual
+environments for all supported versions of python, perform all stylistic
+checks and modifications, and run the test suite.
+
+## Guidelines
+
+Please follow the below short list of guidelines when contributing.
+
+### Pull Request Checklist
+
+1. Please ensure pre-commit was run successfully on your branch. This will
+ensure the code style of the project is matched in addition to other checks.
+This can be done, by executing `tox` locally before pushing. Note that `tox`
+or `pre-commit` will make stylistic modifications to your code.
+
+2. Please ensure that all tests are still passing, which can also be done
+with `tox`. Also, if appropriate, please add tests to ensure your change
+behaves correctly. See the testing section below for more information.
+
+3. Please ensure that any added package, module, class, attribute, function,
+or method has an appropriate google-style docstring. These are used by the
+documentation engine to produce api references. Additionally, if you have
+created a user-facing class, please add those to the autosummary list in
+top level package's `__init__.py`, e.g. `bqskit.ir.__init__`.
+
+4. BQSKit is a type-annotated python package, which helps catch some bugs
+early with static code analysis tools like [Mypy](http://mypy-lang.org/).
+You can see [PEP 484: Type Annotations](https://www.python.org/dev/peps/pep-0484/)
+for more information. Please do annotate your contribution with types.
+Sometimes this can be tricky, if you need help, please don't hesitate to ask.
+
+
+## Testing
+
+After any changes, it is important to ensure that all the previous tests
+still pass on all supported versions of Python. This can be done by running
+the `tox` command after install it. Additionally, you will want to write
+tests for any appropriate changes. Our test suite resides in the `tests`
+folder and uses a combination of `pytest` and `hypothesis`.
+
+- [PyTest](https://docs.pytest.org/en/6.2.x/)
+- [Hypothesis](https://hypothesis.readthedocs.io/en/latest/)
+
+Pytest is a framework for writing and running tests. Any python method or
+function that starts with `test_` that resides in the `tests` folder will
+be collected and run as part of the test suite. You can write normal python
+code and use assert statements in your tests. Although you can place your
+test anywhere in the `tests` folder, please do follow the same structure that is
+there already. The `tests` directory structure closely follows the `bqskit`
+package structure, which makes it easy to find tests. If you are not familiar
+with Pytest, we recommend you read a few of the tests included already and
+ask any questions you may have.
+
+Hypothesis is a powerful library that will intelligently generate inputs
+to tests. Any test that starts with a `given` decorator is using Hypothesis
+to generate inputs according to some strategy. BQSKit has several custom
+strategies that can be found in `bqskit.utils.test` module. We recommend
+using `hypothesis` to test complex functionality that may have corner cases.
diff --git a/docs/intro/start.md b/docs/intro/start.md
index 74b2b2f38..dc150f276 100644
--- a/docs/intro/start.md
+++ b/docs/intro/start.md
@@ -1,108 +1,48 @@
 # Getting Started
 
-BQSKit allows you to use high-quality synthesis algorithms with ease to
-compile quantum circuits. It is designed to be highly customizable to find
-a compiler-flow that suites your needs well. In this guide, we will cover
-installation and some basic workflows, but there is no one-size-fits-all
-when it comes to synthesis. We recommend you explore the package
-once you get more comfortable with the basics.
+The Berkeley Quantum Synthesis Toolkit (BQSKit) \[bis • kit\] is a powerful and portable quantum compiler framework.
+It can be used with ease to compile quantum programs to efficient physical circuits for any QPU.
 
 ## Installation
 
-BQSKit is available for Python 3.7+ on Linux, MacOS, and Windows. BQSKit
+BQSKit is available for Python 3.8+ on Linux, macOS, and Windows. BQSKit
 and its dependencies are listed on the [Python Package Index](https://pypi.org),
-and as such, can be installed with pip:
+and as such, pip can easily install it:
 
 ```sh
 pip install bqskit
 ```
 
-## Basic Usage
-
-BQSKit can be used as a quantum compiler and so much more. Below we have
-examples for the common use cases in BQSKit.
-
-### Circuit Optimization Example
-
-The most obvious use for an optimizing quantum compiler is to optimize
-a quantum program. Here we can optimize a program given as a circuit in
-qasm:
+An optional extension includes pre-built support for many quantum devices and modules for working with other quantum frameworks. Pip can install this extension by appending `[ext]` to the previous command:
 
-```python
-from bqskit.compiler import Compiler
-from bqskit.compiler import CompilationTask
-from bqskit.ir import Circuit
-
-# Load a circuit from QASM
-circuit = Circuit.from_file("tfim.qasm")
+```sh
+pip install 'bqskit[ext]'
+```
 
-# Create a standard CompilationTask to optimize the circuit
-task = CompilationTask.optimize(circuit)
+**Note**: If you are using a device with Apple Silicon, you will need to install BQSKit with Anaconda. See the [instructions here](https://github.com/BQSKit/bqskit-tutorial/blob/master/README.md) for more information. We are currently in the process of implementing native ARM support. When that is ready, we will update this note.
 
-# Spawn a compiler and compile the task
-with Compiler() as compiler:
-    optimized_circuit = compiler.compile(task)
-```
 
-We first load the qasm program into a `Circuit` object. Then we create
-a `CompilationTask` object, which encapsulates an entire compilation flow.
-This includes the circuit to be compiled, as well as, the configured
-algorithms that are desired to be used to compile the program.
+## Basic Usage
 
-We provide some default constructors for the common cases, such as
-`CompilationTask.optimize` and the ones you will see in the following
-sections. These these default configurations aim to be widely applicable,
-and as a result, may be far from the best. The best compilation
-flow for your use case will likely require some experimentation. Once you are
-familiar with the basics, we recommend you explore how these defaults are
-built and toy with them.
+A standard workflow utilizing BQSKit consists of loading a program into the framework, modeling the target QPU, compiling the program, and exporting the resulting circuit. The below example uses BQSKit to optimize an input circuit provided by a qasm file:
 
-The last block of the program spawns a `Compiler` object which is responsible
-for executing the `CompilationTask`. This will handle the execution efficiently.
+```python
+from bqskit import compile, Circuit
 
-### Unitary Synthesis Example
+# Load a circuit from QASM
+circuit = Circuit.from_file("input.qasm")
 
-A compiler built around the concept of synthesis should definitely
-support synthesis. In fact, the BQSKit compiler does support a variety
-of ways to perform synthesis. The following example uses the default
-flow for synthesizing a toffoli unitary:
+# Compile the circuit
+compiled_circuit = compile(circuit)
 
-```python
-import numpy as np
-from bqskit.compiler import Compiler
-from bqskit.compiler import CompilationTask
-
-# Construct the unitary as an NumPy array
-toffoli = np.array([
-    [1, 0, 0, 0, 0, 0, 0, 0],
-    [0, 1, 0, 0, 0, 0, 0, 0],
-    [0, 0, 1, 0, 0, 0, 0, 0],
-    [0, 0, 0, 1, 0, 0, 0, 0],
-    [0, 0, 0, 0, 1, 0, 0, 0],
-    [0, 0, 0, 0, 0, 1, 0, 0],
-    [0, 0, 0, 0, 0, 0, 0, 1],
-    [0, 0, 0, 0, 0, 0, 1, 0],
-])
-
-# Create a standard synthesis CompilationTask
-task = CompilationTask.synthesize(toffoli)
-
-# Spawn a compiler and compile the task
-with Compiler() as compiler:
-    synthesized_circuit = compiler.compile(task)
+# Save output as QASM
+compiled_circuit.save("output.qasm")
 ```
 
-## Verifying Results
+To learn more about BQSKit, follow the [tutorial series](https://github.com/BQSKit/bqskit-tutorial/blob/master/1_comping_with_bqskit.ipynb) or refer to the [documentation](https://bqskit.readthedocs.io/en/latest/).
 
-All of BQSKit's synthesis algorithms are approximate, which mean the
-compiled results might be slightly different than what was inputted.
-For most cases, these differences are close to floating point thresholds,
-but can be greater in magnitude. Most algorithms will offer some sort
-of control over this error, but it is important to be able to measure it.
+## How to Cite
 
-For simulatable circuits, we can compare the unitaries directly:
+You can use the [software disclosure](https://www.osti.gov/biblio/1785933) to cite the BQSKit package.
 
-```python
-dist = synthesized_circuit.get_unitary().get_distance_from(toffoli)
-assert dist < 1e-10
-```
+Additionally, if you used or extended a specific algorithm, you can cite that individually. BQSKit passes will include a relevant reference in their documentation.
diff --git a/docs/tutorials b/docs/tutorials
deleted file mode 120000
index e70182b81..000000000
--- a/docs/tutorials
+++ /dev/null
@@ -1,1 +0,0 @@
-../examples/tutorials
\ No newline at end of file
diff --git a/examples/compile.py b/examples/compile.py
new file mode 100644
index 000000000..761c11d5b
--- /dev/null
+++ b/examples/compile.py
@@ -0,0 +1,18 @@
+"""
+This example demonstrates using the standard BQSKit compile function.
+
+For a much more detailed tutorial, see the bqskit-tutorial repository.
+"""
+from __future__ import annotations
+
+from bqskit import Circuit
+from bqskit import compile
+
+# Load a circuit from QASM
+circuit = Circuit.from_file('tfim.qasm')
+
+# Compile it with optimization level 2
+out_circuit = compile(circuit, optimization_level=2)
+
+# You can choose to save the output as qasm
+out_circuit.save('tfim_out.qasm')
diff --git a/examples/instantiate.py b/examples/instantiate.py
index 1a7595c19..f5a2947d7 100644
--- a/examples/instantiate.py
+++ b/examples/instantiate.py
@@ -1,16 +1,16 @@
-"""Optimize a 3-qubit circuit to be a toffoli gate."""
-from __future__ import annotations
+"""
+Numerical Instantiation is the foundation of many of BQSKit's algorithms.
 
-import logging
+This example demonstrates building a circuit template that can implement the
+toffoli gate and then instantiating it to be the gate.
+"""
+from __future__ import annotations
 
 import numpy as np
 
 from bqskit.ir.circuit import Circuit
 from bqskit.ir.gates import VariableUnitaryGate
-from bqskit.ir.opt.cost import HilbertSchmidtCostGenerator
 from bqskit.qis.unitary import UnitaryMatrix
-# The next two lines start bqskits's logger.
-logging.getLogger('bqskit').setLevel(logging.INFO)
 
 # We will optimize towards the toffoli unitary.
 toffoli = np.array([
@@ -47,9 +47,9 @@
 )
 
 # Calculate and print final distance
-dist = HilbertSchmidtCostGenerator().calc_cost(circuit, toffoli)
+dist = circuit.get_unitary().get_distance_from(toffoli, 1)
 print('Final Distance: ', dist)
 
-# If you would like to convert the unitary operations to native gates,
-# you should use the KAK decomposition for 2 qubit unitaries, or
-# qsearch or qfast for 3+ qubit unitaries.
+# You can use synthesis to convert the `VariableUnitaryGate`s to
+# native gates. Alternatively, you can build a circuit directly out of
+# native gates and use the default instantiater to instantiate directly.
diff --git a/examples/optimize.py b/examples/optimize.py
deleted file mode 100644
index d79bafb53..000000000
--- a/examples/optimize.py
+++ /dev/null
@@ -1,17 +0,0 @@
-from __future__ import annotations
-
-from bqskit.compiler import CompilationTask
-from bqskit.compiler import Compiler
-from bqskit.ir import Circuit
-
-# Load a circuit from QASM
-circuit = Circuit.from_file('tfim.qasm')
-
-# Create a standard CompilationTask to optimize the circuit
-task = CompilationTask.optimize(circuit)
-
-# Spawn a compiler and compile the task
-if __name__ == '__main__':
-    with Compiler() as compiler:
-        optimized_circuit = compiler.compile(task)
-        print(optimized_circuit)
diff --git a/examples/qfast.py b/examples/qfast.py
index 27dfb17ef..8124c4bf7 100644
--- a/examples/qfast.py
+++ b/examples/qfast.py
@@ -11,21 +11,20 @@
 from bqskit.passes import UnfoldPass
 from bqskit.qis import UnitaryMatrix
 
-if __name__ == '__main__':
-    # Let's create a random 3-qubit unitary to synthesize and add it to a
-    # circuit.
-    circuit = Circuit.from_unitary(UnitaryMatrix.random(3))
+# Let's create a random 3-qubit unitary to synthesize and add it to a
+# circuit.
+circuit = Circuit.from_unitary(UnitaryMatrix.random(3))
 
-    # We will now define the CompilationTask we want to run.
-    task = CompilationTask(
-        circuit, [
-            QFASTDecompositionPass(),
-            ForEachBlockPass([LEAPSynthesisPass(), ScanningGateRemovalPass()]),
-            UnfoldPass(),
-        ],
-    )
+# We will now define the CompilationTask we want to run.
+task = CompilationTask(
+    circuit, [
+        QFASTDecompositionPass(),
+        ForEachBlockPass([LEAPSynthesisPass(), ScanningGateRemovalPass()]),
+        UnfoldPass(),
+    ],
+)
 
-    # Finally let's create create the compiler and execute the CompilationTask.
-    with Compiler() as compiler:
-        compiled_circuit = compiler.compile(task)
-        print(compiled_circuit)
+# Finally let's create create the compiler and execute the CompilationTask.
+with Compiler() as compiler:
+    compiled_circuit = compiler.compile(task)
+    print(compiled_circuit.gate_counts)
diff --git a/examples/synthesis.py b/examples/synthesis.py
index ee1094c39..f90d790b0 100644
--- a/examples/synthesis.py
+++ b/examples/synthesis.py
@@ -1,12 +1,10 @@
+"""This example shows how to synthesize a circuit with BQSKit."""
 from __future__ import annotations
 
-import numpy as np
-
-from bqskit.compiler import CompilationTask
-from bqskit.compiler import Compiler
+from bqskit import compile
 
 # Construct the unitary as an NumPy array
-toffoli = np.array([
+toffoli = [
     [1, 0, 0, 0, 0, 0, 0, 0],
     [0, 1, 0, 0, 0, 0, 0, 0],
     [0, 0, 1, 0, 0, 0, 0, 0],
@@ -15,13 +13,8 @@
     [0, 0, 0, 0, 0, 1, 0, 0],
     [0, 0, 0, 0, 0, 0, 0, 1],
     [0, 0, 0, 0, 0, 0, 1, 0],
-])
-
-# Create a standard synthesis CompilationTask
-task = CompilationTask.synthesize(toffoli)
+]
 
-# Spawn a compiler and compile the task
-if __name__ == '__main__':
-    with Compiler() as compiler:
-        synthesized_circuit = compiler.compile(task)
-        print(synthesized_circuit)
+# The compile function will perform synthesis
+synthesized_circuit = compile(toffoli, max_synthesis_size=3)
+print(synthesized_circuit.gate_counts)
diff --git a/setup.cfg b/setup.cfg
index 08114b8b2..57deb0c91 100644
--- a/setup.cfg
+++ b/setup.cfg
@@ -1,6 +1,6 @@
 [metadata]
 name = bqskit
-version = 0.5.2
+version = 1.0.0b2
 description = Berkeley Quantum Synthesis Toolkit
 long_description = file: README.md
 long_description_content_type = text/markdown
@@ -9,14 +9,13 @@ author = LBNL - BQSKit developers
 author_email = edyounis@lbl.gov
 license_file = LICENSE
 classifiers =
-    Development Status :: 2 - Pre-Alpha
+    Development Status :: 4 - Beta
     Environment :: Console
     Intended Audience :: Developers
     Intended Audience :: Education
     Intended Audience :: Science/Research
     Operating System :: OS Independent
     Programming Language :: Python :: 3 :: Only
-    Programming Language :: Python :: 3.7
     Programming Language :: Python :: 3.8
     Programming Language :: Python :: 3.9
     Programming Language :: Python :: 3.10
@@ -33,20 +32,34 @@ project_urls =
 [options]
 packages = find:
 install_requires =
-    bqskitrs>=0.2.0
-    dask[distributed]
+    bqskitrs>=0.2.3
+    dask[distributed]>=2022.07.01
     lark-parser
-    numpy>=1.16.6
-    scipy>=1.4.1
+    numpy>=1.22.0
+    scipy>=1.8.0
     threadpoolctl
     typing-extensions>=4.0.0
-python_requires = >=3.7, <4
+python_requires = >=3.8, <4
 
 [options.packages.find]
 exclude =
     tests
     examples
 
+[options.extras_require]
+dev =
+    hypothesis
+    mypy
+    pre-commit
+    pytest
+    pytket-qiskit
+    tox
+ext =
+    cirq>=1.0.0
+    pytket>=1.5.0
+    qiskit>=0.36.2
+    qutip>=4.7.0
+
 [bdist_wheel]
 universal = 1
 
@@ -70,16 +83,21 @@ plugins = numpy.typing.mypy_plugin
 
 [tox:tox]
 minversion = 3.3.0
-envlist = pre-commit,py37,py38,py39,py310
+envlist = pre-commit,py38,py39,py310
 skip_missing_interpreters = true
 
 [testenv]
 passenv =
     TERM
 deps =
-    numpy>=1.20
+    numpy>=1.22
     pytest>=2.8
     hypothesis[numpy,zoneinfo]>=6.14.5
+    qiskit>=0.37.1
+    pytket>=1.5.0
+    cirq>=1.0.0
+    qutip>=4.7.0
+    pytket-qiskit
 commands =
     pytest {posargs}
 


Test patch:
diff --git a/bqskit/utils/test/strategies.py b/bqskit/utils/test/strategies.py
index 327ab174a..e71db8f36 100644
--- a/bqskit/utils/test/strategies.py
+++ b/bqskit/utils/test/strategies.py
@@ -50,7 +50,7 @@
 def num_qudits(
     draw: Any,
     max_num_qudits: int = 8,
-    min_num_qudits: int = 1,
+    min_num_qudits: int = 1,  # TODO: Param order should be swapped
 ) -> int:
     """Hypothesis strategy for generating a valid number of qudits."""
     return draw(integers(min_num_qudits, max_num_qudits))
diff --git a/tests/compiler/compile/conftest.py b/tests/compiler/compile/conftest.py
new file mode 100644
index 000000000..b553c0ed9
--- /dev/null
+++ b/tests/compiler/compile/conftest.py
@@ -0,0 +1,11 @@
+from __future__ import annotations
+
+from typing import Any
+
+import pytest
+
+
+@pytest.fixture(params=[1, 2, 3])
+def optimization_level(request: Any) -> int:
+    """All valid optimization_levels for the `compile` function."""
+    return request.param
diff --git a/tests/compiler/compile/test_synthesis.py b/tests/compiler/compile/test_synthesis.py
new file mode 100644
index 000000000..7d0c0179e
--- /dev/null
+++ b/tests/compiler/compile/test_synthesis.py
@@ -0,0 +1,112 @@
+from __future__ import annotations
+
+import pytest
+
+from bqskit import compile
+from bqskit import MachineModel
+from bqskit.compiler.compiler import Compiler
+from bqskit.compiler.machine import default_gate_set
+from bqskit.ext.cirq.models import google_gate_set
+from bqskit.ext.honeywell import honeywell_gate_set
+from bqskit.ext.rigetti import rigetti_gate_set
+from bqskit.ir.gate import Gate
+from bqskit.ir.gates import CNOTGate
+from bqskit.ir.gates import IToffoliGate
+from bqskit.ir.gates import PhasedXZGate
+from bqskit.ir.gates import ToffoliGate
+from bqskit.ir.gates import U1qGate
+from bqskit.ir.gates import U3Gate
+from bqskit.ir.gates import XGate
+from bqskit.qis import UnitaryMatrix
+
+
+@pytest.mark.parametrize('sq_utry', [UnitaryMatrix.random(1) for i in range(5)])
+@pytest.mark.parametrize(
+    'gate_set', [
+        {U3Gate()},
+        {PhasedXZGate()},
+        {U1qGate(), XGate()},
+        default_gate_set,
+        rigetti_gate_set,
+        honeywell_gate_set,
+        google_gate_set,
+        {IToffoliGate(), U3Gate()},
+        {IToffoliGate(), CNOTGate(), U3Gate()},
+    ],
+)
+def test_single_qudit_synthesis(
+    sq_utry: UnitaryMatrix,
+    optimization_level: int,
+    gate_set: set[Gate],
+    compiler: Compiler,
+) -> None:
+    out_circuit = compile(
+        sq_utry,
+        model=MachineModel(1, gate_set=gate_set),
+        optimization_level=optimization_level,
+        compiler=compiler,
+    )
+    if U3Gate() in gate_set:
+        assert out_circuit.num_operations == 1
+    assert out_circuit.num_qudits == 1
+    assert len(out_circuit.gate_set.difference(gate_set)) == 0
+    assert out_circuit.get_unitary().get_distance_from(sq_utry) < 5e-6
+
+
+@pytest.mark.parametrize('tq_utry', [UnitaryMatrix.random(2) for i in range(5)])
+@pytest.mark.parametrize(
+    'gate_set', [
+        default_gate_set,
+        rigetti_gate_set,
+        honeywell_gate_set,
+        google_gate_set,
+        {IToffoliGate(), CNOTGate(), U3Gate()},
+    ],
+)
+def test_two_qudit_synthesis(
+    tq_utry: UnitaryMatrix,
+    optimization_level: int,
+    gate_set: set[Gate],
+    compiler: Compiler,
+) -> None:
+    out_circuit = compile(
+        tq_utry,
+        model=MachineModel(2, gate_set=gate_set),
+        optimization_level=optimization_level,
+        compiler=compiler,
+    )
+    assert out_circuit.num_qudits == 2
+    assert len(out_circuit.gate_set.difference(gate_set)) == 0
+    assert out_circuit.get_unitary().get_distance_from(tq_utry) < 5e-6
+
+
+@pytest.mark.parametrize(
+    'gate_set', [
+        default_gate_set,
+        rigetti_gate_set,
+        honeywell_gate_set,
+        {IToffoliGate(), U3Gate()},
+        {ToffoliGate(), U3Gate()},
+    ],
+)
+def test_three_qudit_synthesis(
+    toffoli_unitary: UnitaryMatrix,
+    optimization_level: int,
+    gate_set: set[Gate],
+    compiler: Compiler,
+) -> None:
+    out_circuit = compile(
+        toffoli_unitary,
+        model=MachineModel(3, gate_set=gate_set),
+        optimization_level=optimization_level,
+        compiler=compiler,
+    )
+    assert out_circuit.num_qudits == 3
+    assert len(out_circuit.gate_set.difference(gate_set)) == 0
+    assert out_circuit.get_unitary().get_distance_from(toffoli_unitary) < 5e-6
+
+
+def test_fail_on_larger_max_synthesis_size(compiler: Compiler) -> None:
+    utry = UnitaryMatrix.random(4)
+    with pytest.raises(ValueError):
+        compile(utry, max_synthesis_size=3, compiler=compiler)
diff --git a/tests/compiler/test_compiler.py b/tests/compiler/test_compiler.py
new file mode 100644
index 000000000..da0220fa8
--- /dev/null
+++ b/tests/compiler/test_compiler.py
@@ -0,0 +1,38 @@
+"""This module tests BQSKit's Compiler object."""
+from __future__ import annotations
+
+import logging
+from io import StringIO
+from typing import Any
+from unittest.mock import patch
+
+import pytest
+
+from bqskit.compiler.basepass import BasePass
+from bqskit.compiler.compiler import Compiler
+from bqskit.compiler.task import CompilationTask
+from bqskit.ir.circuit import Circuit
+
+
+def test_errors_raised_locally(compiler: Compiler) -> None:
+    class ErrorPass(BasePass):
+        def run(self, circuit: Circuit, data: dict[str, Any] = {}) -> None:
+            raise RuntimeError()
+
+    task = CompilationTask(Circuit(1), [ErrorPass()])
+
+    with pytest.raises(RuntimeError):
+        compiler.compile(task)
+
+
+def test_log_msg_printed_locally(compiler: Compiler) -> None:
+    class LogPass(BasePass):
+        def run(self, circuit: Circuit, data: dict[str, Any] = {}) -> None:
+            logging.getLogger('bqskit.dummy').debug('Test.')
+
+    task = CompilationTask(Circuit(1), [LogPass()])
+    logging.getLogger('bqskit').setLevel(logging.DEBUG)
+    with patch('sys.stdout', new_callable=StringIO) as mock_out:
+        compiler.compile(task)
+        print(mock_out.getvalue())
+    logging.getLogger('bqskit').setLevel(logging.WARNING)
diff --git a/tests/conftest.py b/tests/conftest.py
index 96d4233e7..812bb6a48 100644
--- a/tests/conftest.py
+++ b/tests/conftest.py
@@ -9,6 +9,7 @@
 import os
 from typing import Any
 from typing import Callable
+from typing import Iterator
 from typing import Sequence
 
 import numpy as np
@@ -18,6 +19,7 @@
 from hypothesis import settings
 from scipy.stats import unitary_group
 
+from bqskit.compiler.compiler import Compiler
 from bqskit.ir.circuit import Circuit
 from bqskit.ir.gate import Gate
 from bqskit.ir.gates import CNOTGate
@@ -80,322 +82,6 @@
     else 10,
 )
 
-# endregion
-
-# region Types
-
-type_dict = {
-    'str_1': '',
-    'str_2': 'a',
-    'str_3': 'abc',
-    'str_4': "abc@!@$$&%^()!@*^<>[]mM,./_+-=\'\"",
-    'int_1': -1,
-    'int_2': 0,
-    'int_3': 1,
-    'int_4': -127649217,
-    'int_5': 212121212,
-    'int_6': np.byte(1234),
-    'int_7': np.short(1234),
-    'int_8': np.intc(1234),
-    'int_9': np.longlong(1234),
-    'int_10': np.int8(1234),
-    'int_11': np.int16(1234),
-    'int_12': np.int32(1234),
-    'int_13': np.int64(1234),
-    'float_1': 0.0,
-    'float_2': 1.0,
-    'float_3': 1e-15,
-    'float_4': 1.26738e14,
-    'float_5': 1.23,
-    'float_6': np.half(1234.0),
-    'float_7': np.single(1234.0),
-    'float_8': np.double(1234.0),
-    'float_9': np.longdouble(1234.0),
-    'float_10': np.float32(1234.0),
-    'float_11': np.float64(1234.0),
-    'complex_1': complex(0.0j),
-    'complex_2': complex(0.0 + 0.0j),
-    'complex_3': 1.0j,
-    'complex_4': 1.0 + 1.0j,
-    'complex_5': 1.0 - 1.0j,
-    'complex_7': np.csingle(1234 + 1234j),
-    'complex_8': np.cdouble(1234 + 1234j),
-    'complex_9': np.clongdouble(1234 + 1234j),
-    'complex_10': np.complex64(1234 + 1234j),
-    'complex_11': np.complex128(1234 + 1234j),
-    'bool_1': False,
-    'bool_2': True,
-    'bool_3': np.bool_(False),
-    'bool_4': np.bool_(True),
-    'seq-str_1': [],
-    'seq-str_2': ['0'],
-    'seq-str_3': ['0, 1', 'abc', '@#!$^%&(#'],
-    'seq-str_4': ['A'] * 10,
-    'seq-int_1': [],
-    'seq-int_2': [0],
-    'seq-int_3': [0, 1, np.int8(1), np.int16(1), np.int32(1), np.int64(1)],
-    'seq-int_4': [3] * 10,
-    'seq-float_1': [],
-    'seq-float_2': [0.0],
-    'seq-float_3': [0.0, 1.23, 1e-14, np.float32(1.0), np.float64(1.0)],
-    'seq-float_4': [1.234e12] * 10,
-    'seq-complex_1': [],
-    'seq-complex_2': [0.0j],
-
-    'seq-complex_3': [
-        0.0j,
-        1.23j,
-        1.1 + 1.0j,
-        np.complex64(1.0 + 1.0j),
-    ],
-    'seq-complex_4': [1.234e12j] * 10,
-    'seq-bool_1': [],
-    'seq-bool_2': [False],
-    'seq-bool_3': [True],
-    'seq-bool_4': [True, False, True, False, np.bool_(False), np.bool_(True)],
-    'seq-bool_5': [False] * 10,
-    'seq-bool_6': [True] * 10,
-}
-
-# str
-
-
-@pytest.fixture(
-    params=[(k, v) for k, v in type_dict.items() if k.split('_')[0] == 'str'],
-    ids=lambda tup: tup[0],
-)
-def a_str(request: Any) -> str:
-    """Provide random values that are strs."""
-    return request.param[1]
-
-
-@pytest.fixture(
-    params=[(k, v) for k, v in type_dict.items() if k.split('_')[0] != 'str'],
-    ids=lambda tup: tup[0],
-)
-def not_a_str(request: Any) -> Any:
-    """Provide random values that are not strs."""
-    return request.param[1]
-
-
-# int
-@pytest.fixture(
-    params=[(k, v) for k, v in type_dict.items() if k.split('_')[0] == 'int'],
-    ids=lambda tup: tup[0],
-)
-def an_int(request: Any) -> int:
-    """Provide random values that are ints."""
-    return request.param[1]
-
-
-@pytest.fixture(
-    params=[(k, v) for k, v in type_dict.items() if k.split('_')[0] != 'int'],
-    ids=lambda tup: tup[0],
-)
-def not_an_int(request: Any) -> Any:
-    """Provide random values that are not ints."""
-    return request.param[1]
-
-
-# float
-@pytest.fixture(
-    params=[
-        (k, v) for k, v in type_dict.items()
-        if k.split('_')[0] == 'float'
-    ],
-    ids=lambda tup: tup[0],
-)
-def a_float(request: Any) -> float:
-    """Provide random values that are floats."""
-    return request.param[1]
-
-
-@pytest.fixture(
-    params=[
-        (k, v) for k, v in type_dict.items()
-        if k.split('_')[0] != 'float'
-    ],
-    ids=lambda tup: tup[0],
-)
-def not_a_float(request: Any) -> Any:
-    """Provide random values that are not floats."""
-    return request.param[1]
-
-
-# complex
-@pytest.fixture(
-    params=[
-        (k, v) for k, v in type_dict.items()
-        if k.split('_')[0] == 'complex'
-    ],
-    ids=lambda tup: tup[0],
-)
-def a_complex(request: Any) -> complex:
-    """Provide random values that are complexs."""
-    return request.param[1]
-
-
-@pytest.fixture(
-    params=[
-        (k, v) for k, v in type_dict.items()
-        if k.split('_')[0] != 'complex'
-    ],
-    ids=lambda tup: tup[0],
-)
-def not_a_complex(request: Any) -> Any:
-    """Provide random values that are not complexs."""
-    return request.param[1]
-
-
-# bool
-@pytest.fixture(
-    params=[(k, v) for k, v in type_dict.items() if k.split('_')[0] == 'bool'],
-    ids=lambda tup: tup[0],
-)
-def a_bool(request: Any) -> bool:
-    """Provide random values that are bools."""
-    return request.param[1]
-
-
-@pytest.fixture(
-    params=[(k, v) for k, v in type_dict.items() if k.split('_')[0] != 'bool'],
-    ids=lambda tup: tup[0],
-)
-def not_a_bool(request: Any) -> Any:
-    """Provide random values that are not bools."""
-    return request.param[1]
-
-
-# seq-str
-@pytest.fixture(
-    params=[
-        (k, v) for k, v in type_dict.items()
-        if k.split('_')[0] == 'seq-str'
-    ],
-    ids=lambda tup: tup[0],
-)
-def a_seq_str(request: Any) -> Sequence[str]:
-    """Provide random values that are sequences of strs."""
-    return request.param[1]
-
-
-@pytest.fixture(
-    params=[
-        (k, v) for k, v in type_dict.items()
-        if k.split('_')[0] != 'seq-str'
-        and (not isinstance(v, list) or len(v) != 0)
-    ],
-    ids=lambda tup: tup[0],
-)
-def not_a_seq_str(request: Any) -> Any:
-    """Provide random values that are not sequences of strs."""
-    return request.param[1]
-
-
-# seq-int
-@pytest.fixture(
-    params=[
-        (k, v) for k, v in type_dict.items()
-        if k.split('_')[0] == 'seq-int'
-    ],
-    ids=lambda tup: tup[0],
-)
-def a_seq_int(request: Any) -> Sequence[int]:
-    """Provide random values that are sequences of ints."""
-    return request.param[1]
-
-
-@pytest.fixture(
-    params=[
-        (k, v) for k, v in type_dict.items()
-        if k.split('_')[0] != 'seq-int'
-        and (not isinstance(v, list) or len(v) != 0)
-    ],
-    ids=lambda tup: tup[0],
-)
-def not_a_seq_int(request: Any) -> Any:
-    """Provide random values that are not sequences of ints."""
-    return request.param[1]
-
-
-# seq-float
-@pytest.fixture(
-    params=[
-        (k, v) for k, v in type_dict.items()
-        if k.split('_')[0] == 'seq-float'
-    ],
-    ids=lambda tup: tup[0],
-)
-def a_seq_float(request: Any) -> Sequence[float]:
-    """Provide random values that are sequences of floats."""
-    return request.param[1]
-
-
-@pytest.fixture(
-    params=[
-        (k, v) for k, v in type_dict.items()
-        if k.split('_')[0] != 'seq-float'
-        and (not isinstance(v, list) or len(v) != 0)
-    ],
-    ids=lambda tup: tup[0],
-)
-def not_a_seq_float(request: Any) -> Any:
-    """Provide random values that are not sequences of floats."""
-    return request.param[1]
-
-
-# seq-complex
-@pytest.fixture(
-    params=[
-        (k, v) for k, v in type_dict.items()
-        if k.split('_')[0] == 'seq-complex'
-    ],
-    ids=lambda tup: tup[0],
-)
-def a_seq_complex(request: Any) -> Sequence[complex]:
-    """Provide random values that are sequences of complexs."""
-    return request.param[1]
-
-
-@pytest.fixture(
-    params=[
-        (k, v) for k, v in type_dict.items()
-        if k.split('_')[0] != 'seq-complex'
-        and (not isinstance(v, list) or len(v) != 0)
-    ],
-    ids=lambda tup: tup[0],
-)
-def not_a_seq_complex(request: Any) -> Any:
-    """Provide random values that are not sequences of complexs."""
-    return request.param[1]
-
-
-# seq-bool
-@pytest.fixture(
-    params=[
-        (k, v) for k, v in type_dict.items()
-        if k.split('_')[0] == 'seq-bool'
-    ],
-    ids=lambda tup: tup[0],
-)
-def a_seq_bool(request: Any) -> Sequence[bool]:
-    """Provide random values that are sequences of bools."""
-    return request.param[1]
-
-
-@pytest.fixture(
-    params=[
-        (k, v) for k, v in type_dict.items()
-        if k.split('_')[0] != 'seq-bool'
-        and (not isinstance(v, list) or len(v) != 0)
-    ],
-    ids=lambda tup: tup[0],
-)
-def not_a_seq_bool(request: Any) -> Any:
-    """Provide random values that are not sequences of bools."""
-    return request.param[1]
-
-
 # endregion
 
 # region Unitaries
@@ -447,6 +133,13 @@ def not_a_seq_bool(request: Any) -> Any:
 )
 
 
+@pytest.fixture(scope='session')
+def compiler() -> Iterator[Compiler]:
+    compiler = Compiler()
+    yield compiler
+    compiler.close()
+
+
 @pytest.fixture
 def toffoli_unitary() -> UnitaryMatrix:
     return UnitaryMatrix(TOFFOLI)
diff --git a/tests/ext/test_cirq.py b/tests/ext/test_cirq.py
new file mode 100644
index 000000000..ede832ce8
--- /dev/null
+++ b/tests/ext/test_cirq.py
@@ -0,0 +1,119 @@
+from __future__ import annotations
+
+import cirq
+import pytest
+from cirq import Circuit as QuantumCircuit
+
+from bqskit.compiler.compile import compile
+from bqskit.compiler.compiler import Compiler
+from bqskit.ext import bqskit_to_cirq
+from bqskit.ext import cirq_to_bqskit
+from bqskit.ir.circuit import Circuit
+from bqskit.ir.gates import CNOTGate
+from bqskit.ir.gates import HGate
+from bqskit.qis import UnitaryMatrix
+
+
+class TestTranslate:
+
+    @pytest.fixture
+    def bqskit_circuit(self) -> Circuit:
+        circuit = Circuit(3)
+        circuit.append_gate(CNOTGate(), (0, 1))
+        circuit.append_gate(HGate(), 0)
+        circuit.append_gate(HGate(), 1)
+        circuit.append_gate(HGate(), 2)
+        circuit.append_gate(CNOTGate(), (0, 1))
+        circuit.append_gate(CNOTGate(), (0, 2))
+        circuit.append_gate(CNOTGate(), (0, 2))
+        circuit.append_gate(HGate(), 0)
+        circuit.append_gate(HGate(), 1)
+        circuit.append_gate(HGate(), 2)
+        circuit.append_gate(HGate(), 2)
+        circuit.append_gate(CNOTGate(), (0, 2))
+        circuit.append_gate(CNOTGate(), (0, 2))
+        circuit.append_gate(CNOTGate(), (0, 1))
+        circuit.append_gate(CNOTGate(), (0, 2))
+        circuit.append_gate(HGate(), 0)
+        circuit.append_gate(HGate(), 1)
+        circuit.append_gate(HGate(), 2)
+        return circuit
+
+    @pytest.fixture
+    def cirq_circuit(self) -> QuantumCircuit:
+        q0, q1, q2 = cirq.LineQubit.range(3)
+        circuit = QuantumCircuit()
+        circuit.append([cirq.CNOT(q0, q1)])
+        circuit.append([cirq.H(q0)])
+        circuit.append([cirq.H(q1)])
+        circuit.append([cirq.H(q2)])
+        circuit.append([cirq.CNOT(q0, q1)])
+        circuit.append([cirq.CNOT(q0, q2)])
+        circuit.append([cirq.CNOT(q0, q2)])
+        circuit.append([cirq.H(q0)])
+        circuit.append([cirq.H(q1)])
+        circuit.append([cirq.H(q2)])
+        circuit.append([cirq.H(q2)])
+        circuit.append([cirq.CNOT(q0, q2)])
+        circuit.append([cirq.CNOT(q0, q2)])
+        circuit.append([cirq.CNOT(q0, q1)])
+        circuit.append([cirq.CNOT(q0, q2)])
+        circuit.append([cirq.H(q0)])
+        circuit.append([cirq.H(q1)])
+        circuit.append([cirq.H(q2)])
+        return circuit
+
+    def test_bqskit_to_bqskit(self, bqskit_circuit: Circuit) -> None:
+        in_utry = bqskit_circuit.get_unitary()
+        out_circuit = cirq_to_bqskit(bqskit_to_cirq(bqskit_circuit))
+        out_utry = out_circuit.get_unitary()
+        assert in_utry.get_distance_from(out_utry) < 1e-7
+
+    def test_cirq_to_cirq(self, cirq_circuit: QuantumCircuit) -> None:
+        qc = cirq_circuit
+        in_utry = UnitaryMatrix(cirq.unitary(qc))
+        out_circuit = bqskit_to_cirq(cirq_to_bqskit(qc))
+        out_utry = UnitaryMatrix(cirq.unitary(out_circuit))
+        assert in_utry.get_distance_from(out_utry) < 1e-7
+
+    def test_compile_bqskit(
+        self,
+        cirq_circuit: QuantumCircuit,
+        compiler: Compiler,
+    ) -> None:
+        qc = cirq_circuit
+        in_utry = UnitaryMatrix(cirq.unitary(qc))
+        bqskit_circuit = cirq_to_bqskit(qc)
+        bqskit_out_circuit = compile(
+            bqskit_circuit,
+            max_synthesis_size=2,
+            compiler=compiler,
+        )
+        out_circuit = bqskit_to_cirq(bqskit_out_circuit)
+        out_utry = UnitaryMatrix(cirq.unitary(out_circuit))
+        assert in_utry.get_distance_from(out_utry) < 1e-5
+
+    def test_synthesis_bqskit(
+        self,
+        cirq_circuit: QuantumCircuit,
+        compiler: Compiler,
+    ) -> None:
+        qc = cirq_circuit
+        in_utry = UnitaryMatrix(cirq.unitary(qc))
+        bqskit_circuit = cirq_to_bqskit(qc)
+        bqskit_out_circuit = compile(
+            bqskit_circuit,
+            compiler=compiler,
+        )
+        out_circuit = bqskit_to_cirq(bqskit_out_circuit)
+        out_utry = UnitaryMatrix(cirq.unitary(out_circuit))
+        assert in_utry.get_distance_from(out_utry) < 1e-5
+
+    def test_same_unitary(
+        self,
+        bqskit_circuit: Circuit,
+        cirq_circuit: QuantumCircuit,
+    ) -> None:
+        bqskit_utry = bqskit_circuit.get_unitary()
+        cirq_utry = UnitaryMatrix(cirq.unitary(cirq_circuit))
+        assert bqskit_utry.get_distance_from(cirq_utry) < 1e-7
diff --git a/tests/ext/test_pytket.py b/tests/ext/test_pytket.py
new file mode 100644
index 000000000..47c4c165f
--- /dev/null
+++ b/tests/ext/test_pytket.py
@@ -0,0 +1,109 @@
+from __future__ import annotations
+
+import numpy as np
+import pytest
+from pytket import OpType
+from pytket.circuit import Circuit as QubitCircuit
+from pytket.extensions.qiskit import AerUnitaryBackend
+
+from bqskit.compiler.compile import compile
+from bqskit.compiler.compiler import Compiler
+from bqskit.ext import bqskit_to_pytket
+from bqskit.ext import pytket_to_bqskit
+from bqskit.ir.circuit import Circuit
+from bqskit.ir.gates import CNOTGate
+from bqskit.ir.gates import U3Gate
+from bqskit.qis import UnitaryMatrix
+
+
+class TestTranslate:
+
+    @pytest.fixture
+    def bqskit_circuit(self) -> Circuit:
+        circuit = Circuit(3)
+        circuit.append_gate(CNOTGate(), (0, 1))
+        circuit.append_gate(U3Gate(), 0, [1, 2, 3])
+        circuit.append_gate(U3Gate(), 1, [1, 2, 3])
+        circuit.append_gate(U3Gate(), 2, [1, 2, 3])
+        circuit.append_gate(CNOTGate(), (0, 1))
+        circuit.append_gate(CNOTGate(), (0, 2))
+        circuit.append_gate(CNOTGate(), (0, 2))
+        circuit.append_gate(U3Gate(), 0, [1, 2.4, 3])
+        circuit.append_gate(U3Gate(), 1, [1, 2.2, 3])
+        circuit.append_gate(U3Gate(), 2, [1, 2.1, 3])
+        circuit.append_gate(U3Gate(), 2, [1, 2.1, 3])
+        circuit.append_gate(CNOTGate(), (0, 2))
+        circuit.append_gate(CNOTGate(), (0, 2))
+        circuit.append_gate(CNOTGate(), (0, 1))
+        circuit.append_gate(CNOTGate(), (0, 2))
+        circuit.append_gate(U3Gate(), 0, [1, 2.4, 3])
+        circuit.append_gate(U3Gate(), 1, [1, 2.2, 3])
+        circuit.append_gate(U3Gate(), 2, [1, 2.1, 3])
+        return circuit
+
+    @pytest.fixture
+    def pytket_circuit(self) -> QubitCircuit:
+        circuit = QubitCircuit(3)
+        circuit.CX(0, 1)
+        circuit.add_gate(OpType.U3, [1, 2, 3], [0])
+        circuit.add_gate(OpType.U3, [1, 2, 3], [1])
+        circuit.add_gate(OpType.U3, [1, 2, 3], [2])
+        circuit.CX(0, 1)
+        circuit.CX(0, 2)
+        circuit.CX(0, 2)
+        circuit.add_gate(OpType.U3, [1, 2.4, 3], [0])
+        circuit.add_gate(OpType.U3, [1, 2.2, 3], [1])
+        circuit.add_gate(OpType.U3, [1, 2.1, 3], [2])
+        circuit.add_gate(OpType.U3, [1, 2.1, 3], [2])
+        circuit.CX(0, 2)
+        circuit.CX(0, 2)
+        circuit.CX(0, 1)
+        circuit.CX(0, 2)
+        circuit.add_gate(OpType.U3, [1, 2.4, 3], [0])
+        circuit.add_gate(OpType.U3, [1, 2.2, 3], [1])
+        circuit.add_gate(OpType.U3, [1, 2.1, 3], [2])
+        return circuit
+
+    def get_unitary(self, qc: QubitCircuit) -> np.ndarray:  # type: ignore
+        backend = AerUnitaryBackend()
+        handle = backend.process_circuit(qc)
+        result = backend.get_result(handle)
+        return result.get_unitary()
+
+    def test_bqskit_to_bqskit(self, bqskit_circuit: Circuit) -> None:
+        in_utry = bqskit_circuit.get_unitary()
+        out_circuit = pytket_to_bqskit(bqskit_to_pytket(bqskit_circuit))
+        out_utry = out_circuit.get_unitary()
+        assert in_utry.get_distance_from(out_utry) < 1e-7
+
+    def test_pytket_to_pytket(self, pytket_circuit: QubitCircuit) -> None:
+        in_utry = UnitaryMatrix(self.get_unitary(pytket_circuit))
+        out_circuit = bqskit_to_pytket(pytket_to_bqskit(pytket_circuit))
+        out_utry = UnitaryMatrix(self.get_unitary(out_circuit))
+        assert in_utry.get_distance_from(out_utry) < 1e-7
+
+    def test_compile_bqskit(
+        self,
+        pytket_circuit: QubitCircuit,
+        compiler: Compiler,
+    ) -> None:
+        in_utry = UnitaryMatrix(self.get_unitary(pytket_circuit))
+        bqskit_circuit = pytket_to_bqskit(pytket_circuit)
+        bqskit_out_circuit = compile(
+            bqskit_circuit, max_synthesis_size=2, compiler=compiler,
+        )
+        out_circuit = bqskit_to_pytket(bqskit_out_circuit)
+        out_utry = UnitaryMatrix(self.get_unitary(out_circuit))
+        assert in_utry.get_distance_from(out_utry) < 1e-5
+
+    def test_synthesis_bqskit(
+        self,
+        pytket_circuit: QubitCircuit,
+        compiler: Compiler,
+    ) -> None:
+        in_utry = UnitaryMatrix(self.get_unitary(pytket_circuit))
+        bqskit_circuit = pytket_to_bqskit(pytket_circuit)
+        bqskit_out_circuit = compile(bqskit_circuit, compiler=compiler)
+        out_circuit = bqskit_to_pytket(bqskit_out_circuit)
+        out_utry = UnitaryMatrix(self.get_unitary(out_circuit))
+        assert in_utry.get_distance_from(out_utry) < 1e-5
diff --git a/tests/ext/test_qiskit.py b/tests/ext/test_qiskit.py
new file mode 100644
index 000000000..76352e3af
--- /dev/null
+++ b/tests/ext/test_qiskit.py
@@ -0,0 +1,113 @@
+from __future__ import annotations
+
+import pytest
+import qiskit.quantum_info as qi
+from qiskit import QuantumCircuit
+from qiskit import transpile
+
+from bqskit.compiler.compile import compile
+from bqskit.compiler.compiler import Compiler
+from bqskit.ext import bqskit_to_qiskit
+from bqskit.ext import qiskit_to_bqskit
+from bqskit.ir.circuit import Circuit
+from bqskit.ir.gates import CNOTGate
+from bqskit.ir.gates import U3Gate
+from bqskit.qis import UnitaryMatrix
+
+
+class TestTranslate:
+
+    @pytest.fixture
+    def bqskit_circuit(self) -> Circuit:
+        circuit = Circuit(3)
+        circuit.append_gate(CNOTGate(), (0, 1))
+        circuit.append_gate(U3Gate(), 0, [1, 2, 3])
+        circuit.append_gate(U3Gate(), 1, [1, 2, 3])
+        circuit.append_gate(U3Gate(), 2, [1, 2, 3])
+        circuit.append_gate(CNOTGate(), (0, 1))
+        circuit.append_gate(CNOTGate(), (0, 2))
+        circuit.append_gate(CNOTGate(), (0, 2))
+        circuit.append_gate(U3Gate(), 0, [1, 2.4, 3])
+        circuit.append_gate(U3Gate(), 1, [1, 2.2, 3])
+        circuit.append_gate(U3Gate(), 2, [1, 2.1, 3])
+        circuit.append_gate(U3Gate(), 2, [1, 2.1, 3])
+        circuit.append_gate(CNOTGate(), (0, 2))
+        circuit.append_gate(CNOTGate(), (0, 2))
+        circuit.append_gate(CNOTGate(), (0, 1))
+        circuit.append_gate(CNOTGate(), (0, 2))
+        circuit.append_gate(U3Gate(), 0, [1, 2.4, 3])
+        circuit.append_gate(U3Gate(), 1, [1, 2.2, 3])
+        circuit.append_gate(U3Gate(), 2, [1, 2.1, 3])
+        return circuit
+
+    @pytest.fixture
+    def qiskit_circuit(self) -> QuantumCircuit:
+        circuit = QuantumCircuit(3)
+        circuit.cnot(0, 1)
+        circuit.u(1, 2, 3, 0)
+        circuit.u(1, 2, 3, 1)
+        circuit.u(1, 2, 3, 2)
+        circuit.cnot(0, 1)
+        circuit.cnot(0, 2)
+        circuit.cnot(0, 2)
+        circuit.u3(1, 2.4, 3, 0)
+        circuit.u(1, 2.2, 3, 1)
+        circuit.u(1, 2.1, 3, 2)
+        circuit.u(1, 2.1, 3, 2)
+        circuit.cnot(0, 2)
+        circuit.cnot(0, 2)
+        circuit.cnot(0, 1)
+        circuit.cnot(0, 2)
+        circuit.u(1, 2.4, 3, 0)
+        circuit.u(1, 2.2, 3, 1)
+        circuit.u(1, 2.1, 3, 2)
+        return circuit
+
+    def test_bqskit_to_bqskit(self, bqskit_circuit: Circuit) -> None:
+        in_utry = bqskit_circuit.get_unitary()
+        out_circuit = qiskit_to_bqskit(bqskit_to_qiskit(bqskit_circuit))
+        out_utry = out_circuit.get_unitary()
+        assert in_utry.get_distance_from(out_utry) < 1e-7
+
+    def test_qiskit_to_qiskit(self, qiskit_circuit: QuantumCircuit) -> None:
+        qc = qiskit_circuit
+        in_utry = UnitaryMatrix(qi.Operator(qc).data)
+        out_circuit = bqskit_to_qiskit(qiskit_to_bqskit(qc))
+        out_utry = UnitaryMatrix(qi.Operator(out_circuit).data)
+        assert in_utry.get_distance_from(out_utry) < 1e-7
+
+    def test_compile_bqskit(
+        self,
+        qiskit_circuit: QuantumCircuit,
+        compiler: Compiler,
+    ) -> None:
+        qc = qiskit_circuit
+        in_utry = UnitaryMatrix(qi.Operator(qc).data)
+        bqskit_circuit = qiskit_to_bqskit(qc)
+        bqskit_out_circuit = compile(
+            bqskit_circuit, max_synthesis_size=2, compiler=compiler,
+        )
+        out_circuit = bqskit_to_qiskit(bqskit_out_circuit)
+        out_utry = UnitaryMatrix(qi.Operator(out_circuit).data)
+        assert in_utry.get_distance_from(out_utry) < 1e-5
+
+    def test_synthesis_bqskit(
+        self,
+        qiskit_circuit: QuantumCircuit,
+        compiler: Compiler,
+    ) -> None:
+        qc = qiskit_circuit
+        in_utry = UnitaryMatrix(qi.Operator(qc).data)
+        bqskit_circuit = qiskit_to_bqskit(qc)
+        bqskit_out_circuit = compile(bqskit_circuit, compiler=compiler)
+        out_circuit = bqskit_to_qiskit(bqskit_out_circuit)
+        out_utry = UnitaryMatrix(qi.Operator(out_circuit).data)
+        assert in_utry.get_distance_from(out_utry) < 1e-5
+
+    def test_compile_qiskit(self, bqskit_circuit: Circuit) -> None:
+        in_utry = bqskit_circuit.get_unitary()
+        qiskit_circuit = bqskit_to_qiskit(bqskit_circuit)
+        qc_out = transpile(qiskit_circuit, optimization_level=3)
+        bqskit_out_circuit = qiskit_to_bqskit(qc_out)
+        out_utry = bqskit_out_circuit.get_unitary()
+        assert in_utry.get_distance_from(out_utry) < 1e-5
diff --git a/tests/ext/test_qutip.py b/tests/ext/test_qutip.py
new file mode 100644
index 000000000..4821c1120
--- /dev/null
+++ b/tests/ext/test_qutip.py
@@ -0,0 +1,121 @@
+from __future__ import annotations
+
+import pytest
+from qutip import CircuitSimulator
+from qutip import QubitCircuit
+
+from bqskit.compiler.compile import compile
+from bqskit.compiler.compiler import Compiler
+from bqskit.ext import bqskit_to_qutip
+from bqskit.ext import qutip_to_bqskit
+from bqskit.ir.circuit import Circuit
+from bqskit.ir.gates import CNOTGate
+from bqskit.ir.gates import U3Gate
+from bqskit.qis import UnitaryMatrix
+
+
+class TestTranslate:
+
+    @pytest.fixture
+    def bqskit_circuit(self) -> Circuit:
+        circuit = Circuit(3)
+        circuit.append_gate(CNOTGate(), (0, 1))
+        circuit.append_gate(U3Gate(), 0, [1, 2, 3])
+        circuit.append_gate(U3Gate(), 1, [1, 2, 3])
+        circuit.append_gate(U3Gate(), 2, [1, 2, 3])
+        circuit.append_gate(CNOTGate(), (0, 1))
+        circuit.append_gate(CNOTGate(), (0, 2))
+        circuit.append_gate(CNOTGate(), (0, 2))
+        circuit.append_gate(U3Gate(), 0, [1, 2.4, 3])
+        circuit.append_gate(U3Gate(), 1, [1, 2.2, 3])
+        circuit.append_gate(U3Gate(), 2, [1, 2.1, 3])
+        circuit.append_gate(U3Gate(), 2, [1, 2.1, 3])
+        circuit.append_gate(CNOTGate(), (0, 2))
+        circuit.append_gate(CNOTGate(), (0, 2))
+        circuit.append_gate(CNOTGate(), (0, 1))
+        circuit.append_gate(CNOTGate(), (0, 2))
+        circuit.append_gate(U3Gate(), 0, [1, 2.4, 3])
+        circuit.append_gate(U3Gate(), 1, [1, 2.2, 3])
+        circuit.append_gate(U3Gate(), 2, [1, 2.1, 3])
+        return circuit
+
+    @pytest.fixture
+    def qutip_circuit(self) -> QubitCircuit:
+        circuit = QubitCircuit(3)
+        circuit.add_gate('CNOT', controls=0, targets=1)
+        circuit.add_gate('QASMU', targets=0, arg_value=[1, 2, 3])
+        circuit.add_gate('QASMU', targets=1, arg_value=[1, 2, 3])
+        circuit.add_gate('QASMU', targets=2, arg_value=[1, 2, 3])
+        circuit.add_gate('CNOT', controls=0, targets=1)
+        circuit.add_gate('CNOT', controls=0, targets=2)
+        circuit.add_gate('CNOT', controls=0, targets=2)
+        circuit.add_gate('QASMU', targets=0, arg_value=[1, 2.4, 3])
+        circuit.add_gate('QASMU', targets=1, arg_value=[1, 2.2, 3])
+        circuit.add_gate('QASMU', targets=2, arg_value=[1, 2.1, 3])
+        circuit.add_gate('QASMU', targets=2, arg_value=[1, 2.1, 3])
+        circuit.add_gate('CNOT', controls=0, targets=2)
+        circuit.add_gate('CNOT', controls=0, targets=2)
+        circuit.add_gate('CNOT', controls=0, targets=1)
+        circuit.add_gate('CNOT', controls=0, targets=2)
+        circuit.add_gate('QASMU', targets=0, arg_value=[1, 2.4, 3])
+        circuit.add_gate('QASMU', targets=1, arg_value=[1, 2.2, 3])
+        circuit.add_gate('QASMU', targets=2, arg_value=[1, 2.1, 3])
+        return circuit
+
+    def test_bqskit_to_bqskit(self, bqskit_circuit: Circuit) -> None:
+        in_utry = bqskit_circuit.get_unitary()
+        out_circuit = qutip_to_bqskit(bqskit_to_qutip(bqskit_circuit))
+        out_utry = out_circuit.get_unitary()
+        assert in_utry.get_distance_from(out_utry) < 1e-7
+
+    def test_qutip_to_qutip(self, qutip_circuit: QubitCircuit) -> None:
+        qc = qutip_circuit
+        utry = CircuitSimulator(qc, precompute_unitary=True).ops[0].data
+        in_utry = UnitaryMatrix(utry.todense())
+        oc = bqskit_to_qutip(qutip_to_bqskit(qc))
+        utry = CircuitSimulator(oc, precompute_unitary=True).ops[0].data
+        out_utry = UnitaryMatrix(utry.todense())
+        assert in_utry.get_distance_from(out_utry) < 1e-7
+
+    def test_compile_bqskit(
+        self,
+        qutip_circuit: QubitCircuit,
+        compiler: Compiler,
+    ) -> None:
+        qc = qutip_circuit
+        utry = CircuitSimulator(qc, precompute_unitary=True).ops[0].data
+        in_utry = UnitaryMatrix(utry.todense())
+        bqskit_circuit = qutip_to_bqskit(qc)
+        bqskit_out_circuit = compile(
+            bqskit_circuit, max_synthesis_size=2, compiler=compiler,
+        )
+        oc = bqskit_to_qutip(bqskit_out_circuit)
+        utry = CircuitSimulator(oc, precompute_unitary=True).ops[0].data
+        out_utry = UnitaryMatrix(utry.todense())
+        assert in_utry.get_distance_from(out_utry) < 1e-5
+
+    def test_synthesis_bqskit(
+        self,
+        qutip_circuit: QubitCircuit,
+        compiler: Compiler,
+    ) -> None:
+        qc = qutip_circuit
+        utry = CircuitSimulator(qc, precompute_unitary=True).ops[0].data
+        in_utry = UnitaryMatrix(utry.todense())
+        bqskit_circuit = qutip_to_bqskit(qc)
+        bqskit_out_circuit = compile(bqskit_circuit, compiler=compiler)
+        oc = bqskit_to_qutip(bqskit_out_circuit)
+        utry = CircuitSimulator(oc, precompute_unitary=True).ops[0].data
+        out_utry = UnitaryMatrix(utry.todense())
+        assert in_utry.get_distance_from(out_utry) < 1e-5
+
+    def test_same_unitary(
+        self,
+        bqskit_circuit: Circuit,
+        qutip_circuit: QubitCircuit,
+    ) -> None:
+        bqskit_utry = bqskit_circuit.get_unitary()
+        qc = qutip_circuit
+        utry = CircuitSimulator(qc, precompute_unitary=True).ops[0].data
+        qutip_utry = UnitaryMatrix(utry.todense())
+        assert bqskit_utry.get_distance_from(qutip_utry) < 1e-7
diff --git a/tests/ir/circuit/test_batch_replace.py b/tests/ir/circuit/test_batch_replace.py
index ae7cde8fa..501621d56 100644
--- a/tests/ir/circuit/test_batch_replace.py
+++ b/tests/ir/circuit/test_batch_replace.py
@@ -106,7 +106,7 @@ def test_batch_replace(self) -> None:
             (3, 1),
         ) == op_5b
 
-    def test_random_batch_remove(self) -> None:
+    def test_random_batch_replace(self) -> None:
         num_gates = 200
         num_q = 10
 
diff --git a/tests/ir/circuit/test_cycle_methods.py b/tests/ir/circuit/test_cycle_methods.py
index 36091af6d..4c4782836 100644
--- a/tests/ir/circuit/test_cycle_methods.py
+++ b/tests/ir/circuit/test_cycle_methods.py
@@ -58,9 +58,14 @@ def test_index_invalid_2(self, cycle_index: int) -> None:
 
     def test_index_valid_1(self, r6_qudit_circuit: Circuit) -> None:
         num_cycles = r6_qudit_circuit.num_cycles
+        print('!' * 80)
         for i in range(num_cycles):
+            print('!' * 20)
+            for cycle in r6_qudit_circuit._circuit:
+                print(cycle)
             r6_qudit_circuit.pop_cycle(-1)
             assert r6_qudit_circuit.num_cycles == num_cycles - i - 1
+            print('!' * 20)
 
     @pytest.mark.parametrize('cycle_index', [-3, -2, -1, 0, 1, 2])
     def test_multi_qudit(self, cycle_index: int) -> None:
diff --git a/tests/ir/circuit/test_logic.py b/tests/ir/circuit/test_logic.py
index c4dfbd8e6..f235f5fa6 100644
--- a/tests/ir/circuit/test_logic.py
+++ b/tests/ir/circuit/test_logic.py
@@ -4,7 +4,7 @@
 from hypothesis import given
 
 from bqskit.ir.circuit import Circuit
-from bqskit.qis.permutation import calc_permutation_matrix
+from bqskit.qis.permutation import PermutationMatrix
 from bqskit.qis.unitary.unitarymatrix import UnitaryMatrix
 from bqskit.utils.test.strategies import circuits
 from bqskit.utils.test.types import invalid_type_test
@@ -32,7 +32,7 @@ def test_invalid_type() -> None:
 @given(circuits((2, 2, 2)))
 def test_renumber(circuit: Circuit) -> None:
     U = circuit.get_unitary()
-    P = calc_permutation_matrix(3, [1, 2, 0])
+    P = PermutationMatrix.from_qubit_location(3, [1, 2, 0])
     circuit.renumber_qudits([1, 2, 0])
     U2 = circuit.get_unitary()
-    assert U == P.T @ U2 @ P
+    assert U == P @ U2 @ P.T
diff --git a/tests/ir/circuit/test_measure.py b/tests/ir/circuit/test_measure.py
new file mode 100644
index 000000000..2be8d3c59
--- /dev/null
+++ b/tests/ir/circuit/test_measure.py
@@ -0,0 +1,24 @@
+from __future__ import annotations
+
+from bqskit.ir.gates.measure import MeasurementPlaceholder
+from bqskit.ir.lang.qasm2 import OPENQASM2Language
+
+
+class TestCircuitRemoveAllMeasurements:
+    def test_measure_single_bit(self) -> None:
+        input = """
+            OPENQASM 2.0;
+            qreg q[1];
+            creg c[1];
+            measure q[0] -> c[0];
+        """
+        circuit = OPENQASM2Language().decode(input)
+        assert any(
+            isinstance(g, MeasurementPlaceholder)
+            for g in circuit.gate_set
+        )
+        circuit.remove_all_measurements()
+        assert not any(
+            isinstance(g, MeasurementPlaceholder)
+            for g in circuit.gate_set
+        )
diff --git a/tests/ir/circuit/test_op_gate_circ_methods.py b/tests/ir/circuit/test_op_gate_circ_methods.py
index 530f60877..7db715789 100644
--- a/tests/ir/circuit/test_op_gate_circ_methods.py
+++ b/tests/ir/circuit/test_op_gate_circ_methods.py
@@ -337,14 +337,6 @@ def test_reconstruct(self, circuit: Circuit) -> None:
 class TestAppendCircuit:
     """This tests `circuit.append_circuit`."""
 
-    @valid_type_test(Circuit(1).append_circuit)
-    def test_valid_type(self) -> None:
-        pass
-
-    @invalid_type_test(Circuit(1).append_circuit)
-    def test_invalid_type(self) -> None:
-        pass
-
     @given(circuits())
     def test_reconstruct(self, circuit: Circuit) -> None:
         new_circuit = Circuit(circuit.num_qudits, circuit.radixes)
@@ -387,10 +379,6 @@ class TestInsert:
     def test_valid_type(self) -> None:
         pass
 
-    @invalid_type_test(Circuit(1).insert, [ValueError])
-    def test_invalid_type(self) -> None:
-        pass
-
     def test_empty(self) -> None:
         circuit = Circuit(2)
         circuit.insert(0, Operation(CXGate(), (0, 1)))
@@ -412,10 +400,6 @@ class TestInsertGate:
     def test_valid_type(self) -> None:
         pass
 
-    @invalid_type_test(Circuit(1).insert_gate, [ValueError])
-    def test_invalid_type(self) -> None:
-        pass
-
     def test_empty(self) -> None:
         circuit = Circuit(2)
         circuit.insert_gate(0, CXGate(), (0, 1))
@@ -433,14 +417,6 @@ def test_insert(self, circuit: Circuit, op: Operation) -> None:
 class TestInsertCircuit:
     """This tests `circuit.insert_circuit`."""
 
-    @valid_type_test(Circuit(1).insert_circuit)
-    def test_valid_type(self) -> None:
-        pass
-
-    @invalid_type_test(Circuit(1).insert_circuit, [ValueError, AttributeError])
-    def test_invalid_type(self) -> None:
-        pass
-
     @given(circuits((2, 2, 2, 2)))
     def test_apply(self, circuit: Circuit) -> None:
         new_circuit = Circuit(circuit.num_qudits, circuit.radixes)
@@ -579,14 +555,6 @@ def test_pop_all(self, circuit: Circuit) -> None:
 class TestBatchPop:
     """This tests `circuit.batch_pop`."""
 
-    @valid_type_test(Circuit(1).batch_pop)
-    def test_valid_type(self) -> None:
-        pass
-
-    @invalid_type_test(Circuit(1).batch_pop, [IndexError])
-    def test_invalid_type(self) -> None:
-        pass
-
     @given(circuits((2, 2, 2, 2)))
     def test_batch_pop_all(self, circuit: Circuit) -> None:
         if circuit.num_operations == 0:
diff --git a/tests/ir/circuit/test_qudit_methods.py b/tests/ir/circuit/test_qudit_methods.py
index 971198914..078d58045 100644
--- a/tests/ir/circuit/test_qudit_methods.py
+++ b/tests/ir/circuit/test_qudit_methods.py
@@ -649,6 +649,7 @@ def test_valid_type(self) -> None:
     def test_invalid_type(self) -> None:
         pass
 
+    @given(integers())
     def test_return_type(self, an_int: int) -> None:
         circuit = Circuit(4, [2, 2, 3, 3])
         assert isinstance(circuit.is_qudit_in_range(an_int), (bool, np.bool_))
diff --git a/tests/ir/gates/test_native.py b/tests/ir/gates/test_native.py
index 07fce6072..a997e7493 100644
--- a/tests/ir/gates/test_native.py
+++ b/tests/ir/gates/test_native.py
@@ -119,7 +119,7 @@ def test_random_circuit_qubit_gates(qubit_gate: Gate) -> None:
     py = circ.get_unitary(x)
     rs = circuit.get_unitary(x)
     assert py.shape == rs.shape
-    assert py.dtype is rs.dtype
+    assert py.dtype == rs.dtype
     np.testing.assert_allclose(py, rs, verbose=True)
 
 
@@ -135,5 +135,5 @@ def test_random_circuit_qutrit_gates(qutrit_gate: Gate) -> None:
     py = circ.get_unitary(x)
     rs = circuit.get_unitary(x)
     assert py.shape == rs.shape
-    assert py.dtype is rs.dtype
+    assert py.dtype == rs.dtype
     np.testing.assert_allclose(py, rs, verbose=True)
diff --git a/tests/ir/lang/test_qasm_decode.py b/tests/ir/lang/test_qasm_decode.py
new file mode 100644
index 000000000..aeb8661b2
--- /dev/null
+++ b/tests/ir/lang/test_qasm_decode.py
@@ -0,0 +1,354 @@
+"""This file tests the qasm2 language support in BQSKit."""
+from __future__ import annotations
+
+from unittest.mock import mock_open
+from unittest.mock import patch
+
+import pytest
+from qiskit import QuantumCircuit
+
+from bqskit.ext import qiskit_to_bqskit
+from bqskit.ir.gates.circuitgate import CircuitGate
+from bqskit.ir.gates.constant.cx import CNOTGate
+from bqskit.ir.gates.measure import MeasurementPlaceholder
+from bqskit.ir.gates.parameterized.u1 import U1Gate
+from bqskit.ir.gates.parameterized.u2 import U2Gate
+from bqskit.ir.gates.parameterized.u3 import U3Gate
+from bqskit.ir.lang.language import LangException
+from bqskit.ir.lang.qasm2 import OPENQASM2Language
+from bqskit.qis.unitary.unitarymatrix import UnitaryMatrix
+
+
+class TestComment:
+    def test_comment_header_1(self) -> None:
+        input = """
+            // This is a comment
+            OPENQASM 2.0;
+            qreg q[1];
+            u1(0.1) q[0];
+        """
+        circuit = OPENQASM2Language().decode(input)
+        assert circuit.get_unitary() == U1Gate().get_unitary([0.1])
+
+    def test_comment_header_2(self) -> None:
+        input = """
+
+            // This is a comment
+
+            OPENQASM 2.0;
+            qreg q[1];
+            u1(0.1) q[0];
+        """
+        circuit = OPENQASM2Language().decode(input)
+        assert circuit.get_unitary() == U1Gate().get_unitary([0.1])
+
+    def test_comment_footer_1(self) -> None:
+        input = """
+            OPENQASM 2.0;
+            qreg q[1];
+            u1(0.1) q[0];
+            // This is a comment
+        """
+        circuit = OPENQASM2Language().decode(input)
+        assert circuit.get_unitary() == U1Gate().get_unitary([0.1])
+
+    def test_comment_footer_2(self) -> None:
+        input = """
+            OPENQASM 2.0;
+            qreg q[1];
+            u1(0.1) q[0];
+
+            // This is a comment
+
+        """
+        circuit = OPENQASM2Language().decode(input)
+        assert circuit.get_unitary() == U1Gate().get_unitary([0.1])
+
+    def test_comment_gatedecl(self) -> None:
+        input = """
+            OPENQASM 2.0;
+            qreg q[1];
+            gate gate_x q0 {
+                // This is a commment
+                u1(0.1) q0;
+                // This is a comment too
+            }
+            gate_x q[0];
+        """
+        circuit = OPENQASM2Language().decode(input)
+        assert circuit.get_unitary() == U1Gate().get_unitary([0.1])
+
+
+class TestGateDecl:
+    def test_simple_gate_decl(self) -> None:
+        input = """
+            OPENQASM 2.0;
+            include "qelib1.inc";
+            qreg q[1];
+            gate gate_x (p0) q0 {
+                u2(p0, 3.5*p0) q0;
+            }
+            gate_x(1.2) q[0];
+        """
+
+        circuit = OPENQASM2Language().decode(input)
+        assert circuit.num_qudits == 1
+        assert circuit.num_operations == 1
+        op = circuit[0, 0]
+        assert isinstance(op.gate, CircuitGate)
+        assert op.location == (0,)
+        assert op.params == [1.2, 4.2]
+        assert op.gate._circuit[0, 0].gate == U2Gate()
+        assert op.gate._circuit[0, 0].location == (0,)
+
+        qc = QuantumCircuit.from_qasm_str(input)
+        bqskit_utry = circuit.get_unitary()
+        qiskit_utry = qiskit_to_bqskit(qc).get_unitary()
+        assert qiskit_utry.get_distance_from(bqskit_utry) < 1e-7
+
+    def test_gate_decl_UCX(self) -> None:
+        input = """
+            OPENQASM 2.0;
+            include "qelib1.inc";
+            qreg q[2];
+            gate gate_x (p0) q0, q1 {
+                U(p0, 2, 3) q0;
+                CX q0, q1;
+            }
+            gate_x(1.2) q[0], q[1];
+        """
+
+        circuit = OPENQASM2Language().decode(input)
+        assert circuit.num_operations == 1
+        op = circuit[0, 0]
+        assert isinstance(op.gate, CircuitGate)
+        assert op.location == (0, 1)
+        U1 = U3Gate().get_unitary([1.2, 2, 3]).otimes(UnitaryMatrix.identity(2))
+        U2 = CNOTGate().get_unitary()
+        assert circuit.get_unitary().get_distance_from(U2 @ U1) < 1e-7
+
+    def test_empty_gate_decl_1(self) -> None:
+        input = """
+            OPENQASM 2.0;
+            include "qelib1.inc";
+            qreg q[1];
+            gate gate_x () q0 {
+            }
+            gate_x q[0];
+        """
+
+        circuit = OPENQASM2Language().decode(input)
+        assert circuit.num_qudits == 1
+        op = circuit[0, 0]
+        assert isinstance(op.gate, CircuitGate)
+        assert op.location == (0,)
+        assert op.params == []
+
+        qc = QuantumCircuit.from_qasm_str(input)
+        bqskit_utry = circuit.get_unitary()
+        qiskit_utry = qiskit_to_bqskit(qc).get_unitary()
+        assert qiskit_utry.get_distance_from(bqskit_utry) < 1e-7
+
+    def test_empty_gate_decl_2(self) -> None:
+        input = """
+            OPENQASM 2.0;
+            include "qelib1.inc";
+            qreg q[1];
+            gate gate_x q0 {}
+            gate_x q[0];
+        """
+
+        circuit = OPENQASM2Language().decode(input)
+        assert circuit.num_qudits == 1
+        op = circuit[0, 0]
+        assert isinstance(op.gate, CircuitGate)
+        assert op.location == (0,)
+        assert op.params == []
+
+        qc = QuantumCircuit.from_qasm_str(input)
+        bqskit_utry = circuit.get_unitary()
+        qiskit_utry = qiskit_to_bqskit(qc).get_unitary()
+        assert qiskit_utry.get_distance_from(bqskit_utry) < 1e-7
+
+    def test_gate_decl_qubit_mixup(self) -> None:
+        input = """
+            OPENQASM 2.0;
+            include "qelib1.inc";
+            qreg q[3];
+            gate gate_x q0, q1, q2 {
+                cx q2, q1;
+                cx q0, q2;
+                cx q1, q0;
+                cx q2, q0;
+            }
+            gate_x q[1], q[2], q[0];
+        """
+
+        circuit = OPENQASM2Language().decode(input)
+        assert circuit.num_qudits == 3
+        op = circuit[0, 0]
+        assert isinstance(op.gate, CircuitGate)
+        assert op.location == (1, 2, 0)
+        assert op.params == []
+
+        subcircuit = op.gate._circuit
+        assert subcircuit.num_qudits == 3
+        assert subcircuit.num_cycles == 4
+        assert subcircuit[0, 1].gate == CNOTGate()
+        assert subcircuit[0, 1].location == (2, 1)
+        assert subcircuit[1, 0].gate == CNOTGate()
+        assert subcircuit[1, 0].location == (0, 2)
+        assert subcircuit[2, 1].gate == CNOTGate()
+        assert subcircuit[2, 1].location == (1, 0)
+        assert subcircuit[3, 2].gate == CNOTGate()
+        assert subcircuit[3, 2].location == (2, 0)
+
+        qc = QuantumCircuit.from_qasm_str(input)
+        bqskit_utry = circuit.get_unitary()
+        qiskit_utry = qiskit_to_bqskit(qc).get_unitary()
+        assert qiskit_utry.get_distance_from(bqskit_utry) < 1e-7
+
+    def test_nested_gate_decl(self) -> None:
+        input = """
+            OPENQASM 2.0;
+            include "qelib1.inc";
+            qreg q[2];
+            u1(0.1) q[0];
+            gate gate_x (p0) q0 {
+                u2(p0, 3.5*p0) q0;
+            }
+            gate gate_y (p0) q0, q1 {
+                gate_x(p0) q0;
+                u1(0.1) q0;
+                gate_x(p0*2) q1;
+            }
+            gate_y(1.2) q[0], q[1];
+        """
+
+        circuit = OPENQASM2Language().decode(input)
+        assert circuit.num_qudits == 2
+        op = circuit[0, 0]
+        assert op.gate == U1Gate()
+        assert op.location == (0,)
+        assert op.params == [0.1]
+
+        op = circuit[1, 1]
+        assert isinstance(op.gate, CircuitGate)
+        assert op.location == (0, 1)
+
+        subcircuit = op.gate._circuit
+        assert isinstance(subcircuit[0, 0].gate, CircuitGate)
+        assert subcircuit[0, 0].gate._circuit[0, 0].gate == U2Gate()
+        assert subcircuit[0, 0].gate._circuit[0, 0].params == [1.2, 4.2]
+        assert isinstance(subcircuit[0, 1].gate, CircuitGate)
+        assert subcircuit[0, 1].gate._circuit[0, 0].gate == U2Gate()
+        assert subcircuit[0, 1].gate._circuit[0, 0].params == [2.4, 8.4]
+        assert subcircuit[1, 0].gate == U1Gate()
+        assert subcircuit[1, 0].params == [0.1]
+
+        # Unable to verify this one with qiskit
+        # https://github.com/Qiskit/qiskit-terra/issues/8558
+        # qc = QuantumCircuit.from_qasm_str(input)
+        # bqskit_utry = circuit.get_unitary()
+        # qiskit_utry = qiskit_to_bqskit(qc).get_unitary()
+        # assert qiskit_utry.get_distance_from(bqskit_utry) < 1e-7
+
+
+class TestIncludeStatements:
+
+    def test_include_no_exists(self) -> None:
+        input = """
+            OPENQASM 2.0;
+            include "qelib1.inc";
+            qreg q[2];
+        """
+        circuit = OPENQASM2Language().decode(input)
+        assert circuit.num_qudits == 2
+        assert circuit.num_operations == 0
+
+    def test_include_simple(self) -> None:
+        idata = """
+            gate test(p) q { u1(p) q; }
+        """
+        input = """
+            OPENQASM 2.0;
+            include "test.inc";
+            qreg q[1];
+            test(0.1) q[0];
+        """
+        with patch('builtins.open', mock_open(read_data=idata)) as mock_file:
+            with patch('os.path.isfile', lambda x: True):
+                circuit = OPENQASM2Language().decode(input)
+            mock_file.assert_called_with('test.inc')
+        assert circuit.num_qudits == 1
+        assert circuit.num_operations == 1
+        gate_unitary = U1Gate().get_unitary([0.1])
+        assert circuit.get_unitary().get_distance_from(gate_unitary) < 1e-7
+
+
+class TestMeasure:
+    def test_measure_single_bit(self) -> None:
+        input = """
+            OPENQASM 2.0;
+            qreg q[1];
+            creg c[1];
+            measure q[0] -> c[0];
+        """
+        circuit = OPENQASM2Language().decode(input)
+        expected = MeasurementPlaceholder([('c', 1)], {0: ('c', 0)})
+        assert circuit[0, 0].gate == expected
+
+    def test_measure_register_1(self) -> None:
+        input = """
+            OPENQASM 2.0;
+            qreg q[1];
+            creg c[1];
+            measure q -> c;
+        """
+        circuit = OPENQASM2Language().decode(input)
+        expected = MeasurementPlaceholder([('c', 1)], {0: ('c', 0)})
+        assert circuit[0, 0].gate == expected
+
+    def test_measure_register_2(self) -> None:
+        input = """
+            OPENQASM 2.0;
+            qreg q[2];
+            creg c[2];
+            measure q -> c;
+        """
+        circuit = OPENQASM2Language().decode(input)
+        measurements = {0: ('c', 0), 1: ('c', 1)}
+        expected = MeasurementPlaceholder([('c', 2)], measurements)
+        assert circuit[0, 0].gate == expected
+
+    def test_measure_register_invalid_size(self) -> None:
+        input = """
+            OPENQASM 2.0;
+            qreg q[2];
+            creg c[1];
+            measure q -> c;
+        """
+        with pytest.raises(LangException):
+            circuit = OPENQASM2Language().decode(input)  # noqa
+
+
+def test_U_gate() -> None:
+    input = """
+        OPENQASM 2.0;
+        qreg q[1];
+        U(1,2.1,3) q[0];
+    """
+    circuit = OPENQASM2Language().decode(input)
+    assert circuit.num_operations == 1
+    assert circuit.get_unitary() == U3Gate().get_unitary([1, 2.1, 3])
+
+
+def test_CX_gate() -> None:
+    input = """
+        OPENQASM 2.0;
+        qreg q[2];
+        CX q[0],q[1];
+    """
+    circuit = OPENQASM2Language().decode(input)
+    assert circuit.num_operations == 1
+    assert circuit[0, 0].gate == CNOTGate()
diff --git a/tests/ir/lang/test_qasm_encode.py b/tests/ir/lang/test_qasm_encode.py
new file mode 100644
index 000000000..272dbd541
--- /dev/null
+++ b/tests/ir/lang/test_qasm_encode.py
@@ -0,0 +1,43 @@
+from __future__ import annotations
+
+from bqskit.ir import Circuit
+from bqskit.ir.gates import CNOTGate
+from bqskit.ir.gates import U3Gate
+from bqskit.ir.lang.qasm2 import OPENQASM2Language
+
+
+class TestCircuitGates:
+    def test_circuitgate(self) -> None:
+        subcircuit = Circuit(4)
+        subcircuit.append_gate(CNOTGate(), (0, 1))
+        subcircuit.append_gate(U3Gate(), 0, [1, 2, 3])
+        subcircuit.append_gate(U3Gate(), 0, [1, 2, 3])
+        circuit = Circuit(4)
+        circuit.append_circuit(subcircuit, (0, 1, 2, 3), True)
+        circuit.append_circuit(subcircuit, (0, 1, 2, 3), True)
+        in_utry = circuit.get_unitary()
+
+        qasm = OPENQASM2Language().encode(circuit)
+        parsed_circuit = OPENQASM2Language().decode(qasm)
+        assert parsed_circuit.get_unitary().get_distance_from(in_utry) < 1e-7
+
+    def test_nested_circuitgate(self) -> None:
+        subsubcircuit = Circuit(4)
+        subsubcircuit.append_gate(CNOTGate(), (0, 1))
+        subsubcircuit.append_gate(U3Gate(), 0, [1, 2, 3])
+        subsubcircuit.append_gate(U3Gate(), 0, [1, 2, 3])
+        subcircuit1 = Circuit(4)
+        subcircuit1.append_circuit(subsubcircuit, (0, 1, 2, 3), True)
+        subcircuit1.append_circuit(subsubcircuit, (0, 1, 2, 3), True)
+        subcircuit2 = Circuit(2)
+        subcircuit2.append_gate(U3Gate(), 0, [4, 6, 1.2])
+        subcircuit2.append_gate(U3Gate(), 1, [4, 6, 1.2])
+        circuit = Circuit(4)
+        circuit.append_circuit(subcircuit1, (0, 1, 2, 3), True)
+        circuit.append_circuit(subcircuit2, (0, 3), True)
+        circuit.append_circuit(subcircuit1, (0, 1, 2, 3), True)
+        in_utry = circuit.get_unitary()
+
+        qasm = OPENQASM2Language().encode(circuit)
+        parsed_circuit = OPENQASM2Language().decode(qasm)
+        assert parsed_circuit.get_unitary().get_distance_from(in_utry) < 1e-7
diff --git a/tests/ir/opt/minimizers/test_scipy.py b/tests/ir/opt/minimizers/test_scipy.py
new file mode 100644
index 000000000..d2872f126
--- /dev/null
+++ b/tests/ir/opt/minimizers/test_scipy.py
@@ -0,0 +1,28 @@
+"""This module tests the ScipyMinimizer class."""
+from __future__ import annotations
+
+from bqskit.ir import Circuit
+from bqskit.ir.gates import RZGate
+from bqskit.ir.gates import SXGate
+from bqskit.ir.opt import HilbertSchmidtCostGenerator
+from bqskit.ir.opt import ScipyMinimizer
+from bqskit.qis.unitary import UnitaryMatrix
+
+
+def test_scipy_minimize_sq_circuit() -> None:
+    circuit = Circuit(1)
+    circuit.append_gate(RZGate(), 0)
+    circuit.append_gate(SXGate(), 0)
+    circuit.append_gate(RZGate(), 0)
+    circuit.append_gate(SXGate(), 0)
+    circuit.append_gate(RZGate(), 0)
+
+    for _ in range(100):
+        utry = UnitaryMatrix.random(1)
+        circuit.instantiate(
+            utry,
+            method='minimization',
+            minimizer=ScipyMinimizer(),
+            cost_fn_gen=HilbertSchmidtCostGenerator(),
+        )
+        assert circuit.get_unitary().get_distance_from(utry, 1) < 1e-12
diff --git a/tests/passes/control/test_dothendecide.py b/tests/passes/control/test_dothendecide.py
new file mode 100644
index 000000000..2ad5a40ba
--- /dev/null
+++ b/tests/passes/control/test_dothendecide.py
@@ -0,0 +1,52 @@
+from __future__ import annotations
+
+from typing import Any
+
+from bqskit.compiler.basepass import BasePass
+from bqskit.ir.circuit import Circuit
+from bqskit.ir.gates.constant.cx import CNOTGate
+from bqskit.passes.control import DoThenDecide
+
+
+def test_dothendecide_accept() -> None:
+    def accept_always(c1: Circuit, c2: Circuit) -> bool:
+        return True
+
+    class AddCNOTPass(BasePass):
+        def run(self, circuit: Circuit, data: dict[str, Any] = {}) -> None:
+            circuit.append_gate(CNOTGate(), (0, 1))
+
+    circuit = Circuit(2)
+    dtd_pass = DoThenDecide(accept_always, AddCNOTPass())
+    dtd_pass.run(circuit)
+    assert circuit.num_operations == 1
+    assert circuit.gate_set == {CNOTGate()}
+
+
+def test_dothendecide_reject() -> None:
+    def reject_always(c1: Circuit, c2: Circuit) -> bool:
+        return False
+
+    class AddCNOTPass(BasePass):
+        def run(self, circuit: Circuit, data: dict[str, Any] = {}) -> None:
+            circuit.append_gate(CNOTGate(), (0, 1))
+
+    circuit = Circuit(2)
+    dtd_pass = DoThenDecide(reject_always, AddCNOTPass())
+    dtd_pass.run(circuit)
+    assert circuit.num_operations == 0
+
+
+def test_dothendecide_list() -> None:
+    def accept_always(c1: Circuit, c2: Circuit) -> bool:
+        return True
+
+    class AddCNOTPass(BasePass):
+        def run(self, circuit: Circuit, data: dict[str, Any] = {}) -> None:
+            circuit.append_gate(CNOTGate(), (0, 1))
+
+    circuit = Circuit(2)
+    dtd_pass = DoThenDecide(accept_always, [AddCNOTPass(), AddCNOTPass()])
+    dtd_pass.run(circuit)
+    assert circuit.num_operations == 2
+    assert circuit.gate_set == {CNOTGate()}
diff --git a/tests/passes/control/test_dowhileloop.py b/tests/passes/control/test_dowhileloop.py
new file mode 100644
index 000000000..e69de29bb
diff --git a/tests/passes/mapping/test_apply.py b/tests/passes/mapping/test_apply.py
new file mode 100644
index 000000000..d47f43f4d
--- /dev/null
+++ b/tests/passes/mapping/test_apply.py
@@ -0,0 +1,20 @@
+from __future__ import annotations
+
+from typing import Any
+
+from bqskit.compiler import MachineModel
+from bqskit.ir import Circuit
+from bqskit.passes import ApplyPlacement
+from bqskit.passes import GreedyPlacementPass
+from bqskit.passes import SetModelPass
+
+
+def test_apply_placement(r3_qubit_circuit: Circuit) -> None:
+    model = MachineModel(6, [(0, 1), (1, 2), (2, 3), (2, 4), (2, 5), (3, 5)])
+    data: dict[str, Any] = {}
+    SetModelPass(model).run(r3_qubit_circuit, data)
+    GreedyPlacementPass().run(r3_qubit_circuit, data)
+    print(data)
+    ApplyPlacement().run(r3_qubit_circuit, data)
+    assert r3_qubit_circuit.num_qudits == 6
+    assert r3_qubit_circuit.active_qudits == [2, 3, 5]
diff --git a/tests/passes/mapping/test_sabre.py b/tests/passes/mapping/test_sabre.py
new file mode 100644
index 000000000..37b527d06
--- /dev/null
+++ b/tests/passes/mapping/test_sabre.py
@@ -0,0 +1,53 @@
+from __future__ import annotations
+
+from bqskit.compiler import CompilationTask
+from bqskit.compiler import Compiler
+from bqskit.compiler import MachineModel
+from bqskit.ir import Circuit
+from bqskit.ir.gates import CNOTGate
+from bqskit.ir.gates import U3Gate
+from bqskit.passes import GeneralizedSabreLayoutPass
+from bqskit.passes import GeneralizedSabreRoutingPass
+from bqskit.passes import GreedyPlacementPass
+from bqskit.passes import SetModelPass
+from bqskit.qis import PermutationMatrix
+from bqskit.qis.unitary.unitarymatrix import UnitaryMatrix
+
+
+def test_simple(compiler: Compiler) -> None:
+    model = MachineModel(
+        8, [(0, 1), (0, 2), (0, 3), (0, 4), (0, 5), (0, 6), (0, 7)],
+    )
+    circuit = Circuit(5)
+    for i in range(4):
+        circuit.append_gate(CNOTGate(), (4, i))
+        circuit.append_gate(
+            U3Gate(), 4, U3Gate.calc_params(
+                UnitaryMatrix.random(1),
+            ),
+        )
+        circuit.append_gate(
+            U3Gate(), i, U3Gate.calc_params(
+                UnitaryMatrix.random(1),
+            ),
+        )
+    circuit.append_gate(CNOTGate(), (0, 1))
+
+    in_utry = circuit.get_unitary()
+
+    task = CompilationTask(
+        circuit,
+        [
+            SetModelPass(model),
+            GreedyPlacementPass(),
+            GeneralizedSabreLayoutPass(),
+            GeneralizedSabreRoutingPass(),
+        ],
+    )
+
+    cc = compiler.compile(task)
+    pi = compiler.analyze(task, 'initial_mapping')
+    pf = compiler.analyze(task, 'final_mapping')
+    PI = PermutationMatrix.from_qubit_location(5, pi)
+    PF = PermutationMatrix.from_qubit_location(5, pf)
+    assert cc.get_unitary().get_distance_from(PF.T @ in_utry @ PI) < 1e-7
diff --git a/tests/passes/partitioning/test_quick.py b/tests/passes/partitioning/test_quick.py
new file mode 100644
index 000000000..8731a23d4
--- /dev/null
+++ b/tests/passes/partitioning/test_quick.py
@@ -0,0 +1,610 @@
+from __future__ import annotations
+
+from bqskit.ir.circuit import Circuit
+from bqskit.ir.gates import CircuitGate
+from bqskit.ir.gates.constant.cx import CNOTGate
+from bqskit.ir.gates.constant.unitary import ConstantUnitaryGate
+from bqskit.passes.partitioning.quick import QuickPartitioner
+from bqskit.qis.unitary.unitarymatrix import UnitaryMatrix
+
+
+class TestQuickPartitioner:
+
+    def test_run_r6(self, r6_qudit_circuit: Circuit) -> None:
+        utry = r6_qudit_circuit.get_unitary()
+        QuickPartitioner(3).run(r6_qudit_circuit, {})
+
+        assert all(
+            isinstance(op.gate, CircuitGate)
+            or len(op.location) > 3
+            for op in r6_qudit_circuit
+        )
+        assert r6_qudit_circuit.get_unitary() == utry
+        for cycle_index in range(r6_qudit_circuit.num_cycles):
+            assert not r6_qudit_circuit._is_cycle_idle(cycle_index)
+
+    def test_corner_case_1(self) -> None:
+        circuit = Circuit(6)
+
+        circuit.append_gate(
+            ConstantUnitaryGate(
+                UnitaryMatrix.random(2),
+            ), [
+                1, 4,
+            ],
+        )
+        circuit.append_gate(ConstantUnitaryGate(UnitaryMatrix.random(1)), [2])
+        circuit.append_gate(ConstantUnitaryGate(UnitaryMatrix.random(1)), [3])
+        circuit.append_gate(ConstantUnitaryGate(UnitaryMatrix.random(1)), [5])
+
+        circuit.append_gate(
+            ConstantUnitaryGate(
+                UnitaryMatrix.random(2),
+            ), [
+                5, 0,
+            ],
+        )
+        circuit.append_gate(ConstantUnitaryGate(UnitaryMatrix.random(1)), [1])
+        circuit.append_gate(ConstantUnitaryGate(UnitaryMatrix.random(1)), [3])
+
+        circuit.append_gate(
+            ConstantUnitaryGate(
+                UnitaryMatrix.random(4),
+            ), [
+                2, 0, 3, 5,
+            ],
+        )
+        circuit.append_gate(ConstantUnitaryGate(UnitaryMatrix.random(1)), [1])
+
+        circuit.append_gate(ConstantUnitaryGate(UnitaryMatrix.random(1)), [1])
+
+        utry = circuit.get_unitary()
+        QuickPartitioner(3).run(circuit, {})
+
+        assert all(
+            isinstance(op.gate, CircuitGate)
+            or len(op.location) > 3
+            for op in circuit
+        )
+        assert circuit.get_unitary() == utry
+        for cycle_index in range(circuit.num_cycles):
+            assert not circuit._is_cycle_idle(cycle_index)
+
+    def test_corner_case_2(self) -> None:
+        circuit = Circuit(6)
+
+        circuit.append_gate(ConstantUnitaryGate(UnitaryMatrix.random(1)), [0])
+        circuit.append_gate(ConstantUnitaryGate(UnitaryMatrix.random(1)), [1])
+        circuit.append_gate(ConstantUnitaryGate(UnitaryMatrix.random(1)), [5])
+
+        circuit.append_gate(
+            ConstantUnitaryGate(
+                UnitaryMatrix.random(2),
+            ), [
+                3, 0,
+            ],
+        )
+
+        circuit.append_gate(
+            ConstantUnitaryGate(
+                UnitaryMatrix.random(2),
+            ), [
+                5, 0,
+            ],
+        )
+        circuit.append_gate(ConstantUnitaryGate(UnitaryMatrix.random(1)), [3])
+
+        circuit.append_gate(
+            ConstantUnitaryGate(
+                UnitaryMatrix.random(4),
+            ), [
+                4, 0, 1, 2,
+            ],
+        )
+        circuit.append_gate(ConstantUnitaryGate(UnitaryMatrix.random(1)), [5])
+
+        circuit.append_gate(
+            ConstantUnitaryGate(
+                UnitaryMatrix.random(3),
+            ), [
+                5, 0, 1,
+            ],
+        )
+
+        circuit.append_gate(ConstantUnitaryGate(UnitaryMatrix.random(1)), [1])
+
+        utry = circuit.get_unitary()
+        QuickPartitioner(3).run(circuit, {})
+
+        assert all(
+            isinstance(op.gate, CircuitGate)
+            or len(op.location) > 3
+            for op in circuit
+        )
+        assert circuit.get_unitary() == utry
+        for cycle_index in range(circuit.num_cycles):
+            assert not circuit._is_cycle_idle(cycle_index)
+
+    def test_corner_case_3(self) -> None:
+        circuit = Circuit(6)
+        circuit.append_gate(ConstantUnitaryGate(UnitaryMatrix.random(1)), [0])
+        circuit.append_gate(
+            ConstantUnitaryGate(
+                UnitaryMatrix.random(2),
+            ), [
+                5, 1,
+            ],
+        )
+        circuit.append_gate(ConstantUnitaryGate(UnitaryMatrix.random(1)), [2])
+        circuit.append_gate(ConstantUnitaryGate(UnitaryMatrix.random(1)), [3])
+
+        circuit.append_gate(ConstantUnitaryGate(UnitaryMatrix.random(1)), [0])
+        circuit.append_gate(ConstantUnitaryGate(UnitaryMatrix.random(1)), [5])
+
+        circuit.append_gate(
+            ConstantUnitaryGate(
+                UnitaryMatrix.random(3),
+            ), [
+                5, 1, 3,
+            ],
+        )
+
+        circuit.append_gate(
+            ConstantUnitaryGate(
+                UnitaryMatrix.random(2),
+            ), [
+                4, 1,
+            ],
+        )
+        circuit.append_gate(ConstantUnitaryGate(UnitaryMatrix.random(1)), [3])
+
+        circuit.append_gate(
+            ConstantUnitaryGate(
+                UnitaryMatrix.random(2),
+            ), [
+                1, 3,
+            ],
+        )
+        utry = circuit.get_unitary()
+        QuickPartitioner(3).run(circuit, {})
+
+        assert all(
+            isinstance(op.gate, CircuitGate)
+            or len(op.location) > 3
+            for op in circuit
+        )
+        assert circuit.get_unitary() == utry
+        for cycle_index in range(circuit.num_cycles):
+            assert not circuit._is_cycle_idle(cycle_index)
+
+    def test_corner_case_4(self) -> None:
+        circuit = Circuit(6)
+
+        circuit.append_gate(
+            ConstantUnitaryGate(
+                UnitaryMatrix.random(2),
+            ), [
+                4, 0,
+            ],
+        )
+        circuit.append_gate(ConstantUnitaryGate(UnitaryMatrix.random(1)), [1])
+        circuit.append_gate(ConstantUnitaryGate(UnitaryMatrix.random(1)), [2])
+        circuit.append_gate(ConstantUnitaryGate(UnitaryMatrix.random(1)), [5])
+
+        circuit.append_gate(ConstantUnitaryGate(UnitaryMatrix.random(1)), [1])
+        circuit.append_gate(ConstantUnitaryGate(UnitaryMatrix.random(1)), [2])
+        circuit.append_gate(ConstantUnitaryGate(UnitaryMatrix.random(1)), [5])
+
+        circuit.append_gate(
+            ConstantUnitaryGate(
+                UnitaryMatrix.random(2),
+            ), [
+                2, 0,
+            ],
+        )
+        circuit.append_gate(
+            ConstantUnitaryGate(
+                UnitaryMatrix.random(2),
+            ), [
+                5, 3,
+            ],
+        )
+
+        circuit.append_gate(
+            ConstantUnitaryGate(
+                UnitaryMatrix.random(2),
+            ), [
+                1, 0,
+            ],
+        )
+
+        utry = circuit.get_unitary()
+        QuickPartitioner(3).run(circuit, {})
+
+        assert all(
+            isinstance(op.gate, CircuitGate)
+            or len(op.location) > 3
+            for op in circuit
+        )
+        assert circuit.get_unitary() == utry
+        for cycle_index in range(circuit.num_cycles):
+            assert not circuit._is_cycle_idle(cycle_index)
+
+    def test_corner_case_5(self) -> None:
+        circuit = Circuit(6)
+
+        circuit.append_gate(
+            ConstantUnitaryGate(
+                UnitaryMatrix.random(2),
+            ), [
+                4, 1,
+            ],
+        )
+        circuit.append_gate(ConstantUnitaryGate(UnitaryMatrix.random(1)), [2])
+        circuit.append_gate(ConstantUnitaryGate(UnitaryMatrix.random(1)), [5])
+
+        circuit.append_gate(ConstantUnitaryGate(UnitaryMatrix.random(1)), [1])
+        circuit.append_gate(ConstantUnitaryGate(UnitaryMatrix.random(1)), [4])
+
+        circuit.append_gate(
+            ConstantUnitaryGate(
+                UnitaryMatrix.random(4),
+            ), [
+                4, 1, 2, 3,
+            ],
+        )
+
+        circuit.append_gate(
+            ConstantUnitaryGate(
+                UnitaryMatrix.random(2),
+            ), [
+                4, 1,
+            ],
+        )
+
+        circuit.append_gate(
+            ConstantUnitaryGate(
+                UnitaryMatrix.random(2),
+            ), [
+                5, 1,
+            ],
+        )
+
+        circuit.append_gate(
+            ConstantUnitaryGate(
+                UnitaryMatrix.random(2),
+            ), [
+                2, 1,
+            ],
+        )
+
+        circuit.append_gate(ConstantUnitaryGate(UnitaryMatrix.random(1)), [2])
+
+        utry = circuit.get_unitary()
+        QuickPartitioner(3).run(circuit, {})
+
+        assert all(
+            isinstance(op.gate, CircuitGate)
+            or len(op.location) > 3
+            for op in circuit
+        )
+        assert circuit.get_unitary() == utry
+        for cycle_index in range(circuit.num_cycles):
+            assert not circuit._is_cycle_idle(cycle_index)
+
+    def test_corner_case_6(self) -> None:
+        circuit = Circuit(6)
+
+        circuit.append_gate(
+            ConstantUnitaryGate(
+                UnitaryMatrix.random(2),
+            ), [
+                1, 5,
+            ],
+        )
+        circuit.append_gate(ConstantUnitaryGate(UnitaryMatrix.random(1)), [2])
+
+        circuit.append_gate(
+            ConstantUnitaryGate(
+                UnitaryMatrix.random(3),
+            ), [
+                4, 0, 2,
+            ],
+        )
+        circuit.append_gate(ConstantUnitaryGate(UnitaryMatrix.random(1)), [5])
+
+        circuit.append_gate(ConstantUnitaryGate(UnitaryMatrix.random(1)), [0])
+        circuit.append_gate(
+            ConstantUnitaryGate(
+                UnitaryMatrix.random(2),
+            ), [
+                5, 1,
+            ],
+        )
+        circuit.append_gate(ConstantUnitaryGate(UnitaryMatrix.random(1)), [2])
+
+        circuit.append_gate(
+            ConstantUnitaryGate(
+                UnitaryMatrix.random(4),
+            ), [
+                0, 2, 3, 4,
+            ],
+        )
+        circuit.append_gate(ConstantUnitaryGate(UnitaryMatrix.random(1)), [1])
+
+        circuit.append_gate(
+            ConstantUnitaryGate(
+                UnitaryMatrix.random(2),
+            ), [
+                1, 5,
+            ],
+        )
+
+        utry = circuit.get_unitary()
+        QuickPartitioner(3).run(circuit, {})
+
+        assert all(
+            isinstance(op.gate, CircuitGate)
+            or len(op.location) > 3
+            for op in circuit
+        )
+        assert circuit.get_unitary() == utry
+        for cycle_index in range(circuit.num_cycles):
+            assert not circuit._is_cycle_idle(cycle_index)
+
+    def test_corner_case_7(self) -> None:
+        circuit = Circuit(6)
+
+        circuit.append_gate(
+            ConstantUnitaryGate(
+                UnitaryMatrix.random(2),
+            ), [
+                0, 1,
+            ],
+        )
+        circuit.append_gate(ConstantUnitaryGate(UnitaryMatrix.random(1)), [2])
+        circuit.append_gate(ConstantUnitaryGate(UnitaryMatrix.random(1)), [3])
+        circuit.append_gate(ConstantUnitaryGate(UnitaryMatrix.random(1)), [4])
+
+        circuit.append_gate(
+            ConstantUnitaryGate(
+                UnitaryMatrix.random(3),
+            ), [
+                0, 1, 3,
+            ],
+        )
+
+        circuit.append_gate(
+            ConstantUnitaryGate(
+                UnitaryMatrix.random(2),
+            ), [
+                5, 0,
+            ],
+        )
+
+        circuit.append_gate(
+            ConstantUnitaryGate(
+                UnitaryMatrix.random(2),
+            ), [
+                0, 1,
+            ],
+        )
+
+        circuit.append_gate(
+            ConstantUnitaryGate(
+                UnitaryMatrix.random(3),
+            ), [
+                4, 0, 1,
+            ],
+        )
+
+        circuit.append_gate(
+            ConstantUnitaryGate(
+                UnitaryMatrix.random(3),
+            ), [
+                1, 0, 3,
+            ],
+        )
+
+        circuit.append_gate(
+            ConstantUnitaryGate(
+                UnitaryMatrix.random(2),
+            ), [
+                4, 0,
+            ],
+        )
+
+        utry = circuit.get_unitary()
+        QuickPartitioner(3).run(circuit, {})
+
+        assert all(
+            isinstance(op.gate, CircuitGate)
+            or len(op.location) > 3
+            for op in circuit
+        )
+        assert circuit.get_unitary() == utry
+        for cycle_index in range(circuit.num_cycles):
+            assert not circuit._is_cycle_idle(cycle_index)
+
+    def test_corner_case_8(self) -> None:
+        circuit = Circuit(6)
+
+        circuit.append_gate(
+            ConstantUnitaryGate(
+                UnitaryMatrix.random(2),
+            ), [
+                5, 0,
+            ],
+        )
+        circuit.append_gate(ConstantUnitaryGate(UnitaryMatrix.random(1)), [1])
+        circuit.append_gate(ConstantUnitaryGate(UnitaryMatrix.random(1)), [3])
+        circuit.append_gate(ConstantUnitaryGate(UnitaryMatrix.random(1)), [4])
+
+        circuit.append_gate(
+            ConstantUnitaryGate(
+                UnitaryMatrix.random(3),
+            ), [
+                1, 0, 2,
+            ],
+        )
+        circuit.append_gate(ConstantUnitaryGate(UnitaryMatrix.random(1)), [3])
+
+        circuit.append_gate(
+            ConstantUnitaryGate(
+                UnitaryMatrix.random(3),
+            ), [
+                5, 0, 1,
+            ],
+        )
+
+        circuit.append_gate(
+            ConstantUnitaryGate(
+                UnitaryMatrix.random(2),
+            ), [
+                4, 0,
+            ],
+        )
+
+        circuit.append_gate(ConstantUnitaryGate(UnitaryMatrix.random(1)), [4])
+
+        circuit.append_gate(ConstantUnitaryGate(UnitaryMatrix.random(1)), [4])
+
+        utry = circuit.get_unitary()
+        QuickPartitioner(3).run(circuit, {})
+
+        assert all(
+            isinstance(op.gate, CircuitGate)
+            or len(op.location) > 3
+            for op in circuit
+        )
+        assert circuit.get_unitary() == utry
+        for cycle_index in range(circuit.num_cycles):
+            assert not circuit._is_cycle_idle(cycle_index)
+
+    def test_corner_case_9(self) -> None:
+        circuit = Circuit(6)
+
+        circuit.append_gate(ConstantUnitaryGate(UnitaryMatrix.random(1)), [0])
+        circuit.append_gate(
+            ConstantUnitaryGate(
+                UnitaryMatrix.random(3),
+            ), [
+                2, 1, 4,
+            ],
+        )
+        circuit.append_gate(ConstantUnitaryGate(UnitaryMatrix.random(1)), [3])
+
+        circuit.append_gate(
+            ConstantUnitaryGate(
+                UnitaryMatrix.random(2),
+            ), [
+                5, 1,
+            ],
+        )
+        circuit.append_gate(ConstantUnitaryGate(UnitaryMatrix.random(1)), [2])
+        circuit.append_gate(ConstantUnitaryGate(UnitaryMatrix.random(1)), [3])
+        circuit.append_gate(ConstantUnitaryGate(UnitaryMatrix.random(1)), [4])
+
+        circuit.append_gate(
+            ConstantUnitaryGate(
+                UnitaryMatrix.random(4),
+            ), [
+                5, 1, 2, 4,
+            ],
+        )
+        circuit.append_gate(ConstantUnitaryGate(UnitaryMatrix.random(1)), [3])
+
+        circuit.append_gate(
+            ConstantUnitaryGate(
+                UnitaryMatrix.random(2),
+            ), [
+                4, 1,
+            ],
+        )
+
+        utry = circuit.get_unitary()
+        QuickPartitioner(3).run(circuit, {})
+
+        assert all(
+            isinstance(op.gate, CircuitGate)
+            or len(op.location) > 3
+            for op in circuit
+        )
+        assert circuit.get_unitary() == utry
+        for cycle_index in range(circuit.num_cycles):
+            assert not circuit._is_cycle_idle(cycle_index)
+
+    def test_corner_case_10(self) -> None:
+        circuit = Circuit(6)
+
+        circuit.append_gate(ConstantUnitaryGate(UnitaryMatrix.random(1)), [0])
+        circuit.append_gate(ConstantUnitaryGate(UnitaryMatrix.random(1)), [1])
+        circuit.append_gate(ConstantUnitaryGate(UnitaryMatrix.random(1)), [4])
+        circuit.append_gate(ConstantUnitaryGate(UnitaryMatrix.random(1)), [5])
+
+        circuit.append_gate(
+            ConstantUnitaryGate(
+                UnitaryMatrix.random(2),
+            ), [
+                1, 2,
+            ],
+        )
+        circuit.append_gate(ConstantUnitaryGate(UnitaryMatrix.random(1)), [5])
+
+        circuit.append_gate(
+            ConstantUnitaryGate(
+                UnitaryMatrix.random(2),
+            ), [
+                4, 1,
+            ],
+        )
+
+        circuit.append_gate(
+            ConstantUnitaryGate(
+                UnitaryMatrix.random(2),
+            ), [
+                3, 1,
+            ],
+        )
+
+        circuit.append_gate(
+            ConstantUnitaryGate(
+                UnitaryMatrix.random(4),
+            ), [
+                1, 2, 3, 4,
+            ],
+        )
+
+        circuit.append_gate(ConstantUnitaryGate(UnitaryMatrix.random(1)), [3])
+
+        utry = circuit.get_unitary()
+        QuickPartitioner(3).run(circuit, {})
+
+        assert all(
+            isinstance(op.gate, CircuitGate)
+            or len(op.location) > 3
+            for op in circuit
+        )
+        assert circuit.get_unitary() == utry
+        for cycle_index in range(circuit.num_cycles):
+            assert not circuit._is_cycle_idle(cycle_index)
+
+    def test_corner_case_11(self) -> None:
+        circuit = Circuit(4)
+
+        circuit.append_gate(CNOTGate(), (0, 1))
+        circuit.append_gate(CNOTGate(), (2, 3))
+
+        circuit.append_gate(CNOTGate(), (0, 3))
+        circuit.append_gate(CNOTGate(), (1, 2))
+
+        utry = circuit.get_unitary()
+        QuickPartitioner(3).run(circuit, {})
+
+        assert all(
+            isinstance(op.gate, CircuitGate)
+            or len(op.location) > 3
+            for op in circuit
+        )
+        assert circuit.num_operations == 3
+        assert circuit.get_unitary() == utry
diff --git a/tests/passes/processing/test_substitute.py b/tests/passes/processing/test_substitute.py
index 7b02b71aa..208740101 100644
--- a/tests/passes/processing/test_substitute.py
+++ b/tests/passes/processing/test_substitute.py
@@ -9,10 +9,6 @@
 from bqskit.qis import UnitaryMatrix
 
 
-def is_variable(op: Operation) -> bool:
-    return isinstance(op.gate, VariableUnitaryGate)
-
-
 class TestSubstitute:
 
     def test_small_qubit(self) -> None:
@@ -22,6 +18,8 @@ def test_small_qubit(self) -> None:
         circuit.instantiate(utry)
         assert circuit.get_unitary().get_distance_from(utry) < 1e-5
 
+        def is_variable(op: Operation) -> bool:
+            return isinstance(op.gate, VariableUnitaryGate)
         substitute = SubstitutePass(is_variable, VariableUnitaryGate(1))
         substitute.run(circuit)
         dist = circuit.get_unitary().get_distance_from(utry)
@@ -29,16 +27,17 @@ def test_small_qubit(self) -> None:
         assert circuit.num_operations == 1
         assert circuit[0, 0].num_qudits == 1
 
-    def test_small_qubit_with_compiler(self) -> None:
+    def test_small_qubit_with_compiler(self, compiler: Compiler) -> None:
         utry = UnitaryMatrix.identity(4)
         circuit = Circuit(2)
         circuit.append_gate(VariableUnitaryGate(2), [0, 1])
         circuit.instantiate(utry)
         assert circuit.get_unitary().get_distance_from(utry) < 1e-5
 
+        def is_variable(op: Operation) -> bool:
+            return isinstance(op.gate, VariableUnitaryGate)
         substitute = SubstitutePass(is_variable, VariableUnitaryGate(1))
-        with Compiler() as compiler:
-            circuit = compiler.compile(CompilationTask(circuit, [substitute]))
+        circuit = compiler.compile(CompilationTask(circuit, [substitute]))
         dist = circuit.get_unitary().get_distance_from(utry)
         assert dist <= 1e-5
         assert circuit.num_operations == 1
diff --git a/tests/passes/synthesis/test_leap.py b/tests/passes/synthesis/test_leap.py
index 559e89161..a48e78516 100644
--- a/tests/passes/synthesis/test_leap.py
+++ b/tests/passes/synthesis/test_leap.py
@@ -20,12 +20,11 @@ def test_small_qubit(self) -> None:
         dist = circuit.get_unitary().get_distance_from(utry)
         assert dist <= 1e-5
 
-    def test_small_qubit_with_compiler(self) -> None:
+    def test_small_qubit_with_compiler(self, compiler: Compiler) -> None:
         utry = UnitaryMatrix.random(2)
         circuit = Circuit.from_unitary(utry)
         leap = LEAPSynthesisPass()
-        with Compiler() as compiler:
-            circuit = compiler.compile(CompilationTask(circuit, [leap]))
+        circuit = compiler.compile(CompilationTask(circuit, [leap]))
         dist = circuit.get_unitary().get_distance_from(utry)
         assert dist <= 1e-5
 
diff --git a/tests/passes/synthesis/test_qfast.py b/tests/passes/synthesis/test_qfast.py
new file mode 100644
index 000000000..e317d83ba
--- /dev/null
+++ b/tests/passes/synthesis/test_qfast.py
@@ -0,0 +1,62 @@
+from __future__ import annotations
+
+import pytest
+
+from bqskit.compiler import CompilationTask
+from bqskit.compiler import Compiler
+from bqskit.ir.circuit import Circuit
+from bqskit.ir.gates import CircuitGate
+from bqskit.ir.gates.constant.cx import CNOTGate
+from bqskit.ir.gates.parameterized.u3 import U3Gate
+from bqskit.passes import QFASTDecompositionPass
+from bqskit.qis import UnitaryMatrix
+
+
+class TestQFAST:
+
+    def test_small_qubit(self) -> None:
+        utry = UnitaryMatrix.random(2)
+        circuit = Circuit.from_unitary(utry)
+        qfast = QFASTDecompositionPass()
+        qfast.run(circuit)
+        dist = circuit.get_unitary().get_distance_from(utry)
+        assert dist <= 1e-5
+
+    def test_small_qubit_with_compiler(self, compiler: Compiler) -> None:
+        utry = UnitaryMatrix.random(2)
+        circuit = Circuit.from_unitary(utry)
+        qfast = QFASTDecompositionPass()
+        circuit = compiler.compile(CompilationTask(circuit, [qfast]))
+        dist = circuit.get_unitary().get_distance_from(utry)
+        assert dist <= 1e-5
+
+    def test_3_qubit(self) -> None:
+        utry = UnitaryMatrix.random(3)
+        circuit = Circuit.from_unitary(utry)
+        qfast = QFASTDecompositionPass()
+        qfast.run(circuit)
+        dist = circuit.get_unitary().get_distance_from(utry)
+        assert dist <= 1e-5
+
+    def test_3_qubit_with_compiler(self, compiler: Compiler) -> None:
+        utry = UnitaryMatrix.random(3)
+        circuit = Circuit.from_unitary(utry)
+        qfast = QFASTDecompositionPass()
+        circuit = compiler.compile(CompilationTask(circuit, [qfast]))
+        dist = circuit.get_unitary().get_distance_from(utry)
+        assert dist <= 1e-5
+
+    @pytest.mark.skip(reason='Issue in bqskitrs supporting CircuitGates.')
+    def test_3_qubit_with_cnot_block(self) -> None:
+        circuit = Circuit(2)
+        circuit.append_gate(CNOTGate(), (0, 1))
+        circuit.append_gate(U3Gate(), (0,))
+        circuit.append_gate(U3Gate(), (1,))
+        cg = CircuitGate(circuit)
+
+        utry = UnitaryMatrix.random(3)
+        circuit = Circuit.from_unitary(utry)
+        qfast = QFASTDecompositionPass(cg)
+        qfast.run(circuit)
+        dist = circuit.get_unitary().get_distance_from(utry)
+        assert dist <= 1e-5
diff --git a/tests/passes/synthesis/test_qpredict.py b/tests/passes/synthesis/test_qpredict.py
new file mode 100644
index 000000000..2e3c22fc5
--- /dev/null
+++ b/tests/passes/synthesis/test_qpredict.py
@@ -0,0 +1,42 @@
+from __future__ import annotations
+
+from bqskit.compiler import CompilationTask
+from bqskit.compiler import Compiler
+from bqskit.ir.circuit import Circuit
+from bqskit.passes import QPredictDecompositionPass
+from bqskit.qis import UnitaryMatrix
+
+
+class TestQPredict:
+
+    def test_small_qubit(self) -> None:
+        utry = UnitaryMatrix.random(2)
+        circuit = Circuit.from_unitary(utry)
+        qpredict = QPredictDecompositionPass()
+        qpredict.run(circuit)
+        dist = circuit.get_unitary().get_distance_from(utry)
+        assert dist <= 1e-5
+
+    def test_small_qubit_with_compiler(self, compiler: Compiler) -> None:
+        utry = UnitaryMatrix.random(2)
+        circuit = Circuit.from_unitary(utry)
+        qpredict = QPredictDecompositionPass()
+        circuit = compiler.compile(CompilationTask(circuit, [qpredict]))
+        dist = circuit.get_unitary().get_distance_from(utry)
+        assert dist <= 1e-5
+
+    def test_3_qubit(self) -> None:
+        utry = UnitaryMatrix.random(3)
+        circuit = Circuit.from_unitary(utry)
+        qpredict = QPredictDecompositionPass()
+        qpredict.run(circuit)
+        dist = circuit.get_unitary().get_distance_from(utry)
+        assert dist <= 1e-4
+
+    def test_3_qubit_with_compiler(self, compiler: Compiler) -> None:
+        utry = UnitaryMatrix.random(3)
+        circuit = Circuit.from_unitary(utry)
+        qpredict = QPredictDecompositionPass()
+        circuit = compiler.compile(CompilationTask(circuit, [qpredict]))
+        dist = circuit.get_unitary().get_distance_from(utry)
+        assert dist <= 1e-4
diff --git a/tests/passes/synthesis/test_qsearch.py b/tests/passes/synthesis/test_qsearch.py
index ee94d1a08..4fa5c8b5f 100644
--- a/tests/passes/synthesis/test_qsearch.py
+++ b/tests/passes/synthesis/test_qsearch.py
@@ -20,12 +20,11 @@ def test_small_qubit(self) -> None:
         dist = circuit.get_unitary().get_distance_from(utry)
         assert dist <= 1e-5
 
-    def test_small_qubit_with_compiler(self) -> None:
+    def test_small_qubit_with_compiler(self, compiler: Compiler) -> None:
         utry = UnitaryMatrix.random(2)
         circuit = Circuit.from_unitary(utry)
         qsearch = QSearchSynthesisPass()
-        with Compiler() as compiler:
-            circuit = compiler.compile(CompilationTask(circuit, [qsearch]))
+        circuit = compiler.compile(CompilationTask(circuit, [qsearch]))
         dist = circuit.get_unitary().get_distance_from(utry)
         assert dist <= 1e-5
 
diff --git a/tests/passes/util/test_extend.py b/tests/passes/util/test_extend.py
new file mode 100644
index 000000000..24f07e17f
--- /dev/null
+++ b/tests/passes/util/test_extend.py
@@ -0,0 +1,26 @@
+from __future__ import annotations
+
+from bqskit.ir import Circuit
+from bqskit.ir.gates import CircuitGate
+from bqskit.passes import ExtendBlockSizePass
+from bqskit.passes import QuickPartitioner
+
+
+def test_extend_3(r6_qudit_circuit: Circuit) -> None:
+    in_utry = r6_qudit_circuit.get_unitary()
+    QuickPartitioner(3).run(r6_qudit_circuit)
+    ExtendBlockSizePass(3).run(r6_qudit_circuit)
+    out_utry = r6_qudit_circuit.get_unitary()
+    assert all(isinstance(op.gate, CircuitGate) for op in r6_qudit_circuit)
+    assert all(op.num_qudits == 3 for op in r6_qudit_circuit)
+    assert in_utry == out_utry
+
+
+def test_extend_4(r6_qudit_circuit: Circuit) -> None:
+    in_utry = r6_qudit_circuit.get_unitary()
+    QuickPartitioner(3).run(r6_qudit_circuit)
+    ExtendBlockSizePass(4).run(r6_qudit_circuit)
+    out_utry = r6_qudit_circuit.get_unitary()
+    assert all(isinstance(op.gate, CircuitGate) for op in r6_qudit_circuit)
+    assert all(op.num_qudits == 4 for op in r6_qudit_circuit)
+    assert in_utry == out_utry
diff --git a/tests/qis/test_graph.py b/tests/qis/test_graph.py
index dff2abb8c..940215beb 100644
--- a/tests/qis/test_graph.py
+++ b/tests/qis/test_graph.py
@@ -39,7 +39,7 @@ def test_invalid(self) -> None:
         coupling_graph = CouplingGraph({(0, 1), (1, 2), (2, 3)})
 
         with pytest.raises(TypeError):
-            coupling_graph.get_subgraphs_of_size('a')
+            coupling_graph.get_subgraphs_of_size('a')  # type: ignore
 
 
 class TestMachineGetSubgraph:
@@ -58,7 +58,7 @@ def test_2(self) -> None:
 
         assert len(l) == 2
         assert (0, 1) in l
-        assert (0, 3) in l
+        assert (0, 2) in l
 
     def test_invalid(self) -> None:
         coupling_graph = CouplingGraph({(0, 1), (1, 2), (2, 3)})
diff --git a/tests/qis/test_permutation.py b/tests/qis/test_permutation.py
index e7ff8e159..b42d04305 100644
--- a/tests/qis/test_permutation.py
+++ b/tests/qis/test_permutation.py
@@ -1,105 +1,28 @@
 from __future__ import annotations
 
-import numpy as np
-import pytest
-import scipy as sp
-from bqskitrs import swap_bit
-
+from bqskit.ir.gates import SwapGate
 from bqskit.qis.permutation import PermutationMatrix
+from bqskit.qis.unitary import UnitaryMatrix
 
 
-class TestSwapBit:
-
-    def test_swap_bit1(self) -> None:
-        assert swap_bit(0, 1, 0) == 0
-        assert swap_bit(0, 1, 1) == 2
-        assert swap_bit(0, 1, 2) == 1
-        assert swap_bit(0, 1, 3) == 3
-        assert swap_bit(0, 1, 4) == 4
-        assert swap_bit(0, 1, 5) == 6
-        assert swap_bit(0, 1, 6) == 5
-        assert swap_bit(0, 1, 7) == 7
-        assert swap_bit(0, 1, 8) == 8
-
-    def test_swap_bit2(self) -> None:
-        assert swap_bit(1, 2, 0) == 0
-        assert swap_bit(1, 2, 1) == 1
-        assert swap_bit(1, 2, 2) == 4
-        assert swap_bit(1, 2, 3) == 5
-        assert swap_bit(1, 2, 4) == 2
-        assert swap_bit(1, 2, 5) == 3
-        assert swap_bit(1, 2, 6) == 6
-        assert swap_bit(1, 2, 7) == 7
-        assert swap_bit(1, 2, 8) == 8
-
-    def test_swap_bit3(self) -> None:
-        for i in range(10):
-            for j in range(10):
-                assert swap_bit(i, j, 2112) == swap_bit(j, i, 2112)
-
-    def test_swap_bit_invalid(self) -> None:
-        with pytest.raises(TypeError):
-            swap_bit('a', 1, 0)
-
-        with pytest.raises(TypeError):
-            swap_bit(0, 'a', 0)
-
-        with pytest.raises(TypeError):
-            swap_bit(0, 1, 'a')
-
-
-class TestFromQubitLocation:
-
-    def test_1(self) -> None:
-        swap_012 = np.array([
-            [1, 0, 0, 0],
-            [0, 0, 1, 0],
-            [0, 1, 0, 0],
-            [0, 0, 0, 1],
-        ])
-
-        perm = PermutationMatrix.from_qubit_location(2, (1, 0))
-        assert np.allclose(perm, swap_012)
-
-        perm = PermutationMatrix.from_qubit_location(2, (1,))
-        assert np.allclose(perm, swap_012)
-
-        perm = PermutationMatrix.from_qubit_location(2, (0, 1))
-        assert np.allclose(perm, np.identity(4))
-
-    def test_calc_permutation_matrix_big(self) -> None:
-        I = np.identity(2, dtype=np.complex128)
-        II = np.kron(I, I)
-        IIII = np.kron(II, II)
-        X = np.array([[0, 1], [1, 0]], dtype=np.complex128)
-        XX = np.kron(X, X)
-        XXI = np.kron(XX, I)
-        IXX = np.kron(I, XX)
-        IIXX = np.kron(I, IXX)
-        IX = np.kron(I, X)
-        IXIX = np.kron(IX, IX)
-        XXXX = np.kron(XX, XX)
-        IXIXIXIX = np.kron(IXIX, IXIX)
+def test_permutation_1() -> None:
+    swap = SwapGate().get_unitary()
+    id = UnitaryMatrix.identity(2)
+    correct = id.otimes(swap)
+    perm = PermutationMatrix.from_qubit_location(3, (0, 2, 1))
+    assert perm == correct
 
-        U0 = sp.linalg.expm(-1j * IXX)
-        U1 = sp.linalg.expm(-1j * XXI)
-        P = PermutationMatrix.from_qubit_location(3, (1, 2))
-        assert np.allclose(U0, P @ U1 @ P.T)
 
-        U0 = sp.linalg.expm(-1j * IIXX)
-        U1 = sp.linalg.expm(-1j * IXIX)
-        P = PermutationMatrix.from_qubit_location(4, (0, 2))
-        assert np.allclose(U0, P @ U1 @ P.T)
+def test_permutation_2() -> None:
+    swap = SwapGate().get_unitary()
+    correct = swap.otimes(swap)
+    perm = PermutationMatrix.from_qubit_location(4, (1, 0, 3, 2))
+    assert perm == correct
 
-        U0 = sp.linalg.expm(-1j * IXIXIXIX)
-        U1 = sp.linalg.expm(-1j * np.kron(XXXX, IIII))
-        P = PermutationMatrix.from_qubit_location(8, (1, 3, 5, 7))
-        assert np.allclose(U0, P @ U1 @ P.T)
 
-    def test_calc_permutation_matrix_invalid(self) -> None:
-        with pytest.raises(TypeError):
-            PermutationMatrix.from_qubit_location(4, 'a')
-        with pytest.raises(TypeError):
-            PermutationMatrix.from_qubit_location(4, ('a'))
-        with pytest.raises(TypeError):
-            PermutationMatrix.from_qubit_location('a', (0, 1))
+def test_permutation_3() -> None:
+    swap = SwapGate().get_unitary()
+    id = UnitaryMatrix.identity(2)
+    correct = id.otimes(swap, id) @ swap.otimes(swap)
+    perm = PermutationMatrix.from_qubit_location(4, (1, 3, 0, 2))
+    assert perm == correct
diff --git a/tests/test_conftest.py b/tests/test_conftest.py
index 5f964b9e3..c92a7df9e 100644
--- a/tests/test_conftest.py
+++ b/tests/test_conftest.py
@@ -13,29 +13,6 @@
     r3_qubit_constant_circuit
     r3_qutrit_circuit
     r6_qudit_circuit
-
-The BQSKit root conftest also defines several type-categorized value
-fixtures that need to be verified:
-    a_str
-    not_a_str
-    an_int
-    not_an_int
-    a_float
-    not_a_float
-    a_complex
-    not_a_complex
-    a_bool
-    not_a_bool
-    a_seq_str
-    not_a_seq_str
-    a_seq_int
-    not_a_seq_int
-    a_seq_float
-    not_a_seq_float
-    a_seq_complex
-    not_a_seq_complex
-    a_seq_bool
-    not_a_seq_bool
 """
 from __future__ import annotations
 
@@ -45,10 +22,6 @@
 
 from bqskit.ir.circuit import Circuit
 from bqskit.qis.unitary import UnitaryMatrix
-from bqskit.utils.typing import is_complex
-from bqskit.utils.typing import is_integer
-from bqskit.utils.typing import is_numeric
-from bqskit.utils.typing import is_sequence
 
 
 class TestGenRandomUtryNp:
@@ -202,94 +175,3 @@ def test_r6_radix(self, r6_qudit_circuit: Circuit) -> None:
 
     def test_r6_depth(self, r6_qudit_circuit: Circuit) -> None:
         assert r6_qudit_circuit.num_operations == 10
-
-
-class TestTypedValues:
-    """This tests the type-categorized value fixtures."""
-
-    def test_a_str(self, a_str: Any) -> None:
-        assert isinstance(a_str, str)
-
-    def test_not_a_str(self, not_a_str: Any) -> None:
-        assert not isinstance(not_a_str, str)
-
-    def test_an_int(self, an_int: Any) -> None:
-        assert is_integer(an_int)
-
-    def test_not_an_int(self, not_an_int: Any) -> None:
-        assert not is_integer(not_an_int)
-
-    def test_a_float(self, a_float: Any) -> None:
-        assert (
-            is_numeric(a_float)
-            and not is_integer(a_float)
-        )
-
-    def test_not_a_float(self, not_a_float: Any) -> None:
-        assert (
-            not is_numeric(not_a_float)
-            or is_integer(not_a_float)
-            or is_complex(not_a_float)
-        )
-
-    def test_a_complex(self, a_complex: Any) -> None:
-        assert is_complex(a_complex)
-
-    def test_a_bool(self, a_bool: Any) -> None:
-        assert isinstance(a_bool, (bool, np.bool_))
-
-    def test_not_a_bool(self, not_a_bool: Any) -> None:
-        assert not isinstance(not_a_bool, (bool, np.bool_))
-
-    def test_a_seq_str(self, a_seq_str: Any) -> None:
-        assert is_sequence(a_seq_str)
-        assert len(a_seq_str) >= 0
-        assert all(isinstance(s, str) for s in a_seq_str)
-
-    def test_not_a_seq_str(self, not_a_seq_str: Any) -> None:
-        assert (
-            not is_sequence(not_a_seq_str)
-            or isinstance(not_a_seq_str, str)
-            or any(not isinstance(s, str) for s in not_a_seq_str)
-        )
-
-    def test_a_seq_int(self, a_seq_int: Any) -> None:
-        assert is_sequence(a_seq_int)
-        assert len(a_seq_int) >= 0
-        assert all(is_integer(i) for i in a_seq_int)
-
-    def test_not_a_seq_int(self, not_a_seq_int: Any) -> None:
-        assert (
-            not is_sequence(not_a_seq_int)
-            or isinstance(not_a_seq_int, str)
-            or any(not is_integer(i) for i in not_a_seq_int)
-        )
-
-    def test_not_a_seq_float(self, not_a_seq_float: Any) -> None:
-        assert (
-            not is_sequence(not_a_seq_float)
-            or isinstance(not_a_seq_float, str)
-            or any(
-                not is_numeric(f)
-                or is_integer(f)
-                or is_complex(f)
-                for f in not_a_seq_float
-            )
-        )
-
-    def test_a_seq_complex(self, a_seq_complex: Any) -> None:
-        assert is_sequence(a_seq_complex)
-        assert len(a_seq_complex) >= 0
-        assert all(is_complex(c) for c in a_seq_complex)
-
-    def test_a_seq_bool(self, a_seq_bool: Any) -> None:
-        assert is_sequence(a_seq_bool)
-        assert len(a_seq_bool) >= 0
-        assert all(isinstance(b, (bool, np.bool_)) for b in a_seq_bool)
-
-    def test_not_a_seq_bool(self, not_a_seq_bool: Any) -> None:
-        assert (
-            not is_sequence(not_a_seq_bool)
-            or isinstance(not_a_seq_bool, str)
-            or any(not isinstance(b, (bool, np.bool_)) for b in not_a_seq_bool)
-        )