Repository: Qiskit/qiskit

Issue:
GaussianSquare pulse is parameterized oddly
<!-- ⚠️ If you do not respect this template, your issue will be closed -->
<!-- ⚠️ Make sure to browse the opened and closed issues to confirm this idea does not exist. -->

### What is the expected enhancement?


Just looking at the parameterization of GaussianSquare in qiskit pulse (https://qiskit.org/documentation/stubs/qiskit.pulse.GaussianSquare.html) — it seems to make the “natural” thing to do when sweeping the duration to leave the width of the square top part constant and vary the width of the rise/fall..  

A user might reasonably expect the width of the top part to sweep with duration so longer duration pulses have linearly more power.

This happens to be extremely inefficient with our backend hardware in terms of memory consumption (and is probably not what most people want to do when they sweep a duration anyway.)  Would it make sense to provide a more natural parameterization (possibly as a named risefall argument or something)?  

Lauren suggested using a num_sigma parameter instead of width to characterize how long (in units of sigma) the rise/fall sections should be.



Discussion:
Hi, I can work on that.
Thank you @Feigenbaum4669, I've assigned. Please comment if you need clarification or help 😄.
Hi, should I modify the existing implementation of GaussianSquare (and function gaussian_square) or rather create the new class, e.g. GaussianSquareNatural (and function, e.g. gaussian_square_natural)? If the former is true then could please specify how you want the class to be overloaded? I assume we want to keep the old functionality anyway. If it is not true then I can, of course, change the original ones.

The question concerns mainly the class since I can adapt the function gaussian_square to support both parametrizations quite easily.
I think you can add argument something like `risefall` (or similaly ratio of `risefall` to `sigma`) and deprecate `width`, or you can keep `width` and set default to `None`. One reason of keeping the original class is dependency on backend compiler. Because we need to guarantee the same parametic pulse is defined in the backend to generate the same waveform, basically you cannot define arbitrary parametric pulse on front end. Eventually `risefall` should be converted into `width` to conform to the spec of qobj.

https://github.com/Qiskit/qiskit-terra/blob/656564587a83bc4bb5b409d8123b09696c4b3007/qiskit/qobj/converters/pulse_instruction.py#L386-L393

Here you can find how parametric pulses are defined in the qobj. If you change the `parameters` field, that will crash the backend compiler.
@nkanazawa1989 Thank you!

Resolving patch:
diff --git a/qiskit/pulse/library/parametric_pulses.py b/qiskit/pulse/library/parametric_pulses.py
--- a/qiskit/pulse/library/parametric_pulses.py
+++ b/qiskit/pulse/library/parametric_pulses.py
@@ -191,12 +191,25 @@ def __repr__(self) -> str:
 
 
 class GaussianSquare(ParametricPulse):
-    """A square pulse with a Gaussian shaped risefall on either side:
+    """A square pulse with a Gaussian shaped risefall on both sides. Either risefall_sigma_ratio
+     or width parameter has to be specified.
+
+    If risefall_sigma_ratio is not None and width is None:
+
+    :math:`risefall = risefall` _ :math:`to` _ :math:`sigma * sigma`
+
+    :math:`width = duration - 2 * risefall`
+
+    If width is not None and risefall_sigma_ratio is None:
 
     .. math::
 
         risefall = (duration - width) / 2
 
+    In both cases, the pulse is defined as:
+
+    .. math::
+
         0 <= x < risefall
 
         f(x) = amp * exp( -(1/2) * (x - risefall/2)^2 / sigma^2) )
@@ -208,6 +221,7 @@ class GaussianSquare(ParametricPulse):
         risefall + width <= x < duration
 
         f(x) = amp * exp( -(1/2) * (x - (risefall + width)/2)^2 / sigma^2) )
+
     """
 
     def __init__(
@@ -215,7 +229,8 @@ def __init__(
         duration: Union[int, ParameterExpression],
         amp: Union[complex, ParameterExpression],
         sigma: Union[float, ParameterExpression],
-        width: Union[float, ParameterExpression],
+        width: Union[float, ParameterExpression] = None,
+        risefall_sigma_ratio: Union[float, ParameterExpression] = None,
         name: Optional[str] = None,
     ):
         """Initialize the gaussian square pulse.
