Encoding data in superposition

If you want to encode data in a quantum register as a uniform superposition of states, this can be done with an algorithm proposed in a very readable 2001 paper by C.A. Trugenberger.

The algorithm utilizes three registers, a processing register to load the data, a memory register, that eventually stores the target state and a two bit auxiliary register. The main idea of the algorithm is to prepare the target state in a superposition with an auxiliary bit, rotate the state into the memory state to isolate it from the following rewinding of the operations.

The implementation in PennyLane, firstly some initialization…

import pennylane as qml
from pennylane import numpy as np

data = [0,2,5]
reg_size = int(np.ceil(np.log2(np.max(data)+1)))

processing_reg = qml.wires.Wires(range(reg_size))
utility_reg = qml.wires.Wires(range(reg_size, reg_size+2))
memory_reg = qml.wires.Wires(range(reg_size+2, 2*reg_size+2))

all_wires = qml.wires.Wires.all_wires([processing_reg, utility_reg, memory_reg])
n_wires = len(all_wires)

dev = qml.device("default.qubit", wires=all_wires)

Then a small helper function to encode data, into the processing register …

def load_data(n, wires=processing_reg, ctrl_wire=utility_reg[1]):
    
    assert len(wires) == reg_size, f"wires expected a list of size {reg_size}, got {len(wires)}."
    
    n_bin_str = np.binary_repr(n, width=reg_size)
    n_bin = [int(s) for s in n_bin_str]
    
    for i, v in enumerate(n_bin):
        if v:
            qml.PauliX(wires[i])

Eventually the center piece

@qml.qnode(dev)
def circuit():    
    qml.PauliX(wires=utility_reg[1])
    
    for m, value in enumerate(data):
        # initial state |data,01,000>
        load_data(value)
        
        for i in range(reg_size):
            qml.Toffoli(wires=[processing_reg[i], utility_reg[1], memory_reg[i]])
        
        for i in range(reg_size):
            qml.CNOT(wires=[processing_reg[i], memory_reg[i]])
            qml.PauliX(wires=memory_reg[i])
        
        qml.MultiControlledX(control_wires=memory_reg, wires=[utility_reg[0]])
        
        mu = len(data) - m
        angle = np.pi if mu == 1 else 2.*np.arctan(-np.sqrt(1/(mu-1)))
        qml.CRY(angle, wires=utility_reg)
        
        qml.MultiControlledX(control_wires=memory_reg, wires=[utility_reg[0]])
        
        for i in range(reg_size):
            qml.CNOT(wires=[processing_reg[i], memory_reg[i]])
            qml.PauliX(wires=memory_reg[i])
        
        for i in range(reg_size):
            qml.Toffoli(wires=[processing_reg[i], utility_reg[1], memory_reg[i]])
        
        load_data(value)
    
    return qml.probs(wires=memory_reg)

The algorithm creates correct uniform superpositions for different input data-sets.

Retrieving data, will be the next step.

QHack 2023: 4 week prep-challenge, day 23, Draper adder

I went for another QOSF task (Oct. 2022), there I tackled the seemingly easy task of implementing a Draper adder, as an intermediate step towards a multiplier. However, I got lost in index engineering. At this point, I am not satisfied with the solution, but the tests check out, so it is usable.

I added two quantum registers; instead of the much easier multiplication of a quantum with a classical register.

import pennylane as qml
from pennylane import numpy as np

reg_size = 4
n_wires = 2*reg_size

dev = qml.device("default.qubit", wires=n_wires)

def base_encode(a, wires_offset=0):
    a_bin = np.binary_repr(a, reg_size)
    a_bin = a_bin if wires_offset else reversed(a_bin)
    
    for i, ai in enumerate(a_bin):
        if ai == "1":
            qml.PauliX(i + wires_offset)

@qml.qnode(dev)
def circuit(a, b):
    
    # encode basis state
    base_encode(a)
    base_encode(b, wires_offset=reg_size)
    
    # apply QFT to one register
    qml.QFT(wires=range(reg_size, n_wires))
    
    # in this implementation, I pretend that I do not know the value of b, 
    # this could be the case, if this addition was amidst a longer calculation
    for i in range(reg_size): # ith significant bit (target)
        for j in range(reg_size-i): # control bits
            trgt_idx = n_wires - 1 - i
            ctrl_idx = trgt_idx - reg_size - j
            angle = np.pi/(2**j)
            qml.ControlledPhaseShift(angle, wires=[ctrl_idx, trgt_idx])
    
    # invert QFT
    qml.adjoint(qml.QFT)(wires=range(reg_size, n_wires))
    
    return qml.probs(wires=range(reg_size, n_wires))# [qml.expval(qml.PauliZ(i)) for i in range(reg_size, n_wires)]

