Arbitrary State preparation

Hi,

I’m currently working on the Variational Quantum Linear Solver tutorial, and I would like to generalize it to any vector b and matrix A.

Is there anyway to do that with pennylane?

Best regards

Hi @Ramy! You can definitely do that.

You would need to redefine the function U_b and CA defined in the Circuits of the quantum linear problem section.

You will probably also need to edit the variational_block function. The StronglyEntanglingLayers is a good place to start.

And finally, you will need to change the hyperparameters, with the new number of qubits.

Let me know if this works :slight_smile:

Hi @christina,
Thanks for your response!

I know that these functions need to be edited (U_b and CA) but I’m still new to pennylane so I don’t know how to do that. Is there a function similar to initialize function in qiskit which prepares any arbitrary state?

I’d recommend taking a look at Mottonen state preperation and the control transform.

1 Like

Thank you!
I will check that

1 Like

Hi @christina again,

For the state preparation, I think it is clear now, but I’m still confused with the matrix part. Suppose I have a hermitian matrix and I would like to convert it into operations, Do you have any idea on how to do it?

Regards

Hi @Ramy!

Are you looking for the qml.utils.decompose_hamiltonian function?

This function takes an Hermitian matrix, and returns a tuple of coefficients and Pauli operators:

>>> A = np.array( ... [[-2, -2+1j, -2, -2], [-2-1j, 0, 0, -1], [-2, 0, -2, -1], [-2, -1, -1, 0]])
>>> coeffs, obs_list = decompose_hamiltonian(A)
>>> coeffs [-1.0, -1.5, -0.5, -1.0, -1.5, -1.0, -0.5, 1.0, -0.5, -0.5]

We can use the output coefficients and tensor Pauli terms to construct a Hamiltonian :

>>> H = qml.Hamiltonian(coeffs, obs_list)
>>> print(H)
(-1.0) [I0 I1]
 + (-1.5) [X1]
 + (-0.5) [Y1]
 + (-1.0) [Z1]
 + (-1.5) [X0]
 + (-1.0) [X0 X1]
 + (-0.5) [X0 Z1]
 + (1.0) [Y0 Y1]
 + (-0.5) [Z0 X1]
 + (-0.5) [Z0 Y1]
1 Like

Thank you for your response, I think this is what I need.
just to make sure, it should work with qml.ctrl right?

Hi @Ramy, you might have to provide more details in terms of how you would like to use it with qml.ctrl :slight_smile:

Hi @josh, I’m trying to generalize Variational Quantum Linear for any Hermitian matrix A and arbitrary state b.

Hi @Ramy,

Thanks for the questions! qml.ctrl is mainly meant to be used with quantum operations, rather than an observable or Hamiltonian. How could qml.ctrl fit into your use case of generalizing the Variational Quantum Linear solver?

1 Like

Hi @antalszava, thanks for the clarification!
Actually, I’m not sure if it can be used or not.
My original question was how to generalize the Variational Quantum Linear solver for any hermition matrix?

Hi @Ramy,

The previously mentioned components are useful in achieving that.

  1. For changing U_b, the proposed qml.MottonenStatePreparation can be used.
  2. For changing CA, first let’s have a look at how we can decompose an Hermitian matrix A. We can represent A as A = \sum_{I}C_I\hat{P}_I

where C_I are numerical coefficients, and P_I are Pauli
β€œwords”, products of Pauli operators of different qubits \hat{P}_I=\prod_{i=1}^N\hat{\sigma}_i^{(I)}

where

\hat{\sigma}_i^{(I)} is one of the \hat{x}, \hat{y}, \hat{z} Pauli operators or identity \hat{e} for the i^{th} qubit.

(As described in the introduction of this reference for qubit Hamiltonians, i.e., Hermitian operators).

To get this decomposition, the decompose_hamiltonian function mentioned by Josh can be used as basis. The main difference, from how this function is being used usually, is that we’d create a Hamiltonian class using qml.Hamiltonian. A qml.Hamiltonian object is usually used with qml.ExpvalCost to return the expectation value of the Pauli words.

In the tutorial, however, we are applying the unitaries (i.e., the Pauli words from our decomposition) as quantum operations.

