# Copyright 2019-2024 Cambridge Quantum Computing## 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."""`OutcomeArray` class and associated methods."""importoperatorfromcollectionsimportCounterfromcollections.abcimportSequencefromfunctoolsimportreducefromtypingimportAny,castimportnumpyasnpimportnumpy.typingasnptfromnumpy.typingimportArrayLike
[docs]classOutcomeArray(np.ndarray):""" Array of measured outcomes from qubits. Derived class of `numpy.ndarray`. Bitwise outcomes are compressed into unsigned 8-bit integers, each representing up to 8 qubit measurements. Each row is a repeat measurement. :param width: Number of bit entries stored, less than or equal to the bit capacity of the array. :type width: int :param n_outcomes: Number of outcomes stored. :type n_outcomes: int """def__new__(cls,input_array:npt.ArrayLike,width:int)->"OutcomeArray":# Input array is an already formed ndarray instance# We first cast to be our class typeobj=np.asarray(input_array).view(cls)# add the new attribute to the created instanceiflen(obj.shape)!=2orobj.dtype!=np.uint8:raiseValueError("OutcomeArray must be a two dimensional array of dtype uint8.")bitcapacity=obj.shape[-1]*8ifwidth>bitcapacity:raiseValueError(f"Width {width} is larger than maxium bitlength of "f"array: {bitcapacity}.")obj._width=width# Finally, we must return the newly created object:returnobjdef__array_finalize__(self,obj:Any,*args:Any,**kwargs:Any)->None:# see InfoArray.__array_finalize__ for commentsifobjisNone:returnself._width:int|None=getattr(obj,"_width",None)@propertydefwidth(self)->int:"""Number of bit entries stored, less than or equal to the bit capacity of the array."""asserttype(self._width)isintreturnself._width@propertydefn_outcomes(self)->Any:"""Number of outcomes stored."""returnself.shape[0]# A numpy ndarray is explicitly unhashable (its __hash__ has type None). But as we# are dealing with integral arrays only it makes sense to define a hash.def__hash__(self)->int:# type: ignorereturnhash((self.tobytes(),self.width))def__eq__(self,other:object)->bool:ifisinstance(other,OutcomeArray):returnbool(np.array_equal(self,other)andself.width==other.width)returnFalse
[docs]@classmethoddeffrom_readouts(cls,readouts:ArrayLike)->"OutcomeArray":"""Create OutcomeArray from a 2D array like object of read-out integers, e.g. [[1, 1, 0], [0, 1, 1]]"""readouts_ar=np.array(readouts,dtype=int)returncls(np.packbits(readouts_ar,axis=-1),readouts_ar.shape[-1])
[docs]defto_readouts(self)->np.ndarray:"""Convert OutcomeArray to a 2D array of readouts, each row a separate outcome and each column a bit value."""returncast(np.ndarray,np.asarray(np.unpackbits(self,axis=-1))[...,:self.width])
[docs]defto_readout(self)->np.ndarray:"""Convert a singleton to a single readout (1D array)"""ifself.n_outcomes>1:raiseValueError(f"Not a singleton: {self.n_outcomes} readouts")returncast(np.ndarray,self.to_readouts()[0])
[docs]defto_intlist(self,big_endian:bool=True)->list[int]:"""Express each outcome as an integer corresponding to the bit values. :param big_endian: whether to use big endian encoding (or little endian if False), defaults to True :type big_endian: bool, optional :return: List of integers, each corresponding to an outcome. :rtype: List[int] """ifbig_endian:array=selfelse:array=OutcomeArray.from_readouts(np.fliplr(self.to_readouts()))bitcapacity=array.shape[-1]*8intify=lambdabytear:reduce(operator.or_,(int(num)<<(8*i)fori,numinenumerate(bytear[::-1])),0)>>(bitcapacity-array.width)intar=np.apply_along_axis(intify,-1,array)returnlist(intar)
[docs]@classmethoddeffrom_ints(cls,ints:Sequence[int],width:int,big_endian:bool=True)->"OutcomeArray":"""Create OutcomeArray from iterator of integers corresponding to outcomes where the bitwise representation of the integer corresponds to the readouts. :param ints: Iterable of outcome integers :type ints: Iterable[int] :param width: Number of qubit measurements :type width: int :param big_endian: whether to use big endian encoding (or little endian if False), defaults to True :type big_endian: bool, optional :return: OutcomeArray instance :rtype: OutcomeArray """n_ints=len(ints)bitstrings=(bin(int_val)[2:].zfill(width)[::(-1)**(notbig_endian)]forint_valinints)bitar=np.frombuffer("".join(bitstrings).encode("ascii"),dtype=np.uint8,count=n_ints*width)-ord("0")bitar.resize((n_ints,width))returncls.from_readouts(bitar)
[docs]defcounts(self)->Counter["OutcomeArray"]:"""Calculate counts of outcomes in OutcomeArray :return: Counter of outcome, number of instances :rtype: Counter[OutcomeArray] """ars,count_vals=np.unique(self,axis=0,return_counts=True)width=self.widthoalist=[OutcomeArray(x[None,:],width)forxinars]returnCounter(dict(zip(oalist,count_vals)))
[docs]defchoose_indices(self,indices:list[int])->"OutcomeArray":"""Permute ordering of bits in outcomes or choose subset of bits. e.g. [1, 0, 2] acting on a bitstring of length 4 swaps bit locations 0 & 1, leaves 2 in the same place and deletes location 3. :param indices: New locations for readout bits. :type indices: List[int] :return: New array corresponding to given permutation. :rtype: OutcomeArray """returnOutcomeArray.from_readouts(self.to_readouts()[...,indices])
[docs]defto_dict(self)->dict[str,Any]:"""Return a JSON serializable dictionary representation of the OutcomeArray. :return: JSON serializable dictionary :rtype: Dict[str, Any] """return{"width":self.width,"array":self.tolist()}
[docs]@classmethoddeffrom_dict(cls,ar_dict:dict[str,Any])->"OutcomeArray":"""Create an OutcomeArray from JSON serializable dictionary (as created by `to_dict`). :param dict: Dictionary representation of OutcomeArray. :type indices: Dict[str, Any] :return: Instance of OutcomeArray :rtype: OutcomeArray """returnOutcomeArray(np.array(ar_dict["array"],dtype=np.uint8),width=ar_dict["width"])
[docs]defreadout_counts(ctr:Counter[OutcomeArray],)->Counter[tuple[int,...]]:"""Convert counts from :py:class:`OutcomeArray` types to tuples of ints."""returnCounter({tuple(map(int,oa.to_readout())):int(n)foroa,ninctr.items()})