How to Continue Making a Wormhole

Part 3: A Really Existing Quantum Computer

Here's a quick example using Rigetti's quantum computer. You go on their site: rigetti.com and make an account. The idea is you have a classical virtual server sitting next to the quantum lattices. You can ssh in, put code to it. Meanwhile, you reserve a lattice at a certain time using their web interface. And when your time arrives, your classical server knows about it, and if you call "get_qc" in your python code with the lattice name, then your program runs on the quantum hardware. They have pretty good documentation:

https://www.rigetti.com/qcs-docs

http://docs.rigetti.com/en/stable/

https://grove-docs.readthedocs.io/en/latest/

When you run things offline, you have to first make sure you do

qvm -S

quilc -S

which starts the quantum virtual machine and the compiler. The idea is you can specify your quantum program in terms of a circuit of unitary matrices and measurements. Then this is compiled down into local gates, etc, which are optimized for the specific lattice structure you're running on, wherein only certain qubits can interact. Moreover your python defined program is compiled into an intermediate language.

So here we just do a simple wormhole. We just have to translate over our qutip operators into forest operators, which require definitions, etc. We use grove's "create_arbitrary_state" to get a unitary that takes us from the fixed initial state to the TFD. In the end, we just measure the output qubit, and repeat the experiment many times.

Now I'm sure there are much much more elegant ways of translating the wormhole stuff into something really tuned to Rigetti's system, but that's for the future. As it is, if I do any more qubits that 5 total, the compiler/qvm system times out. You'll find a very weak effect. But that's all we can do for now!

It's also recommended to use pytket which allows your quantum programs to run on many of the currently available quantum computing platforms.


In [ ]:
import numpy as np
import qutip as qt
from qutip.qip.operations import swap

from pyquil import Program
from pyquil.quil import DefGate
from pyquil.gates import MEASURE, SWAP
from pyquil import Program
from pyquil import get_qc
import grove
import grove.alpha.arbitrary_state.arbitrary_state

####################################################################################################

def construct_tfd(E, beta=0):
    El, Ev = E.eigenstates()
    return (1/np.sqrt((-beta*E).expm().tr()))*\
             sum([np.exp(-(1/2)*beta*l)*\
                 qt.tensor(Ev[i].conj(), Ev[i])\
                    for i, l in enumerate(El)])

def construct_coupling(n):
    return\
    (1/(n-1))*sum([qt.tensor(*[qt.sigmaz()\
                       if j == i else qt.identity(2) \
                            for j in range(2*n)])*
                   qt.tensor(*[qt.sigmaz()\
                        if j == i+n else qt.identity(2) \
                            for j in range(2*n)])
                                for i in range(n)])

def optimize_g(n, t, state, E, V, ID2, IDn):
    def prep_worm(n, t, E, ID2, IDn):
        U1 = qt.tensor(qt.identity(2), (1j*E*t).expm(), IDn)
        U2 = swap(N=2*n+1, targets=[1,2])
        U3 = qt.tensor(qt.identity(2), (-1j*E*t).expm(), IDn)
        U5 = qt.tensor(qt.identity(2), IDn, (-1j*E.trans()*t).expm())
        return [U5, U3*U2*U1]
    def worm(n, g, V, state, ops, ID2):
        A, B = ops
        U4 = qt.tensor(qt.identity(2), (1j*g*V).expm())
        state = A*U4*B*state
        return qt.expect(qt.sigmaz(), state.ptrace(n+1))
    G = np.linspace(0, 15, 60)
    worm_prep = prep_worm(n, t, E, ID2, IDn)
    Z = [worm(n, g, V, state, worm_prep, ID2) for g in G]
    g = G[np.argmin(np.array(Z))]
    return g

####################################################################################################

n = 2
N = 2*n + 1
beta = 0
t = 10

ID2 = qt.identity(2)
IDn = qt.identity(2**n)
IDn.dims = [[2]*n, [2]*n]
E = qt.rand_herm(2**n)
E.dims = IDn.dims
tfd = construct_tfd(E, beta=beta)
tfd.dims = [[2]*(2*n), [1]*(2*n)]

msg = qt.basis(2,0)
state = qt.tensor(msg, tfd)
V = construct_coupling(n)
g = optimize_g(n, t, state, E, V, ID2, IDn)

####################################################################################################

CREATE = grove.\
alpha.\
arbitrary_state.\
arbitrary_state.\
create_arbitrary_state(state.full().T[0])

LBACK_ = (1j*E*t).expm()
LBACK_DEF = DefGate("LBACK", LBACK_.full())
LBACK = LBACK_DEF.get_constructor()

LFORWARD_ = (-1j*E*t).expm()
LFORWARD_DEF = DefGate("LFORWARD", LFORWARD_.full())
LFORWARD = LFORWARD_DEF.get_constructor()

def coupling(g, V):
    V_ = (1j*g*V).expm().full()
    V_DEF = DefGate("V", V_)
    V = V_DEF.get_constructor()
    return (V_DEF, V)

RFORWARD_ = (-1j*E.trans()*t).expm()
RFORWARD_DEF = DefGate("RFORWARD", RFORWARD_.full())
RFORWARD = RFORWARD_DEF.get_constructor()

def wormhole(g):
    global CREATE, V, LBACK_DEF, LFORWARD_DEF, RFORWARD_DEF,\
            LBACK, LFORWARD, RFORWARD, N, n
    COUPLING_DEF, COUPLING = coupling(g, V)
    p = Program()
    ro = p.declare('ro', 'BIT', 1)
    p += LBACK_DEF
    p += LFORWARD_DEF
    p += COUPLING_DEF
    p += RFORWARD_DEF
    p += CREATE
    p += LBACK(*range(1, n+1))
    p += SWAP(0, 1)
    p += LFORWARD(*range(1, n+1))
    p += COUPLING(*range(1, 2*n+1))
    p += RFORWARD(*range(n+1, 2*n+1))
    p += MEASURE(n+1, ro[0])
    p.wrap_in_numshots_loop(1000)
    return p

W = wormhole(g)
qc = get_qc('%dq-qvm' % N)  # You can make any 'nq-qvm' this way for any reasonable 'n'
executable = qc.compile(W)
result = qc.run(executable)
nonzero = np.count_nonzero(result)
R = -1*(nonzero/len(tresult)) + 1*((len(result)-nonzero)/len(result))
print(R)