Repository: BQSKit/bqskit (language: python)

Issue:
Basic change to avoid passing in gate to replace
This PR removes the need to pass in what gate should be replaced in the rebase pass and replaces every 2-qubit gate in the circuit.


Discussion / hints:

Fix patch:
diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml
index 89b6f9898..bcdf16e4f 100644
--- a/.pre-commit-config.yaml
+++ b/.pre-commit-config.yaml
@@ -20,8 +20,6 @@ repos:
     -   id: fix-byte-order-marker
     -   id: fix-encoding-pragma
         args: ['--remove']
-    -   id: no-commit-to-branch
-        args: [--branch, master]
 -   repo: https://github.com/PyCQA/docformatter
     rev: v1.7.5
     hooks:
@@ -41,7 +39,7 @@ repos:
             - --ignore=E731
         exclude: 'tests/ext.*'
 -   repo: https://github.com/asottile/pyupgrade
-    rev: v3.9.0
+    rev: v3.10.1
     hooks:
     -   id: pyupgrade
         args:
@@ -56,7 +54,7 @@ repos:
             - --py37-plus
         exclude: 'tests/ext.*'
 -   repo: https://github.com/asottile/add-trailing-comma
-    rev: v3.0.0
+    rev: v3.0.1
     hooks:
     -   id: add-trailing-comma
         args:
@@ -68,13 +66,13 @@ repos:
       args:
         - --in-place
 -   repo: https://github.com/pre-commit/mirrors-mypy
-    rev: v1.4.1
+    rev: v1.5.0
     hooks:
     - id: mypy
       exclude: tests/qis/test_pauli.py
       additional_dependencies: ["numpy>=1.21"]
 -   repo: https://github.com/PyCQA/flake8
-    rev: 6.0.0
+    rev: 6.1.0
     hooks:
     -   id: flake8
         args:
diff --git a/bqskit/compiler/basepass.py b/bqskit/compiler/basepass.py
index 472f2cffb..413625f4e 100644
--- a/bqskit/compiler/basepass.py
+++ b/bqskit/compiler/basepass.py
@@ -53,6 +53,10 @@ async def run(self, circuit: Circuit, data: PassData) -> None:
               This is because it will be potentially run in parallel.
         """
 
+    def __str__(self) -> str:
+        """Return a string representation of the pass."""
+        return self.name
+
     @staticmethod
     def get_model(_: Any, data: PassData) -> MachineModel:
         """
diff --git a/bqskit/compiler/compile.py b/bqskit/compiler/compile.py
index 22be718ab..94686b809 100644
--- a/bqskit/compiler/compile.py
+++ b/bqskit/compiler/compile.py
@@ -1,42 +1,46 @@
 """This module defines a standard `compile` function using BQSKit."""
 from __future__ import annotations
 
-import functools
 import logging
 import warnings
 from typing import Any
-from typing import Callable
 from typing import Literal
 from typing import overload
+from typing import Sequence
 from typing import TYPE_CHECKING
+from typing import Union
 
 import numpy as np
 
 from bqskit.compiler.compiler import Compiler
 from bqskit.compiler.machine import MachineModel
+from bqskit.compiler.passdata import PassData
 from bqskit.compiler.workflow import Workflow
+from bqskit.compiler.workflow import WorkflowLike
 from bqskit.ir.circuit import Circuit
-from bqskit.ir.gate import Gate
 from bqskit.ir.gates.barrier import BarrierPlaceholder
-from bqskit.ir.gates.circuitgate import CircuitGate
-from bqskit.ir.gates.constant.swap import SwapGate
 from bqskit.ir.gates.constant.sx import SqrtXGate
 from bqskit.ir.gates.measure import MeasurementPlaceholder
 from bqskit.ir.gates.parameterized.rz import RZGate
 from bqskit.ir.gates.parameterized.u3 import U3Gate
-from bqskit.ir.gates.parameterized.u8 import U8Gate
-from bqskit.ir.gates.parameterized.unitary import VariableUnitaryGate
 from bqskit.ir.operation import Operation
 from bqskit.ir.opt import HilbertSchmidtCostGenerator
 from bqskit.ir.opt import ScipyMinimizer
 from bqskit.ir.opt.minimizers.lbfgs import LBFGSMinimizer
 from bqskit.passes.control.foreach import ForEachBlockPass
+from bqskit.passes.control.foreach import gen_replace_filter
+from bqskit.passes.control.foreach import ReplaceFilterFn
 from bqskit.passes.control.ifthenelse import IfThenElsePass
 from bqskit.passes.control.predicates.change import ChangePredicate
 from bqskit.passes.control.predicates.count import GateCountPredicate
+from bqskit.passes.control.predicates.many import ManyQuditGatesPredicate
 from bqskit.passes.control.predicates.multi import MultiPhysicalPredicate
 from bqskit.passes.control.predicates.notpredicate import NotPredicate
+from bqskit.passes.control.predicates.single import AllConstantSingleQuditGates
+from bqskit.passes.control.predicates.single import HasGeneralSingleQuditGate
+from bqskit.passes.control.predicates.single import NoSingleQuditGatesInModel
 from bqskit.passes.control.predicates.single import SinglePhysicalPredicate
+from bqskit.passes.control.predicates.single import ZXGatePredicate
 from bqskit.passes.control.predicates.width import WidthPredicate
 from bqskit.passes.control.whileloop import WhileLoopPass
 from bqskit.passes.group import PassGroup
@@ -54,8 +58,9 @@
 from bqskit.passes.noop import NOOPPass
 from bqskit.passes.partitioning.quick import QuickPartitioner
 from bqskit.passes.partitioning.single import GroupSingleQuditGatePass
-from bqskit.passes.processing.rebase import Rebase2QuditGatePass
 from bqskit.passes.processing.scan import ScanningGateRemovalPass
+from bqskit.passes.retarget.auto import AutoRebase2QuditGatePass
+from bqskit.passes.retarget.general import GeneralSQDecomposition
 from bqskit.passes.rules.u3 import U3Decomposition
 from bqskit.passes.rules.zxzxz import ZXZXZDecomposition
 from bqskit.passes.search.generators.single import SingleQuditLayerGenerator
@@ -71,26 +76,30 @@
 from bqskit.passes.util.log import LogPass
 from bqskit.passes.util.random import SetRandomSeedPass
 from bqskit.passes.util.unfold import UnfoldPass
+from bqskit.qis.state import StateLike
 from bqskit.qis.state.state import StateVector
 from bqskit.qis.state.system import StateSystem
 from bqskit.qis.state.system import StateSystemLike
+from bqskit.qis.unitary import UnitaryLike
 from bqskit.qis.unitary.unitarymatrix import UnitaryMatrix
 from bqskit.utils.typing import is_integer
+from bqskit.utils.typing import is_iterable
 from bqskit.utils.typing import is_real_number
 
-
 if TYPE_CHECKING:
-    from bqskit.qis.unitary import UnitaryLike
-    from bqskit.qis.state import StateLike
     from bqskit.compiler.basepass import BasePass
 
 
 _logger = logging.getLogger(__name__)
 
 
