Repository: Qiskit/qiskit

Issue:
ParameterVector support for Schedules
<!-- ⚠️ 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?

Currently, `QuantumCircuit`s support `assign_parameters` with `ParameterVector` input. The task is to add support similarly for `Schedule`s, which now support parameter assignment as well.



Discussion:
can I try this one? 
Yes, please! Let me know if you need help. I would start by looking in `circuit/quantumcircuit.py` and expect to make changes in `pulse/schedule.py`. (Plus a reno note and tests)
Hello, @lcapelluto I am a first-time Open Source contributor, do you mind if I take a look at the issue?
Hi @alejomonbar ! any progress?
Hi @1ucian0. Yes, I just got some feedback from @lcapelluto but I have this question https://github.com/Qiskit/qiskit-terra/pull/5240#issuecomment-793194776 for concluding with this issue.
This issue can be **closed** as this functionality is available in the current version of qiskit.

```python

from qiskit import pulse
from qiskit.pulse import Schedule
from qiskit.circuit import ParameterVector


schedule = pulse.Schedule()
parameters = ParameterVector("θ", 2)
gate = pulse.Gaussian(640, parameters[0], parameters[1])
schedule += pulse.Play(gate, pulse.DriveChannel(0))
schedule.assign_parameters({parameters:[0.75, 160]})

# Schedule((0, Play(Gaussian(duration=640, sigma=θ[1], amp=θ[0], angle=0), DriveChannel(0))), name="sched2")
```

Resolving patch:
diff --git a/qiskit/pulse/parameter_manager.py b/qiskit/pulse/parameter_manager.py
--- a/qiskit/pulse/parameter_manager.py
+++ b/qiskit/pulse/parameter_manager.py
@@ -52,8 +52,9 @@
 """
 from __future__ import annotations
 from copy import copy
-from typing import Any
+from typing import Any, Mapping, Sequence
 
+from qiskit.circuit import ParameterVector
 from qiskit.circuit.parameter import Parameter
 from qiskit.circuit.parameterexpression import ParameterExpression, ParameterValueType
 from qiskit.pulse import instructions, channels
@@ -360,7 +361,9 @@ def get_parameters(self, parameter_name: str) -> list[Parameter]:
     def assign_parameters(
         self,
         pulse_program: Any,
-        value_dict: dict[ParameterExpression, ParameterValueType],
+        value_dict: dict[
+            ParameterExpression | ParameterVector, ParameterValueType | Sequence[ParameterValueType]
+        ],
     ) -> Any:
         """Modify and return program data with parameters assigned according to the input.
 