Therefore, we can use a custom definition of the decompose_hamiltonian function:

def decompose_hamiltonian_unitary_ops(H):
    """Auxiliary function similar to pennylane.utils.decompose_hamiltonian.
    
    This function returns a nested list of 
    """
    n = int(np.log2(len(H)))
    N = 2 ** n

    if H.shape != (N, N):
        raise ValueError(
            "The Hamiltonian should have shape (2**n, 2**n), for any qubit number n>=1"
        )

    if not np.allclose(H, H.conj().T):
        raise ValueError("The Hamiltonian is not Hermitian")

    paulis = [qml.Identity, qml.PauliX, qml.PauliY, qml.PauliZ]
    ops = []
    coeffs = []

    for term in itertools.product(paulis, repeat=n):
        matrices = [i._matrix() for i in term]
        coeff = np.trace(functools.reduce(np.kron, matrices) @ H) / N
        coeff = np.real_if_close(coeff).item()

        if not np.allclose(coeff, 0):
            coeffs.append(coeff)

            if not all(t is qml.Identity for t in term) and True:
                ops.append([t(i) for i, t in enumerate(term) if t is not qml.Identity])

    return coeffs, ops

This definition is very similar to the original implementation. The key difference is, that with the original version we are creating the tensor products of Paulis, whereas with this version, we accumulate them in lists that are contained in ops.

Once we have this function, we can decompose arbitrary Hermitian matrices:

H = np.array([[-2, -2+1j, -2, -2], [-2-1j, 0, 0, -1], [-2, 0, -2, -1], [-2, -1, -1, 0]])

coeffs, ops_list = decompose_hamiltonian_unitary_ops(H)

At this point, we just have to define a quantum function (e.g., called applying_unitaries), that will apply the unitaries that we’ve gathered in ops_list. Under the hood, PennyLane keeps track of operations in a quantum function using a concept called queuing. Each operation in the circuit is being queued when an operation is created when calling the quantum function.

For example, the qml.RY and the qml.PauliZ operators are queued in the following example (this example is not required for the solution):

def circuit():
    qml.RY(0.3, wires=[0])
    return qml.expval(qml.PauliZ(0))

In our case, however, we have already created our operations, so we have to explicitly queue them:

def applying_unitaries():
    """Quantum function that applies the unitaries building up a Hamiltonian"""
    for ops in ops_list:
        for o in ops:
            o.queue()

Once we have this quantum function, all is ready to make a controlled version of it using qml.ctrl:

# Creating a controlled version for applying the unitaries
# The second argument are the control wire(s) to use
ctrl_ops = qml.ctrl(applying_unitaries, [2])

ctrl_ops will return a quantum function that can be used in any QNode. We can check that we have the correct circuit with controlled operations by comparing the original circuit and the new circuit:

dev = qml.device('default.qubit', 3)

@qml.qnode(dev)
def applying_unitaries_with_expval():
    applying_unitaries()
    return qml.expval(qml.PauliZ(0))
    
applying_unitaries_with_expval()
print(applying_unitaries_with_expval.draw())

@qml.qnode(dev)
def ctrl_circuit_with_expval():
    ctrl_ops()
    return qml.expval(qml.PauliZ(0))

ctrl_circuit_with_expval()

print(ctrl_circuit_with_expval.draw())
 0: ──X──X──X──Y──Z──Z───────── ⟨Z⟩ 
 1: ──X──Y──Z──X──Z──Y──X──Y───     

 0: ───────────────╭X──╭X──────╭X──────╭CY───────╭Z──────╭Z──────── ⟨Z⟩ 
 1: ──╭X──╭CY──╭Z──│───│───╭X──│───╭Z──│────╭CY──│───╭X──│───╭CY───     
 2: ──╰C──╰CY──╰C──╰C──╰C──╰C──╰C──╰C──╰CY──╰CY──╰C──╰C──╰C──╰CY───     

To adjust this code to be written as CA in the tutorial, we will likely need to index into the list of operations.

Hope this helps! :slightly_smiling_face:

1 Like

Thank you very much for the detailed explanation @antalszava!
It will take me some time to go through it but I think it is clear now.
I appreciate it :blush:

1 Like