Repository: Qiskit/qiskit

Issue:
`Target.instruction_schedule_map().has_custom_gate()` always returns True
### Environment

- **Qiskit Terra version**: 0.23.1
- **Python version**:  3.11
- **Operating system**: Linux


### What is happening?

When using the `InstructionScheduleMap` returned from a Target object's (specifically from a FakeBackend object, but I think this occurs generally) `.instruction_schedule_map()` method the `.has_custom_gate()` method is always returning True. This wasn't the case pre-0.23.0 (and I expect means this is related to #8885 ). This leads to a transpiler performance regression because the transpiler thinks we need to evaluate pulse gates despite not having a custom calibration set.

### How can we reproduce the issue?

```python3
from qiskit.providers.fake_provider import FakeGeneva

backend = FakeGeneva()
instmap = backend.instruction_schedule_map
assert not instmap.has_custom_gate()
```

### What should happen?

The above code snippet should not raise an assertion error because no custom gates are defined in the instruction schedule map.

### Any suggestions?

I think this is being caused by a type mismatch and differing expectations between the `Target`/`BackendV2` path and the `InstructionScheduleMap` class. I originally thought this was just a case of a differing type being used in the target path post-#8885 to represent the default calibration from the defaults file now that we're lazy loading it. But looking at it a bit more closely the `InstructionScheduleMap`'s entries from the fake backend are all `ScheduleDef` which would be the same as a manually created `Schedule` object. I"m not sure the best way to resolve this though.


Discussion:

Resolving patch:
diff --git a/qiskit/pulse/calibration_entries.py b/qiskit/pulse/calibration_entries.py
--- a/qiskit/pulse/calibration_entries.py
+++ b/qiskit/pulse/calibration_entries.py
@@ -122,6 +122,9 @@ def _parse_argument(self):
 
     def define(self, definition: Union[Schedule, ScheduleBlock]):
         self._definition = definition
+        # add metadata
+        if "publisher" not in definition.metadata:
+            definition.metadata["publisher"] = CalibrationPublisher.QISKIT
         self._parse_argument()
 
     def get_signature(self) -> inspect.Signature:
@@ -184,7 +187,11 @@ def get_schedule(self, *args, **kwargs) -> Union[Schedule, ScheduleBlock]:
         except TypeError as ex:
             raise PulseError("Assigned parameter doesn't match with function signature.") from ex
 
-        return self._definition(**to_bind.arguments)
+        schedule = self._definition(**to_bind.arguments)
+        # add metadata
+        if "publisher" not in schedule.metadata:
+            schedule.metadata["publisher"] = CalibrationPublisher.QISKIT
+        return schedule
 
     def __eq__(self, other):
         # We cannot evaluate function equality without parsing python AST.
diff --git a/qiskit/pulse/instruction_schedule_map.py b/qiskit/pulse/instruction_schedule_map.py
--- a/qiskit/pulse/instruction_schedule_map.py
+++ b/qiskit/pulse/instruction_schedule_map.py
@@ -10,6 +10,8 @@
 # copyright notice, and modified files need to carry a notice indicating
 # that they have been altered from the originals.
 
+# pylint: disable=unused-import
+
 """
 A convenient way to track reusable subschedules by name and qubit.
 
@@ -34,11 +36,12 @@
 from qiskit.circuit.instruction import Instruction
 from qiskit.circuit.parameterexpression import ParameterExpression
 from qiskit.pulse.calibration_entries import (
-    CalibrationPublisher,
     CalibrationEntry,
     ScheduleDef,
     CallableDef,
     PulseQobjDef,
+    # for backward compatibility
+    CalibrationPublisher,
 )
 from qiskit.pulse.exceptions import PulseError
 from qiskit.pulse.schedule import Schedule, ScheduleBlock
@@ -248,9 +251,6 @@ def add(
         # generate signature
         if isinstance(schedule, (Schedule, ScheduleBlock)):
             entry = ScheduleDef(arguments)
-            # add metadata
-            if "publisher" not in schedule.metadata:
-                schedule.metadata["publisher"] = CalibrationPublisher.QISKIT
         elif callable(schedule):
             if arguments:
                 warnings.warn(
diff --git a/qiskit/transpiler/target.py b/qiskit/transpiler/target.py
--- a/qiskit/transpiler/target.py
+++ b/qiskit/transpiler/target.py
@@ -29,7 +29,7 @@
 
 from qiskit.circuit.parameter import Parameter
 from qiskit.pulse.instruction_schedule_map import InstructionScheduleMap
-from qiskit.pulse.calibration_entries import CalibrationEntry
+from qiskit.pulse.calibration_entries import CalibrationEntry, ScheduleDef
 from qiskit.pulse.schedule import Schedule, ScheduleBlock
 from qiskit.transpiler.coupling import CouplingMap
 from qiskit.transpiler.exceptions import TranspilerError
@@ -72,20 +72,25 @@ def __init__(
                 set of qubits.
             calibration: The pulse representation of the instruction.
         """
+        self._calibration = None
+
         self.duration = duration
         self.error = error
-        self._calibration = calibration
+        self.calibration = calibration
 
     @property
     def calibration(self):
         """The pulse representation of the instruction."""
-        if isinstance(self._calibration, CalibrationEntry):
-            return self._calibration.get_schedule()
-        return self._calibration
+        return self._calibration.get_schedule()
 
     @calibration.setter
     def calibration(self, calibration: Union[Schedule, ScheduleBlock, CalibrationEntry]):
-        self._calibration = calibration
+        if isinstance(calibration, (Schedule, ScheduleBlock)):
+            new_entry = ScheduleDef()
+            new_entry.define(calibration)
+        else:
+            new_entry = calibration
+        self._calibration = new_entry
 
     def __repr__(self):
         return (
@@ -532,8 +537,12 @@ def instruction_schedule_map(self):
         out_inst_schedule_map = InstructionScheduleMap()
         for instruction, qargs in self._gate_map.items():
             for qarg, properties in qargs.items():
-                if properties is not None and properties.calibration is not None:
-                    out_inst_schedule_map.add(instruction, qarg, properties.calibration)
+                # Directly getting CalibrationEntry not to invoke .get_schedule().
+                # This keeps PulseQobjDef un-parsed.
+                cal_entry = getattr(properties, "_calibration", None)
+                if cal_entry is not None:
+                    # Use fast-path to add entries to the inst map.
+                    out_inst_schedule_map._add(instruction, qarg, cal_entry)
         self._instruction_schedule_map = out_inst_schedule_map
         return out_inst_schedule_map
 
@@ -1026,7 +1035,7 @@ def __str__(self):
                 error = getattr(props, "error", None)
                 if error is not None:
                     prop_str_pieces.append(f"\t\t\tError Rate: {error}\n")
-                schedule = getattr(props, "calibration", None)
+                schedule = getattr(props, "_calibration", None)
                 if schedule is not None:
                     prop_str_pieces.append("\t\t\tWith pulse schedule calibration\n")
                 extra_props = getattr(props, "properties", None)


Test patch:
diff --git a/test/python/pulse/test_instruction_schedule_map.py b/test/python/pulse/test_instruction_schedule_map.py
--- a/test/python/pulse/test_instruction_schedule_map.py
+++ b/test/python/pulse/test_instruction_schedule_map.py
@@ -30,7 +30,7 @@
     ShiftPhase,
     Constant,
 )
-from qiskit.pulse.instruction_schedule_map import CalibrationPublisher
+from qiskit.pulse.calibration_entries import CalibrationPublisher
 from qiskit.pulse.channels import DriveChannel
 from qiskit.qobj import PulseQobjInstruction
 from qiskit.qobj.converters import QobjToInstructionConverter
@@ -602,8 +602,12 @@ def test_has_custom_gate(self):
 
         self.assertFalse(instmap.has_custom_gate())
 
-        # add something
+        # add custom schedule
         some_sched = Schedule()
         instmap.add("u3", (0,), some_sched)
 
         self.assertTrue(instmap.has_custom_gate())
+
+        # delete custom schedule
+        instmap.remove("u3", (0,))
+        self.assertFalse(instmap.has_custom_gate())
diff --git a/test/python/transpiler/test_target.py b/test/python/transpiler/test_target.py
--- a/test/python/transpiler/test_target.py
+++ b/test/python/transpiler/test_target.py
@@ -35,6 +35,7 @@
 from qiskit.circuit.parameter import Parameter
 from qiskit import pulse
 from qiskit.pulse.instruction_schedule_map import InstructionScheduleMap
+from qiskit.pulse.calibration_entries import CalibrationPublisher
 from qiskit.transpiler.coupling import CouplingMap
 from qiskit.transpiler.instruction_durations import InstructionDurations
 from qiskit.transpiler.timing_constraints import TimingConstraints
@@ -42,7 +43,7 @@
 from qiskit.transpiler import Target
 from qiskit.transpiler import InstructionProperties
 from qiskit.test import QiskitTestCase
-from qiskit.providers.fake_provider import FakeBackendV2, FakeMumbaiFractionalCX
+from qiskit.providers.fake_provider import FakeBackendV2, FakeMumbaiFractionalCX, FakeGeneva
 
 
 class TestTarget(QiskitTestCase):
@@ -1228,6 +1229,36 @@ def test_timing_constraints(self):
                 f"{getattr(generated_constraints, i)}!={getattr(expected_constraints, i)}",
             )
 
