Repository: Qiskit/qiskit

Issue:
Add pulse channels to `FakeBackendV2`
### What is the expected enhancement?

https://github.com/Qiskit/qiskit-terra/pull/7643 implemented mocked IBM Quantum Systems backends using the new BackendV2 interface. 

The BackendV2 class contains [pulse channels](https://github.com/Qiskit/qiskit-terra/blob/main/qiskit/providers/backend.py#L484-L549) (`drive_channel`, `measure_channel`, `acquire_channel`, `control_channel`) that were not implemented by #7643. 

It's unclear to me whether these channels are useful for fake backends. If they are useful, we should add them.





Discussion:

Resolving patch:
diff --git a/qiskit/providers/backend.py b/qiskit/providers/backend.py
--- a/qiskit/providers/backend.py
+++ b/qiskit/providers/backend.py
@@ -553,7 +553,7 @@ def control_channel(self, qubits: Iterable[int]):
                 ``(control_qubit, target_qubit)``.
 
         Returns:
-            List[ControlChannel]: The Qubit measurement acquisition line.
+            List[ControlChannel]: The multi qubit control line.
 
         Raises:
             NotImplementedError: if the backend doesn't support querying the
diff --git a/qiskit/providers/fake_provider/fake_backend.py b/qiskit/providers/fake_provider/fake_backend.py
--- a/qiskit/providers/fake_provider/fake_backend.py
+++ b/qiskit/providers/fake_provider/fake_backend.py
@@ -17,10 +17,12 @@
 """
 
 import warnings
+import collections
 import json
 import os
+import re
 
-from typing import List
+from typing import List, Iterable
 
 from qiskit import circuit
 from qiskit.providers.models import BackendProperties
@@ -84,6 +86,36 @@ def __init__(self):
         self._target = None
         self.sim = None
 
+        if "channels" in self._conf_dict:
+            self._parse_channels(self._conf_dict["channels"])
+
+    def _parse_channels(self, channels):
+        type_map = {
+            "acquire": pulse.AcquireChannel,
+            "drive": pulse.DriveChannel,
+            "measure": pulse.MeasureChannel,
+            "control": pulse.ControlChannel,
+        }
+        identifier_pattern = re.compile(r"\D+(?P<index>\d+)")
+
+        channels_map = {
+            "acquire": collections.defaultdict(list),
+            "drive": collections.defaultdict(list),
+            "measure": collections.defaultdict(list),
+            "control": collections.defaultdict(list),
+        }
+        for identifier, spec in channels.items():
+            channel_type = spec["type"]
+            out = re.match(identifier_pattern, identifier)
+            if out is None:
+                # Identifier is not a valid channel name format
+                continue
+            channel_index = int(out.groupdict()["index"])
+            qubit_index = tuple(spec["operates"]["qubits"])
+            chan_obj = type_map[channel_type](channel_index)
+            channels_map[channel_type][qubit_index].append(chan_obj)
+        setattr(self, "channels_map", channels_map)
+
     def _setup_sim(self):
         if _optionals.HAS_AER:
             from qiskit.providers import aer
@@ -193,6 +225,73 @@ def meas_map(self) -> List[List[int]]:
         """
         return self._conf_dict.get("meas_map")
 
+    def drive_channel(self, qubit: int):
+        """Return the drive channel for the given qubit.
+
+        This is required to be implemented if the backend supports Pulse
+        scheduling.
+
+        Returns:
+            DriveChannel: The Qubit drive channel
+        """
+        drive_channels_map = getattr(self, "channels_map", {}).get("drive", {})
+        qubits = (qubit,)
+        if qubits in drive_channels_map:
+            return drive_channels_map[qubits][0]
+        return None
+
+    def measure_channel(self, qubit: int):
+        """Return the measure stimulus channel for the given qubit.
+
+        This is required to be implemented if the backend supports Pulse
+        scheduling.
+
+        Returns:
+            MeasureChannel: The Qubit measurement stimulus line
+        """
+        measure_channels_map = getattr(self, "channels_map", {}).get("measure", {})
+        qubits = (qubit,)
+        if qubits in measure_channels_map:
+            return measure_channels_map[qubits][0]
+        return None
+
+    def acquire_channel(self, qubit: int):
+        """Return the acquisition channel for the given qubit.
+
+        This is required to be implemented if the backend supports Pulse
+        scheduling.
+
+        Returns:
+            AcquireChannel: The Qubit measurement acquisition line.
+        """
+        acquire_channels_map = getattr(self, "channels_map", {}).get("acquire", {})
+        qubits = (qubit,)
+        if qubits in acquire_channels_map:
+            return acquire_channels_map[qubits][0]
+        return None
+
+    def control_channel(self, qubits: Iterable[int]):
+        """Return the secondary drive channel for the given qubit
+
+        This is typically utilized for controlling multiqubit interactions.
+        This channel is derived from other channels.
+
+        This is required to be implemented if the backend supports Pulse
+        scheduling.
+
+        Args:
+            qubits: Tuple or list of qubits of the form
+                ``(control_qubit, target_qubit)``.
+
+        Returns:
+            List[ControlChannel]: The multi qubit control line.
+        """
+        control_channels_map = getattr(self, "channels_map", {}).get("control", {})
+        qubits = tuple(qubits)
+        if qubits in control_channels_map:
+            return control_channels_map[qubits]
+        return []
+
     def run(self, run_input, **options):
         """Run on the fake backend using a simulator.
 


Test patch:
diff --git a/test/python/providers/test_backend_v2.py b/test/python/providers/test_backend_v2.py
--- a/test/python/providers/test_backend_v2.py
+++ b/test/python/providers/test_backend_v2.py
@@ -31,7 +31,9 @@
     FakeBackendSimple,
     FakeBackendV2LegacyQubitProps,
 )
+from qiskit.providers.fake_provider.backends import FakeBogotaV2
 from qiskit.quantum_info import Operator
+from qiskit.pulse import channels
 
 
 @ddt
@@ -190,3 +192,45 @@ def test_transpile_parse_inst_map(self):
         """Test that transpiler._parse_inst_map() supports BackendV2."""
         inst_map = _parse_inst_map(inst_map=None, backend=self.backend)
         self.assertIsInstance(inst_map, InstructionScheduleMap)
+
+    @data(0, 1, 2, 3, 4)
+    def test_drive_channel(self, qubit):
+        """Test getting drive channel with qubit index."""
+        backend = FakeBogotaV2()
+        chan = backend.drive_channel(qubit)
+        ref = channels.DriveChannel(qubit)
+        self.assertEqual(chan, ref)
+
+    @data(0, 1, 2, 3, 4)
+    def test_measure_channel(self, qubit):
+        """Test getting measure channel with qubit index."""
+        backend = FakeBogotaV2()
+        chan = backend.measure_channel(qubit)
+        ref = channels.MeasureChannel(qubit)
+        self.assertEqual(chan, ref)
+
+    @data(0, 1, 2, 3, 4)
+    def test_acquire_channel(self, qubit):
+        """Test getting acquire channel with qubit index."""
+        backend = FakeBogotaV2()
+        chan = backend.acquire_channel(qubit)
+        ref = channels.AcquireChannel(qubit)
+        self.assertEqual(chan, ref)
+
+    @data((4, 3), (3, 4), (3, 2), (2, 3), (1, 2), (2, 1), (1, 0), (0, 1))
+    def test_control_channel(self, qubits):
+        """Test getting acquire channel with qubit index."""
+        bogota_cr_channels_map = {
+            (4, 3): 7,
+            (3, 4): 6,
+            (3, 2): 5,
+            (2, 3): 4,
+            (1, 2): 2,
+            (2, 1): 3,
+            (1, 0): 1,
+            (0, 1): 0,
+        }
+        backend = FakeBogotaV2()
+        chan = backend.control_channel(qubits)[0]
+        ref = channels.ControlChannel(bogota_cr_channels_map[qubits])
+        self.assertEqual(chan, ref)