State Conventions
Here is an overview of the state conventions used in Qadence together with practical examples.
Qubit register order
Qubit registers in quantum computing are often indexed in increasing or decreasing order from left to right. In Qadence, the convention is qubit indexation in increasing order. For example, a register of four qubits in bra-ket notation reads:
Furthermore, when displaying a quantum circuit, qubits are ordered from top to bottom.
Basis state order
Basis state ordering refers to how basis states are ordered when considering the conversion from bra-ket notation to the standard linear algebra basis. In Qadence, basis states are ordered in the following manner:
Endianness
Endianness refers to the storage convention for binary information (in bytes) in a classical memory register. In quantum computing, information is either stored in bits or in qubits. The most commonly used conventions are:
- A big-endian system stores the most significant bit of a binary word at the smallest memory address.
- A little-endian system stores the least significant bit of a binary word at the smallest memory address.
Given the register convention in Qadence, the integer \(2\) written in binary big-endian as \(10\) can be encoded in a qubit register in both big-endian as \(|10\rangle\) or little-endian as \(|01\rangle\).
The convention for Qadence is big-endian.
Quantum states
In practical scenarios, conventions regarding register order, basis state order and endianness are very much intertwined, and identical results can be obtained by fixing or varying any of them. In Qadence, we assume that qubit ordering and basis state ordering is fixed, and allow an endianness
argument that can be passed to control the expected result. Here are a few examples:
A simple and direct way to exemplify the endianness convention is using convenience functions for state preparation.
Bitstring convention as inputs
When a bitstring is passed as input to a function for state preparation, it has to be understood in big-endian convention.
from qadence import Endianness, product_state
# The state |10>, the 3rd basis state.
state_big = product_state("10", endianness=Endianness.BIG) # or just "Big"
# The state |01>, the 2nd basis state.
state_little = product_state("10", endianness=Endianness.LITTLE) # or just "Little"
Here, a bitword expressed as a Python string to encode the integer 2 in big-endian is used to create the respective basis state in both conventions. However, note that the same results can be obtained by fixing the endianness convention as big-endian (thus creating the state \(|10\rangle\) in both cases), and changing the basis state ordering. A similar argument holds for fixing both endianness and basis state ordering and simply changing the qubit index order.
Another example where endianness directly comes into play is when measuring a register. A big- or little-endian measurement will choose the first or the last qubit, respectively, as the most significant bit. Let's see this in an example:
from qadence import I, H, sample
# Create superposition state: |00> + |01> (normalized)
block = I(0) @ H(1) # Identity on qubit 0, Hadamard on qubit 1
# Generate bitword samples following both conventions
# Samples "00" and "01"
result_big = sample(block, endianness=Endianness.BIG)
# Samples "00" and "10"
result_little = sample(block, endianness=Endianness.LITTLE)
In Qadence, endianness can be flipped for many relevant objects:
from qadence import invert_endianness
# Equivalent to sampling in little-endian.
flip_big_sample = invert_endianness(result_big)
# Equivalent to a state created in little-endian.
flip_big_state = invert_endianness(state_big)
Quantum operations
When looking at the matricial form of quantum operations, the usage of the term endianness becomes slightly abusive. To exemplify, we may consider the CNOT
operation with control = 0
and target = 1
. This operation is often described with two different matrices:
The difference can be easily explained either by considering a different ordering of the qubit indices, or a different ordering of the basis states. In Qadence, both can be retrieved through the endianness
argument:
from qadence import block_to_tensor, CNOT
matrix_big = block_to_tensor(CNOT(0, 1), endianness=Endianness.BIG)
matrix_little = block_to_tensor(CNOT(0, 1), endianness=Endianness.LITTLE)
CNOT matrix in big endian =
tensor([[[1.+0.j, 0.+0.j, 0.+0.j, 0.+0.j],
[0.+0.j, 1.+0.j, 0.+0.j, 0.+0.j],
[0.+0.j, 0.+0.j, 0.+0.j, 1.+0.j],
[0.+0.j, 0.+0.j, 1.+0.j, 0.+0.j]]])
CNOT matrix in little endian =
tensor([[[1.+0.j, 0.+0.j, 0.+0.j, 0.+0.j],
[0.+0.j, 0.+0.j, 0.+0.j, 1.+0.j],
[0.+0.j, 0.+0.j, 1.+0.j, 0.+0.j],
[0.+0.j, 1.+0.j, 0.+0.j, 0.+0.j]]])
Backends
An important part of having clear state conventions is that we need to make sure our results are consistent accross different computational backends, which may have their own conventions. In Qadence, this is taken care of automatically: by calling operations for different backends, the result is expected to be equivalent up to qubit ordering.
from qadence import BackendName, RX, run, sample, PI
# RX(PI/4) on qubit 1
n_qubits = 2
op = RX(1, PI/4)
Same sampling order in big endian:
On PyQTorch = [OrderedCounter({'00': 77, '01': 23})]
On Pulser = [Counter({'00': 88, '01': 12})]
Same wavefunction order:
On PyQTorch = tensor([[0.9239+0.0000j, 0.0000-0.3827j, 0.0000+0.0000j, 0.0000+0.0000j]])
On Pulser = tensor([[0.9241+0.0000j, 0.0000-0.3822j, 0.0000+0.0000j, 0.0000+0.0000j]])