+    def test_default_instmap_has_no_custom_gate(self):
+        backend = FakeGeneva()
+        target = backend.target
+
+        # This copies .calibraiton of InstructionProperties of each instruction
+        # This must not convert PulseQobj to Schedule during this.
+        # See qiskit-terra/#9595
+        inst_map = target.instruction_schedule_map()
+        self.assertFalse(inst_map.has_custom_gate())
+
+        # Get pulse schedule. This generates Schedule provided by backend.
+        sched = inst_map.get("sx", (0,))
+        self.assertEqual(sched.metadata["publisher"], CalibrationPublisher.BACKEND_PROVIDER)
+        self.assertFalse(inst_map.has_custom_gate())
+
+        # Update target with custom instruction. This is user provided schedule.
+        new_prop = InstructionProperties(
+            duration=self.custom_sx_q0.duration,
+            error=None,
+            calibration=self.custom_sx_q0,
+        )
+        target.update_instruction_properties(instruction="sx", qargs=(0,), properties=new_prop)
+        inst_map = target.instruction_schedule_map()
+        self.assertTrue(inst_map.has_custom_gate())
+
+        empty = InstructionProperties()
+        target.update_instruction_properties(instruction="sx", qargs=(0,), properties=empty)
+        inst_map = target.instruction_schedule_map()
+        self.assertFalse(inst_map.has_custom_gate())
+
 
 class TestGlobalVariableWidthOperations(QiskitTestCase):
     def setUp(self):