Repository: BQSKit/bqskit

Issue:
Compiler ignores custom gate set when compiling a random 2-qubit circuit
Example to reproduce:
```
import bqskit
from bqskit.ext import qiskit_to_bqskit
import qiskit as qs
import qutip as qt
import numpy as np

circ = qs.QuantumCircuit(2)
u = qt.rand_unitary_haar(4)
random_unitary = qs.quantum_info.operators.Operator(np.array(u))
circ.unitary(random_unitary, [0,1], label='random')

model = bqskit.MachineModel(2, gate_set={bqskit.ir.gates.CZGate(2), bqskit.ir.gates.U3Gate(1)})
circ_bqskit = qiskit_to_bqskit(circ)
output_circuit = bqskit.compile(circ_bqskit, model=model)
print(output_circuit.gate_set)
```
This will print `{U3Gate, CNOTGate}` instead of the desired `{U3Gate, CZGate}` (and the circuit `output_circuit` will consist of CNOTs and U3 gates instead of using the desired CZ operations). This appears to be the case for any non-CNOT 2-qubit gate.

Behavior is correct when using `bqskit.compile(np.array(u), model=model)`, so it seems that the circuit object is causing the problem.

Behavior is correct when specifying a 3-qubit gate instead of a 2-qubit gate, e.g. `gate_set={bqskit.ir.gates.Toffoli(3), bqskit.ir.gates.U3Gate(1)}` (if circuit size is extended to 3 qubits instead of 2).

Discussion:

Patch:
diff --git a/bqskit/compiler/compile.py b/bqskit/compiler/compile.py
index ee9fcbe..1237948 100644
--- a/bqskit/compiler/compile.py
+++ b/bqskit/compiler/compile.py
@@ -443,7 +443,7 @@ def _opt1_workflow(
             if g.num_qudits != 1
         )
         non_native_gates = [
-            g for g in circuit.gate_set
+            g for g in circuit.gate_set_no_blocks
             if g not in model.gate_set
         ]
         non_native_tq_gates = [
@@ -454,7 +454,7 @@ def _opt1_workflow(
             non_native_tq_gates.append(SwapGate(model.radixes[0]))
         native_tq_gates = [g for g in model.gate_set if g.num_qudits == 2]
 
-        all_gates = model.gate_set.union(circuit.gate_set)
+        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:
@@ -580,7 +580,7 @@ def _opt2_workflow(
             if g.num_qudits != 1
         )
         non_native_gates = [
-            g for g in circuit.gate_set
+            g for g in circuit.gate_set_no_blocks
             if g not in model.gate_set
         ]
         non_native_tq_gates = [
@@ -591,7 +591,7 @@ def _opt2_workflow(
             non_native_tq_gates.append(SwapGate(model.radixes[0]))
         native_tq_gates = [g for g in model.gate_set if g.num_qudits == 2]
 
-        all_gates = model.gate_set.union(circuit.gate_set)
+        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:
@@ -733,7 +733,7 @@ def _opt3_workflow(
             if g.num_qudits != 1
         )
         non_native_gates = [
-            g for g in circuit.gate_set
+            g for g in circuit.gate_set_no_blocks
             if g not in model.gate_set
         ]
         non_native_tq_gates = [
@@ -745,7 +745,7 @@ def _opt3_workflow(
         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)
+        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:
diff --git a/bqskit/ir/circuit.py b/bqskit/ir/circuit.py
index 3a59031..c3ba125 100644
--- a/bqskit/ir/circuit.py
+++ b/bqskit/ir/circuit.py
@@ -238,6 +238,17 @@ class Circuit(DifferentiableUnitary, StateVectorMap, Collection[Operation]):
         """The set of gates in the circuit."""
         return set(self._gate_info.keys())
 
+    @property
+    def gate_set_no_blocks(self) -> set[Gate]:
+        """The set of gates in the circuit, recurses into circuit gates."""
+        gates = set()
+        for g, _ in self._gate_info.items():
+            if isinstance(g, CircuitGate):
+                gates.update(g._circuit.gate_set)
+            else:
+                gates.add(g)
+        return gates
+
     @property
     def gate_counts(self) -> dict[Gate, int]:
         """The count of each type of gate in the circuit."""


Test patch:
diff --git a/tests/compiler/compile/test_blocked_input.py b/tests/compiler/compile/test_blocked_input.py
new file mode 100644
index 0000000..19fe3db
--- /dev/null
+++ b/tests/compiler/compile/test_blocked_input.py
@@ -0,0 +1,27 @@
+from __future__ import annotations
+
+from bqskit import compile
+from bqskit import MachineModel
+from bqskit.ir.circuit import Circuit
+from bqskit.ir.gates import CircuitGate
+from bqskit.ir.gates import CNOTGate
+from bqskit.ir.gates import CZGate
+from bqskit.ir.gates import U3Gate
+
+
+def test_compile_blocked_input_circuit_unfold_rebase_correctly() -> None:
+    circuit = Circuit(2)
+    circuit.append_gate(U3Gate(), 0)
+    circuit.append_gate(U3Gate(), 1)
+    circuit.append_gate(CNOTGate(), (0, 1))
+    circuit.append_gate(U3Gate(), 0)
+    circuit.append_gate(U3Gate(), 1)
+    cg = CircuitGate(circuit, True)
+    blocked_circuit = Circuit(2)
+    blocked_circuit.append_gate(cg, (0, 1))
+    model = MachineModel(2, gate_set={CZGate(), U3Gate()})
+    out_circuit = compile(blocked_circuit, model)
+    assert len(out_circuit.gate_set) == 2
+    assert CZGate() in out_circuit.gate_set
+    assert U3Gate() in out_circuit.gate_set
+    assert CNOTGate() not in out_circuit.gate_set