As already mentioned, I wouldn’t trust the implementation, if the tests didn’t check out, but they do.

circ_draw = qml.draw(circuit)

res = np.array([])
for a in range(reg_size**2 - 1):
    for b in range(reg_size**2 - 1 -a):
        sample = circuit(a, b)
        expected_result = a+b
        if np.argmax(sample)!=expected_result:
            print(f"a: {a}, b: {b}, sample: {np.argmax(sample)}")
            print(circ_draw(a, b))
            break
        
        res = np.append(res, [np.argmax(sample)==a+b])
        
print(f"The algorithm is correct! {len(res)} tests successful!" if np.all(res) else "There is an error")

QHack 2023: 4 week prep-challenge, day 22, Constructing diagonal operators, pt. 2

After implementing the basic level task of creating a 2×2 diagonal unitary operator (i.e. only possible values in the diagonal are +1 and -1), today I am generalizing the algorithm to create arbitrary diagonal unitary operators, with complex eigenvalues. For this purpose I used the multi-controlled P-gate, which differs slightly from the RZ-gate:

\[ RZ(\phi) = \left( \begin{array}{rr} e^{-i \frac{\phi}{2}} & 0 \\ 0 & e^{i \frac{\phi}{2}} \end{array} \right) \text{, and} \\ P(\phi) = \left( \begin{array}{rr} 1 & 0 \\ 0 & e^{i \phi} \end{array} \right) \]

In code, most remained the same to last day’s solution, I just had to modify the marking function from a CZ to a multi-controlled P-gate.

import pennylane as qml
from pennylane import numpy as np

num_wires = 3
dev = qml.device("default.qubit", wires=num_wires)

def prepare_address(address):
    """Flip bits, to prepare addressation by oracle, or the like.
    """
    bin_address = np.binary_repr(address, width=num_wires)
    
    for i, addr_bit in enumerate(bin_address):
        if addr_bit == "0":
            qml.PauliX(i)


@qml.qnode(dev)
def prepare_unitary(u_eigenvalues):
    """Prepare a circuit, that realizes a unitary transformation, with a diagonal matrix representation, with the provided eigenvalues.
    
    Args: 
        - u_eigenvalues (np.array(complex)): Array that holds a list of unitary eigenvalues of the form np.exp(1.j*alpha), i.e. complex, norm 1
    
    Return: 
        - qml.State
    """
    assert np.all(np.isclose(np.absolute(u_eigenvalues), 1.)), "Invalid eigenvalues provided, do not have norm of 1"
    
    angles = np.angle(u_eigenvalues)
    
    for i, angle in enumerate(angles):
        prepare_address(i)
        qml.ctrl(qml.PhaseShift(angle, num_wires-1), range(num_wires-1))
        prepare_address(i)
        
    return qml.state()


def adjust_global_phase_of_diag_unitary(trg_arr, ref_value=1.+0.j):
    """Rotate the target diagonal matrix such, that it's global aligns with that of a source matrix. 
    If no source matrix is provided, the target matrix is rotated such, that the first element is real.
    
    Args:
        - trg_arr (np.array): approx. diagonalized unitary matrix (i.e. the norm of the diagonal elements approximate to 1, all other elements approx. to 0)
        - ref_value (complex): the reference value, holding the target phase; defaults to 1.+0.j 
        
    Return:
    
    """
    assert np.all(np.isclose(np.absolute(trg_arr), np.identity(len(trg_arr)))), "Invalid diagonalized unitary provided."
    assert np.isclose(np.absolute(ref_value), 1.), "Invalid reference value provided; must be complex with norm 1"
    
    angle_trg = np.angle(trg_arr.flat[0])
    angle_src = np.angle(ref_value)
    return trg_arr * np.exp(1.j*(angle_src - angle_trg))

.. and a small adjustment, to the test data + checking procedure.

# prepare test data
rng = np.random.default_rng(seed=314159)
rand_angles = rng.random(2**num_wires) * 2. * np.pi
eigenvalues = np.exp(1.j*rand_angles)

