#
# Copyright © 2022-2023 University of Strasbourg. All Rights Reserved.
#
# 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.
#
from bitarray import bitarray, frozenbitarray
def _helper_bitvec_to_int(arr):
res = 0
for bit in arr:
res = (res << 1) | bit
return res
# Function to convert a bitarray to an integer
def bitvec_to_int(arr):
if not isinstance(arr, bitarray) or not isinstance(arr, frozenbitarray):
raise TypeError("Input must be a bitarray or BitVector.")
num_bits = len(arr)
if num_bits > 0:
return _helper_bitvec_to_int(arr)
else:
raise ValueError("Input array is empty.")
# Function to convert an integer to a bitarray of given size (pad)
def int_to_bitvec(x, pad, frozen=True):
if not isinstance(x, int):
raise TypeError("Input must be an integer.")
if x < 0:
raise ValueError("Input integer must be non-negative.")
binary_str = bin(x)[2:].zfill(pad)
if frozen:
return frozenbitarray(binary_str)
else:
return bitarray(binary_str)
[docs]
class BitString:
"""BitString for the quantum states.
Representation of the quantum state of a quantum register with definite
values for each qubit.
Examples:
Initialization:
>>> from mimiqcircuits import *
>>> from bitarray import bitarray
>>> BitString(16) # number of qubits
bs"0000000000000000"
>>> BitString('10101') # binary string
bs"10101"
>>> BitString([1,0,0,0,1]) # binary string
bs"10001"
>>> BitString((1,0,0,0,1)) # binary string
bs"10001"
>>> BitString(bitarray('101010')) # bitarray
bs"101010"
Other initializations:
>>> BitString.fromnonzeros(16, [1, 3, 5, 7, 9, 11, 13, 15])
bs"0101010101010101"
>>> BitString.fromfunction(16, lambda i: i % 2 == 1)
bs"0101010101010101"
>>> BitString.fromstring('10101')
bs"10101"
>>> BitString.fromint(16, 21)
bs"1010100000000000"
>>> BitString.fromint(16, 21, 'little')
bs"0000000000010101"
Accessing the bits:
>>> bs = BitString(16)
>>> bs[0] # get the 0th bit
0
>>> bs[0:4] # get the first 4 bits
bs"0000"
Bitwise operations:
>>> bs1 = BitString('10101')
>>> bs2 = BitString('11100')
>>> bs1 | bs2 # OR
bs"11101"
>>> bs1 & bs2 # AND
bs"10100"
>>> bs1 ^ bs2 # XOR
bs"01001"
>>> ~bs1 # NOT
bs"01010"
>>> bs1 << 2 # left shift
bs"10100"
>>> bs1 >> 2 # right shift
bs"00101"
Other operations:
>>> bs1 + bs2 # concatenation
bs"1010111100"
>>> bs1 * 2 # repetition
bs"1010110101"
"""
_bits = bitarray(0)
def __init__(self, arg):
"""Initialize the BitString.
If the number of qubits is given, then the BitString is initialized to
the all-zero state.
If a binary string or a bitarray is given, then the BitString is the
corresponding state.
Examples:
>>> BitString(16) # number of qubits
bs"0000000000000000"
>>> BitString('10101') # binary string
bs"10101"
>>> BitString(bitarray('101010')) # bitarray
bs"101010"
"""
if isinstance(arg, int):
bitstring = arg * "0"
elif isinstance(arg, (str, bitarray, frozenbitarray, list, tuple)):
bitstring = arg
else:
raise TypeError(
"Invalid input type. Expected 'str', 'int', 'bitarray', 'list', 'tuple', 'frozenbitarray'"
)
self._bits = frozenbitarray(bitstring)
@property
def bits(self):
return self._bits
@bits.setter
def bits(self, bits):
raise AttributeError("Cannot set the bits of a BitString.")
[docs]
@staticmethod
def fromnonzeros(num_qubits: int, nonzeros: list):
"""Initialize a BitString with specific non-zero qubits.
Arguments:
num_qubits (int): The number of qubits in the BitString.
nonzeros (list): A list of non-zero qubit indices to set in the BitString.
Returns:
A BitString with the specified non-zero qubits.
"""
bitstring = ""
if not all(0 <= i < num_qubits for i in nonzeros):
raise ValueError("Invalid nonzero index.")
for i in range(num_qubits):
bitstring += "1" if i in nonzeros else "0"
return BitString(bitstring)
[docs]
@staticmethod
def fromfunction(num_qubits: int, f: type[lambda: None]):
"""Initialize a BitString from a function.
Arguments:
num_qubits (int): The number of qubits in the BitString.
f (function): A function that takes an integer and returns a boolean.
Returns:
A BitString.
"""
bitstring = ""
for i in range(num_qubits):
bitstring += "1" if f(i) else "0"
return BitString(bitstring)
[docs]
@staticmethod
def fromstring(bitstring: str):
"""Initialize a BitString from a string.
Arguments:
bitstring (str): The string representation of the BitString.
Returns:
A BitString.
"""
return BitString(bitstring)
[docs]
@staticmethod
def fromint(num_qubits: int, integer: int, endianess: str = "big"):
"""Initialize a BitString from an integer.
Arguments:
num_qubits (int): The number of qubits in the BitString.
integer (int): The integer value of the BitString.
endianess (str): The endianess of the integer. Default is 'big'.
Returns:
A BitString.
"""
if len(bin(integer)[2:]) > num_qubits:
raise ValueError("Integer is too large for the given number of qubits.")
if endianess == "little":
bitstring = bin(integer)[2:].zfill(num_qubits)[::-1]
elif endianess == "big":
bitstring = bin(integer)[2:].zfill(num_qubits)
else:
raise ValueError("endian must be either 'big' or 'little'")
return BitString(bitstring[::-1])
[docs]
def num_qubits(self):
"""Return the number of qubits in the BitString."""
return len(self.bits)
[docs]
def nonzeros(self):
"""Return the indices of the non-zero qubits."""
return [i for i, bit in enumerate(self.bits) if bit]
[docs]
def zeros(self):
"""Return the indices of the zero qubits."""
return [i for i, bit in enumerate(self.bits) if not bit]
[docs]
def tointeger(self, endianess: str = "big"):
"""Return the integer value of the BitString.
Arguments:
endianess (str): The endianess of the integer. Default is 'big'.
Returns:
The integer value of the BitString.
"""
if endianess == "big":
return bitvec_to_int(self.bits[::-1])
elif endianess == "little":
return bitvec_to_int(self.bits)
else:
raise ValueError("Invalid endianess. Must be 'big' or 'little'.")
[docs]
def to01(self, endianess="big"):
"""Return the binary string representation of the BitString.
Arguments:
endianess (str): The endianess of the integer. Default is 'big'
Retruns:
The binary string representation of the BitString.
"""
if endianess == "big":
return "".join(map(str, self.bits))
elif endianess == "little":
return "".join(map(str, reversed(self.bits)))
else:
raise ValueError("Invalid endianess. Must be 'big' or 'little'.")
[docs]
def toindex(self, endianess: str = "big"):
"""Return the integer index of the BitString.
Arguments:
endianess (str): The endianess of the integer. Default is 'big'.
Returns:
The integer index of the BitString.
"""
if endianess == "big":
return bitvec_to_int(self.bits[::-1])
elif endianess == "little":
return bitvec_to_int(self.bits)
else:
raise ValueError("endian must be either 'big' or 'little'")
def __eq__(self, other):
if not isinstance(other, BitString):
return False
return self.bits == other.bits
def __str__(self):
s = f"{len(self.bits)}-qubit BitString"
nz = self.nonzeros()
if len(nz) != 0:
s += f" with {len(nz)} non-zero qubits:\n"
s += f"├── |{self.bits.to01()}⟩\n"
s += f"└── non-zero qubits: {nz}"
else:
s += f":\n└── |{self.bits.to01()}⟩"
return s
def __repr__(self):
return "bs" + '"' + self.to01() + '"'
def __len__(self):
return len(self.bits)
def __hash__(self):
return hash(self.bits.to01())
def __iter__(self):
return iter(self.bits)
def __getitem__(self, index):
if type(index) is slice:
return BitString(self.bits[index])
return self.bits[index]
def __or__(self, other):
if not isinstance(other, BitString):
raise TypeError("BitString can only be ORed with another BitString.")
return BitString(self.bits | other.bits)
def __and__(self, other):
if not isinstance(other, BitString):
raise TypeError("BitString can only be ANDed with another BitString.")
return BitString(self.bits & other.bits)
def __xor__(self, other):
if not isinstance(other, BitString):
raise TypeError("BitString can only be XORed with another BitString.")
return BitString(self.bits ^ other.bits)
def __invert__(self):
return BitString(~self.bits)
def __lshift__(self, other):
if not isinstance(other, int):
raise TypeError("BitString can only be left shifted by an integer.")
return BitString(self.bits << other)
def __rshift__(self, other):
if not isinstance(other, int):
raise TypeError("BitString can only be right shifted by an integer.")
return BitString(self.bits >> other)
def __add__(self, other):
if not isinstance(other, BitString):
raise TypeError("BitString can only be added with another BitString.")
return BitString(self.bits + other.bits)
def __mul__(self, other):
if not isinstance(other, int):
raise TypeError("BitString can only be multiplied by an integer.")
return BitString(self.bits * other)
__all__ = ["BitString"]