Qiskit / qiskit

Qiskit is an open-source SDK for working with quantum computers at the level of extended quantum circuits, operators, and primitives.
https://www.ibm.com/quantum/qiskit
Apache License 2.0
5.04k stars 2.32k forks source link

Pulse API Proposal #1713

Closed taalexander closed 5 years ago

taalexander commented 5 years ago

Terra: Pulse API

Status Proposed
Author(s) talexander@ibm.com, knzwnao@jp.ibm.com, itoko@jp.ibm.com
Updated 2019-03-04

Objective

The Qiskit Backend Specifications document provides a description of a standardized data format for OpenQASM (which Terra currently supports) and a new protocol for performing pulse level experiments on backends through OpenPULSE. Certain IBMQ backends have begun to support the ability to perform OpenPulse experiments, however, experiment pulse Qobjs must currently be built by hand. In the same way that Terra provides a convenient API for building circuits, Terra should also provide a simple, but powerful interface to construct pulse QObjs.

This document contains a working proposal for a Pulse API, with the aim of engaging the Qiskit community in the development process of this powerful new Terra feature.

The accepted solution should be:

Pulse Overview

The Backend specification provides a succinct description of the endpoint changes required to run Pulse experiments. There are three major components:

  1. Configuration information supplied to the user required for bootstrapping the construction of pulses. This includes additional fields in the configuration.json file supplied by the backend endpoints such as the pulse drive and measurement sample discretization times dt and dtm and an additional backend endpoint defaults which supplies a JSON file that contains default information about the backend including the estimated qubit and measurement frequencies, and example gate to pulse mappings through the cmd_def and pulse_library.

    • [x] Support for defaults endpoint.
  2. Submitting a PULSE type QObj rather than a QASM which is how QuantumCircuits are submitted to backends. Pulse commands are applied to channels, these channels may be thought of as a logical abstraction of a microwave channel (although this may vary depending on the type of quantum hardware). The specification gives drive, measurement, and u-channels. For PULSE experiments the supported commands are sample pulses, fc, pv, and acquire.

    • [x] Support for PULSE QObj construction.
  3. Pulse results can be in one of three forms depending on whether measurement level is 0,1,2. Support must be given for the measurement levels.

    • [x] 0: Corresponds to the raw pulse results from the device.
    • [x] 1: Corresponds to the kernel integrated values returned from the discriminator.
    • [x] 2: Discriminated kernel integrated values. This is the analog to the shots returned from the device for the current QASM path.

    Furthermore, pulses may specify avg or single which determines whether the average shot data should be returned or if the individual shot data should be given.

Pulse Channels

Channels are a discrete time ordered lists of commands. Channel commands have an initial time (t0) and duration (which may be implicit). Non zero duration commands may not overlap on a channel.

d

Every qubit has an associated drive channel.

m

Every qubit has an associated measurement drive channel.

u

Additional control channels may be given as U channels. The action of these channels will be described by the Hamiltonian returned by the backend.

acquire

Acquire commands do not have run time output, but they do have an initial time and duration. Future hardware may support multiple acquires. A channel should exist for each entry in the meas_map.

snapshot

Simulator backends may support snapshots. These may be scheduled on a snapshot channel.

Pulse Commands

Pulse Qobjs have a new set of supported commands. Commands may be conditionally executed depending on the state of a register (for hardware that supports it)

Sample Pulse

Sample pulse commands reference a pulse in the pulse library with their name parameter. The pulse has an initial time t0 and its duration is implicitly defined by the length of the pulse it references.

fc

The frame change pulse specifies an advance of all future pulses on the pulse channel. The frame change has a time t0 and its duration is implicitly 0.

pv

The persistent value pulse holds its input value from the initial time t0 until the next pulse on the channel, which implicitly defines its duration.

acquire

The acquire command specifies an acquisition of measurement data. It does not include the measurement stimulus, which may independently managed with pulses applied to the m channels. The output data from the acquire command depends on the meas_level set in the Qobj configuration.

