Source code for povm_toolbox.quantum_info.product_frame
# (C) Copyright IBM 2024.## This code is licensed under the Apache License, Version 2.0. You may# obtain a copy of this license in the LICENSE.txt file in the root directory# of this source tree or at http://www.apache.org/licenses/LICENSE-2.0.## Any modifications or derivative works of this code must retain this# copyright notice, and modified files need to carry a notice indicating# that they have been altered from the originals."""ProductFrame."""from__future__importannotationsimportmathimportsysimportwarningsfromcollections.abcimportSequencefromtypingimportGeneric,TypeVarifsys.version_info<(3,11):fromtyping_extensionsimportSelfelse:fromtypingimportSelf# pragma: no coverifsys.version_info<(3,12):fromtyping_extensionsimportoverrideelse:fromtypingimportoverride# pragma: no coverimportnumpyasnpfromqiskit.quantum_infoimportOperator,SparsePauliOpfrom.baseimportBaseFramefrom.multi_qubit_frameimportMultiQubitFrameT=TypeVar("T",bound=MultiQubitFrame)
[docs]classProductFrame(BaseFrame[tuple[int,...]],Generic[T]):r"""Class to represent a set of product frame operators. A product frame :math:`M` is made of local frames :math:`M1, M2, ...` acting on respective subsystems. Each global operator can be written as the tensor product of local operators, :math:`M_{k_1, k_2, ...} = M1_{k_1} \otimes M2_{k_2} \otimes \cdots`. .. note:: This is a base class which collects functionality common to various subclasses. As an end-user you would not use this class directly. Check out :mod:`povm_toolbox.quantum_info` for more general information. """def__init__(self,frames:dict[tuple[int,...],T])->None:"""Initialize from a mapping of local frames. Args: frames: a dictionary mapping from a tuple of subsystem indices to a local frame objects. Raises: ValueError: if any key in ``frames`` is not a tuple consisting of unique integers. In other words, every local frame must act on a distinct set of subsystem indices which do not overlap with each other. ValueError: if any key in ``frames`` re-uses a previously used subsystem index. In other words, all local frames must act on mutually exclusive subsystem indices. ValueError: if any key in ``frames`` does not specify the number of subsystem indices, which matches the number of systems acted upon by that local frame (:meth:`MultiQubitFrame.num_subsystems`). """subsystem_indices=set()self._dimension=1self._num_operators=1shape:list[int]=[]foridx,frameinframes.items():idx_set=set(idx)iflen(idx)!=len(idx_set):raiseValueError("The subsystem indices acted upon by any local frame must be mutually "f"exclusive. The index '{idx}' does not fulfill this criterion.")ifany(iinsubsystem_indicesforiinidx):raiseValueError("The subsystem indices acted upon by all the local frames must be mutually "f"exclusive. However, one of the indices in '{idx}' was already encountered ""before.")iflen(idx_set)!=frame.num_subsystems:raiseValueError("The number of subsystem indices for a local frame must match the number of ""subsystems which it acts upon. This is not satisfied for the local frame "f"specified to act on subsystems '{idx}' but having support on "f"'{frame.num_subsystems}' subsystems.")subsystem_indices.update(idx_set)self._dimension*=frame.dimensionself._num_operators*=frame.num_operatorsshape.append(frame.num_operators)self._informationally_complete:bool=all([frame.informationally_completeforframeinframes.values()])self._frames=framesself._shape:tuple[int,...]=tuple(shape)self._check_validity()def__repr__(self)->str:"""Return the string representation of a :class:`.ProductFrame` instance."""f_repr="\n "+"\n ".join(f"{name}: {value}"forname,valueinself._frames.items())return(f"{self.__class__.__name__}(num_subsystems={self.num_subsystems})"f"<{','.join(map(str,self.shape))}>:{f_repr}")
[docs]@classmethoddeffrom_list(cls,frames:Sequence[T])->Self:"""Construct a :class:`.ProductFrame` from a list of :class:`.MultiQubitFrame` objects. This is a convenience method to simplify the construction of a :class:`.ProductFrame` for the cases in which the local frame objects act on a sequential order of subsystems. In other words, this method converts the sequence of frames to a dictionary of frames in accordance with the input to :meth:`.ProductFrame.__init__` by using the positions along the sequence as subsystem indices. Below are some examples: >>> from qiskit.quantum_info import Operator >>> from povm_toolbox.quantum_info import SingleQubitPOVM, MultiQubitPOVM, ProductPOVM >>> sqp = SingleQubitPOVM([Operator.from_label("0"), Operator.from_label("1")]) >>> product = ProductPOVM.from_list([sqp, sqp]) >>> # is equivalent to >>> product = ProductPOVM({(0,): sqp, (1,): sqp}) >>> mqp = MultiQubitPOVM( ... [ ... Operator.from_label("00"), ... Operator.from_label("01"), ... Operator.from_label("10"), ... Operator.from_label("11"), ... ] ... ) >>> product = ProductPOVM.from_list([mqp, mqp]) >>> # is equivalent to >>> product = ProductPOVM({(0, 1): mqp, (2, 3): mqp}) >>> product = ProductPOVM.from_list([sqp, sqp, mqp]) >>> # is equivalent to >>> product = ProductPOVM({(0,): sqp, (1,): sqp, (2, 3): mqp}) >>> product = ProductPOVM.from_list([sqp, mqp, sqp]) >>> # is equivalent to >>> product = ProductPOVM({(0,): sqp, (1, 2): mqp, (3,): sqp}) Args: frames: a sequence of :class:`.MultiQubitFrame` objects. Returns: A new :class:`.ProductFrame` instance. """frame_dict={}idx=0forframeinframes:prev_idx=idxidx+=frame.num_subsystemsframe_dict[tuple(range(prev_idx,idx))]=framereturncls(frame_dict)
@propertydefinformationally_complete(self)->bool:"""If the frame spans the entire Hilbert space."""returnself._informationally_complete@propertydefdimension(self)->int:"""The dimension of the Hilbert space on which the effects act."""returnself._dimension@propertydefnum_operators(self)->int:"""The number of effects of the frame."""returnself._num_operators@propertydefshape(self)->tuple[int,...]:"""Give the number of operators per sub-system."""returnself._shape@propertydefsub_systems(self)->list[tuple[int,...]]:"""Give the number of operators per sub-system."""returnlist(self._frames.keys())def_check_validity(self)->None:"""Check if frame axioms are fulfilled for all local frames. .. note:: This raises whatever errors the local frames' methods may raise. """forpovminself._frames.values():povm._check_validity()def__getitem__(self,sub_system:tuple[int,...])->T:r"""Return the :class:`.MultiQubitFrame` acting on the specified sub-system. Args: sub_system: indicate the sub-system on which the queried frame acts on. Returns: The :class:`.MultiQubitFrame` acting on the specified sub-system. """returnself._frames[sub_system]def__len__(self)->int:"""Return the number of outcomes of the product frame."""returnself.num_operatorsdef_trace_of_prod(self,operator:SparsePauliOp,frame_op_idx:tuple[int,...])->float:"""Return the trace of the product of a Hermitian operator with a specific frame operator. Args: operator: the input operator to multiply with a frame operator. frame_op_idx: the label specifying the frame operator to use. The frame operator is labeled by a tuple of integers (one index per local frame). Returns: The trace of the product of the input operator with the specified frame operator. Raises: IndexError: when the provided outcome label (tuple of integers) has a number of integers which does not correspond to the number of local frames making up the product frame. IndexError: when a local index exceeds the number of operators of the corresponding local frame. ValueError: when the output is not a real number. """p_idx=0.0+0.0j# Second, we iterate over our input operator, ``operator``.forlabel,op_coeffinoperator.label_iter():summand=op_coeff# Third, we iterate over the POVMs stored inside the ProductPOVM.# - ``j`` is the index of the POVM inside the ``ProductPOVM``. This encodes the axis# of the high-dimensional array ``p_init`` along which this local POVM is encoded.# - ``idx`` are the qubit indices on which this local POVM acts.# - ``povm`` is the actual local POVM object.forj,(idx,povm)inenumerate(self._frames.items()):# Extract the local Pauli term on the qubit indices of this local POVM.sublabel="".join(label[-(i+1)]foriinidx)# Try to obtain the coefficient of the local POVM for this local Pauli term.try:local_idx=frame_op_idx[j]coeff=povm.pauli_operators[local_idx][sublabel]exceptKeyError:# If it does not exist, the current summand becomes 0 because it would be# multiplied by 0.summand=0.0# In this case we can break the iteration over the remaining local POVMs.breakexceptIndexErrorasexc:iflen(frame_op_idx)<=j:raiseIndexError(f"The outcome label {frame_op_idx} does not match the expected shape. "f"It is supposed to contain {len(self._frames)} integers, but has "f"{len(frame_op_idx)}.")fromexcifpovm.num_operators<=frame_op_idx[j]:raiseIndexError(f"Outcome index '{frame_op_idx[j]}' is out of range for the local POVM"f" acting on subsystems {idx}. This POVM has {povm.num_operators}"" outcomes.")fromexcraiseexcelse:# If the label does exist, we multiply the coefficient into our summand.# The factor 2^N_qubit comes from Tr[(P_1...P_N)^2] = 2^N.summand*=coeff*2**povm.num_subsystems# Once we have finished computing our summand, we add it into ``p_init``.p_idx+=summandifabs(p_idx.imag)>operator.atol:warnings.warn(f"Expected a real number, instead got {p_idx}.",stacklevel=2)returnfloat(p_idx.real)
[docs]@overridedefanalysis(self,hermitian_op:SparsePauliOp|Operator,frame_op_idx:tuple[int,...]|set[tuple[int,...]]|None=None,)->float|dict[tuple[int,...],float]|np.ndarray:ifnotisinstance(hermitian_op,SparsePauliOp):# Convert the provided operator to a Pauli operator.hermitian_op=SparsePauliOp.from_operator(hermitian_op)# Assert matching operator and POVM sizes.ifhermitian_op.num_qubits!=self.num_subsystems:raiseValueError(f"Size of the operator ({hermitian_op.num_qubits}) does not match the size of the "f"povm ({math.log2(self.dimension)}).")# If frame_op_idx is ``None``, it means all outcomes are queriedifframe_op_idxisNone:# Extract the number of outcomes for each local POVM.# Create the output probability array as a high-dimensional matrix. This matrix will# have its number of dimensions equal to the number of POVMs stored inside the# ProductPOVM. The length of each dimension is given by the number of outcomes of the# POVM encoded along it.p_init:np.ndarray=np.zeros(self.shape,dtype=float)# First, we iterate over all the positions of ``p_init``. This corresponds to the# different probabilities for the different outcomes whose probability we want to# compute.# - ``m`` is the multi-dimensional index into the high-dimensional ``p_init`` array.form,_innp.ndenumerate(p_init):p_init[m]=self._trace_of_prod(hermitian_op,m)returnp_initifisinstance(frame_op_idx,set):return{idx:self._trace_of_prod(hermitian_op,idx)foridxinframe_op_idx}ifisinstance(frame_op_idx,tuple):returnself._trace_of_prod(hermitian_op,frame_op_idx)raiseTypeError("Wrong type for ``frame_op_idx``.")