+CompilationInputLike = Union[Circuit, UnitaryLike, StateLike, StateSystemLike]
+CompilationInput = Union[Circuit, UnitaryMatrix, StateVector, StateSystem]
+
+
 @overload
 def compile(
-    input: Circuit | UnitaryLike | StateLike | StateSystemLike,
+    input: CompilationInputLike,
     model: MachineModel | None = ...,
     *,
     with_mapping: Literal[False] = ...,
@@ -108,7 +117,25 @@ def compile(
 
 @overload
 def compile(
-    input: Circuit | UnitaryLike | StateLike | StateSystemLike,
+    input: Sequence[CompilationInput],
+    model: MachineModel | None = ...,
+    *,
+    with_mapping: Literal[False] = ...,
+    optimization_level: int = ...,
+    max_synthesis_size: int = ...,
+    synthesis_epsilon: float = ...,
+    error_threshold: float | None = ...,
+    error_sim_size: int = ...,
+    compiler: Compiler | None = ...,
+    seed: int | None = ...,
+    **compiler_kwargs: Any,
+) -> list[Circuit]:
+    ...
+
+
+@overload
+def compile(
+    input: CompilationInputLike,
     model: MachineModel | None = ...,
     *,
     with_mapping: Literal[True],
@@ -126,7 +153,25 @@ def compile(
 
 @overload
 def compile(
-    input: Circuit | UnitaryLike | StateLike | StateSystemLike,
+    input: Sequence[CompilationInput],
+    model: MachineModel | None = ...,
+    *,
+    with_mapping: Literal[True],
+    optimization_level: int = ...,
+    max_synthesis_size: int = ...,
+    synthesis_epsilon: float = ...,
+    error_threshold: float | None = ...,
+    error_sim_size: int = ...,
+    compiler: Compiler | None = ...,
+    seed: int | None = ...,
+    **compiler_kwargs: Any,
+) -> list[tuple[Circuit, tuple[int, ...], tuple[int, ...]]]:
+    ...
+
+
+@overload
+def compile(
+    input: CompilationInputLike,
     model: MachineModel | None = ...,
     *,
     with_mapping: bool,
@@ -142,8 +187,26 @@ def compile(
     ...
 
 
+@overload
+def compile(
+    input: Sequence[CompilationInput],
+    model: MachineModel | None = ...,
+    *,
+    with_mapping: bool,
+    optimization_level: int = ...,
+    max_synthesis_size: int = ...,
+    synthesis_epsilon: float = ...,
+    error_threshold: float | None = ...,
+    error_sim_size: int = ...,
+    compiler: Compiler | None = ...,
+    seed: int | None = ...,
+    **compiler_kwargs: Any,
+) -> list[Circuit] | list[tuple[Circuit, tuple[int, ...], tuple[int, ...]]]:
+    ...
+
+
 def compile(
-    input: Circuit | UnitaryLike | StateLike | StateSystemLike,
+    input: CompilationInputLike | Sequence[CompilationInput],
     model: MachineModel | None = None,
     optimization_level: int = 1,
     max_synthesis_size: int = 3,
@@ -154,12 +217,20 @@ def compile(
     seed: int | None = None,
     with_mapping: bool = False,
     **compiler_kwargs: Any,
-) -> Circuit | tuple[Circuit, tuple[int, ...], tuple[int, ...]]:
+) -> (
+    Circuit
+    | tuple[Circuit, tuple[int, ...], tuple[int, ...]]
+    | list[Circuit]
+    | list[tuple[Circuit, tuple[int, ...], tuple[int, ...]]]
+):
     """
     Compile a circuit, unitary, or state with a standard workflow.
 
     Args:
-        input (Circuit | UnitaryLike | StateLike): The input to compile.
+        input (CompilationInputLike | Sequence[CompilationInput]): The input
+            or inputs to compile. If a single input is given, a single
+            circuit will be returned. If an iterable of inputs is given,
+            a list of circuits will be returned.
 
         model (MachineModel | None): A model of the target machine.
             Defaults to an all-to-all connected hardware with CNOTs and U3s
@@ -192,7 +263,9 @@ def compile(
         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)
+            becomes more accurate. This setting is ignored with direct
+            synthesis compilations, i.e., when a state, system, or unitary
+            are given as input. (Default: 8)
 
         compiler (Compiler | None): Pass a :class:`Compiler` to prevent
             creating one. Save on startup time by passing a compiler in
@@ -223,73 +296,62 @@ def 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)
+        >>> from bqskit import compile
+        >>> from bqskit.qis.state import StateVector
+        >>> state = StateVector.random(3)
+        >>> compiled_circuit = compile(state)
+        >>> compiled_circuit.save('output.qasm')
 
-        elif StateVector.is_pure_state(input):
-            input = StateVector(input)
+        >>> from bqskit import compile
+        >>> from bqskit.qis.unitary import UnitaryMatrix
+        >>> utry = UnitaryMatrix.random(3)
+        >>> compiled_circuit = compile(utry)
+        >>> compiled_circuit.save('output.qasm')
 
-        elif StateSystem.is_state_system(input):
-            input = StateSystem(input)
+        >>> from bqskit import compile, Circuit
+        >>> circuits = [Circuit.from_file(f'input{i}.qasm') for i in range(5)]
+        >>> compiled_circuits = compile(circuits)
+        >>> for i, circuit in enumerate(compiled_circuits):
+        ...     circuit.save(f'output{i}.qasm')
 
-        else:
-            raise TypeError(
-                'Input is neither a circuit, a unitary, a state system'
-                f', nor a state. 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
+        >>> from bqskit import compile, Circuit, MachineModel
+        >>> from bqskit.ir.gates import CZGate, RZGate, SqrtXGate
+        >>> target_gate_set = {CZGate(), RZGate(), SqrtXGate()}
+        >>> circuit = Circuit.from_file('input.qasm')
+        >>> model = MachineModel(circuit.num_qudits, gate_set=target_gate_set)
+        >>> compiled_circuit = compile(circuit, model, optimization_level=2)
 
-    assert isinstance(input, (Circuit, UnitaryMatrix, StateVector, StateSystem))
+    Raises:
+        ValueError: If the input is an empty iterable.
 
-    if not all(r == input.radixes[0] for r in input.radixes):
-        raise ValueError(
-            'Currently, can only automatically build a workflow '
-            'for same-level systems, such as qubit-only or qutrit-only'
-            'systems. Heterogenous-radix systems are not yet supported'
-            'with the standard workflows.',
-        )
+        ValueError: If the input (or any input) is larger than the model.
 
-    # Check `model`
-    if model is None:
-        model = MachineModel(input.num_qudits, radixes=input.radixes)
+        ValueError: If either the input or the model has mixed radixes.
 
-    if not isinstance(model, MachineModel):
-        raise TypeError(f'Expected MachineModel for model, got {type(model)}.')
+        ValueError: If the model has a mismatched radix with the circuit.
 
-    if model.num_qudits < input.num_qudits:
-        raise ValueError('Machine is too small for circuit.')
+        ValueError: If the model doesn't contain any entangling gates and
+            the input (or any input) is larger than one-qudit.
 
-    if not all(r == input.radixes[0] for r in model.radixes):
-        raise ValueError(
-            'Currently, can only automatically build a workflow '
-            'for same-level systems, such as qubit-only or qutrit-only'
-            'systems. Heterogenous-radix systems are not yet supported'
-            'with the standard workflows.',
-        )
+        ValueError: If the model does not contain any entangling gates
+            that are less than or equal to the input size.
 
-    model_mq_gates = [g for g in model.gate_set if g.num_qudits >= 2]
+        ValueError: If optimization_level is not 1, 2, 3, or 4.
 
-    if len(model_mq_gates) == 0 and input.num_qudits > 1:
-        raise ValueError('No entangling gates in native gate set.')
+        ValueError: If the maximum synthesis size is less than the
+            largest gate in the model.
 
-    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.',
-        )
+        ValueError: If the maximum synthesis size is less than 2.
+
+        ValueError: If the synthesis epsilon is not between 0 and 1.
+
+        ValueError: The error simulation size is less than the maximum
+            synthesis size.
+    """
+    # Check `model`
+    if model is not None and not isinstance(model, MachineModel):
+        raise TypeError(f'Expected MachineModel for model, got {type(model)}.')
 
     # Check `optimization_level`
     if not is_integer(optimization_level):
@@ -304,12 +366,6 @@ def compile(
             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(
@@ -317,13 +373,21 @@ def compile(
             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)
+    if max_synthesis_size <= 1:
         raise ValueError(
-            'The maximum synthesis size needs to be greater than or equal to'
-            f' the largest native gate size: {max_synthesis_size} < {value}.',
+            'The maximum synthesis size must be greater than or equal to 2.'
+            f' Got {max_synthesis_size}.',
         )
 
+    if model is not None:
+        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 the largest native gate size: '
+                f'{max_synthesis_size} < {value}.',
+            )
+
     if max_synthesis_size > 6:
         _logger.warning('Large max synthesis size; expect long compile times.')
 
@@ -367,29 +431,87 @@ def compile(
             f'maximum synthesis size: {error_sim_size} < {max_synthesis_size}.',
         )
 
+    # Check `compiler`
+    if compiler is not None and not isinstance(compiler, Compiler):
+        raise TypeError(
+            'Expected Compiler object for compiler parameter'
+            f', got {type(compiler)}.',
+        )
+
     # Check `seed`
     if seed is not None and not is_integer(seed):
         raise TypeError(f'Expected integer for seed, got {type(seed)}.')
 
-    # Build workflow
-    workflow = build_workflow(
-        input,
-        model,
-        optimization_level,
-        synthesis_epsilon,
-        max_synthesis_size,
-        error_threshold,
-        error_sim_size,
-        seed,
-    )
-    if isinstance(input, Circuit):
-        in_circuit = input
+    # check `with_mapping`
+    if not isinstance(with_mapping, bool):
+        raise TypeError(
+            f'Expected bool for with_mapping, got {type(with_mapping)}.',
+        )
 
-    elif isinstance(input, UnitaryMatrix):
-        in_circuit = Circuit.from_unitary(input)
+    def type_and_check_input(input: CompilationInputLike) -> CompilationInput:
+        """Check input to be valid and convert to a proper typed object."""
+        try:
+            if isinstance(input, Circuit):
+                pass
 
-    else:
-        in_circuit = Circuit(1)
+            elif UnitaryMatrix.is_unitary(input):
+                input = UnitaryMatrix(input)
+
+            elif StateVector.is_pure_state(input):
+                input = StateVector(input)
+
+            elif StateSystem.is_state_system(input):
+                input = StateSystem(input)
+
+            else:
+                raise TypeError(
+                    'Input is neither a circuit, a unitary, a state system'
+                    f', nor a state. 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, state, or state-system.',
+            ) from e
+
+        assert isinstance(
+            input, (
+                Circuit,
+                UnitaryMatrix,
+                StateVector,
+                StateSystem,
+            ),
+        )
+
+        if not all(r == input.radixes[0] for r in input.radixes):
+            raise ValueError(
+                'Currently, can only automatically build a workflow '
+                'for same-level systems, such as qubit-only or qutrit-only'
+                'systems. mixed-radix systems are not yet supported'
+                'with the standard workflows.',
+            )
+
+        if model is not None:
+            if model.num_qudits < input.num_qudits:
+                raise ValueError('Machine is too small for this input.')
+
+            if not all(r == input.radixes[0] for r in model.radixes):
+                raise ValueError('Model has a mismatch in radixes with input.')
+
+            if len(model.gate_set.multi_qudit_gates) == 0:
+                if 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.',
+                )
+
+        return input
 
     # Connect to or construct a Compiler
     managed_compiler = compiler is None
@@ -397,41 +519,133 @@ def compile(
     if managed_compiler:
         compiler = Compiler(**compiler_kwargs)
 
-    elif not isinstance(compiler, Compiler):
-        raise TypeError(
-            'Expected Compiler object for compiler parameter'
-            f', got {type(compiler)}.',
+    assert compiler is not None
+
+    if (
+        is_iterable(input)
+        and not isinstance(input, Circuit)
+        and not UnitaryMatrix.is_unitary(input)
+        and not StateVector.is_pure_state(input)
+        and not StateSystem.is_state_system(input)
+    ):
+        typed_inputs = [type_and_check_input(i) for i in input]  # type: ignore
+
+        if len(typed_inputs) == 0:
+            raise ValueError('Input iterable is empty.')
+
+        # Build workflows
+        workflows = [
+            build_workflow(
+                typed_input,
+                model,
+                optimization_level,
+                synthesis_epsilon,
+                max_synthesis_size,
+                error_threshold,
+                error_sim_size,
+                seed,
+            )
+            for typed_input in typed_inputs
+        ]
+        in_circuits = [
+            typed_input if isinstance(typed_input, Circuit) else Circuit(1)
+            for typed_input in typed_inputs
+        ]
+
+        # Perform the compilations in parallel
+        job_ids = [
+            compiler.submit(in_circuit, workflow, True)
+            for in_circuit, workflow in zip(in_circuits, workflows)
+        ]
+        results = [compiler.result(job_id) for job_id in job_ids]
+
+        outs: list[Circuit] = []
+        datas: list[PassData] = []
+        for result in results:
+            out, data = result
+            outs.append(out)  # type: ignore
+            datas.append(data)  # type: ignore
+
+        # Log error if necessary
+        if error_threshold is not None:
+            for i, data in enumerate(datas):
+                error = data.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}'
+                        f' in compilation index {i}.',
+                    )
+
+        # Gather mapping data if necessary
+        if with_mapping:
+            pis = []
+            pfs = []
+            for typed_input, data in zip(typed_inputs, datas):
+                default = list(range(typed_input.num_qudits))
+                pi = data.get('initial_mapping', default)
+                pf = data.get('final_mapping', default)
+                pis.append(pi)
+                pfs.append(pf)
+            to_return: Any = list(zip(outs, pis, pfs))
+
+        else:
+            to_return = outs
+
+    else:
+        typed_input = type_and_check_input(input)  # type: ignore
+
+        # Build workflow
+        workflow = build_workflow(
+            typed_input,
+            model,
+            optimization_level,
+            synthesis_epsilon,
+            max_synthesis_size,
+            error_threshold,
+            error_sim_size,
+            seed,
         )
+        if isinstance(typed_input, Circuit):
+            in_circuit = typed_input
 
-    # Perform the compilation
-    out, data = compiler.compile(in_circuit, workflow, True)
+        else:
+            in_circuit = Circuit(1)
+
+        # Perform the compilation
+        out, data = compiler.compile(in_circuit, workflow, True)
+
+        # Log error if necessary
+        if error_threshold is not None:
+            error = data.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}.',
+                )
 
-    # Log error if necessary
-    if error_threshold is not None:
-        error = data.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}.',
-            )
+        # Gather mapping data if necessary
+        if with_mapping:
+            default = list(range(typed_input.num_qudits))
+            pi = data.get('initial_mapping', default)
+            pf = data.get('final_mapping', default)
+            to_return = (out, pi, pf)
+
+        else:
+            to_return = out
 
     # Close managed compiler
     if managed_compiler:
         compiler.close()
 
-    # Gather mapping data if necessary
-    if with_mapping:
-        pi = data.get('initial_mapping', list(range(input.num_qudits)))
-        pf = data.get('final_mapping', list(range(input.num_qudits)))
-        return out, pi, pf
-
-    return out
+    return to_return
 
 
 def build_workflow(
     input: Circuit | UnitaryMatrix | StateVector | StateSystem,
-    model: MachineModel,
+    model: MachineModel | None,
     optimization_level: int = 1,
     synthesis_epsilon: float = 1e-10,
     max_synthesis_size: int = 3,
@@ -440,6 +654,9 @@ def build_workflow(
     seed: int | None = None,
 ) -> Workflow:
     """Build a BQSKit Off-the-Shelf workflow, see :func:`compile` for info."""
+    if model is None:
+        model = MachineModel(input.num_qudits, radixes=input.radixes)
+
     if isinstance(input, Circuit):
         if input.num_qudits > max_synthesis_size:
             if any(
@@ -458,7 +675,6 @@ def build_workflow(
                 )
 
         return _circuit_workflow(
-            input,
             model,
             optimization_level,
             synthesis_epsilon,
@@ -469,16 +685,14 @@ def build_workflow(
         )
 
     elif isinstance(input, UnitaryMatrix):
-        utry = input
-
-        if utry.num_qudits > max_synthesis_size:
+        if input.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.',
+                'Unable to compile unitary with size larger than'
+                ' max_synthesis_size.\nConsider adjusting it.',
             )
 
-        return _circuit_workflow(
-            Circuit.from_unitary(utry),
+        return _synthesis_workflow(
+            input,
             model,
             optimization_level,
             synthesis_epsilon,
@@ -489,16 +703,14 @@ def build_workflow(
         )
 
     elif isinstance(input, StateVector):
-        state = input
-
-        if state.num_qudits > max_synthesis_size:
+        if input.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.',
+                'Unable to compile states with size larger than'
+                ' max_synthesis_size.\nConsider adjusting it.',
             )
 
         return _stateprep_workflow(
-            state,
+            input,
             model,
             optimization_level,
             synthesis_epsilon,
@@ -509,16 +721,14 @@ def build_workflow(
         )
 
     elif isinstance(input, StateSystem):
-        statemap = input
-
-        if statemap.num_qudits > max_synthesis_size:
+        if input.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.',
+                'Unable to compile state systems with size larger than'
+                ' max_synthesis_size.\nConsider adjusting it.',
             )
 
         return _statemap_workflow(
-            statemap,
+            input,
             model,
             optimization_level,
             synthesis_epsilon,
@@ -532,7 +742,6 @@ def build_workflow(
 
 
 def _circuit_workflow(
-    circuit: Circuit,
     model: MachineModel,
     optimization_level: int = 1,
     synthesis_epsilon: float = 1e-10,
@@ -552,7 +761,6 @@ def _circuit_workflow(
     workflow: list[BasePass] = [] if seed is None else [SetRandomSeedPass(seed)]
     workflow += [UnfoldPass(), ExtractMeasurements()]
     workflow += workflow_builder(
-        circuit,
         model,
         synthesis_epsilon,
         max_synthesis_size,
@@ -563,280 +771,499 @@ def _circuit_workflow(
     return Workflow(workflow)
 
 
-def _opt1_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 1 workflow for circuit compilation."""
-    layer_gen = model.gate_set.build_mq_layer_generator()
-    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_no_blocks
-            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(model.radixes[0]) not in model.gate_set:
-            non_native_tq_gates.append(SwapGate(model.radixes[0]))
-        native_tq_gates = [g for g in model.gate_set if g.num_qudits == 2]
+def get_instantiate_options(optimization_level: int) -> dict[str, Any]:
+    """
+    Return good default instantiate options based on an optimization level.
+
+    Args:
+        optimization_level (int): The optimization level.
+
+    Returns:
+        (dict[str, Any]): The options.
+
+    Raises:
+        ValueError: If the optimization level is not 1, 2, 3, or 4.
+    """
+    if optimization_level == 1:
+        return {
+            'multistarts': 1,
+            'ftol': 1e-6,
+            'gtol': 1e-10,
+        }
+
+    elif optimization_level == 2:
+        return {
+            'multistarts': 2,
+            'ftol': 5e-12,
+            'gtol': 1e-14,
+        }
+
+    elif optimization_level == 3:
+        return {
+            'multistarts': 4,
+            'ftol': 5e-16,
+            'gtol': 1e-15,
+        }
+
+    elif optimization_level == 4:
+        return {
+            'multistarts': 8,
+            'ftol': 5e-16,
+            'gtol': 1e-15,
+        }
 
-        all_gates = model.gate_set.union(circuit.gate_set_no_blocks)
-        if any(g.num_qudits > 2 for g in all_gates):
-            multi_qudit_gate_rebase: BasePass = direct_synthesis
-        else:
-            if model.radixes[0] == 2:
-                sq_gate: Gate = U3Gate()
-            elif model.radixes[0] == 3:
-                sq_gate = U8Gate()
-            else:
-                sq_gate = VariableUnitaryGate(1, [model.radixes[0]])
-            multi_qudit_gate_rebase = Rebase2QuditGatePass(
-                non_native_tq_gates,
-                native_tq_gates,
-                max_depth=3,
-                max_retries=5,
-                single_qudit_gate=sq_gate,
-            )
     else:
-        smallest_entangler_size = 1
-        multi_qudit_gate_rebase = NOOPPass()
+        raise ValueError(
+            'Invalid optimization level, must be 1, 2, 3, or 4.'
+            f' Got {optimization_level}.',
+        )
 
-    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(),
-                            IfThenElsePass(
-                                NotPredicate(MultiPhysicalPredicate()),
-                                multi_qudit_gate_rebase,
-                                scan_mq,
-                            ),
-                        ],
-                        replace_filter=_gen_replace_filter(model),
-                    ),
-                    calculate_error_bound=(error_threshold is not None),
-                ),
-                UnfoldPass(),
-            ],
-        ),
+def build_partitioning_workflow(
+    inner_workflow: WorkflowLike,
+    block_size: int = 3,
+    error_sim_size: int | None = None,
+    replace_filter_method: str = 'less-than-respecting-multi',
+) -> Workflow:
+    """
+    Build standard partitioning workflow for circuit compilation.
 
-        # Mapping via Sabre
-        LogPass('Mapping circuit.'),
-        GreedyPlacementPass(),
-        GeneralizedSabreLayoutPass(),
-        GeneralizedSabreRoutingPass(),
+    This includes additional steps than normally necessary for error upper
+    bound analysis.
 
-        # 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(),
-                            IfThenElsePass(
-                                NotPredicate(MultiPhysicalPredicate()),
-                                multi_qudit_gate_rebase,
-                            ),
-                        ],
-                        replace_filter=_gen_replace_filter(model),
-                    ),
-                    calculate_error_bound=(error_threshold is not None),
-                ),
-                UnfoldPass(),
-            ],
-        ),
+    Args:
+        inner_workflow (WorkflowLike): The workflow to partition.
 
-        # Single-qudit gate retargeting
-        LogPass('Retargeting single-qudit gates.'),
-        single_qudit_gate_rebase,
+        block_size (int): The size of the blocks to partition into.
 
-        # Finalizing
-        LogErrorPass(),
-        ApplyPlacement(),
-    ]
+        error_sim_size (int | None): The size of the blocks to simulate
+            errors on. If None, then no error analysis is performed.
 
+        replace_filter_method (str): The method to use for replacing
+            gates in the circuit. See :class:`ForEachBlockPass` for more.
+
+    Returns:
+        Workflow: The partitioning workflow.
+
+    Raises:
+        ValueError: If block_size < 2.
+
+        ValueError: If error_sim_size < block_size.
+    """
+    if not is_integer(block_size):
+        raise TypeError(
+            f'Expected block_size to be int, got {type(block_size)}.',
+        )
+
+    if block_size < 2:
+        raise ValueError(f'Expected block_size > 1, got {block_size}.')
+
+    if error_sim_size is not None and not is_integer(block_size):
+        raise TypeError(
+            f'Expected int for error_sim_size, got {type(error_sim_size)}.',
+        )
+
+    if error_sim_size is not None and error_sim_size < block_size:
+        raise ValueError(
+            f'Expected error_sim_size >= block_size, got {error_sim_size}.',
+        )
+
+    pass_list = [QuickPartitioner(block_size), ExtendBlockSizePass()]
+
+    if error_sim_size is None:
+        pass_list += [
+            ForEachBlockPass(
+                inner_workflow,
+                replace_filter=replace_filter_method,
+            ),
+        ]
+
+    elif error_sim_size == block_size:
+        pass_list += [
+            ForEachBlockPass(
+                inner_workflow,
+                replace_filter=replace_filter_method,
+                calculate_error_bound=True,
+            ),
+        ]
+
+    else:
+        pass_list += [
+            QuickPartitioner(error_sim_size),
+            ForEachBlockPass(
+                ForEachBlockPass(
+                    inner_workflow,
+                    replace_filter=replace_filter_method,
+                ),
+                calculate_error_bound=True,
+            ),
+        ]
+
+    pass_list += [UnfoldPass()]
+    return Workflow(pass_list, name='Partitioning')
+
+
+def build_standard_search_synthesis_workflow(
+    optimization_level: int = 1,
+    synthesis_epsilon: float = 1e-10,
+) -> BasePass:
+    """
+    Build standard search-based synthesis pass for block-level compilation.
+
+    Args:
+        optimization_level (int): The optimization level. See :func:`compile`
+            for more information.
+
+        synthesis_epsilon (float): The maximum distance between target
+            and circuit unitary allowed to declare successful synthesis.
+            Set to 0 for exact synthesis. (Default: 1e-10)
+
+    Returns:
+        BasePass: The synthesis pass.
+
+    Raises:
+        ValueError: If the optimization level is not 1, 2, 3, or 4.
+
+    Note:
+        For larger circuits, this pass may take a very long time to run.
+        If unitary synthesis is your ultimate goal, rather than circuit
+        compilation, consider designing a custom instantiation-based
+        synthesis method or using alternative synthesis techniques
+        -- such as QFAST or QPredict -- for large unitaries.
+    """
+    if optimization_level not in [1, 2, 3, 4]:
+        raise ValueError(
+            'Invalid optimization level, must be 1, 2, 3, or 4.'
+            f' Got {optimization_level}.',
+        )
+
+    if not is_real_number(synthesis_epsilon):
+        raise TypeError(
+            'Expected float for synthesis_epsilon'
+            f', got {type(synthesis_epsilon)}.',
+        )
 
-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 = model.gate_set.build_mq_layer_generator()
-    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,
+        instantiate_options=get_instantiate_options(optimization_level),
     )
     leap = LEAPSynthesisPass(
         success_threshold=synthesis_epsilon,
-        layer_generator=layer_gen,
-        instantiate_options=inst_ops,
-        min_prefix_size=5,
+        min_prefix_size=[3, 4, 7, 9][optimization_level - 1],
+        instantiate_options=get_instantiate_options(optimization_level),
     )
-    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_no_blocks
-            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(model.radixes[0]) not in model.gate_set:
-            non_native_tq_gates.append(SwapGate(model.radixes[0]))
-        native_tq_gates = [g for g in model.gate_set if g.num_qudits == 2]
+    return IfThenElsePass(WidthPredicate(3), qsearch, leap)
 
-        all_gates = model.gate_set.union(circuit.gate_set_no_blocks)
-        if any(g.num_qudits > 2 for g in all_gates):
-            multi_qudit_gate_rebase: BasePass = direct_synthesis
-        else:
-            if model.radixes[0] == 2:
-                sq_gate: Gate = U3Gate()
-            elif model.radixes[0] == 3:
-                sq_gate = U8Gate()
-            else:
-                sq_gate = VariableUnitaryGate(1, [model.radixes[0]])
-            multi_qudit_gate_rebase = Rebase2QuditGatePass(
-                non_native_tq_gates,
-                native_tq_gates,
-                max_depth=3,
-                max_retries=5,
-                single_qudit_gate=sq_gate,
-            )
-    else:
-        smallest_entangler_size = 1
-        multi_qudit_gate_rebase = NOOPPass()
 
-    return [
-        SetModelPass(model),
+def build_multi_qudit_retarget_workflow(
+    optimization_level: int = 1,
+    synthesis_epsilon: float = 1e-10,
+    max_synthesis_size: int = 3,
+    error_threshold: float | None = None,
+    error_sim_size: int = 8,
+) -> Workflow:
+    """
+    Build standard workflow for circuit multi-qudit gate set retargeting.
 
-        # Multi-qudit gate retargeting
+    This workflow assumes that SetModelPass will be run earlier in the full
+    workflow and doesn't add it in here.
+    """
+    return Workflow([
         IfThenElsePass(
             NotPredicate(WidthPredicate(2)),
             [
                 LogPass('Retargeting multi-qudit gates.'),
-                QuickPartitioner(max_synthesis_size),
-                ExtendBlockSizePass(smallest_entangler_size),
-                QuickPartitioner(error_sim_size),
-                ForEachBlockPass(
-                    ForEachBlockPass(
-                        [
-                            FillSingleQuditGatesPass(),
+                build_partitioning_workflow(
+                    [
+                        FillSingleQuditGatesPass(),
+                        IfThenElsePass(
+                            NotPredicate(MultiPhysicalPredicate()),
                             IfThenElsePass(
-                                NotPredicate(MultiPhysicalPredicate()),
-                                multi_qudit_gate_rebase,
-                                scan_mq,
+                                ManyQuditGatesPredicate(),
+                                build_standard_search_synthesis_workflow(
+                                    optimization_level,
+                                    synthesis_epsilon,
+                                ),
+                                AutoRebase2QuditGatePass(3, 5),
                             ),
-                        ],
-                        replace_filter=_gen_replace_filter(model),
-                    ),
-                    calculate_error_bound=(error_threshold is not None),
+                            ScanningGateRemovalPass(
+                                success_threshold=synthesis_epsilon,
+                                collection_filter=_mq_gate_collection_filter,
+                                instantiate_options=get_instantiate_options(
+                                    optimization_level,
+                                ),
+                            ),
+                        ),
+                    ],
+                    max_synthesis_size,
+                    None if error_threshold is None else error_sim_size,
                 ),
-                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(
+def build_single_qudit_retarget_workflow(
+    optimization_level: int = 1,
+    synthesis_epsilon: float = 1e-10,
+    max_synthesis_size: int = 3,
+    error_threshold: float | None = None,
+    error_sim_size: int = 8,
+) -> BasePass:
+    """Build a pass to convert single-qudit-gates to the native gate set."""
+    sq_synthesis = QSearchSynthesisPass(
+        layer_generator=SingleQuditLayerGenerator(None, allow_repeats=True),
+        heuristic_function=DijkstraHeuristic(),
+        instantiate_options={
+            'method': 'minimization',
+            'minimizer': ScipyMinimizer(),
+            'cost_fn_gen': HilbertSchmidtCostGenerator(),
+        },
+    )
+
+    sq_gate_deletion = build_partitioning_workflow(
+        ScanningGateRemovalPass(
+            success_threshold=synthesis_epsilon,
+            collection_filter=_sq_gate_collection_filter,
+            instantiate_options=get_instantiate_options(optimization_level),
+        ),
+        max_synthesis_size,
+        None if error_threshold is None else error_sim_size,
+    )
+
+    return Workflow(
+        [
+            IfThenElsePass(
+                NotPredicate(SinglePhysicalPredicate()),
+                [
+                    LogPass('Retargeting single-qudit gates.'),
+                    UnfoldPass(),
+                    GroupSingleQuditGatePass(),
+                    IfThenElsePass(
+                        AllConstantSingleQuditGates(),
+                        # TODO: Implement better retargeting techniques
+                        # for constant gate sets
+                        LogPass(
+                            'The standard workflow with BQSKit may have trouble'
+                            ' targeting gate sets containing no parameterized'
+                            ' single-qudit gates.',
+                            logging.WARNING,
+                        ),
+                    ),
+                    IfThenElsePass(
+                        NoSingleQuditGatesInModel(),
                         [
-                            FillSingleQuditGatesPass(),
+                            LogPass('Attempting to remove single-qudit gates.'),
+                            sq_gate_deletion,
                             IfThenElsePass(
-                                NotPredicate(MultiPhysicalPredicate()),
-                                multi_qudit_gate_rebase,
+                                NotPredicate(SinglePhysicalPredicate()),
+                                LogPass(
+                                    'Unable to remove all single-qudit gates;'
+                                    ' gate set may not be universal without'
+                                    ' single-qudit gates. Consider changing'
+                                    ' gate set, increasing optimization_level'
+                                    ' or max_synthesis_size.',
+                                    logging.WARNING,
+                                ),
+                            ),
+                        ],
+                        [
+                            ForEachBlockPass(
+                                IfThenElsePass(
+                                    NotPredicate(SinglePhysicalPredicate()),
+                                    [
+                                        IfThenElsePass(
+                                            HasGeneralSingleQuditGate(),
+                                            GeneralSQDecomposition(),
+                                            IfThenElsePass(
+                                                ZXGatePredicate(),
+                                                ZXZXZDecomposition(),
+                                                sq_synthesis,
+                                            ),
+                                        ),
+                                    ],
+                                ),
                             ),
                         ],
-                        replace_filter=_gen_replace_filter(model),
                     ),
-                    calculate_error_bound=(error_threshold is not None),
+                    UnfoldPass(),
+                ],
+            ),
+        ], name='Single Qudit Retargeting',
+    )
+
+
+def build_sabre_mapping_workflow() -> Workflow:
+    """
+    Build standard workflow for circuit mapping.
+
+    Note:
+        - This workflow assumes that SetModelPass will be run earlier in the
+          full workflow and doesn't add it in here.
+
+        - It also assumes that ApplyPlacement will be run later in the full
+          workflow and doesn't add it in here.
+    """
+    return Workflow([
+        LogPass('Mapping circuit.'),
+        GreedyPlacementPass(),
+        GeneralizedSabreLayoutPass(),
+        GeneralizedSabreRoutingPass(),
+    ])
+
+
+def build_gate_deletion_optimization_workflow(
+    optimization_level: int = 1,
+    synthesis_epsilon: float = 1e-10,
+    max_synthesis_size: int = 3,
+    error_threshold: float | None = None,
+    error_sim_size: int = 8,
+    iterative: bool = False,
+) -> Workflow:
+    """Build standard workflow for circuit gate deletion optimization."""
+    core_workflow = Workflow([
+        LogPass('Attempting to delete gates.'),
+        build_partitioning_workflow(
+            ScanningGateRemovalPass(
+                success_threshold=synthesis_epsilon,
+                instantiate_options=get_instantiate_options(optimization_level),
+            ),
+            max_synthesis_size,
+            None if error_threshold is None else error_sim_size,
+        ),
+    ])
+
+    if iterative:
+        return Workflow(WhileLoopPass(ChangePredicate(), core_workflow))
+
+    return core_workflow
+
+
+def build_resynthesis_optimization_workflow(
+    optimization_level: int = 1,
+    synthesis_epsilon: float = 1e-10,
+    max_synthesis_size: int = 3,
+    error_threshold: float | None = None,
+    error_sim_size: int = 8,
+    iterative: bool = False,
+) -> Workflow:
+    """Build standard workflow for circuit resynthesis optimization."""
+    core_workflow = Workflow([
+        LogPass('Resynthesizing blocks.'),
+        build_partitioning_workflow(
+            IfThenElsePass(
+                NotPredicate(WidthPredicate(2)),
+                build_standard_search_synthesis_workflow(
+                    optimization_level,
+                    synthesis_epsilon,
                 ),
-                UnfoldPass(),
-            ],
+            ),
+            max_synthesis_size,
+            None if error_threshold is None else error_sim_size,
         ),
+    ])
 
-        # Single-qudit gate retargeting
-        LogPass('Retargeting single-qudit gates.'),
-        single_qudit_gate_rebase,
+    if iterative:
+        return Workflow([
+            WhileLoopPass(
+                GateCountPredicate('multi'),
+                core_workflow,
+            ),
+        ])
 
-        # 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),
+    return core_workflow
+
+
+def _opt1_workflow(
+    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 1 workflow for circuit compilation."""
+    return [
+        # Initializing
+        SetModelPass(model),
+
+        build_multi_qudit_retarget_workflow(
+            1,
+            synthesis_epsilon,
+            max_synthesis_size,
+            error_threshold,
+            error_sim_size,
+        ),
+
+        build_sabre_mapping_workflow(),
+
+        build_multi_qudit_retarget_workflow(
+            1,
+            synthesis_epsilon,
+            max_synthesis_size,
+            error_threshold,
+            error_sim_size,
+        ),
+
+        build_single_qudit_retarget_workflow(
+            1,
+            synthesis_epsilon,
+            max_synthesis_size,
+            error_threshold,
+            error_sim_size,
+        ),
+
+        # Finalizing
+        LogErrorPass(),
+        ApplyPlacement(),
+    ]
+
+
+def _opt2_workflow(
+    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."""
+    return [
+        # Initializing
+        SetModelPass(model),
+
+        build_multi_qudit_retarget_workflow(
+            2,
+            synthesis_epsilon,
+            max_synthesis_size,
+            error_threshold,
+            error_sim_size,
+        ),
+
+        build_sabre_mapping_workflow(),
+
+
+        build_multi_qudit_retarget_workflow(
+            2,
+            synthesis_epsilon,
+            max_synthesis_size,
+            error_threshold,
+            error_sim_size,
+        ),
+
+        build_single_qudit_retarget_workflow(
+            2,
+            synthesis_epsilon,
+            max_synthesis_size,
+            error_threshold,
+            error_sim_size,
+        ),
+
+        build_gate_deletion_optimization_workflow(
+            2,
+            synthesis_epsilon,
+            max_synthesis_size,
+            error_threshold,
+            error_sim_size,
         ),
-        UnfoldPass(),
 
         # Finalizing
         LogErrorPass(),
@@ -845,7 +1272,6 @@ def _opt2_workflow(
 
 
 def _opt3_workflow(
-    circuit: Circuit,
     model: MachineModel,
     synthesis_epsilon: float = 1e-10,
     max_synthesis_size: int = 3,
@@ -853,172 +1279,53 @@ def _opt3_workflow(
     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 = model.gate_set.build_mq_layer_generator()
-    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_no_blocks
-            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(model.radixes[0]) not in model.gate_set:
-            non_native_tq_gates.append(SwapGate(model.radixes[0]))
-        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_no_blocks)
-        if any(g.num_qudits > 2 for g in all_gates):
-            multi_qudit_gate_rebase: BasePass = direct_synthesis
-        else:
-            if model.radixes[0] == 2:
-                sq_gate: Gate = U3Gate()
-            elif model.radixes[0] == 3:
-                sq_gate = U8Gate()
-            else:
-                sq_gate = VariableUnitaryGate(1, [model.radixes[0]])
-            multi_qudit_gate_rebase = Rebase2QuditGatePass(
-                non_native_tq_gates,
-                native_tq_gates,
-                max_depth=3,
-                max_retries=5,
-                single_qudit_gate=sq_gate,
-            )
-    else:
-        smallest_entangler_size = 1
-        multi_qudit_gate_rebase = NOOPPass()
-        native_mq_gates = []
-
     return [
+        # Initializing
         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(),
-                            IfThenElsePass(
-                                NotPredicate(MultiPhysicalPredicate()),
-                                multi_qudit_gate_rebase,
-                                scan_mq,
-                            ),
-                        ],
-                        replace_filter=_gen_replace_filter(model),
-                    ),
-                    calculate_error_bound=(error_threshold is not None),
-                ),
-                UnfoldPass(),
-            ],
+        build_multi_qudit_retarget_workflow(
+            3,
+            synthesis_epsilon,
+            max_synthesis_size,
+            error_threshold,
+            error_sim_size,
         ),
 
-        # Mapping via Sabre
-        LogPass('Mapping circuit.'),
-        GreedyPlacementPass(),
-        GeneralizedSabreLayoutPass(),
-        GeneralizedSabreRoutingPass(),
+        build_sabre_mapping_workflow(),
 
-        # 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(),
-                            IfThenElsePass(
-                                NotPredicate(MultiPhysicalPredicate()),
-                                multi_qudit_gate_rebase,
-                            ),
-                        ],
-                        replace_filter=_gen_replace_filter(model),
-                    ),
-                    calculate_error_bound=(error_threshold is not None),
-                ),
-                UnfoldPass(),
-            ],
+
+        build_multi_qudit_retarget_workflow(
+            3,
+            synthesis_epsilon,
+            max_synthesis_size,
+            error_threshold,
+            error_sim_size,
         ),
 
-        # 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(),
-                    ],
-                ),
-            ],
+        build_resynthesis_optimization_workflow(
+            3,
+            synthesis_epsilon,
+            max_synthesis_size,
+            error_threshold,
+            error_sim_size,
+            True,
         ),
 
-        # Single-qudit gate retargeting
-        LogPass('Retargeting single-qudit gates.'),
-        single_qudit_gate_rebase,
+        build_single_qudit_retarget_workflow(
+            3,
+            synthesis_epsilon,
+            max_synthesis_size,
+            error_threshold,
+            error_sim_size,
+        ),
 
-        # 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(),
-            ],
+        build_gate_deletion_optimization_workflow(
+            3,
+            synthesis_epsilon,
+            max_synthesis_size,
+            error_threshold,
+            error_sim_size,
+            True,
         ),
 
         # Finalizing
@@ -1028,7 +1335,6 @@ def _opt3_workflow(
 
 
 def _opt4_workflow(
-    circuit: Circuit,
     model: MachineModel,
     synthesis_epsilon: float = 1e-10,
     max_synthesis_size: int = 3,
@@ -1048,192 +1354,187 @@ def _opt4_workflow(
             ' for optimization level 4. This may change in the future.',
         )
 
-    inst_ops = {
-        'multistarts': 8,
-        'method': 'minimization',
-        'ftol': 5e-16,
-        'gtol': 1e-15,
-    }
-    layer_gen = model.gate_set.build_mq_layer_generator()
-    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,
+        instantiate_options=get_instantiate_options(4),
     )
     leap = LEAPSynthesisPass(
         success_threshold=synthesis_epsilon,
-        layer_generator=layer_gen,
-        instantiate_options=inst_ops,
-        min_prefix_size=7,
+        min_prefix_size=9,
+        instantiate_options=get_instantiate_options(4),
     )
-    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(model.radixes[0]) not in model.gate_set:
-            non_native_tq_gates.append(SwapGate(model.radixes[0]))
-        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:
-            if model.radixes[0] == 2:
-                sq_gate: Gate = U3Gate()
-            elif model.radixes[0] == 3:
-                sq_gate = U8Gate()
-            else:
-                sq_gate = VariableUnitaryGate(1, [model.radixes[0]])
-            multi_qudit_gate_rebase = Rebase2QuditGatePass(
-                non_native_tq_gates,
-                native_tq_gates,
-                max_depth=3,
-                max_retries=5,
-                single_qudit_gate=sq_gate,
-            )
-    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(),
-                            IfThenElsePass(
-                                NotPredicate(MultiPhysicalPredicate()),
-                                multi_qudit_gate_rebase,
-                                scan_mq,
-                            ),
-                        ],
-                        replace_filter=_gen_replace_filter(model),
-                    ),
-                    calculate_error_bound=(error_threshold is not None),
-                ),
-                UnfoldPass(),
-            ],
+        SetModelPass(
+            MachineModel(
+                model.num_qudits,
+                None,
+                model.gate_set,
+                model.radixes,
+            ),
         ),
-
-        # Mapping via PostPAM
-        LogPass('Mapping circuit.'),
         IfThenElsePass(
             NotPredicate(WidthPredicate(2)),
             [
-                SubtopologySelectionPass(max_synthesis_size),
-                QuickPartitioner(max_synthesis_size),
+                QuickPartitioner(3),
                 ForEachBlockPass(
-                    EmbedAllPermutationsPass(inner_synthesis=qsearch),
+                    IfThenElsePass(
+                        WidthPredicate(4),
+                        EmbedAllPermutationsPass(
+                            inner_synthesis=qsearch,
+                            input_perm=True,
+                            output_perm=False,
+                            vary_topology=False,
+                        ),
+                        EmbedAllPermutationsPass(
+                            inner_synthesis=leap,
+                            input_perm=True,
+                            output_perm=False,
+                            vary_topology=False,
+                        ),
+                    ),
                 ),
-                GreedyPlacementPass(),
-                PAMLayoutPass(5),
                 PAMRoutingPass(),
                 UnfoldPass(),
-                ApplyPlacement(),
-            ],
-        ),
 
-        # Swap gate retargeting
-        IfThenElsePass(
-            NotPredicate(WidthPredicate(2)),
-            [
-                LogPass('Retargeting swap gates.'),
-                QuickPartitioner(max_synthesis_size),
-                ExtendBlockSizePass(smallest_entangler_size),
-                QuickPartitioner(error_sim_size),
+                SetModelPass(model),
+                SubtopologySelectionPass(3),
+                QuickPartitioner(3),
                 ForEachBlockPass(
-                    ForEachBlockPass(
-                        [
-                            FillSingleQuditGatesPass(),
-                            IfThenElsePass(
-                                NotPredicate(MultiPhysicalPredicate()),
-                                multi_qudit_gate_rebase,
-                            ),
-                        ],
-                        replace_filter=_gen_replace_filter(model),
+                    IfThenElsePass(
+                        WidthPredicate(4),
+                        EmbedAllPermutationsPass(
+                            inner_synthesis=qsearch,
+                            input_perm=False,
+                            output_perm=True,
+                            vary_topology=True,
+                        ),
+                        EmbedAllPermutationsPass(
+                            inner_synthesis=leap,
+                            input_perm=False,
+                            output_perm=True,
+                            vary_topology=True,
+                        ),
                     ),
-                    calculate_error_bound=(error_threshold is not None),
                 ),
+                ApplyPlacement(),
+                PAMLayoutPass(3),
+                PAMRoutingPass(0.1),
                 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(),
-                    ],
-                ),
-            ],
+        build_multi_qudit_retarget_workflow(
+            4,
+            synthesis_epsilon,
+            max_synthesis_size,
+            error_threshold,
+            error_sim_size,
         ),
 
-        # Single-qudit gate retargeting
-        LogPass('Retargeting single-qudit gates.'),
-        single_qudit_gate_rebase,
+        build_resynthesis_optimization_workflow(
+            4,
+            synthesis_epsilon,
+            max_synthesis_size,
+            error_threshold,
+            error_sim_size,
+            True,
+        ),
 
-        # 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(),
-            ],
+        build_single_qudit_retarget_workflow(
+            4,
+            synthesis_epsilon,
+            max_synthesis_size,
+            error_threshold,
+            error_sim_size,
+        ),
+
+        build_gate_deletion_optimization_workflow(
+            4,
+            synthesis_epsilon,
+            max_synthesis_size,
+            error_threshold,
+            error_sim_size,
+            True,
         ),
 
         # Finalizing
         LogErrorPass(),
-        ApplyPlacement(),
     ]
 
 
+def _synthesis_workflow(
+    input: UnitaryMatrix,
+    model: MachineModel,
+    optimization_level: int = 1,
+    synthesis_epsilon: float = 1e-8,
+    max_synthesis_size: int = 3,
+    error_threshold: float | None = None,
+    error_sim_size: int = 8,
+    seed: int | None = None,
+) -> Workflow:
+    """Build a workflow for unitary synthesis."""
+    if error_threshold is not None:
+        if error_threshold < synthesis_epsilon:
+            raise ValueError(
+                'When performing direct synthesis, the error threshold'
+                ' cannot be less than the synthesis epsilon.',
+            )
+
+    sq_synthesis = QSearchSynthesisPass(
+        layer_generator=SingleQuditLayerGenerator(None, allow_repeats=True),
+        heuristic_function=DijkstraHeuristic(),
+        instantiate_options={
+            'method': 'minimization',
+            'minimizer': ScipyMinimizer(),
+            'cost_fn_gen': HilbertSchmidtCostGenerator(),
+        },
+    )
+
+    qsearch = QSearchSynthesisPass(
+        success_threshold=synthesis_epsilon,
+        instantiate_options=get_instantiate_options(optimization_level),
+    )
+
+    leap = LEAPSynthesisPass(
+        success_threshold=synthesis_epsilon,
+        min_prefix_size=[3, 4, 7, 9][optimization_level - 1],
+        instantiate_options=get_instantiate_options(optimization_level),
+    )
+
+    if optimization_level < 4:
+        synthesis: BasePass = IfThenElsePass(WidthPredicate(3), qsearch, leap)
+
+    else:
+        in_synthesis = qsearch if input.num_qudits <= 3 else leap
+        synthesis = PermutationAwareSynthesisPass(inner_synthesis=in_synthesis)
+
+    if input.num_qudits == 1:
+        synthesis = sq_synthesis
+
+    scan = ScanningGateRemovalPass(
+        success_threshold=synthesis_epsilon,
+        instantiate_options=get_instantiate_options(optimization_level),
+    )
+
+    workflow: list[BasePass] = [] if seed is None else [SetRandomSeedPass(seed)]
+    workflow += [
+        SetModelPass(model),
+        SetTargetPass(input),
+        synthesis,
+        build_single_qudit_retarget_workflow(
+            optimization_level,
+            synthesis_epsilon,
+            max_synthesis_size,
+            error_threshold,
+            error_sim_size,
+        ),
+        scan if optimization_level >= 2 else NOOPPass(),
+    ]
+
+    return Workflow(workflow)
+
+
 def _stateprep_workflow(
     state: StateVector,
     model: MachineModel,
@@ -1435,6 +1736,14 @@ def _statemap_workflow(
 
 def _get_single_qudit_gate_rebase_pass(model: MachineModel) -> BasePass:
     """Build a pass to convert single-qudit-gates to the native gate set."""
+    warnings.warn(
+        'This single qudit rebase workflow has moved'
+        ' to build_single_qudit_retarget_workflow. See Workflow for more'
+        ' information. This function is deprecated and will be removed'
+        ' in a future release.',
+        DeprecationWarning,
+    )
+
     sq_gates = [g for g in model.gate_set if g.num_qudits == 1]
 
     if len(sq_gates) == 0:
@@ -1487,38 +1796,23 @@ def _get_single_qudit_gate_rebase_pass(model: MachineModel) -> BasePass:
     ])
 
 
-def _replace_filter(new: Circuit, old: Operation, model: MachineModel) -> 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)
-
-
-def _gen_replace_filter(
-    model: MachineModel,
-) -> Callable[[Circuit, Operation], bool]:
+def _gen_replace_filter(model: MachineModel) -> ReplaceFilterFn:
     """Generate a replace filter for use during the standard workflow."""
-    return functools.partial(_replace_filter, model=model)
+    warnings.warn(
+        'The replace filter generated by _gen_replace_filter has moved'
+        ' to gen_replace_filter in foreach. See ForEachBlockPass for more'
+        ' information. This function is deprecated and will be removed'
+        ' in a future release.',
+        DeprecationWarning,
+    )
+    return gen_replace_filter('less-then-respecting-multi', model)
 
 
 def _mq_gate_collection_filter(op: Operation) -> bool:
     """Return true if `op` is a multi-qudit operation."""
     return op.num_qudits > 1
+
+
+def _sq_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/gateset.py b/bqskit/compiler/gateset.py
index d5a9ba0ae..4bc6f94a1 100644
--- a/bqskit/compiler/gateset.py
+++ b/bqskit/compiler/gateset.py
@@ -87,6 +87,10 @@ def build_mq_layer_generator(self) -> LayerGenerator:
                 return FourParamGenerator()
 
             gate = list(self.two_qudit_gates)[0]
+
+            if isinstance(gate, GeneralGate):
+                return WideLayerGenerator(gate, None)
+
             return SimpleLayerGenerator(gate, self.get_general_sq_gate())
 
         return WideLayerGenerator(
diff --git a/bqskit/compiler/passdata.py b/bqskit/compiler/passdata.py
index 329ce7cae..e35268c86 100644
--- a/bqskit/compiler/passdata.py
+++ b/bqskit/compiler/passdata.py
@@ -36,6 +36,8 @@ class PassData(MutableMapping[str, Any]):
         'error',
         'seed',
         'machine_model',
+        'initial_mapping',
+        'final_mapping',
     ]
 
     def __init__(self, circuit: Circuit) -> None:
@@ -44,11 +46,13 @@ def __init__(self, circuit: Circuit) -> None:
         if circuit.num_qudits <= 8:
             self._target = circuit.get_unitary()
         else:
-            self._target = circuit
+            self._target = circuit  # Lazy evaluation
 
         self._error = 0.0
         self._model = MachineModel(circuit.num_qudits)
         self._placement = list(range(circuit.num_qudits))
+        self._initial_mapping = list(range(circuit.num_qudits))
+        self._final_mapping = list(range(circuit.num_qudits))
         self._data: dict[str, Any] = {}
         self._seed: int | None = None
 
@@ -137,6 +141,46 @@ def placement(self, _val: Sequence[int]) -> None:
 
         self._placement = list(int(x) for x in _val)
 
+    @property
+    def initial_mapping(self) -> list[int]:
+        """Return the initial mapping of logical to physical qudits."""
+        return self._initial_mapping
+
+    @initial_mapping.setter
+    def initial_mapping(self, _val: Sequence[int]) -> None:
+        if not is_sequence(_val):
+            raise TypeError(
+                f'Cannot set initial_mapping to {type(_val)}.'
+                ' Expected a sequence of integers.',
+            )
+
+        if not all(is_integer(x) for x in _val):
+            raise TypeError(
+                'Cannot set initial_mapping. Expected a sequence of integers.',
+            )
+
+        self._initial_mapping = list(int(x) for x in _val)
+
+    @property
+    def final_mapping(self) -> list[int]:
+        """Return the final mapping of logical to physical qudits."""
+        return self._final_mapping
+
+    @final_mapping.setter
+    def final_mapping(self, _val: Sequence[int]) -> None:
+        if not is_sequence(_val):
+            raise TypeError(
+                f'Cannot set final_mapping to {type(_val)}.'
+                ' Expected a sequence of integers.',
+            )
+
+        if not all(is_integer(x) for x in _val):
+            raise TypeError(
+                'Cannot set final_mapping. Expected a sequence of integers.',
+            )
+
+        self._final_mapping = list(int(x) for x in _val)
+
     @property
     def seed(self) -> int | None:
         """Return the pass's seed."""
diff --git a/bqskit/compiler/workflow.py b/bqskit/compiler/workflow.py
index a4900eca5..edf2f6e15 100644
--- a/bqskit/compiler/workflow.py
+++ b/bqskit/compiler/workflow.py
@@ -2,6 +2,7 @@
 from __future__ import annotations
 
 import copy
+import logging
 from typing import Iterable
 from typing import Iterator
 from typing import overload
@@ -18,13 +19,21 @@
     from bqskit.ir.circuit import Circuit
 
 
+_logger = logging.getLogger(__name__)
+
+
 class Workflow(BasePass, Sequence[BasePass]):
     """A BQSKit workflow captures a quantum circuit compilation process."""
 
-    def __init__(self, passes: WorkflowLike) -> None:
+    def __init__(self, passes: WorkflowLike, name: str = '') -> None:
         """
         Initialize a workflow object from a sequence of passes.
 
+        Args:
+            passes (WorkflowLike): The passes to run in the workflow.
+
+            name (str): The optional name of the workflow.
+
         Raises:
             ValueError: If passes is empty.
         """
@@ -45,6 +54,10 @@ def __init__(self, passes: WorkflowLike) -> None:
             msg = f'Expected Pass or sequence of Passes, got {wrong_type}.'
             raise TypeError(msg)
 
+        if not isinstance(name, str):
+            raise TypeError(f'Expected name to be str, got {type(name)}.')
+
+        self._name = name
         self._passes = list(passes)
 
         if len(self._passes) == 0:
@@ -55,28 +68,9 @@ async def run(self, circuit: Circuit, data: PassData) -> None:
         for pass_obj in self._passes:
             if data.seed is not None:
                 seed_random_sources(data.seed)
+            _logger.debug(f'Running {pass_obj.name}')
             await pass_obj.run(circuit, data)
 
-    # @staticmethod
-    # def build_compile_flow() -> Workflow:
-    #     pass
-
-    # @staticmethod
-    # def build_transpile_flow() -> Workflow:
-    #     pass
-
-    # @staticmethod
-    # def build_synthesize_flow() -> Workflow:
-    #     pass
-
-    # @staticmethod
-    # def build_map_flow() -> Workflow:
-    #     pass
-
-    # @staticmethod
-    # def build_prepare_flow() -> Workflow:
-    #     pass
-
     def save(self, filename: str) -> None:
         import pickle
         with open(filename, 'wb') as f:
@@ -88,11 +82,15 @@ def load(filename: str) -> Workflow:
         with open(filename, 'rb') as f:
             return pickle.load(f)
 
-    # def __str__(self) -> str:
-    #     pass  # TODO:
+    @property
+    def name(self) -> str:
+        """The name of the pass."""
+        return self._name or self.__class__.__name__
 
-    # def __repr__(self) -> str:
-    #     pass  # TODO:
+    def __str__(self) -> str:
+        name_seq = f'Workflow: {self.name}\n\t'
+        pass_strs = [f'{i}. {p}' for i, p in enumerate(self._passes)]
+        return name_seq + '\n\t'.join(pass_strs)
 
     def __add__(self, other: WorkflowLike) -> Workflow:
         return Workflow(self._passes + Workflow(other)._passes)
diff --git a/bqskit/ir/circuit.py b/bqskit/ir/circuit.py
index 9c4bbec46..39ffe8e7d 100644
--- a/bqskit/ir/circuit.py
+++ b/bqskit/ir/circuit.py
@@ -271,10 +271,7 @@ def gate_counts(self) -> dict[Gate, int]:
     @property
     def active_qudits(self) -> list[int]:
         """The qudits involved in at least one operation."""
-        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])
+        active_qudits = set()
         for qudit, point in self._front.items():
             if point is not None:
                 active_qudits.add(qudit)
diff --git a/bqskit/passes/__init__.py b/bqskit/passes/__init__.py
index 208a098ce..fc2b4f097 100644
--- a/bqskit/passes/__init__.py
+++ b/bqskit/passes/__init__.py
@@ -42,10 +42,19 @@
 
     ExhaustiveGateRemovalPass
     IterativeScanningGateRemovalPass
-    Rebase2QuditGatePass
     ScanningGateRemovalPass
     SubstitutePass
 
+.. rubric:: Retargeting Passes
+
+.. autosummary::
+    :toctree: autogen
+    :recursive:
+
+    AutoRebase2QuditGatePass
+    GeneralSQDecomposition
+    Rebase2QuditGatePass
+
 .. rubric:: Control Passes
 
 This are passes that are used to manage control flow inside of a workflow.
@@ -72,11 +81,16 @@
     PassPredicate
     ChangePredicate
     GateCountPredicate
+    ManyQuditGatesPredicate
     NotPredicate
     WidthPredicate
     PhysicalPredicate
     SinglePhysicalPredicate
     MultiPhysicalPredicate
+    NoSingleQuditGatesInModel
+    HasGeneralSingleQuditGate
+    ZXGatePredicate
+    AllConstantSingleQuditGates
 
 .. rubric:: Rule-based Passes
 
@@ -184,10 +198,15 @@
 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.many import ManyQuditGatesPredicate
 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 AllConstantSingleQuditGates
+from bqskit.passes.control.predicates.single import HasGeneralSingleQuditGate
+from bqskit.passes.control.predicates.single import NoSingleQuditGatesInModel
 from bqskit.passes.control.predicates.single import SinglePhysicalPredicate
+from bqskit.passes.control.predicates.single import ZXGatePredicate
 from bqskit.passes.control.predicates.width import WidthPredicate
 from bqskit.passes.control.whileloop import WhileLoopPass
 from bqskit.passes.group import PassGroup
@@ -215,9 +234,11 @@
 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.retarget.auto import AutoRebase2QuditGatePass
+from bqskit.passes.retarget.general import GeneralSQDecomposition
+from bqskit.passes.retarget.two import Rebase2QuditGatePass
 from bqskit.passes.rules.ch2cnot import CHToCNOTPass
 from bqskit.passes.rules.cnot2ch import CNOTToCHPass
 from bqskit.passes.rules.cnot2cy import CNOTToCYPass
@@ -345,4 +366,11 @@
     'SubtopologySelectionPass',
     'PermutationAwareSynthesisPass',
     'ToVariablePass',
+    'AutoRebase2QuditGatePass',
+    'ManyQuditGatesPredicate',
+    'NoSingleQuditGatesInModel',
+    'HasGeneralSingleQuditGate',
+    'ZXGatePredicate',
+    'AllConstantSingleQuditGates',
+    'GeneralSQDecomposition',
 ]
diff --git a/bqskit/passes/alias.py b/bqskit/passes/alias.py
index e5a5faa46..4e3f2e02b 100644
--- a/bqskit/passes/alias.py
+++ b/bqskit/passes/alias.py
@@ -5,6 +5,7 @@
 from typing import TYPE_CHECKING
 
 from bqskit.compiler.basepass import BasePass
+from bqskit.compiler.workflow import Workflow
 
 if TYPE_CHECKING:
     from bqskit.compiler.passdata import PassData
@@ -20,5 +21,4 @@ def get_passes(self) -> list[BasePass]:
 
     async def run(self, circuit: Circuit, data: PassData) -> None:
         """Perform the pass's operation, see :class:`BasePass` for more."""
-        for bqskit_pass in self.get_passes():
-            await bqskit_pass.run(circuit, data)
+        await Workflow(self.get_passes()).run(circuit, data)
diff --git a/bqskit/passes/control/__init__.py b/bqskit/passes/control/__init__.py
index 013a0e790..302a71867 100644
--- a/bqskit/passes/control/__init__.py
+++ b/bqskit/passes/control/__init__.py
@@ -9,9 +9,15 @@
 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.many import ManyQuditGatesPredicate
 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 AllConstantSingleQuditGates
+from bqskit.passes.control.predicates.single import HasGeneralSingleQuditGate
+from bqskit.passes.control.predicates.single import NoSingleQuditGatesInModel
+from bqskit.passes.control.predicates.single import SinglePhysicalPredicate
+from bqskit.passes.control.predicates.single import ZXGatePredicate
 from bqskit.passes.control.predicates.width import WidthPredicate
 from bqskit.passes.control.whileloop import WhileLoopPass
 
@@ -22,6 +28,7 @@
     'PassPredicate',
     'ChangePredicate',
     'GateCountPredicate',
+    'ManyQuditGatesPredicate',
     'NotPredicate',
     'WhileLoopPass',
     'DoThenDecide',
@@ -29,4 +36,9 @@
     'WidthPredicate',
     'PhysicalPredicate',
     'MultiPhysicalPredicate',
+    'SinglePhysicalPredicate',
+    'NoSingleQuditGatesInModel',
+    'HasGeneralSingleQuditGate',
+    'ZXGatePredicate',
+    'AllConstantSingleQuditGates',
 ]
diff --git a/bqskit/passes/control/foreach.py b/bqskit/passes/control/foreach.py
index f6f7f5b97..197f1920a 100644
--- a/bqskit/passes/control/foreach.py
+++ b/bqskit/passes/control/foreach.py
@@ -1,6 +1,7 @@
 """This module implements the ForEachBlockPass class."""
 from __future__ import annotations
 
+import functools
 import logging
 from typing import Callable
 
@@ -15,6 +16,7 @@
 from bqskit.ir.gates.constant.unitary import ConstantUnitaryGate
 from bqskit.ir.gates.parameterized.pauli import PauliGate
 from bqskit.ir.gates.parameterized.unitary import VariableUnitaryGate
+from bqskit.ir.location import CircuitLocation
 from bqskit.ir.operation import Operation
 from bqskit.ir.point import CircuitPoint
 from bqskit.runtime import get_runtime
@@ -41,7 +43,7 @@ def __init__(
         loop_body: WorkflowLike,
         calculate_error_bound: bool = False,
         collection_filter: Callable[[Operation], bool] | None = None,
-        replace_filter: Callable[[Circuit, Operation], bool] | None = None,
+        replace_filter: ReplaceFilterFn | str = 'always',
         batch_size: int | None = None,
     ) -> None:
         """
@@ -62,14 +64,49 @@ def __init__(
                 be formed into an individual circuit and passed through
                 `loop_body`. Defaults to all CircuitGates,
                 ConstantUnitaryGates, and VariableUnitaryGates.
+                #TODO: address importability
 
-            replace_filter (Callable[[Circuit, Operation], bool] | None):
+            replace_filter (ReplaceFilterFn | str | None):
                 A predicate that determines if the resulting circuit, after
                 calling `loop_body` on a block, should replace the original
                 operation. Called with the circuit output from `loop_body`
                 and the original operation. If this returns true, the
                 operation will be replaced with the new circuit.
-                Defaults to always replace.
+                Defaults to always replace. If none is passed, will
+                generate a replace filter always replaces. If a string is
+                passed, will generate a replace filter corresponding to
+                the string. The string should either be 'always', 'less-than',
+                'less-than-multi', 'less-than-many', 'less-than-respecting',
+                'less-than-respecting-multi', or 'less-than-respecting-many'.
+                    - 'always' will always replace
+                    - 'less-than' will replace if the new circuit has fewer
+                        gates than the old circuit.
+                    - 'less-than-multi' will replace if the new circuit has
+                        fewer multi-qudit gates than the old circuit.
+                    - 'less-than-many' will replace if the new circuit has
+                        fewer many-qudit gates than the old circuit.
+                    - 'less-than-respecting' will replace if the new circuit
+                        has fewer gates than the old circuit or the old
+                        doesn't respect the model (ignoring single-qudit
+                        gate sets).
+                    - 'less-than-respecting-multi' will replace if the new
+                        circuit has fewer multi-qudit gates than the old
+                        circuit or the old doesn't respect the model
+                        (ignoring single-qudit gate sets).
+                    - 'less-than-respecting-many' will replace if the new
+                        circuit has fewer many-qudit gates than the old
+                        circuit or the old doesn't respect the model
+                        (ignoring single-qudit gate sets).
+                    - 'less-than-respecting-fully' will replace if the new
+                        circuit has fewer gates than the old circuit or
+                        the old doesn't respect the model.
+                    - 'less-than-respecting-fully-multi' will replace if
+                        the new circuit has fewer multi-qudit gates than
+                        the old circuit or the old doesn't respect the model.
+                    - 'less-than-respecting-fully-many' will replace if
+                        the new circuit has fewer many-qudit gates than
+                        the old circuit or the old doesn't respect the model.
+                Defaults to 'always'.  #TODO: address importability
 
             batch_size (int): (Deprecated).
         """
@@ -92,14 +129,24 @@ def __init__(
                 f' collection_filter, got {type(self.collection_filter)}.',
             )
 
-        if not callable(self.replace_filter):
-            raise TypeError(
-                'Expected callable method that maps Circuit and Operations to'
-                f' bools for replace_filter, got {type(self.replace_filter)}.',
-            )
+        if not isinstance(self.replace_filter, str):
+            if not callable(self.replace_filter):
+                raise TypeError(
+                    'Expected either string representing a valid replacement'
+                    ' filter or callable method that maps Circuit and'
+                    ' Operations to bools for replace_filter'
+                    f' , got {type(self.replace_filter)}.',
+                )
 
     async def run(self, circuit: Circuit, data: PassData) -> None:
         """Perform the pass's operation, see :class:`BasePass` for more."""
+        # Get the callable replacement filter
+        if isinstance(self.replace_filter, str):
+            method = self.replace_filter
+            replace_filter = gen_replace_filter(method, data.model)
+        else:
+            replace_filter = self.replace_filter
+
         # Make room in data for block data
         if self.key not in data:
             data[self.key] = []
@@ -175,7 +222,7 @@ async def run(self, circuit: Circuit, data: PassData) -> None:
             block_data = completed_block_datas[i]
 
             # Mark Blocks to be Replaced
-            if self.replace_filter(subcircuit, op):
+            if replace_filter(subcircuit, op):
                 _logger.debug(f'Replacing block {i}.')
                 points.append(CircuitPoint(cycle, op.location[0]))
                 ops.append(
@@ -217,4 +264,245 @@ def default_collection_filter(op: Operation) -> bool:
 
 
 def default_replace_filter(circuit: Circuit, op: Operation) -> bool:
+    """Always replace."""
+    # legacy name and style for backwards compatibility
+    return True
+
+
+def _less_than(new: Circuit, old: Operation) -> bool:
+    """Return true if the new circuit has fewer gates."""
+    if isinstance(old.gate, CircuitGate):
+        return new.num_operations < old.gate._circuit.num_operations
+
+    return True  # TODO: Re-evaluate always true when old is not a circuit
+
+
+def _less_than_multi(new: Circuit, old: Operation) -> bool:
+    """Return true if the new circuit has fewer multi-qudit gates."""
+    if isinstance(old.gate, CircuitGate):
+        org = old.gate._circuit
+        omq = sum([c for g, c in org.gate_counts.items() if g.num_qudits > 1])
+        osq = sum([c for g, c in org.gate_counts.items() if g.num_qudits == 1])
+        nmq = sum([c for g, c in new.gate_counts.items() if g.num_qudits > 1])
+        nsq = sum([c for g, c in new.gate_counts.items() if g.num_qudits == 1])
+        return (nmq, nsq) < (omq, osq)
+
     return True
+
+
+def _less_than_many(new: Circuit, old: Operation) -> bool:
+    """Return true if the new circuit has fewer many-qudit gates."""
+    if isinstance(old.gate, CircuitGate):
+        org = old.gate._circuit
+        omq = sum([c for g, c in org.gate_counts.items() if g.num_qudits > 2])
+        otq = sum([c for g, c in org.gate_counts.items() if g.num_qudits == 2])
+        osq = sum([c for g, c in org.gate_counts.items() if g.num_qudits == 1])
+        nmq = sum([c for g, c in new.gate_counts.items() if g.num_qudits > 2])
+        ntq = sum([c for g, c in new.gate_counts.items() if g.num_qudits == 2])
+        nsq = sum([c for g, c in new.gate_counts.items() if g.num_qudits == 1])
+        return (nmq, ntq, nsq) < (omq, otq, osq)
+
+    return True
+
+
+def _is_respecting(
+    circuit: Circuit,
+    location: CircuitLocation,
+    model: MachineModel,
+    fully: bool = False,
+) -> bool:
+    """
+    Return true if the `circuit` respects the `model` at `location`.
+
+    Args:
+        circuit (Circuit): The circuit to check.
+
+        location (CircuitLocation): The location to check.
+
+        model (MachineModel): The machine model to check against.
+
+        fully (bool): If set to true, will check if the circuit respects
+            the model fully. If set to false, will ignore single-qudit
+            gate sets. (Default: False)
+
+    Returns:
+        True if the circuit respects the model at the location. This implies
+        that the circuit can be run on the machine at the location.
+    """
+    org_mq_gates = circuit.gate_set.multi_qudit_gates
+    org_sq_gates = circuit.gate_set.single_qudit_gates
+
+    if any(g not in model.gate_set for g in org_mq_gates):
+        return False
+
+    if fully and any(g not in model.gate_set for g in org_sq_gates):
+        return False
+
+    if any(
+        (location[e[0]], location[e[1]]) not in model.coupling_graph
+        for e in circuit.coupling_graph
+    ):
+        return False
+
+    return True
+
+
+def _less_than_fn_respecting(
+    new: Circuit,
+    old: Operation,
+    model: MachineModel,
+    fn: ReplaceFilterFn,
+) -> bool:
+    """Return true if the new circuit has fewer gates or the old doesn't respect
+    the model."""
+    if isinstance(old.gate, CircuitGate):
+        if not _is_respecting(old.gate._circuit, old.location, model):
+            if not _is_respecting(new, old.location, model):
+                _logger.warning("New circuit doesn't respect model.")
+            return True
+
+        if not _is_respecting(new, old.location, model):
+            _logger.warning("New circuit doesn't respect model.")
+            return False
+
+    return fn(new, old)
+
+
+def _less_than_fn_respecting_fully(
+    new: Circuit,
+    old: Operation,
+    model: MachineModel,
+    fn: ReplaceFilterFn,
+) -> bool:
+    """Return true if the new circuit has fewer gates or the old doesn't respect
+    the model."""
+    if isinstance(old.gate, CircuitGate):
+        if not _is_respecting(old.gate._circuit, old.location, model, True):
+            if not _is_respecting(new, old.location, model, True):
+                _logger.warning("New circuit doesn't respect model.")
+            return True
+
+        if not _is_respecting(new, old.location, model, True):
+            _logger.warning("New circuit doesn't respect model.")
+            return False
+
+    return fn(new, old)
+
+
+def gen_always(model: MachineModel) -> ReplaceFilterFn:
+    """Generate a replace filter that always replaces."""
+    # legacy name and style for backwards compatibility
+    return default_replace_filter
+
+
+def gen_less_than(model: MachineModel) -> ReplaceFilterFn:
+    """Generate a replace filter that replaces if the new circuit has fewer
+    gates."""
+    return _less_than
+
+
+def gen_less_than_multi(model: MachineModel) -> ReplaceFilterFn:
+    """Generate a replace filter that replaces if the new circuit has fewer
+    multi-qudit gates."""
+    return _less_than_multi
+
+
+def gen_less_than_many(model: MachineModel) -> ReplaceFilterFn:
+    """Generate a replace filter that replaces if the new circuit has fewer
+    many-qudit gates."""
+    return _less_than_many
+
+
+def gen_less_than_rspt(model: MachineModel) -> ReplaceFilterFn:
+    """Generate a replace filter that replaces if the new circuit has fewer
+    gates or the old doesn't respect the model."""
+    return functools.partial(
+        _less_than_fn_respecting,
+        model=model,
+        fn=_less_than,
+    )
+
+
+def gen_less_than_rspt_multi(model: MachineModel) -> ReplaceFilterFn:
+    """Generate a replace filter that replaces if the new circuit has fewer
+    multi-qudit gates or the old doesn't respect the model."""
+    return functools.partial(
+        _less_than_fn_respecting,
+        model=model,
+        fn=_less_than_multi,
+    )
+
+
+def gen_less_than_rspt_many(model: MachineModel) -> ReplaceFilterFn:
+    """Generate a replace filter that replaces if the new circuit has fewer
+    many-qudit gates or the old doesn't respect the model."""
+    return functools.partial(
+        _less_than_fn_respecting,
+        model=model,
+        fn=_less_than_many,
+    )
+
+
+def gen_less_than_rspt_fully(model: MachineModel) -> ReplaceFilterFn:
+    """Generate a replace filter that replaces if the new circuit has fewer
+    gates or the old doesn't respect the model."""
+    return functools.partial(
+        _less_than_fn_respecting_fully,
+        model=model,
+        fn=_less_than,
+    )
+
+
+def gen_less_than_rspt_fully_multi(model: MachineModel) -> ReplaceFilterFn:
+    """Generate a replace filter that replaces if the new circuit has fewer
+    multi-qudit gates or the old doesn't respect the model."""
+    return functools.partial(
+        _less_than_fn_respecting_fully,
+        model=model,
+        fn=_less_than_multi,
+    )
+
+
+def gen_less_than_rspt_fully_many(model: MachineModel) -> ReplaceFilterFn:
+    """Generate a replace filter that replaces if the new circuit has fewer
+    many-qudit gates or the old doesn't respect the model."""
+    return functools.partial(
+        _less_than_fn_respecting_fully,
+        model=model,
+        fn=_less_than_many,
+    )
+
+
+def gen_replace_filter(method: str, model: MachineModel) -> ReplaceFilterFn:
+    """
+    Generate a replace filter for use during the standard workflow.
+
+    Args:
+        method (str): The method to use for the replace filter. See
+            :class:`ForEachBlockPass` for more information.
+
+        model (MachineModel): The machine model to potentially respect.
+
+    Returns:
+        A replace filter function.
+    """
+    replace_filters = {
+        'always': gen_always,
+        'less-than': gen_less_than,
+        'less-than-multi': gen_less_than_multi,
+        'less-than-many': gen_less_than_many,
+        'less-than-respecting': gen_less_than_rspt,
+        'less-than-respecting-multi': gen_less_than_rspt_multi,
+        'less-than-respecting-many': gen_less_than_rspt_many,
+        'less-than-respecting-fully': gen_less_than_rspt_fully,
+        'less-than-respecting-fully-multi': gen_less_than_rspt_fully_multi,
+        'less-than-respecting-fully-many': gen_less_than_rspt_fully_many,
+    }
+
+    if method not in replace_filters:
+        raise ValueError(f'Unknown replace filter method {method}.')
+
+    return replace_filters[method](model)
+
+
+ReplaceFilterFn = Callable[[Circuit, Operation], bool]
diff --git a/bqskit/passes/control/predicates/__init__.py b/bqskit/passes/control/predicates/__init__.py
index 2037cb5c3..55d9ef6d4 100644
--- a/bqskit/passes/control/predicates/__init__.py
+++ b/bqskit/passes/control/predicates/__init__.py
@@ -4,11 +4,16 @@
 from bqskit.passes.control.predicates.andpredicate import AndPredicate
 from bqskit.passes.control.predicates.change import ChangePredicate
 from bqskit.passes.control.predicates.count import GateCountPredicate
+from bqskit.passes.control.predicates.many import ManyQuditGatesPredicate
 from bqskit.passes.control.predicates.multi import MultiPhysicalPredicate
 from bqskit.passes.control.predicates.notpredicate import NotPredicate
 from bqskit.passes.control.predicates.orpredicate import OrPredicate
 from bqskit.passes.control.predicates.physical import PhysicalPredicate
+from bqskit.passes.control.predicates.single import AllConstantSingleQuditGates
+from bqskit.passes.control.predicates.single import HasGeneralSingleQuditGate
+from bqskit.passes.control.predicates.single import NoSingleQuditGatesInModel
 from bqskit.passes.control.predicates.single import SinglePhysicalPredicate
+from bqskit.passes.control.predicates.single import ZXGatePredicate
 from bqskit.passes.control.predicates.width import WidthPredicate
 
 __all__ = [
@@ -21,4 +26,9 @@
     'PhysicalPredicate',
     'SinglePhysicalPredicate',
     'MultiPhysicalPredicate',
+    'ManyQuditGatesPredicate',
+    'NoSingleQuditGatesInModel',
+    'HasGeneralSingleQuditGate',
+    'ZXGatePredicate',
+    'AllConstantSingleQuditGates',
 ]
diff --git a/bqskit/passes/control/predicates/count.py b/bqskit/passes/control/predicates/count.py
index c0dfaf37c..93475f075 100644
--- a/bqskit/passes/control/predicates/count.py
+++ b/bqskit/passes/control/predicates/count.py
@@ -28,20 +28,50 @@ class GateCountPredicate(PassPredicate):
 
     key = 'GateCountPredicate_circuit_count'
 
-    def __init__(self, gate: Gate | Sequence[Gate]) -> None:
-        """Construct a GateCountPredicate."""
+    def __init__(self, gate: Gate | Sequence[Gate] | str) -> None:
+        """
+        Construct a GateCountPredicate.
+
+        Args:
+            gate (Gate | Sequence[Gate] | str): The gate(s) to count.
+             Either a single gate, a sequence of gates, or a string
+                representing the gate type. The allowed strings are:
+                    - 'sq' for single qudit gates
+                    - 'tq' for two qudit gates
+                    - 'multi' for multi-qudit gates
+                    - 'many' for gates with more than 2 qudits
+        """
 
         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)}')
+        if not isinstance(gate, str):
+            if not is_sequence(gate):
+                raise TypeError(f'Expected gate, got {type(gate)}')
 
-        self.gate = gate
+            if not all(isinstance(g, Gate) for g in gate):
+                raise TypeError(f'Expected gate, got {type(gate)}')
+
+        else:
+            if gate not in ['sq', 'tq', 'multi', 'many']:
+                raise ValueError(f'Unknown gate type {gate}.')
+
+        self.gate = list(gate) if not isinstance(gate, str) else gate
 
     def get_truth_value(self, circuit: Circuit, data: PassData) -> bool:
         """Call this predicate, see :class:`PassPredicate` for more info."""
-        gate_count = sum(circuit.count(g) for g in self.gate)
+        if isinstance(self.gate, str):
+            gates = list({
+                'sq': circuit.gate_set.single_qudit_gates,
+                'tq': circuit.gate_set.two_qudit_gates,
+                'multi': circuit.gate_set.multi_qudit_gates,
+                'many': circuit.gate_set.many_qudit_gates,
+            }[self.gate])
+
+        else:
+            gates = self.gate  # type: ignore
+
+        gate_count = sum(circuit.count(g) for g in gates)
 
         # If first call, record data and return true
         if self.key not in data:
diff --git a/bqskit/passes/control/predicates/many.py b/bqskit/passes/control/predicates/many.py
new file mode 100644
index 000000000..e6d0197c8
--- /dev/null
+++ b/bqskit/passes/control/predicates/many.py
@@ -0,0 +1,69 @@
+"""This module implements the ManyQuditGatesPredicate class."""
+from __future__ import annotations
+
+import logging
+from typing import TYPE_CHECKING
+
+from bqskit.passes.control.predicate import PassPredicate
+
+if TYPE_CHECKING:
+    from bqskit.compiler.passdata import PassData
+    from bqskit.ir.circuit import Circuit
+
+_logger = logging.getLogger(__name__)
+
+
+class ManyQuditGatesPredicate(PassPredicate):
+    """Check if many-qudit gates are in the circuit and/or model."""
+
+    def __init__(
+        self,
+        check_circuit: bool = True,
+        check_model: bool = True,
+    ) -> None:
+        """
+        Construct a ManyQuditGatesPredicate.
+
+        Args:
+            check_circuit (bool): Whether to check the circuit for many-qudit
+                gates. (Default: True)
+
+            check_model (bool): Whether to check the model for many-qudit
+                gates. (Default: True)
+
+        Raises:
+            ValueError: If both check_circuit and check_model are False.
+        """
+        if not isinstance(check_circuit, bool):
+            raise TypeError(
+                f'Expected bool for check_circuit, got {type(check_circuit)}.',
+            )
+
+        if not isinstance(check_model, bool):
+            raise TypeError(
+                f'Expected bool for check_model, got {type(check_model)}.',
+            )
+
+        if not check_circuit and not check_model:
+            raise ValueError(
+                'At least one of check_circuit or check_model must be True.',
+            )
+
+        self.check_circuit = check_circuit
+        self.check_model = check_model
+
+    def get_truth_value(self, circuit: Circuit, data: PassData) -> bool:
+        """Call this predicate, see :class:`PassPredicate` for more info."""
+        if self.check_circuit:
+            for gate in circuit.gate_set_no_blocks:
+                if gate.num_qudits > 2:
+                    _logger.debug(f'Found many-qudit gate in circuit: {gate}.')
+                    return True
+
+        if self.check_model:
+            for gate in data.gate_set:
+                if gate.num_qudits > 2:
+                    _logger.debug(f'Found many-qudit gate in model: {gate}.')
+                    return True
+
+        return False
diff --git a/bqskit/passes/control/predicates/single.py b/bqskit/passes/control/predicates/single.py
index c935862e2..ce7b33a6a 100644
--- a/bqskit/passes/control/predicates/single.py
+++ b/bqskit/passes/control/predicates/single.py
@@ -4,6 +4,11 @@
 import logging
 from typing import TYPE_CHECKING
 
+from bqskit.ir.gates.constant.sx import SXGate
+from bqskit.ir.gates.generalgate import GeneralGate
+from bqskit.ir.gates.parameterized.rx import RXGate
+from bqskit.ir.gates.parameterized.rz import RZGate
+from bqskit.ir.gates.parameterized.u1 import U1Gate
 from bqskit.passes.control.predicate import PassPredicate
 
 if TYPE_CHECKING:
@@ -23,12 +28,54 @@ class SinglePhysicalPredicate(PassPredicate):
 
     def get_truth_value(self, circuit: Circuit, data: PassData) -> bool:
         """Call this predicate, see :class:`PassPredicate` for more info."""
-        model = data.model
         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}.')
