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

Update pulse instruction interface to the new pulse model #11726

Merged

Conversation

TsafrirA
Copy link
Collaborator

@TsafrirA TsafrirA commented Feb 6, 2024

Summary

The Pulse Instructions interface is updated to accommodate the new pulse model (#10694).

Details and comments

Following discussions in #11464 (which is replaced by this PR), it was decided that the new pulse model will be introduced into the instructions interface in the following way.

The signature of instruction initialization will be updated to (demonstrated here for the Play instruction, but the same logic holds for all relevant instructions):

def __init__(self, pulse: Pulse, *, target = None, frame = None, mixed_frame = None, channel = None, name = None):

Upon initialization, the input arguments are validated to guarantee that a correct combination is provided (mixed frame for Play, mixed frame\frame for phase\frequency instructions, etc.). This signature is the most flexible one, while also providing a path for a minimally breaking transition from the old model to the new model. An intermediate signature during the deprecation period (Qiskit 1.x) would be:

def __init__(self, pulse: Pulse, channel = None, name = None, *, target = None, frame = None, mixed_frame = None):

such that a minimal change is needed to update existing code.

As discussed in #11464, this option is not without limitations, and it is quite possible that eventually the interface would change. However, with its flexibility the option presented here provides a solid starting step, which could be easily modified later on.

For posterity, it should be mentioned that another option which was favorably considered is:

def __init__(self, pulse: Pulse, mixed_frame: Union[PulseChannel, MixedFrame, Tuple[PulseTarget, Frame]], name = None):

The presented implementation removes the _validate abstract method. The method was implemented individually for all instructions, and thus didn't reduce code, and hid the errors away from the initialization function. However, a FrameInstruction subclass was introduced with a _validate function, because the validation logic is shared across all set\shift phase\frequency instructions.

The following instructions were not changed:

  • Reference
  • Snapshot
  • Directives

The Acquire instruction was updated in a shallow way, and requires further discussion. Main questions include -

  • Where should the MemorySlot (RegisterSlot) class sit in the new model?
  • Should we support acquiring of a general port, or only Qubit?
  • As a side question, is there a need for RegisterSlot? Does it serve any function on any relevant backend?

@TsafrirA TsafrirA requested review from eggerdj, wshanks and a team as code owners February 6, 2024 14:55
@qiskit-bot
Copy link
Collaborator

One or more of the the following people are requested to review this:

  • @Qiskit/terra-core
  • @nkanazawa1989

@nkanazawa1989 nkanazawa1989 added experimental Experimental feature without API stability guarantee mod: pulse Related to the Pulse module labels Feb 6, 2024
@TsafrirA TsafrirA requested review from a team and nonhermitian as code owners February 6, 2024 22:36
Copy link
Contributor

@nkanazawa1989 nkanazawa1989 left a comment

Choose a reason for hiding this comment

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

Thanks @TsafrirA for your hard work of unittest update. I like new pattern for instruction construction. Since new model allows multiple patterns to specify how the instruction is executed, user must explicitly give operands. Although new pattern looks complicated, I believe our target users are familiar with the concept and they can use new model with well-written migration guide (TODO).

On the other hand, tons of changes to the unittests is an indication of breaking API change. In some minor release of 1.0 we should add deprecation warning for the use of non-keyword args. This is just a reminder and doesn't block this PR.

qiskit/pulse/instructions/acquire.py Outdated Show resolved Hide resolved
@property
def channel(self) -> AcquireChannel:
def channel(self) -> Union[AcquireChannel, None]:
Copy link
Contributor

Choose a reason for hiding this comment

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

Can we deprecate channel and acquire at minimum cost?

Copy link
Collaborator Author

Choose a reason for hiding this comment

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

Once we're done, I think yes.

qiskit/pulse/instructions/delay.py Outdated Show resolved Hide resolved
return inst_target

@property
def channel(self) -> Union[PulseChannel, None]:
Copy link
Contributor

Choose a reason for hiding this comment

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

Because we will deprecate channel at some point and move to mixed-frame model, we don't need to add channel property to newly introduced classes.

Copy link
Collaborator Author

Choose a reason for hiding this comment

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

This isn't really an addition. It's just a way to keep the channel property of all frame instructions with less code clutter. It will make it easier to deprecate it later.

Copy link
Contributor

Choose a reason for hiding this comment

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

I would add channel property to each subclass rather in the baseclass. In this way newly added instructions will not gain legacy property. Anyways this is small difference and doesn't block merge.

``FrameInstruction`` should be considered abstract, and should not be instantiated as is.
"""

def _validate_and_format_frame(
Copy link
Contributor

Choose a reason for hiding this comment

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

Perhaps you also need to overwrite _validate in the sueprclass? Since Instruction._validate is called inside the constructor and it checks if all self.channels are Channel instance. When either mixed_frame or frame is passed, self.channel returns None and the validation will fail.

qiskit/pulse/instructions/phase.py Outdated Show resolved Hide resolved
Comment on lines 71 to 85
if (target is not None) != (frame is not None):
raise PulseError("target and frame must be provided simultaneously")
if (channel is not None) + (mixed_frame is not None) + (target is not None) != 1:
raise PulseError(
"Exactly one of mixed_frame, channel or the duo target and frame has to be provided"
)

if target is not None:
if not isinstance(target, PulseTarget):
raise PulseError(f"Expected a PulseTarget, got {target} instead.")
if not isinstance(frame, Frame):
raise PulseError(f"Expected a Frame, got {frame} instead.")
mixed_frame = MixedFrame(target, frame)
else:
mixed_frame = mixed_frame or channel
Copy link
Contributor

Choose a reason for hiding this comment

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

Suggested change
if (target is not None) != (frame is not None):
raise PulseError("target and frame must be provided simultaneously")
if (channel is not None) + (mixed_frame is not None) + (target is not None) != 1:
raise PulseError(
"Exactly one of mixed_frame, channel or the duo target and frame has to be provided"
)
if target is not None:
if not isinstance(target, PulseTarget):
raise PulseError(f"Expected a PulseTarget, got {target} instead.")
if not isinstance(frame, Frame):
raise PulseError(f"Expected a Frame, got {frame} instead.")
mixed_frame = MixedFrame(target, frame)
else:
mixed_frame = mixed_frame or channel
if (channel is not None) + (mixed_frame is not None) + (target is not None) != 1:
raise PulseError(
"Exactly one of mixed_frame, channel or the duo target and frame has to be provided"
)
if target is not None:
if not isinstance(target, PulseTarget):
raise PulseError(f"Expected a PulseTarget, got {target} instead.")
if not isinstance(frame, Frame):
raise PulseError(f"Expected a Frame, got {frame} instead.")
mixed_frame = MixedFrame(target, frame)
else:
mixed_frame = mixed_frame or channel

Also equivalent right?

Copy link
Collaborator Author

Choose a reason for hiding this comment

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

This will actually fail to raise an error if you provide frame and mixed_frame (the frame argument will be ignored).

"""
for key in self.ref_keys:
ref_keys = (name,) + tuple(extra_keys)
Copy link
Contributor

Choose a reason for hiding this comment

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

Why did you move validation to the constructor? _validate is a base class method which is called after class is instantiated, which defines an extra validation protocol for instruction-specific operands. This makes constructor bit cleaner. If you don't like it, feel free to remove it from the base class (because this is just a protected method). Since you added lots of typecheck directly in the constructor, this delayed validation mechanism doesn't fit in with new framework.

test/python/pulse/test_instructions.py Outdated Show resolved Hide resolved
test/python/pulse/test_instructions.py Outdated Show resolved Hide resolved
@nkanazawa1989 nkanazawa1989 linked an issue Feb 7, 2024 that may be closed by this pull request
9 tasks
@nkanazawa1989 nkanazawa1989 changed the title [pulse feature branch] Update pulse instruction interface to the new pulse model Update pulse instruction interface to the new pulse model Feb 7, 2024
@nkanazawa1989 nkanazawa1989 mentioned this pull request Feb 7, 2024
9 tasks
@nkanazawa1989 nkanazawa1989 self-assigned this Feb 9, 2024
@TsafrirA
Copy link
Collaborator Author

Thanks @nkanazawa1989 for the quick review. I've modified the tests and some of the validation logic as you requested.

As I wrote, I removed entirely the _validate method. I think it wasn't very useful, as most instructions implemented it individually anyway. I think the current implementation better reflects the errors to the user, where the user wants to see them.
The only exception is the validation method which was added to the FrameInstruction class, because here we can really reduce code clutter.

Copy link
Contributor

@nkanazawa1989 nkanazawa1989 left a comment

Choose a reason for hiding this comment

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

One more comment for class name. Overall the PR looks good to go. Can you resolve conflict too?

qiskit/pulse/instructions/instruction.py Outdated Show resolved Hide resolved
@TsafrirA
Copy link
Collaborator Author

@nkanazawa1989
I've resolved the conflicts.

Copy link
Contributor

@nkanazawa1989 nkanazawa1989 left a comment

Choose a reason for hiding this comment

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

Thanks @TsafrirA LGTM :)

@nkanazawa1989 nkanazawa1989 merged commit 8fb185b into Qiskit:feature/pulse-ir Feb 12, 2024
10 checks passed
@TsafrirA TsafrirA deleted the InstructionInterfaceV2 branch February 14, 2024 20:40
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
experimental Experimental feature without API stability guarantee mod: pulse Related to the Pulse module
Projects
None yet
Development

Successfully merging this pull request may close these issues.

Pulse Compiler and IR
3 participants