Repository: BQSKit/bqskit

Issue:
`DaggerGate` vs actually inverting the gate, qasm output for `DaggerGate`
The `get_inverse` method currently returns `DaggerGate` objects.  For small gates (single-qubit gates, CNOT, etc), is there a reason to save these as, eg, `DaggerGate(CNOTGate)` rather than just computing the adjoint (in this case, again CNOT)?

The rough use case is something like 
U^dg @ O @ U @ psi,
where U is given by some circuit.  

One might then want to print the full circuit, including reversed gates, to qasm (currently not possible with `DaggerGate`).  Also to get gate counts where `DaggerGate(CNOTGate)` is just counted as `CNOTGate`

Discussion:

Patch:
diff --git a/.github/workflows/pre-commit.yml b/.github/workflows/pre-commit.yml
index 57e478b..34e4ce9 100644
--- a/.github/workflows/pre-commit.yml
+++ b/.github/workflows/pre-commit.yml
@@ -3,7 +3,7 @@ name: pre-commit
 on:
   pull_request:
   push:
-    branches: [main]
+    branches: [master]
 
 jobs:
   pre-commit:
@@ -12,7 +12,7 @@ jobs:
     - uses: actions/checkout@v3
     - uses: actions/setup-python@v4
       with:
-        python-version: '3.11'
+        python-version: '3.10.6' # TODO: Remove .6 when mypy fixes issue 13627
     - name: set PY
       run: echo "name=PY::$(python -c 'import hashlib, sys;print(hashlib.sha256(sys.version.encode()+sys.executable.encode()).hexdigest())')" >> $GITHUB_ENV
     - uses: actions/cache@v1
diff --git a/bqskit/compiler/compile.py b/bqskit/compiler/compile.py
index 22be718..07e4e82 100644
--- a/bqskit/compiler/compile.py
+++ b/bqskit/compiler/compile.py
@@ -6,8 +6,6 @@ import logging
 import warnings
 from typing import Any
 from typing import Callable
-from typing import Literal
-from typing import overload
 from typing import TYPE_CHECKING
 
 import numpy as np
@@ -88,60 +86,6 @@ if TYPE_CHECKING:
 _logger = logging.getLogger(__name__)
 
 