+            if gate not in data.gate_set:
+                _logger.debug(f'{gate} not in native set: {data.gate_set}.')
                 return False
 
         return True
+
+
+class NoSingleQuditGatesInModel(PassPredicate):
+    """A predicate that checks if the model has single qudit gates."""
+
+    def get_truth_value(self, circuit: Circuit, data: PassData) -> bool:
+        """Call this predicate, see :class:`PassPredicate` for more info."""
+        return len(data.gate_set.single_qudit_gates) == 0
+
+
+class HasGeneralSingleQuditGate(PassPredicate):
+    """A predicate that checks if the model has a general single qudit gate."""
+
+    def get_truth_value(self, circuit: Circuit, data: PassData) -> bool:
+        """Call this predicate, see :class:`PassPredicate` for more info."""
+        return any(
+            isinstance(g, GeneralGate)
+            for g in data.gate_set.single_qudit_gates
+        )
+
+
+class ZXGatePredicate(PassPredicate):
+    """A predicate that checks if the model has a RZ or U1 and SX or RX gate."""
+
+    def get_truth_value(self, circuit: Circuit, data: PassData) -> bool:
+        """Call this predicate, see :class:`PassPredicate` for more info."""
+        return (
+            (
+                RZGate() in data.gate_set.single_qudit_gates
+                or U1Gate() in data.gate_set.single_qudit_gates
+            ) and (
+                SXGate() in data.gate_set.single_qudit_gates
+                or RXGate() in data.gate_set.single_qudit_gates
+            )
+        )
+
+
+class AllConstantSingleQuditGates(PassPredicate):
+    """A predicate that checks if all single qudit gates are constant."""
+
+    def get_truth_value(self, circuit: Circuit, data: PassData) -> bool:
+        """Call this predicate, see :class:`PassPredicate` for more info."""
+        return all(g.is_constant() for g in data.gate_set.single_qudit_gates)
diff --git a/bqskit/passes/mapping/layout/pam.py b/bqskit/passes/mapping/layout/pam.py
index a0a4bc2e7..86e0c4708 100644
--- a/bqskit/passes/mapping/layout/pam.py
+++ b/bqskit/passes/mapping/layout/pam.py
@@ -82,10 +82,10 @@ async def run(self, circuit: Circuit, data: PassData) -> None:
         if not subgraph.is_fully_connected():
             raise RuntimeError('Cannot route circuit on disconnected qudits.')
 
