VQE on 1D TFIM#

Overview#

The main aim of this tutorial is not about the physics perspective of VQE, instead, we demonstrate the main ingredients of TensorCircuit by this simple VQE toy model.

Background#

Basically, we train a parameterized quantum circuit with repetitions of \(e^{i\theta} ZZ\) and \(e^{i\theta X}\) layers as \(U(\rm{\theta})\). And the objective to be minimized is this task is \(\mathcal{L}(\rm{\theta})=\langle 0^n\vert U(\theta)^\dagger H U(\theta)\vert 0^n\rangle\). The Hamiltonian is from TFIM as \(H = \sum_{i} Z_iZ_{i+1} -\sum_i X_i\).

Setup#

[1]:
from functools import partial
import numpy as np
import tensorflow as tf
import jax
from jax.config import config

config.update("jax_enable_x64", True)
from jax import numpy as jnp
from jax.experimental import optimizers
import tensorcircuit as tc

To enable automatic differentiation support, we should set the TensorCircuit backend beyond the default one “NumPy”. And we can also set the high precision complex128 for the simulation.

[2]:
tc.set_backend("tensorflow")
tc.set_dtype("complex128")
[3]:
print(
    "complex dtype of simulation:",
    tc.dtypestr,
    "\nreal dtype of simulation:",
    tc.rdtypestr,
    "\nbackend package of simulation:",
    tc.backend.name,
)
complex dtype of simulation: complex128
real dtype of simulation: float64
backend package of simulation: tensorflow
[4]:
# zz gate matrix to be utilized
zz = np.kron(tc.gates._z_matrix, tc.gates._z_matrix)
print(zz)
[[ 1.  0.  0.  0.]
 [ 0. -1.  0. -0.]
 [ 0.  0. -1. -0.]
 [ 0. -0. -0.  1.]]

Higher-level API#

We first design the Hamiltonian energy expectation function with the input as quantum circuit.

[5]:
def tfi_energy(c: tc.Circuit, j: float = 1.0, h: float = -1.0):
    e = 0.0
    n = c._nqubits
    for i in range(n):
        e += h * c.expectation((tc.gates.x(), [i]))  # <X_i>
    for i in range(n - 1):  # OBC
        e += j * c.expectation(
            (tc.gates.z(), [i]), (tc.gates.z(), [(i + 1) % n])
        )  # <Z_iZ_{i+1}>
    return tc.backend.real(e)

Now we make the quantum function with \(\rm{\theta}\) as input and energy expectation \(\mathcal{L}\) as output.

[6]:
def vqe_tfim(param, n, nlayers):
    c = tc.Circuit(n)
    paramc = tc.backend.cast(
        param, tc.dtypestr
    )  # We assume the input param with dtype float64
    for i in range(n):
        c.H(i)
    for j in range(nlayers):
        for i in range(n - 1):
            c.exp1(i, i + 1, unitary=zz, theta=paramc[2 * j, i])
        for i in range(n):
            c.rx(i, theta=paramc[2 * j + 1, i])
    e = tfi_energy(c)
    return e

To train the parameterized circuit, we should utilize the gradient information \(\frac{\partial \mathcal{L}}{\partial \rm{\theta}}\) with gradient descent. We also use jit to wrap the value and grad function for a substantial speedup. Note how (1, 2) args of vqe_tfim are labeled as static since they are just integers for qubit number and layer number instead of tensors.

[7]:
vqe_tfim_vag = tc.backend.jit(
    tc.backend.value_and_grad(vqe_tfim), static_argnums=(1, 2)
)
[8]:
def train_step_tf(n, nlayers, maxiter=10000):
    param = tf.Variable(
        initial_value=tf.random.normal(
            shape=[nlayers * 2, n], stddev=0.1, dtype=getattr(tf, tc.rdtypestr)
        )
    )
    opt = tf.keras.optimizers.Adam(1e-2)
    for i in range(maxiter):
        e, grad = vqe_tfim_vag(param, n, nlayers)
        opt.apply_gradients([(grad, param)])
        if i % 200 == 0:
            print(e)
    return e


