# How to convert between little/big-endian unitary forms in Braket?

As noted in this post, the Amazon Braket unitary calculation method as_unitary has been deprecated (#325) as it uses little-endian qubit order. The new, big-endian method is to_unitary. Here's a code snippet showing the difference in the two forms for a simple bell circuit:

from braket.circuits import Circuit

circuit = Circuit().h(0).cnot(0, 1)

u_little_endian = circuit.as_unitary()  # little-endian method
u_big_endian = circuit.to_unitary()     # big-endian method

print(f"little-endian: \n{u_little_endian}\n")
print(f"big-endian: \n{u_big_endian}\n")

little-endian:
[[ 0.70710678+0.j  0.70710678+0.j  0.        +0.j  0.        +0.j]
[ 0.        +0.j  0.        +0.j  0.70710678+0.j -0.70710678+0.j]
[ 0.        +0.j  0.        +0.j  0.70710678+0.j  0.70710678+0.j]
[ 0.70710678+0.j -0.70710678+0.j  0.        +0.j  0.        +0.j]]

big-endian:
[[ 0.70710678+0.j  0.        +0.j  0.70710678+0.j  0.        +0.j]
[ 0.        +0.j  0.70710678+0.j  0.        +0.j  0.70710678+0.j]
[ 0.        +0.j  0.70710678+0.j  0.        +0.j -0.70710678+0.j]
[ 0.70710678+0.j  0.        +0.j -0.70710678+0.j  0.        +0.j]]


I'm in the process of transitioning some of my own projects to reflect this upgrade, but am also hoping to maintain backward compatibility with the little-endian method. For testing, I'm attempting to create a function that can convert a matrix calculated using the big-endian method to its little-endian "equivalent", or vice versa. For example, I'm looking to implement a function my_conversion that would satisfy the following:

import numpy as np

u_converted = my_conversion(u_big_endian)
assert np.allclose(u_little_endian, u_converted)


In my own attempts, I've tried to work backward from the underlying functions, calculate_unitary and calculate_unitary_big_endian, however, the procedures used have been somewhat difficult to decipher. Mathematically, this post was helpful, but programmatically, I'm still not sure where to start. Maybe such a function already exists in the braket repo for internal use?

One intuitive way to do this is to think in terms of tensors.

Suppose you have a 3-qubit state $$\psi$$. This is an 8-dimensional vector that you can reshape into a contravariant tensor $$\psi^{ijk}$$. Let's assume the qubits in this state are big-endian, following Braket convention.

Now suppose you have a 3-qubit unitary $$U$$ that you want to apply to $$\psi$$ to obtain $$U \psi$$. Matching up the indices of $$U$$ with those of $$\psi$$ according to Einstein convention, you get

$$(U \psi)^{lmn} = U^{lmn}_{ijk} \psi^{ijk}$$

where the original indices of $$\psi$$ are matched with the covariant (lower) indices of $$U$$ and traced out, leaving the indices $$l, m, n$$.

So far, everything has been big-endian. What if we want to switch to little-endian qubits? Well, the state $$\psi$$ needs to have its indices reversed, giving $$\psi^{kji}$$. The lower indices of $$U$$ will then be reversed as well to ensure that the correct entries are multiplied together, and the contravariant (upper) indices need to be reversed so the output state is also little-endian:

$$(U \psi)^{nml} = U^{nml}_{kji} \psi^{kji}$$

Now, in terms of code, you can get the little-endian $$U^{nml}_{kji}$$ with np.einsum; you just have to reshape first:

# U is 8 * 8
U_tensor = U.reshape( * 6)
little_endian_tensor = np.einsum(U_tensor, [2, 1, 0, 5, 4, 3])
U_little_endian = little_endian_tensor.reshape([8, 8])


A general function would then be

# Circuit is the Braket class
def little_endian_unitary(circ: Circuit) -> np.ndarray:
qubit_count = circ.qubit_count
U_tensor = circ.to_unitary().reshape( * 2 * qubit_count)
input = list(reversed(range(qubit_count)))
output = [i + qubit_count for i in input]
little_endian_tensor = np.einsum(U_tensor, input + output)
return little_endian_tensor.reshape([2 ** qubit_count, 2 ** qubit_count])