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

Pulse reference mechanism #8005

Merged
merged 48 commits into from
Sep 14, 2022
Merged
Show file tree
Hide file tree
Changes from 47 commits
Commits
Show all changes
48 commits
Select commit Hold shift + click to select a range
1ca9667
This commit add reference to schedule block and cleanup data structur…
nkanazawa1989 Apr 27, 2022
5618d83
Introduce ref_id instead of using (name, channels) tuple as a key of …
nkanazawa1989 Apr 27, 2022
cb8b832
Add scope to reference manager
nkanazawa1989 Apr 30, 2022
3ccb1d1
fix parameter scope bug
nkanazawa1989 May 1, 2022
2960d62
add more test
nkanazawa1989 May 1, 2022
be48b5b
documentation update
nkanazawa1989 May 1, 2022
cf3c0d4
support regex in get_parameters
nkanazawa1989 May 1, 2022
a66f3cd
update reference manager implementation
nkanazawa1989 Jun 12, 2022
3c99fa3
wording edits
nkanazawa1989 Jun 20, 2022
372b684
add details of returned order
nkanazawa1989 Jun 20, 2022
8cc3716
fix bug in reference management
nkanazawa1989 Jun 21, 2022
9d3c020
update reference model
nkanazawa1989 Jun 24, 2022
b4a90ad
add test for special parameter name
nkanazawa1989 Jun 24, 2022
7a08296
docs update
nkanazawa1989 Jul 6, 2022
220a6f1
update delimiter
nkanazawa1989 Jul 6, 2022
944076c
misc updates
nkanazawa1989 Jul 6, 2022
6d87410
update reference argument (name, **extra_keys)
nkanazawa1989 Jul 6, 2022
021577e
update builder command refer -> reference
nkanazawa1989 Jul 7, 2022
44d4959
update parallel alignment dag generator to consider unassigned refere…
nkanazawa1989 Jul 7, 2022
f92fa65
add search_parameters method
nkanazawa1989 Jul 7, 2022
6871a4f
Merge branch 'main' of github.com:Qiskit/qiskit-terra into feature/ca…
nkanazawa1989 Jul 7, 2022
12d0870
cleanup
nkanazawa1989 Jul 7, 2022
b14ee6c
reno
nkanazawa1989 Jul 7, 2022
118004c
documentation update
nkanazawa1989 Jul 12, 2022
f24745a
update parameter collection method not to merge parameter object in d…
nkanazawa1989 Jul 12, 2022
670b333
avoid overriding existing reference
nkanazawa1989 Jul 12, 2022
d4ae163
update comments about reference policy
nkanazawa1989 Jul 12, 2022
04a656e
cleanup
nkanazawa1989 Jul 12, 2022
ad60789
Merge branch 'main' into feature/call_with_name
nkanazawa1989 Sep 5, 2022
620e67d
add comment about parent
nkanazawa1989 Sep 5, 2022
b59bbac
Merge branch 'main' into feature/call_with_name
eggerdj Sep 5, 2022
fe65c1a
Merge branch 'main' of github.com:Qiskit/qiskit-terra into feature/ca…
nkanazawa1989 Sep 8, 2022
329c6bc
Update union of parameters, add some test for expression.
nkanazawa1989 Sep 9, 2022
ae506e8
Switch baseclass to UserDict
nkanazawa1989 Sep 12, 2022
f483456
Remove .name setter
nkanazawa1989 Sep 12, 2022
5b7eeef
Update parameter logic. Turn scoped_parameters into a method.
nkanazawa1989 Sep 12, 2022
7a19c58
update _get_references logic
nkanazawa1989 Sep 12, 2022
c973d61
Fix sequential dag generation logic
nkanazawa1989 Sep 12, 2022
f579889
add warning for parameter collision
nkanazawa1989 Sep 12, 2022
1531480
add test of sequential two edges
nkanazawa1989 Sep 12, 2022
fe6fd7f
fix replace logic
nkanazawa1989 Sep 12, 2022
cdefe16
add explicit validate logic and cleanup constructor of instruction su…
nkanazawa1989 Sep 12, 2022
90334dc
fix test
nkanazawa1989 Sep 12, 2022
ffd2130
add upgrade notice and API doc
nkanazawa1989 Sep 12, 2022
0fc4705
revert return type of parameters
nkanazawa1989 Sep 12, 2022
6ca8cfb
fix documentation
nkanazawa1989 Sep 12, 2022
a16a596
Merge branch 'main' into feature/call_with_name
nkanazawa1989 Sep 13, 2022
ba531c7
Merge branch 'main' into feature/call_with_name
mergify[bot] Sep 14, 2022
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
1 change: 1 addition & 0 deletions qiskit/pulse/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -74,6 +74,7 @@
call,
delay,
play,
reference,
set_frequency,
set_phase,
shift_frequency,
Expand Down
266 changes: 186 additions & 80 deletions qiskit/pulse/builder.py
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,9 @@
# that they have been altered from the originals.