-        pi = [i for i in range(circuit.num_qudits)]
+        pi = data.initial_mapping
         for _ in range(self.total_passes):
             self.forward_pass(circuit, pi, subgraph, perm_data)
             self.backward_pass(circuit, pi, subgraph)
 
-        data['initial_mapping'] = pi
+        data.initial_mapping = pi
         _logger.info(f'Found layout: {str(pi)}')
diff --git a/bqskit/passes/mapping/layout/sabre.py b/bqskit/passes/mapping/layout/sabre.py
index 7cecca197..b37150833 100644
--- a/bqskit/passes/mapping/layout/sabre.py
+++ b/bqskit/passes/mapping/layout/sabre.py
@@ -73,11 +73,11 @@ async def run(self, circuit: Circuit, data: PassData) -> None:
         if not subgraph.is_fully_connected():
             raise RuntimeError('Cannot layout circuit on disconnected qudits.')
 
-        pi = [i for i in range(circuit.num_qudits)]
+        pi = data.initial_mapping
         for _ in range(self.total_passes):
             self.forward_pass(circuit, pi, subgraph)
             self.backward_pass(circuit, pi, subgraph)
 
         # select qubits
+        data.initial_mapping = pi
         _logger.info(f'Found layout: {str(pi)}')
-        data['initial_mapping'] = pi
diff --git a/bqskit/passes/processing/__init__.py b/bqskit/passes/processing/__init__.py
index 2d7712107..00136b54f 100644
--- a/bqskit/passes/processing/__init__.py
+++ b/bqskit/passes/processing/__init__.py
@@ -3,7 +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.rebase import Rebase2QuditGatePass  # TODO: Remove
 from bqskit.passes.processing.scan import ScanningGateRemovalPass
 from bqskit.passes.processing.substitute import SubstitutePass
 
