Repository: qiboteam/qibo

Issue:
method `Circuit.invert()` breaks when noisy channels are present
`Circuit.invert()` is raising error when `ThermalRelaxationChannel` or `PauliNoiseChannel` are present in the original circuit. E.g.

```python3
from qibo import gates
from qibo.models import Circuit

circuit = Circuit(1, density_matrix=True)
circuit.add(gates.X(0))
circuit.add(gates.PauliNoiseChannel(0, [("X", 0.1)]))

print(circuit.invert())
```
I have tried with `ThermalRelaxationChannel` and is also breaks. However, it does not break with `DepolarizingChannel`.
`backend.asmatrix` and `Circuit.unitary()` issues.
`Gate.matrix` property and `Gate.asmatrix` method seem to be redundant. Would it be possible to remove `Gate.matrix` and make `backend` parameter optional in `asmatrix` so that it defaults to `GlobalBackend`?
https://github.com/qiboteam/qibo/blob/914b05301f854afb10dd9d1c63d84c16759dfd5e/src/qibo/gates/abstract.py#L286-L291

### `Circuit.unitary()` and `backend.asmatrix` are not handling `gates.Channel` properly.
Consider a circuit with 2 Depolarizing channels.

```python
from qibo.models import Circuit
from qibo.gates import DepolarizingChannel, PauliNoiseChannel

channel_1 = DepolarizingChannel(0, 0.1)
channel_2 = DepolarizingChannel(0, 0.5)
circuit = Circuit(1)
circuit.add([channel_1, channel_2])
```

Bugs related to `Circuit.unitary()`:
- `circuit.unitary()` 
raises `AttributeError: 'NumpyMatrices' object has no attribute 'DepolarizingChannel'`
- `circuit.fuse().unitary()` 
returns Identity matrix

but should be `NotImplementedError` or defaults to superoperator representation (then should be renamed).

Bugs related to `backend.asmatrix`:
- `backend.asmatrix(channel_1)` and `backend.asmatrix_fused(circuit.fuse().queue[0])`
raise `AttributeError: 'NumpyMatrices' object has no attribute 'DepolarizingChannel'`

but should be superoperator representation.

Discussion:

Patch:
diff --git a/src/qibo/models/circuit.py b/src/qibo/models/circuit.py
index 8a2a1aa6e..bd92eadfb 100644
--- a/src/qibo/models/circuit.py
+++ b/src/qibo/models/circuit.py
@@ -405,7 +405,12 @@ class Circuit:
         measurements = []
         new_circuit = self.__class__(**self.init_kwargs)
         for gate in self.queue[::-1]:
-            if isinstance(gate, gates.M) and skip_measurements:
+            if isinstance(gate, gates.Channel):
+                raise_error(
+                    NotImplementedError,
+                    "`invert` method not implemented for circuits that contain noise channels.",
+                )
+            elif isinstance(gate, gates.M) and skip_measurements:
                 measurements.append(gate)
             else:
                 new_gate = gate.dagger()
@@ -944,7 +949,12 @@ class Circuit:
 
         fgate = gates.FusedGate(*range(self.nqubits))
         for gate in self.queue:
-            if not isinstance(gate, (gates.SpecialGate, gates.M)):
+            if isinstance(gate, gates.Channel):
+                raise_error(
+                    NotImplementedError,
+                    "`unitary` method not implemented for circuits that contain noise channels.",
+                )
+            elif not isinstance(gate, (gates.SpecialGate, gates.M)):
                 fgate.append(gate)
         return fgate.matrix(backend)
 


Test patch:
diff --git a/tests/test_models_circuit_features.py b/tests/test_models_circuit_features.py
index 99b45b74c..8944b25b6 100644
--- a/tests/test_models_circuit_features.py
+++ b/tests/test_models_circuit_features.py
@@ -45,6 +45,17 @@ def test_circuit_unitary_bigger(backend, with_measurement):
     backend.assert_allclose(final_matrix, target_matrix)
 
 
+def test_circuit_unitary_and_inverse_with_noise_channel(backend):
+    circuit = Circuit(2)
+    circuit.add(gates.H(0))
+    circuit.add(gates.CNOT(0, 1))
+    circuit.add(gates.DepolarizingChannel([0, 1], 0.2))
+    with pytest.raises(NotImplementedError):
+        circuit.unitary(backend)
+    with pytest.raises(NotImplementedError):
+        circuit.invert()
+
+
 @pytest.mark.parametrize("compile", [False, True])
 def test_circuit_vs_gate_execution(backend, compile):
     """Check consistency between executing circuit and stand alone gates."""