r"""

.. _pulse_builder:

=============
Pulse Builder
=============
Expand Down Expand Up @@ -299,6 +302,7 @@
call
delay
play
reference
set_frequency
set_phase
shift_frequency
Expand Down Expand Up @@ -440,6 +444,8 @@
import contextvars
import functools
import itertools
import uuid
import warnings
from contextlib import contextmanager
from typing import (
Any,
Expand Down Expand Up @@ -736,6 +742,16 @@ def append_block(self, context_block: ScheduleBlock):
if len(context_block) > 0:
self._context_stack[-1].append(context_block)

def append_reference(self, name: str, *extra_keys: str):
"""Add external program as a :class:`~qiskit.pulse.instructions.Reference` instruction.

Args:
name: Name of subroutine.
extra_keys: Assistance keys to uniquely specify the subroutine.
"""
inst = instructions.Reference(name, *extra_keys)
self.append_instruction(inst)

def call_subroutine(
self,
subroutine: Union[circuit.QuantumCircuit, Schedule, ScheduleBlock],
Expand All @@ -747,7 +763,7 @@ def call_subroutine(

The ``subroutine`` is appended to the context schedule as a call instruction.
This logic just generates a convenient program representation in the compiler.
Thus this doesn't affect execution of inline subroutines.
Thus, this doesn't affect execution of inline subroutines.
See :class:`~pulse.instructions.Call` for more details.

Args:
Expand All @@ -769,39 +785,53 @@ def call_subroutine(
self._compile_lazy_circuit()
subroutine = self._compile_circuit(subroutine)

empty_subroutine = True
if isinstance(subroutine, Schedule):
if len(subroutine.instructions) > 0:
empty_subroutine = False
elif isinstance(subroutine, ScheduleBlock):
if len(subroutine.blocks) > 0:
empty_subroutine = False
else:
if not isinstance(subroutine, (Schedule, ScheduleBlock)):
raise exceptions.PulseError(
f"Subroutine type {subroutine.__class__.__name__} is "
"not valid data format. Call QuantumCircuit, "
"Schedule, or ScheduleBlock."
)

if not empty_subroutine:
param_value_map = {}
for param_name, assigned_value in kw_params.items():
nkanazawa1989 marked this conversation as resolved.
Show resolved Hide resolved
param_objs = subroutine.get_parameters(param_name)
if len(param_objs) > 0:
for param_obj in param_objs:
param_value_map[param_obj] = assigned_value
else:
raise exceptions.PulseError(
f"Parameter {param_name} is not defined in the target subroutine. "
f'{", ".join(map(str, subroutine.parameters))} can be specified.'
)

if value_dict:
param_value_map.update(value_dict)
if len(subroutine) == 0:
return

call_def = instructions.Call(subroutine, param_value_map, name)

self.append_instruction(call_def)
# Create local parameter assignment
local_assignment = dict()
for param_name, value in kw_params.items():
params = subroutine.get_parameters(param_name)
if not params:
raise exceptions.PulseError(
f"Parameter {param_name} is not defined in the target subroutine. "
f'{", ".join(map(str, subroutine.parameters))} can be specified.'
)
for param in params:
local_assignment[param] = value
if value_dict:
if local_assignment.keys() & value_dict.keys():
warnings.warn(
"Some parameters provided by 'value_dict' conflict with one through "
"keyword arguments. Parameter values in the keyword arguments "
"are overridden by the dictionary values.",
UserWarning,
)
local_assignment.update(value_dict)

if isinstance(subroutine, ScheduleBlock):
# If subroutine is schedule block, use reference mechanism.
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

What is the status of Call and ScheduleBlock after this change? Before, one could use Call on a ScheduleBlock. I think this PR does not block that case but changes most of the convenient ways of a calling a ScheduleBlock to substitute in a reference instead.

I think the justification for doing this is that the reference mechanism allows scoping of parameters and deduplication of multiple calls of the same subroutine. I think that is reasonable, though I still wonder about leaving the call interface alone and making the reference interface separate, so one needs to choose to use it.

Copy link
Contributor Author

@nkanazawa1989 nkanazawa1989 Jun 20, 2022

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'm thinking to remove the use case of calling block as a Call instruction. I don't think there are many users directly building a pulse program without builder syntax, so this will be kind of internal mechanism update.

The reason Call is introduced is to improve memory efficiency at program construction and serialization. A Call instance separately stores parameter-unassigned schedule and parameter table, and it is intended to reuse unassigned schedule among multiple instances. However, it is difficult to perform deduplication of schedules stored in separate instruction instances, on the other hand, reference mechanism easily realizes this because subroutine is now stored in the schedule block.
Reference mechanism doesn't separately keep parameter-unassigned schedule, but I don't have any use case in mind that requires assignment of different parameters to the same parameterized sequence within the same schedule (i.e. Rabi experiment scans pulse amp, but each scan is different schedule instance). Indeed, current Call just degrades memory efficiency by having two schedule instances (one unassigned and other assigned one in cache) within the instance.

If we keep calling the schedule block, then called schedule is not recognized as a reference. If a program has mixed representation of call and reference, it seems like we can easily run into some edge case.

if local_assignment:
subroutine = subroutine.assign_parameters(local_assignment, inplace=False)
if name is None:
# Add unique string, not to accidentally override existing reference entry.
keys = (subroutine.name, uuid.uuid4().hex)
else:
keys = (name,)
Comment on lines +823 to +827
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This seems somewhat unusual to me - it feels like you have name shadowing if the name is explicitly given, and not (and an unknowable lookup key for the user?) if it's not given. I admittedly don't fully understand the context, but it feels cleaner if shadowing either always happens, or is always an error?

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think always using shadowing is probably best. You need to use shadowing when the name is not given because a schedule could call multiple schedules with the same name (e.g. a two qubit schedule calling x on each qubit). While using the name as the key feels intuitive, it's not needed to preserve the way call() worked before.

One issue with the current behavior is that if you use the same name for multiple calls with different schedules (which does not seem like a good thing to do) the last one overwrites the earlier ones whereas before the individual schedules would be kept.

Besides backwards compatibility, the nice thing about call() is that adds a reference and assigns it in a single step. Maybe the new reference() method could optionally take a ScheduleBlock and assign it if given? Since it is a new method, it does not have to worry about backwards compatibility and can just always use the required name parameter as the key.

Copy link
Member

@jakelishman jakelishman Sep 8, 2022

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think we used opposite meanings of "shadowing" here - I meant "overwrites" (so the previous definitions of "x" are inaccessible now), and I think you meant "adds to"? It doesn't matter too much if we did - I don't have any position on what's best for the API here, because I've never used the code. As long as you and Naoki come to an agreement on what's best, I can sign off on the PR. If that's the extra uuid bit of the key in all cases, or always making it an error, or always silently overriding, any of those are absolutely fine by me.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yes, you are right. I got it backwards from you. I was thinking of the function obscuring the subroutine's name as shadowing the name.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This is good point. I'm not 100 percent confident on the current implementation. This at least guarantees the conventional behavior. The reference key is important only when a user assigns a subroutine to reference. Since, .call performs this operation in a single function, basically the reference key can be whatever. There is no reason to update explicitly provided name, but I added uuid here to avoid local parameter collision since we can call .call with parameter assignment. Remember that reference manager does NOT allow assigning multiple objects to a single reference. Calling the same schedule with different parameter assignments will generate different subroutines with the same name on the fly, and this operation usually fails without shadowing. This should be supported for backward compatibility. I wanted to generate unique name considering parameters, but I needed to give up because of mutability of .assign_parameters (I needed unnecessarily heavy logic to realize). Another option would be

Suggested change
if name is None:
# Add unique string, not to accidentally override existing reference entry.
keys = (subroutine.name, uuid.uuid4().hex)
else:
keys = (name,)
keys = name or uuid.uuid4().hex

probably this makes more sense since subroutine.name is no longer the effective identifier due to uuid.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The reference key is important only when a user assigns a subroutine to reference.

The key also factors into the new parameter scoping features. If you call a schedule without passing a name, you will get difficult to use scoped parameter names.

One thing I wanted to check -- do you want to support calling different schedules with the same name passed explicitly? It sounds like the reference manager will raise an exception for the second schedule now whereas before the PR it was allowed to use the same name since the name was just attached to the instruction and not inserted into a shared namespace.

keys = name or uuid.uuid4().hex

The problem with this is that the first key gets used for Instruction.name so it is probably best to keep that a readable string.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yes, this changes the behavior. I updated API doc and release note accordingly in ffd2130

The problem with this is that the first key gets used for Instruction.name so it is probably best to keep that a readable string.

These comments all make sense. I leave the current implementation.

self.append_reference(*keys)
self.get_context().assign_references({keys: subroutine}, inplace=True)
else:
# If subroutine is schedule, use Call instruction.
name = name or subroutine.name
call_instruction = instructions.Call(subroutine, local_assignment, name)
self.append_instruction(call_instruction)

@_requires_backend
def call_gate(self, gate: circuit.Gate, qubits: Tuple[int, ...], lazy: bool = True):
Expand Down Expand Up @@ -1679,18 +1709,17 @@ def acquire(

from qiskit import pulse

d0 = pulse.MeasureChannel(0)
acq0 = pulse.AcquireChannel(0)
mem0 = pulse.MemorySlot(0)

with pulse.build() as pulse_prog:
pulse.acquire(100, d0, mem0)
pulse.acquire(100, acq0, mem0)

# measurement metadata
kernel = pulse.configuration.Kernel('linear_discriminator')
pulse.acquire(100, d0, mem0, kernel=kernel)
pulse.acquire(100, acq0, mem0, kernel=kernel)

.. note:: The type of data acquire will depend on the execution
``meas_level``.
.. note:: The type of data acquire will depend on the execution ``meas_level``.

Args:
duration: Duration to acquire data for
Expand Down Expand Up @@ -1832,89 +1861,166 @@ def snapshot(label: str, snapshot_type: str = "statevector"):


def call(
target: Union[circuit.QuantumCircuit, Schedule, ScheduleBlock],
target: Optional[Union[circuit.QuantumCircuit, Schedule, ScheduleBlock]],
name: Optional[str] = None,
value_dict: Optional[Dict[ParameterValueType, ParameterValueType]] = None,
**kw_params: ParameterValueType,
):
"""Call the ``target`` within the currently active builder context with arbitrary
"""Call the subroutine within the currently active builder context with arbitrary
parameters which will be assigned to the target program.

.. note::
The ``target`` program is inserted as a ``Call`` instruction.
This instruction defines a subroutine. See :class:`~qiskit.pulse.instructions.Call`
for more details.

If the ``target`` program is a :class:`.ScheduleBlock`, then a :class:`.Reference`
instruction will be created and appended to the current context.
The ``target`` program will be immediately assigned to the current scope as a subroutine.
If the ``target`` program is :class:`.Schedule`, it will be wrapped by the
:class:`.Call` instruction and appended to the current context to avoid
a mixed representation of :class:`.ScheduleBlock` and :class:`.Schedule`.
If the ``target`` program is a :class:`.QuantumCircuit` it will be scheduled
and the new :class:`.Schedule` will be added as a :class:`.Call` instruction.

Examples:

.. code-block:: python
1. Calling a schedule block (recommended)

from qiskit import circuit, pulse, schedule, transpile
from qiskit.providers.fake_provider import FakeOpenPulse2Q
.. jupyter-execute::

backend = FakeOpenPulse2Q()
from qiskit import circuit, pulse
from qiskit.providers.fake_provider import FakeBogotaV2

qc = circuit.QuantumCircuit(2)
qc.cx(0, 1)
qc_transpiled = transpile(qc, optimization_level=3)
sched = schedule(qc_transpiled, backend)
backend = FakeBogotaV2()

with pulse.build(backend) as pulse_prog:
pulse.call(sched)
pulse.call(qc)
with pulse.build() as x_sched:
pulse.play(pulse.Gaussian(160, 0.1, 40), pulse.DriveChannel(0))

This function can optionally take parameter dictionary with the parameterized target program.
with pulse.build() as pulse_prog:
pulse.call(x_sched)

.. code-block:: python
print(pulse_prog)

from qiskit import circuit, pulse
The actual program is stored in the reference table attached to the schedule.

amp = circuit.Parameter('amp')
.. jupyter-execute::

with pulse.build() as subroutine:
pulse.play(pulse.Gaussian(160, amp, 40), pulse.DriveChannel(0))
print(pulse_prog.references)

with pulse.build() as main_prog:
pulse.call(subroutine, amp=0.1)
pulse.call(subroutine, amp=0.3)
In addition, you can call a parameterized target program with parameter assignment.

If there is any parameter name collision, you can distinguish them by specifying
each parameter object as a python dictionary. Otherwise ``amp1`` and ``amp2`` will be
updated with the same value.
.. jupyter-execute::

.. code-block:: python
amp = circuit.Parameter("amp")

from qiskit import circuit, pulse
with pulse.build() as subroutine:
pulse.play(pulse.Gaussian(160, amp, 40), pulse.DriveChannel(0))

amp1 = circuit.Parameter('amp')
amp2 = circuit.Parameter('amp')
with pulse.build() as pulse_prog:
pulse.call(subroutine, amp=0.1)
pulse.call(subroutine, amp=0.3)

with pulse.build() as subroutine:
pulse.play(pulse.Gaussian(160, amp1, 40), pulse.DriveChannel(0))
pulse.play(pulse.Gaussian(160, amp2, 40), pulse.DriveChannel(1))
print(pulse_prog)

with pulse.build() as main_prog:
pulse.call(subroutine, value_dict={amp1: 0.1, amp2: 0.2})
If there is a name collision between parameters, you can distinguish them by specifying
each parameter object in a python dictionary. For example,

.. jupyter-execute::

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

with pulse.build() as subroutine:
pulse.play(pulse.Gaussian(160, amp1, 40), pulse.DriveChannel(0))
pulse.play(pulse.Gaussian(160, amp2, 40), pulse.DriveChannel(1))

with pulse.build() as pulse_prog:
pulse.call(subroutine, value_dict={amp1: 0.1, amp2: 0.3})
Comment on lines +1923 to +1936
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Is there a reason to allow the user to write a subroutine with a naming clash between its parameters? Seems a bit odd.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I can't think of why you would set out to write a schedule like the example, but I could see it coming up in more complex code, like calling different functions to generate different parts of a schedule or using references to different schedules and then using inline_subroutines to merge them into one big schedule block.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

That's totally fine by me, if there's a reason. I really don't know any of the context - I was basically just reviewing by "smell".

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yeah basically this kind of problem occurs because the pulse module uses Parameter object designed for circuit as-is. Usually pulse parameter is scoped by pulse name (this allows user to safely have the same name in a schedule, i.e., sequence of a pulse), and thus UUID mechanism is not necessary. In pulse programming, it will never happen to have the same parameter name in the same pulse. So proper model should be Parameter(scope, name), rather than Parameter(name, uuid).


print(pulse_prog)

2. Calling a schedule

.. jupyter-execute::

x_sched = backend.instruction_schedule_map.get("x", (0,))

with pulse.build(backend) as pulse_prog:
pulse.call(x_sched)

print(pulse_prog)

Currently, the backend calibrated gates are provided in the form of :class:`~.Schedule`.
The parameter assignment mechanism is available also for schedules.
However, the called schedule is not treated as a reference.

3. Calling a quantum circuit

.. jupyter-execute::

backend = FakeBogotaV2()

qc = circuit.QuantumCircuit(1)
qc.x(0)

with pulse.build(backend) as pulse_prog:
pulse.call(qc)

print(pulse_prog)

.. warning::

Calling a circuit from a schedule is not encouraged. Currently, the Qiskit execution model
is migrating toward the pulse gate model, where schedules are attached to
circuits through the :meth:`.QuantumCircuit.add_calibration` method.

Args:
target: Target circuit or pulse schedule to call.
name: Name of subroutine if defined.
value_dict: Parameter object and assigned value mapping. This is more precise way to
identify a parameter since mapping is managed with unique object id rather than
name. Especially there is any name collision in a parameter table.
kw_params: Parameter values to bind to the target subroutine
with string parameter names. If there are parameter name overlapping,
these parameters are updated with the same assigned value.
name: Optional. A unique name of subroutine if defined. When the name is explicitly
provided, one cannot call different schedule blocks with the same name.
value_dict: Optional. Parameters assigned to the ``target`` program.
If this dictionary is provided, the ``target`` program is copied and
then stored in the main built schedule and its parameters are assigned to the given values.
This dictionary is keyed on :class:`~.Parameter` objects,
allowing parameter name collision to be avoided.
kw_params: Alternative way to provide parameters.
Since this is keyed on the string parameter name,
the parameters having the same name are all updated together.
If you want to avoid name collision, use ``value_dict`` with :class:`~.Parameter`
objects instead.

Raises:
exceptions.PulseError: If the input ``target`` type is not supported.
"""
if not isinstance(target, (circuit.QuantumCircuit, Schedule, ScheduleBlock)):
raise exceptions.PulseError(
f'Target of type "{target.__class__.__name__}" is not supported.'
)
raise exceptions.PulseError(f"'{target.__class__.__name__}' is not a valid target object.")