diff --git a/bqskit/passes/processing/rebase.py b/bqskit/passes/processing/rebase.py
index 4182b0293..ecbb5dde9 100644
--- a/bqskit/passes/processing/rebase.py
+++ b/bqskit/passes/processing/rebase.py
@@ -1,324 +1,13 @@
-"""This module implements the Rebase2QuditGatePass."""
+"""This module implements the Rebase2QuditGatePass (Deprecated Location)."""
 from __future__ import annotations
 
-import logging
-from typing import Any
-from typing import Sequence
+import warnings
 
-from bqskit.compiler.basepass import BasePass
-from bqskit.compiler.passdata import PassData
-from bqskit.ir.circuit import Circuit
-from bqskit.ir.gate import Gate
-from bqskit.ir.gates.parameterized.u3 import U3Gate
-from bqskit.ir.opt.cost.functions import HilbertSchmidtResidualsGenerator
-from bqskit.ir.opt.cost.generator import CostFunctionGenerator
-from bqskit.ir.point import CircuitPoint as Point
-from bqskit.runtime import get_runtime
-from bqskit.utils.typing import is_integer
-from bqskit.utils.typing import is_real_number
-from bqskit.utils.typing import is_sequence
-_logger = logging.getLogger(__name__)
+warnings.warn(
+    'The Rebase2QuditGatePass has moved to bqskit.passes.retarget.two.'
+    ' The old location will be removed and this warning will become an'
+    ' error in the future.',
+    DeprecationWarning,
+)
 