-@overload
-def compile(
-    input: Circuit | UnitaryLike | StateLike | StateSystemLike,
-    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,
-) -> Circuit:
-    ...
-
-
-@overload
-def compile(
-    input: Circuit | UnitaryLike | StateLike | StateSystemLike,
-    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,
-) -> tuple[Circuit, tuple[int, ...], tuple[int, ...]]:
-    ...
-
-
-@overload
-def compile(
-    input: Circuit | UnitaryLike | StateLike | StateSystemLike,
-    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,
-) -> Circuit | tuple[Circuit, tuple[int, ...], tuple[int, ...]]:
-    ...
-
-
 def compile(
     input: Circuit | UnitaryLike | StateLike | StateSystemLike,
     model: MachineModel | None = None,
@@ -152,9 +96,9 @@ def compile(
     error_sim_size: int = 8,
     compiler: Compiler | None = None,
     seed: int | None = None,
-    with_mapping: bool = False,
+    *compiler_args: Any,
     **compiler_kwargs: Any,
-) -> Circuit | tuple[Circuit, tuple[int, ...], tuple[int, ...]]:
+) -> Circuit:
     """
     Compile a circuit, unitary, or state with a standard workflow.
 
@@ -201,15 +145,9 @@ def compile(
         seed (int | None): Set a seed for the compile function for
             better reproducibility. If left as None, will not set seed.
 
-        with_mapping (bool): If True, three values will be returned
-            instead of just the compiled circuit. The first value is the
-            compiled circuit, the second value is the initial mapping,
-            and the third value is the final mapping. The initial mapping
-            is a tuple where `initial_mapping[i] = j` implies that logical
-            qudit `i` in the input system starts on the physical qudit
-            `q` in the final circuit. Likewise, the final mapping describes
-            where the logical qudits are in the physical circuit at the end
-            of execution. (Default: False)
+        compiler_args (Any): Passed directly to BQSKit compiler construction.
+            Arguments for connecting to a cluster can go here.
+            See :class:`Compiler` for more info.
 
         compiler_kwargs (Any): Passed directly to BQSKit compiler construction.
             Arguments for connecting to a cluster can go here.
@@ -395,7 +333,7 @@ def compile(
     managed_compiler = compiler is None
 
     if managed_compiler:
-        compiler = Compiler(**compiler_kwargs)
+        compiler = Compiler(*compiler_args, **compiler_kwargs)
 
     elif not isinstance(compiler, Compiler):
         raise TypeError(
@@ -420,12 +358,6 @@ def compile(
     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
 
 
diff --git a/bqskit/compiler/compiler.py b/bqskit/compiler/compiler.py
index 10edc07..87d75f5 100644
--- a/bqskit/compiler/compiler.py
+++ b/bqskit/compiler/compiler.py
@@ -14,7 +14,6 @@ from multiprocessing.connection import Client
 from multiprocessing.connection import Connection
 from subprocess import Popen
 from types import FrameType
-from typing import Literal
 from typing import overload
 from typing import TYPE_CHECKING
 
@@ -344,6 +343,7 @@ class Compiler:
         self,
         task_or_circuit: CompilationTask,
     ) -> Circuit | tuple[Circuit, PassData]:
+        """Submit a task, wait for its results; see :func:`submit` for more."""
         ...
 
     @overload
@@ -351,21 +351,11 @@ class Compiler:
         self,
         task_or_circuit: Circuit,
         workflow: WorkflowLike,
-        request_data: Literal[False] = ...,
-        logging_level: int | None = ...,
-        max_logging_depth: int = ...,
+        request_data: None = None,
+        logging_level: int | None = None,
+        max_logging_depth: int = -1,
     ) -> Circuit:
-        ...
-
-    @overload
-    def compile(
-        self,
-        task_or_circuit: Circuit,
-        workflow: WorkflowLike,
-        request_data: Literal[True],
-        logging_level: int | None = ...,
-        max_logging_depth: int = ...,
-    ) -> tuple[Circuit, PassData]:
+        """Submit a task, wait for its results; see :func:`submit` for more."""
         ...
 
     @overload
@@ -374,16 +364,17 @@ class Compiler:
         task_or_circuit: Circuit,
         workflow: WorkflowLike,
         request_data: bool,
-        logging_level: int | None = ...,
-        max_logging_depth: int = ...,
-    ) -> Circuit | tuple[Circuit, PassData]:
+        logging_level: int | None = None,
+        max_logging_depth: int = -1,
+    ) -> tuple[Circuit, PassData]:
+        """Submit a task, wait for its results; see :func:`submit` for more."""
         ...
 
     def compile(
         self,
         task_or_circuit: CompilationTask | Circuit,
         workflow: WorkflowLike | None = None,
-        request_data: bool = False,
+        request_data: bool | None = None,
         logging_level: int | None = None,
         max_logging_depth: int = -1,
     ) -> Circuit | tuple[Circuit, PassData]:
@@ -400,7 +391,7 @@ class Compiler:
         task_id = self.submit(
             task_or_circuit,
             workflow,
-            request_data,
+            request_data if request_data else False,
             logging_level,
             max_logging_depth,
         )
diff --git a/bqskit/compiler/machine.py b/bqskit/compiler/machine.py
index eb05071..b8d5640 100644
--- a/bqskit/compiler/machine.py
+++ b/bqskit/compiler/machine.py
@@ -97,6 +97,7 @@ class MachineModel:
         """Return all `block_size` connected blocks of qudit indicies."""
         return self.coupling_graph.get_subgraphs_of_size(block_size)
 
+    # TODO: add placement support
     def is_compatible(
         self,
         circuit: Circuit,
@@ -109,19 +110,10 @@ class MachineModel:
         if any(g not in self.gate_set for g in circuit.gate_set):
             return False
 
-        if placement is None:
-            placement = list(range(circuit.num_qudits))
-
-        if any(
-            (placement[e[0]], placement[e[1]]) not in self.coupling_graph
-            for e in circuit.coupling_graph
-        ):
+        if any(e not in self.coupling_graph for e in circuit.coupling_graph):
             return False
 
-        if any(
-            r != self.radixes[placement[i]]
-            for i, r in enumerate(circuit.radixes)
-        ):
+        if any(r != self.radixes[i] for i, r in enumerate(circuit.radixes)):
             return False
 
         return True
diff --git a/bqskit/ir/circuit.py b/bqskit/ir/circuit.py
index 9c4bbec..f1ced93 100644
--- a/bqskit/ir/circuit.py
+++ b/bqskit/ir/circuit.py
@@ -23,7 +23,6 @@ import numpy.typing as npt
 
 from bqskit.ir.gate import Gate
 from bqskit.ir.gates.circuitgate import CircuitGate
-from bqskit.ir.gates.composed.daggergate import DaggerGate
 from bqskit.ir.gates.constant.unitary import ConstantUnitaryGate
 from bqskit.ir.gates.measure import MeasurementPlaceholder
 from bqskit.ir.iterator import CircuitIterator
@@ -451,9 +450,7 @@ class Circuit(DifferentiableUnitary, StateVectorMap, Collection[Operation]):
         for cycle_index, cycle in enumerate(self._circuit):
             if cycle[qudit_index] is not None:
                 points.append((cycle_index, qudit_index))
-
-        if len(points) > 0:
-            self.batch_pop(points)
+        self.batch_pop(points)
 
         # Update circuit properties
         self._num_qudits -= 1
@@ -2524,13 +2521,7 @@ class Circuit(DifferentiableUnitary, StateVectorMap, Collection[Operation]):
         """Return the circuit's inverse circuit."""
         circuit = Circuit(self.num_qudits, self.radixes)
         for op in reversed(self):
