Source code for mpqp.execution.result

"""Once the computation ended, the :class:`Result` contains all the data from
the execution.

The job type affects the data contained in the :class:`Result`. For a given 
``result``, here are how to retrieve the data depending on the job type:

- for a job type ``STATE_VECTOR`` you can retrieve the :class:`StateVector` from 
  ``result.state_vector``. If you want to directly get the amplitudes of your
  state vector, you can reach for ``result.amplitudes``;
- for a job type ``SAMPLE`` you can retrieve the list of :class:`Sample` from 
  ``result.samples``. For a ``SAMPLE`` job type, you might be interested in
  results packed in a different shape than a list of :class:`Sample`, even
  though you could rebuild them from said list, we also provide a few shorthands
  like ``result.probabilities`` and ``result.counts``;
- for a job type ``OBSERVABLE`` you can retrieve the expectation value (a 
  ``float``) from ``result.expectation_value``.

When several devices are given to :func:`run<mpqp.execution.runner.run>`, the 
results are stored in a :class:`BatchResult`.
"""

from __future__ import annotations

import math
import random
from numbers import Complex
from typing import Optional

import numpy as np
import numpy.typing as npt
from typeguard import typechecked

from mpqp.execution.devices import AvailableDevice
from mpqp.tools.errors import ResultAttributeError
from mpqp.tools.generics import clean_array

from .job import Job, JobType


