Source code for povm_toolbox.post_processor.dual_from_empirical_frequencies

# (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.

"""dual_from_empirical_frequencies."""

from __future__ import annotations

from typing import cast

import numpy as np
from qiskit.quantum_info import DensityMatrix, SparsePauliOp, Statevector

from povm_toolbox.post_processor.povm_post_processor import POVMPostProcessor
from povm_toolbox.quantum_info import ProductDual, ProductPOVM
from povm_toolbox.quantum_info.base import BaseDual


[docs] def dual_from_empirical_frequencies( povm_post_processor: POVMPostProcessor, *, loc: int | tuple[int, ...] | None = None, bias: list[float] | float | None = None, ansatz: list[SparsePauliOp | DensityMatrix | Statevector] | SparsePauliOp | DensityMatrix | Statevector | None = None, ) -> BaseDual: """Return the Dual frame of ``povm`` based on the frequencies of the sampled outcomes. Given outcomes sampled from a :class:`.ProductPOVM`, each local Dual frame is parametrized with the alpha-parameters set as the marginal outcome frequencies. For stability, the (local) empirical frequencies can be biased towards the (marginal) outcome probabilities of an ``ansatz`` state. Args: povm_post_processor: the :class:`.POVMPostProcessor` object from which to extract the :attr:`.POVMPostProcessor.povm` and the empirical frequencies to build the Dual frame. loc: index of the results to use. This is relevant if multiple sets of parameter values were supplied to the sampler in the same Pub. If ``None``, it is assumed that the supplied circuit was not parametrized or that a unique set of parameter values was supplied. In this case, ``loc`` is trivially set to 0. bias: the strength of the bias towards the outcome distribution of the ``ansatz`` state. If it is a ``float``, the same bias is applied to each (local) sub-system. If it is a list of ``float``, a specific bias is applied to each sub-system. If ``None``, the bias for each sub-system is set to be the number of outcomes of the POVM acting on this sub-system. ansatz: list of quantum states for each local sub-system. If a single (local) quantum state is supplied, it is used for all sub-systems. From these states, the local outcome probability distributions are computed for each sub-system. The empirical marginal frequencies are biased towards these distributions. If None, the fully mixed state is used for each sub-system. Raises: NotImplementedError: if :attr:`.POVMPostProcessor.povm` is not a :class:`.ProductPOVM` instance. ValueError: if ``loc`` is ``None`` and :attr:`.POVMPostProcessor.counts` stores more than a single counter (i.e., multiple sets of parameter values were supplied to the sampler in a single Pub). ValueError: if ``bias`` is a list but its length does not match the number of local POVMs forming the product POVM. ValueError: if ``ansatz`` is a list but its length does not match the number of local POVMs forming the product POVM. Returns: The Dual frame based on the empirical outcome frequencies from the post-processed result. """ povm = povm_post_processor.povm if not isinstance(povm, ProductPOVM): raise NotImplementedError("This method is only implemented for `ProductPOVM` objects.") if loc is None: if povm_post_processor.counts.shape == (1,): loc = (0,) else: raise ValueError( "`loc` has to be specified if the POVM post-processor stores" " more than one counter (i.e., if multiple sets of parameter" " values were supplied to the sampler in a single pub). The" f" array of counters is of shape {povm_post_processor.counts.shape}." ) counts = povm_post_processor.counts[loc] if isinstance(bias, list) and len(bias) != len(povm.sub_systems): raise ValueError( f"A list of biases was submitted but its length ({len(bias)})" f" does not match the number of local POVMs ({len(povm.sub_systems)})." ) if isinstance(ansatz, list) and len(ansatz) != len(povm.sub_systems): raise ValueError( "A list of ansatz local states was submitted but its length" f" ({len(ansatz)}) does not match the number of local POVMs" f" ({len(povm.sub_systems)})." ) marginals = [np.zeros(subsystem_shape) for subsystem_shape in povm.shape] # Computing marginals shots = sum(counts.values()) for outcome, count in counts.items(): for i, k_i in enumerate(outcome): marginals[i][k_i] += count / shots alphas = [] # Computing alphas for each subsystem for i, sub_system in enumerate(povm.sub_systems): sub_povm = povm[sub_system] dim = sub_povm.dimension ansatz_state = ( DensityMatrix(np.eye(dim) / dim) if ansatz is None else (ansatz[i] if isinstance(ansatz, list) else ansatz) ) sub_bias = ( sub_povm.num_outcomes if bias is None else (bias[i] if isinstance(bias, list) else bias) ) sub_alphas = shots * marginals[i] + sub_bias * cast( np.ndarray, sub_povm.get_prob(ansatz_state) ) alphas.append(tuple(sub_alphas / (shots + sub_bias))) # Building ProductDual from frequencies return ProductDual.build_dual_from_frame(povm, alphas=tuple(alphas))