Repository: Qiskit/qiskit-terra (python)

Issue:
Remove symbolic pulse subclasses
### What should we add?

### Background

We have four parametric pulse subclass in pulse library, namely, `Gaussian`, `GaussianSquare`, `Drag`, `Constant`.
```python
from qiskit.pulse.library import Gaussian
instance = Gaussian(160, 0.1, 40)
```
This returns a class instance dedicated to each waveform kind.

In #7821 , we replaced the base class of them from `ParametricPulse` to `SymbolicPulse`. In contrast to parametric pulse, symbolic pulse can uniquely determine waveform with instance variables `self._envelope` and `self._pulse_type` which are a symbolic equation and string, respectively. This means, these subclasses are no longer necessary and we can turn everything into symbolic pulse instance. This can simplify the deserialization logic of QPY (actually type information is not stored).

### Proposal

I want to convert `Gaussian` class into a symbolic pulse factory while keeping camel case :
```python
def Gaussian(duration, amp, sigma, name, limit_amplitude):
    ...
    return SymbolicPulse(...)
```
so that user code works without deprecation, e.g.
```python
with build() as schedule:
    # now the first arg is SymbolicPulse, rather than Gaussian
    play(Gaussian(160, 0.1, 40), DriveChannel(0))
```

### Problems
However, sometimes type information is used in the codebase. For example, in the RZX transpiler pass:
https://github.com/Qiskit/qiskit-terra/blob/10822404936e98df01f2975ef43b8378006621ba/qiskit/transpiler/passes/calibration/builders.py#L163-L168
I can update such use case in our codebase. This can be written without type information like below.
```python
if pulse_.pulse_type == "GaussianSquare":
```
However, because we have never prohibited users from using type information, we need proper communication with some warning mechanism. Adding deprecation warning in symbolic pulse constructor is very annoying since it will issue tones of warnings (e.g. loading each pulse in backend CmdDef). Probably overriding `__instancecheck__` method to show deprecation warning is possible, however, this is not sensitive to, for example, `__class__.__name__`.

Pull request description:
Remove symbolic pulse subclass implementation


<!--
⚠️ If you do not respect this template, your pull request will be closed.
⚠️ Your pull request title should be short detailed and understandable for all.
⚠️ Also, please add a release note file using reno if the change needs to be
  documented in the release notes.
⚠️ If your pull request fixes an open issue, please link to the issue.

- [ ] I have added the tests to cover my changes.
- [ ] I have updated the documentation accordingly.
- [ ] I have read the CONTRIBUTING document.
-->

### Summary

This PR removes implementation of symbolic pulse subclasses. These classes become a factory of `SymbolicPulse`, and envelope kind-wise subclassing will be prohibited (classes are not actually removed, but never instantiated). Accordingly, use of `isinstance` has been also discouraged by the user warning.

```python
pulse = Gaussian(160, 0.1, 40)

isinstance(pulse, Gaussian)  # True
```
with user warning
```
UserWarning: Typechecking with the symbolic pulse subclass will be deprecated. 'Gaussian' subclass instance is turned into SymbolicPulse instance. Use self.pulse_type == 'Gaussian' instead.
```

(future plan)
After sufficient transition period from type information to `.pulse_type`, we will promote the user warning to deprecation warning, then eventually we will remove the metaclass and convert pulse subclasses into python functions.

close #8261

### Details and comments