[docs]@typechecked class StateVector: """Class representing the state vector of a multi-qubit quantum system. Args: vector: List of amplitudes defining the state vector. nb_qubits: Number of qubits of the state. probabilities: List of probabilities associated with the state vector. Example: >>> state_vector = StateVector(np.array([1, 1, 1, -1])/2, 2) >>> state_vector.probabilities array([0.25, 0.25, 0.25, 0.25]) >>> print(state_vector) State vector: [0.5, 0.5, 0.5, -0.5] Probabilities: [0.25, 0.25, 0.25, 0.25] Number of qubits: 2 """ def __init__( self, vector: list[Complex] | npt.NDArray[np.complex64], nb_qubits: Optional[int] = None, probabilities: Optional[list[float] | npt.NDArray[np.float32]] = None, ): if len(np.asarray(vector)) == 0: raise ValueError("vector should not be empty") self.vector: npt.NDArray[np.complex64] = np.array(vector, dtype=complex) self.nb_qubits = ( int(math.log(len(vector), 2)) if nb_qubits is None else nb_qubits ) """See parameter description.""" self.probabilities = ( abs(self.vector) ** 2 if probabilities is None else np.array(probabilities) ) """See parameter description.""" @property def amplitudes(self): """Return the amplitudes of the state vector""" return self.vector def __str__(self): return f""" State vector: {clean_array(self.vector)} Probabilities: {clean_array(self.probabilities)} Number of qubits: {self.nb_qubits}""" def __repr__(self) -> str: return f"StateVector({clean_array(self.vector)})"
[docs]@typechecked class Sample: """A sample is a partial result of job job with type ``SAMPLE``. It contains the count (and potentially the associated probability) for a given basis state, *i.e.* the number of times this basis state was measured. Args: nb_qubits: Number of qubits of the quantum system of the experiment. probability: Probability of measuring the basis state associated to this sample. index: Index in decimal notation representing the basis state. count: Number of times this basis state was measured during the experiment. bin_str: String representing the basis state in binary notation. Examples: >>> print(Sample(3, index=3, count=250, bin_str="011")) State: 011, Index: 3, Count: 250, Probability: None >>> print(Sample(4, index=6, probability=0.5)) State: 0110, Index: 6, Count: None, Probability: 0.5 >>> print(Sample(5, bin_str="01011", count=1234)) State: 01011, Index: 11, Count: 1234, Probability: None """ def __init__( self, nb_qubits: int, probability: Optional[float] = None, index: Optional[int] = None, count: Optional[int] = None, bin_str: Optional[str] = None, ): self.nb_qubits = nb_qubits """See parameter description.""" self.count = count """See parameter description.""" self.probability = probability """See parameter description.""" self.bin_str: str """See parameter description.""" self.index: int """See parameter description.""" if index is None: if bin_str is None: raise ValueError( "At least one of `bin_str` and `index` arguments is necessary" ) else: self.index = int(bin_str, 2) self.bin_str = bin_str else: computed_bin_str = bin(index)[2:].zfill(self.nb_qubits) if bin_str is None: self.index = index self.bin_str = computed_bin_str else: if computed_bin_str == bin_str: self.index = index self.bin_str = bin_str else: raise ResultAttributeError( f"The value of bin_str {bin_str} doesn't match with the" f" index provided {index} and the number of qubits {self.nb_qubits}" ) def __str__(self): return f"State: {self.bin_str}, Index: {self.index}, Count: {self.count}, Probability: {self.probability}" def __repr__(self): return f"Sample({self.nb_qubits}, index={self.index}, count={self.count}, probability={self.probability})"
[docs]@typechecked class Result: """Result associated to a submitted job. The data type in a result depends on the job type, according to the following chart: +-------------+--------------+ | Job Type | Data Type | +=============+==============+ | OBSERVABLE | float | +-------------+--------------+ | SAMPLE | list[Sample] | +-------------+--------------+ | STATE_VECTOR| StateVector | +-------------+--------------+ Args: job: Type of the job related to this result. data: Data of the result, can be an expectation value (float), a StateVector, or a list of sample depending on the job_type. error: Information about the error or the variance in the measurement. shots: Number of shots of the experiment (equal to zero if the exact value was required). Examples: >>> job = Job(JobType.STATE_VECTOR, QCircuit(2), ATOSDevice.MYQLM_CLINALG) >>> print(Result(job, StateVector(np.array([1, 1, 1, -1], dtype=np.complex64) / 2, 2), 0, 0)) # doctest: +NORMALIZE_WHITESPACE Result: None, ATOSDevice, MYQLM_CLINALG State vector: [0.5, 0.5, 0.5, -0.5] Probabilities: [0.25, 0.25, 0.25, 0.25] Number of qubits: 2 >>> job = Job(JobType.SAMPLE, QCircuit(2), ATOSDevice.MYQLM_CLINALG, BasisMeasure([0, 1], shots=1000)) >>> print(Result(job, [ ... Sample(2, index=0, count=250), ... Sample(2, index=3, count=250) ... ], 0.034, 500)) # doctest: +NORMALIZE_WHITESPACE Result: None, ATOSDevice, MYQLM_CLINALG Counts: [250, 0, 0, 250] Probabilities: [0.5, 0, 0, 0.5] Samples: State: 00, Index: 0, Count: 250, Probability: 0.5 State: 11, Index: 3, Count: 250, Probability: 0.5 Error: 0.034 >>> job = Job(JobType.OBSERVABLE, QCircuit(2), ATOSDevice.MYQLM_CLINALG) >>> print(Result(job, -3.09834, 0.021, 2048)) # doctest: +NORMALIZE_WHITESPACE Result: None, ATOSDevice, MYQLM_CLINALG Expectation value: -3.09834 Error/Variance: 0.021 """ # TODO: in this class, there is a lot of manual type checking, this is an # anti-pattern in my opinion, it should probably be fixed using subclasses def __init__( self, job: Job, data: float | StateVector | list[Sample], error: Optional[float] = None, shots: int = 0, ): self.job = job """See parameter description.""" self._expectation_value = None self._state_vector = None self._probabilities = None self._counts = None self._samples = None self.shots = shots """See parameter description.""" self.error = error """See parameter description.""" self._data = data # depending on the type of job, fills the result info from the data in parameter if job.job_type == JobType.OBSERVABLE: if not isinstance(data, float): raise TypeError( "Wrong type of data in the result. " "Expecting float for expectation value of an observable" ) else: self._expectation_value = data elif job.job_type == JobType.STATE_VECTOR: if not isinstance(data, StateVector): raise TypeError( "Wrong type of data in the result. Expecting StateVector" ) else: self._state_vector = data self._probabilities = data.probabilities elif job.job_type == JobType.SAMPLE: if not isinstance(data, list): raise TypeError( "Wrong type of data in the result (not a list). Expecting list of Sample" ) if self.job.measure is None: raise ValueError( f"{self.job=} has no measure, making the counting impossible" ) self._samples = data is_counts = all([sample.count is not None for sample in data]) is_probas = all([sample.probability is not None for sample in data]) if is_probas: probas = [0.0] * (2**self.job.measure.nb_qubits) for sample in data: probas[sample.index] = sample.probability self._probabilities = np.array(probas, dtype=float) counts = [ int(count) for count in np.round(self.job.measure.shots * self._probabilities) ] self._counts = counts for sample in self._samples: sample.count = self._counts[sample.index] elif is_counts: counts: list[int] = [0] * (2**self.job.measure.nb_qubits) for sample in data: assert sample.count is not None counts[sample.index] = sample.count self._counts = counts assert shots != 0 self._probabilities = np.array(counts, dtype=float) / self.shots for sample in self._samples: sample.probability = self._probabilities[sample.index] else: raise ValueError( f"For {JobType.SAMPLE.name} jobs, all samples must contain" " either `count` or `probability` (and the non-None " "attribute amongst the two must be the same in all samples)." ) self.samples.sort(key=lambda sample: sample.bin_str) else: raise ValueError(f"{job.job_type} not handled") @property def device(self) -> AvailableDevice: """Device on which the job of this result was run""" return self.job.device @property def expectation_value(self) -> float: """Get the expectation value stored in this result""" if self.job.job_type != JobType.OBSERVABLE: raise ResultAttributeError( f"Job type: {self.job.job_type.name} but cannot get expectation" " value if the job type is not OBSERVABLE." ) assert self._expectation_value is not None return self._expectation_value @property def amplitudes(self) -> npt.NDArray[np.complex64]: """Get the amplitudes of the state of this result""" if self.job.job_type != JobType.STATE_VECTOR: raise ResultAttributeError( "Cannot get amplitudes if the job was not of type STATE_VECTOR" ) assert self._state_vector is not None return self._state_vector.amplitudes @property def state_vector(self) -> StateVector: """Get the state vector of the state associated with this result""" if self.job.job_type != JobType.STATE_VECTOR: raise ResultAttributeError( "Cannot get state vector if the job was not of type STATE_VECTOR" ) assert self._state_vector is not None return self._state_vector @property def samples(self) -> list[Sample]: """Get the list of samples of the result""" if self.job.job_type != JobType.SAMPLE: raise ResultAttributeError( "Cannot get samples if the job was not of type SAMPLE" ) assert self._samples is not None return self._samples @property def probabilities(self) -> npt.NDArray[np.float32]: """Get the list of probabilities associated with this result""" if self.job.job_type not in (JobType.SAMPLE, JobType.STATE_VECTOR): raise ResultAttributeError( "Cannot get probabilities if the job was not of" " type SAMPLE or STATE_VECTOR" ) assert self._probabilities is not None return self._probabilities @property def counts(self) -> list[int]: """Get the list of counts for each sample of the experiment""" if self.job.job_type != JobType.SAMPLE: raise ResultAttributeError( "Cannot get counts if the job was not of type SAMPLE" ) assert self._counts is not None return self._counts def __str__(self): header = ( f"Result: {self.job.circuit.label}, {type(self.device).__name__}, {self.device.name}" ) if self.job.job_type == JobType.SAMPLE: samples_str = "\n".join(map(lambda s: f" {s}", self.samples)) return f"""{header} Counts: {self._counts} Probabilities: {clean_array(self.probabilities)} Samples: {samples_str} Error: {self.error}""" if self.job.job_type == JobType.STATE_VECTOR: return header + "\n" + str(self.state_vector) if self.job.job_type == JobType.OBSERVABLE: return f"""{header} Expectation value: {self.expectation_value} Error/Variance: {self.error}""" raise NotImplementedError( f"I don't know how to represent results of {self.job.job_type} jobs" " as a string." ) def __repr__(self) -> str: return ( f"Result({repr(self.job)}, {repr(self._data)}, {repr(self.error)}, " f"{repr(self.shots)})" )
[docs] def plot(self, show: bool = True): """Extract sampling info from the result and construct the bar diagram plot. Args: show: ``plt.show()`` is only executed if ``show``, useful to batch plots. """ from matplotlib import pyplot as plt if show: plt.figure() x_array, y_array = self._to_display_lists() x_axis = range(len(x_array)) plt.bar(x_axis, y_array, color=(*[random.random() for _ in range(3)], 0.9)) plt.xticks(x_axis, x_array, rotation=-60) plt.xlabel("State") plt.ylabel("Counts") device = self.job.device plt.title(type(device).__name__ + "\n" + device.name) if show: plt.show()
def _to_display_lists(self) -> tuple[list[str], list[int]]: """Transform a result into an x and y array containing the string of basis state with the associated counts. Returns: The list of each basis state and the corresponding counts. """ if self.job.job_type != JobType.SAMPLE: raise NotImplementedError( f"{self.job.job_type} not handled, only {JobType.SAMPLE} is handled for now." ) if self.job.measure is None: raise ValueError( f"{self.job=} has no measure, making the counting impossible" ) n = self.job.measure.nb_qubits x_array = [f"|{bin(i)[2:].zfill(n)}⟩" for i in range(2**n)] y_array = self.counts return x_array, y_array
[docs]@typechecked class BatchResult: """Class used to handle several Result instances. Args: results: List of results. Example: >>> result1 = Result( ... Job(JobType.STATE_VECTOR,QCircuit(0),ATOSDevice.MYQLM_PYLINALG), ... StateVector(np.array([1, 1, 1, -1])/2, 2), ... 0, ... 0 ... ) >>> result2 = Result( ... Job( ... JobType.SAMPLE, ... QCircuit(0), ... ATOSDevice.MYQLM_PYLINALG, ... BasisMeasure([0,1],shots=500) ... ), ... [Sample(2, index=0, count=250), Sample(2, index=3, count=250)], ... 0.034, ... 500) >>> result3 = Result( ... Job(JobType.OBSERVABLE,QCircuit(0),ATOSDevice.MYQLM_PYLINALG), ... -3.09834, ... 0.021, ... 2048 ... ) >>> batch_result = BatchResult([result1, result2, result3]) >>> print(batch_result) BatchResult: 3 results Result: None, ATOSDevice, MYQLM_PYLINALG State vector: [0.5, 0.5, 0.5, -0.5] Probabilities: [0.25, 0.25, 0.25, 0.25] Number of qubits: 2 Result: None, ATOSDevice, MYQLM_PYLINALG Counts: [250, 0, 0, 250] Probabilities: [0.5, 0, 0, 0.5] Samples: State: 00, Index: 0, Count: 250, Probability: 0.5 State: 11, Index: 3, Count: 250, Probability: 0.5 Error: 0.034 Result: None, ATOSDevice, MYQLM_PYLINALG Expectation value: -3.09834 Error/Variance: 0.021 >>> print(batch_result[0]) Result: None, ATOSDevice, MYQLM_PYLINALG State vector: [0.5, 0.5, 0.5, -0.5] Probabilities: [0.25, 0.25, 0.25, 0.25] Number of qubits: 2 """ def __init__(self, results: list[Result]): self.results = results """See parameter description.""" def __str__(self): header = f"BatchResult: {len(self.results)} results\n" body = "\n".join(map(str, self.results)) return header + body def __repr__(self): return f"BatchResult({self.results})" def __getitem__(self, index: int): return self.results[index]
[docs] def plot(self, show: bool = True): """Display the result(s) using ``matplotlib.pyplot``. The result(s) must be from a job who's ``job_type`` is ``SAMPLE``. They will be displayed as histograms. If a ``BatchResult`` is given, the contained results will be displayed in a grid using subplots. Args: show: ``plt.show()`` is only executed if ``show``, useful to batch plots. """ from matplotlib import pyplot as plt n_cols = math.ceil((len(self.results) + 1) // 2) n_rows = math.ceil(len(self.results) / n_cols) for index, result in enumerate(self.results): plt.subplot(n_rows, n_cols, index + 1) result.plot(show=False) plt.tight_layout() if show: plt.show()