Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Remove pulse scoped_parameters and search_parameters #11692

Merged
merged 2 commits into from
Feb 1, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
206 changes: 25 additions & 181 deletions qiskit/pulse/schedule.py
Original file line number Diff line number Diff line change
Expand Up @@ -36,7 +36,6 @@
import functools
import itertools
import multiprocessing as mp
import re
import sys
import warnings
from collections.abc import Callable, Iterable
Expand Down Expand Up @@ -879,121 +878,74 @@ class ScheduleBlock:
Appended reference directives are resolved when the main program is executed.
Subroutines must be assigned through :meth:`assign_references` before execution.

.. rubric:: Program Scoping

When you call a subroutine from another subroutine, or append a schedule block
to another schedule block, the management of references and parameters
can be a hard task. Schedule block offers a convenient feature to help with this
by automatically scoping the parameters and subroutines.
One way to reference a subroutine in a schedule is to use the pulse
builder's :func:`~qiskit.pulse.builder.reference` function to declare an
unassigned reference. In this example, the program is called with the
reference key "grand_child". You can call a subroutine without specifying
a substantial program.

.. code-block::

from qiskit import pulse
from qiskit.circuit.parameter import Parameter

amp1 = Parameter("amp")
amp1 = Parameter("amp1")
amp2 = Parameter("amp2")

with pulse.build() as sched1:
with pulse.build() as sched_inner:
pulse.play(pulse.Constant(100, amp1), pulse.DriveChannel(0))

print(sched1.scoped_parameters())

.. parsed-literal::

(Parameter(root::amp),)

The :meth:`~ScheduleBlock.scoped_parameters` method returns all :class:`~.Parameter`
objects defined in the schedule block. The parameter name is updated to reflect
its scope information, i.e. where it is defined.
The outer scope is called "root". Since the "amp" parameter is directly used
in the current builder context, it is prefixed with "root".
Note that the :class:`Parameter` object returned by :meth:`~ScheduleBlock.scoped_parameters`
preserves the hidden `UUID`_ key, and thus the scoped name doesn't break references
to the original :class:`Parameter`.

You may want to call this program from another program.
In this example, the program is called with the reference key "grand_child".
You can call a subroutine without specifying a substantial program
(like ``sched1`` above which we will assign later).

.. code-block::

amp2 = Parameter("amp")

with pulse.build() as sched2:
with pulse.build() as sched_outer:
with pulse.align_right():
pulse.reference("grand_child")
pulse.play(pulse.Constant(200, amp2), pulse.DriveChannel(0))

print(sched2.scoped_parameters())

.. parsed-literal::

(Parameter(root::amp),)

This only returns "root::amp" because the "grand_child" reference is unknown.
Now you assign the actual pulse program to this reference.
Now you assign the inner pulse program to this reference.

.. code-block::

sched2.assign_references({("grand_child", ): sched1})
print(sched2.scoped_parameters())
sched_outer.assign_references({("grand_child", ): sched_inner})
print(sched_outer.parameters)

.. parsed-literal::

(Parameter(root::amp), Parameter(root::grand_child::amp))
{Parameter(amp1), Parameter(amp2)}

Now you get two parameters "root::amp" and "root::grand_child::amp".
The second parameter name indicates it is defined within the referred program "grand_child".
The outer program now has the parameter ``amp2`` from the inner program,
indicating that the inner program's data has been made available to the
outer program.
The program calling the "grand_child" has a reference program description
which is accessed through :attr:`ScheduleBlock.references`.

.. code-block::

print(sched2.references)
print(sched_outer.references)

.. parsed-literal::