get_matrix = qml.matrix(prepare_unitary)
matrix = get_matrix(eigenvalues)
phase_adjusted_matrix = adjust_global_phase_of_diag_unitary(matrix, eigenvalues[0])

result_str = "correct!" if np.all(np.isclose(phase_adjusted_matrix, np.diagflat(eigenvalues))) else "INCORRECT! "

print("The result is", result_str)

The tests check out, all fine. It remains to exchange the MCP-gate with a series of two qubit gates to solve the challenge in it’s entirety.

QHack 2023: 4 week prep-challenge, day 21, Constructing unitary diagonal operators, pt. 1

I dabbled into the first QOSF monthly challenge, first published in November 2020. The task is to create a circuit, that implements a unitary operator in it’s eigenbasis, i.e. a diagonal matrix, where the diagonal elements have norm 1.

The task is split into three difficulty levels, today I’m tackling the easiest level: Create an algorithm that constructs a circuit, that translates into a 4×4 diagonal matrix with real entries. Now, the only two allowed real values for the diagonal elements are +1 and -1.

I set out to solve the more difficult problem, to create a circuit that allows for complex values in the diagonal, therefore, the logic is formulated slightly more general (except for the crucial gates, there the limits are hard in place).

import pennylane as qml
from pennylane import numpy as np

num_wires = 2
dev = qml.device("default.qubit", wires=num_wires)

def prepare_address(address):
    """Flip bits, to prepare addressation by oracle, or the like.
    """
    bin_address = np.binary_repr(address, width=num_wires)
    
    for i, addr_bit in enumerate(bin_address):
        if addr_bit == "0":
            qml.PauliX(i)


@qml.qnode(dev)
def prepare_unitary(u_eigenvalues):
    """Prepare a circuit, that realizes a unitary transformation, with a diagonal matrix representation, with the provided eigenvalues.
    
    Args: 
        - u_eigenvalues (np.array(complex)): Array that holds a list of unitary eigenvalues of the form np.exp(1.j*alpha), i.e. complex, norm 1
    
    Return: 
        - qml.State
    """
    assert np.all(np.isclose(np.absolute(u_eigenvalues), 1.)), "Invalid eigenvalues provided, do not have norm of 1"
    
    angles = np.angle(u_eigenvalues)
    
    for i, angle in enumerate(angles):
        prepare_address(i)
        if angle:
            qml.CZ(wires=range(num_wires))
        prepare_address(i)
        
    return qml.state()

My previous attempts lead me to write this little helper function, to adjust the global phase of a result matrix, for easier comparison….


def adjust_global_phase_of_diag_unitary(trg_arr, ref_value=1.+0.j):
    """Rotate the target diagonal matrix such, that it's global aligns with that of a source matrix. 
    If no source matrix is provided, the target matrix is rotated such, that the first element is real.
    
    Args:
        - trg_arr (np.array): approx. diagonalized unitary matrix (i.e. the norm of the diagonal elements approximate to 1, all other elements approx. to 0)
        - ref_value (complex): the reference value, holding the target phase; defaults to 1.+0.j 
        
    Return:
    
    """
    assert np.all(np.isclose(np.absolute(trg_arr), np.identity(len(trg_arr)))), "Invalid diagonalized unitary provided."
    assert np.isclose(np.absolute(ref_value), 1.), "Invalid reference value provided; must be complex with norm 1"
    
    angle_trg = np.angle(trg_arr.flat[0])
    angle_src = np.angle(ref_value)
    return trg_arr * np.exp(1.j*(angle_src - angle_trg))

Eventually, I tested the code, with a few configurations…

eigenvalues = np.array([1,-1,-1,1])

get_matrix = qml.matrix(prepare_unitary)
matrix = get_matrix(eigenvalues)
phase_adjusted_matrix = adjust_global_phase_of_diag_unitary(matrix, eigenvalues[0])

result_str = "correct!" if np.all(np.isclose(phase_adjusted_matrix, np.diagflat(eigenvalues))) else "INCORRECT! "

print("The result is", result_str)

… and I end up with correct results.

As a next step, I will generalize this algorithm to allow for arbitrary dimensions.

QHack 2023: 4 week prep challenge, day 20

Today, I tried to solve further exercises, but the remaining problem statements in the QHack 2022 Games category don’t play into my strengths. I will need to improve my theoretical understanding , to progress on them.