train_step_tf(6, 3, 2000)
tf.Tensor(-5.2044235531710905, shape=(), dtype=float64)
tf.Tensor(-7.168460907768175, shape=(), dtype=float64)
tf.Tensor(-7.229007202330065, shape=(), dtype=float64)
tf.Tensor(-7.2368387790165105, shape=(), dtype=float64)
tf.Tensor(-7.246179597523659, shape=(), dtype=float64)
tf.Tensor(-7.262673966580785, shape=(), dtype=float64)
tf.Tensor(-7.286129364991173, shape=(), dtype=float64)
tf.Tensor(-7.291252895716095, shape=(), dtype=float64)
tf.Tensor(-7.2930457160020765, shape=(), dtype=float64)
tf.Tensor(-7.293225326335964, shape=(), dtype=float64)
[8]:
<tf.Tensor: shape=(), dtype=float64, numpy=-7.293297606006469>

Batched VQE Example#

We can even run a batched version of VQE optimization, namely, we simultaneously optimize parameterized circuits for different random initializations, so that we can try our best to avoid local minimums and locate the best of the converged energies.

[9]:
vqe_tfim_vvag = tc.backend.jit(
    tc.backend.vectorized_value_and_grad(vqe_tfim), static_argnums=(1, 2)
)
[10]:
def batched_train_step_tf(batch, n, nlayers, maxiter=10000):
    param = tf.Variable(
        initial_value=tf.random.normal(
            shape=[batch, nlayers * 2, n], stddev=0.1, dtype=getattr(tf, tc.rdtypestr)
        )
    )
    opt = tf.keras.optimizers.Adam(1e-2)
    for i in range(maxiter):
        e, grad = vqe_tfim_vvag(param, n, nlayers)
        opt.apply_gradients([(grad, param)])
        if i % 200 == 0:
            print(e)
    return e


batched_train_step_tf(16, 6, 3, 2000)
tf.Tensor(
[-4.56780182 -5.32411397 -5.34948039 -5.49728838 -5.51974631 -4.89464895
 -5.23113926 -5.70097167 -5.4384308  -5.27898261 -4.73926061 -5.43748391
 -5.02246224 -4.46749643 -5.34320604 -5.29828815], shape=(16,), dtype=float64)
tf.Tensor(
[-7.15906597 -7.20867528 -7.16615816 -7.16164269 -7.15427498 -7.17176534
 -7.15677645 -7.19769858 -7.1876547  -7.17160745 -7.14313137 -7.16458417
 -7.12556993 -7.1043696  -7.17233218 -7.17955502], shape=(16,), dtype=float64)
tf.Tensor(
[-7.22332735 -7.28775096 -7.22854626 -7.28800389 -7.22006811 -7.2773814
 -7.22241623 -7.23446324 -7.23115651 -7.23081143 -7.25399986 -7.26564648
 -7.16463543 -7.27854832 -7.23574558 -7.28935649], shape=(16,), dtype=float64)
tf.Tensor(
[-7.23956454 -7.29093555 -7.23464822 -7.2914774  -7.22326999 -7.29014637
 -7.24891067 -7.2505597  -7.23879431 -7.23826618 -7.28737831 -7.29193732
 -7.22649018 -7.29136679 -7.25276205 -7.29214669], shape=(16,), dtype=float64)
tf.Tensor(
[-7.24561853 -7.29413883 -7.23950499 -7.29230127 -7.22749993 -7.29051998
 -7.28702174 -7.289441   -7.25016979 -7.26370483 -7.29320874 -7.29451577
 -7.22882824 -7.29213765 -7.27040912 -7.29358236], shape=(16,), dtype=float64)
tf.Tensor(
[-7.24997971 -7.294748   -7.25420008 -7.29271584 -7.24577837 -7.29082466
 -7.29171805 -7.29016935 -7.28645108 -7.29170429 -7.29499124 -7.29520514
 -7.23115011 -7.29305292 -7.28793637 -7.2949226 ], shape=(16,), dtype=float64)
tf.Tensor(
[-7.25300306 -7.29512508 -7.28240557 -7.29287622 -7.28264095 -7.29125472
 -7.29399162 -7.29066326 -7.29233232 -7.29290676 -7.29521188 -7.29530935
 -7.23475933 -7.29429836 -7.29053038 -7.29559969], shape=(16,), dtype=float64)