-
-class Rebase2QuditGatePass(BasePass):
-    """
-    The Rebase2QuditGatePass class.
-
-    Will use instantiation to change the a 2-qudit gate to a different one.
-    """
-
-    def __init__(
-        self,
-        gate_in_circuit: Gate | Sequence[Gate],
-        new_gate: Gate | Sequence[Gate],
-        max_depth: int = 3,
-        max_retries: int = -1,
-        success_threshold: float = 1e-10,
-        cost: CostFunctionGenerator = HilbertSchmidtResidualsGenerator(),
-        instantiate_options: dict[str, Any] = {},
-        single_qudit_gate: Gate = U3Gate(),
-    ) -> None:
-        """
-        Construct a Rebase2QuditGatePass.
-
-        Args:
-            gate_in_circuit (Gate | Sequence[Gate]): The two-qudit gate
-                or gates in the circuit that you want to replace.
-
-            new_gate (Gate | Sequence[Gate]): The two-qudit new gate or
-                gates you want to put in the circuit.
-
-            max_depth (int): The maximum number of new gates to replace
-                an old gate with. (Default: 3)
-
-            max_retries (int): The number of retries for the same gate
-                before we increase the maximum depth. If left as -1,
-                then never increase max depth. (Default: -1)
-
-            success_threshold (float): The distance threshold that
-                determines successful termintation. Measured in cost
-                described by the hilbert schmidt cost function.
-                (Default: 1e-10)
-
-            cost (CostFunction | None): The cost function that determines
-                successful removal of a gate.
-                (Default: HilbertSchmidtResidualsGenerator())
-
-            instantiate_options (dict[str: Any]): Options passed directly
-                to circuit.instantiate when instantiating circuit
-                templates. (Default: {})
-
-            single_qudit_gate (Gate): A single-qudit gate to fill
-                in between two-qudit gates.
-
-        Raises:
-            ValueError: If `gate_in_circuit` or `new_gate` is not a 2-qudit
-                gate.
-
-            ValueError: if `max_depth` is nonnegative.
-        """
-
-        if is_sequence(gate_in_circuit):
-            if any(not isinstance(g, Gate) for g in gate_in_circuit):
-                raise TypeError('Expected Gate or Gate list.')
-
-        elif not isinstance(gate_in_circuit, Gate):
-            raise TypeError(f'Expected Gate, got {type(gate_in_circuit)}.')
-
-        else:
-            gate_in_circuit = [gate_in_circuit]
-
-        if any(g.num_qudits != 2 for g in gate_in_circuit):
-            raise ValueError('Expected 2-qudit gate.')
-
-        if is_sequence(new_gate):
-            if any(not isinstance(g, Gate) for g in new_gate):
-                raise TypeError('Expected Gate or Gate list.')
-
-        elif not isinstance(new_gate, Gate):
-            raise TypeError(f'Expected Gate, got {type(new_gate)}.')
-
-        else:
-            new_gate = [new_gate]
-
-        if any(g.num_qudits != 2 for g in new_gate):
-            raise ValueError('Expected 2-qudit gate.')
-
-        if not is_integer(max_depth):
-            raise TypeError(f'Expected integer, got {max_depth}.')
-
-        if max_depth < 0:
-            raise ValueError(f'Expected nonnegative depth, got: {max_depth}.')
-
-        if not is_integer(max_retries):
-            raise TypeError(f'Expected integer, got {max_retries}.')
-
-        if not is_real_number(success_threshold):
-            raise TypeError(
-                'Expected real number for success_threshold'
-                ', got %s' % type(success_threshold),
-            )
-
-        if not isinstance(cost, CostFunctionGenerator):
-            raise TypeError(
-                'Expected cost to be a CostFunctionGenerator, got %s'
-                % type(cost),
-            )
-
-        if not isinstance(instantiate_options, dict):
-            raise TypeError(
-                'Expected dictionary for instantiate_options, got %s.'
-                % type(instantiate_options),
-            )
-
-        if not isinstance(single_qudit_gate, Gate):
-            raise TypeError(f'Expected Gate, got {type(single_qudit_gate)}.')
-
-        if single_qudit_gate.num_qudits != 1:
-            raise ValueError(
-                f'Expected single-qudit gate, got {single_qudit_gate}.',
-            )
-
-        self.gates = gate_in_circuit
-        self.ngates = new_gate
-        self.max_depth = max_depth
-        self.max_retries = max_retries
-        self.success_threshold = success_threshold
-        self.cost = cost
-        self.instantiate_options: dict[str, Any] = {
-            'dist_tol': self.success_threshold,
-            'min_iters': 100,
-        }
-        self.instantiate_options.update(instantiate_options)
-        self.sq = single_qudit_gate
-        self.generate_new_gate_templates()
-
-    async def run(self, circuit: Circuit, data: PassData) -> None:
-        """Perform the pass's operation, see :class:`BasePass` for more."""
-        instantiate_options = self.instantiate_options.copy()
-        if 'seed' not in instantiate_options:
-            instantiate_options['seed'] = data.seed
-        _logger.debug(f'Rebasing gates from {self.gates} to {self.ngates}.')
-
-        target = self.get_target(circuit, data)
-
-        for g in self.gates:
-            # Track retries to check for no progress
-            num_retries = 0
-            prev_count = circuit.count(g)
-
-            while g in circuit.gate_set:
-                # Change the seed every iteration to prevent stalls
-                if instantiate_options['seed'] is not None:
-                    instantiate_options['seed'] += 1
-
-                # Check if we made progress from last loop
-                gates_left = circuit.count(g)
-                if prev_count == gates_left:
-                    num_retries += 1
-                else:
-                    prev_count = gates_left
-                    num_retries = 0
-
-                # Group together a 2-qubit block composed of gates from old set
-                point = self.group_near_gates(circuit, circuit.point(g))
-                circuits_with_new_gate = []
-                for circ in self.circs:
-                    circuit_copy = circuit.copy()
-                    circuit_copy.replace_with_circuit(point, circ)
-                    circuits_with_new_gate.append(circuit_copy)
-
-                # If we have exceeded the number of retries, up the max depth
-                if self.max_retries >= 0 and num_retries > self.max_retries:
-                    _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)
-
-                instantiated_circuits = await get_runtime().map(
-                    Circuit.instantiate,
-                    circuits_with_new_gate,
-                    target=target,
-                    **instantiate_options,
-                )
-
-                dists = [self.cost(c, target) for c in instantiated_circuits]
-
-                # Find the successful circuit with the least gates
-                best_index = None
-                best_count = self.max_depth + 2
-                for i, dist in enumerate(dists):
-                    if dist < self.success_threshold:
-                        if self.counts[i] < best_count:
-                            best_index = i
-                            best_count = self.counts[i]
-
-                if best_index is None:
-                    circuit.unfold(point)
-                    continue
-
-                _logger.debug(self.replaced_log_messages[best_index])
-                circuit.become(instantiated_circuits[best_index])
-
-    def group_near_gates(self, circuit: Circuit, center: Point) -> Point:
-        """Group gates similar to the gate at center on the same qubits."""
-        op = circuit[center]
-        qubits = op.location
-        counts = {g: 0.0 for g in self.gates}
-        counts[op.gate] += 1.0
-
-        # Go to the left until cant
-        i = 0
-        moving_left = True
-        while moving_left:
-            i += 1
-            if center.cycle - i < 0:
-                i = center.cycle
-                break
-            for q in qubits:
-                point = (center.cycle - i, q)
-                if not circuit.is_point_idle(point):
-                    lop = circuit[point]
-                    if any(p not in qubits for p in lop.location):
-                        i -= 1
-                        moving_left = False
-                        break
-                    if lop.num_qudits != 1 and lop.gate not in self.gates:
-                        i -= 1
-                        moving_left = False
-                        break
-                    if lop.num_qudits == 2:
-                        counts[lop.gate] += 0.5
-
-        j = 0
-        moving_right = True
-        while moving_right:
-            j += 1
-            if center.cycle + j >= circuit.num_cycles:
-                j = circuit.num_cycles - center.cycle - 1
-                break
-            for q in qubits:
-                point = (center.cycle + j, q)
-                if not circuit.is_point_idle(point):
-                    rop = circuit[point]
-                    if any(p not in qubits for p in rop.location):
-                        j -= 1
-                        moving_right = False
-                        break
-                    if rop.num_qudits != 1 and rop.gate not in self.gates:
-                        j -= 1
-                        moving_right = False
-                        break
-                    if rop.num_qudits == 2:
-                        counts[rop.gate] += 0.5
-
-        region = {q: (center.cycle - i, center.cycle + j) for q in qubits}
-        grouped_gate_str = ', '.join([
-            f'{int(c)} {g}' + ('s' if c > 1 else '')
-            for g, c in counts.items()
-        ])
-        _logger.debug(f'Grouped together {grouped_gate_str}.')
-        return circuit.fold(region)
-
-    def generate_new_gate_templates(self) -> None:
-        """Generate the templates to be instantiated over old circuits."""
-        self.circs = []
-        self.counts = []
-
-        circ = Circuit(2, self.ngates[0].radixes)
-        circ.append_gate(self.sq, 0)
-        circ.append_gate(self.sq, 1)
-        self.circs.append(circ)
-        self.counts.append(0)
-
-        for g in self.ngates:
-            for i in range(1, self.max_depth + 1):
-                circ = Circuit(2, self.ngates[0].radixes)
-                circ.append_gate(self.sq, 0)
-                circ.append_gate(self.sq, 1)
-
-                for _ in range(i):
-                    circ.append_gate(g, (0, 1))
-                    circ.append_gate(self.sq, 0)
-                    circ.append_gate(self.sq, 1)
-
-                self.counts.append(i)
-                self.circs.append(circ)
-
-        # Add overdrive circuit, incase we exceed retry limit
-        self.overdrive = self.circs[-1].copy()
-        self.overdrive.append_gate(self.ngates[-1], (0, 1))
-        self.overdrive.append_gate(self.sq, 0)
-        self.overdrive.append_gate(self.sq, 1)
-        self.counts.append(self.counts[-1] + 1)
-
-        # Preprocess log messages
-        self.replaced_log_messages = []
-        for circ in self.circs + [self.overdrive]:
-            counts = [circ.count(g) for g in circ.gate_set]
-            gate_count_str = ', '.join([
-                f'{c} {g}' + ('s' if c > 1 else '')
-                for c, g in zip(counts, circ.gate_set)
-            ])
-            msg = f'Replaced gate with {gate_count_str}.'
-            self.replaced_log_messages.append(msg)
+from bqskit.passes.retarget.two import Rebase2QuditGatePass  # noqa
diff --git a/bqskit/passes/retarget/__init__.py b/bqskit/passes/retarget/__init__.py
new file mode 100644
index 000000000..6638eb1e4
--- /dev/null
+++ b/bqskit/passes/retarget/__init__.py
@@ -0,0 +1,12 @@
+"""This package implements BQSKit's gate retargeting passes."""
+from __future__ import annotations
+
+from bqskit.passes.retarget.auto import AutoRebase2QuditGatePass
+from bqskit.passes.retarget.general import GeneralSQDecomposition
+from bqskit.passes.retarget.two import Rebase2QuditGatePass
+
+__all__ = [
+    'AutoRebase2QuditGatePass',
+    'GeneralSQDecomposition',
+    'Rebase2QuditGatePass',
+]
diff --git a/bqskit/passes/retarget/auto.py b/bqskit/passes/retarget/auto.py
new file mode 100644
index 000000000..5380991cc
--- /dev/null
+++ b/bqskit/passes/retarget/auto.py
@@ -0,0 +1,175 @@
+"""This module implements the Rebase2QuditGatePass."""
+from __future__ import annotations
+
+import logging
+from typing import Any
+
+from bqskit.compiler.passdata import PassData
+from bqskit.ir.circuit import Circuit
+from bqskit.ir.opt.cost.functions import HilbertSchmidtResidualsGenerator
+from bqskit.ir.opt.cost.generator import CostFunctionGenerator
+from bqskit.passes.retarget.two import Rebase2QuditGatePass
+from bqskit.runtime import get_runtime
+from bqskit.utils.typing import is_integer
+from bqskit.utils.typing import is_real_number
+_logger = logging.getLogger(__name__)
+
+
+class AutoRebase2QuditGatePass(Rebase2QuditGatePass):
+    """
+    The AutoRebase2QuditGatePass class.
+
+    Will use instantiation to change the a 2-qudit gate to a different one.
+    """
+
+    def __init__(
+        self,
+        max_depth: int = 3,
+        max_retries: int = -1,
+        success_threshold: float = 1e-10,
+        cost: CostFunctionGenerator = HilbertSchmidtResidualsGenerator(),
+        instantiate_options: dict[str, Any] = {},
+    ) -> None:
+        """
+        Construct a AutoRebase2QuditGatePass.
+
+        Args:
+            max_depth (int): The maximum number of new gates to replace
+                an old gate with. (Default: 3)
+
+            max_retries (int): The number of retries for the same gate
+                before we increase the maximum depth. If left as -1,
+                then never increase max depth. (Default: -1)
+
+            success_threshold (float): The distance threshold that
+                determines successful termintation. Measured in cost
+                described by the hilbert schmidt cost function.
+                (Default: 1e-10)
+
+            cost (CostFunction | None): The cost function that determines
+                successful removal of a gate.
+                (Default: HilbertSchmidtResidualsGenerator())
+
+            instantiate_options (dict[str: Any]): Options passed directly
+                to circuit.instantiate when instantiating circuit
+                templates. (Default: {})
+
+        Raises:
+            ValueError: if `max_depth` is nonnegative.
+        """
+        if not is_integer(max_depth):
+            raise TypeError(f'Expected integer, got {max_depth}.')
+
+        if max_depth < 0:
+            raise ValueError(f'Expected nonnegative depth, got: {max_depth}.')
+
+        if not is_integer(max_retries):
+            raise TypeError(f'Expected integer, got {max_retries}.')
+
+        if not is_real_number(success_threshold):
+            raise TypeError(
+                'Expected real number for success_threshold'
+                ', got %s' % type(success_threshold),
+            )
+
+        if not isinstance(cost, CostFunctionGenerator):
+            raise TypeError(
+                'Expected cost to be a CostFunctionGenerator, got %s'
+                % type(cost),
+            )
+
+        if not isinstance(instantiate_options, dict):
+            raise TypeError(
+                'Expected dictionary for instantiate_options, got %s.'
+                % type(instantiate_options),
+            )
+        self.max_depth = max_depth
+        self.max_retries = max_retries
+        self.success_threshold = success_threshold
+        self.cost = cost
+        self.instantiate_options: dict[str, Any] = {
+            'dist_tol': self.success_threshold,
+            'min_iters': 100,
+        }
+        self.instantiate_options.update(instantiate_options)
+
+    async def run(self, circuit: Circuit, data: PassData) -> None:
+        """Perform the pass's operation, see :class:`BasePass` for more."""
+        new_gates = [g for g in data.gate_set if g.num_qudits == 2]
+        old_gates = [
+            g for g in circuit.gate_set_no_blocks
+            if g not in new_gates and g.num_qudits == 2
+        ]
+        circs, counts, overdrive, msgs = self.generate_new_gate_templates(
+            new_gates,
+            data.gate_set.get_general_sq_gate(),
+        )
+
+        instantiate_options = self.instantiate_options.copy()
+        if 'seed' not in instantiate_options:
+            instantiate_options['seed'] = data.seed
+        _logger.debug(f'Rebasing gates from {old_gates} to {new_gates}.')
+
+        target = self.get_target(circuit, data)
+
+        for g in old_gates:
+            # Track retries to check for no progress
+            num_retries = 0
+            prev_count = circuit.count(g)
+
+            while g in circuit.gate_set:
+                # Change the seed every iteration to prevent stalls
+                if instantiate_options['seed'] is not None:
+                    instantiate_options['seed'] += 1
+
+                # Check if we made progress from last loop
+                gates_left = circuit.count(g)
+                if prev_count == gates_left:
+                    num_retries += 1
+                else:
+                    prev_count = gates_left
+                    num_retries = 0
+
+                # Group together a 2-qubit block composed of gates from old set
+                point = self.group_near_gates(
+                    circuit,
+                    circuit.point(g),
+                    old_gates,
+                )
+                circuits_with_new_gate = []
+                for circ in circs:
+                    circuit_copy = circuit.copy()
+                    circuit_copy.replace_with_circuit(point, circ)
+                    circuits_with_new_gate.append(circuit_copy)
+
+                # If we have exceeded the number of retries, up the max depth
+                if self.max_retries >= 0 and num_retries > self.max_retries:
+                    _logger.debug('Exceeded max retries, increasing depth.')
+                    circuit_copy = circuit.copy()
+                    circuit_copy.replace_with_circuit(point, overdrive)
+                    circuits_with_new_gate.append(circuit_copy)
+
+                instantiated_circuits = await get_runtime().map(
+                    Circuit.instantiate,
+                    circuits_with_new_gate,
+                    target=target,
+                    **instantiate_options,
+                )
+
+                dists = [self.cost(c, target) for c in instantiated_circuits]
+
+                # Find the successful circuit with the least gates
+                best_index = None
+                best_count = self.max_depth + 2
+                for i, dist in enumerate(dists):
+                    if dist < self.success_threshold:
+                        if counts[i] < best_count:
+                            best_index = i
+                            best_count = counts[i]
+
+                if best_index is None:
+                    circuit.unfold(point)
+                    continue
+
+                _logger.debug(msgs[best_index])
+                circuit.become(instantiated_circuits[best_index])
diff --git a/bqskit/passes/retarget/general.py b/bqskit/passes/retarget/general.py
new file mode 100644
index 000000000..83d6ccc8e
--- /dev/null
+++ b/bqskit/passes/retarget/general.py
@@ -0,0 +1,42 @@
+"""This module implements the GeneralSQDecomposition."""
+from __future__ import annotations
+
+from bqskit.compiler.basepass import BasePass
+from bqskit.compiler.passdata import PassData
+from bqskit.ir.circuit import Circuit
+from bqskit.ir.gates.generalgate import GeneralGate
+
+
+class GeneralSQDecomposition(BasePass):
+    """Convert to a general single-qudit gate from the model's gate set."""
+
+    async def run(self, circuit: Circuit, data: PassData) -> None:
+        """Perform the pass's operation, see :class:`BasePass` for more."""
+
+        if circuit.num_qudits != 1:
+            raise ValueError(
+                'Cannot convert multi-qudit circuit into'
+                ' general single-qudit gate.',
+            )
+
+        # Check radixes
+        radix = circuit.radixes[0]
+
+        # Pick a general gate
+        general_gate: GeneralGate | None = None
+
+        for gate in data.gate_set.single_qudit_gates:
+            if isinstance(gate, GeneralGate):
+                if gate.radixes[0] == radix:
+                    general_gate = gate
+                    break
+
+        if general_gate is None:
+            raise ValueError(
+                f'No general single-qudit gate with radix {radix} in gate set.',
+            )
+
+        utry = circuit.get_unitary()
+        new_circuit = Circuit(1)
+        new_circuit.append_gate(general_gate, 0, general_gate.calc_params(utry))
+        circuit.become(new_circuit)
diff --git a/bqskit/passes/retarget/two.py b/bqskit/passes/retarget/two.py
new file mode 100644
index 000000000..93ed224eb
--- /dev/null
+++ b/bqskit/passes/retarget/two.py
@@ -0,0 +1,341 @@
+"""This module implements the Rebase2QuditGatePass."""
+from __future__ import annotations
+
+import logging
+from typing import Any
+from typing import Sequence
+
+from bqskit.compiler.basepass import BasePass
+from bqskit.compiler.passdata import PassData
+from bqskit.ir.circuit import Circuit
+from bqskit.ir.gate import Gate
+from bqskit.ir.gates.parameterized.u3 import U3Gate
+from bqskit.ir.opt.cost.functions import HilbertSchmidtResidualsGenerator
+from bqskit.ir.opt.cost.generator import CostFunctionGenerator
+from bqskit.ir.point import CircuitPoint as Point
+from bqskit.runtime import get_runtime
+from bqskit.utils.typing import is_integer
+from bqskit.utils.typing import is_real_number
+from bqskit.utils.typing import is_sequence
+_logger = logging.getLogger(__name__)
+
+
+class Rebase2QuditGatePass(BasePass):
+    """
+    The Rebase2QuditGatePass class.
+
+    Will use instantiation to change the a 2-qudit gate to a different one.
+    """
+
+    def __init__(
+        self,
+        gate_in_circuit: Gate | Sequence[Gate],
+        new_gate: Gate | Sequence[Gate],
+        max_depth: int = 3,
+        max_retries: int = -1,
+        success_threshold: float = 1e-10,
+        cost: CostFunctionGenerator = HilbertSchmidtResidualsGenerator(),
+        instantiate_options: dict[str, Any] = {},
+        single_qudit_gate: Gate = U3Gate(),
+    ) -> None:
+        """
+        Construct a Rebase2QuditGatePass.
+
+        Args:
+            gate_in_circuit (Gate | Sequence[Gate]): The two-qudit gate
+                or gates in the circuit that you want to replace.
+
+            new_gate (Gate | Sequence[Gate]): The two-qudit new gate or
+                gates you want to put in the circuit.
+
+            max_depth (int): The maximum number of new gates to replace
+                an old gate with. (Default: 3)
+
+            max_retries (int): The number of retries for the same gate
+                before we increase the maximum depth. If left as -1,
+                then never increase max depth. (Default: -1)
+
+            success_threshold (float): The distance threshold that
+                determines successful termintation. Measured in cost
+                described by the hilbert schmidt cost function.
+                (Default: 1e-10)
+
+            cost (CostFunction | None): The cost function that determines
+                successful removal of a gate.
+                (Default: HilbertSchmidtResidualsGenerator())
+
+            instantiate_options (dict[str: Any]): Options passed directly
+                to circuit.instantiate when instantiating circuit
+                templates. (Default: {})
+
+            single_qudit_gate (Gate): A single-qudit gate to fill
+                in between two-qudit gates.
+
+        Raises:
+            ValueError: If `gate_in_circuit` or `new_gate` is not a 2-qudit
+                gate.
+
+            ValueError: if `max_depth` is nonnegative.
+        """
+
+        if is_sequence(gate_in_circuit):
+            if any(not isinstance(g, Gate) for g in gate_in_circuit):
+                raise TypeError('Expected Gate or Gate list.')
+
+        elif not isinstance(gate_in_circuit, Gate):
+            raise TypeError(f'Expected Gate, got {type(gate_in_circuit)}.')
+
+        else:
+            gate_in_circuit = [gate_in_circuit]
+
+        if any(g.num_qudits != 2 for g in gate_in_circuit):
+            raise ValueError('Expected 2-qudit gate.')
+
+        if is_sequence(new_gate):
+            if any(not isinstance(g, Gate) for g in new_gate):
+                raise TypeError('Expected Gate or Gate list.')
+
+        elif not isinstance(new_gate, Gate):
+            raise TypeError(f'Expected Gate, got {type(new_gate)}.')
+
+        else:
+            new_gate = [new_gate]
+
+        if any(g.num_qudits != 2 for g in new_gate):
+            raise ValueError('Expected 2-qudit gate.')
+
+        if not is_integer(max_depth):
+            raise TypeError(f'Expected integer, got {max_depth}.')
+
+        if max_depth < 0:
+            raise ValueError(f'Expected nonnegative depth, got: {max_depth}.')
+
+        if not is_integer(max_retries):
+            raise TypeError(f'Expected integer, got {max_retries}.')
+
+        if not is_real_number(success_threshold):
+            raise TypeError(
+                'Expected real number for success_threshold'
+                ', got %s' % type(success_threshold),
+            )
+
+        if not isinstance(cost, CostFunctionGenerator):
+            raise TypeError(
+                'Expected cost to be a CostFunctionGenerator, got %s'
+                % type(cost),
+            )
+
+        if not isinstance(instantiate_options, dict):
+            raise TypeError(
+                'Expected dictionary for instantiate_options, got %s.'
+                % type(instantiate_options),
+            )
+
+        if not isinstance(single_qudit_gate, Gate):
+            raise TypeError(f'Expected Gate, got {type(single_qudit_gate)}.')
+
+        if single_qudit_gate.num_qudits != 1:
+            raise ValueError(
+                f'Expected single-qudit gate, got {single_qudit_gate}.',
+            )
+
+        self.gates: list[Gate] = list(gate_in_circuit)
+        self.ngates = new_gate
+        self.max_depth = max_depth  # TODO: Compute from weyl chamber coords
+        self.max_retries = max_retries
+        self.success_threshold = success_threshold
+        self.cost = cost
+        self.instantiate_options: dict[str, Any] = {
+            'dist_tol': self.success_threshold,
+            'min_iters': 100,
+        }
+        self.instantiate_options.update(instantiate_options)
+        self.sq = single_qudit_gate
+        temps = self.generate_new_gate_templates(self.ngates, self.sq)
+        self.circs, self.counts, self.overdrive, msgs = temps
+        self.replaced_log_messages = msgs
+
+    async def run(self, circuit: Circuit, data: PassData) -> None:
+        """Perform the pass's operation, see :class:`BasePass` for more."""
+        instantiate_options = self.instantiate_options.copy()
+        if 'seed' not in instantiate_options:
+            instantiate_options['seed'] = data.seed
+        _logger.debug(f'Rebasing gates from {self.gates} to {self.ngates}.')
+
+        target = self.get_target(circuit, data)
+
+        for g in self.gates:
+            # Track retries to check for no progress
+            num_retries = 0
+            prev_count = circuit.count(g)
+
+            while g in circuit.gate_set:
+                # Change the seed every iteration to prevent stalls
+                if instantiate_options['seed'] is not None:
+                    instantiate_options['seed'] += 1
+
+                # Check if we made progress from last loop
+                gates_left = circuit.count(g)
+                if prev_count == gates_left:
+                    num_retries += 1
+                else:
+                    prev_count = gates_left
+                    num_retries = 0
+
+                # Group together a 2-qubit block composed of gates from old set
+                point = self.group_near_gates(
+                    circuit,
+                    circuit.point(g),
+                    self.gates,
+                )
+                circuits_with_new_gate = []
+                for circ in self.circs:
+                    circuit_copy = circuit.copy()
+                    circuit_copy.replace_with_circuit(point, circ)
+                    circuits_with_new_gate.append(circuit_copy)
+
+                # If we have exceeded the number of retries, up the max depth
+                if self.max_retries >= 0 and num_retries > self.max_retries:
+                    _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)
+
+                instantiated_circuits = await get_runtime().map(
+                    Circuit.instantiate,
+                    circuits_with_new_gate,
+                    target=target,
+                    **instantiate_options,
+                )
+
+                dists = [self.cost(c, target) for c in instantiated_circuits]
+
+                # Find the successful circuit with the least gates
+                best_index = None
+                best_count = self.max_depth + 2
+                for i, dist in enumerate(dists):
+                    if dist < self.success_threshold:
+                        if self.counts[i] < best_count:
+                            best_index = i
+                            best_count = self.counts[i]
+
+                if best_index is None:
+                    circuit.unfold(point)
+                    continue
+
+                _logger.debug(self.replaced_log_messages[best_index])
+                circuit.become(instantiated_circuits[best_index])
+
+    def group_near_gates(
+        self,
+        circuit: Circuit,
+        center: Point,
+        gates: list[Gate],
+    ) -> Point:
+        """Group gates similar to the gate at center on the same qubits."""
+        op = circuit[center]
+        qubits = op.location
+        counts = {g: 0.0 for g in gates}
+        counts[op.gate] += 1.0
+
+        # Go to the left until cant
+        i = 0
+        moving_left = True
+        while moving_left:
+            i += 1
+            if center.cycle - i < 0:
+                i = center.cycle
+                break
+            for q in qubits:
+                point = (center.cycle - i, q)
+                if not circuit.is_point_idle(point):
+                    lop = circuit[point]
+                    if any(p not in qubits for p in lop.location):
+                        i -= 1
+                        moving_left = False
+                        break
+                    if lop.num_qudits != 1 and lop.gate not in gates:
+                        i -= 1
+                        moving_left = False
+                        break
+                    if lop.num_qudits == 2:
+                        counts[lop.gate] += 0.5
+
+        j = 0
+        moving_right = True
+        while moving_right:
+            j += 1
+            if center.cycle + j >= circuit.num_cycles:
+                j = circuit.num_cycles - center.cycle - 1
+                break
+            for q in qubits:
+                point = (center.cycle + j, q)
+                if not circuit.is_point_idle(point):
+                    rop = circuit[point]
+                    if any(p not in qubits for p in rop.location):
+                        j -= 1
+                        moving_right = False
+                        break
+                    if rop.num_qudits != 1 and rop.gate not in gates:
+                        j -= 1
+                        moving_right = False
+                        break
+                    if rop.num_qudits == 2:
+                        counts[rop.gate] += 0.5
+
+        region = {q: (center.cycle - i, center.cycle + j) for q in qubits}
+        grouped_gate_str = ', '.join([
+            f'{int(c)} {g}' + ('s' if c > 1 else '')
+            for g, c in counts.items()
+        ])
+        _logger.debug(f'Grouped together {grouped_gate_str}.')
+        return circuit.fold(region)
+
+    def generate_new_gate_templates(
+        self,
+        new_gates: Sequence[Gate],
+        single_qudit_gate: Gate,
+    ) -> tuple[list[Circuit], list[int], Circuit, list[str]]:
+        """Generate the templates to be instantiated over old circuits."""
+        circs: list[Circuit] = []
+        counts: list[int] = []
+
+        circ = Circuit(2, new_gates[0].radixes)
+        circ.append_gate(single_qudit_gate, 0)
+        circ.append_gate(single_qudit_gate, 1)
+        circs.append(circ)
+        counts.append(0)
+
+        for g in new_gates:
+            for i in range(1, self.max_depth + 1):
+                circ = Circuit(2, new_gates[0].radixes)
+                circ.append_gate(single_qudit_gate, 0)
+                circ.append_gate(single_qudit_gate, 1)
+
+                for _ in range(i):
+                    circ.append_gate(g, (0, 1))
+                    circ.append_gate(single_qudit_gate, 0)
+                    circ.append_gate(single_qudit_gate, 1)
+
+                counts.append(i)
+                circs.append(circ)
+
+        # Add overdrive circuit, incase we exceed retry limit
+        overdrive = circs[-1].copy()
+        overdrive.append_gate(new_gates[-1], (0, 1))
+        overdrive.append_gate(single_qudit_gate, 0)
+        overdrive.append_gate(single_qudit_gate, 1)
+        counts.append(counts[-1] + 1)
+
+        # Preprocess log messages
+        replaced_log_messages = []
+        for circ in circs + [overdrive]:
+            g_counts = [circ.count(g) for g in circ.gate_set]
+            gate_count_str = ', '.join([
+                f'{c} {g}' + ('s' if c > 1 else '')
+                for c, g in zip(g_counts, circ.gate_set)
+            ])
+            msg = f'Replaced gate with {gate_count_str}.'
+            replaced_log_messages.append(msg)
+
+        return circs, counts, overdrive, replaced_log_messages
diff --git a/bqskit/passes/rules/u3.py b/bqskit/passes/rules/u3.py
index 096c50918..cdf5ac185 100644
--- a/bqskit/passes/rules/u3.py
+++ b/bqskit/passes/rules/u3.py
@@ -19,12 +19,12 @@ async def run(self, circuit: Circuit, data: PassData) -> None:
 
         if circuit.num_qudits != 1:
             raise ValueError(
-                'Cannot convert multi-qudit circuit into ZXZXZ sequence.',
+                'Cannot convert multi-qudit circuit into U3 gate.',
             )
 
         if circuit.radixes[0] != 2:
             raise ValueError(
-                'Cannot convert non-qubit circuit into ZXZXZ sequence.',
+                'Cannot convert non-qubit circuit into U3 gate.',
             )
 
         utry = circuit.get_unitary()