ReferenceManager:
- ('grand_child',): ScheduleBlock(Play(Constant(duration=100, amp=amp,...
- ('grand_child',): ScheduleBlock(Play(Constant(duration=100, amp=amp1,...

Finally, you may want to call this program from another program.
Here we try a different approach to define subroutine. Namely, we call
a subroutine from the root program with the actual program ``sched2``.

.. code-block::

amp3 = Parameter("amp")
amp3 = Parameter("amp3")

with pulse.build() as main:
pulse.play(pulse.Constant(300, amp3), pulse.DriveChannel(0))
pulse.call(sched2, name="child")
pulse.call(sched_outer, name="child")

print(main.scoped_parameters())
print(main.parameters)

.. parsed-literal::

(Parameter(root::amp), Parameter(root::child::amp), Parameter(root::child::grand_child::amp))
{Parameter(amp1), Parameter(amp2), Parameter(amp3}

This implicitly creates a reference named "child" within
the root program and assigns ``sched2`` to it.
You get three parameters "root::amp", "root::child::amp", and "root::child::grand_child::amp".
As you can see, each parameter name reflects the layer of calls from the root program.
If you know the scope of a parameter, you can directly get the parameter object
using :meth:`ScheduleBlock.search_parameters` as follows.

.. code-block::

main.search_parameters("root::child::grand_child::amp")

You can use a regular expression to specify the scope.
The following returns the parameters defined within the scope of "ground_child"
regardless of its parent scope. This is sometimes convenient if you
want to extract parameters from a deeply nested program.

.. code-block::

main.search_parameters("\\S::grand_child::amp")
the root program and assigns ``sched_outer`` to it.

Note that the root program is only aware of its direct references.

Expand All @@ -1015,10 +967,8 @@ class ScheduleBlock:

main.references[("child", )].references[("grand_child", )]

Note that :attr:`ScheduleBlock.parameters` and :meth:`ScheduleBlock.scoped_parameters()`
still collect all parameters also from the subroutine once it's assigned.

.. _UUID: https://docs.python.org/3/library/uuid.html#module-uuid
Note that :attr:`ScheduleBlock.parameters` still collects all parameters
also from the subroutine once it's assigned.
"""

__slots__ = (
Expand Down Expand Up @@ -1226,25 +1176,6 @@ def parameters(self) -> set[Parameter]:

return out_params

def scoped_parameters(self) -> tuple[Parameter, ...]:
"""Return unassigned parameters with scoped names.

.. note::

If a parameter is defined within a nested scope,
it is prefixed with all parent-scope names with the delimiter string,
which is "::". If a reference key of the scope consists of
multiple key strings, it will be represented by a single string joined with ",".
For example, "root::xgate,q0::amp" for the parameter "amp" defined in the
reference specified by the key strings ("xgate", "q0").
"""
return tuple(
sorted(
_collect_scoped_parameters(self, current_scope="root").values(),
key=lambda p: p.name,
)
)

@property
def references(self) -> ReferenceManager:
"""Return a reference manager of the current scope."""
Expand Down Expand Up @@ -1624,43 +1555,6 @@ def get_parameters(self, parameter_name: str) -> list[Parameter]:
matched = [p for p in self.parameters if p.name == parameter_name]
return matched

def search_parameters(self, parameter_regex: str) -> list[Parameter]:
"""Search parameter with regular expression.

This method looks for the scope-aware parameters.
For example,

.. code-block:: python

from qiskit import pulse, circuit

amp1 = circuit.Parameter("amp")
amp2 = circuit.Parameter("amp")

with pulse.build() as sub_prog:
pulse.play(pulse.Constant(100, amp1), pulse.DriveChannel(0))

with pulse.build() as main_prog:
pulse.call(sub_prog, name="sub")
pulse.play(pulse.Constant(100, amp2), pulse.DriveChannel(0))

main_prog.search_parameters("root::sub::amp")

This finds ``amp1`` with scoped name "root::sub::amp".

Args:
parameter_regex: Regular expression for scoped parameter name.

Returns:
Parameter objects that have corresponding name.
"""
pattern = re.compile(parameter_regex)

return sorted(
_collect_scoped_parameters(self, current_scope="root", filter_regex=pattern).values(),
key=lambda p: p.name,
)

def __len__(self) -> int:
"""Return number of instructions in the schedule."""
return len(self.blocks)
Expand Down Expand Up @@ -1938,56 +1832,6 @@ def _get_references(block_elms: list["BlockComponent"]) -> set[Reference]:
return references


def _collect_scoped_parameters(
schedule: ScheduleBlock,
current_scope: str,
filter_regex: re.Pattern | None = None,
) -> dict[tuple[str, int], Parameter]:
"""A helper function to collect parameters from all references in scope-aware fashion.

Parameter object is renamed with attached scope information but its UUID is remained.
This means object is treated identically on the assignment logic.
This function returns a dictionary of all parameters existing in the target program
including its reference, which is keyed on the unique identifier consisting of
scoped parameter name and parameter object UUID.

This logic prevents parameter clash in the different scope.
For example, when two parameter objects with the same UUID exist in different references,
both of them appear in the output dictionary, even though they are technically the same object.
This feature is particularly convenient to search parameter object with associated scope.

Args:
schedule: Schedule to get parameters.
current_scope: Name of scope where schedule exist.
filter_regex: Optional. Compiled regex to sort parameter by name.

Returns:
A dictionary of scoped parameter objects.
"""
parameters_out = {}
for param in schedule._parameter_manager.parameters:
new_name = f"{current_scope}{Reference.scope_delimiter}{param.name}"

if filter_regex and not re.search(filter_regex, new_name):
continue
scoped_param = Parameter(new_name, uuid=getattr(param, "_uuid"))

unique_key = new_name, hash(param)
parameters_out[unique_key] = scoped_param

for sub_namespace, subroutine in schedule.references.items():
if subroutine is None:
continue
composite_key = Reference.key_delimiter.join(sub_namespace)
full_path = f"{current_scope}{Reference.scope_delimiter}{composite_key}"
sub_parameters = _collect_scoped_parameters(
subroutine, current_scope=full_path, filter_regex=filter_regex
)
parameters_out.update(sub_parameters)

return parameters_out


# These type aliases are defined at the bottom of the file, because as of 2022-01-18 they are
# imported into other parts of Terra. Previously, the aliases were at the top of the file and used
# forwards references within themselves. This was fine within the same file, but causes scoping
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
---
upgrade:
- |
The ``scoped_parameters`` and ``search_parameters`` methods have been
removed from the :class:`.ScheduleBlock` class. These methods returned
:class:`.Parameter` objects that partially linked to the parameters in the
:class:`.ScheduleBlock` instance but assigning values using these objects did not
work correctly. Users should use :attr:`.ScheduleBlock.parameters` instead and
iterate through :attr:`.ScheduleBlock.references` and compare to the
:attr:`.Schedule.parameters` attributes of the subreferences when needing to
distinguish which subroutine a parameter is used in. See `#11654
https://github.com/Qiskit/qiskit/issues/11654>`__ for more information.
Loading
Loading