tf.Tensor(
[-7.25706762 -7.29527205 -7.29168082 -7.29292216 -7.29221412 -7.29183888
 -7.29474989 -7.29119684 -7.29306204 -7.29300514 -7.29525079 -7.29538907
 -7.24226472 -7.29544118 -7.29086393 -7.29576418], shape=(16,), dtype=float64)
tf.Tensor(
[-7.26218683 -7.29529443 -7.29438024 -7.29294969 -7.29426403 -7.29243576
 -7.29491073 -7.29171828 -7.29384631 -7.29301505 -7.29527304 -7.29545727
 -7.25772904 -7.29581457 -7.2910381  -7.29582192], shape=(16,), dtype=float64)
tf.Tensor(
[-7.26654138 -7.29529938 -7.29515899 -7.29297612 -7.29499748 -7.29281659
 -7.29500531 -7.29227399 -7.29455691 -7.29302087 -7.29529063 -7.29551808
 -7.2892432  -7.29593328 -7.29120759 -7.29585771], shape=(16,), dtype=float64)
[10]:
<tf.Tensor: shape=(16,), dtype=float64, numpy=
array([-7.29011428, -7.29530356, -7.29549915, -7.29300424, -7.29529224,
       -7.29296047, -7.29508027, -7.29270021, -7.29499648, -7.29302725,
       -7.29530574, -7.2955733 , -7.29593608, -7.29604964, -7.29138482,
       -7.29589095])>

Different Backends#

We can change the backends at runtime without even changing one line of the code!

However, in normal user cases, we strongly recommend the users stick to one backend in one jupyter or python script. One can enjoy the facility provided by other backends by changing the set_backend line and running the same script again. This approach is much safer than using multiple backends in the same file unless you know the lower-level details of TensorCircuit enough.

[11]:
tc.set_backend("jax")  # change to jax backend
[12]:
vqe_tfim_vvag = tc.backend.jit(
    tc.backend.vectorized_value_and_grad(vqe_tfim), static_argnums=(1, 2)
)


def batched_train_step_jax(batch, n, nlayers, maxiter=10000):
    key = jax.random.PRNGKey(42)
    param = jax.random.normal(key, shape=[batch, nlayers * 2, n]) * 0.1
    opt_init, opt_update, get_params = optimizers.adam(step_size=1e-2)
    opt_state = opt_init(param)

    def update(i, opt_state):
        param = get_params(opt_state)
        (value, gradient) = vqe_tfim_vvag(param, n, nlayers)
        return value, opt_update(i, gradient, opt_state)

    for i in range(maxiter):
        value, opt_state = update(i, opt_state)
        param = get_params(opt_state)
        if i % 200 == 0:
            print(value)
    return value


batched_train_step_jax(16, 6, 3, 2000)
WARNING:absl:No GPU/TPU found, falling back to CPU. (Set TF_CPP_MIN_LOG_LEVEL=0 and rerun for more info.)
[-5.67575948 -5.44768444 -5.7821556  -5.36699503 -5.00485098 -5.59416181
 -5.13421084 -5.70462279 -5.73699416 -5.25819658 -4.70729299 -5.82823766
 -5.69154358 -5.51112311 -5.46091316 -5.31649863]
[-7.16831387 -7.17873365 -7.21905991 -7.17714641 -7.21910053 -7.17729778
 -7.23594046 -7.1978075  -7.2311691  -7.18566164 -7.15141273 -7.1760751
 -7.20727055 -7.22174427 -7.15227955 -7.15343225]
[-7.24047827 -7.23486717 -7.26382185 -7.25267406 -7.23938877 -7.24135079
 -7.28655961 -7.24413064 -7.28070556 -7.24825735 -7.23400189 -7.25234153
 -7.25756263 -7.2505181  -7.22647645 -7.2589444 ]
[-7.28642159 -7.23707926 -7.28988032 -7.28627451 -7.28716418 -7.25068739
 -7.29122589 -7.2510777  -7.2906953  -7.25976327 -7.23891735 -7.29227009
 -7.28973637 -7.26238069 -7.245065   -7.29155041]
[-7.29198674 -7.24196434 -7.29188725 -7.29243688 -7.2926968  -7.26254168
 -7.29233808 -7.26729904 -7.29277165 -7.28066403 -7.24315235 -7.29344766
 -7.2920645  -7.26717433 -7.26959622 -7.29307748]