Patch:
diff --git a/qiskit/assembler/assemble_schedules.py b/qiskit/assembler/assemble_schedules.py
index 7987c92d3..6482dabbe 100644
--- a/qiskit/assembler/assemble_schedules.py
+++ b/qiskit/assembler/assemble_schedules.py
@@ -191,7 +191,7 @@ def _assemble_instructions(
             if isinstance(instruction.pulse, (library.ParametricPulse, library.SymbolicPulse)):
                 is_backend_supported = True
                 try:
-                    pulse_shape = ParametricPulseShapes(type(instruction.pulse)).name
+                    pulse_shape = ParametricPulseShapes.from_instance(instruction.pulse).name
                     if pulse_shape not in run_config.parametric_pulses:
                         is_backend_supported = False
                 except ValueError:
diff --git a/qiskit/pulse/library/symbolic_pulses.py b/qiskit/pulse/library/symbolic_pulses.py
index 8f07130f3..4f1aa909f 100644
--- a/qiskit/pulse/library/symbolic_pulses.py
+++ b/qiskit/pulse/library/symbolic_pulses.py
@@ -19,6 +19,7 @@ parameter constraints.
 """
 
 import functools
+import warnings
 from typing import Any, Dict, List, Optional, Union, Callable
 
 import numpy as np
@@ -574,7 +575,39 @@ class SymbolicPulse(Pulse):
         )
 
 
-class Gaussian(SymbolicPulse):
+class _PulseType(type):
+    """Metaclass to warn at isinstance check."""
+
+    def __instancecheck__(cls, instance):
+        cls_alias = getattr(cls, "alias", None)
+
+        # TODO promote this to Deprecation warning in future.
+        #  Once type information usage is removed from user code,
+        #  we will convert pulse classes into functions.
+        warnings.warn(
+            "Typechecking with the symbolic pulse subclass will be deprecated. "
+            f"'{cls_alias}' subclass instance is turned into SymbolicPulse instance. "
+            f"Use self.pulse_type == '{cls_alias}' instead.",
+            PendingDeprecationWarning,
+        )
+
+        if not isinstance(instance, SymbolicPulse):
+            return False
+        return instance.pulse_type == cls_alias
+
+    def __getattr__(cls, item):
+        # For pylint. A SymbolicPulse subclass must implement several methods
+        # such as .get_waveform and .validate_parameters.
+        # In addition, they conventionally offer attribute-like access to the pulse parameters,
+        # for example, instance.amp returns instance._params["amp"].
+        # If pulse classes are directly instantiated, pylint yells no-member
+        # since the pulse class itself implements nothing. These classes just
+        # behave like a factory by internally instantiating the SymbolicPulse and return it.
+        # It is not realistic to write disable=no-member across qiskit packages.
+        return NotImplemented
+
+
+class Gaussian(metaclass=_PulseType):
     r"""A lifted and truncated pulse envelope shaped according to the Gaussian function whose
     mean is centered at the center of the pulse (duration / 2):
 
@@ -586,14 +619,16 @@ class Gaussian(SymbolicPulse):
     where :math:`f'(x)` is the gaussian waveform without lifting or amplitude scaling.
     """
 
-    def __init__(
-        self,
+    alias = "Gaussian"
+
+    def __new__(
+        cls,
         duration: Union[int, ParameterExpression],
         amp: Union[complex, ParameterExpression],
         sigma: Union[float, ParameterExpression],
         name: Optional[str] = None,
         limit_amplitude: Optional[bool] = None,
-    ):
+    ) -> SymbolicPulse:
         """Create new pulse instance.
 
         Args:
@@ -605,6 +640,8 @@ class Gaussian(SymbolicPulse):
             limit_amplitude: If ``True``, then limit the amplitude of the
                 waveform to 1. The default is ``True`` and the amplitude is constrained to 1.
 
+        Returns:
+            SymbolicPulse instance.
         """
         parameters = {"amp": amp, "sigma": sigma}
 
@@ -616,8 +653,8 @@ class Gaussian(SymbolicPulse):
         consts_expr = _sigma > 0
         valid_amp_conditions_expr = sym.Abs(_amp) <= 1.0
 
-        super().__init__(
-            pulse_type=self.__class__.__name__,
+        instance = SymbolicPulse(
+            pulse_type=cls.alias,
             duration=duration,
             parameters=parameters,
             name=name,
@@ -626,10 +663,12 @@ class Gaussian(SymbolicPulse):
             constraints=consts_expr,
             valid_amp_conditions=valid_amp_conditions_expr,
         )
-        self.validate_parameters()
+        instance.validate_parameters()
+
+        return instance
 
 
-class GaussianSquare(SymbolicPulse):
+class GaussianSquare(metaclass=_PulseType):
     """A square pulse with a Gaussian shaped risefall on both sides lifted such that
     its first sample is zero.
 
@@ -667,8 +706,10 @@ class GaussianSquare(SymbolicPulse):
     where :math:`f'(x)` is the gaussian square waveform without lifting or amplitude scaling.
     """
 
-    def __init__(
-        self,
+    alias = "GaussianSquare"
+
+    def __new__(
+        cls,
         duration: Union[int, ParameterExpression],
         amp: Union[complex, ParameterExpression],
         sigma: Union[float, ParameterExpression],
@@ -676,7 +717,7 @@ class GaussianSquare(SymbolicPulse):
         risefall_sigma_ratio: Optional[Union[float, ParameterExpression]] = None,
         name: Optional[str] = None,
         limit_amplitude: Optional[bool] = None,
-    ):
+    ) -> SymbolicPulse:
         """Create new pulse instance.
 
         Args:
@@ -690,6 +731,9 @@ class GaussianSquare(SymbolicPulse):
             limit_amplitude: If ``True``, then limit the amplitude of the
                 waveform to 1. The default is ``True`` and the amplitude is constrained to 1.
 
+        Returns:
+            SymbolicPulse instance.
+
         Raises:
             PulseError: When width and risefall_sigma_ratio are both empty or both non-empty.
         """
@@ -724,8 +768,8 @@ class GaussianSquare(SymbolicPulse):
         consts_expr = sym.And(_sigma > 0, _width >= 0, _duration >= _width)
         valid_amp_conditions_expr = sym.Abs(_amp) <= 1.0
 
-        super().__init__(
-            pulse_type=self.__class__.__name__,
+        instance = SymbolicPulse(
+            pulse_type=cls.alias,
             duration=duration,
             parameters=parameters,
             name=name,
@@ -734,15 +778,12 @@ class GaussianSquare(SymbolicPulse):
             constraints=consts_expr,
             valid_amp_conditions=valid_amp_conditions_expr,
         )
-        self.validate_parameters()
+        instance.validate_parameters()
 
-    @property
-    def risefall_sigma_ratio(self):
-        """Return risefall_sigma_ratio. This is auxiliary parameter to define width."""
-        return (self.duration - self.width) / (2.0 * self.sigma)
+        return instance
 
 
-class Drag(SymbolicPulse):
+class Drag(metaclass=_PulseType):
     """The Derivative Removal by Adiabatic Gate (DRAG) pulse is a standard Gaussian pulse
     with an additional Gaussian derivative component and lifting applied.
 
@@ -779,15 +820,17 @@ class Drag(SymbolicPulse):
            Phys. Rev. Lett. 103, 110501 – Published 8 September 2009.*
     """
 
-    def __init__(
-        self,
+    alias = "Drag"
+
+    def __new__(
+        cls,
         duration: Union[int, ParameterExpression],
         amp: Union[complex, ParameterExpression],
         sigma: Union[float, ParameterExpression],
         beta: Union[float, ParameterExpression],
         name: Optional[str] = None,
         limit_amplitude: Optional[bool] = None,
-    ):
+    ) -> SymbolicPulse:
         """Create new pulse instance.
 
         Args:
@@ -799,6 +842,9 @@ class Drag(SymbolicPulse):
             name: Display name for this pulse envelope.
             limit_amplitude: If ``True``, then limit the amplitude of the
                 waveform to 1. The default is ``True`` and the amplitude is constrained to 1.
+
+        Returns:
+            SymbolicPulse instance.
         """
         parameters = {"amp": amp, "sigma": sigma, "beta": beta}
 
@@ -814,8 +860,8 @@ class Drag(SymbolicPulse):
         consts_expr = _sigma > 0
         valid_amp_conditions_expr = sym.And(sym.Abs(_amp) <= 1.0, sym.Abs(_beta) < _sigma)
 
-        super().__init__(
-            pulse_type=self.__class__.__name__,
+        instance = SymbolicPulse(
+            pulse_type="Drag",
             duration=duration,
             parameters=parameters,
             name=name,
@@ -824,10 +870,12 @@ class Drag(SymbolicPulse):
             constraints=consts_expr,
             valid_amp_conditions=valid_amp_conditions_expr,
         )
-        self.validate_parameters()
+        instance.validate_parameters()
 
+        return instance
 
-class Constant(SymbolicPulse):
+
+class Constant(metaclass=_PulseType):
     """A simple constant pulse, with an amplitude value and a duration:
 
     .. math::
@@ -836,13 +884,15 @@ class Constant(SymbolicPulse):
         f(x) = 0      ,  elsewhere
     """
 
-    def __init__(
-        self,
+    alias = "Constant"
+
+    def __new__(
+        cls,
         duration: Union[int, ParameterExpression],
         amp: Union[complex, ParameterExpression],
         name: Optional[str] = None,
         limit_amplitude: Optional[bool] = None,
-    ):
+    ) -> SymbolicPulse:
         """Create new pulse instance.
 
         Args:
@@ -851,6 +901,9 @@ class Constant(SymbolicPulse):
             name: Display name for this pulse envelope.
             limit_amplitude: If ``True``, then limit the amplitude of the
                 waveform to 1. The default is ``True`` and the amplitude is constrained to 1.
+
+        Returns:
+            SymbolicPulse instance.
         """
         parameters = {"amp": amp}
 
@@ -867,8 +920,8 @@ class Constant(SymbolicPulse):
         envelope_expr = _amp * sym.Piecewise((1, sym.And(_t >= 0, _t <= _duration)), (0, True))
         valid_amp_conditions_expr = sym.Abs(_amp) <= 1.0
 
-        super().__init__(
-            pulse_type=self.__class__.__name__,
+        instance = SymbolicPulse(
+            pulse_type="Constant",
             duration=duration,
             parameters=parameters,
             name=name,
@@ -876,4 +929,6 @@ class Constant(SymbolicPulse):
             envelope=envelope_expr,
             valid_amp_conditions=valid_amp_conditions_expr,
         )
-        self.validate_parameters()
+        instance.validate_parameters()
+
+        return instance
diff --git a/qiskit/qobj/converters/pulse_instruction.py b/qiskit/qobj/converters/pulse_instruction.py
index 1bdb049a8..1ea4d1912 100644
--- a/qiskit/qobj/converters/pulse_instruction.py
+++ b/qiskit/qobj/converters/pulse_instruction.py
@@ -40,10 +40,51 @@ class ParametricPulseShapes(Enum):
     value is its mapping to the OpenPulse Command in Qiskit.
     """
 
-    gaussian = library.Gaussian
-    gaussian_square = library.GaussianSquare
-    drag = library.Drag
-    constant = library.Constant
+    gaussian = "Gaussian"
+    gaussian_square = "GaussianSquare"
+    drag = "Drag"
+    constant = "Constant"
+
+    @classmethod
+    def from_instance(
+        cls,
+        instance: Union[library.ParametricPulse, library.SymbolicPulse],
+    ) -> "ParametricPulseShapes":
+        """Get Qobj name from the pulse class instance.
+
+        Args:
+            instance: Symbolic or ParametricPulse class.
+
+        Returns:
+            Qobj name.
+
+        Raises:
+            QiskitError: When pulse instance is not recognizable type.
+        """
+        if isinstance(instance, library.SymbolicPulse):
+            return cls(instance.pulse_type)
+        if isinstance(instance, library.parametric_pulses.Gaussian):
+            return ParametricPulseShapes.gaussian
+        if isinstance(instance, library.parametric_pulses.GaussianSquare):
+            return ParametricPulseShapes.gaussian_square
+        if isinstance(instance, library.parametric_pulses.Drag):
+            return ParametricPulseShapes.drag
+        if isinstance(instance, library.parametric_pulses.Constant):
+            return ParametricPulseShapes.constant
+
+        raise QiskitError(f"'{instance}' is not valid pulse type.")
+
+    @classmethod
+    def to_type(cls, name: str) -> library.SymbolicPulse:
+        """Get symbolic pulse class from the name.
+
+        Args:
+            name: Qobj name of the pulse.
+
+        Returns:
+            Corresponding class.
+        """
+        return getattr(library, cls[name].value)
 
 
 class ConversionMethodBinder:
@@ -386,7 +427,7 @@ class InstructionToQobjConverter:
         if isinstance(instruction.pulse, (library.ParametricPulse, library.SymbolicPulse)):
             command_dict = {
                 "name": "parametric_pulse",
-                "pulse_shape": ParametricPulseShapes(type(instruction.pulse)).name,
+                "pulse_shape": ParametricPulseShapes.from_instance(instruction.pulse).name,
                 "t0": shift + instruction.start_time,
                 "ch": instruction.channel.name,
                 "parameters": instruction.pulse.parameters,
@@ -683,7 +724,7 @@ class QobjToInstructionConverter:
             short_pulse_id = hashlib.md5(base_str.encode("utf-8")).hexdigest()[:4]
             pulse_name = f"{instruction.pulse_shape}_{short_pulse_id}"
 
-        pulse = ParametricPulseShapes[instruction.pulse_shape].value(
+        pulse = ParametricPulseShapes.to_type(instruction.pulse_shape)(
             **instruction.parameters, name=pulse_name
         )
         return instructions.Play(pulse, channel) << t0
diff --git a/qiskit/qpy/binary_io/schedules.py b/qiskit/qpy/binary_io/schedules.py
index 2468d8dbf..223e2e77b 100644
--- a/qiskit/qpy/binary_io/schedules.py
+++ b/qiskit/qpy/binary_io/schedules.py
@@ -91,32 +91,16 @@ def _read_symbolic_pulse(file_obj, version):
     duration = value.read_value(file_obj, version, {})
     name = value.read_value(file_obj, version, {})
 
-    # TODO remove this and merge subclasses into a single kind of SymbolicPulse
-    #  We need some refactoring of our codebase,
-    #  mainly removal of isinstance check and name access with self.__class__.__name__.
-    if pulse_type == "Gaussian":
-        pulse_cls = library.Gaussian
-    elif pulse_type == "GaussianSquare":
-        pulse_cls = library.GaussianSquare
-    elif pulse_type == "Drag":
-        pulse_cls = library.Drag
-    elif pulse_type == "Constant":
-        pulse_cls = library.Constant
-    else:
-        pulse_cls = library.SymbolicPulse
-
-    # Skip calling constructor to absorb signature mismatch in subclass.
-    instance = object.__new__(pulse_cls)
-    instance.duration = duration
-    instance.name = name
-    instance._limit_amplitude = header.amp_limited
-    instance._pulse_type = pulse_type
-    instance._params = parameters
-    instance._envelope = envelope
-    instance._constraints = constraints
-    instance._valid_amp_conditions = valid_amp_conditions
-
-    return instance
+    return library.SymbolicPulse(
+        pulse_type=pulse_type,
+        duration=duration,
+        parameters=parameters,
+        name=name,
+        limit_amplitude=header.amp_limited,
+        envelope=envelope,
+        constraints=constraints,
+        valid_amp_conditions=valid_amp_conditions,
+    )
 
 
 def _read_alignment_context(file_obj, version):
diff --git a/qiskit/visualization/pulse_v2/generators/waveform.py b/qiskit/visualization/pulse_v2/generators/waveform.py
index 6172a283d..7a4b8c00f 100644
--- a/qiskit/visualization/pulse_v2/generators/waveform.py
+++ b/qiskit/visualization/pulse_v2/generators/waveform.py
@@ -44,7 +44,7 @@ from typing import Dict, Any, List, Union
 import numpy as np
 
 from qiskit import pulse, circuit
-from qiskit.pulse import instructions
+from qiskit.pulse import instructions, library
 from qiskit.visualization.exceptions import VisualizationError
 from qiskit.visualization.pulse_v2 import drawings, types, device_info
 
@@ -105,10 +105,11 @@ def gen_filled_waveform_stepwise(
             if isinstance(pval, circuit.ParameterExpression):
                 unbound_params.append(pname)
 
-        if hasattr(data.inst.pulse, "pulse_type"):
-            pulse_shape = data.inst.pulse.pulse_type
+        pulse_data = data.inst.pulse
+        if isinstance(pulse_data, library.SymbolicPulse):
+            pulse_shape = pulse_data.pulse_type
         else:
-            pulse_shape = data.inst.pulse.__class__.__name__
+            pulse_shape = "Waveform"
 
         return _draw_opaque_waveform(
             init_time=data.t0,
@@ -174,7 +175,14 @@ def gen_ibmq_latex_waveform_name(
         systematic_name = data.inst.name or "Delay"
         latex_name = None
     else:
-        systematic_name = data.inst.pulse.name or data.inst.pulse.__class__.__name__
+        pulse_data = data.inst.pulse
+        if pulse_data.name:
+            systematic_name = pulse_data.name
+        else:
+            if isinstance(pulse_data, library.SymbolicPulse):
+                systematic_name = pulse_data.pulse_type
+            else:
+                systematic_name = "Waveform"
 
         template = r"(?P<op>[A-Z]+)(?P<angle>[0-9]+)?(?P<sign>[pm])_(?P<ch>[dum])[0-9]+"
         match_result = re.match(template, systematic_name)
@@ -560,11 +568,11 @@ def _parse_waveform(
             if isinstance(duration, circuit.Parameter):
                 duration = None
 
-            if hasattr(operand, "pulse_type"):
-                # Symbolic pulse
-                meta["waveform shape"] = operand.pulse_type
+            if isinstance(operand, library.SymbolicPulse):
+                pulse_shape = operand.pulse_type
             else:
-                meta["waveform"] = operand.__class__.__name__
+                pulse_shape = "Waveform"
+            meta["waveform shape"] = pulse_shape
 
             meta.update(
                 {
diff --git a/releasenotes/notes/remove-symbolic-pulse-subclasses-77314a1654521852.yaml b/releasenotes/notes/remove-symbolic-pulse-subclasses-77314a1654521852.yaml
new file mode 100644
index 000000000..9bab49b21
--- /dev/null
+++ b/releasenotes/notes/remove-symbolic-pulse-subclasses-77314a1654521852.yaml
@@ -0,0 +1,41 @@
+---
+features:
+  - |
+    Symbolic pulse subclasses :class:`.Gaussian`, :class:`.GaussianSquare`,
+    :class:`.Drag` and :class:`.Constant` have been upgraded to
+    instantiate :class:`SymbolicPulse` rather than the subclass itself.
+    All parametric pulse objects in pulse programs must be symbolic pulse instances,
+    because subclassing is no longer neccesary. Note that :class:`SymbolicPulse` can
+    uniquely identify a particular envelope with the symbolic expression object
+    defined in :attr:`SymbolicPulse.envelope`.
+upgrade:
+  - |
+    ``isinstance`` check with pulse classes :class:`.Gaussian`, :class:`.GaussianSquare`,
+    :class:`.Drag` and :class:`.Constant` has been invalidated because
+    these pulse subclasses are no longer instantiated.
+    Instead of using type information, :attr:`SymbolicPulse.pulse_type` must be used.
+    This is assumed to be a unique string identifer for pulse envelopes,
+    and we can use string equality to investigate the pulse types. For example,
+
+    .. code-block:: python
+
+      from qiskit.pulse.library import Gaussian
+
+      pulse = Gaussian(160, 0.1, 40)
+
+      if isinstance(pulse, Gaussian)
+        print("This is Gaussian pulse.")
+
+    This code should be upgraded to
+
+    .. code-block:: python
+
+      from qiskit.pulse.library import Gaussian
+
+      pulse = Gaussian(160, 0.1, 40)
+
+      if pulse.pulse_type == "Gaussian":
+        print("This is Gaussian pulse.")
+
+    With the same reason, the class attributes such as ``pulse.__class__.__name__``
+    should not be accessed to get pulse type information.


Test patch:
diff --git a/test/python/pulse/test_calibrationbuilder.py b/test/python/pulse/test_calibrationbuilder.py
index 303365e01..ba108ca9f 100644
--- a/test/python/pulse/test_calibrationbuilder.py
+++ b/test/python/pulse/test_calibrationbuilder.py
@@ -121,12 +121,12 @@ class TestRZXCalibrationBuilderNoEcho(TestCalibrationBuilder):
         rzx_qc_instructions = cal_qc.calibrations["rzx"][((1, 0), (theta / 2,))].instructions
         self.assertEqual(rzx_qc_instructions[0][1].channel, DriveChannel(0))
         self.assertTrue(isinstance(rzx_qc_instructions[0][1], Play))
-        self.assertTrue(isinstance(rzx_qc_instructions[0][1].pulse, GaussianSquare))
+        self.assertEqual(rzx_qc_instructions[0][1].pulse.pulse_type, "GaussianSquare")
         self.assertEqual(rzx_qc_instructions[1][1].channel, DriveChannel(1))
         self.assertTrue(isinstance(rzx_qc_instructions[1][1], Delay))
         self.assertEqual(rzx_qc_instructions[2][1].channel, ControlChannel(1))
         self.assertTrue(isinstance(rzx_qc_instructions[2][1], Play))
-        self.assertTrue(isinstance(rzx_qc_instructions[2][1].pulse, GaussianSquare))
+        self.assertEqual(rzx_qc_instructions[2][1].pulse.pulse_type, "GaussianSquare")
 
         # Calculate the duration of one scaled Gaussian square pulse from the CX gate.
         cx_sched = self.inst_map.get("cx", qubits=(1, 0))
diff --git a/test/python/pulse/test_pulse_lib.py b/test/python/pulse/test_pulse_lib.py
index d49b1409c..44e892cc0 100644
--- a/test/python/pulse/test_pulse_lib.py
+++ b/test/python/pulse/test_pulse_lib.py
@@ -418,6 +418,65 @@ class TestParametricPulses(QiskitTestCase):
         reference = np.concatenate([-0.1 * np.ones(30), 0.1j * np.ones(50), -0.1 * np.ones(20)])
         np.testing.assert_array_almost_equal(waveform.samples, reference)
 
+    def test_no_subclass(self):
+        """Test no dedicated pulse subclass is created."""
+
+        gaussian_pulse = Gaussian(160, 0.1, 40)
+        self.assertIs(type(gaussian_pulse), SymbolicPulse)
+
+        gaussian_square_pulse = GaussianSquare(800, 0.1, 64, 544)
+        self.assertIs(type(gaussian_square_pulse), SymbolicPulse)
+
+        drag_pulse = Drag(160, 0.1, 40, 1.5)
+        self.assertIs(type(drag_pulse), SymbolicPulse)
+
+        constant_pulse = Constant(800, 0.1)
+        self.assertIs(type(constant_pulse), SymbolicPulse)
+
+    def test_gaussian_deprecated_type_check(self):
+        """Test isinstance check works with deprecation."""
+        gaussian_pulse = Gaussian(160, 0.1, 40)
+
+        self.assertTrue(isinstance(gaussian_pulse, SymbolicPulse))
+        with self.assertWarns(PendingDeprecationWarning):
+            self.assertTrue(isinstance(gaussian_pulse, Gaussian))
+            self.assertFalse(isinstance(gaussian_pulse, GaussianSquare))
+            self.assertFalse(isinstance(gaussian_pulse, Drag))
+            self.assertFalse(isinstance(gaussian_pulse, Constant))
+
+    def test_gaussian_square_deprecated_type_check(self):
+        """Test isinstance check works with deprecation."""
+        gaussian_square_pulse = GaussianSquare(800, 0.1, 64, 544)
+
+        self.assertTrue(isinstance(gaussian_square_pulse, SymbolicPulse))
+        with self.assertWarns(PendingDeprecationWarning):
+            self.assertFalse(isinstance(gaussian_square_pulse, Gaussian))
+            self.assertTrue(isinstance(gaussian_square_pulse, GaussianSquare))
+            self.assertFalse(isinstance(gaussian_square_pulse, Drag))
+            self.assertFalse(isinstance(gaussian_square_pulse, Constant))
+
+    def test_drag_deprecated_type_check(self):
+        """Test isinstance check works with deprecation."""
+        drag_pulse = Drag(160, 0.1, 40, 1.5)
+
+        self.assertTrue(isinstance(drag_pulse, SymbolicPulse))
+        with self.assertWarns(PendingDeprecationWarning):
+            self.assertFalse(isinstance(drag_pulse, Gaussian))
+            self.assertFalse(isinstance(drag_pulse, GaussianSquare))
+            self.assertTrue(isinstance(drag_pulse, Drag))
+            self.assertFalse(isinstance(drag_pulse, Constant))
+
+    def test_constant_deprecated_type_check(self):
+        """Test isinstance check works with deprecation."""
+        constant_pulse = Constant(160, 0.1, 40, 1.5)
+
+        self.assertTrue(isinstance(constant_pulse, SymbolicPulse))
+        with self.assertWarns(PendingDeprecationWarning):
+            self.assertFalse(isinstance(constant_pulse, Gaussian))
+            self.assertFalse(isinstance(constant_pulse, GaussianSquare))
+            self.assertFalse(isinstance(constant_pulse, Drag))
+            self.assertTrue(isinstance(constant_pulse, Constant))
+
 
 class TestFunctionalPulse(QiskitTestCase):
     """Waveform tests."""