# This code is part of Mthree.
#
# (C) Copyright IBM 2021.
#
# 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.
# pylint: disable=no-name-in-module
"""
Utility functions
-----------------
.. autosummary::
:toctree: ../stubs/
final_measurement_mapping
expval
stddev
expval_and_stddev
marginal_distribution
"""
import numpy as np
from qiskit.result import marginal_distribution as marg_dist
from mthree.exceptions import M3Error
from mthree.classes import (
QuasiDistribution,
ProbDistribution,
QuasiCollection,
ProbCollection,
)
[docs]
def final_measurement_mapping(circuit):
"""Return the final measurement mapping for the circuit.
Dict keys label measured qubits, whereas the values indicate the
classical bit onto which that qubits measurement result is stored.
Parameters:
circuit (QuantumCircuit or list): Input Qiskit QuantumCircuit or circuits.
Returns:
dict or list: Mapping of classical bits to qubits for final measurements.
"""
given_list = False
if isinstance(circuit, (list, np.ndarray)):
given_list = True
if not given_list:
circuit = [circuit]
maps_out = [_final_measurement_mapping(circ) for circ in circuit]
if not given_list:
return maps_out[0]
return maps_out
[docs]
def marginal_distribution(dist, indices, mapping=None):
"""Grab the marginal counts from a given distribution.
If an operator is passed for the `indices` then the position of the
non-identity elements in the string will be used to set the indices
to marginalize over.
The mapping passed will be marginalized so that it can be directly
used in applying the correction. The type of mapping at output is
the same as that input.
Parameters:
dist (dict): Input distribution
indices (array_like or str): Indices (qubits) to keep or operator string
mapping (dict or array_like): Optional, final measurement mapping.
Returns:
dict: Marginal distribution
list or dict: The reduced mapping if an optional mapping (list or dict) is given
Raises:
M3Error: Operator length does not equal bit-string length
M3Error: One or more indices is out of bounds
"""
key_len = len(next(iter(dist)))
if isinstance(indices, str):
indices = indices.upper()
if len(indices) != key_len:
raise M3Error(
"Operator length does not equal distribution bit-string length."
)
indices = [
(key_len - kk - 1)
for kk in range(key_len - 1, -1, -1)
if indices[kk] != "I"
]
out_dist = marg_dist(dist, indices)
if mapping:
if isinstance(mapping, list):
out_mapping = [mapping[kk] for kk in indices]
else:
# mapping is a dict
out_mapping = {}
for idx, ind in enumerate(indices):
out_mapping[idx] = mapping[ind]
return out_dist, out_mapping
return out_dist
def _final_measurement_mapping(circuit):
"""Return the measurement mapping for the circuit.
Dict keys label classical bits, whereas the values indicate the
physical qubits that are measured to produce those bit values.
Parameters:
circuit (QuantumCircuit): Input Qiskit QuantumCircuit.
Returns:
dict: Mapping of classical bits to qubits for final measurements.
"""
active_qubits = list(range(circuit.num_qubits))
active_cbits = list(range(circuit.num_clbits))
# Map registers to ints
qint_map = {}
for idx, qq in enumerate(circuit.qubits):
qint_map[qq] = idx
cint_map = {}
for idx, qq in enumerate(circuit.clbits):
cint_map[qq] = idx
# Find final measurements starting in back
qmap = []
cmap = []
for item in circuit._data[::-1]:
if item[0].name == "measure":
cbit = cint_map[item[2][0]]
qbit = qint_map[item[1][0]]
if cbit in active_cbits and qbit in active_qubits:
qmap.append(qbit)
cmap.append(cbit)
active_cbits.remove(cbit)
if not active_cbits or not active_qubits:
break
mapping = {}
if cmap and qmap:
for idx, qubit in enumerate(qmap):
mapping[cmap[idx]] = qubit
# Sort so that classical bits are in numeric order low->high.
mapping = dict(sorted(mapping.items(), key=lambda item: item[0]))
return mapping
def _expval_std(items, exp_ops="", method=0):
"""Compute expectation values from distributions.
Parameters:
items (list or dict or Counts or ProbDistribution or QuasiDistribution): Input
distributions.
exp_ops (str or dict or list): String or dict representation of diagonal qubit
operators used in computing the expectation value.
method (int): 0=expvals, 1=stddev, 2=expval and stddev.
Returns:
float : Expectation value.
tuple: Expectation value and stddev.
ndarray: Array of expectation values or stddev
list: List of expvals and stddev tuples.
Raises:
M3Error: Not a valid method.
"""
if method not in [0, 1, 2]:
raise M3Error("Invalid method int {} passed.".format(method))
got_list = False
if isinstance(items, list):
got_list = True
else:
items = [items]
if isinstance(exp_ops, list):
if not len(exp_ops) == len(items):
raise M3Error(
(
"exp_ops length ({}) does not match number "
+ "of items passed ({})."
).format(len(exp_ops), len(items))
)
else:
exp_ops = [exp_ops] * len(items)
if isinstance(items[0], (ProbCollection, QuasiCollection)):
if method == 0:
out = items.expval(exp_ops)
elif method == 1:
out = items.stddev()
else:
out = items.expval_and_stddev(exp_ops)
elif not isinstance(items[0], (ProbDistribution, QuasiDistribution)):
out = []
if method == 0:
for idx, it in enumerate(items):
out.append(ProbDistribution(it).expval(exp_ops[idx]))
out = np.asarray(out, dtype=np.float32)
elif method == 1:
for _, it in enumerate(items):
out.append(ProbDistribution(it).stddev())
out = np.asarray(out, dtype=np.float32)
else:
for idx, it in enumerate(items):
out.append(ProbDistribution(it).expval_and_stddev(exp_ops[idx]))
else:
out = []
if method == 0:
for idx, it in enumerate(items):
out.append(it.expval(exp_ops[idx]))
out = np.asarray(out, dtype=np.float32)
elif method == 1:
for _, it in enumerate(items):
out.append(it.stddev())
out = np.asarray(out, dtype=float)
else:
for idx, it in enumerate(items):
out.append(it.expval_and_stddev(exp_ops[idx]))
if not got_list:
return out[0]
return out
[docs]
def expval(items, exp_ops=""):
"""Compute expectation values from distributions.
.. versionadded:: 0.16.0
Parameters:
items (list or dict or Counts or ProbDistribution or QuasiDistribution): Input
distributions.
exp_ops (str or dict or list): String or dict representation of diagonal
qubit operators used in computing the expectation
value.
Returns:
float : Expectation value.
ndarray: Array of expectation values
Notes:
Cannot mix Counts and dicts with M3 Distributions in the same call.
The dict operator format is a sparse diagonal format
using bitstrings as the keys.
"""
return _expval_std(items, exp_ops=exp_ops, method=0)
[docs]
def stddev(items):
"""Compute expectation values from distributions.
.. versionadded:: 0.16.0
Parameters:
items (list or dict or Counts or ProbDistribution or QuasiDistribution): Input
distributions.
Returns:
float : Expectation value.
ndarray: Array of expectation values
Notes:
Cannot mix Counts and dicts with M3 Distributions in the same call.
"""
return _expval_std(items, method=1)
[docs]
def expval_and_stddev(items, exp_ops=""):
"""Compute expectation values from distributions.
.. versionadded:: 0.16.0
Parameters:
items (list or dict or Counts or ProbDistribution or QuasiDistribution): Input
distributions.
exp_ops (str or dict or list): String or dict representation of diagonal qubit
operators used in computing the expectation value.
Returns:
float : Expectation value.
ndarray: Array of expectation values
Notes:
Cannot mix Counts and dicts with M3 Distributions in the same call.
The dict operator format is a sparse diagonal format
using bitstrings as the keys.
"""
return _expval_std(items, exp_ops=exp_ops, method=2)
def counts_to_vector(counts):
"""Return probability vector from counts dict.
Parameters:
counts (dict): Input dict of counts.
Returns:
ndarray: 1D array of probabilities.
"""
num_bitstrings = len(counts)
shots = sum(counts.values())
vec = np.zeros(num_bitstrings, dtype=np.float32)
idx = 0
for val in counts.values():
vec[idx] = val / shots
idx += 1
return vec
def vector_to_quasiprobs(vec, counts):
"""Return dict of quasi-probabilities.
Parameters:
vec (ndarray): 1d vector of quasi-probabilites.
counts (dict): Dict of counts
Returns:
QuasiDistribution: dict of quasi-probabilities
"""
out_counts = {}
idx = 0
for key in counts:
out_counts[key] = vec[idx]
idx += 1
return QuasiDistribution(out_counts)