Acquire statements also take kernels and discriminators data structures that specify the kernel integration of the acquired data to be applied (if meas_level= 1 or 2) and the discriminator function to apply to the kernel integrated data (if meas_level=2) respectively.

Acquire may store their results into a memory_slot for meas_level 0,1 and 2.

Acquire may store their discriminated result into a register it and operations may later be done conditionally on the register state. (for simulators/hardware that supports)

Pulse Library

Sample pulses reference a pulse in the pulse_library.

Command Definitions

Pulse Construction

Each Pulse Qobj will have many experiments, and each experiment will consist of a set of channels that contain time ordered commands. These pulse commands will be constructed to target a specific backend.

QASM to PULSE

Backends provide command definitions which provide a direct mapping between QASM gates and sequences of PULSE commands.

Proposed Solution

The proposed solution below is a bottom up API for constructing a Python object oriented representation of a QObj with an easy to use builder interface.

Schedule API (simple v0.8, refined v0.9)

This document will use sample code to describe the pulse API. We will build from commands up to the scheduler and establish the API hierarchy. A separate proposal will detail the internal representation of the Scheduler (ie., DAG).

The specific example will be the case of performing a spin locking experiment. The given pulse sequence involves a single qubit. The pulse sequence consists of

  1. An initial calibrated y90p=ry(pi/2) pulse to initialize qubit in transverse plane of Bloch sphere.
  2. A long spin locking pulse with amplitude A and phase \phi for time $\tau$.
  3. Another calibrated pulse y90m=ry(-pi/2)
  4. A measurement pulse measure.
  5. An acquisition statement to measure the qubit.
  6. Sweep \phi and fit data to obtain phase of x quadrature with respect to calibrated pulses.

Building our pulse sweep.

Initializing Channels (v0.8)

First obtain our backend

backend = IBMQ.get_backend('pulse_backend')

We then must initialize the available channels from our backend

from qi import pulse
# this is the analog of
# qc = qi.QuantumCircuit(n_qubits)
channel_bank = pulse.ChannelBank(backend)
# alternatively we could provide override parameters
channel_bank = pulse.ChannelBank(backend, n_qubits=10)

note we are very much open to a better name than ChannelBank. The channel bank is a wrapper collection over collections of different types of channels.

from pulse.channels import *
assert isinstance(channel_bank.drive, DriveChannelCollection)
assert isinstance(channel_bank.measure, MeasureChannelCollection)
assert isinstance(channel_bank.composite, CompositeChannelCollection)
assert isinstance(channel_bank.acquire, AcquireChannelCollection)
assert isinstance(channel_bank.snapshot, SnapshotChannelCollection)

A channel collection is analogous in interface usage to a Quantum/ClassicalRegister (in fact this was initially name ChannelRegister and we are still open to this in order to maintain consistency with the circuit api).

drive_channels = channel_bank.drive
drive_channel_0 = drive_channels[0]
drive_channel_slice = drive_channels[0:10]
assert drive_channel_0 == drive_channel_slice[0]

Note that by default there is an acquire channel for each qubit. Non-trivial meas_maps will be enforced at compile time. It will be up to the backend to determine its behavior for qubits tied in the same meas_map entry.

Initializing the Schedule (v0.8)

We now initialize our Schedule.

exp_schedule = pulse.Schedule(channel_bank, name='phase_exp', buffer=2)

The Schedule is a timed scheduled collection of commands across channels and is the pulse equivalent of the QuantumCircuit. Schedules are immutable (useful for tracking schedule reuse among other things). Internally a Schedule will be described by a control flow graph (CFG) and directed acyclic graph (DAG), these implementations will be covered in a later document and the underlying machinery should be shared between QuantumCircuit and Schedule (require collaboration with Transpiler team).

A schedule maintains information about the Commands it contains such as