@@ -372,7 +375,10 @@ def assign_parameters(
         Returns:
             Updated program data.
         """
-        valid_map = {k: value_dict[k] for k in value_dict.keys() & self._parameters}
+        unrolled_value_dict = self._unroll_param_dict(value_dict)
+        valid_map = {
+            k: unrolled_value_dict[k] for k in unrolled_value_dict.keys() & self._parameters
+        }
         if valid_map:
             visitor = ParameterSetter(param_map=valid_map)
             return visitor.visit(pulse_program)
@@ -387,3 +393,38 @@ def update_parameter_table(self, new_node: Any):
         visitor = ParameterGetter()
         visitor.visit(new_node)
         self._parameters |= visitor.parameters
+
+    def _unroll_param_dict(
+        self,
+        parameter_binds: Mapping[
+            Parameter | ParameterVector, ParameterValueType | Sequence[ParameterValueType]
+        ],
+    ) -> Mapping[Parameter, ParameterValueType]:
+        """
+        Unroll parameter dictionary to a map from parameter to value.
+
+        Args:
+            parameter_binds: A dictionary from parameter to value or a list of values.
+
+        Returns:
+            A dictionary from parameter to value.
+        """
+        out = {}
+        for parameter, value in parameter_binds.items():
+            if isinstance(parameter, ParameterVector):
+                if not isinstance(value, Sequence):
+                    raise PulseError(
+                        f"Parameter vector '{parameter.name}' has length {len(parameter)},"
+                        f" but was assigned to a single value."
+                    )
+                if len(parameter) != len(value):
+                    raise PulseError(
+                        f"Parameter vector '{parameter.name}' has length {len(parameter)},"
+                        f" but was assigned to {len(value)} values."
+                    )
+                out.update(zip(parameter, value))
+            elif isinstance(parameter, str):
+                out[self.get_parameters(parameter)] = value
+            else:
+                out[parameter] = value
+        return out
diff --git a/qiskit/pulse/schedule.py b/qiskit/pulse/schedule.py
--- a/qiskit/pulse/schedule.py
+++ b/qiskit/pulse/schedule.py
@@ -39,11 +39,12 @@
 import sys
 import warnings
 from collections.abc import Callable, Iterable
-from typing import List, Tuple, Union, Dict, Any
+from typing import List, Tuple, Union, Dict, Any, Sequence
 
 import numpy as np
 import rustworkx as rx
 
+from qiskit.circuit import ParameterVector
 from qiskit.circuit.parameter import Parameter
 from qiskit.circuit.parameterexpression import ParameterExpression, ParameterValueType
 from qiskit.pulse.channels import Channel
@@ -711,13 +712,18 @@ def is_parameterized(self) -> bool:
         return self._parameter_manager.is_parameterized()
 
     def assign_parameters(
-        self, value_dict: dict[ParameterExpression, ParameterValueType], inplace: bool = True
+        self,
+        value_dict: dict[
+            ParameterExpression | ParameterVector, ParameterValueType | Sequence[ParameterValueType]
+        ],
+        inplace: bool = True,
     ) -> "Schedule":
         """Assign the parameters in this schedule according to the input.
 
         Args:
-            value_dict: A mapping from Parameters to either numeric values or another
-                Parameter expression.
+            value_dict: A mapping from parameters (parameter vectors) to either
+            numeric values (list of numeric values)
+            or another Parameter expression (list of Parameter expressions).
             inplace: Set ``True`` to override this instance with new parameter.
 
         Returns:
@@ -1408,14 +1414,17 @@ def is_referenced(self) -> bool:
 
     def assign_parameters(
         self,
-        value_dict: dict[ParameterExpression, ParameterValueType],
+        value_dict: dict[
+            ParameterExpression | ParameterVector, ParameterValueType | Sequence[ParameterValueType]
+        ],
         inplace: bool = True,
     ) -> "ScheduleBlock":
         """Assign the parameters in this schedule according to the input.
 
         Args:
-            value_dict: A mapping from Parameters to either numeric values or another
-                Parameter expression.
+            value_dict: A mapping from parameters (parameter vectors) to either numeric values
+            (list of numeric values)
+            or another parameter expression (list of parameter expressions).
             inplace: Set ``True`` to override this instance with new parameter.
 
         Returns:


Test patch:
diff --git a/test/python/pulse/test_parameter_manager.py b/test/python/pulse/test_parameter_manager.py
--- a/test/python/pulse/test_parameter_manager.py
+++ b/test/python/pulse/test_parameter_manager.py
@@ -21,7 +21,7 @@
 import numpy as np
 
 from qiskit import pulse
-from qiskit.circuit import Parameter
+from qiskit.circuit import Parameter, ParameterVector
 from qiskit.pulse.exceptions import PulseError, UnassignedDurationError
 from qiskit.pulse.parameter_manager import ParameterGetter, ParameterSetter
 from qiskit.pulse.transforms import AlignEquispaced, AlignLeft, inline_subroutines
@@ -483,6 +483,38 @@ def test_parametric_pulses(self):
         self.assertEqual(block.blocks[0].pulse.amp, 0.2)
         self.assertEqual(block.blocks[0].pulse.sigma, 4.0)
 
+    def test_parametric_pulses_with_parameter_vector(self):
+        """Test Parametric Pulses with parameters determined by a ParameterVector
+        in the Play instruction."""
+        param_vec = ParameterVector("param_vec", 3)
+        param = Parameter("param")
+
+        waveform = pulse.library.Gaussian(duration=128, sigma=param_vec[0], amp=param_vec[1])
+
+        block = pulse.ScheduleBlock()
+        block += pulse.Play(waveform, pulse.DriveChannel(10))
+        block += pulse.ShiftPhase(param_vec[2], pulse.DriveChannel(10))
+        block1 = block.assign_parameters({param_vec: [4, 0.2, 0.1]}, inplace=False)
+        block2 = block.assign_parameters({param_vec: [4, param, 0.1]}, inplace=False)
+        self.assertEqual(block1.blocks[0].pulse.amp, 0.2)
+        self.assertEqual(block1.blocks[0].pulse.sigma, 4.0)
+        self.assertEqual(block1.blocks[1].phase, 0.1)
+        self.assertEqual(block2.blocks[0].pulse.amp, param)
+        self.assertEqual(block2.blocks[0].pulse.sigma, 4.0)
+        self.assertEqual(block2.blocks[1].phase, 0.1)
+
+        sched = pulse.Schedule()
+        sched += pulse.Play(waveform, pulse.DriveChannel(10))
+        sched += pulse.ShiftPhase(param_vec[2], pulse.DriveChannel(10))
+        sched1 = sched.assign_parameters({param_vec: [4, 0.2, 0.1]}, inplace=False)
+        sched2 = sched.assign_parameters({param_vec: [4, param, 0.1]}, inplace=False)
+        self.assertEqual(sched1.instructions[0][1].pulse.amp, 0.2)
+        self.assertEqual(sched1.instructions[0][1].pulse.sigma, 4.0)
+        self.assertEqual(sched1.instructions[1][1].phase, 0.1)
+        self.assertEqual(sched2.instructions[0][1].pulse.amp, param)
+        self.assertEqual(sched2.instructions[0][1].pulse.sigma, 4.0)
+        self.assertEqual(sched2.instructions[1][1].phase, 0.1)
+
 
 class TestScheduleTimeslots(QiskitTestCase):
     """Test for edge cases of timing overlap on parametrized channels.