diff --git a/bqskit/passes/rules/zxzxz.py b/bqskit/passes/rules/zxzxz.py
index 7ebd6e075..44aa8edf0 100644
--- a/bqskit/passes/rules/zxzxz.py
+++ b/bqskit/passes/rules/zxzxz.py
@@ -9,7 +9,9 @@
 from bqskit.compiler.passdata import PassData
 from bqskit.ir.circuit import Circuit
 from bqskit.ir.gates.constant.sx import SqrtXGate
+from bqskit.ir.gates.parameterized.rx import RXGate
 from bqskit.ir.gates.parameterized.rz import RZGate
+from bqskit.ir.gates.parameterized.u1 import U1Gate
 
 
 class ZXZXZDecomposition(BasePass):
@@ -19,6 +21,33 @@ class ZXZXZDecomposition(BasePass):
     Convert a single-qubit circuit to ZXZXZ sequence.
     """
 
+    def __init__(
+        self,
+        always_use_rx: bool = False,
+        always_use_u1: bool = False,
+    ) -> None:
+        """
+        Construct a ZXZXZDecomposition pass.
+
+        Args:
+            always_use_rx (bool): If True, always use RX instead of SX.
+
+            always_use_u1 (bool): If True, always use U1 instead of RZ.
+        """
+
+        if not isinstance(always_use_rx, bool):
+            raise TypeError(
+                f'Expected bool for always_use_rx, got {type(always_use_rx)}.',
+            )
+
+        if not isinstance(always_use_u1, bool):
+            raise TypeError(
+                f'Expected bool for always_use_u1, got {type(always_use_u1)}.',
+            )
+
+        self.always_use_rx = always_use_rx
+        self.always_use_u1 = always_use_u1
+
     async def run(self, circuit: Circuit, data: PassData) -> None:
         """Perform the pass's operation, see :class:`BasePass` for more."""
 
@@ -32,6 +61,14 @@ async def run(self, circuit: Circuit, data: PassData) -> None:
                 'Cannot convert non-qubit circuit into ZXZXZ sequence.',
             )
 
+        # Decide on RX or SX
+        no_sx = RXGate() in data.gate_set and SqrtXGate() not in data.gate_set
+        use_rx = self.always_use_rx or no_sx
+
+        # Decide on RZ or U1
+        no_rz = U1Gate() in data.gate_set and RZGate() not in data.gate_set
+        use_u1 = self.always_use_u1 or no_rz
+
         utry = circuit.get_unitary()
 
         # Calculate params
@@ -48,9 +85,30 @@ async def run(self, circuit: Circuit, data: PassData) -> None:
         l = (l + np.pi) % (2 * np.pi) - np.pi
 
         new_circuit = Circuit(1)
-        new_circuit.append_gate(RZGate(), 0, [l])
-        new_circuit.append_gate(SqrtXGate(), 0)
-        new_circuit.append_gate(RZGate(), 0, [t])
-        new_circuit.append_gate(SqrtXGate(), 0)
-        new_circuit.append_gate(RZGate(), 0, [p])
+
+        if use_u1:
+            new_circuit.append_gate(U1Gate(), 0, [l])
+        else:
+            new_circuit.append_gate(RZGate(), 0, [l])
+
+        if use_rx:
+            new_circuit.append_gate(RXGate(), 0, [np.pi / 2])
+        else:
+            new_circuit.append_gate(SqrtXGate(), 0)
+
+        if use_u1:
+            new_circuit.append_gate(U1Gate(), 0, [t])
+        else:
+            new_circuit.append_gate(RZGate(), 0, [t])
+
+        if use_rx:
+            new_circuit.append_gate(RXGate(), 0, [np.pi / 2])
+        else:
+            new_circuit.append_gate(SqrtXGate(), 0)
+
+        if use_u1:
+            new_circuit.append_gate(U1Gate(), 0, [p])
+        else:
+            new_circuit.append_gate(RZGate(), 0, [p])
+
         circuit.become(new_circuit)
diff --git a/bqskit/passes/search/generators/single.py b/bqskit/passes/search/generators/single.py
index 0b0afcfdd..e4a72a32f 100644
--- a/bqskit/passes/search/generators/single.py
+++ b/bqskit/passes/search/generators/single.py
@@ -2,6 +2,7 @@
 from __future__ import annotations
 
 import logging
+from typing import Sequence
 
 from bqskit.compiler.passdata import PassData
 from bqskit.ir.circuit import Circuit
@@ -10,6 +11,7 @@
 from bqskit.qis.state.state import StateVector
 from bqskit.qis.state.system import StateSystem
 from bqskit.qis.unitary.unitarymatrix import UnitaryMatrix
+from bqskit.utils.typing import is_sequence
 _logger = logging.getLogger(__name__)
 
 
@@ -21,12 +23,18 @@ class SingleQuditLayerGenerator(LayerGenerator):
     hetergenous gate support.
     """
 
-    def __init__(self, gates: list[Gate], allow_repeats: bool = False) -> None:
+    def __init__(
+        self,
+        gates: Sequence[Gate] | None,
+        allow_repeats: bool = False,
+    ) -> None:
         """
         Construct a SingleQuditLayerGenerator.
 
         Args:
-            gates (Gate): The single-qudit gate set.
+            gates (Sequence[Gate] | None): The single-qudit gate list. If None,
+                then the generator will read the pass data for the model's
+                gate set.
 
             allow_repeats (bool): By default this is set to False,
                 and the generator will not generate a circuit that
