Source code for pennylane.math.binary_linalg

# Copyright 2026 Xanadu Quantum Technologies Inc.

# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at

#     http://www.apache.org/licenses/LICENSE-2.0

# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.
"""This module contains linear algebraic functions over the binary numbers,
also referred to as finite field F_2, Galois field GF_2, or ℤ_2."""
import numpy as np


[docs] def int_to_binary(integer: int | np.ndarray, width: int) -> np.ndarray: """Convert an integer or an array of integers to an array of bitstrings of given length, representing the integers as binaries. Args: integer (int | np.ndarray): Integer(s) to convert. Either a single integers or an array of integers. width (int): Length of the bitstrings to which the integer(s) are converted. Note that the ``width`` **least** significant bits corresponding to ``integers % 2**width`` are returned, discarding the most significant contributions if ``integer > 2**(width-1)-1``. Returns: np.ndarray: Array of bitstrings representing the ``integer`` input. If ``integer`` is an ``int``, the returned array has shape ``(width,)``. If ``integer`` is an array of shape ``S``, the returned array has shape ``(*S, width)``. **Example** We may compute the binary representation of the integer ``13`` on five bits, for example: >>> width = 5 >>> print(qml.math.int_to_binary(13, width=width)) [0 1 1 0 1] This matches the output of ``np.binary_repr`` but returns a numerical array instead of a string: >>> print(np.binary_repr(13, width=width)) 01101 For an array-typed input, we obtain a new array with an additional axis in the last position, of size ``width``: >>> x = np.array([[7, 3], [17, 9], [2, 8]]) >>> print(x.shape) (3, 2) >>> bits = qml.math.int_to_binary(x, width=width) >>> print(bits.shape) (3, 2, 5) The input ``integer`` can be reconstructed from the bit strings via >>> powers_of_two = 2**np.arange(width-1, -1, -1) >>> reconstruction = bits @ powers_of_two >>> print(reconstruction) [[ 7 3] [17 9] [ 2 8]] """ shifts = np.arange(width - 1, -1, -1) # [width-1, width-2, ... 1, 0] if isinstance(integer, int): return (integer >> shifts) % 2 return (integer[..., None] >> shifts) % 2 # Broadcasting (new) shape (*S, 1) with (width,)
[docs] def binary_finite_reduced_row_echelon(binary_matrix, inplace=False): r"""Computes the `reduced row echelon form (RREF) <https://en.wikipedia.org/wiki/Row_echelon_form>`__ of a matrix with entries from the binary numbers, a.k.a. the finite field :math:`\mathbb{Z}_2`. Args: binary_matrix (array[int]): binary matrix inplace (bool): Whether to perform the modification of ``binary_matrix`` in place. Defaults to ``False``, making a copy before running the calculation. Returns: array[int]: reduced row-echelon form of the given ``binary_matrix``. The output has the same shape as the input. If ``inplace=True``, the returned array is the same object as the input ``binary_matrix``, which then has been modified in place. .. warning:: This function is currently not compatible with JAX. **Example** >>> binary_matrix = np.array([[1, 0, 0, 0, 0, 1, 0, 0], ... [1, 0, 1, 0, 0, 0, 1, 0], ... [0, 0, 0, 1, 1, 0, 0, 1]]) >>> print(qml.math.binary_finite_reduced_row_echelon(binary_matrix)) [[1, 0, 0, 0, 0, 1, 0, 0], [0, 0, 1, 0, 0, 1, 1, 0], [0, 0, 0, 1, 1, 0, 0, 1]] """ rref_mat = binary_matrix if inplace else binary_matrix.copy() shape = rref_mat.shape icol = 0 for irow in range(shape[0]): while icol < shape[1] and not rref_mat[irow][icol]: # get the nonzero indices in the remainder of column icol non_zero_idx = rref_mat[irow:, icol].nonzero()[0] if len(non_zero_idx) == 0: # if remainder of column icol is all zero icol += 1 else: # find value and index of largest element in remainder of column icol krow = irow + non_zero_idx[0] # swap rows krow and irow rref_mat[irow, icol:], rref_mat[krow, icol:] = ( rref_mat[krow, icol:].copy(), rref_mat[irow, icol:].copy(), ) if icol < shape[1] and rref_mat[irow][icol]: # store remainder right hand side columns of the pivot row irow rpvt_cols = rref_mat[irow, icol:].copy() # get the column icol and set its irow element to 0 to avoid XORing pivot row with itself currcol = rref_mat[:, icol].copy() currcol[irow] = 0 # XOR the right hand side of the pivot row irow with all of the other rows rref_mat[:, icol:] ^= np.outer(currcol, rpvt_cols) icol += 1 return rref_mat
[docs] def binary_matrix_rank(binary_matrix: np.ndarray) -> int: r""" Find the rank of a matrix over :math:`\mathbb{Z}_2`. Args: binary_matrix (np.ndarray): Matrix of binary entries. Returns: int: Rank of ``binary_matrix`` over the finite field :math:`\mathbb{Z}_2`. Note that the ranks of a matrix over :math:`\mathbb{Z}_2` and over :math:`\mathbb{Z}` (or :math:`\mathbb{R}`) may differ. This function does not modify the input. .. warning:: This function is currently not compatible with JAX. **Example** Consider the following binary matrix of shape ``(4, 4)``: >>> binary_matrix = np.array([ ... [0, 1, 1, 0], ... [0, 1, 0, 1], ... [1, 0, 1, 1], ... [1, 0, 0, 0], ... ]) >>> print(binary_matrix.shape) (4, 4) We may compute its rank over :math:`\mathbb{Z}_2` and find that it does not have full rank: >>> print(qml.math.binary_matrix_rank(binary_matrix)) 3 Note that it would have full rank over the real numbers :math:`\mathbb{R}`: >>> print(qml.math.linalg.matrix_rank(binary_matrix)) 4 """ binary_matrix = binary_matrix.copy() rank = 0 while len(binary_matrix): # Take last row as potential pivot and reduce remaining matrix binary_matrix, pivot = binary_matrix[:-1], binary_matrix[-1] if np.any(pivot): # If it the potential pivot is not all zeros, it provides a new degree of freedom rank += 1 # Find least significant bit that is set to 1 in the pivot least_sig_bit = np.where(pivot)[0][-1] # XOR all rows with the pivot that have the bit in the position least_sig_bit set to 1 binary_matrix[np.where(binary_matrix[:, least_sig_bit])] ^= pivot return rank
[docs] def binary_solve_linear_system(A: np.ndarray, b: np.ndarray) -> np.ndarray: r"""Solve the linear system of equations :math:`Ax=b` over the Booleans/:math:`\mathbb{Z}_2`, where :math:`A` is assumed to be regular, i.e., non-singular. The implementation is based on Gaussian elimination to bring the augmented matrix :math:`(A|b)` into (reduced) row echelon form via :func:`~.math.binary_finite_reduced_row_echelon`. Args: A (np.ndarray): Square matrix of the linear system of equations with binary entries. b (np.ndarray): Binary coefficient vector with same length as ``A``. Returns: np.ndarray: Binary solution vector with same length as ``A`` and ``b``. .. warning:: This function is currently not compatible with JAX. **Example** Consider a simple regular Boolean matrix ``A`` and a coefficient vector ``b``: >>> A = np.array([ ... [1, 0, 0], ... [0, 1, 1], ... [1, 0, 1] ... ]) >>> b = np.array([1, 1, 1]) Then we can solve the system ``A@x=b`` for ``x`` over :math:`\mathbb{Z}_2`: >>> x = qml.math.binary_solve_linear_system(A, b) >>> print(x) [1 1 0] Internally, this is done with the Gauss-Jordan elimination of the extended matrix ``(A | b)``. Indeed, we can verify that ``A @ x = b`` (over :math:`\mathbb{Z}_2`): >>> print(np.allclose((A @ x) % 2, b)) True """ # Create augmented matrix for Gauss-Jordan elimination. This also creates a copy so that the # input ``A`` is not modified in place, and we can skip copying within the echelon form call rref = np.hstack([A, b[:, None]]) rref = binary_finite_reduced_row_echelon(rref, inplace=True) if np.any(np.sum(rref[:, :-1], axis=1) == 0): # Matrix A is singular raise np.linalg.LinAlgError("Singular binary matrix.") # Potential solution is written in the last column of the augmented matrix after obtained RREF return rref[:, -1]
[docs] def binary_is_independent(vector: np.ndarray, basis: np.ndarray) -> bool: r"""Check whether a binary vector, i.e., a bitstring, is linearly independent (over :math:`\mathbb{Z}_2`) of a basis of binary vectors, given as column vectors of a matrix. Args: vector (np.ndarray): Binary vector to check. basis (np.ndarray): Basis of binary vectors against which ``vector`` is checked. If ``vector`` has shape ``(r,)``, ``basis`` should have shape ``(r, m)`` and rank ``min(r, m)``. I.e., the columns of ``basis`` all need to be linearly independent. Returns: bool: Whether ``vector`` is linearly independent of ``basis`` over :math:`\mathbb{Z}_2`. .. warning:: This function is currently not compatible with JAX. """ # We assume ``basis`` to have full rank. r = vector.shape[0] if basis.shape[0] != r: raise ValueError( "The columns of `basis` should have the same length as `vector`. " f"Got {vector.shape=} and {basis.shape=}" ) basis_rank = min(basis.shape) rk = binary_matrix_rank(np.concatenate([basis, vector[:, None]], axis=1)) return rk > basis_rank
[docs] def binary_select_basis(bitstrings: np.ndarray): r"""Select bitstrings from an array of bitstrings that form a basis for the column space of the array. Also returns the bitstrings that were not selected. Args: bitstrings (np.ndarray): Input bitstrings. The columns of the array span the space for which a basis is selected. Returns: tuple[np.ndarray, np.ndarray]: Two binary arrays. The first contains a selection of columns from ``bitstrings`` that form a basis for the column space of ``bitstrings`` over :math:`\mathbb{Z}_2`. The second contains all other columns. .. warning:: This function is currently not compatible with JAX. """ r = bitstrings.shape[0] basis = np.zeros((r, 0), dtype=int) other_cols = [] for col in bitstrings.T: if basis.shape[1] < r and binary_is_independent(col, basis): basis = np.concatenate([basis, col[:, None]], axis=1) else: other_cols.append(col) if not other_cols: other_cols = np.zeros((r, 0), dtype=int) else: other_cols = np.array(other_cols).T return basis, other_cols