Creating variational circuits

The Qaptiva framework comes with a collection of tools to efficiently describe and run variational quantum algorithm. This page introduces the basic mechanics allowing you to write variational schemes.

For a more advanced usage, combinatorial optimization defines a high-level interface to describe combinatorial optimization problems and automatically generate parametrized Ansätze.

Variational jobs

When running a variational quantum algorithm, we are, most of the time, interested in minimizing the energy of some observable \(H\) over some parametrized quantum state \(|\psi(\theta)\rangle\), i.e minimizing \(\langle \psi(\theta)| H |\psi(\theta) \rangle\).

We can build a variational quantum circuit by introducing open parameters in a pyAQASM Program:

from qat.lang import qrout, RY, RZ

@qrout
def circuit(theta):
    RY(theta)(0)
    RZ(4 * theta)(0)

print("Variables:", circuit.get_variables())

job = circuit.to_job()
print("Job variables:", job.get_variables())
Variables: ['theta']
Job variables: ['theta']
from qat.lang import Program, RY, RZ

prog = Program()
qbits = prog.qalloc(1)

variable = prog.new_var(float, "a")
RY(variable)(qbits)
RZ(4 * variable)(qbits)

circuit = prog.to_circ()

print("Variables:", circuit.get_variables())

job = circuit.to_job()
print("Job variables:", job.get_variables())
Variables: ['a']
Job variables: ['a']

Additionally, the sampled observable itself can have parametrized coefficients:

from qat.core import Observable, Term, Variable

t = Variable("t")

obs = Observable(3)
# Obs = \sum_i \sigma_x^i
for i in range(3):
    obs += (1 - t) * Observable.sigma_x(i, 3)

print(obs)
print("Observable variables:", obs.get_variables())

from qat.lang import qrout, RY, RZ

@qrout
def circuit(theta):
    for qbit_idx in range(3):
        RY(theta)(qbit_idx)
        RZ(4 * theta)(qbit_idx)

print("Circuit variables:", circuit.get_variables())

job = circuit.to_job(observable=obs)
print("Job variables:", job.get_variables())
(1 - t) * (X|[0]) +
(1 - t) * (X|[1]) +
(1 - t) * (X|[2])
Observable variables: ['t']
Circuit variables: ['theta']
Job variables: ['t', 'theta']
from qat.core import Observable, Term, Variable

t = Variable("t")

obs = Observable(3)
# Obs = \sum_i \sigma_x^i
for i in range(3):
    obs += (1 - t) * Observable.sigma_x(i, 3)

print(obs)
print("Observable variables:", obs.get_variables())

from qat.lang import Program, RY, RZ

prog = Program()
qbits = prog.qalloc(3)

variable = prog.new_var(float, "a")
for qbit in qbits:
    RY(variable)(qbit)
    RZ(4 * variable)(qbit)

circuit = prog.to_circ()

print("Circuit variables:", circuit.get_variables())

job = circuit.to_job(observable=obs)
print("Job variables:", job.get_variables())
(1 - t) * (X|[0]) +
(1 - t) * (X|[1]) +
(1 - t) * (X|[2])
Observable variables: ['t']
Circuit variables: ['a']
Job variables: ['a', 't']

This offers the possiblity to have layered parametrized optimization, or even compilation tradeoffs where some variational parameters end up in the sampled observable.

Binding variables

Once we’ve built a parametrized job, its variables can be instantiated using the overloaded __call__ operator:

import numpy as np
from qat.lang import qrout, RY, RZ
from qat.core import Observable, Term, Variable

# Step 1: defining observable
t = Variable("t")

obs = Observable(3)
for i in range(3):
    obs += (1 - t) * Observable.sigma_x(i, 3)

# Step 2: defining circuit
@qrout
def circuit(theta):
    for qbit_idx in range(3):
        RY(theta)(qbit_idx)
        RZ(4 * theta)(qbit_idx)

# Step 3: creating job and binding variables
job = circuit.to_job(observable=obs)

job_2 = job(t=0.5)
print(job_2.observable)

job_3 = job(** {v: np.random.random() for v in job.get_variables()})
print(job_3.observable)

for op in job_3.circuit.iterate_simple():
    print(op)
0.5 * (X|[0]) +
0.5 * (X|[1]) +
0.5 * (X|[2])
0.7417652664744875 * (X|[0]) +
0.7417652664744875 * (X|[1]) +
0.7417652664744875 * (X|[2])
('RY', [0.840632159792299], [0])
('RZ', [3.362528639169196], [0])
('RY', [0.840632159792299], [1])
('RZ', [3.362528639169196], [1])
('RY', [0.840632159792299], [2])
('RZ', [3.362528639169196], [2])
import numpy as np
from qat.lang import Program, RY, RZ
from qat.core import Observable, Term, Variable

# Step 1: defining observable
t = Variable("t")

obs = Observable(3)
for i in range(3):
    obs += (1 - t) * Observable.sigma_x(i, 3)

# Step 2: defining circuit
prog = Program()
qbits = prog.qalloc(3)

variable = prog.new_var(float, "a")
for qbit in qbits:
    RY(variable)(qbit)
    RZ(4 * variable)(qbit)

# Step 3: creating job and binding variables
job = prog.to_circ().to_job(observable=obs)

job_2 = job(t=0.5)
print(job_2.observable)

job_3 = job(** {v: np.random.random() for v in job.get_variables()})
print(job_3.observable)

for op in job_3.circuit.iterate_simple():
    print(op)
0.5 * (X|[0]) +
0.5 * (X|[1]) +
0.5 * (X|[2])
0.555755435605076 * (X|[0]) +
0.555755435605076 * (X|[1]) +
0.555755435605076 * (X|[2])
('RY', [0.34802875156509516], [0])
('RZ', [1.3921150062603806], [0])
('RY', [0.34802875156509516], [1])
('RZ', [1.3921150062603806], [1])
('RY', [0.34802875156509516], [2])
('RZ', [1.3921150062603806], [2])