# ---
# jupyter:
#   jupytext:
#     text_representation:
#       extension: .py
#       format_name: light
#       format_version: '1.5'
#       jupytext_version: 1.14.4
#   kernelspec:
#     display_name: Python 3
#     language: python
#     name: python3
# ---

# # Simple gate-level operations
# This notebook demonstrates some of the simple wrapping provided to examine the values of cascaded gate operations. Support is provided to manipulate the pybind11 wrapped matrices (DCM), or mapping them directly to numpy types, allowing use of the full scipy ecosystem.

# %matplotlib notebook
from matplotlib import rc
rc('text', usetex=True)
import matplotlib.pyplot as plt

# +
# Load the wrqpped simulator as before
from PyQNLPSimulator import PyQNLPSimulator as p

# Load the wrapped 2x2 matrix type; dcm => double complex matrix
from PyQNLPSimulator import DCMatrix as dcm

# Load the QNLP python packages
import QNLP as q

# Useful for examining various product types
from itertools import product

# Load numpy and matrix-based functions from scipy
import numpy as np
from scipy.linalg import expm, sinm, cosm
# -

# Define a matrix using lexicographical ordering
ll=[0,1,1,0]
m = dcm(ll)

# Using the matrix `m` we can perform a variety of oeprations using the underlying C++ wrapped methods. 
#
# Note: We do not restrict ourselves to these being unitary.

print(m*m)
print(m + m)
print(2*m - 5*m)

# Additionally, to avail of the numpy/scipy ecosystem, it makes sense to have conversions between the numpy datatypes and the above matrices. This allows using to perform a variety of useful operations.

# Create numpy variant
nm = m.as_numpy()

# Below we demonstrate comparisons between both types, and the operations available to each. We can load the matrices directly using the 'DCM' type, from the PyQNLPSimulator class 'GateOps', or access them from an initialised simulator object.

# +
from PyQNLPSimulator import GateOps

sim = p(3, False)

gops = GateOps(sim)

#Direct from simulator
X = sim.getGateX()
Y = sim.getGateY()
Z = sim.getGateZ()
H = sim.getGateH()

#GateOps access
Xgo = gops.X

#Numpy variant
Xnp = np.matrix(X.as_numpy()) 
Hnp_go = gops.Hnp
# -

print(H * X)
print(Hnp_go * Xnp)

# Using the numpy variants, we can explicitly pass our matrices to the matrix functions loaded from `scipy.linalg`. In this case, we calculate $\exp(iH)$, where $H$ is the Hadamard gate.

expm(1j*H.as_numpy())


# We can thus define rotation matrices from the above gate definitions using the $R_a(\theta) = \exp\left(-\frac{i\theta a}{2}\right)$ definition. The below listed methods are all available within the `GateOps` class

def RX(theta):
    return np.matrix(expm(-0.5*1j*theta*X.as_numpy()))
def RY(theta):
    return np.matrix(expm(-0.5*1j*theta*Y.as_numpy()))
def RZ(theta):
    return np.matrix(expm(-0.5*1j*theta*Z.as_numpy()))
def RHam(theta, Ham):
    return np.matrix(expm(-0.5*1j*theta*Ham.as_numpy()))


# Looking at the application of this matrix on a simple vector can be performed as follows:

for i in range(10):
    data = (RY(np.pi*(i/10))*np.array([[1],[0]]))
    print(data)

# We can visualise this rotation as follows:

arr = np.array([[1],[0]])
for i in range(10):
    arr = RY(np.pi/10)*arr
arr

bin(296)

# +
from mpl_toolkits.mplot3d import Axes3D
from matplotlib import cm, colors
fig = plt.figure(figsize=plt.figaspect(1))
ax = fig.add_subplot(111, projection='3d')

# Bloch sphere
r = 0.99
phi, theta = np.mgrid[0.0:np.pi:100j, 0.0:2.0*np.pi:100j]
x0 = r*np.sin(phi)*np.cos(theta)
y0 = r*np.sin(phi)*np.sin(theta)
z0 = r*np.cos(phi)

ax.plot_surface(x0, y0, z0, color='violet', linewidth=1.0, alpha=0.55)

data = np.array([[0],[1]])
arrow0 = ax.quiver(0,0,0, np.real(data[0]), 0, np.real(data[1]), length=1.0, color='black')

for i in range(10):
    data = (RY(np.pi*(i/10))*np.array([[1],[0]]))
    arrow = ax.quiver(0,0,0, np.real(data[0]), 0, np.real(data[1]), length=1.0)

    ax.set_xlim([-1,1])
    ax.set_ylim([-1,1])
    ax.set_zlim([-1,1])
    #ax.set_aspect("equal")
    plt.tight_layout()
    ax.grid(False)
    plt.pause(0.05)
    plt.draw()
# -

# To examine the product of these gates, as would be offered by the quantum circuit model, we can create tensor products as:

# We can, as demonstrated previously, raise these to exponents. Care should be taken though, as Kronecker products grow the memory footprint greatly.

s1 = np.kron(gops.Inp, gops.Znp)
s2 = 0.5*(gops.Znp + gops.Inp)
s3 = np.kron(Xnp, np.kron(s2,s1))

s3

# where the matrix `s3` is given by $\mathbf{I}\otimes(\vert 0 \rangle\langle 0 \vert )\otimes(\mathbf{I}\otimes\sigma_z)$.

aa = expm(-0.5*1j*np.pi*s3)

fig = plt.figure()

# +
plt.subplot(121)
plt.imshow(abs(aa)**2)
plt.colorbar()

plt.subplot(122)
plt.imshow(np.angle(aa)) 
plt.colorbar()

plt.show()


# -

# Next, we can demonstrate the usage of the `UnitaryFinder` class. As an example, say we are given an arbitrary 2x2 unitary matrix, and a set of gates implemenetd on our simulator/hardware platform. We cannot directly implement the given gate, but would like to decompose it (e.g. compile it) to the available gate-set. Using the matrix form given by `createS(i)`, we attempt to find the optimal gates to create this matrix. 

def createS(i):
    a = np.sqrt((i-1)/i)
    b = 1/np.sqrt(i)
    return dcm([a,b,-b,a])


np.matrix(createS(4).as_numpy()).flatten().tolist()[0]

from PyQNLPSimulator import UnitaryFinder

# Creating the UnitaryFinder object, we give the S matrix as an argument
uf = UnitaryFinder(sim, np.matrix(createS(4).as_numpy()).flatten().tolist()[0])

# Next, we generate all possible combinations of gates in the given gate-set (taken from GateOps) up to a given depth.

gate_op_combo = uf.genOps(uf.gates, depth=3)

print(gate_op_combo[0:4])

# Lastly, we iterate through this gate combination list, and aim to find the gate combinations that best match our target matrix, to within a given tolerance.

for i in gate_op_combo:
    res, vals = uf.findOps(i)
    if res:
        print(i, vals)

# The values returned above list the gate combinations (applied right to left) and the parameters (angles for rotation matrices) that best-approximated the given unitary.
#
# For our example above of S4, some simple arithmetic shows that $S4 = \sigma_x RY(\cos^{-1}(1/2))\sigma_x$. A further investigation gives us a relationship for $S_i = \sigma_x RY(\cos^{-1}((i-2)/2)))\sigma_x$ as  one of many possible combinations.



StarcoderdataJupyterScriptsDedupFiltered