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.