-            circuit.append(
-                Operation(
-                    DaggerGate(op.gate),
-                    op.location,
-                    op.params,
-                ),
-            )
+            circuit.append(op.get_inverse())
         return circuit
 
     def get_unitary(self, params: RealVector = []) -> UnitaryMatrix:
diff --git a/bqskit/ir/gate.py b/bqskit/ir/gate.py
index edc5c2a..f23dd42 100644
--- a/bqskit/ir/gate.py
+++ b/bqskit/ir/gate.py
@@ -52,6 +52,32 @@ class Gate(Unitary):
             '], q['.join([str(q) for q in location]),
         ).replace('()', '')
 
+    def get_inverse_params(self, params: RealVector = []) -> RealVector:
+        """
+        Return the parameters that invert the gate.
+
+        Args:
+            params (RealVector): The parameters of the gate to invert.
+
+        Note:
+            - The default implementation returns the same paramters because
+              the default implementation of `Gate.get_inverse` returns a
+              :class:`DaggerGate` wrapper of the gate. The wrapper will
+              correctly handle the inversion. When overriding `get_inverse`,
+              on a parameterized gate this method should be overridden
+              as well.
+        """
+        return params
+
+    def get_inverse(self) -> Gate:
+        """Return the gate's inverse as a gate."""
+        if self.is_constant() and self.is_self_inverse():
+            return self
+
+        from bqskit.ir.gates.composed import DaggerGate
+        return getattr(self, '_inverse', DaggerGate(self))
+        # TODO: Fill out inverse definitions throughout the gate library
+
     with_frozen_params: ClassVar[
         Callable[[Gate, dict[int, float]], FrozenParameterGate]
     ]