exp_schedule.t0  # time of first command
exp_schedule.tf  # time of last command
exp_schedule.duration  # total duration of schedule
exp_schedule.children  # get children Schedules that Schedule is composed of
exp_schedule.get_commands(filter_expr=None, type=None)  # get all commands that satisfy given constraint. Useful for building commands such as below
exp_schedule.get_sample_pulses()  # used to construct pulse library for qobj.

Building Commands(v0.8)

The PulseCommand is the base object within the pulse API. To build our pulse schedule for the phase identification experiment we will use a combination of commands supplied by the backend and commands that we create ourselves.

We could first create a sample pulse by directly creating a complex list of samples

n = 10
samples = np.ones(n, dtype=np.complex32)
spin_lock_0 = pulse.SamplePulse(samples, name='spin_lock_0')

Alternatively we can create a function that generates the sample pulse (outputting a numpy array) and make it a DiscreteFunctionalPulse

@pulse.DiscreteFunction
def square(duration, val):
  return np.constant(val, np.complex32)

The above DiscreteFunction decorator creates a DiscreteFunction and is not yet a command. We must call square with a duration and val to create a DiscreteFunctionCommand

spin_lock_1 = square(n, 1., name='spin_lock_1')

We will also create a library of standard pulses so alternatively we could use the builtin version

spin_lock_2 = pulse.library.square(n, 1., name='spin_Lock_1')

We can verify the equality of the pulses

assert spin_lock_1 == spin_lock_2

Which will check that the pulses are of the same type and if so their samples are equal.

Finally we will also support continuous functions in conjunction with a decimation strategy (v0.9).

@pulse.AnalyticFunction(pulse.decimation.left)
def square_continuous(time, val):
  return val

assert square_continuous(n, val) == spin_lock_1

Dunder methods (v0.9)

Both SamplePulse, DiscretePulse, AnalyticPulse will support dunder methods to manipulate and create new pulse objects such as __mul__, __exp__ etc. Optionally we could expose them as one dimensional numpy arrays and treat them as such.

Pulse Filters (v0.9)

to be completed Scipy filtering backbone

Command Definitions (v0.8)

The command definitions from the backend will be converted into a CommandDefinition and will be available in backend.defaults. A given command for a set of qubits will be accesed by

cmd_def = backend.defaults.cmd_def
u3_q0 = cmd_def.u3[0]
measure_q0 = cmd_def.measure[0]
cx_q01 = cmd_def.cx[0, 1]  #non parameterized

The schedules returned are parameterized (except for cx). This will be expanded on later with types and parameters. This will also touch on hardware support analytic pulses. Parameters may be shared across pulses and schedules. durations cannot be parameterized as this would allow scheduling constraints to be violated. A parameterized schedule may be evaluated to generate an absolute schedule.

x_q0 = u3_q0(pi, 0, pi)

The CommandDefinition will have available all gates available in qiskit.extensions that may be constructed from the supplied entries in cmd_def and will be dynamically created and then cached. We can therefore obtain our desired initialization pulse with

y90p_q0 = cmd_def.ry[0](pi/2, 0)
y90m_q0 = cmd_def.ry[0](-pi/2, 0)

We also require an acquire command for our qubit

acq_dur = 100
acquire_q0 = pulse.Acquire(acq_dur)

We could optionally supply a Kernel or Discriminator to the Acquire command which will be used depending on the meas_level of the compiled schedule.

