Source code for qarp.endianness
"""Centralized endianness helpers for the qarpx LSB qubit convention.
qarp/qarpx index statevectors *and* operator matrices (``op.sparse_matrix()``)
LSB-first: qubit ``q`` carries bit ``q`` of the amplitude index, i.e.
``i = Σ_q 2^q · b_q`` (see ``docs/contracts/qarp_conventions.md``
§1). Nothing inside qarp needs converting. External tools — openfermion (its
layout is exposed for interop as ``qarp.operators.compat.get_sparse_operator``),
cirq, pennylane, pytket, quimb's kron-ordered ``from_dense`` — are MSB-first:
qubit 0 is the *most*-significant bit. Any operator or statevector crossing
such a boundary must be bit-reversed — use these helpers instead of re-deriving
the permutation in place.
Bit reversal is an involution, so the MSB→LSB and LSB→MSB conversions are the
same permutation; both names are provided so call sites document their
direction.
Reading order: binary *notation* (``0b110``, ``bin()``, ``format(x, 'b')``) is
written MSB-first, so qubit 0 is the rightmost digit. Bitstring *containers*
(sampler keys, ONVs, basis-state lists) are LSB-first, so qubit 0 is the first
element: ``6 = 0b110`` ↔ ``(0, 1, 1)``. Converting between the two requires a
reversal — that is what :func:`label_to_bits` / :func:`bits_to_label` do.
"""
from typing import List, Sequence, Union
import numpy as np
__all__ = [
"bit_reverse_perm",
"msb_to_lsb_matrix",
"lsb_to_msb_matrix",
"msb_to_lsb_statevector",
"lsb_to_msb_statevector",
"label_to_bits",
"bits_to_label",
]
[docs]
def bit_reverse_perm(n_qubits: int) -> np.ndarray:
"""Permutation mapping each computational-basis index to its bit-reversed
counterpart: ``perm[i] = int(format(i, f"0{n}b")[::-1], 2)``.
Applying it to an axis of a statevector or operator swaps the MSB and LSB
qubit-ordering conventions. It is its own inverse.
"""
return np.array([int(format(i, f"0{n_qubits}b")[::-1], 2) for i in range(2**n_qubits)])
[docs]
def msb_to_lsb_matrix(matrix: np.ndarray) -> np.ndarray:
"""Reorder a dense operator from OpenFermion's MSB qubit convention
(qubit 0 = most-significant bit) to qarpx's LSB convention (qubit 0 =
least-significant bit) by bit-reversing both computational-basis indices.
qarpx statevectors / unitaries / ``sparse_matrix()`` are LSB-indexed, so
any external MSB matrix (openfermion, ``compat.get_sparse_operator``) must
be bit-reversed before being contracted with them (e.g. ``U_matrix† H
U_matrix``) or indexed by an LSB walker label.
"""
dim = matrix.shape[0]
n = dim.bit_length() - 1
perm = bit_reverse_perm(n)
return matrix[np.ix_(perm, perm)]
#: Bit reversal is an involution — the LSB→MSB conversion is the same permutation.
lsb_to_msb_matrix = msb_to_lsb_matrix
[docs]
def msb_to_lsb_statevector(statevector: np.ndarray) -> np.ndarray:
"""Reorder a statevector from the MSB qubit convention to qarpx's LSB
convention by bit-reversing the computational-basis index.
"""
statevector = np.asarray(statevector)
dim = statevector.shape[0]
n = dim.bit_length() - 1
return statevector[bit_reverse_perm(n)]
#: Bit reversal is an involution — the LSB→MSB conversion is the same permutation.
lsb_to_msb_statevector = msb_to_lsb_statevector
[docs]
def label_to_bits(label: Union[int, str], n_qubits: int) -> List[int]:
"""LSB-ordered bitstring of an integer basis-state label: ``bits[q]`` is
the occupation of qubit ``q``. Matches ``ComputationalBasisStateBlock``
and ``generate_states_new_basis`` (qarpx LSB), unlike ``bin(label)`` which
is MSB-ordered.
"""
label = int(label)
return [(label >> q) & 1 for q in range(n_qubits)]
[docs]
def bits_to_label(bits: Sequence[Union[int, str]]) -> int:
"""Inverse of :func:`label_to_bits` — pack an LSB-ordered bitstring
(``bits[q]`` = qubit ``q``) into an integer label.
"""
return sum(int(b) << q for q, b in enumerate(bits))