Repository: Qiskit/qiskit (python)

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.


Pull request description:
ParameterVector support for pulse parameter assignment
<!--
⚠️ If you do not respect this template, your pull request will be closed.
⚠️ Your pull request title should be short detailed and understandable for all.
⚠️ Also, please add a release note file using reno if the change needs to be
  documented in the release notes.
⚠️ If your pull request fixes an open issue, please link to the issue.

- [ ] I have added the tests to cover my changes.
- [ ] I have updated the documentation accordingly.
- [ ] I have read the CONTRIBUTING document.
-->

### Summary
This PR proposes an extended use of the current method `assign_parameters` in the `ParameterManager`to enable the assignment of a list of values through `ParameterVector`class. This enables an extended usage for the same methods in the `Schedule`and `ScheduleBlock`objects.


### Details and comments

The PR simply takes the already existing unrolling method from `QuantumCircuit`and applies it for the pulse level case, to enable a simple extension of the existing methods for parameter assignment.
This can be practical when mixing pulse level calibrations with many parameters.
This PR closes #5233 


Patch:
diff --git a/qiskit/pulse/parameter_manager.py b/qiskit/pulse/parameter_manager.py
index 5c8163cb1..561eac01f 100644
--- a/qiskit/pulse/parameter_manager.py
+++ b/qiskit/pulse/parameter_manager.py
@@ -52,8 +52,9 @@ and thus this parameter framework gives greater scalability to the pulse module.
 """
 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 @@ class ParameterManager:
     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 @@ class ParameterManager:
         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 @@ class ParameterManager:
         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
index 96ef90706..2a32b06a4 100644
--- a/qiskit/pulse/schedule.py
+++ b/qiskit/pulse/schedule.py
@@ -39,11 +39,12 @@ import multiprocessing as mp
 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 @@ class Schedule:
         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 @@ class ScheduleBlock:
 
     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:
diff --git a/releasenotes/notes/pulse_parameter_manager_compat_with_ParameterVector-7d31395fd4019827.yaml b/releasenotes/notes/pulse_parameter_manager_compat_with_ParameterVector-7d31395fd4019827.yaml
new file mode 100644
index 000000000..0cb13cb34
--- /dev/null
+++ b/releasenotes/notes/pulse_parameter_manager_compat_with_ParameterVector-7d31395fd4019827.yaml
@@ -0,0 +1,7 @@
+---
+features_pulse:
+  - |
+    The ``assign_parameters`` methods of :class:`.Schedule` and :class:`.ScheduleBlock`
+    now support assigning a :class:`.ParameterVector` to a list of parameter values
+    simultaneously in addition to assigning individual :class:`.Parameter` instances to
+    individual values.


Test patch:
diff --git a/test/python/pulse/test_parameter_manager.py b/test/python/pulse/test_parameter_manager.py
index 16cbc7bc8..54268af14 100644
--- a/test/python/pulse/test_parameter_manager.py
+++ b/test/python/pulse/test_parameter_manager.py
@@ -21,7 +21,7 @@ import ddt
 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 @@ class TestAssignFromProgram(QiskitTestCase):
         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.