Repository: Qiskit/qiskit

Issue:
InstructionScheduleMap parameters should be ParameterExpressions
<!-- ⚠️ 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?
The input parameters into the InstructionScheduleMap should be expected to be `ParameterExpression`s. Types and tests should be updated accordingly. This is a follow on to #3385.




Discussion:
Do you mind if I take this on?
@srijithprabhu If you’re not still looking at this, I’d be interested in taking a look
Given there's been little movement on this I'm good to reassign to you @menehune23. Thanks for taking this on.

The follow up if interested after is #3384 which would be a very valuable feature, essentially turning Schedules into functions and opening up the door to reusable code representations.
Sure @taalexander, I’d love to take them both on :)
If you have questions that need quicker answers you can always feel free to find me on our Slack.
@taalexander I've got a bit of a concern, as it looks like `ParameterExpression`s represent real numbers (based on how they return `self` when conjugating), while the existing param type for `InstructionScheduleMap`'s `get()` and `pop()` seems to support complex numbers (`Union[int, float, complex]`). Are parameters here expected to be real always?

Also, is this change going to be a breaking change for any downstream consumers of this class? It seems so but I'll have to dig further.
For now if a `ParameterExpression` is passed to `get`/`pop` could we just pass it through to the underlying function? If passed to the very rough https://github.com/Qiskit/qiskit-terra/blob/f132cf7fc69920f393bc2055e98f20e8e7d6bce0/qiskit/pulse/schedule.py#L744
(was originally meant to be temporary and replaced by #3384). 

This class could be modified to try and cast the `ParameterExpression` to a float, and raise if not possible.
@taalexander Were you referring to casting from within `ParameterizedSchedule`? I could go with that approach, but since this issue pertains to params for schedule generator functions as well as `ParameterizedSchedule`s (for instance, this generator:  https://github.com/Qiskit/qiskit-terra/blob/master/test/python/pulse/test_instruction_schedule_map.py#L195), it might be less breaking to cast to a float before calling any generators/schedules (i.e. within `InstructionScheduleMap` itself). It's easy to update `ParameterizedSchedule`, but difficult to make users update their generator functions. Thoughts?
I suppose if schedules will eventually support parameters (per #3384), I could see the argument for NOT converting to floats within the `InstructionScheduleMap`. I put up a PR (#4940), so feel free to discuss further there if appropriate.
My apologies for the delay (I work a lot on the backend and often have to put my head down there). If you find me taking forever to respond, I'm typically pretty quick on Slack.

> I suppose if schedules will eventually support parameters (per #3384), I could see the argument for NOT converting to floats within the `InstructionScheduleMap`. I put up a PR (#4940), so feel free to discuss further there if appropriate.

I think I agree with this. I view the `ParameterizedSchedule` as more of a constrained schedule generator function for calibration generation functions coming from the backend, eg., it expects a certain format and is constrained to a limited amount of parameter math so as to prevent arbitrary code execution dictated by a remote source. It was always meant to be a temporary solution being deprecated in favour of a proper representation for functions with a signature ("parameterized schedules") in the pulse IR.


@taalexander No problem at all. For myself, I don't get as much free time to work on Qiskit as I'd like due to my family and regular job, so I'm not in a big rush.

I've thought about this some more in the meantime. Given that `ParameterizedSchedule` is meant to be temporary, it might be good to cast to float before passing params to a schedule (again, to not break existing generator functions). In the future, if params need to be left unbound when passed to a newer schedule type (i.e. when #3384 is implemented), we can do a type check and choose to only cast to float for `function` schedules, leaving as `ParameterExpression`s otherwise.

If that makes sense, and if you agree with it, then my PR #4940 can remain as is.
Yes, this sounds good to me.

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,12 @@
 
 """
 import inspect
-
 from collections import defaultdict
-from typing import List, Tuple, Iterable, Union, Callable
+from typing import Callable, Iterable, List, Tuple, Union
 
-from .schedule import Schedule, ParameterizedSchedule
-from .exceptions import PulseError
+from qiskit.circuit.parameterexpression import ParameterExpression
+from qiskit.pulse.exceptions import PulseError
+from qiskit.pulse.schedule import ParameterizedSchedule, Schedule
 
 
 class InstructionScheduleMap():
@@ -140,8 +140,8 @@ def assert_has(self, instruction: str, qubits: Union[int, Iterable[int]]) -> Non
     def get(self,
             instruction: str,
             qubits: Union[int, Iterable[int]],
-            *params: Union[int, float, complex],
-            **kwparams: Union[int, float, complex]) -> Schedule:
+            *params: Union[int, float, complex, ParameterExpression],
+            **kwparams: Union[int, float, complex, ParameterExpression]) -> Schedule:
         """Return the defined :py:class:`~qiskit.pulse.Schedule` for the given instruction on
         the given qubits.
 
@@ -204,8 +204,8 @@ def remove(self, instruction: str, qubits: Union[int, Iterable[int]]) -> None:
     def pop(self,
             instruction: str,
             qubits: Union[int, Iterable[int]],
-            *params: Union[int, float, complex],
-            **kwparams: Union[int, float, complex]) -> Schedule:
+            *params: Union[int, float, complex, ParameterExpression],
+            **kwparams: Union[int, float, complex, ParameterExpression]) -> Schedule:
         """Remove and return the defined ``Schedule`` for the given instruction on the given
         qubits.
 
diff --git a/qiskit/pulse/schedule.py b/qiskit/pulse/schedule.py
--- a/qiskit/pulse/schedule.py
+++ b/qiskit/pulse/schedule.py
@@ -20,12 +20,13 @@
 import itertools
 import multiprocessing as mp
 import sys
-from typing import List, Tuple, Iterable, Union, Dict, Callable, Set, Optional
+from typing import Callable, Dict, Iterable, List, Optional, Set, Tuple, Union
 
-from qiskit.util import is_main_process
+from qiskit.circuit.parameterexpression import ParameterExpression
 from qiskit.pulse.channels import Channel
-from qiskit.pulse.interfaces import ScheduleComponent
 from qiskit.pulse.exceptions import PulseError
+from qiskit.pulse.interfaces import ScheduleComponent
+from qiskit.util import is_main_process
 
 # pylint: disable=missing-return-doc
 
@@ -780,8 +781,9 @@ def parameters(self) -> Tuple[str]:
         """Schedule parameters."""
         return self._parameters
 
-    def bind_parameters(self, *args: List[Union[float, complex]],
-                        **kwargs: Dict[str, Union[float, complex]]) -> Schedule:
+    def bind_parameters(self,
+                        *args: Union[int, float, complex, ParameterExpression],
+                        **kwargs: Union[int, float, complex, ParameterExpression]) -> Schedule:
         """Generate the Schedule from params to evaluate command expressions"""
         bound_schedule = Schedule(name=self.name)
         schedules = list(self._schedules)
@@ -821,8 +823,8 @@ def bind_parameters(self, *args: List[Union[float, complex]],
 
         return bound_schedule
 
-    def __call__(self, *args: List[Union[float, complex]],
-                 **kwargs: Dict[str, Union[float, complex]]) -> Schedule:
+    def __call__(self, *args: Union[int, float, complex, ParameterExpression],
+                 **kwargs: Union[int, float, complex, ParameterExpression]) -> Schedule:
         return self.bind_parameters(*args, **kwargs)
 
 


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
@@ -16,13 +16,16 @@
 import numpy as np
 
 import qiskit.pulse.library as library
-from qiskit.test import QiskitTestCase
-from qiskit.test.mock import FakeOpenPulse2Q
-from qiskit.qobj.converters import QobjToInstructionConverter
-from qiskit.qobj import PulseQobjInstruction
-from qiskit.pulse import InstructionScheduleMap, Play, Waveform, Schedule, PulseError
+from qiskit.circuit.parameter import Parameter
+from qiskit.circuit.parameterexpression import ParameterExpression
+from qiskit.pulse import (InstructionScheduleMap, Play, PulseError, Schedule,
+                          Waveform)
 from qiskit.pulse.channels import DriveChannel
 from qiskit.pulse.schedule import ParameterizedSchedule
+from qiskit.qobj import PulseQobjInstruction
+from qiskit.qobj.converters import QobjToInstructionConverter
+from qiskit.test import QiskitTestCase
+from qiskit.test.mock import FakeOpenPulse2Q
 
 
 class TestInstructionScheduleMap(QiskitTestCase):
@@ -151,6 +154,7 @@ def test_pop(self):
 
     def test_sequenced_parameterized_schedule(self):
         """Test parametrized schedule consists of multiple instruction. """
+
         converter = QobjToInstructionConverter([], buffer=0)
         qobjs = [PulseQobjInstruction(name='fc', ch='d0', t0=10, phase='P1'),
                  PulseQobjInstruction(name='fc', ch='d0', t0=20, phase='P2'),
@@ -171,17 +175,20 @@ def test_sequenced_parameterized_schedule(self):
         with self.assertRaises(PulseError):
             inst_map.get('inst_seq', 0, 1, 2, 3, P1=1)
 
-        sched = inst_map.get('inst_seq', 0, 1, 2, 3)
+        p3_expr = Parameter('p3')
+        p3_expr = p3_expr.bind({p3_expr: 3})
+
+        sched = inst_map.get('inst_seq', 0, 1, 2, p3_expr)
         self.assertEqual(sched.instructions[0][-1].phase, 1)
         self.assertEqual(sched.instructions[1][-1].phase, 2)
         self.assertEqual(sched.instructions[2][-1].phase, 3)
 
-        sched = inst_map.get('inst_seq', 0, P1=1, P2=2, P3=3)
+        sched = inst_map.get('inst_seq', 0, P1=1, P2=2, P3=p3_expr)
         self.assertEqual(sched.instructions[0][-1].phase, 1)
         self.assertEqual(sched.instructions[1][-1].phase, 2)
         self.assertEqual(sched.instructions[2][-1].phase, 3)
 
-        sched = inst_map.get('inst_seq', 0, 1, 2, P3=3)
+        sched = inst_map.get('inst_seq', 0, 1, 2, P3=p3_expr)
         self.assertEqual(sched.instructions[0][-1].phase, 1)
         self.assertEqual(sched.instructions[1][-1].phase, 2)
         self.assertEqual(sched.instructions[2][-1].phase, 3)
@@ -189,19 +196,40 @@ def test_sequenced_parameterized_schedule(self):
     def test_schedule_generator(self):
         """Test schedule generator functionalty."""
 
-        x_test = 10
-        amp_test = 1.0
+        dur_val = 10
+        amp = 1.0
+
+        def test_func(dur: int):
+            sched = Schedule()
+            sched += Play(library.constant(int(dur), amp), DriveChannel(0))
+            return sched
+
+        expected_sched = Schedule()
+        expected_sched += Play(library.constant(dur_val, amp), DriveChannel(0))
+
+        inst_map = InstructionScheduleMap()
+        inst_map.add('f', (0,), test_func)
+        self.assertEqual(inst_map.get('f', (0,), dur_val), expected_sched)
+
+        self.assertEqual(inst_map.get_parameters('f', (0,)), ('dur',))
+
+    def test_schedule_generator_supports_parameter_expressions(self):
+        """Test expression-based schedule generator functionalty."""
+
+        t_param = Parameter('t')
+        amp = 1.0
 
-        def test_func(x):
+        def test_func(dur: ParameterExpression, t_val: int):
+            dur_bound = dur.bind({t_param: t_val})
             sched = Schedule()
-            sched += Play(library.constant(int(x), amp_test), DriveChannel(0))
+            sched += Play(library.constant(int(float(dur_bound)), amp), DriveChannel(0))
             return sched
 
-        ref_sched = Schedule()
-        ref_sched += Play(library.constant(x_test, amp_test), DriveChannel(0))
+        expected_sched = Schedule()
+        expected_sched += Play(library.constant(10, amp), DriveChannel(0))
 
         inst_map = InstructionScheduleMap()
         inst_map.add('f', (0,), test_func)
-        self.assertEqual(inst_map.get('f', (0,), x_test), ref_sched)
+        self.assertEqual(inst_map.get('f', (0,), dur=2*t_param, t_val=5), expected_sched)
 
-        self.assertEqual(inst_map.get_parameters('f', (0,)), ('x',))
+        self.assertEqual(inst_map.get_parameters('f', (0,)), ('dur', 't_val',))