@@ -226,12 +241,14 @@ def __init__(
             sigma: A measure of how wide or narrow the Gaussian risefall is; see the class
                    docstring for more details.
             width: The duration of the embedded square pulse.
+            risefall_sigma_ratio: The ratio of each risefall duration to sigma.
             name: Display name for this pulse envelope.
         """
         if not _is_parameterized(amp):
             amp = complex(amp)
         self._amp = amp
         self._sigma = sigma
+        self._risefall_sigma_ratio = risefall_sigma_ratio
         self._width = width
         super().__init__(duration=duration, name=name)
 
@@ -245,6 +262,11 @@ def sigma(self) -> Union[float, ParameterExpression]:
         """The Gaussian standard deviation of the pulse width."""
         return self._sigma
 
+    @property
+    def risefall_sigma_ratio(self) -> Union[float, ParameterExpression]:
+        """The duration of each risefall in terms of sigma."""
+        return self._risefall_sigma_ratio
+
     @property
     def width(self) -> Union[float, ParameterExpression]:
         """The width of the square portion of the pulse."""
@@ -256,12 +278,43 @@ def get_waveform(self) -> Waveform:
         )
 
     def validate_parameters(self) -> None:
+
         if not _is_parameterized(self.amp) and abs(self.amp) > 1.0:
             raise PulseError("The amplitude norm must be <= 1, " "found: {}".format(abs(self.amp)))
         if not _is_parameterized(self.sigma) and self.sigma <= 0:
             raise PulseError("Sigma must be greater than 0.")
-        if not _is_parameterized(self.width) and (self.width < 0 or self.width >= self.duration):
-            raise PulseError("The pulse width must be at least 0 and less than its duration.")
+        if self.width is not None and self.risefall_sigma_ratio is not None:
+            raise PulseError(
+                "Either the pulse width or the risefall_sigma_ratio parameter can be specified"
+                " but not both."
+            )
+        if self.width is None and self.risefall_sigma_ratio is None:
+            raise PulseError(
+                "Either the pulse width or the risefall_sigma_ratio parameter must be specified."
+            )
+        if self.width is not None:
+            if not _is_parameterized(self.width) and self.width < 0:
+                raise PulseError("The pulse width must be at least 0.")
+            if (
+                not (_is_parameterized(self.width) or _is_parameterized(self.duration))
+                and self.width >= self.duration
+            ):
+                raise PulseError("The pulse width must be less than its duration.")
+            self._risefall_sigma_ratio = (self.duration - self.width) / (2.0 * self.sigma)
+
+        else:
+            if not _is_parameterized(self.risefall_sigma_ratio) and self.risefall_sigma_ratio <= 0:
+                raise PulseError("The parameter risefall_sigma_ratio must be greater than 0.")
+            if not (
+                _is_parameterized(self.risefall_sigma_ratio)
+                or _is_parameterized(self.duration)
+                or _is_parameterized(self.sigma)
+            ) and self.risefall_sigma_ratio >= self.duration / (2.0 * self.sigma):
+                raise PulseError(
+                    "The parameter risefall_sigma_ratio must be less than duration/("
+                    "2*sigma)={}.".format(self.duration / (2.0 * self.sigma))
+                )
+            self._width = self.duration - 2.0 * self.risefall_sigma_ratio * self.sigma
 
     @property
     def parameters(self) -> Dict[str, Any]:


Test patch:
diff --git a/test/python/pulse/test_pulse_lib.py b/test/python/pulse/test_pulse_lib.py
--- a/test/python/pulse/test_pulse_lib.py
+++ b/test/python/pulse/test_pulse_lib.py
@@ -104,6 +104,7 @@ def test_construction(self):
         """Test that parametric pulses can be constructed without error."""
         Gaussian(duration=25, sigma=4, amp=0.5j)
         GaussianSquare(duration=150, amp=0.2, sigma=8, width=140)
+        GaussianSquare(duration=150, amp=0.2, sigma=8, risefall_sigma_ratio=2.5)
         Constant(duration=150, amp=0.1 + 0.4j)
         Drag(duration=25, amp=0.2 + 0.3j, sigma=7.8, beta=4)
 
@@ -124,6 +125,13 @@ def test_gaussian_square_pulse(self):
             duration=125, sigma=4, amp=0.5j, width=100, zero_ends=True
         ).samples
         np.testing.assert_almost_equal(sample_pulse.samples, pulse_lib_gauss_sq)
+        gauss_sq = GaussianSquare(duration=125, sigma=4, amp=0.5j, risefall_sigma_ratio=3.125)
+        sample_pulse = gauss_sq.get_waveform()
+        self.assertIsInstance(sample_pulse, Waveform)
+        pulse_lib_gauss_sq = gaussian_square(
+            duration=125, sigma=4, amp=0.5j, width=100, zero_ends=True
+        ).samples
+        np.testing.assert_almost_equal(sample_pulse.samples, pulse_lib_gauss_sq)
 
     def test_gauss_square_extremes(self):
         """Test that the gaussian square pulse can build a gaussian."""
@@ -207,6 +215,10 @@ def test_repr(self):
         self.assertEqual(
             repr(gaus_square), "GaussianSquare(duration=20, amp=(1+0j), sigma=30, width=3)"
         )
+        gaus_square = GaussianSquare(duration=20, sigma=30, amp=1.0, risefall_sigma_ratio=0.1)
+        self.assertEqual(
+            repr(gaus_square), "GaussianSquare(duration=20, amp=(1+0j), sigma=30, width=14.0)"
+        )
         drag = Drag(duration=5, amp=0.5, sigma=7, beta=1)
         self.assertEqual(repr(drag), "Drag(duration=5, amp=(0.5+0j), sigma=7, beta=1)")
         const = Constant(duration=150, amp=0.1 + 0.4j)
@@ -221,8 +233,14 @@ def test_param_validation(self):
         """Test that parametric pulse parameters are validated when initialized."""
         with self.assertRaises(PulseError):
             Gaussian(duration=25, sigma=0, amp=0.5j)
+        with self.assertRaises(PulseError):
+            GaussianSquare(duration=150, amp=0.2, sigma=8)
+        with self.assertRaises(PulseError):
+            GaussianSquare(duration=150, amp=0.2, sigma=8, width=100, risefall_sigma_ratio=5)
         with self.assertRaises(PulseError):
             GaussianSquare(duration=150, amp=0.2, sigma=8, width=160)
+        with self.assertRaises(PulseError):
+            GaussianSquare(duration=150, amp=0.2, sigma=8, risefall_sigma_ratio=10)
         with self.assertRaises(PulseError):
             Constant(duration=150, amp=0.9 + 0.8j)
         with self.assertRaises(PulseError):