diff --git a/bqskit/ir/gates/composed/daggergate.py b/bqskit/ir/gates/composed/daggergate.py
index 6877654..40dbc56 100644
--- a/bqskit/ir/gates/composed/daggergate.py
+++ b/bqskit/ir/gates/composed/daggergate.py
@@ -106,3 +106,7 @@ class DaggerGate(
 
     def __hash__(self) -> int:
         return hash(self.gate)
+
+    def get_inverse(self) -> Gate:
+        """Return the gate's inverse as a gate."""
+        return self.gate
diff --git a/bqskit/ir/gates/parameterized/u3.py b/bqskit/ir/gates/parameterized/u3.py
index c70ab58..0b137e4 100644
--- a/bqskit/ir/gates/parameterized/u3.py
+++ b/bqskit/ir/gates/parameterized/u3.py
@@ -4,6 +4,7 @@ from __future__ import annotations
 import numpy as np
 import numpy.typing as npt
 
+from bqskit.ir.gate import Gate
 from bqskit.ir.gates.generalgate import GeneralGate
 from bqskit.ir.gates.qubitgate import QubitGate
 from bqskit.qis.unitary.differentiable import DifferentiableUnitary
@@ -118,3 +119,12 @@ class U3Gate(QubitGate, DifferentiableUnitary, CachedClass, GeneralGate):
         phi = (a + b)
         lamb = (a - b)
         return [theta, phi, lamb]
+
+    def get_inverse_params(self, params: RealVector = []) -> RealVector:
+        """Return the parameters that invert the gate, see :class:`Gate`."""
+        self.check_parameters(params)
+        return [-params[0], -params[2], -params[1]]
+
+    def get_inverse(self) -> Gate:
+        """Return the inverse of this gate."""
+        return U3Gate()
diff --git a/bqskit/ir/operation.py b/bqskit/ir/operation.py
index e53cb17..7072c17 100644
--- a/bqskit/ir/operation.py
+++ b/bqskit/ir/operation.py
@@ -107,6 +107,14 @@ class Operation(DifferentiableUnitary):
 
         return self.gate.get_unitary(self.params)
 
+    def get_inverse(self) -> Operation:
+        """Return the operation's inverse operation."""
+        return Operation(
+            self.gate.get_inverse(),
+            self.location,
+            self.gate.get_inverse_params(self.params),
+        )
+
     def get_grad(self, params: RealVector = []) -> npt.NDArray[np.complex128]:
         """
         Return the gradient for this operation.
diff --git a/bqskit/passes/mapping/apply.py b/bqskit/passes/mapping/apply.py
index c3ea462..1dc6379 100644
--- a/bqskit/passes/mapping/apply.py
+++ b/bqskit/passes/mapping/apply.py
@@ -23,7 +23,4 @@ class ApplyPlacement(BasePass):
         if 'final_mapping' in data:
             pi = data['final_mapping']
             data['final_mapping'] = [placement[p] for p in pi]
-        if 'initial_mapping' in data:
-            pi = data['initial_mapping']
-            data['initial_mapping'] = [placement[p] for p in pi]
         data.placement = list(i for i in range(model.num_qudits))
diff --git a/bqskit/passes/mapping/placement/greedy.py b/bqskit/passes/mapping/placement/greedy.py
index 41b7d93..ac8a588 100644
--- a/bqskit/passes/mapping/placement/greedy.py
+++ b/bqskit/passes/mapping/placement/greedy.py
@@ -45,11 +45,11 @@ class GreedyPlacementPass(BasePass):
                 if n not in placement and n not in neighbors:
                     neighbors.append(n)
 
-        data.placement = sorted(placement)
+        data['placement'] = sorted(placement)
 
-        _logger.info(f'Placed qudits on {data.placement}')
+        _logger.info(f'Placed qudits on {data["placement"]}')
 
         # Raise an error if this is not a valid placement
-        sg = graph.get_subgraph(data.placement)
+        sg = graph.get_subgraph(data['placement'])
         if not sg.is_fully_connected():
             raise RuntimeError('No valid placement found.')
diff --git a/bqskit/passes/mapping/routing/sabre.py b/bqskit/passes/mapping/routing/sabre.py
index 8b899ec..3179a03 100644
--- a/bqskit/passes/mapping/routing/sabre.py
+++ b/bqskit/passes/mapping/routing/sabre.py
@@ -32,6 +32,5 @@ class GeneralizedSabreRoutingPass(BasePass, GeneralizedSabreAlgorithm):
             pi = [i for i in range(circuit.num_qudits)]
 
         self.forward_pass(circuit, pi, subgraph, modify_circuit=True)
-        # TODO: if final_mapping is already in data, apply it first
         data['final_mapping'] = pi
         _logger.info(f'Finished routing with layout: {str(pi)}')
diff --git a/bqskit/passes/synthesis/pas.py b/bqskit/passes/synthesis/pas.py
index 10c7b85..564ad7a 100644
--- a/bqskit/passes/synthesis/pas.py
+++ b/bqskit/passes/synthesis/pas.py
@@ -51,7 +51,6 @@ class PermutationAwareSynthesisPass(SynthesisPass):
                 score the permuted circuits. The smallest score wins.
                 (Default: :func:`op_count`)
         """
-        # TODO: Add option to choose first returned result
         if not isinstance(inner_synthesis, SynthesisPass):
             bad_type = type(inner_synthesis)
             raise TypeError(f'Expected SynthesisPass object, got {bad_type}.')
diff --git a/bqskit/qis/unitary/unitary.py b/bqskit/qis/unitary/unitary.py
index bf5816f..cb4ada6 100644
--- a/bqskit/qis/unitary/unitary.py
+++ b/bqskit/qis/unitary/unitary.py
@@ -124,5 +124,31 @@ class Unitary(metaclass=UnitaryMeta):
                 % (self.num_params, len(params)),
             )
 
