Describing and manipulating time-dependent schedules¶

Time-dependent Hamiltonians are implemented by objects of the type Schedule, which allow one to describe Hamiltonians decomposed as:

$$ H(t) = \sum_i \lambda_i(t) H_i $$

with $\lambda_i(t)$ a time-dependent coefficient and $H_i$ a Hermitian operator (implemented as an Observable object).

Schedules can be used to create jobs to be submitted to analog QPUs.

Building schedules¶

Schedule objects are specified using a collection of pairs of time-dependent coefficients and Observable objects. Time dependent coefficients are simply arithmetic expressions (built from a Variable) with a possible open parameter representing the time dependence (usually a variable t). This collection of pairs is called a drive.

Here is a simple example that constructs a schedule containing the time-dependent Hamiltonian:

$$ H(t) = (1 - t) \sigma_0^{z} + t \sigma_0^{x} $$

In [1]:
from qat.core import Variable, Schedule, Observable, Term
    
t_variable = Variable("t")
schedule = Schedule(drive=[(1 - t_variable, Observable(1, pauli_terms=[Term(1, 'Z', [0])])),
                           (t_variable, Observable(1, pauli_terms=[Term(1, 'X', [0])]))],
                    tmax=23.0)

print(schedule)
drive:
(1 - t) * 1 * (Z|[0])
t * 1 * (X|[0])
tmax = 23.0

As you can see, one also needs to provide the time during which the schedule is defined (the tmax parameter).

All scalars (i.e coefficients, tmax, etc) can be abstract arithmetic expressions:

In [2]:
from qat.core import Variable, Schedule, Observable, Term
    
t_variable = Variable("t")
tmax_variable = 15 * Variable("tmax") - 5
schedule = Schedule(drive=[(1 - t_variable, Observable(1, pauli_terms=[Term(1, 'Z', [0])])),
                           (t_variable, Observable(1, pauli_terms=[Term(1, 'X', [0])]))],
                    tmax=tmax_variable)

print(schedule, "\n")
print(schedule(tmax=10))
drive:
(1 - t) * 1 * (Z|[0])
t * 1 * (X|[0])
tmax = ((15 * tmax) - 5) 

drive:
(1 - t) * 1 * (Z|[0])
t * 1 * (X|[0])
tmax = 145

Drives can also be declared using an Observable with time-dependent coefficients:

In [3]:
from qat.core import Variable, Schedule, Observable, Term
    
t_variable = Variable("t")
hamiltonian = (1 - t_variable) * Observable(1, pauli_terms=[Term(1, 'Z', [0])]) + \
    t_variable * Observable(1, pauli_terms=[Term(1, 'X', [0])])
schedule = Schedule(drive=hamiltonian,
                    tmax=23.0)
print(schedule)
drive:
1 * (1 - t) * (Z|[0]) +
t * (X|[0])
tmax = 23.0

Manipulating schedules¶

The Schedule class has some overloaded operators which allow you to manipulate them efficiently.

Temporal composition¶

Two schedules can be temporally composed using the pipe/or operator:

In [4]:
from qat.core import Variable, Observable, Schedule, Term

t_variable = Variable("t")
schedule1 = Schedule(drive=(1 - t_variable) * Observable(1, pauli_terms=[Term(1, 'Z', [0])]),
                     tmax=2.0)
schedule2 = Schedule(drive=t_variable * Observable(1, pauli_terms=[Term(1, 'X', [0])]),
                     tmax=3.0)

print(schedule1 | schedule2)
drive:
heaviside(t,0,2.0) * (1 - t) * (Z|[0])
heaviside(t,2.0,5.0) * (t - 2.0) * (X|[0])
tmax = 5.0

Note how the coefficients are ponderated by a heaviside signal to filter the ranges of the two schedules.

Parallel composition¶

Two schedules can be merged into a single schedule containing the sum of the two drives using an addition.

In [5]:
from qat.core import Variable, Observable, Schedule, Term

t_variable = Variable("t")
schedule1 = Schedule(drive=(1 - t_variable) * Observable(1, pauli_terms=[Term(1, 'Z', [0])]),
                     tmax=2.0)
schedule2 = Schedule(drive= t_variable * Observable(1, pauli_terms=[Term(1, 'X', [0])]),
                     tmax=3.0)

print(schedule1 + schedule2)
drive:
heaviside(t,0,2.0) * (1 - t) * (Z|[0])
heaviside(t,0,3.0) * t * (X|[0])
tmax = 3.0

Rescaling a schedule¶

A Schedule can be rescaled via multiplication by a scalar:

In [6]:
from qat.core import Variable, Observable, Schedule, Term

t_variable = Variable("t")
schedule1 = Schedule(drive=(1 - t_variable) * Observable(1, pauli_terms=[Term(1, 'Z', [0])]),
                     tmax=2.0)

print(45 * schedule1, "\n")
print(Variable("foo") * schedule1)
Reverse mult 45 * drive:
1 * (1 - t) * (Z|[0])
tmax = 2.0
drive:
45 * (1 - t) * (Z|[0])
tmax = 2.0 

Reverse mult foo * drive:
1 * (1 - t) * (Z|[0])
tmax = 2.0
drive:
foo * (1 - t) * (Z|[0])
tmax = 2.0

Time translation of a schedule¶

A Schedule can be delayed (in the past or the future) by using the bit shift operators << and >>:

In [7]:
from qat.core import Variable, Observable, Schedule, Term

t_variable = Variable("t")
schedule1 = Schedule(drive=(1 - t_variable) * Observable(1, pauli_terms=[Term(1, 'Z', [0])]),
                     tmax=2.0)

print(schedule1 >> 3)
print(schedule1 << Variable('bar'))
drive:
heaviside(t,3,5.0) * (1 - (t - 3)) * (Z|[0])
tmax = 5.0
drive:
heaviside(t,-(bar),(2.0 + -(bar))) * (1 - (t - -(bar))) * (Z|[0])
tmax = (2.0 + -(bar))

Analog Jobs¶

Similarly to quantum circuits, schedules can be turned into jobs using the to_job method:

In [8]:
from qat.core import Variable, Observable, Schedule, Term
    
t_variable = Variable("t")
schedule = Schedule(drive=(1 - t_variable) * Observable(1, pauli_terms=[Term(1, 'Z', [0])]),
                    tmax=2.0)

# To simply sample the final state in the computational basis
job = schedule.to_job()

# To evaluate some observable at the end of the computation
job = schedule.to_job(observable=Observable(1, pauli_terms=[Term(1, 'Z', [0])]))

This method takes more or less the same arguments as the quantum circuit's method with the same name.

One important difference to notice: it is possible to change the starting state of the computation using the psi_0 argument:

In [9]:
import numpy as np
from qat.core import Variable, Observable, Schedule, Term

t_variable = Variable("t")
schedule = Schedule(drive=(1 - t_variable) * (Observable(2, pauli_terms=[Term(1, 'Z', [0])]) +
                                              Observable(2, pauli_terms=[Term(1, 'Z', [1])])),
                    tmax=2.0)

# Starting from |++> state
job = schedule.to_job(psi_0='++')

# Starting from |+1> state
job = schedule.to_job(psi_0='+1')

# Starting from a random initial state (simulator only)
vec = np.random.random(4)
vec /= np.linalg.norm(vec)
job = schedule.to_job(psi_0=vec)
In [ ]: