Repository: Qiskit/qiskit

Issue:
SamplePulse should clip approximately unit norm pulses to unit norm
<!-- ⚠️ If you do not respect this template, your issue will be closed -->
<!-- ⚠️ Make sure to browse the opened and closed issues -->

### Information

- **Qiskit Terra version**:
- **Python version**:
- **Operating system**:

### What is the current behavior?
The qobj specification does not specify a datatype for pulse samples datatypes. This is because JSON does not have a floating point datatypes. Backends that create a pulse_library may not emit sufficient precision to JSON and this may cause `SamplePulse`s to have a norm greater than one when cast to `np.complex128` even if it should be exactly unit norm.

### Example
```python
import cmath

import numpy as np

from qiskit.pulse import SamplePulse

pulse = np.array([cmath.exp(2*np.pi*i*1j) for i in np.linspace(0, 1, 1000)], dtype=np.complex64)
samples = np.stack(np.transpose([np.real(pulse), np.imag(pulse)]))
samples_list = samples.tolist()
samples = [sample[0]+sample[1]*1j for sample in samples_list]
samples = np.asarray(samples, dtype=np.complex128)

SamplePulse(samples)
```
### What is the expected behavior?
Should not fail

### Suggested solutions
Samples that have a norm of almost but greater than 1. should have their norm scaled to 1.


Discussion:

Resolving patch:
diff --git a/qiskit/pulse/commands/sample_pulse.py b/qiskit/pulse/commands/sample_pulse.py
--- a/qiskit/pulse/commands/sample_pulse.py
+++ b/qiskit/pulse/commands/sample_pulse.py
@@ -31,21 +31,23 @@ class SamplePulse(Command):
 
     prefix = 'p'
 
-    def __init__(self, samples: Union[np.ndarray, List[complex]], name: Optional[str] = None):
+    def __init__(self, samples: Union[np.ndarray, List[complex]], name: Optional[str] = None,
+                 epsilon: float = 1e-6):
         """Create new sample pulse command.
 
         Args:
             samples: Complex array of pulse envelope
             name: Unique name to identify the pulse
-        Raises:
-            PulseError: when pulse envelope amplitude exceeds 1
+            epsilon: Pulse sample norm tolerance for clipping.
+                If any sample's norm exceeds unity by less than or equal to epsilon
+                it will be clipped to unit norm. If the sample
+                norm is greater than 1+epsilon an error will be raised
         """
         super().__init__(duration=len(samples))
 
-        if np.any(np.abs(samples) > 1):
-            raise PulseError('Absolute value of pulse envelope amplitude exceeds 1.')
+        samples = np.asarray(samples, dtype=np.complex_)
 
-        self._samples = np.asarray(samples, dtype=np.complex_)
+        self._samples = self._clip(samples, epsilon=epsilon)
         self._name = SamplePulse.create_name(name)
 
     @property
@@ -53,6 +55,37 @@ def samples(self):
         """Return sample values."""
         return self._samples
 
+    def _clip(self, samples: np.ndarray, epsilon: float = 1e-6):
+        """If samples are within epsilon of unit norm, clip sample by reducing norm by (1-epsilon).
+
+        If difference is greater than epsilon error is raised.
+
+        Args:
+            samples: Complex array of pulse envelope
+            epsilon: Pulse sample norm tolerance for clipping.
+                If any sample's norm exceeds unity by less than or equal to epsilon
+                it will be clipped to unit norm. If the sample
+                norm is greater than 1+epsilon an error will be raised
+
+        Returns:
+            np.ndarray: Clipped pulse samples
+        Raises:
+            PulseError: If there exists a pulse sample with a norm greater than 1+epsilon
+        """
+        samples_norm = np.abs(samples)
+        to_clip = (samples_norm > 1.) & (samples_norm <= 1. + epsilon)
+
+        if np.any(to_clip):
+            clip_where = np.argwhere(to_clip)
+            clipped_samples = np.exp(1j*np.angle(samples[clip_where]), dtype=np.complex_)
+            samples[clip_where] = clipped_samples
+            samples_norm[clip_where] = np.abs(clipped_samples)
+
+        if np.any(samples_norm > 1.):
+            raise PulseError('Pulse contains sample with norm greater than 1+epsilon.')
+
+        return samples
+
     def draw(self, dt: float = 1, style: Optional['PulseStyle'] = None,
              filename: Optional[str] = None, interp_method: Optional[Callable] = None,
              scaling: float = 1, interactive: bool = False):


