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} $$
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:
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:
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
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.
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:
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 >>
:
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:
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:
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)