For the next few days, I will leave them aside, and try to apply myself to other problem sets. I believe, I can learn plenty from the QOSF monthly challenges.

QHack 2023: 4 week prep-challenge, day 19, A reading night

Recently, I was feeling quite comfortable with solving these challenges, to me this is a sign, that I can set the bar a little higher or mix-up my activities to increase my learning. The QHack comprises talks, the coding challenge and an open hackathon. The coding challenges will be of the same format, as my exercises of the last days, the hackathon invites for free-form contributions and offer much bigger rewards. Now, I deem it beneficial to broaden my view and get some inspiration reading up a little bit.

I read an insightful industry and market analysis by BCG, providing forecasts for market sizes and areas for near term applications.

Further, I found a consortium of big German companies, evaluating quantum computing: QUTAC. Some of the researched applications are outlined in this paper. Over the next few days, I want to study this overview more carefully.

QHack 2023: 4 week prep-challenge, day 18, QAOA with custom Hamiltonian

The final exercise on the QML track, included some bug-hunting for me, but this made it much more rewarding, to eventually solve it.

The basic task was conceivably easy: given some data, create a hamiltonian and use a QAOA optimization scheme to obtain a ground state approximation. The problem described some special graph covering problem. The mistakes that took some efforts to resolve where

  • Mistake 1: PennyLane is exceptionally greedy, when arrays appear along the optimization scheme. The way I constructed the hamiltonian included numpy arrays for indexing qubits, PennyLane tried to differentiate the problem by them and vary them subsequently – that lead to exceptions, and it was a real pain to find the source of the error.
  • Mistake 2: I tried to construct operators, instead of decomposing the occupation operator into Pauli matrices. This might work somehow, but I had an easier time decomposing the operators eventually.
  • Mistake 3: I omitted the constant terms, the optimal configuration is not affected by the constant term, but my tests relied on the final energy.

The code for the Hamiltonian

def hamiltonian_coeffs_and_obs(graph):
    """Creates an ordered list of coefficients and observables used to construct
    the UDMIS Hamiltonian.

    Args:
        - graph (list((float, float))): A list of x,y coordinates. e.g. graph = [(1.0, 1.1), (4.5, 3.1)]

    Returns:
        - coeffs (list): List of coefficients for elementary parts of the UDMIS Hamiltonian
        - obs (list(qml.ops)): List of qml.ops
    """

    num_vertices = len(graph)
    E, num_edges = edges(graph)
    u = 1.35
    obs = []
    coeffs = []

    # single terms
    for i in range(num_vertices):
        obs.append(qml.PauliZ(wires=i))
        coeffs.append(-.5)
    
    # constant term
    obs.append(qml.Identity(wires=0))
    coeffs.append(-.5*num_vertices)
    
    # interaction terms
    pairs = np.argwhere(E>0)
    for pair in pairs:
        i = int(pair[0])
        j = int(pair[1])
        obs.append(qml.PauliZ(wires=i) @ qml.PauliZ(wires=j))
        coeffs.append(.25*u)
        coeffs[i] += .25*u
        coeffs[j] += .25*u
        coeffs[num_vertices] += .25*u
    
    return coeffs, obs

The tests check out, success!

QHack 2023: 4 week prep challenge, day 17, QRAM

Today, I am working on the next exercise on the QML track. The goal is to create a QRAM, that entangles phase-information with address bits. A good description of QRAM that I know of, stems from an IBM Quantum hackathon from 2020. The main idea is to encode the addresses, and entangle the address register with the data register via a controlled rotation.

My code …

def prep_address(address):
    bin_address = np.binary_repr(address, width=3)
        
    for i, add_bit in enumerate(bin_address):
        if add_bit == "0":
            qml.PauliX(wires=i)

dev = qml.device("default.qubit", wires=range(4))

@qml.qnode(dev)
def circuit():
    qml.broadcast(qml.Hadamard, range(3), pattern="single")
        
    for i, theta in enumerate(thetas):
        prep_address(i)
        qml.ctrl(op=qml.RY(theta, wires=3), control=range(3))
        prep_address(i)
    return qml.state()

print(qml.draw(circuit)())

Instinctively, I would have reversed the address bit-order, but as long as you keep it consistent, it doesn’t matter. I also didn’t care about saving gates or anything, it’s just important to calculate the addresses correctly.