+    def is_self_inverse(self, params: RealVector = []) -> bool:
+        """
+        Checks whether the unitary is its own inverse.
+
+        A unitary is its own inverse if its matrix is equal to its
+        Hermitian conjugate.
+
+        Args:
+            params (RealVector): The parameters of the unitary to check.
+
+        Returns:
+            bool: True if the unitary is self-inverse, False otherwise.
+
+        Note:
+            - This checks that the unitary is self-inverse for the given
+              parameters only.
+        """
+        # Get the unitary matrix of the gate
+        unitary_matrix = self.get_unitary(params)
+
+        # Calculate the Hermitian conjugate (adjoint) of the unitary matrix
+        hermitian_conjugate = unitary_matrix.dagger
+
+        # Check if the unitary matrix is equal to its Hermitian conjugate
+        return np.allclose(unitary_matrix, hermitian_conjugate)
+
 
 RealVector = Union[Sequence[float], np.ndarray]


Test patch:
diff --git a/.github/workflows/tests.yml b/.github/workflows/tests.yml
index 890a440..15adf2d 100644
--- a/.github/workflows/tests.yml
+++ b/.github/workflows/tests.yml
@@ -3,7 +3,7 @@ name: tests
 on:
   pull_request:
   push:
-    branches: [main]
+    branches: [master]
 
 jobs:
   tests:
diff --git a/tests/compiler/compile/test_with_mapping.py b/tests/compiler/compile/test_with_mapping.py
deleted file mode 100644
index 15d15f8..0000000
--- a/tests/compiler/compile/test_with_mapping.py
+++ /dev/null
@@ -1,64 +0,0 @@
-from __future__ import annotations
-
-from bqskit.compiler.compile import compile
-from bqskit.compiler.compiler import Compiler
-from bqskit.compiler.machine import MachineModel
-from bqskit.ir.circuit import Circuit
-from bqskit.ir.gates import CNOTGate
-from bqskit.ir.gates import U3Gate
-from bqskit.qis.graph import CouplingGraph
-from bqskit.qis.permutation import PermutationMatrix
-
-
-def test_compile_with_mapping(
-        compiler: Compiler,
-        optimization_level: int,
-) -> None:
-    circuit = Circuit(3)
-    circuit.append_gate(U3Gate(), [0], [0, 1, 2])
-    circuit.append_gate(U3Gate(), [1], [0, 1, 2])
-    circuit.append_gate(U3Gate(), [2], [0, 1, 2])
-    circuit.append_gate(CNOTGate(), [0, 1])
-    circuit.append_gate(U3Gate(), [0], [0, 1, 2])
-    circuit.append_gate(U3Gate(), [1], [0, 1, 2])
-    circuit.append_gate(U3Gate(), [2], [0, 1, 2])
-    circuit.append_gate(CNOTGate(), [1, 2])
-    circuit.append_gate(U3Gate(), [0], [0, 1, 2])
-    circuit.append_gate(U3Gate(), [1], [0, 1, 2])
-    circuit.append_gate(U3Gate(), [2], [0, 1, 2])
-    circuit.append_gate(CNOTGate(), [0, 2])
-    circuit.append_gate(U3Gate(), [0], [0, 1, 2])
-    circuit.append_gate(U3Gate(), [1], [0, 1, 2])
-    circuit.append_gate(U3Gate(), [2], [0, 1, 2])
-    correct_utry = circuit.get_unitary()
-
-    coupling_graph = CouplingGraph([(i, i + 1) for i in range(5)] + [(3, 5)])
-    model = MachineModel(6, coupling_graph)
-
-    circuit, initial_mapping, final_mapping = compile(
-        circuit,
-        model,
-        compiler=compiler,
-        with_mapping=True,
-        optimization_level=optimization_level,
-    )
-
-    # This simple circuit shouldn't have moving pieces in the mapping
-    assert set(initial_mapping) == set(final_mapping)
-
-    # cut out the mapped subcircuit
-    mapped_subcircuit = circuit.copy()
-    inactive_qudits = set(range(circuit.num_qudits)) - set(initial_mapping)
-    for qudit_index in sorted(inactive_qudits, reverse=True):
-        mapped_subcircuit.pop_qudit(qudit_index)
-
-    # convert global mappings to local permutations
-    global_qudits = list(sorted(final_mapping))
-    local_qudit_initial_perm = [global_qudits.index(i) for i in initial_mapping]
-    local_qudit_final_perm = [global_qudits.index(i) for i in final_mapping]
-
-    # undo the mapping and compare to the original unitary
-    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-6
diff --git a/tests/compiler/test_machine.py b/tests/compiler/test_machine.py
index 75ac1b5..8c6e470 100644
--- a/tests/compiler/test_machine.py
+++ b/tests/compiler/test_machine.py
@@ -1,12 +1,9 @@
+# TODO: is_compatabile
 from __future__ import annotations
 
 import pytest
 
 from bqskit.compiler.machine import MachineModel
