Repository: Qiskit/qiskit

Issue:
Instruction schedule map cannot be pickled with python3.6
<!-- ⚠️ 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**: master (see note)
- **Python version**: 3.6.9
- **Operating system**: 18.04.5 LTS (Bionic Beaver), WSL2.0

### What is the current behavior?

```python
from qiskit.test.mock import FakeAthens
from qiskit.tools.parallel import parallel_map

backend = FakeAthens()
inst_map = backend.defaults().instruction_schedule_map

def task(inst_map):
    print(inst_map.instructions)

result = parallel_map(task, [inst_map, inst_map])
```

This causes `_pickle.PicklingError: Can't pickle typing.Union[qiskit.circuit.parameterexpression.ParameterExpression, float]: it's not the same object as typing.Union` and hangs the parallel map. This is the blocker of #6759 .

I was thinking this is fork issue and tried to deep copy the inst_map, but seems like this is a different problem.

### Note
To test above code, a part of #6759 is needed (the master uses lambda function here but this cannot be pickled).
https://github.com/Qiskit/qiskit-terra/blob/0f9aa882b8cbdcef4614b94031ddd3244832d8d6/qiskit/pulse/instruction_schedule_map.py#L73-L76

### What is the expected behavior?

Currently inst map depends on the builtin inspect module, because the map accepts both callback function and `Schedule` with `Parameter` to define a template schedule. From `InstructionScheduleMap.get` method, these programs look like a function that has several named arguments (i.e. pulse parameters), but can have default values and also partially assigned if the program is `Schedule`. To support this complicated behavior, we implemented parameter signature management and assignment logic with `instpect.Signature`. For example,

https://github.com/Qiskit/qiskit-terra/blob/a34b2ab002c0e0935d02691026ae676ee0adf90b/qiskit/pulse/instruction_schedule_map.py#L190-L197

This object cannot be dropped unless we find alternative approach.

### Suggested solutions
No idea. Seems like a defect of python3.6.



Discussion:
There is an similar issue here https://github.com/huggingface/transformers/issues/8212
Seems like the solution is to upgrade Python (drop py36 test) or change implementation of `InstructionScheduleMap`.

Resolving patch:
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
@@ -27,12 +27,13 @@
 
 """
 import inspect
+import functools
 import warnings
 from collections import defaultdict
 from typing import Callable, Iterable, List, Tuple, Union, Optional, NamedTuple
 
 from qiskit.circuit.instruction import Instruction
-from qiskit.circuit.parameterexpression import ParameterExpression, ParameterValueType
+from qiskit.circuit.parameterexpression import ParameterExpression
 from qiskit.pulse.exceptions import PulseError
 from qiskit.pulse.schedule import Schedule, ScheduleBlock, ParameterizedSchedule
 
@@ -59,7 +60,12 @@ class InstructionScheduleMap:
     def __init__(self):
         """Initialize a circuit instruction to schedule mapper instance."""
         # The processed and reformatted circuit instruction definitions
-        self._map = defaultdict(lambda: defaultdict(Generator))
+
+        # Do not use lambda function for nested defaultdict, i.e. lambda: defaultdict(Generator).
+        # This crashes qiskit parallel. Note that parallel framework passes args as
+        # pickled object, however lambda function cannot be pickled.
+        self._map = defaultdict(functools.partial(defaultdict, Generator))
+
         # A backwards mapping from qubit to supported instructions
         self._qubit_instructions = defaultdict(set)
 
@@ -260,7 +266,6 @@ def add(
             for argname in ordered_names:
                 param_signature = inspect.Parameter(
                     name=argname,
-                    annotation=ParameterValueType,
                     kind=inspect.Parameter.POSITIONAL_OR_KEYWORD,
                 )
                 parameters.append(param_signature)
@@ -278,7 +283,6 @@ def add(
             for argname in schedule.parameters:
                 param_signature = inspect.Parameter(
                     name=argname,
-                    annotation=ParameterValueType,
                     kind=inspect.Parameter.POSITIONAL_OR_KEYWORD,
                 )
                 parameters.append(param_signature)
@@ -374,6 +378,19 @@ def __str__(self):
         instructions = single_q_insts + multi_q_insts
         return f"<{self.__class__.__name__}({instructions})>"
 
+    def __eq__(self, other):
+        if not isinstance(other, InstructionScheduleMap):
+            return False
+
+        for inst in self.instructions:
+            for qinds in self.qubits_with_instruction(inst):
+                try:
+                    if self._map[inst][_to_tuple(qinds)] != other._map[inst][_to_tuple(qinds)]:
+                        return False
+                except KeyError:
+                    return False
+        return True
+
 
 def _to_tuple(values: Union[int, Iterable[int]]) -> Tuple[int, ...]:
     """Return the input as a tuple.


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
@@ -11,6 +11,9 @@
 # that they have been altered from the originals.
 
 """Test the InstructionScheduleMap."""
+import copy
+import pickle
+
 import numpy as np
 
 import qiskit.pulse.library as library
@@ -31,7 +34,7 @@
 from qiskit.qobj import PulseQobjInstruction
 from qiskit.qobj.converters import QobjToInstructionConverter
 from qiskit.test import QiskitTestCase
-from qiskit.test.mock import FakeOpenPulse2Q
+from qiskit.test.mock import FakeOpenPulse2Q, FakeAthens
 
 
 class TestInstructionScheduleMap(QiskitTestCase):
@@ -520,3 +523,29 @@ def test_callable_sched1(par_b):
         inst_map.add("my_gate2", (0,), test_callable_sched2, ["par_b"])
         ret_sched = inst_map.get("my_gate2", (0,), par_b=0.1)
         self.assertEqual(ret_sched, ref_sched)
+
+    def test_two_instmaps_equal(self):
+        """Test eq method when two instmaps are identical."""
+        instmap1 = FakeAthens().defaults().instruction_schedule_map
+        instmap2 = copy.deepcopy(instmap1)
+
+        self.assertEqual(instmap1, instmap2)
+
+    def test_two_instmaps_different(self):
+        """Test eq method when two instmaps are not identical."""
+        instmap1 = FakeAthens().defaults().instruction_schedule_map
+        instmap2 = copy.deepcopy(instmap1)
+
+        # override one of instruction
+        instmap2.add("sx", (0,), Schedule())
+
+        self.assertNotEqual(instmap1, instmap2)
+
+    def test_instmap_picklable(self):
+        """Test if instmap can be pickled."""
+        instmap = FakeAthens().defaults().instruction_schedule_map
+
+        ser_obj = pickle.dumps(instmap)
+        deser_instmap = pickle.loads(ser_obj)
+
+        self.assertEqual(instmap, deser_instmap)