[-7.29320541 -7.27162341 -7.29245991 -7.2934821  -7.29360574 -7.27103573
 -7.29311302 -7.29213505 -7.29356392 -7.29162927 -7.24981922 -7.29384423
 -7.2930642  -7.27323089 -7.29156252 -7.293684  ]
[-7.29384132 -7.29130123 -7.29333191 -7.29442748 -7.29396942 -7.29011734
 -7.2936549  -7.2929216  -7.29430937 -7.29288465 -7.27758063 -7.29446428
 -7.2939575  -7.27958774 -7.29260011 -7.29408982]
[-7.29433271 -7.29273603 -7.29467579 -7.29507754 -7.29438517 -7.29298445
 -7.2940647  -7.29297896 -7.29494014 -7.29348052 -7.29142477 -7.29524322
 -7.29455267 -7.28904449 -7.29319333 -7.29437335]
[-7.29480095 -7.29294578 -7.29507043 -7.29530148 -7.29479745 -7.29390014
 -7.29439439 -7.29304737 -7.29520739 -7.29397645 -7.29276852 -7.2955135
 -7.29492853 -7.29145529 -7.29390729 -7.29462868]
[-7.29530498 -7.29299558 -7.29520643 -7.29537379 -7.29505127 -7.2945667
 -7.29466477 -7.29312996 -7.29529573 -7.29447499 -7.29327567 -7.29560796
 -7.29520605 -7.29179062 -7.29484071 -7.29489978]
[12]:
DeviceArray([-7.29575181, -7.29302602, -7.29529976, -7.29541094,
             -7.29517778, -7.29488194, -7.29487651, -7.29323608,
             -7.29532772, -7.29494698, -7.29369784, -7.29567791,
             -7.29534388, -7.29187906, -7.29536221, -7.29516005],            dtype=float64)

Lower-level API#

The higher-level API under the namespace of TensorCircuit provides a unified framework to do linear algebra and automatic differentiation which is backend agnostic.

One may also use the related APIs (ops, AD-related, jit-related) directly provided by TensorFlow or Jax, as long as one is ok to stick with one fixed backend. See the tensorflow backend example below.

[13]:
tc.set_backend("tensorflow")
[14]:
def tfi_energy(c: tc.Circuit, j: float = 1.0, h: float = -1.0):
    e = 0.0
    n = c._nqubits
    for i in range(n):
        e += h * c.expectation((tc.gates.x(), [i]))  # <X_i>
    for i in range(n - 1):  # OBC
        e += j * c.expectation(
            (tc.gates.z(), [i]), (tc.gates.z(), [(i + 1) % n])
        )  # <Z_iZ_{i+1}>
    return tf.math.real(e)


def vqe_tfim(param, n, nlayers):
    c = tc.Circuit(n)
    paramc = tf.cast(param, tf.complex128)
    for i in range(n):
        c.H(i)
    for j in range(nlayers):
        for i in range(n - 1):
            c.exp1(i, i + 1, unitary=zz, theta=paramc[2 * j, i])
        for i in range(n):
            c.rx(i, theta=paramc[2 * j + 1, i])
    e = tfi_energy(c)
    return e


@tf.function
def vqe_tfim_vag(param, n, nlayers):
    with tf.GradientTape() as tape:
        tape.watch(param)
        v = vqe_tfim(param, n, nlayers)
    grad = tape.gradient(v, param)
    return v, grad
[15]:
train_step_tf(6, 3, 2000)
tf.Tensor(-5.5454151788179376, shape=(), dtype=float64)
tf.Tensor(-7.167693061786028, shape=(), dtype=float64)
tf.Tensor(-7.254761404891117, shape=(), dtype=float64)
tf.Tensor(-7.290050014550046, shape=(), dtype=float64)
tf.Tensor(-7.29133881232428, shape=(), dtype=float64)
tf.Tensor(-7.2918048286324915, shape=(), dtype=float64)
tf.Tensor(-7.292590929769901, shape=(), dtype=float64)
tf.Tensor(-7.294195015132205, shape=(), dtype=float64)
tf.Tensor(-7.295013538531699, shape=(), dtype=float64)
tf.Tensor(-7.2951174084838835, shape=(), dtype=float64)
[15]:
<tf.Tensor: shape=(), dtype=float64, numpy=-7.295170441938537>