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

Add function to obtain the waveform on a PulseChannel #6995

Closed
wants to merge 0 commits into from

Conversation

snsunx
Copy link

@snsunx snsunx commented Sep 9, 2021

Summary

I have written a function get_channel_waveform in pulse/transforms/channel_transforms.py to obtain the waveform on a PulseChannel.

Details and comments

I have tested the function on predefined pulse schedules, specifically the cx01 and meas0 in the following code.

IBMQ.load_account()
provider = IBMQ.get_provider('redacted')
backend = provider.get_backend('ibmq_bogota')
inst_sched_map = backend.defaults().instruction_schedule_map
cx01 = inst_sched_map.get('cx', (0, 1))
meas0 = inst_sched_map.get('measure', 0)

It feels to me that the function should be a method of Schedule rather than a standalone function in a standalone script. I would like to know what the maintainers think.

@CLAassistant
Copy link

CLAassistant commented Sep 9, 2021

CLA assistant check
All committers have signed the CLA.

Copy link
Contributor

@taalexander taalexander left a comment

Choose a reason for hiding this comment

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

This is a great start. Could you also please add tests for this functionality? I would also appreciate if @nkanazawa1989 could take a look at this.

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 for taking a stab at waveform transform. I'm grad to see we have another transform function. My two major points are

  • Move frame parser to transform

    • Transform should not have dependency on visualizer. You can create new parser in the pulse transform module, then use new transform function in the visualizer.
  • Split function into multiple pieces

    • One huge transform function is usually hard to maintain. If we split the transform into multiple atomic functions, we can efficiently write unittest. For example,
      • Just call get waveform if any parametric pulse exist (to_waveform)
      • Apply phase shift to each waveform (apply_phase)
      • Apply carrier waves based on frequency at t0 (apply_carrier)
      • Combine all waveforms in the channel to create full waveform array (to_full_waveform)
    • By combining these passes with target_qobj_transform, you can create new transform function, something like waveform_transform. For the visualizer, you can create another transform function that only performs to_waveform and apply_phase. Note that the visualizer draws pulses individually.

It feels to me that the function should be a method of Schedule rather than a standalone function in a standalone script. I would like to know what the maintainers think.

This is usability vs maintenance issue. I think Schedule is a container of instructions, i.e. a program, and thus the operations (i.e. transforms) and programs should be separated. In Qiskit, QuantumCircuit doesn't take this approach and it has many method to transform itself, however, now the class has nearly 5000 lines of code. The number of lines will be further increased if we add new transform.

This is not preferable in terms of test and code maintenance. If we separate operations from the program, we can implement as many transforms as we want while keeping the size of Schedule class. Any thoughts? @taalexander

@snsunx
Copy link
Author

snsunx commented Sep 10, 2021

Thank you two for your comments. I have updated some argument type checking statements, which I think addressed most of @taalexander's comments. One remaining task is to figure out the correct frequency for a ControlChannel. Additionally, since I'm new to Qiskit development, can I ask where I should include the unit tests? Is there an example script I can look at?

I will take a look at how to port the functionalities of ChannelEvents over to the pulse/transform module, which may take a bit of time to figure out.

@nkanazawa1989
Copy link
Contributor

Here you can find some unittests for the pulse transforms. Please let me know if you have any question about the implementation of the ChannelEvents. The frequency of control channel is computed in the https://github.com/Qiskit/qiskit-terra/blob/117d55df8933ca79753d01f14483267bc7e6112a/qiskit/visualization/pulse_v2/device_info.py#L95

Note that according to the OpenPulse specification, control channel can be whatever we can describe as a device Hamiltonian. This means, the frequency computation of this channel may depend on the device architecture. So you need to write a base class that implements abstract method to compute control channel frequencies, along with its subclass that implements a specific computation for IBM devices.

@snsunx
Copy link
Author

snsunx commented Sep 21, 2021

