From 9488287fe4e1810c2e0a89d89ed45e16d7825608 Mon Sep 17 00:00:00 2001 From: Stavros Efthymiou <35475381+stavros11@users.noreply.github.com> Date: Tue, 2 Jul 2024 15:36:32 +0400 Subject: [PATCH 1/5] feat: add measurement gate --- examples/circuit.rs | 5 +++++ src/circuit.rs | 13 +++++++++++++ src/gate/one.rs | 2 ++ 3 files changed, 20 insertions(+) diff --git a/examples/circuit.rs b/examples/circuit.rs index 010a95a..2c0928f 100644 --- a/examples/circuit.rs +++ b/examples/circuit.rs @@ -10,11 +10,16 @@ fn main() { c.add(X.into(), vec![1, 4]); c.add(Y.into(), vec![4]); c.add(RX(3.14).into(), vec![0]); + c.add(M.into(), vec![1]); + c.add(M.into(), vec![3]); + c.add(M.into(), vec![4]); println!("{}\n", c); println!("{:?}\n", c.elements(gid)); + println!("{:?}\n", c.measured_elements()); + for gid in 0..c.n_gates() { println!("{:?}", c.elements(gid)); } diff --git a/src/circuit.rs b/src/circuit.rs index 16b6a3f..c336e06 100644 --- a/src/circuit.rs +++ b/src/circuit.rs @@ -1,6 +1,7 @@ use std::fmt::{self, Display}; use crate::gate::Gate; +use crate::prelude::One; #[derive(Debug, Clone, Copy, PartialEq)] struct Node { @@ -148,6 +149,18 @@ impl Circuit { self.gates[gid] } + pub fn measured_elements(&self) -> Vec { + self.gates.iter().enumerate() + .filter_map(|(gid, gate)| { + if let Gate::One(One::M) = gate { + Some(self.elements(gid)[0]) + } else { + None + } + }) + .collect() + } + pub fn draw(&self) -> String { let mut wires: Vec = (0..self.n_elements()).map(|i| format!("q{i}: ")).collect(); diff --git a/src/gate/one.rs b/src/gate/one.rs index de6c25b..5087643 100644 --- a/src/gate/one.rs +++ b/src/gate/one.rs @@ -68,6 +68,7 @@ pub enum One { /// \\end{pmatrix} /// $$ S, + M, RX(f64), U1(f64), } @@ -82,6 +83,7 @@ impl Display for One { | Self::SX | Self::SXDG | Self::S + | Self::M | Self::RX(_) | Self::U1(_)) => extract_name(g), }) From 617ade9e92f6b3a2a6b2e0a3f3ab8725dca177c7 Mon Sep 17 00:00:00 2001 From: Stavros Efthymiou <35475381+stavros11@users.noreply.github.com> Date: Tue, 2 Jul 2024 15:53:39 +0400 Subject: [PATCH 2/5] refactor: simplify measurement results --- crates/py/examples/circuit.py | 3 + crates/py/qibo_core/measurements.py | 13 -- crates/py/qibo_core/numpy.py | 10 +- crates/py/qibo_core/result.py | 343 ++++++---------------------- crates/py/src/circuit.rs | 4 +- crates/py/src/gate.rs | 3 + 6 files changed, 81 insertions(+), 295 deletions(-) diff --git a/crates/py/examples/circuit.py b/crates/py/examples/circuit.py index 429a1e6..64b5f8e 100644 --- a/crates/py/examples/circuit.py +++ b/crates/py/examples/circuit.py @@ -12,6 +12,9 @@ c.add(gates.Y(), [4]) c.add(gates.X(), [4, 1]) c.add(gates.X(), [0]) +c.add(gates.M(), [1]) +c.add(gates.M(), [3]) +c.add(gates.M(), [4]) print(c) diff --git a/crates/py/qibo_core/measurements.py b/crates/py/qibo_core/measurements.py index a4efdf7..4fd1c38 100644 --- a/crates/py/qibo_core/measurements.py +++ b/crates/py/qibo_core/measurements.py @@ -7,19 +7,6 @@ def frequencies_to_binary(frequencies, nqubits): ) -def apply_bitflips(result, p0, p1=None): - gate = result.measurement_gate - if p1 is None: - probs = 2 * (gate._get_bitflip_tuple(gate.qubits, p0),) - else: - probs = ( - gate._get_bitflip_tuple(gate.qubits, p0), - gate._get_bitflip_tuple(gate.qubits, p1), - ) - noiseless_samples = result.samples() - return result.backend.apply_bitflips(noiseless_samples, probs) - - class MeasurementResult: """Data structure for holding measurement outcomes. diff --git a/crates/py/qibo_core/numpy.py b/crates/py/qibo_core/numpy.py index 73f30ef..aab42f7 100644 --- a/crates/py/qibo_core/numpy.py +++ b/crates/py/qibo_core/numpy.py @@ -4,10 +4,12 @@ import numpy as np from . import einsum_utils +from .qibo_core import gate from .npmatrices import NumpyMatrices from .result import CircuitResult, MeasurementOutcomes, QuantumState from .config import SHOT_BATCH_SIZE +gates = gate.Gate class NumpyBackend: def __init__(self): @@ -416,12 +418,12 @@ def execute_circuit( state = self.cast(initial_state) for gate, elements in circuit: - # TODO: Handle measurements and ``CallbackGate`` - state = self.apply_gate(gate, elements, state, nqubits) + if not isinstance(gate, gates.M): + state = self.apply_gate(gate, elements, state, nqubits) - if len(circuit.measurements) > 0: + if len(circuit.measured_elements) > 0: return CircuitResult( - state, circuit.measurements, backend=self, nshots=nshots + state, circuit.measured_elements, nshots=nshots, backend=self, ) return QuantumState(state, backend=self) diff --git a/crates/py/qibo_core/result.py b/crates/py/qibo_core/result.py index 45a950f..93ffd9c 100644 --- a/crates/py/qibo_core/result.py +++ b/crates/py/qibo_core/result.py @@ -4,7 +4,6 @@ import numpy as np -from .measurements import apply_bitflips, frequencies_to_binary from .qibo_core import __version__ from .qibo_core import gate @@ -172,259 +171,40 @@ class MeasurementOutcomes: def __init__( self, - measurements, - backend=None, - probabilities=None, samples: Optional[int] = None, - nshots: int = 1000, + frequencies: Optional[dict] = None, + backend=None, ): self.backend = backend - self.measurements = measurements - self.nshots = nshots - - 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]) - else: - for gate in self.measurements: - gate.result.reset() - - def frequencies(self, binary: bool = True, registers: bool = False): - """Returns the frequencies of measured samples. - - Args: - binary (bool, optional): Return frequency keys in binary or decimal form. - registers (bool, optional): Group frequencies according to registers. - - Returns: - A `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 `Counter` are in binary form, as strings of - :math:`0`s and :math`1`s. - If ``binary`` is ``False`` - the keys of the ``Counter`` are integers. - If ``registers`` is ``True`` - a `dict` of `Counter` s is returned where keys are the name of - each register. - If ``registers`` is ``False`` - a single ``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()} - ) + self._frequencies = frequencies + @property + def frequencies(self): + """Frequencies of measured samples.""" 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)} - reg_frequencies = {} - 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, self.backend) - else: - self._frequencies = self.backend.calculate_frequencies( - self.samples(binary=False) - ) - - if registers: - return { - gate.register_name: gate.result.frequencies(binary) - for gate in self.measurements - } - - if binary: - return frequencies_to_binary(self._frequencies, len(qubits)) - + self._frequencies = self.backend.calculate_frequencies(self.samples) return self._frequencies - def probabilities(self, qubits: Optional[Union[list, set]] = None): - """Calculate the probabilities as frequencies / nshots. - - Returns: - The array containing the probabilities of the measured qubits. - """ - nqubits = len(self.measurement_gate.qubits) - if qubits is None: - qubits = range(nqubits) - else: - if not set(qubits).issubset(self.measurement_gate.qubits): - raise RuntimeError( - "Asking probabilities for qubits {qubits}, but only qubits {self.measurement_gate.qubits} were measured." - ) - qubits = [self.measurement_gate.qubits.index(q) for q in qubits] - - if self._probs is not None and not self.measurement_gate.has_bitflip_noise(): - return self.backend.calculate_probabilities( - np.sqrt(self._probs), qubits, nqubits - ) - - probs = [0 for _ in range(2**nqubits)] - for state, freq in self.frequencies(binary=False).items(): - probs[state] = freq / self.nshots - probs = self.backend.cast(probs) - self._probs = probs - return self.backend.calculate_probabilities(np.sqrt(probs), qubits, nqubits) - - 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 - - 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.np.concatenate( - [gate.result.samples() 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 = np.concatenate( - [np.repeat(x, f) for x, f in frequencies.items()] - ) - np.random.shuffle(samples) - 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 = [ - [p0.get(q) for q in qubits], - [p1.get(q) for q in qubits], - ] - samples = self.backend.apply_bitflips( - samples, bitflip_probabilities - ) - # register samples to individual gate ``MeasurementResult`` - qubit_map = { - q: i for i, q in enumerate(self.measurement_gate.target_qubits) - } - self._samples = self.backend.cast(samples, "int32") - for gate in self.measurements: - rqubits = tuple(qubit_map.get(q) for q in gate.target_qubits) - gate.result.register_samples( - self._samples[:, rqubits], self.backend - ) - - if registers: - return { - gate.register_name: gate.result.samples(binary) - 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 - - 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) - - 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. - """ - freq = self.frequencies(binary=True) - qubit_map = self.measurement_gate.qubits - return observable.expectation_from_samples(freq, qubit_map) + def samples(self): + """Raw measurement samples.""" + if self._samples is None: + # generate samples that respect the existing frequencies + frequencies = self.frequencies(binary=False) + samples = np.concatenate( + [np.repeat(x, f) for x, f in frequencies.items()] + ) + np.random.shuffle(samples) + self._samples = self.backend.cast(samples, "int32") + return self._samples 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, + "samples": self.samples, + "frequencies": self.frequencies, "dtype": self.__class__.__name__, "qibo": __version__, } @@ -453,19 +233,16 @@ def from_dict(cls, payload: dict): """ from . import backends - if payload["probabilities"] is not None and payload["samples"] is not None: + if payload["frequencies"] 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`." + "Both `frequencies` and `samples` found, discarding the `frequencies` and building out of the `samples`." ) - payload.pop("probabilities") + payload.pop("frequencies") backend = backends.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"), + frequencies=payload.get("frequencies"), + backend=backend, ) @classmethod @@ -483,7 +260,13 @@ def load(cls, filename: str): return cls.from_dict(payload) -class CircuitResult(QuantumState, MeasurementOutcomes): +def frequencies_to_binary(frequencies, nqubits): + return collections.Counter( + {"{:b}".format(k).zfill(nqubits): v for k, v in frequencies.items()} + ) + + +class CircuitResult(QuantumState): """Object to store both the outcomes of measurements and the final state after circuit execution. @@ -496,36 +279,46 @@ class CircuitResult(QuantumState, MeasurementOutcomes): 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: + def __init__(self, state, elements, nshots=1000, outcomes=None, backend=None): + super().__init__(state, backend) + if len(elements) == 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, - ) + self.elements = elements + self.nshots = nshots + self.outcomes = outcomes - 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) + @property + def samples(self): + """Raw measurement samples.""" + if self.outcomes is None: + probs = self.probabilities(self.elements) + samples = self.backend.sample_shots(probs, self.nshots) + samples = self.backend.samples_to_binary(samples, len(self.elements)) + self.outcomes = MeasurementOutcomes(samples=samples, backend=self.backend) + return self.outcomes.samples + + + @property + def frequencies(self): + """Frequencies of measured samples.""" + if self.outcomes is None: + probs = self.probabilities(self.elements) + frequencies = self.backend.sample_frequencies(probs, self.nshots) + frequencies = frequencies_to_binary(frequencies, len(self.elements)) + self.outcomes = MeasurementOutcomes(samples=samples, backend=self.backend) + return self.outcomes.frequencies 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__}) + args = super().to_dict() + args["elements"] = self.elements + args["nshots"] = self.nshots + if self.outcomes is not None: + args["outcomes"] = self.outcomes.to_dict() + args["dtype"] = self.__class__.__name__ return args @classmethod @@ -540,11 +333,9 @@ def from_dict(cls, payload: dict): """ state_load = {"state": payload.pop("state")} state = QuantumState.from_dict(state_load) - measurements = MeasurementOutcomes.from_dict(payload) + if "outcomes" in payload: + payload["outcomes"] = MeasurementOutcomes.from_dict(**payload.pop("outcomes")) return cls( state.state(), - measurements.measurements, - backend=state.backend, - samples=measurements.samples(), - nshots=measurements.nshots, + **payload ) diff --git a/crates/py/src/circuit.rs b/crates/py/src/circuit.rs index 57ba2a8..252ec12 100644 --- a/crates/py/src/circuit.rs +++ b/crates/py/src/circuit.rs @@ -28,8 +28,8 @@ pub mod circuit { } #[getter] - fn measurements(&self) -> Vec { - vec![] + fn measured_elements(&self) -> Vec { + self.0.measured_elements() } fn __getitem__(&self, gid: usize) -> PyResult<(Gate, Vec)> { diff --git a/crates/py/src/gate.rs b/crates/py/src/gate.rs index a6a2bc6..c09ef8d 100644 --- a/crates/py/src/gate.rs +++ b/crates/py/src/gate.rs @@ -13,6 +13,7 @@ pub mod gate { X {}, Y {}, Z {}, + M {}, RX { angle: f64 }, U1 { angle: f64 }, SWAP {}, @@ -26,6 +27,7 @@ pub mod gate { &Self::X {} => One::X.into(), &Self::Y {} => One::Y.into(), &Self::Z {} => One::Z.into(), + &Self::M {} => One::M.into(), &Self::RX { angle } => One::RX(*angle).into(), &Self::U1 { angle } => One::U1(*angle).into(), &Self::SWAP {} => Two::SWAP.into(), @@ -39,6 +41,7 @@ pub mod gate { prelude::Gate::One(One::X) => Self::X {}, prelude::Gate::One(One::Y) => Self::Y {}, prelude::Gate::One(One::Z) => Self::Z {}, + prelude::Gate::One(One::M) => Self::M {}, prelude::Gate::One(One::RX(angle)) => Self::RX { angle }, prelude::Gate::One(One::U1(angle)) => Self::U1 { angle }, prelude::Gate::Two(Two::SWAP) => Self::SWAP {}, From 3d3c176fca89be0b55a13b7c5347e6d19413f6e1 Mon Sep 17 00:00:00 2001 From: Stavros Efthymiou <35475381+stavros11@users.noreply.github.com> Date: Tue, 2 Jul 2024 15:54:25 +0400 Subject: [PATCH 3/5] chore: drop MeasurementResult --- crates/py/qibo_core/measurements.py | 135 ---------------------------- 1 file changed, 135 deletions(-) delete mode 100644 crates/py/qibo_core/measurements.py diff --git a/crates/py/qibo_core/measurements.py b/crates/py/qibo_core/measurements.py deleted file mode 100644 index 4fd1c38..0000000 --- a/crates/py/qibo_core/measurements.py +++ /dev/null @@ -1,135 +0,0 @@ -import collections - - -def frequencies_to_binary(frequencies, nqubits): - return collections.Counter( - {"{:b}".format(k).zfill(nqubits): v for k, v in frequencies.items()} - ) - - -class MeasurementResult: - """Data structure for holding measurement outcomes. - - :class:`qibo.measurements.MeasurementResult` objects can be obtained - when adding measurement gates to a circuit. - - Args: - gate (:class:`qibo.gates.M`): Measurement gate associated with - this result object. - nshots (int): Number of measurement shots. - backend (:class:`qibo.backends.abstract.AbstractBackend`): Backend - to use for calculations. - """ - - def __init__(self, gate, nshots=0, backend=None): - self.measurement_gate = gate - self.backend = backend - self.nshots = nshots - self.circuit = None - - self._samples = None - self._frequencies = None - self._bitflip_p0 = None - self._bitflip_p1 = None - self._symbols = None - - def __repr__(self): - qubits = self.measurement_gate.qubits - nshots = self.nshots - return f"MeasurementResult(qubits={qubits}, nshots={nshots})" - - def add_shot(self, probs): - qubits = sorted(self.measurement_gate.target_qubits) - shot = self.backend.sample_shots(probs, 1) - bshot = self.backend.samples_to_binary(shot, len(qubits)) - if self._samples: - self._samples.append(bshot[0]) - else: - self._samples = [bshot[0]] - self.nshots += 1 - return shot - - def add_shot_from_sample(self, sample): - if self._samples: - self._samples.append(sample) - else: - self._samples = [sample] - self.nshots += 1 - - def has_samples(self): - return self._samples is not None - - def register_samples(self, samples, backend=None): - """Register samples array to the ``MeasurementResult`` object.""" - self._samples = samples - self.nshots = len(samples) - - def register_frequencies(self, frequencies, backend=None): - """Register frequencies to the ``MeasurementResult`` object.""" - self._frequencies = frequencies - self.nshots = sum(frequencies.values()) - - def reset(self): - """Remove all registered samples and frequencies.""" - self._samples = None - self._frequencies = None - - def samples(self, binary=True, registers=False): - """Returns raw measurement samples. - - Args: - binary (bool): Return samples in binary or decimal form. - registers (bool): 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 self._samples is None: - if self.circuit is None: - raise RuntimeError( - "Cannot calculate samples if circuit is not provided." - ) - # calculate samples for the whole circuit so that - # individual register samples are registered here - self.circuit.final_state.samples() - if binary: - return self.backend.cast(self._samples, dtype="int32") - else: - qubits = self.measurement_gate.target_qubits - return self.backend.samples_to_decimal(self._samples, len(qubits)) - - def frequencies(self, binary=True, registers=False): - """Returns the frequencies of measured samples. - - Args: - binary (bool): Return frequency keys in binary or decimal form. - registers (bool): Group frequencies according to registers. - - Returns: - A `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 `Counter` are in binary form, as strings of - 0s and 1s. - If `binary` is `False` - the keys of the `Counter` are integers. - """ - if self._frequencies is None: - self._frequencies = self.backend.calculate_frequencies( - self.samples(binary=False) - ) - if binary: - qubits = self.measurement_gate.target_qubits - return frequencies_to_binary(self._frequencies, len(qubits)) - else: - return self._frequencies - - def apply_bitflips(self, p0, p1=None): # pragma: no cover - return apply_bitflips(self, p0, p1) From 77ab54ffda48b2c35db6b428f05960f4b9aa9bd9 Mon Sep 17 00:00:00 2001 From: Stavros Efthymiou <35475381+stavros11@users.noreply.github.com> Date: Tue, 2 Jul 2024 17:08:49 +0400 Subject: [PATCH 4/5] fix: don't use prelude in circuit.rs --- src/circuit.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/circuit.rs b/src/circuit.rs index c336e06..04ac75d 100644 --- a/src/circuit.rs +++ b/src/circuit.rs @@ -1,7 +1,7 @@ use std::fmt::{self, Display}; use crate::gate::Gate; -use crate::prelude::One; +use crate::gate::One; #[derive(Debug, Clone, Copy, PartialEq)] struct Node { From 27755c39cdb62dff4f5460a2a3ae268ddf906deb Mon Sep 17 00:00:00 2001 From: Stavros Efthymiou <35475381+stavros11@users.noreply.github.com> Date: Tue, 2 Jul 2024 17:32:03 +0400 Subject: [PATCH 5/5] test: add test involving measurements --- crates/py/qibo_core/result.py | 33 +++++++++++++++------------------ crates/py/tests/test_circuit.py | 19 +++++++++++++++++++ 2 files changed, 34 insertions(+), 18 deletions(-) diff --git a/crates/py/qibo_core/result.py b/crates/py/qibo_core/result.py index 93ffd9c..3372e6d 100644 --- a/crates/py/qibo_core/result.py +++ b/crates/py/qibo_core/result.py @@ -157,6 +157,12 @@ def load(cls, filename: str): return cls.from_dict(payload) +def frequencies_to_binary(frequencies, nqubits): + return collections.Counter( + {"{:b}".format(k).zfill(nqubits): v for k, v in frequencies.items()} + ) + + class MeasurementOutcomes: """Object to store the outcomes of measurements after circuit execution. @@ -173,9 +179,7 @@ def __init__( self, samples: Optional[int] = None, frequencies: Optional[dict] = None, - backend=None, ): - self.backend = backend self._samples = samples self._frequencies = frequencies @@ -183,7 +187,9 @@ def __init__( def frequencies(self): """Frequencies of measured samples.""" if self._frequencies is None: - self._frequencies = self.backend.calculate_frequencies(self.samples) + nelements = len(self.samples[0]) + results, counts = np.unique(self.samples, axis=0, return_counts=True) + self._frequencies = {"".join(str(i) for i in r): c for r, c in zip(results.astype(int), counts)} return self._frequencies @property @@ -191,12 +197,11 @@ def samples(self): """Raw measurement samples.""" if self._samples is None: # generate samples that respect the existing frequencies - frequencies = self.frequencies(binary=False) - samples = np.concatenate( - [np.repeat(x, f) for x, f in frequencies.items()] - ) + samples = [] + for bitstring, counts in self.frequencies.items(): + samples.extend(counts * [[int(b) for b in bitstring]]) np.random.shuffle(samples) - self._samples = self.backend.cast(samples, "int32") + self._samples = np.array(samples, dtype="int32") return self._samples def to_dict(self): @@ -238,11 +243,9 @@ def from_dict(cls, payload: dict): "Both `frequencies` and `samples` found, discarding the `frequencies` and building out of the `samples`." ) payload.pop("frequencies") - backend = backends.construct_backend("numpy") return cls( samples=payload.get("samples"), frequencies=payload.get("frequencies"), - backend=backend, ) @classmethod @@ -260,12 +263,6 @@ def load(cls, filename: str): return cls.from_dict(payload) -def frequencies_to_binary(frequencies, nqubits): - return collections.Counter( - {"{:b}".format(k).zfill(nqubits): v for k, v in frequencies.items()} - ) - - class CircuitResult(QuantumState): """Object to store both the outcomes of measurements and the final state after circuit execution. @@ -296,7 +293,7 @@ def samples(self): probs = self.probabilities(self.elements) samples = self.backend.sample_shots(probs, self.nshots) samples = self.backend.samples_to_binary(samples, len(self.elements)) - self.outcomes = MeasurementOutcomes(samples=samples, backend=self.backend) + self.outcomes = MeasurementOutcomes(samples=samples) return self.outcomes.samples @@ -307,7 +304,7 @@ def frequencies(self): probs = self.probabilities(self.elements) frequencies = self.backend.sample_frequencies(probs, self.nshots) frequencies = frequencies_to_binary(frequencies, len(self.elements)) - self.outcomes = MeasurementOutcomes(samples=samples, backend=self.backend) + self.outcomes = MeasurementOutcomes(frequencies=frequencies) return self.outcomes.frequencies def to_dict(self): diff --git a/crates/py/tests/test_circuit.py b/crates/py/tests/test_circuit.py index 84a546f..9a1ff75 100644 --- a/crates/py/tests/test_circuit.py +++ b/crates/py/tests/test_circuit.py @@ -7,6 +7,7 @@ X = gate.Gate.X Y = gate.Gate.Y RX = gate.Gate.RX +M = gate.Gate.M def qibo_circuit(): cq = qibo.Circuit(5) @@ -50,3 +51,21 @@ def test_execute(): result = NumpyBackend().execute_circuit(c) target_result = NumpyBackend_().execute_circuit(cq) np.testing.assert_allclose(result.state(), target_result.state()) + + +def test_measurement(): + c = qibo_core_circuit() + c.add(M(), [1]) + c.add(M(), [3]) + c.add(M(), [4]) + + target_samples = np.zeros((100, 3)) + target_samples[:, 2] = 1 + + result = NumpyBackend().execute_circuit(c, nshots=100) + np.testing.assert_allclose(result.samples, target_samples) + assert result.frequencies == {"001": 100} + + result = NumpyBackend().execute_circuit(c, nshots=100) + assert result.frequencies == {"001": 100} + np.testing.assert_allclose(result.samples, target_samples)