_active_builder().call_subroutine(target, name, value_dict, **kw_params)
_active_builder().call_subroutine(
subroutine=target, name=name, value_dict=value_dict, **kw_params
)


def reference(name: str, *extra_keys: str):
"""Refer to undefined subroutine by string keys.

A :class:`~qiskit.pulse.instructions.Reference` instruction is implicitly created
and a schedule can be separately registered to the reference at a later stage.

.. code-block:: python

from qiskit import pulse

with pulse.build() as main_prog:
pulse.reference("x_gate", "q0")

with pulse.build() as subroutine:
pulse.play(pulse.Gaussian(160, 0.1, 40), pulse.DriveChannel(0))

main_prog.assign_references(subroutine_dict={("x_gate", "q0"): subroutine})

Args:
name: Name of subroutine.
extra_keys: Helper keys to uniquely specify the subroutine.
"""
_active_builder().append_reference(name, *extra_keys)


# Directives
Expand Down
4 changes: 4 additions & 0 deletions qiskit/pulse/exceptions.py
Original file line number Diff line number Diff line change
Expand Up @@ -37,3 +37,7 @@ class NoActiveBuilder(PulseError):

class UnassignedDurationError(PulseError):
"""Raised if instruction duration is unassigned."""


class UnassignedReferenceError(PulseError):
"""Raised if subroutine is unassigned."""
2 changes: 2 additions & 0 deletions qiskit/pulse/instructions/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -42,6 +42,7 @@

Acquire
Call
Reference
Delay
Play
SetFrequency
Expand All @@ -63,3 +64,4 @@
from .phase import ShiftPhase, SetPhase
from .play import Play
from .snapshot import Snapshot
from .reference import Reference
Loading