@nkanazawa1989 Thank you! Sorry I have been busy with some other work and haven't been able to get to this. I should be able to give another update later this week.

@snsunx
Copy link
Author

snsunx commented Sep 29, 2021

Hi, sorry for a slightly late update. I moved ChannelEvents to pulse.transforms and renamed it to ChannelTransforms. The new function get_parsed_instructions is mostly based on get_waveforms in the ChannelEvents class but also includes three additional steps: _parse_waveform, _apply_phase, and optionally _apply_frequency. I still need to work on integrating it with the plotting functions under the visualization module along with some cleanup. I will also write some unit tests later. Meanwhile if any of you has suggestions please let me know.

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 @snsunx for the constant update! I think now it is moving toward right direction. You can also create a dedicated folder in the pulse transforms since this transform creates several files. And you need to move some data structure back to drawer, e.g. class LineType(str, Enum):.

I'll convert this PR into draft until the code is finalized :)

The channel transform manager for the specified channel.
"""
# flatten the schedule
program = target_qobj_transform(program)
Copy link
Contributor

Choose a reason for hiding this comment

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

We can assume this is externally done by writing another transform function including this class like a pass. Otherwise this transform will have unnecessary dependency on target_qobj_transform.

@snsunx
Copy link
Author

snsunx commented Oct 4, 2021

Hi @nkanazawa1989, I didn't create a new folder but made the following organizational changes:

  • I put PhaseFreqTuple and ParsedInstruction in channel_transforms.py. I was thinking maybe putting them in a separate script, but they seem to be only used in ChannelTransforms and I think fewer scripts are easier to maintain
  • I moved the script backend_info.py which contains BackendInfo and OpenPulseBackendInfo to qiskit/providers/models. I removed "Drawer" from the name DrawerBackendInfo since the class itself doesn't seem to have direct implications to drawer functions

I wonder what are your thoughts on these changes. If these changes are ok, I'll go on to write some unit tests and also modify the existing unit tests in test/python/visualization/test_generators.py, which is the reason for some of the failed checks.

Also I made a forced amend push (from 30125c1 to 7c9fe84), but I realized it's a bad idea. I'll avoid doing that in the future.

@nkanazawa1989
Copy link
Contributor

I put PhaseFreqTuple and ParsedInstruction in channel_transforms.py. I was thinking maybe putting them in a separate script, but they seem to be only used in ChannelTransforms and I think fewer scripts are easier to maintain

Yes, that should be okey.

I moved the script backend_info.py which contains BackendInfo and OpenPulseBackendInfo to qiskit/providers/models. I removed "Drawer" from the name DrawerBackendInfo since the class itself doesn't seem to have direct implications to drawer functions

I don't think this is good direction. The qiskit.providers is a module that defines a template of backend and provider model so that third party (i.e. non-IBM) provider can use these classes as a baseclass of their cloud and backend API. BackendInfo doesn't provide such API and this is just a parser of it. If this class is used only by the transform the file should be in the transform module, but if you find this class is also useful for other pulse functionality probably you can put the file in qiskit.pulse.

@snsunx snsunx marked this pull request as ready for review October 7, 2021 23:36
@snsunx snsunx requested a review from nonhermitian as a code owner October 7, 2021 23:36
@snsunx
Copy link
Author

snsunx commented Oct 8, 2021

Hi all, I think I finished all the required features. To summarize mainly what I have done

  • Create a new class ChannelTransforms based on the old ChannelEvents class
  • Create a new function get_parsed_instructions in ChannelTransforms based on the old get_waveforms function, which additionally has the functionalities of applying phase and (optionally) frequency modulation
  • Add a function get_waveform which returns all the waveforms as a single Waveform on a channel
  • Remove the waveform parsing part in _parse_waveform in visualization/pulse_v2/generators/waveform.py and rename it to _get_metadata
  • Remove the phase modulation part in gen_filled_waveform_stepwise in visualization/pulse_v2/generators/waveform.py

I moved the unit tests around a bit. The new test class TestChannelTransforms combines TestChannelEvents and the two waveform parsing tests in TestWaveformGenerators. I added two new unit tests called test_get_waveform and test_get_waveform_with_frequency.

The checks were not successful due to two errors from the linter, one being a format issue of the attribute t0, the other being a potentially circular import of device_info.py. I wonder what your thoughts are on these two errors, especially for the latter one maybe I should device_info.py under pulse/transforms instead of under pulse.

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.

Sorry about slow response. This is really great job and thanks for your hard work!

I think your code is in right direction! However, you don't need to bind yourself to what ChannelEvents originally implemented ;) Indeed the ChannelEvents converts a Schedule into some other program representation that is not well defined (I intended to use this representation only in the drawer). Thus following transforms (if exist) need to write dedicated parser for it.

To avoid that, I would add MicroInstruction (to be used as a data container that can be used in the Schedule, probably you have better name), that would look like

class MicroInstruction(Instruction):
    def __init__(
            self, 
            t0: int,
            samples: np.ndarray,
            channels: Tuple[Channels],
            metadata: Optional[Dict] = None,
            name: Optional[str] = None, 
        ):
        super().__init__(operands=(t0, samples, channels, metadata), name=name)

    @property
    def duration(self):
        return len(self.samples)
    
    @property 
    def start_time(self):
        return self._operands[0]

    @property 
    def stop_time(self):
        return self.start_time + self.duration

    @property
    def samples(self):
        return self._operands[1]

    @property
    def channels(self):
        return self._operands[2]

    @property
    def metadata(self):
        return self._operands[3]

Then the parsed frequency and phase will be stored as a part of metadata. You can load the instruction metadata and apply phase or sideband modulation to samples in each transfrom pass.

In this approach, we don't need to introduce ParsedInstruction, and most of transform pass function signature will become transform_pass(program: Schedule, *args, **kwargs) -> Schedule which is much more handy. I'd like to hear your thought @taalexander

(EDIT)
regarding the circular import: this is because you are importing pulse module from the pulse module. you should selectively import a specific component as needed basis.

ydata: np.ndarray = None


class ChannelTransforms:
Copy link
Contributor

Choose a reason for hiding this comment

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

Now you can divide this class into small pieces with two motivations.

  • Small transformation script is easy to write a unittest. If there is a huge script, we can induce many edge cases.
  • If some people want to customize a part of this script, one needs to prepare almost the copy of ChannelTransforms, and there will be a lot of boilerplate code to manage. If the transformation is a collection of small pieces, we can reuse rest of it.

To me it looks like this class consists of 5 functionalities

  1. Convert ParametricPulse into Waveform (to_waveform)
  2. Track phase and frequency of time t and add frame information to each play instruction (track_frame)
  3. Apply phase shift based on attached phase (apply_phase_modulation)
  4. Apply sideband modulation based on attached frequency (apply_frequency_modulation)
  5. Merge waveforms on the huge ndarray (merge_channel_waveforms)

I think these transforms can be written as a separate functions rather than a huge script, i.e. passes. At this level of segmentation, we can flexibly build arbitrary transform chain.

For example, in the V2 pulse drawer, pass 3 and 4 are only called based on user preference (because some users don't want to apply these modulation) and 5 will be never called. I would write a visualizer_transform function by combining previous passes with passes 1-4 above (that is different version of target_qobj_transform).

Note that you may need validation for the program duration in the pass 5. Since channel waveform is usually complex128 datatype, the long array (e.g, if you schedule QV circuit, you'll get very long schedule) will consume the computer memory and it may hang up.

@snsunx
Copy link
Author

snsunx commented Oct 22, 2021

Hi @nkanazawa1989, thanks for your comments. I added events.py back with proper deprecation warnings.

In this approach, we don't need to introduce ParsedInstruction, and most of transform pass function signature will become transform_pass(program: Schedule, *args, **kwargs) -> Schedule which is much more handy. I'd like to hear your thought @taalexander

I was thinking about this and am not sure if this approach would be better than what ChannelEvents or the new ChannelTransforms does. I can think of mainly two reasons:

  1. The visualizer module or the user who needs the merged waveforms just needs the parsed waveforms but won't need a Schedule object.
  2. Currently the schedule.instructions property returns a tuple of (t0, inst) where inst is an Instruction, so it seems that t0 and also frames shouldn't be bound to components of a Schedule. Hence instead of MicroInstruction, it's probably a good idea to make the current ParsedInstruction (or the old PulseInstruction) independent of Schedule, as what ChannelEvents originally does.

I wonder what you and @taalexander think about these.

With regard to splitting functionalities,

  1. Convert ParametricPulse into Waveform (to_waveform)
  2. Track phase and frequency of time t and add frame information to each play instruction (track_frame)
  3. Apply phase shift based on attached phase (apply_phase_modulation)
  4. Apply sideband modulation based on attached frequency (apply_frequency_modulation)
  5. Merge waveforms on the huge ndarray (merge_channel_waveforms)

I think 1 corresponds to _parse_waveform, 2 to 4 corresponds to get_parsed_instructions, and 5 corresponds to get_waveform. I noticed your comments earlier on making apply_phase_modulation and apply_frequency_modulation as separate functions, but since both are one-line functions I think putting boolean flags should be sufficient (as far as I understand phase modulation should always be applied while frequency modulation is applied optionally). But if writing them as separate functions is better I can go on to do that.

Since a lot of scripts are already modified and moved around in this PR, I made minimal changes to the functions from the original visualization module to avoid accidentally breaking something in the code. I indeed think there are a few more places that can be improved in the module, and it may be better to open another PR for that.

@nkanazawa1989
Copy link
Contributor

I made minimal changes to the functions from the original visualization module to avoid accidentally breaking something in the code. I indeed think there are a few more places that can be improved in the module, and it may be better to open another PR for that.

Yes, agreed. The qiskit.pulse.transforms is replica of transpiler passes, and I intend to keep capability of integration of this chain into the pass manager. However, the ChannelTransforms class is no longer a pass because of two reasons.

  • We cannot divide functionality. Always need to add entire functionality with disabling unused part. This doesn't fit in with the concept of passes.
  • I don't want to introduce another representation of schedule, i.e. ParsedInstruction. This is intended to be in visualization. The data representation existing in qiskit.pulse should be operable as a schedule. However, this is just a list of dataclass and not compatible with Schedule class. We recently added ScheduleBlock to the pulse module, but this already causes some complexity in the stack.

Back to your original motivation, probably you just need to add get_waveform method to original ChannelEvents. Since this method already exist for different purpose, you can add to_single_waveform helper function that takes ChannelEvents instance as an input. You can create util.py file under the qiskit.visualization.pulse_v2 for this.

Creation of these passes will be done in follow-up PRs if you will :)

@snsunx
Copy link
Author

snsunx commented Nov 12, 2021

Now I see that this class should not be in qiskit.pulse.transforms. I will open a new PR for this issue starting from the current main branch.

Back to your original motivation, probably you just need to add get_waveform method to original ChannelEvents. Since this method already exist for different purpose, you can add to_single_waveform helper function that takes ChannelEvents instance as an input. You can create util.py file under the qiskit.visualization.pulse_v2 for this.

Should I add the to_single_waveform in a new script utils.py under qiskit.visualization.pulse_v2, or should I add it as a method in ChannelEvents as done in this PR? Currently there is no utils.py under qiskit.visualization.pulse_v2 so I'm not sure if it's a good idea to have a separate script for this function.

@nkanazawa1989
Copy link
Contributor

Of course you can add the method to the ChannelEvents under different name.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

Successfully merging this pull request may close these issues.

4 participants