"""Module defining classes that store results of circuit execution."""
import collections
import warnings
from typing import Optional, Union
import numpy as np
from numpy.typing import ArrayLike
from qibo import __version__, gates
from qibo.config import raise_error
from qibo.measurements import apply_bitflips, frequencies_to_binary
def load_result(filename: str):
"""Loads the results of a circuit execution saved to disk.
Args:
filename (str): Path to the file containing the results.
Returns:
:class:`qibo.result.QuantumState` or
:class:`qibo.result.MeasurementOutcomes` or
:class:`qibo.result.CircuitResult`: result of circuit execution saved to disk,
depending on saved filed.
"""
payload = np.load(filename, allow_pickle=True).item()
return globals()[payload.pop("dtype")].from_dict(payload)
[docs]class QuantumState:
"""Data structure to represent the final state after circuit execution.
Args:
state (ndarray): Input quantum state as ``ndarray``.
backend (:class:`qibo.backends.abstract.Backend`): Backend used for the calculations.
If not provided, the global backend is going to be used.
"""
def __init__(self, state, backend=None):
from qibo.backends import ( # pylint: disable=import-outside-toplevel
_check_backend,
)
self.backend = _check_backend(backend)
self.density_matrix = len(state.shape) == 2
self.nqubits = int(np.log2(state.shape[0]))
self._state = state
[docs] def symbolic(self, decimals: int = 5, cutoff: float = 1e-10, max_terms: int = 20):
"""Dirac notation representation of the state in the computational basis.
Args:
decimals (int, optional): Number of decimals for the amplitudes.
Defaults to :math:`5`.
cutoff (float, optional): Amplitudes with absolute value smaller than the
cutoff are ignored from the representation. Defaults to ``1e-10``.
max_terms (int, optional): Maximum number of terms to print. If the state
contains more terms they will be ignored. Defaults to :math:`20`.
Returns:
str: String representing the state in the computational basis.
"""
terms = self.backend.calculate_symbolic(
self._state, self.nqubits, decimals, cutoff, max_terms
)
return " + ".join(terms)
[docs] def state(self, numpy: bool = False):
"""State's tensor representation as a backend tensor.
.. note::
If the state has Hamming weight :math:`k` and is computed using the
``HammingWeightBackend``, its dimension is :math:`d = \\binom{n}{k}`,
where :math:`n` is the number of qubits.
Args:
numpy (bool, optional): If ``True`` the returned tensor will be a ``numpy`` array,
otherwise it will follow the backend tensor type.
Defaults to ``False``.
Returns:
The state in the computational basis.
"""
if numpy:
return np.array(self._state.tolist())
return self._state
[docs] def probabilities(self, qubits: Optional[Union[list, set]] = None):
"""Calculates measurement probabilities by tracing out qubits.
When noisy model is applied to a circuit and `circuit.density_matrix=False`,
this method returns the average probability resulting from
repeated execution. This probability distribution approximates the
exact probability distribution obtained when `circuit.density_matrix=True`.
Args:
qubits (list or set, optional): Set of qubits that are measured.
If ``None``, ``qubits`` equates the total number of qubits.
Defauts to ``None``.
Returns:
(np.ndarray): Probabilities over the input qubits.
"""
if qubits is None:
qubits = tuple(range(self.nqubits))
return self.backend.calculate_probabilities(
self._state, qubits, self.nqubits, density_matrix=self.density_matrix
)
def __str__(self):
return self.symbolic()
[docs] def to_dict(self):
"""Returns a dictonary containinig all the information needed to
rebuild the ``QuantumState``"""
return {
"state": self.state(numpy=True),
"dtype": self.__class__.__name__,
"qibo": __version__,
}
[docs] def dump(self, filename: str):
"""Writes to file the ``QuantumState`` for future reloading.
Args:
filename (str): Path to the file to write to.
"""
with open(filename, "wb") as f:
np.save(f, self.to_dict())
[docs] @classmethod
def from_dict(cls, payload: dict):
"""Builds a ``QuantumState`` object starting from a dictionary.
Args:
payload (dict): Dictionary containing all the information
to load the ``QuantumState`` object.
Returns:
:class:`qibo.result.QuantumState`: Quantum state object..
"""
from qibo.backends import ( # pylint: disable=import-outside-toplevel
construct_backend,
)
backend = construct_backend("numpy")
return cls(payload.get("state"), backend=backend)
[docs] @classmethod
def load(cls, filename: str):
"""Builds the ``QuantumState`` object stored in a file.
Args:
filename (str): Path to the file containing the ``QuantumState``.
Returns:
:class:`qibo.result.QuantumState`: Quantum state object.
"""
payload = np.load(filename, allow_pickle=True).item()
return cls.from_dict(payload)
[docs]class MeasurementOutcomes:
"""Object to store the outcomes of measurements after circuit execution.
Args:
measurements (:class:`qibo.gates.M`): Measurement gates.
backend (:class:`qibo.backends.abstract.Backend`): Backend used for the calculations.
If ``None``, then the current backend is used. Defaults to ``None``.
probabilities (np.ndarray): Use these probabilities to generate samples and frequencies.
samples (np.darray): Use these samples to generate probabilities and frequencies.
nshots (int): Number of shots used for samples, probabilities and frequencies generation.
nqubits (int, optional): Total number of qubits in the circuit. When set,
:meth:`probabilities` with ``qubits=None`` returns probabilities over
all circuit qubits (not just the measured ones). If ``None``, defaults
to the number of measured qubits. Defaults to ``None``.
"""
def __init__(
self,
measurements,
backend=None,
probabilities=None,
samples: Optional[int] = None,
nshots: int = 1000,
nqubits: Optional[int] = None,
):
self.backend = backend
self.measurements = measurements
self.nshots = nshots
self._nqubits = nqubits
self._measurement_gate = None
self._probs = probabilities
self._samples = samples
self._frequencies = None
self._repeated_execution_frequencies = None
if samples is not None:
for m in measurements:
indices = [self.measurement_gate.qubits.index(q) for q in m.qubits]
m.result.register_samples(samples[:, indices])
[docs] def frequencies(self, binary: bool = True, registers: bool = False):
"""Returns the frequencies of measured samples.
Args:
binary (bool, optional): If ``True``, returns frequency keys in binary form.
If ``False``, returns them in decimal form. Defaults to ``True``.
registers (bool, optional): Group frequencies according to registers.
Defaults to ``False``.
Returns:
A :class:`collections.Counter` where the keys are the observed values
and the values the corresponding frequencies, that is the number
of times each measured value/bitstring appears.
If ``binary`` is ``True``
the keys of the :class:`collections.Counter` are in binary form,
as strings of :math:`0` and :math`1`.
If ``binary`` is ``False``
the keys of the :class:`collections.Counter` are integers.
If ``registers`` is ``True``
a `dict` of :class:`collections.Counter` is returned where keys are
the name of each register.
If ``registers`` is ``False``
a single :class:`collections.Counter` is returned which contains samples
from all the measured qubits, independently of their registers.
"""
qubits = self.measurement_gate.qubits
if self._repeated_execution_frequencies is not None:
if binary:
return self._repeated_execution_frequencies
return collections.Counter(
{int(k, 2): v for k, v in self._repeated_execution_frequencies.items()}
)
if self._frequencies is None:
if self.measurement_gate.has_bitflip_noise() and not self.has_samples():
self._samples = self.samples()
if not self.has_samples():
# generate new frequencies
self._frequencies = self.backend.sample_frequencies(
self._probs, self.nshots
)
# register frequencies to individual gate ``MeasurementResult``
qubit_map = {q: i for i, q in enumerate(qubits)}
binary_frequencies = frequencies_to_binary(
self._frequencies, len(qubits)
)
for gate in self.measurements:
rfreqs = collections.Counter()
for bitstring, freq in binary_frequencies.items():
idx = 0
rqubits = gate.target_qubits
for i, q in enumerate(rqubits):
if int(bitstring[qubit_map.get(q)]):
idx += 2 ** (len(rqubits) - i - 1)
rfreqs[idx] += freq
gate.result.register_frequencies(rfreqs)
else:
self._frequencies = self.backend.calculate_frequencies(
self.samples(binary=False)
)
if registers:
return {
gate.register_name: gate.result.frequencies(
binary, backend=self.backend
)
for gate in self.measurements
}
if binary:
return frequencies_to_binary(self._frequencies, len(qubits))
return self._frequencies
[docs] def probabilities(self, qubits: Optional[Union[list, set]] = None) -> ArrayLike:
"""Calculate the probabilities as frequencies / nshots
Args:
qubits (list or set, optional): Set of qubits for which to compute
probabilities. If ``None`` and ``nqubits`` was provided at
construction, probabilities are returned over all circuit
qubits; otherwise only over the measured qubits.
Defaults to ``None``.
Returns:
ArrayLike: The array containing the probabilities of the requested qubits.
"""
measured_qubits = self.measurement_gate.qubits
n_measured = len(measured_qubits)
nqubits = self._nqubits if self._nqubits is not None else n_measured
if qubits is None:
qubits = range(nqubits)
elif set(qubits).issubset(set(range(nqubits))):
pass # keep qubits as-is; they index into the full qubit space
else:
raise_error(
RuntimeError,
f"Asking probabilities for qubits {qubits}, "
+ f"but the system only has {nqubits} qubits "
+ f"(measured qubits: {measured_qubits}).",
)
# Build probability array in the measured-qubit space
if self._probs is not None and not self.measurement_gate.has_bitflip_noise():
measured_probs = self._probs
else:
measured_probs = [0] * 2**n_measured
for state, freq in self.frequencies(binary=False).items():
measured_probs[state] = freq / self.nshots
measured_probs = self.backend.cast(
measured_probs, dtype=self.backend.float64
)
self._probs = measured_probs
if nqubits == n_measured:
# No unmeasured qubits: use the standard path
return self.backend.calculate_probabilities(
self.backend.sqrt(measured_probs),
list(qubits),
n_measured,
)
# Expand measured probabilities into the full circuit qubit space.
# Unmeasured qubits are placed in the |0⟩ state, consistent with the
# standard qubit initialisation convention.
full_probs = self.backend.zeros(2**nqubits, dtype=self.backend.float64)
for measured_state in range(2**n_measured):
p = float(measured_probs[measured_state])
if p == 0:
continue
# Map the measured-state bits into the full-state index,
# leaving unmeasured qubit bits as 0.
full_state = 0
m_bit_idx = 0
for bit_pos in range(nqubits):
if bit_pos in measured_qubits:
bit_val = (measured_state >> (n_measured - 1 - m_bit_idx)) & 1
m_bit_idx += 1
full_state |= bit_val << (nqubits - 1 - bit_pos)
full_probs[full_state] = p
return self.backend.calculate_probabilities(
self.backend.sqrt(full_probs),
list(qubits),
nqubits,
)
[docs] def has_samples(self):
"""Check whether the samples are available already.
Returns:
(bool): ``True`` if the samples are available, ``False`` otherwise.
"""
return self.measurements[0].result.has_samples() or self._samples is not None
[docs] def samples(self, binary: bool = True, registers: bool = False):
"""Returns raw measurement samples.
Args:
binary (bool, optional): Return samples in binary or decimal form.
registers (bool, optional): Group samples according to registers.
Returns:
If ``binary`` is ``True``
samples are returned in binary form as a tensor
of shape ``(nshots, n_measured_qubits)``.
If ``binary`` is ``False``
samples are returned in decimal form as a tensor
of shape ``(nshots,)``.
If ``registers`` is ``True``
samples are returned in a ``dict`` where the keys are the register
names and the values are the samples tensors for each register.
If ``registers`` is ``False``
a single tensor is returned which contains samples from all the
measured qubits, independently of their registers.
"""
qubits = self.measurement_gate.target_qubits
if self._samples is None:
if self.measurements[0].result.has_samples():
self._samples = self.backend.concatenate(
[
gate.result.samples(backend=self.backend)
for gate in self.measurements
],
axis=1,
)
else:
if self._frequencies is not None:
# generate samples that respect the existing frequencies
frequencies = self.frequencies(binary=False)
samples = [
self.backend.repeat(x, f) for x, f in frequencies.items()
]
samples = self.backend.concatenate(samples)
self.backend.shuffle(samples)
samples = self.backend.cast(samples, dtype=self.backend.int64)
else:
# generate new samples
samples = self.backend.sample_shots(self._probs, self.nshots)
samples = self.backend.samples_to_binary(samples, len(qubits))
if self.measurement_gate.has_bitflip_noise():
p0, p1 = self.measurement_gate.bitflip_map
bitflip_probabilities = self.backend.cast(
[
[p0.get(q) for q in qubits],
[p1.get(q) for q in qubits],
],
dtype=self.backend.float64,
)
samples = self.backend.apply_bitflips(
samples, bitflip_probabilities
)
# register samples to individual gate ``MeasurementResult``
qubit_map = self.measurement_gate.target_qubits
qubit_map = dict(zip(qubit_map, range(len(qubit_map))))
self._samples = samples
for gate in self.measurements:
rqubits = tuple(qubit_map.get(q) for q in gate.target_qubits)
gate.result.register_samples(self._samples[:, rqubits])
if registers:
return {
gate.register_name: gate.result.samples(binary, backend=self.backend)
for gate in self.measurements
}
if binary:
return self._samples
return self.backend.samples_to_decimal(self._samples, len(qubits))
@property
def measurement_gate(self):
"""Single measurement gate containing all measured qubits.
Useful for sampling all measured qubits at once when simulating.
"""
if self._measurement_gate is None:
for gate in self.measurements:
if self._measurement_gate is None:
self._measurement_gate = gates.M(
*gate.init_args, **gate.init_kwargs
)
else:
self._measurement_gate.add(gate)
return self._measurement_gate
[docs] def apply_bitflips(self, p0: float, p1: Optional[float] = None):
"""Apply bitflips to the measurements with probabilities `p0` and `p1`
Args:
p0 (float): Probability of the 0->1 flip.
p1 (float): Probability of the 1->0 flip.
"""
return apply_bitflips(self, p0, p1)
[docs] def expectation_from_samples(self, observable):
"""Computes the real expectation value of a diagonal observable from frequencies.
Args:
observable (Hamiltonian/SymbolicHamiltonian): diagonal observable in the
computational basis.
Returns:
(float): expectation value from samples.
"""
return observable.expectation_from_samples(self.frequencies())
[docs] def to_dict(self):
"""Returns a dictonary containinig all the information needed to rebuild the
:class:`qibo.result.MeasurementOutcomes`."""
args = {
"measurements": [m.to_json() for m in self.measurements],
"probabilities": self._probs,
"samples": self._samples,
"nshots": self.nshots,
"nqubits": self._nqubits,
"dtype": self.__class__.__name__,
"qibo": __version__,
}
return args
[docs] def dump(self, filename: str):
"""Writes to file the :class:`qibo.result.MeasurementOutcomes` for future reloading.
Args:
filename (str): Path to the file to write to.
"""
with open(filename, "wb") as f:
np.save(f, self.to_dict())
[docs] @classmethod
def from_dict(cls, payload: dict):
"""Builds a :class:`qibo.result.MeasurementOutcomes` object starting from a dictionary.
Args:
payload (dict): Dictionary containing all the information to load the
:class:`qibo.result.MeasurementOutcomes` object.
Returns:
:class:`qibo.result.MeasurementOutcomes`: Object storing the measurement outcomes.
"""
from qibo.backends import construct_backend # pylint: disable=C0415
if payload["probabilities"] is not None and payload["samples"] is not None:
warnings.warn(
"Both `probabilities` and `samples` found, discarding the `probabilities`"
+ "and building out of the `samples`."
)
payload.pop("probabilities")
backend = construct_backend("numpy")
measurements = [gates.M.load(m) for m in payload.get("measurements")]
return cls(
measurements,
backend=backend,
probabilities=payload.get("probabilities"),
samples=payload.get("samples"),
nshots=payload.get("nshots"),
nqubits=payload.get("nqubits"),
)
[docs] @classmethod
def load(cls, filename: str):
"""Builds the :class:`qibo.result.MeasurementOutcomes` object stored in a file.
Args:
filename (str): Path to the file containing the
:class:`qibo.result.MeasurementOutcomes`.
Returns:
:class:`qibo.result.MeasurementOutcomes`: instance of the
``MeasurementOutcomes`` class.
"""
payload = np.load(filename, allow_pickle=True).item()
return cls.from_dict(payload)
[docs] @classmethod
def from_samples(
cls,
samples,
qubits: Optional[Union[list[int], tuple[int, ...]]] = None,
backend: Optional["Backend"] = None,
):
"""Constructs a :class:`qibo.result.MeasurementOutcomes` directly from
a binary samples array.
This is useful when building measurement outcomes from experimental data
without needing to manually construct measurement gates.
Args:
samples (ArrayLike): Binary array of shape ``(nshots, nqubits)``.
where each row is a measurement outcome with 0/1 values.
qubits (tuple or list, optional): Qubit indices for the measured
qubits. If ``None``, defaults to ``(0, 1, ..., nqubits - 1)``.
Defaults to ``None``.
backend (:class:`qibo.backends.abstract.Backend`, optional): Backend
used for calculations. If ``None``, the current default backend
is used. Defaults to ``None``.
Returns:
:class:`qibo.result.MeasurementOutcomes`: Object storing the
measurement outcomes.
Example:
.. code-block:: python
import numpy as np
from qibo.result import MeasurementOutcomes
samples = np.array([[0, 1], [1, 0], [1, 1]])
result = MeasurementOutcomes.from_samples(samples)
print(result.frequencies()) # Counter({'01': 1, '10': 1, '11': 1})
"""
from qibo.backends import ( # pylint: disable=import-outside-toplevel
_check_backend,
)
backend = _check_backend(backend)
samples = backend.cast(samples, dtype=backend.int8)
if samples.ndim != 2:
raise_error(
ValueError,
f"samples must be a 2D array of shape (nshots, nqubits), "
f"got shape {samples.shape}.",
)
if not backend.all((samples == 0) | (samples == 1)):
raise_error(
ValueError, "samples array must contain only binary values (0 or 1)."
)
nshots, nqubits = samples.shape
if qubits is None:
qubits = tuple(range(nqubits))
else:
qubits = tuple(qubits)
if len(qubits) != nqubits:
raise_error(
ValueError,
f"Length of qubits ({len(qubits)}) does not match the number "
f"of columns in samples ({nqubits}).",
)
if len(set(qubits)) != len(qubits):
raise_error(ValueError, "Qubit indices must be unique.")
measurements = [gates.M(*qubits)]
return cls(measurements, backend=backend, samples=samples, nshots=nshots)
[docs] @classmethod
def from_frequencies(
cls,
frequencies,
nqubits: Optional[int] = None,
qubits: Optional[Union[list[int], tuple[int, ...]]] = None,
seed: Optional[int] = None,
backend: Optional["Backend"] = None,
):
"""Constructs a :class:`qibo.result.MeasurementOutcomes` from a
frequencies dictionary.
The frequencies are expanded into a binary samples array, shuffled to
avoid ordering artifacts, and passed through the standard construction
path so that all methods (``samples()``, ``frequencies()``,
``probabilities()``) work correctly.
Args:
frequencies (dict or Counter): Mapping from measurement outcomes to
their counts. Keys can be binary strings (e.g. ``"010"``) or
integers (e.g. ``2``). Values are non-negative integer counts.
nqubits (int, optional): Total number of qubits in the circuit.
When binary-string keys are used, the number of *measured*
qubits is inferred from the key length; ``nqubits`` may be
larger than or equal to that length, and the extra qubits are
treated as unmeasured.
When integer keys are used *without* ``qubits``, ``nqubits``
is required and is interpreted as the number of *measured*
qubits (backward-compatible behaviour).
Defaults to ``None``.
qubits (tuple or list, optional): Qubit indices for the measured
qubits. If ``None``, defaults to ``(0, 1, ..., n_measured - 1)``
where ``n_measured`` is the number of measured qubits.
Defaults to ``None``.
seed (int, optional): Seed for sampling generation.
backend (:class:`qibo.backends.abstract.Backend`, optional): Backend
used for calculations. If ``None``, the current default backend
is used. Defaults to ``None``.
Returns:
:class:`qibo.result.MeasurementOutcomes`: Object storing the
measurement outcomes.
Raises:
ValueError: If the number of measured qubits cannot be determined
from the inputs.
Example:
.. code-block:: python
from qibo.result import MeasurementOutcomes
freq = {"00": 50, "11": 50}
result = MeasurementOutcomes.from_frequencies(freq)
print(result.frequencies()) # Counter({'00': 50, '11': 50})
"""
from qibo.backends import ( # pylint: disable=import-outside-toplevel
_check_backend,
)
backend = _check_backend(backend)
frequencies = dict(frequencies)
if len(frequencies) == 0:
raise_error(ValueError, "frequencies dictionary must not be empty.")
# Detect key type and normalise to integer-keyed dict
first_key = next(iter(frequencies))
if isinstance(first_key, str):
if not all(isinstance(k, str) for k in frequencies):
raise_error(
TypeError,
"All frequency keys must be of the same type (all strings or all integers).",
)
# Binary-string keys: infer n_measured from key length
key_lengths = {len(k) for k in frequencies}
if len(key_lengths) != 1:
raise_error(
ValueError,
"All binary-string keys must have the same length, "
f"got lengths {key_lengths}.",
)
inferred_n_measured = key_lengths.pop()
int_frequencies = {int(k, 2): v for k, v in frequencies.items()}
else:
# Integer keys
inferred_n_measured = None
int_frequencies = {int(k): v for k, v in frequencies.items()}
# Resolve the number of measured qubits
n_measured: int
if qubits is not None:
qubits = tuple(qubits)
n_measured = len(qubits)
elif inferred_n_measured is not None:
n_measured = inferred_n_measured
elif nqubits is not None:
# Integer keys without qubits: nqubits gives the measured count
n_measured = nqubits
else:
raise_error(
ValueError,
"Cannot determine the number of measured qubits. Provide "
"`nqubits` or `qubits` when using integer keys in frequencies.",
)
# Validate consistency between inferred measured count and qubits
if inferred_n_measured is not None and qubits is not None:
if inferred_n_measured != len(qubits):
raise_error(
ValueError,
f"Binary-string key length ({inferred_n_measured}) does not "
f"match the number of qubits provided ({len(qubits)}).",
)
if qubits is None:
qubits = tuple(range(n_measured))
# Resolve total nqubits for the circuit
if nqubits is not None:
if nqubits < n_measured:
raise_error(
ValueError,
f"nqubits ({nqubits}) must be >= the number of measured "
f"qubits ({n_measured}).",
)
total_nqubits = nqubits
else:
total_nqubits = None # will default to n_measured inside __init__
# Expand frequencies into a binary samples array
for state_int, count in int_frequencies.items():
if not isinstance(count, (int, np.integer)):
raise_error(
ValueError,
f"Frequency count for state {state_int} must be an integer, got {type(count)}.",
)
if count < 0:
raise_error(
ValueError,
f"Frequency count for state {state_int} must be non-negative, got {count}.",
)
nshots = sum(int_frequencies.values())
if nshots <= 0:
raise_error(ValueError, "Total number of shots must be positive.")
max_state = 2**n_measured - 1
for state_int in int_frequencies:
if state_int < 0 or state_int > max_state:
raise_error(
ValueError,
f"State integer {state_int} is out of range for "
f"{n_measured} measured qubits (valid range: 0 to {max_state}).",
)
sample_rows = []
for state_int, count in int_frequencies.items():
if count == 0:
continue
# Convert integer state to binary row
row = np.array(
[(state_int >> (n_measured - 1 - i)) & 1 for i in range(n_measured)],
dtype=int,
)
sample_rows.append(np.tile(row, (count, 1)))
samples = np.concatenate(sample_rows, axis=0)
# Shuffle to avoid ordering artifacts
rng = np.random.default_rng(seed)
rng.shuffle(samples)
measurements = [gates.M(*qubits)]
return cls(
measurements,
backend=backend,
samples=samples,
nshots=nshots,
nqubits=total_nqubits,
)
[docs]class CircuitResult(QuantumState, MeasurementOutcomes):
"""Object to store both the outcomes of measurements and the final state
after circuit execution.
Args:
final_state (ndarray): Input quantum state as np.ndarray.
measurements (:class:`qibo.gates.M`): The measurement gates containing the measurements.
backend (:class:`qibo.backends.abstract.Backend`): Backend used for the calculations.
If not provided, then the current backend is going to be used.
probabilities (ndarray): Use these probabilities to generate samples and frequencies.
samples (ndarray): Use these samples to generate probabilities and frequencies.
nshots (int): Number of shots used for samples, probabilities and frequencies generation.
"""
def __init__(
self, final_state, measurements, backend=None, samples=None, nshots=1000
):
QuantumState.__init__(self, final_state, backend)
qubits = [q for m in measurements for q in m.target_qubits]
if len(qubits) == 0:
raise ValueError(
"Circuit does not contain measurements. Use a `QuantumState` instead."
)
probs = QuantumState.probabilities(self, qubits) if samples is None else None
MeasurementOutcomes.__init__(
self,
measurements,
backend=backend,
probabilities=probs,
samples=samples,
nshots=nshots,
nqubits=self.nqubits,
)
[docs] def probabilities(self, qubits: Optional[Union[list, set]] = None):
if self.measurement_gate.has_bitflip_noise():
return MeasurementOutcomes.probabilities(self, qubits)
return QuantumState.probabilities(self, qubits)
[docs] def to_dict(self):
"""Returns a dictonary containinig all the information needed to rebuild the
``CircuitResult``."""
args = MeasurementOutcomes.to_dict(self)
args.update(QuantumState.to_dict(self))
args.update({"dtype": self.__class__.__name__})
return args
[docs] @classmethod
def from_dict(cls, payload: dict):
"""Builds a ``CircuitResult`` object starting from a dictionary.
Args:
payload (dict): Dictionary containing all the information to load the
``CircuitResult`` object.
Returns:
:class:`qibo.result.CircuitResult`: circuit result object.
"""
state_load = {"state": payload.pop("state")}
state = QuantumState.from_dict(state_load)
measurements = MeasurementOutcomes.from_dict(payload)
return cls(
state.state(),
measurements.measurements,
backend=state.backend,
samples=measurements.samples(),
nshots=measurements.nshots,
)