Repository: Qiskit/qiskit

Issue:
Add id field for unique identification of Instructions and PulseLibraryItem in pulse
<!-- ⚠️ 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?

As suggested by @lcapelluto : Add some form of generated unique identifier `id` field for `SamplePulse`/`Instruction`/`PulseLibraryItem`/`...` in pulse, so that they can be identified using this instead of the `name` field.

### Purpose

As far as I am aware, the only way to identify a `SamplePulse`/`Instruction`/`PulseLibraryItem`/`...` in pulse is through the `name` field. This has caused a recent issue in `Aer` as the names of `PulseLibraryItem`s now get a hash added at the end, and as such there ends up being a name mismatch between an `Instruction` in an experiment to play a sample pulse, and the name of the sample pulse itself in an assembled `PulseQobj`'s `pulse_library` (please correct me if the terminology i'm using is incorrect). E.g. if a sample pulse has name `rabi_pulse`, the `Instruction` to play the pulse will inherit the name `rabi_pulse`, but the name of the pulse in the `pulse_library` will be something like `rabi_pulse--83975728198`. As such, relating the `Instruction` to the sample pulse in the `pulse_library` using the `name` requires ignoring the `--83975728198`, which to work requires assumptions on what a user will "typically" enter into the `name` field.


Discussion:

Resolving patch:
diff --git a/qiskit/assembler/assemble_schedules.py b/qiskit/assembler/assemble_schedules.py
--- a/qiskit/assembler/assemble_schedules.py
+++ b/qiskit/assembler/assemble_schedules.py
@@ -15,6 +15,7 @@
 """Assemble function for converting a list of circuits into a qobj."""
 from collections import defaultdict
 from typing import Any, Dict, List, Tuple
+import hashlib
 
 from qiskit.exceptions import QiskitError
 from qiskit.pulse import Schedule, Acquire, Delay, Play
@@ -134,8 +135,8 @@ def _assemble_experiments(
 
     # Top level Qobj configuration
     experiment_config = {
-        'pulse_library': [PulseLibraryItem(name=pulse.name, samples=pulse.samples)
-                          for pulse in user_pulselib.values()],
+        'pulse_library': [PulseLibraryItem(name=name, samples=samples)
+                          for name, samples in user_pulselib.items()],
         'memory_slots': max([exp.header.memory_slots for exp in experiments])
     }
 
@@ -171,6 +172,9 @@ def _assemble_instructions(
     acquire_instruction_map = defaultdict(list)
     for time, instruction in schedule.instructions:
 
+        if isinstance(instruction, ParametricInstruction):  # deprecated
+            instruction = Play(instruction.command, instruction.channels[0], name=instruction.name)
+
         if isinstance(instruction, Play) and isinstance(instruction.pulse, ParametricPulse):
             pulse_shape = ParametricPulseShapes(type(instruction.pulse)).name
             if pulse_shape not in run_config.parametric_pulses:
@@ -178,33 +182,16 @@ def _assemble_instructions(
                                    instruction.channel,
                                    name=instruction.name)
 
-        if isinstance(instruction, ParametricInstruction):  # deprecated
-            pulse_shape = ParametricPulseShapes(type(instruction.command)).name
-            if pulse_shape not in run_config.parametric_pulses:
-                # Convert to SamplePulse if the backend does not support it
-                instruction = PulseInstruction(instruction.command.get_sample_pulse(),
-                                               instruction.channels[0],
-                                               name=instruction.name)
+        if isinstance(instruction, PulseInstruction):  # deprecated
+            instruction = Play(SamplePulse(name=name, samples=instruction.command.samples),
+                               instruction.channels[0], name=name)
 
         if isinstance(instruction, Play) and isinstance(instruction.pulse, SamplePulse):
-            name = instruction.pulse.name
-            if instruction.pulse != user_pulselib.get(name):
-                name = "{0}-{1:x}".format(name, hash(instruction.pulse.samples.tostring()))
-                instruction = Play(SamplePulse(name=name, samples=instruction.pulse.samples),
-                                   channel=instruction.channel,
-                                   name=instruction.name)
-            user_pulselib[name] = instruction.pulse
-
-        if isinstance(instruction, PulseInstruction):  # deprecated
-            name = instruction.command.name
-            if name in user_pulselib and instruction.command != user_pulselib[name]:
-                name = "{0}-{1:x}".format(name, hash(instruction.command.samples.tostring()))
-                instruction = PulseInstruction(
-                    command=SamplePulse(name=name, samples=instruction.command.samples),
-                    name=instruction.name,
-                    channel=instruction.channels[0])
-            # add samples to pulse library
-            user_pulselib[name] = instruction.command
+            name = hashlib.sha256(instruction.pulse.samples).hexdigest()
+            instruction = Play(SamplePulse(name=name, samples=instruction.pulse.samples),
+                               channel=instruction.channel,
+                               name=name)
+            user_pulselib[name] = instruction.pulse.samples
 
         if isinstance(instruction, (AcquireInstruction, Acquire)):
             max_memory_slot = max(max_memory_slot,


Test patch:
diff --git a/test/python/compiler/test_assembler.py b/test/python/compiler/test_assembler.py
--- a/test/python/compiler/test_assembler.py
+++ b/test/python/compiler/test_assembler.py
@@ -379,6 +379,40 @@ def setUp(self):
             'backend_version': '0.0.0'
         }
 
+    def test_assemble_sample_pulse(self):
+        """Test that the pulse lib and qobj instruction can be paired up."""
+        schedule = pulse.Schedule()
+        schedule += pulse.Play(pulse.SamplePulse([0.1]*16, name='test0'),
+                               pulse.DriveChannel(0),
+                               name='test1')
+        schedule += pulse.Play(pulse.SamplePulse([0.1]*16, name='test1'),
+                               pulse.DriveChannel(0),
+                               name='test2')
+        schedule += pulse.Play(pulse.SamplePulse([0.5]*16, name='test0'),
+                               pulse.DriveChannel(0),
+                               name='test1')
+        qobj = assemble(schedule,
+                        qobj_header=self.header,
+                        qubit_lo_freq=self.default_qubit_lo_freq,
+                        meas_lo_freq=self.default_meas_lo_freq,
+                        schedule_los=[],
+                        **self.config)
+        validate_qobj_against_schema(qobj)
+
+        test_dict = qobj.to_dict()
+        experiment = test_dict['experiments'][0]
+        inst0_name = experiment['instructions'][0]['name']
+        inst1_name = experiment['instructions'][1]['name']
+        inst2_name = experiment['instructions'][2]['name']
+        pulses = {}
+        for item in test_dict['config']['pulse_library']:
+            pulses[item['name']] = item['samples']
+        self.assertTrue(all(name in pulses for name in [inst0_name, inst1_name, inst2_name]))
+        # Their pulses are the same
+        self.assertEqual(inst0_name, inst1_name)
+        self.assertTrue(np.allclose(pulses[inst0_name], [0.1]*16))
+        self.assertTrue(np.allclose(pulses[inst2_name], [0.5]*16))
+
     def test_assemble_single_schedule_without_lo_config(self):
         """Test assembling a single schedule, no lo config."""
         qobj = assemble(self.schedule,