Building Schedules (#1919, v0.8)

A schedule has a handle on Now that we have all of our pulses we may build our experiment schedule. The schedule supports two ways to add commands.

  1. _insert_command which has signature def _insert_command(self, block: Union[PulseCommand, Schedule], t0: int, *args, **kwargs)
  2. _append_command which has signature def _append_command(self, block: Union[PulseCommand, Schedule], *args, **kwargs) and appends after Schedule.tf. Typically the first args will be
    • Command a channel(s)/registers(s) to operate on.
    • Schedule no options required. Which are more pythonically called using the schedule channel handles. Below our simple experiment is created.
      exp_schedule.channels.drive[0].append(y90p_q0)
      exp_schedule.channels.drive[0].insert(exp_schedule.channels.drive[0].tf, spin_lock_0)
      exp_schedule.drive.append(y90m_q0)
      exp_schedule.channels.measure[0].insert(exp_schedule.tf, measure_q0)
      exp_schedule.channels.measure[0].insert(exp_schedule.channels.measure.t0, measure_q0)

On insertion appending of a schedule all commands will be added to the new schedule and command overlaps will be enforced.

Note we do not currently allow broadcasting as a given command is almost always unique to a specific channel.

To create more complicated pulses we may compose Schedules by inserting/appending a Schedule to another Schedule just as we would with a command to a channel.

sched1.append(sched2)
sched1.insert(sched1.tf, sched2)

Drive/Measure channel local oscillator (#1901, v0.8)

The local oscillator of a drive/measure channel may be set with

sched1.channels.drive[0].lo = 5.0
sched1.channels.drive[0].meas = 7.0

If not set, will default to qubit(meas)_freq_est of qubit corresponding to channel.

Scheduling (simple v0.8, passmanger v0.9)

Now that we have our experiment we must compile to obtain a PulseQobj

spin_lock_qobj = pulse.compile(sched1, backend, meas_level=1, average=False, rep_time=100, memory_slot_size=None)
assert isinstance(qobj, PulseQobj)

The pulse library will be built on schedule time from all classes that inherit SamplePulse. We can optionally provide a list of schedules

spin_lock_qobj = pulse.schedule([sched1], backend, meas_level=1)

We may optionally provide a PassManager which will contain ReSchedulePasses (among other pulses for other compilation steps). A ReschedulePass takes Schedules to Schedules. For instance these may be decoupling passes. The PassManager is input to pulse.reschedule. The actual implementation of this functionality will be covered in more detail at a later point in time.

spin_lock_qobj = pulse.compile(sched1, backend, passmanager=passmanager, meas_level=1)

The schedule pass will be added as an optional path in the compile method alongside the internal call to transpile.

Running Job

Given scheduled qobj, running is as simple as

job = backend.run(spin_lock_qobj)
result = job.result()

The experiment memory for a given experiment may be accessed as a numpy array for measurement levels 0 and 1

result.experiments[0].get_memory()

and counts for measurement level 2.

result.experiments[0].get_counts()

In the above example we have shown how to build a phase estimation experiment, schedule/create a pulse Qobj and then run that Qobj on a backend to obtain results.

Scheduling (QuantumCircuit to Schedule) (simple v0.8, passmanager v0.9)

The most opportunity for rearranging the order of pulses exists when mapping logically ordered gates to absolutely time-ordered commands as the gate logic maps may be used to optimize pulse scheduling (care must also be taken when scheduling classical logic).

This will simply be made available with

sched = pulse.schedule(quantum_circuit, backend, cmd_def=cmd_def, hamiltonian=hamiltonian, passmanager=passmanager)

pulse.schedule will use passes of type SchedulePass and these will be designed to exploit the underlying logic of the gates to intelligently place pulses.

Transpilation, scheduling and rescheduling may be accomplished in a single step with default passes

qobj = qi.compile(qc, backend, pulse=True)

This similarly extends to execute

job = qi.execute(qc, backend, pulse=True)

Visualization (v0.8)

Can plot:

Conditional jumps can be implemented just like a conditional Command but on a Schedule

exp_cond.append(sched1).c_if(reg_0, 1)

Analysis (V0.9)

Signal

Variety of signal analysis methods to be completed

Kerneling

Provide framework and basic implementation to be completed

Discrimination

Provide framework and basic implementation to be completed

Pulse API Layout (to be updated)

image

taalexander commented 5 years ago

Closing as API sans-scheduler has been implemented.