@@ -43,22 +51,28 @@ def __init__(self, gates: list[Gate], allow_repeats: bool = False) -> None:
 
             ValueError: If `gates` is empty.
         """
-        self.radix = None
-        for gate in gates:
-            if not isinstance(gate, Gate):
-                raise TypeError(f'Expected gate, got {type(gate)}.')
+        if gates is not None:
+            self.radix = None
+            if not is_sequence(gates):
+                raise TypeError(
+                    f'Expected sequence of gates, got {type(gates)}.',
+                )
+
+            for gate in gates:
+                if not isinstance(gate, Gate):
+                    raise TypeError(f'Expected gate, got {type(gate)}.')
 
-            if gate.num_qudits > 1:
-                raise ValueError(f'Expected single-qudit gate, got {gate}.')
+                if gate.num_qudits > 1:
+                    raise ValueError(f'Expected single-qudit gate, got {gate}.')
 
-            if self.radix is None:
-                self.radix = gate.radixes[0]
+                if self.radix is None:
+                    self.radix = gate.radixes[0]
 
-            elif gate.radixes[0] != self.radix:
-                raise ValueError('Gate set has a radix inconsistency.')
+                elif gate.radixes[0] != self.radix:
+                    raise ValueError('Gate set has a radix inconsistency.')
 
-        if len(gates) == 0:
-            raise ValueError('Empty gate set.')
+            if len(gates) == 0:
+                raise ValueError('Empty gate set.')
 
         self.gates = gates
         self.allow_repeats = allow_repeats
@@ -77,6 +91,11 @@ def gen_initial_layer(
 
             ValueError: If `target` is not single-qudit.
         """
+        if self.gates is None:
+            self.gates = list(data.gate_set.single_qudit_gates)
+            self.radix = self.gates[0].radixes[0]
+            if not all(g.radixes[0] == self.radix for g in self.gates):
+                raise RuntimeError('Gate set has a radix inconsistency.')
 
         if not isinstance(target, (UnitaryMatrix, StateVector, StateSystem)):
             raise TypeError(
@@ -111,7 +130,7 @@ def gen_successors(self, circuit: Circuit, data: PassData) -> list[Circuit]:
 
         # Generate successors
         successors = []
-        for gate in self.gates:
+        for gate in self.gates:  # type: ignore  # self.gates is set earlier
 
             # Skip adding a gate that repeats the last one on the circuit
             if not self.allow_repeats and gate == last_gate:
diff --git a/bqskit/passes/search/generators/wide.py b/bqskit/passes/search/generators/wide.py
index 1da89ed50..e373c9fd6 100644
--- a/bqskit/passes/search/generators/wide.py
+++ b/bqskit/passes/search/generators/wide.py
@@ -28,7 +28,7 @@ class WideLayerGenerator(LayerGenerator):
     def __init__(
         self,
         multi_qudit_gates: GateSetLike = IToffoliGate(),
-        single_qudit_gate: Gate = U3Gate(),
+        single_qudit_gate: Gate | None = U3Gate(),
     ) -> None:
         """
         Construct a WideLayerGenerator.
@@ -40,8 +40,11 @@ def __init__(
                 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())
+            single_qudit_gate (Gate | None): A single-qudit gate that
+                follows `multi_qudit_gate` in the building block. Can
+                set to None to disable single-qudit gates, which might
+                be useful with general multi-qudit gates.
+                (Default: U3Gate())
 
         Raises:
             ValueError: If the single-qudit gate's size is not 1.
@@ -57,27 +60,28 @@ def __init__(
                 % [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,
-            )
+        if single_qudit_gate is not None:
+            if not isinstance(single_qudit_gate, Gate):
+                raise TypeError(
+                    'Expected gate for single_qudit_gate, got %s.'
+                    % type(single_qudit_gate),
+                )
 
-        sr = single_qudit_gate.radixes[0]
-        for mg in multi_qudit_gates:
-            if any(r != sr for r in mg.radixes):
+            if single_qudit_gate.num_qudits != 1:
                 raise ValueError(
-                    'Radix mismatch between gates'
-                    f': {mg.radixes} !~ {single_qudit_gate.radixes}.',
+                    '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
 
@@ -99,15 +103,20 @@ def gen_initial_layer(
                 'Expected unitary or state, got %s.' % type(target),
             )
 
+        init_circuit = Circuit(target.num_qudits, target.radixes)
+
+        if self.single_qudit_gate is None:
+            return init_circuit
+
         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: PassData) -> list[Circuit]:
@@ -135,8 +144,9 @@ def gen_successors(self, circuit: Circuit, data: PassData) -> list[Circuit]:
             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)
+                if self.single_qudit_gate is not None:
+                    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 b924c2b8e..04c0bf8d7 100644
--- a/bqskit/passes/synthesis/leap.py
+++ b/bqskit/passes/synthesis/leap.py
@@ -13,7 +13,6 @@
 from bqskit.ir.opt.cost.generator import CostFunctionGenerator
 from bqskit.passes.search.frontier import Frontier
 from bqskit.passes.search.generator import LayerGenerator
-from bqskit.passes.search.generators import SimpleLayerGenerator
 from bqskit.passes.search.heuristic import HeuristicFunction
 from bqskit.passes.search.heuristics import AStarHeuristic
 from bqskit.passes.synthesis.synthesis import SynthesisPass
@@ -42,7 +41,7 @@ class LEAPSynthesisPass(SynthesisPass):
     def __init__(
         self,
         heuristic_function: HeuristicFunction = AStarHeuristic(),
-        layer_generator: LayerGenerator = SimpleLayerGenerator(),
+        layer_generator: LayerGenerator | None = None,
         success_threshold: float = 1e-10,
         cost: CostFunctionGenerator = HilbertSchmidtResidualsGenerator(),
         max_layer: int | None = None,
@@ -58,8 +57,10 @@ def __init__(
             heuristic_function (HeuristicFunction): The heuristic to guide
                 search.
 
-            layer_generator (LayerGenerator): The successor function
-                to guide node expansion.
+            layer_generator (LayerGenerator | None): The successor function
+                to guide node expansion. If left as none, then a default
+                will be selected before synthesis based on the target
+                model's gate set. (Default: None)
 
             success_threshold (float): The distance threshold that
                 determines successful termintation. Measured in cost
@@ -98,11 +99,11 @@ def __init__(
                 % type(heuristic_function),
             )
 
-        if not isinstance(layer_generator, LayerGenerator):
-            raise TypeError(
-                'Expected LayerGenerator, got %s.'
-                % type(layer_generator),
-            )
+        if layer_generator is not None:
+            if not isinstance(layer_generator, LayerGenerator):
+                raise TypeError(
+                    f'Expected LayerGenerator, got {type(layer_generator)}.',
+                )
 
         if not is_real_number(success_threshold):
             raise TypeError(
@@ -163,13 +164,16 @@ async def synthesize(
         data: PassData,
     ) -> Circuit:
         """Synthesize `utry`, see :class:`SynthesisPass` for more."""
-        frontier = Frontier(utry, self.heuristic_function)
+        # Initialize run-dependent options
         instantiate_options = self.instantiate_options.copy()
         if 'seed' not in instantiate_options:
             instantiate_options['seed'] = data.seed
 
+        layer_gen = self.layer_gen or data.gate_set.build_mq_layer_generator()
+
         # Seed the search with an initial layer
-        initial_layer = self.layer_gen.gen_initial_layer(utry, data)
+        frontier = Frontier(utry, self.heuristic_function)
+        initial_layer = layer_gen.gen_initial_layer(utry, data)
         initial_layer.instantiate(utry, **instantiate_options)
         frontier.add(initial_layer, 0)
 
@@ -196,7 +200,7 @@ async def synthesize(
             top_circuit, layer = frontier.pop()
 
             # Generate successors
-            successors = self.layer_gen.gen_successors(top_circuit, data)
+            successors = layer_gen.gen_successors(top_circuit, data)
 
             if len(successors) == 0:
                 continue
diff --git a/bqskit/passes/synthesis/qsearch.py b/bqskit/passes/synthesis/qsearch.py
index ca8a64b9b..91ee0ebd5 100644
--- a/bqskit/passes/synthesis/qsearch.py
+++ b/bqskit/passes/synthesis/qsearch.py
@@ -10,7 +10,6 @@
 from bqskit.ir.opt.cost.generator import CostFunctionGenerator
 from bqskit.passes.search.frontier import Frontier
 from bqskit.passes.search.generator import LayerGenerator
-from bqskit.passes.search.generators import SimpleLayerGenerator
 from bqskit.passes.search.heuristic import HeuristicFunction
 from bqskit.passes.search.heuristics import AStarHeuristic
 from bqskit.passes.synthesis.synthesis import SynthesisPass
@@ -38,7 +37,7 @@ class QSearchSynthesisPass(SynthesisPass):
     def __init__(
         self,
         heuristic_function: HeuristicFunction = AStarHeuristic(),
-        layer_generator: LayerGenerator = SimpleLayerGenerator(),
+        layer_generator: LayerGenerator | None = None,
         success_threshold: float = 1e-10,
         cost: CostFunctionGenerator = HilbertSchmidtResidualsGenerator(),
         max_layer: int | None = None,
@@ -53,8 +52,10 @@ def __init__(
             heuristic_function (HeuristicFunction): The heuristic to guide
                 search.
 
-            layer_generator (LayerGenerator): The successor function
-                to guide node expansion.
+            layer_generator (LayerGenerator | None): The successor function
+                to guide node expansion. If left as none, then a default
+                will be selected before synthesis based on the target
+                model's gate set. (Default: None)
 
             success_threshold (float): The distance threshold that
                 determines successful termintation. Measured in cost
@@ -90,11 +91,11 @@ def __init__(
                 % type(heuristic_function),
             )
 
-        if not isinstance(layer_generator, LayerGenerator):
-            raise TypeError(
-                'Expected LayerGenerator, got %s.'
-                % type(layer_generator),
-            )
+        if layer_generator is not None:
+            if not isinstance(layer_generator, LayerGenerator):
+                raise TypeError(
+                    f'Expected LayerGenerator, got {type(layer_generator)}.',
+                )
 
         if not is_real_number(success_threshold):
             raise TypeError(
@@ -142,14 +143,17 @@ async def synthesize(
         data: PassData,
     ) -> Circuit:
         """Synthesize `utry`, see :class:`SynthesisPass` for more."""
+        # Initialize run-dependent options
         instantiate_options = self.instantiate_options.copy()
+
         if 'seed' not in instantiate_options:
             instantiate_options['seed'] = data.seed
 
-        frontier = Frontier(utry, self.heuristic_function)
+        layer_gen = self.layer_gen or data.gate_set.build_mq_layer_generator()
 
         # Seed the search with an initial layer
-        initial_layer = self.layer_gen.gen_initial_layer(utry, data)
+        frontier = Frontier(utry, self.heuristic_function)
+        initial_layer = layer_gen.gen_initial_layer(utry, data)
         initial_layer.instantiate(utry, **instantiate_options)
         frontier.add(initial_layer, 0)
 
@@ -173,7 +177,7 @@ async def synthesize(
             top_circuit, layer = frontier.pop()
 
             # Generate successors
-            successors = self.layer_gen.gen_successors(top_circuit, data)
+            successors = layer_gen.gen_successors(top_circuit, data)
 
             if len(successors) == 0:
                 continue
diff --git a/bqskit/passes/util/extend.py b/bqskit/passes/util/extend.py
index 682a30978..4491c3c89 100644
--- a/bqskit/passes/util/extend.py
+++ b/bqskit/passes/util/extend.py
@@ -19,30 +19,47 @@
 class ExtendBlockSizePass(BasePass):
     """Ensure all blocks are at least a given size."""
 
-    def __init__(self, minimum_size: int) -> None:
+    def __init__(self, minimum_size: int | None = None) -> None:
         """
         Construct a ExtendBlockSizePass.
 
         Args:
-            minimum_size (int): Extend all blocks to at least this size.
+            minimum_size (int | None): Extend all blocks to at least this
+                size. If left as None, the minimum size will be the size of
+                the smallest multi-qudit gate in the model.
         """
-        if not is_integer(minimum_size):
-            raise TypeError('Expected an integer for minimum size.')
+        if not is_integer(minimum_size) and minimum_size is not None:
+            raise TypeError('Expected an integer or None for minimum size.')
 
         self.minimum_size = minimum_size
 
     async def run(self, circuit: Circuit, data: PassData) -> None:
         """Perform the pass's operation, see :class:`BasePass` for more."""
-        if circuit.num_qudits < self.minimum_size:
+        if circuit.num_qudits == 1:
+            _logger.debug('Skipping extend pass for single-qudit circuit.')
+            return
+
+        minimum_size = self.minimum_size
+        if minimum_size is None:
+            minimum_size = min(
+                g.num_qudits for g in data.gate_set
+                if g.num_qudits != 1
+            )
+
+        if minimum_size is None:
+            _logger.warning('No multi-qudit gates in gate set.')
+            return
+
+        if circuit.num_qudits < minimum_size:
             raise RuntimeError('Cannot extend block larger than circuit.')
 
-        cg = self.get_connectivity(circuit, data)
+        cg = data.connectivity
 
         # 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:
+                if op.gate.num_qudits < minimum_size:
                     small_blocks.append((cycle, op.location[0]))
         small_blocks.sort()
 
@@ -52,7 +69,7 @@ async def run(self, circuit: Circuit, data: PassData) -> None:
             cycle = block_point[0]
             op = circuit[block_point]
 
-            num_to_add = self.minimum_size - op.gate.num_qudits
+            num_to_add = minimum_size - op.gate.num_qudits
             qudits = list(op.location)
             added = []
             for _ in range(num_to_add):
diff --git a/bqskit/qis/state/state.py b/bqskit/qis/state/state.py
index a71bc59b1..bd8f93e88 100644
--- a/bqskit/qis/state/state.py
+++ b/bqskit/qis/state/state.py
@@ -433,4 +433,4 @@ def __repr__(self) -> str:
         return repr(self._vec)
 
 
-StateLike = Union[StateVector, np.ndarray, Sequence[Any]]
+StateLike = Union[StateVector, np.ndarray, Sequence[Union[int, float, complex]]]
diff --git a/bqskit/qis/state/system.py b/bqskit/qis/state/system.py
index b64d7162c..0dae28e5a 100644
--- a/bqskit/qis/state/system.py
+++ b/bqskit/qis/state/system.py
@@ -38,7 +38,7 @@ def __init__(self, system: StateSystemLike) -> None:
         self._dim = list(self._system.keys())[0].dim
         self._vec_count = len(self._system)
 
-        for k, v in system.items():
+        for k, v in self._system.items():
             if k.radixes != self.radixes:
                 raise ValueError('States in system have mismatch in dimension.')
             if v.radixes != self.radixes:
diff --git a/bqskit/qis/unitary/unitarymatrix.py b/bqskit/qis/unitary/unitarymatrix.py
index c54b0338a..5302a8f76 100644
--- a/bqskit/qis/unitary/unitarymatrix.py
+++ b/bqskit/qis/unitary/unitarymatrix.py
@@ -490,4 +490,8 @@ def __hash__(self) -> int:
         return hash((self._utry[0][0], self._utry[-1][-1], self.shape))
 
 
-UnitaryLike = Union[UnitaryMatrix, np.ndarray, Sequence[Sequence[Any]]]
+UnitaryLike = Union[
+    UnitaryMatrix,
+    np.ndarray,
+    Sequence[Sequence[Union[int, float, complex]]],
+]


Test patch:
diff --git a/tests/compiler/compile/test_prep.py b/tests/compiler/compile/test_prep.py
index 9bb1fd948..8581a5cd0 100644
--- a/tests/compiler/compile/test_prep.py
+++ b/tests/compiler/compile/test_prep.py
@@ -1,7 +1,5 @@
 from __future__ import annotations
 
-import pytest
-
 from bqskit import compile
 from bqskit.compiler.compiler import Compiler
 from bqskit.ir.circuit import Circuit
@@ -89,7 +87,6 @@ def test_state_inst_3() -> None:
     assert out_state.get_distance_from(state) < 1e-8
 
 
-@pytest.mark.skip(reason='Tests run for 4 hours, and falls sometimes')
 def test_state_map(compiler: Compiler) -> None:
     num_qudits = 3
     utry = UnitaryMatrix.random(num_qudits)
@@ -99,7 +96,7 @@ def test_state_map(compiler: Compiler) -> None:
         out_state = utry.get_statevector(in_state)
         state_map[in_state] = out_state
     system = StateSystem(state_map)
-    out_circuit = compile(system, compiler=compiler)
+    out_circuit = compile(system, compiler=compiler, synthesis_epsilon=1e-6)
     print(out_circuit.gate_counts)
 
     for in_state, out_state in state_map.items():
diff --git a/tests/compiler/compile/test_with_mapping.py b/tests/compiler/compile/test_with_mapping.py
index 068872479..196ad79a6 100644
--- a/tests/compiler/compile/test_with_mapping.py
+++ b/tests/compiler/compile/test_with_mapping.py
@@ -43,6 +43,9 @@ def test_compile_with_mapping(
         optimization_level=optimization_level,
     )
 
+    assert len(initial_mapping) == 3
+    assert len(final_mapping) == 3
+
     # This simple circuit shouldn't have moving pieces in the mapping
     assert set(initial_mapping) == set(final_mapping)
 
@@ -61,4 +64,4 @@ def test_compile_with_mapping(
     out_utry = mapped_subcircuit.get_unitary()
     PI = PermutationMatrix.from_qubit_location(3, local_qudit_initial_perm)
     PF = PermutationMatrix.from_qubit_location(3, local_qudit_final_perm)
-    assert out_utry.get_distance_from(PF.T @ correct_utry @ PI) < 1e-7
+    assert out_utry.get_distance_from(PF.T @ correct_utry @ PI) < 1e-6
diff --git a/tests/ir/circuit/test_properties.py b/tests/ir/circuit/test_properties.py
index 2d9cc8f9a..1640802c6 100644
--- a/tests/ir/circuit/test_properties.py
+++ b/tests/ir/circuit/test_properties.py
@@ -1009,6 +1009,45 @@ def test_qudit(self) -> None:
         assert CSUMGate() in circuit.gate_set
 
 
+def test_active_qudits() -> None:
+    # Create a circuit with 3 qudits and 3 gates
+    circuit1 = Circuit(3)
+    circuit1.append_gate(HGate(), 0)
+    circuit1.append_gate(CNOTGate(), [0, 1])
+    circuit1.append_gate(CNOTGate(), [1, 2])
+
+    # Check that the active qudits are correct
+    assert circuit1.active_qudits == [0, 1, 2]
+
+    # Create a circuit with 2 qudits and 2 gates
+    circuit2 = Circuit(2)
+    circuit2.append_gate(HGate(), 0)
+    circuit2.append_gate(CNOTGate(), [0, 1])
+
+    # Check that the active qudits are correct
+    assert circuit2.active_qudits == [0, 1]
+
+    # Create a circuit with 1 qudit and 1 gate
+    circuit3 = Circuit(1)
+    circuit3.append_gate(HGate(), 0)
+
+    # Check that the active qudits are correct
+    assert circuit3.active_qudits == [0]
+
+    # Create a circuit with 3 qudits and no gates
+    circuit4 = Circuit(3)
+
+    # Check that the active qudits are correct
+    assert circuit4.active_qudits == []
+
+    # Create a circuit with 3 qudits and a single-qudit gate
+    circuit5 = Circuit(3)
+    circuit5.append_gate(HGate(), 0)
+
+    # Check that the active qudits are correct
+    assert circuit5.active_qudits == [0]
+
+
 class TestIsDifferentiable:
     """This tests `circuit.is_differentiable`."""