-from bqskit.ir.circuit import Circuit
-from bqskit.ir.gates.constant.cx import CNOTGate
-from bqskit.ir.gates.constant.h import HGate
-from bqskit.ir.gates.constant.t import TGate
 
 
 class TestMachineConstructor:
@@ -61,47 +58,3 @@ class TestMachineConstructor:
             MachineModel(2, 0)  # type: ignore
         with pytest.raises(TypeError):
             MachineModel(2, 'a')  # type: ignore
-
-
-def test_is_compatible() -> None:
-    # Create a model with 3 qudits linearly connected
-    model = MachineModel(
-        num_qudits=3,
-        coupling_graph=[(0, 1), (1, 2)],
-        gate_set={HGate(), CNOTGate()},
-        radixes=[2, 2, 2],
-    )
-
-    # Create a 2 qudit circuit with a gate set that is a subset
-    circuit1 = Circuit(num_qudits=2, radixes=[2, 2])
-    circuit1.append_gate(HGate(), 0)
-    circuit1.append_gate(CNOTGate(), (0, 1))
-
-    # Create a 3 qudit circuit with a gate set that is not a subset
-    circuit2 = Circuit(num_qudits=3, radixes=[2, 2, 2])
-    circuit2.append_gate(HGate(), 0)
-    circuit2.append_gate(TGate(), 1)
-    circuit2.append_gate(CNOTGate(), (1, 2))
-
-    # Create a 3 qudit circuit with a different radix
-    circuit3 = Circuit(num_qudits=3, radixes=[2, 2, 3])
-    circuit3.append_gate(HGate(), 0)
-    circuit3.append_gate(CNOTGate(), (0, 1))
-
-    # Create a 3 qudit circuit with a different coupling graph
-    circuit4 = Circuit(num_qudits=3, radixes=[2, 2, 2])
-    circuit4.append_gate(HGate(), 0)
-    circuit4.append_gate(CNOTGate(), (0, 2))
-
-    # Create a 3 qudit circuit with a valid placement
-    circuit5 = Circuit(num_qudits=3, radixes=[2, 2, 2])
-    circuit5.append_gate(HGate(), 0)
-    circuit5.append_gate(CNOTGate(), (0, 2))
-    placement = [0, 2, 1]
-
-    # Check compatibility of each circuit with the model
-    assert model.is_compatible(circuit1)
-    assert not model.is_compatible(circuit2)
-    assert not model.is_compatible(circuit3)
-    assert not model.is_compatible(circuit4)
-    assert model.is_compatible(circuit5, placement)
diff --git a/tests/ir/test_inverse.py b/tests/ir/test_inverse.py
new file mode 100644
index 0000000..0c9ccf7
--- /dev/null
+++ b/tests/ir/test_inverse.py
@@ -0,0 +1,15 @@
+from __future__ import annotations
+
+import numpy as np
+from hypothesis import given
+
+from bqskit.ir.operation import Operation
+from bqskit.utils.test.strategies import operations
+
+
+@given(operations())
+def test_gate_inverse(op: Operation) -> None:
+    inverse_op = op.get_inverse()
+    iden = np.identity(op.dim)
+    dist = (inverse_op.get_unitary() @ op.get_unitary()).get_distance_from(iden)
+    assert dist < 1e-7