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])