Test patch:
diff --git a/test/python/pulse/test_commands.py b/test/python/pulse/test_commands.py
--- a/test/python/pulse/test_commands.py
+++ b/test/python/pulse/test_commands.py
@@ -19,11 +19,78 @@
 import unittest
 import numpy as np
 
-from qiskit.pulse import (Acquire, FrameChange, PersistentValue,
-                          Snapshot, Kernel, Discriminator, functional_pulse)
+from qiskit.pulse import (SamplePulse, Acquire, FrameChange, PersistentValue,
+                          Snapshot, Kernel, Discriminator, functional_pulse,
+                          PulseError)
 from qiskit.test import QiskitTestCase
 
 
+class TestSamplePulse(QiskitTestCase):
+    """SamplePulse tests."""
+
+    def test_sample_pulse(self):
+        """Test pulse initialization."""
+        n_samples = 100
+        samples = np.linspace(0, 1., n_samples, dtype=np.complex128)
+        name = 'test'
+        sample_pulse = SamplePulse(samples, name=name)
+
+        self.assertEqual(sample_pulse.samples.dtype, np.complex128)
+        np.testing.assert_almost_equal(sample_pulse.samples, samples)
+
+        self.assertEqual(sample_pulse.duration, n_samples)
+        self.assertEqual(sample_pulse.name, name)
+
+    def test_type_casting(self):
+        """Test casting of input samples to numpy array."""
+        n_samples = 100
+        samples_f64 = np.linspace(0, 1., n_samples, dtype=np.float64)
+
+        sample_pulse_f64 = SamplePulse(samples_f64)
+        self.assertEqual(sample_pulse_f64.samples.dtype, np.complex128)
+
+        samples_c64 = np.linspace(0, 1., n_samples, dtype=np.complex64)
+
+        sample_pulse_c64 = SamplePulse(samples_c64)
+        self.assertEqual(sample_pulse_c64.samples.dtype, np.complex128)
+
+        samples_list = np.linspace(0, 1., n_samples).tolist()
+
+        sample_pulse_list = SamplePulse(samples_list)
+        self.assertEqual(sample_pulse_list.samples.dtype, np.complex128)
+
+    def test_pulse_limits(self):
+        """Test that limits of pulse norm of one are enforced properly."""
+
+        # test norm is correct for complex128 numpy data
+        unit_pulse_c128 = np.exp(1j*2*np.pi*np.linspace(0, 1, 1000), dtype=np.complex128)
+        # test does not raise error
+        try:
+            SamplePulse(unit_pulse_c128)
+        except PulseError:
+            self.fail('SamplePulse incorrectly failed on approximately unit norm samples.')
+
+        invalid_const = 1.1
+        with self.assertRaises(PulseError):
+            SamplePulse(invalid_const*np.exp(1j*2*np.pi*np.linspace(0, 1, 1000)))
+
+        # Test case where data is converted to python types with complex as a list
+        # with form [re, im] and back to a numpy array.
+        # This is how the transport layer handles samples in the qobj so it is important
+        # to test.
+        unit_pulse_c64 = np.exp(1j*2*np.pi*np.linspace(0, 1, 1000), dtype=np.complex64)
+        sample_components = np.stack(np.transpose([np.real(unit_pulse_c64),
+                                                   np.imag(unit_pulse_c64)]))
+        pulse_list = sample_components.tolist()
+        recombined_pulse = [sample[0]+sample[1]*1j for sample in pulse_list]
+
+        # test does not raise error
+        try:
+            SamplePulse(recombined_pulse)
+        except PulseError:
+            self.fail('SamplePulse incorrectly failed to approximately unit norm samples.')
+
+
 class TestAcquire(QiskitTestCase):
     """Acquisition tests."""