diff --git a/qiskit/pulse/parameter_manager.py b/qiskit/pulse/parameter_manager.py index 5c8163cb10fe..561eac01f55d 100644 --- 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 index 96ef90706ce0..2a32b06a4780 100644 --- 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: 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 000000000000..0cb13cb346c1 --- /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. diff --git a/test/python/pulse/test_parameter_manager.py b/test/python/pulse/test_parameter_manager.py index 16cbc7bc8117..54268af14577 100644 --- 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.