From 8c93af092959f150b719a362a153234c5b1f102b Mon Sep 17 00:00:00 2001 From: Roland Guichard Date: Mon, 23 Oct 2023 15:01:54 +0100 Subject: [PATCH 01/83] First attempt at error protocol. --- qadence/backend.py | 2 ++ qadence/backends/pyqtorch/backend.py | 10 +++++++++- qadence/errors/__init__.py | 3 ++- qadence/models/quantum_model.py | 8 +++++++- 4 files changed, 20 insertions(+), 3 deletions(-) diff --git a/qadence/backend.py b/qadence/backend.py index 29348840..5c8726fa 100644 --- a/qadence/backend.py +++ b/qadence/backend.py @@ -20,6 +20,7 @@ ) from qadence.blocks.analog import ConstantAnalogRotation, WaitBlock from qadence.circuit import QuantumCircuit +from qadence.errors import Errors from qadence.measurements import Measurements from qadence.parameters import stringify from qadence.types import BackendName, DiffMode, Endianness @@ -217,6 +218,7 @@ def sample( param_values: dict[str, Tensor] = {}, n_shots: int = 1000, state: Tensor | None = None, + error: Errors | None = None, endianness: Endianness = Endianness.BIG, ) -> list[Counter]: """Sample bit strings. diff --git a/qadence/backends/pyqtorch/backend.py b/qadence/backends/pyqtorch/backend.py index e2d8100b..72fe62e4 100644 --- a/qadence/backends/pyqtorch/backend.py +++ b/qadence/backends/pyqtorch/backend.py @@ -14,6 +14,7 @@ from qadence.backends.utils import to_list_of_dicts from qadence.blocks import AbstractBlock from qadence.circuit import QuantumCircuit +from qadence.errors import Errors from qadence.measurements import Measurements from qadence.overlap import overlap_exact from qadence.states import zero_state @@ -195,6 +196,7 @@ def sample( param_values: dict[str, Tensor] = {}, n_shots: int = 1, state: Tensor | None = None, + error: Errors | None = None, endianness: Endianness = Endianness.BIG, ) -> list[Counter]: if n_shots < 1: @@ -215,17 +217,23 @@ def _sample(_probs: Tensor, n_shots: int, endianness: Endianness, n_qubits: int) wf = self.run(circuit=circuit, param_values=param_values, state=state) probs = torch.abs(torch.pow(wf, 2)) - return list( + counters = list( map( lambda _probs: _sample( _probs=_probs, n_shots=n_shots, + error=error, endianness=endianness, n_qubits=circuit.abstract.n_qubits, ), probs, ) ) + if error is not None: + error_fn = error.get_error_fn() + return error_fn(counters=counters, n_qubits=circuit.abstract.n_qubits) + else: + return counters def assign_parameters(self, circuit: ConvertedCircuit, param_values: dict[str, Tensor]) -> Any: raise NotImplementedError diff --git a/qadence/errors/__init__.py b/qadence/errors/__init__.py index dbc249b0..efca511f 100644 --- a/qadence/errors/__init__.py +++ b/qadence/errors/__init__.py @@ -1,6 +1,7 @@ from __future__ import annotations from .errors import NotPauliBlockError, NotSupportedError, QadenceException +from .protocols import Errors # Modules to be automatically added to the qadence namespace -__all__ = [] # type: ignore +__all__ = ["Errors"] diff --git a/qadence/models/quantum_model.py b/qadence/models/quantum_model.py index bd549a5f..a78cb795 100644 --- a/qadence/models/quantum_model.py +++ b/qadence/models/quantum_model.py @@ -20,6 +20,7 @@ from qadence.backends.pytorch_wrapper import DiffMode from qadence.blocks import AbstractBlock from qadence.circuit import QuantumCircuit +from qadence.errors import Errors from qadence.logger import get_logger from qadence.measurements import Measurements from qadence.utils import Endianness @@ -49,6 +50,7 @@ def __init__( diff_mode: DiffMode = DiffMode.AD, protocol: Measurements | None = None, configuration: BackendConfiguration | dict | None = None, + error: Errors | None = None, ): """Initialize a generic QuantumModel instance. @@ -93,6 +95,7 @@ def __init__( self._backend_name = backend self._diff_mode = diff_mode self._protocol = protocol + self._error = error self._params = nn.ParameterDict( { @@ -162,11 +165,14 @@ def sample( values: dict[str, torch.Tensor] = {}, n_shots: int = 1000, state: torch.Tensor | None = None, + error: Errors | None = None, endianness: Endianness = Endianness.BIG, ) -> list[Counter]: params = self.embedding_fn(self._params, values) + if error is None: + error = self._error return self.backend.sample( - self._circuit, params, n_shots=n_shots, state=state, endianness=endianness + self._circuit, params, n_shots=n_shots, state=state, error=error, endianness=endianness ) def expectation( From 04f34a884942200268b05c14ddb1c600ce71896b Mon Sep 17 00:00:00 2001 From: Gergana Velikova Date: Tue, 24 Oct 2023 08:52:45 +0200 Subject: [PATCH 02/83] change errors to exceptions, rename protocol --- qadence/__init__.py | 2 ++ qadence/backend.py | 6 +++-- qadence/backends/braket/backend.py | 2 +- qadence/backends/braket/convert_ops.py | 2 +- qadence/backends/pulser/backend.py | 2 +- qadence/backends/pulser/pulses.py | 8 +++---- qadence/backends/pyqtorch/backend.py | 8 +++---- qadence/backends/pytorch_wrapper.py | 22 +++++++++---------- qadence/blocks/utils.py | 2 +- qadence/errors/__init__.py | 1 - qadence/errors/protocols.py | 2 ++ qadence/exceptions/__init__.py | 7 ++++++ .../errors.py => exceptions/exceptions.py} | 0 qadence/measurements/__init__.py | 2 +- .../{protocols.py => measurement.py} | 4 ++-- qadence/ml_tools/models.py | 4 ++-- qadence/models/qnn.py | 14 ++++++------ qadence/models/quantum_model.py | 18 +++++++-------- qadence/overlap.py | 6 ++--- .../qadence/test_measurements/test_shadows.py | 10 ++++----- .../test_measurements/test_tomography.py | 22 +++++++++---------- 21 files changed, 78 insertions(+), 66 deletions(-) create mode 100644 qadence/errors/protocols.py create mode 100644 qadence/exceptions/__init__.py rename qadence/{errors/errors.py => exceptions/exceptions.py} (100%) rename qadence/measurements/{protocols.py => measurement.py} (90%) diff --git a/qadence/__init__.py b/qadence/__init__.py index c4f673fe..9b6eba99 100644 --- a/qadence/__init__.py +++ b/qadence/__init__.py @@ -11,6 +11,7 @@ from .circuit import * from .constructors import * from .errors import * +from .exceptions import * from .execution import * from .measurements import * from .ml_tools import * @@ -44,6 +45,7 @@ ".circuit", ".constructors", ".errors", + ".exceptions", ".execution", ".measurements", ".ml_tools", diff --git a/qadence/backend.py b/qadence/backend.py index 5c8726fa..8f3d48c6 100644 --- a/qadence/backend.py +++ b/qadence/backend.py @@ -154,7 +154,7 @@ def convert( self, circuit: QuantumCircuit, observable: list[AbstractBlock] | AbstractBlock | None = None ) -> Converted: """Convert an abstract circuit (and optionally and observable) to their native - representation. Additionally this function constructs an embedding function which maps from + representation. Additionally, this function constructs an embedding function which maps from user-facing parameters to device parameters (read more on parameter embedding [here][qadence.blocks.embedding.embedding]). """ @@ -263,7 +263,7 @@ def expectation( observable: list[ConvertedObservable] | ConvertedObservable, param_values: dict[str, Tensor] = {}, state: Tensor | None = None, - protocol: Measurements | None = None, + measurement: Measurements | None = None, endianness: Endianness = Endianness.BIG, ) -> Tensor: """Compute the expectation value of the `circuit` with the given `observable`. @@ -273,6 +273,8 @@ def expectation( param_values: _**Already embedded**_ parameters of the circuit. See [`embedding`][qadence.blocks.embedding.embedding] for more info. state: Initial state. + measurement: Optional measurement protocol. If None, use + exact expectation value with a statevector simulator. endianness: Endianness of the resulting bitstrings. """ raise NotImplementedError diff --git a/qadence/backends/braket/backend.py b/qadence/backends/braket/backend.py index 5c7aa868..2f456209 100644 --- a/qadence/backends/braket/backend.py +++ b/qadence/backends/braket/backend.py @@ -153,7 +153,7 @@ def expectation( observable: list[ConvertedObservable] | ConvertedObservable, param_values: dict[str, Tensor] = {}, state: Tensor | None = None, - protocol: Measurements | None = None, + measurement: Measurements | None = None, endianness: Endianness = Endianness.BIG, ) -> Tensor: # Do not flip endianness here because then we would have to reverse the observable diff --git a/qadence/backends/braket/convert_ops.py b/qadence/backends/braket/convert_ops.py index 09c76a59..5517203b 100644 --- a/qadence/backends/braket/convert_ops.py +++ b/qadence/backends/braket/convert_ops.py @@ -24,7 +24,7 @@ from braket.parametric import FreeParameter from qadence.blocks import AbstractBlock, CompositeBlock, PrimitiveBlock -from qadence.errors import NotSupportedError +from qadence.exceptions import NotSupportedError from qadence.operations import OpName from qadence.parameters import evaluate diff --git a/qadence/backends/pulser/backend.py b/qadence/backends/pulser/backend.py index af94c6c2..4e0dd100 100644 --- a/qadence/backends/pulser/backend.py +++ b/qadence/backends/pulser/backend.py @@ -220,7 +220,7 @@ def expectation( observable: list[ConvertedObservable] | ConvertedObservable, param_values: dict[str, Tensor] = {}, state: Tensor | None = None, - protocol: Measurements | None = None, + measurement: Measurements | None = None, endianness: Endianness = Endianness.BIG, ) -> Tensor: state = self.run(circuit, param_values=param_values, state=state, endianness=endianness) diff --git a/qadence/backends/pulser/pulses.py b/qadence/backends/pulser/pulses.py index 877346bc..189b50c6 100644 --- a/qadence/backends/pulser/pulses.py +++ b/qadence/backends/pulser/pulses.py @@ -102,17 +102,17 @@ def add_pulses( if block.qubit_support.is_global: pulse = analog_rot_pulse(a, w, p, d, global_channel, config) - sequence.add(pulse, GLOBAL_CHANNEL, protocol="wait-for-all") + sequence.add(pulse, GLOBAL_CHANNEL, measurement="wait-for-all") else: pulse = analog_rot_pulse(a, w, p, d, local_channel, config) sequence.target(qubit_support, LOCAL_CHANNEL) - sequence.add(pulse, LOCAL_CHANNEL, protocol="wait-for-all") + sequence.add(pulse, LOCAL_CHANNEL, measurement="wait-for-all") elif isinstance(block, AnalogEntanglement): (uuid, duration) = block.parameters.uuid_param("duration") t = evaluate(duration) if duration.is_number else sequence.declare_variable(uuid) sequence.add( - entangle_pulse(t, global_channel, config), GLOBAL_CHANNEL, protocol="wait-for-all" + entangle_pulse(t, global_channel, config), GLOBAL_CHANNEL, measurement="wait-for-all" ) elif isinstance(block, (RX, RY)): @@ -120,7 +120,7 @@ def add_pulses( angle = evaluate(p) if p.is_number else sequence.declare_variable(uuid) pulse = rx(angle) if isinstance(block, RX) else ry(angle) sequence.target(qubit_support, LOCAL_CHANNEL) - sequence.add(pulse, LOCAL_CHANNEL, protocol="wait-for-all") + sequence.add(pulse, LOCAL_CHANNEL, measurement="wait-for-all") elif isinstance(block, CompositeBlock) or isinstance(block, AnalogComposite): for block in block.blocks: diff --git a/qadence/backends/pyqtorch/backend.py b/qadence/backends/pyqtorch/backend.py index 72fe62e4..c6d2e469 100644 --- a/qadence/backends/pyqtorch/backend.py +++ b/qadence/backends/pyqtorch/backend.py @@ -127,7 +127,7 @@ def _batched_expectation( observable: list[ConvertedObservable] | ConvertedObservable, param_values: dict[str, Tensor] = {}, state: Tensor | None = None, - protocol: Measurements | None = None, + measurement: Measurements | None = None, endianness: Endianness = Endianness.BIG, ) -> Tensor: state = self.run( @@ -151,7 +151,7 @@ def _looped_expectation( observable: list[ConvertedObservable] | ConvertedObservable, param_values: dict[str, Tensor] = {}, state: Tensor | None = None, - protocol: Measurements | None = None, + measurement: Measurements | None = None, endianness: Endianness = Endianness.BIG, ) -> Tensor: state = zero_state(circuit.abstract.n_qubits, batch_size=1) if state is None else state @@ -177,7 +177,7 @@ def expectation( observable: list[ConvertedObservable] | ConvertedObservable, param_values: dict[str, Tensor] = {}, state: Tensor | None = None, - protocol: Measurements | None = None, + measurement: Measurements | None = None, endianness: Endianness = Endianness.BIG, ) -> Tensor: fn = self._looped_expectation if self.config.loop_expectation else self._batched_expectation @@ -186,7 +186,7 @@ def expectation( observable=observable, param_values=param_values, state=state, - protocol=protocol, + measurement=measurement, endianness=endianness, ) diff --git a/qadence/backends/pytorch_wrapper.py b/qadence/backends/pytorch_wrapper.py index 4c0fca56..637613c5 100644 --- a/qadence/backends/pytorch_wrapper.py +++ b/qadence/backends/pytorch_wrapper.py @@ -84,20 +84,20 @@ class DifferentiableExpectation: observable: list[ConvertedObservable] | ConvertedObservable param_values: dict[str, Tensor] state: Tensor | None = None - protocol: Measurements | None = None + measurement: Measurements | None = None endianness: Endianness = Endianness.BIG def ad(self) -> Tensor: self.observable = ( self.observable if isinstance(self.observable, list) else [self.observable] ) - if self.protocol: - expectation_fn = self.protocol.get_measurement_fn() + if self.measurement: + expectation_fn = self.measurement.get_measurement_fn() expectations = expectation_fn( circuit=self.circuit.original, observables=[obs.original for obs in self.observable], param_values=self.param_values, - options=self.protocol.options, + options=self.measurement.options, state=self.state, endianness=self.endianness, ) @@ -117,19 +117,19 @@ def psr(self, psr_fn: Callable, **psr_args: int | float | None) -> Tensor: # wrapper which unpacks the parameters # as pytorch grads can only calculated w.r.t tensors # so we unpack the params, feed in the names separately - # as apply doesnt take keyword arguments + # as apply doesn't take keyword arguments # We also fold in the observable into the backend which makes # life easier in the custom autodiff. self.observable = ( self.observable if isinstance(self.observable, list) else [self.observable] ) - if self.protocol is not None: + if self.measurement is not None: expectation_fn = partial( - self.protocol.get_measurement_fn(), + self.measurement.get_measurement_fn(), circuit=self.circuit.original, observables=[obs.original for obs in self.observable], - options=self.protocol.options, + options=self.measurement.options, state=self.state, endianness=self.endianness, ) @@ -229,7 +229,7 @@ def expectation( observable: list[ConvertedObservable] | ConvertedObservable, param_values: dict[str, Tensor] = {}, state: Tensor | None = None, - protocol: Measurements | None = None, + measurement: Measurements | None = None, endianness: Endianness = Endianness.BIG, ) -> Tensor: """Compute the expectation value of a given observable. @@ -239,7 +239,7 @@ def expectation( observable: A backend native observable to compute the expectation value from. param_values: A dict of values for symbolic substitution. state: An initial state. - protocol: A shot-based measurement protocol. + measurement: A shot-based measurement protocol. endianness: Endianness of the state. Returns: @@ -252,7 +252,7 @@ def expectation( observable=observable, param_values=param_values, state=state, - protocol=protocol, + measurement=measurement, endianness=endianness, ) diff --git a/qadence/blocks/utils.py b/qadence/blocks/utils.py index 216cdd40..387015ea 100644 --- a/qadence/blocks/utils.py +++ b/qadence/blocks/utils.py @@ -23,7 +23,7 @@ from qadence.blocks.analog import AnalogBlock, AnalogComposite, ConstantAnalogRotation, WaitBlock from qadence.blocks.analog import chain as analog_chain from qadence.blocks.analog import kron as analog_kron -from qadence.errors import NotPauliBlockError +from qadence.exceptions import NotPauliBlockError from qadence.logger import get_logger from qadence.parameters import Parameter diff --git a/qadence/errors/__init__.py b/qadence/errors/__init__.py index efca511f..8044d360 100644 --- a/qadence/errors/__init__.py +++ b/qadence/errors/__init__.py @@ -1,6 +1,5 @@ from __future__ import annotations -from .errors import NotPauliBlockError, NotSupportedError, QadenceException from .protocols import Errors # Modules to be automatically added to the qadence namespace diff --git a/qadence/errors/protocols.py b/qadence/errors/protocols.py new file mode 100644 index 00000000..acb8ab0c --- /dev/null +++ b/qadence/errors/protocols.py @@ -0,0 +1,2 @@ +class Errors(): + pass # placeholder \ No newline at end of file diff --git a/qadence/exceptions/__init__.py b/qadence/exceptions/__init__.py new file mode 100644 index 00000000..36ef4614 --- /dev/null +++ b/qadence/exceptions/__init__.py @@ -0,0 +1,7 @@ +from __future__ import annotations + +from .exceptions import NotPauliBlockError, NotSupportedError, QadenceException + +# Modules to be automatically added to the qadence namespace +__all__ = ["exceptions"] + diff --git a/qadence/errors/errors.py b/qadence/exceptions/exceptions.py similarity index 100% rename from qadence/errors/errors.py rename to qadence/exceptions/exceptions.py diff --git a/qadence/measurements/__init__.py b/qadence/measurements/__init__.py index dde75742..348aedcb 100644 --- a/qadence/measurements/__init__.py +++ b/qadence/measurements/__init__.py @@ -1,6 +1,6 @@ from __future__ import annotations -from .protocols import Measurements +from .measurement import Measurements # Modules to be automatically added to the qadence namespace __all__ = ["Measurements"] diff --git a/qadence/measurements/protocols.py b/qadence/measurements/measurement.py similarity index 90% rename from qadence/measurements/protocols.py rename to qadence/measurements/measurement.py index cd2ea6fa..b0393879 100644 --- a/qadence/measurements/protocols.py +++ b/qadence/measurements/measurement.py @@ -16,8 +16,8 @@ class Measurements: TOMOGRAPHY = "tomography" SHADOW = "shadow" - def __init__(self, protocol: str, options: dict) -> None: - self.protocol: str = protocol + def __init__(self, measurement: str, options: dict) -> None: + self.measurement: str = protocol self.options: dict = options def get_measurement_fn(self) -> Callable: diff --git a/qadence/ml_tools/models.py b/qadence/ml_tools/models.py index 45cc2d9e..af047f1f 100644 --- a/qadence/ml_tools/models.py +++ b/qadence/ml_tools/models.py @@ -214,7 +214,7 @@ def expectation( values: dict[str, torch.Tensor], observable: List[ConvertedObservable] | ConvertedObservable | None = None, state: torch.Tensor | None = None, - protocol: Measurements | None = None, + measurement: Measurements | None = None, endianness: Endianness = Endianness.BIG, ) -> Tensor: """ @@ -228,7 +228,7 @@ def expectation( values=self._transform_x(values), observable=observable if observable is not None else self.model._observable, state=state, - protocol=protocol, + measurement=measurement, endianness=endianness, ) return self._output_scaling * exp + self._output_shifting diff --git a/qadence/models/qnn.py b/qadence/models/qnn.py index 1cd14158..3fddb9cd 100644 --- a/qadence/models/qnn.py +++ b/qadence/models/qnn.py @@ -46,7 +46,7 @@ def __init__( transform: Callable[[Tensor], Tensor] = None, # transform output of the QNN backend: BackendName = BackendName.PYQTORCH, diff_mode: DiffMode = DiffMode.AD, - protocol: Measurements | None = None, + measurement: Measurements | None = None, configuration: BackendConfiguration | dict | None = None, ): """Initialize the QNN @@ -60,7 +60,7 @@ def __init__( transform: A transformation applied to the output of the QNN. backend: The chosen quantum backend. diff_mode: The differentiation engine to use. Choices 'gpsr' or 'ad'. - protocol: optional measurement protocol. If None, + measurement: optional measurement protocol. If None, use exact expectation value with a statevector simulator configuration: optional configuration for the backend @@ -70,7 +70,7 @@ def __init__( observable=observable, backend=backend, diff_mode=diff_mode, - protocol=protocol, + measurement=measurement, configuration=configuration, ) @@ -83,7 +83,7 @@ def forward( self, values: dict[str, Tensor] | Tensor = None, state: Tensor | None = None, - protocol: Measurements | None = None, + measurement: Measurements | None = None, endianness: Endianness = Endianness.BIG, ) -> Tensor: """Forward pass of the model @@ -110,11 +110,11 @@ def forward( values = {} if not isinstance(values, dict): values = self._format_to_dict(values) - if protocol is None: - protocol = self._protocol + if measurement is None: + measurement = self._measurement return self.transform( - self.expectation(values=values, state=state, protocol=protocol, endianness=endianness) + self.expectation(values=values, state=state, measurement=measurement, endianness=endianness) ) def _format_to_dict(self, values: Tensor) -> dict[str, Tensor]: diff --git a/qadence/models/quantum_model.py b/qadence/models/quantum_model.py index a78cb795..b2609983 100644 --- a/qadence/models/quantum_model.py +++ b/qadence/models/quantum_model.py @@ -48,7 +48,7 @@ def __init__( observable: list[AbstractBlock] | AbstractBlock | None = None, backend: BackendName | str = BackendName.PYQTORCH, diff_mode: DiffMode = DiffMode.AD, - protocol: Measurements | None = None, + measurement: Measurements | None = None, configuration: BackendConfiguration | dict | None = None, error: Errors | None = None, ): @@ -61,7 +61,7 @@ def __init__( backend: A backend for circuit execution. diff_mode: A differentiability mode. Parameter shift based modes work on all backends. AD based modes only on PyTorch based backends. - protocol: Optional measurement protocol. If None, use + measurement: Optional measurement protocol. If None, use exact expectation value with a statevector simulator. configuration: Configuration for the backend. @@ -94,7 +94,7 @@ def __init__( self._observable = conv.observable self._backend_name = backend self._diff_mode = diff_mode - self._protocol = protocol + self._measurement = measurement self._error = error self._params = nn.ParameterDict( @@ -180,7 +180,7 @@ def expectation( values: dict[str, Tensor] = {}, observable: list[ConvertedObservable] | ConvertedObservable | None = None, state: Optional[Tensor] = None, - protocol: Measurements | None = None, + measurement: Measurements | None = None, endianness: Endianness = Endianness.BIG, ) -> Tensor: """Compute expectation using the given backend. @@ -198,15 +198,15 @@ def expectation( observable = self._observable params = self.embedding_fn(self._params, values) - if protocol is None: - protocol = self._protocol + if measurement is None: + measurement = self._measurement return self.backend.expectation( circuit=self._circuit, observable=observable, param_values=params, state=state, - protocol=protocol, + measurement=measurement, endianness=endianness, ) @@ -224,7 +224,7 @@ def _to_dict(self, save_params: bool = False) -> dict[str, Any]: "observable": abs_obs, "backend": self._backend_name, "diff_mode": self._diff_mode, - "protocol": self._protocol._to_dict() if self._protocol is not None else {}, + "measurement": self._measurement._to_dict() if self._measurement is not None else {}, "backend_configuration": asdict(self.backend.backend.config), # type: ignore } param_dict_conv = {} @@ -246,7 +246,7 @@ def _from_dict(cls, d: dict, as_torch: bool = False) -> QuantumModel: ), backend=qm_dict["backend"], diff_mode=qm_dict["diff_mode"], - protocol=Measurements._from_dict(qm_dict["protocol"]), + measurement=Measurements._from_dict(qm_dict["measurement"]), configuration=config_factory(qm_dict["backend"], qm_dict["backend_configuration"]), ) diff --git a/qadence/overlap.py b/qadence/overlap.py index c5fa9615..cce44727 100644 --- a/qadence/overlap.py +++ b/qadence/overlap.py @@ -317,7 +317,7 @@ def __init__( ket_circuit: QuantumCircuit, backend: BackendName = BackendName.PYQTORCH, diff_mode: DiffMode = DiffMode.AD, - protocol: Measurements | None = None, + measurement: Measurements | None = None, configuration: BackendConfiguration | dict | None = None, method: OverlapMethod = OverlapMethod.EXACT, ): @@ -333,7 +333,7 @@ def __init__( bra_circuit, backend=backend, diff_mode=diff_mode, - protocol=protocol, + measurement=measurement, configuration=configuration, ) self.bra_feat_param_names = set([inp.name for inp in self.inputs]) @@ -343,7 +343,7 @@ def __init__( ket_circuit, backend=backend, diff_mode=diff_mode, - protocol=protocol, + measurement=measurement, configuration=configuration, ) self.ket_feat_param_names = set([inp.name for inp in self.ket_model.inputs]) diff --git a/tests/qadence/test_measurements/test_shadows.py b/tests/qadence/test_measurements/test_shadows.py index 5ef43152..990fa4f5 100644 --- a/tests/qadence/test_measurements/test_shadows.py +++ b/tests/qadence/test_measurements/test_shadows.py @@ -232,12 +232,12 @@ def test_estimations_comparison_tomo_forward_pass(circuit: QuantumCircuit, value options = {"n_shots": 100000} estimated_exp_tomo = model.expectation( values=values, - protocol=Measurements(protocol=Measurements.TOMOGRAPHY, options=options), + measurement=Measurements(measurement=Measurements.TOMOGRAPHY, options=options), ) new_options = {"accuracy": 0.1, "confidence": 0.1} estimated_exp_shadow = model.expectation( values=values, - protocol=Measurements(protocol=Measurements.SHADOW, options=new_options), + measurement=Measurements(measurement=Measurements.SHADOW, options=new_options), ) # N = 54400. assert torch.allclose(estimated_exp_tomo, pyq_exp_exact, atol=1.0e-2) assert torch.allclose(estimated_exp_shadow, pyq_exp_exact, atol=0.1) @@ -265,7 +265,7 @@ def test_chemistry_hamiltonian_1() -> None: exact = model.expectation(values=param_values) estim = model.expectation( values=param_values, - protocol=Measurements(protocol=Measurements.SHADOW, options=kwargs), + measurement=Measurements(measurement=Measurements.SHADOW, options=kwargs), ) assert torch.allclose(estim, exact, atol=0.3) @@ -291,7 +291,7 @@ def test_chemistry_hamiltonian_2() -> None: exact = model.expectation(values=param_values) estim = model.expectation( values=param_values, - protocol=Measurements(protocol=Measurements.SHADOW, options=kwargs), + measurement=Measurements(measurement=Measurements.SHADOW, options=kwargs), ) assert torch.allclose(estim, exact, atol=0.2) @@ -320,6 +320,6 @@ def test_chemistry_hamiltonian_3() -> None: exact = model.expectation(values=param_values) estim = model.expectation( values=param_values, - protocol=Measurements(protocol=Measurements.SHADOW, options=kwargs), + measurement=Measurements(measurement=Measurements.SHADOW, options=kwargs), ) assert torch.allclose(estim, exact, atol=0.3) diff --git a/tests/qadence/test_measurements/test_tomography.py b/tests/qadence/test_measurements/test_tomography.py index dbed4bf4..1a06b9b5 100644 --- a/tests/qadence/test_measurements/test_tomography.py +++ b/tests/qadence/test_measurements/test_tomography.py @@ -326,8 +326,8 @@ def test_basic_tomography_for_backend_forward_pass(circuit: QuantumCircuit) -> N qm = QuantumModel(circuit=circuit, observable=obs, backend=backend, diff_mode=diff_mode) exp_tomo = qm.expectation( values=inputs, - protocol=Measurements( - protocol=Measurements.TOMOGRAPHY, + measurement=Measurements( + measurement=Measurements.TOMOGRAPHY, options=kwargs, ), )[0] @@ -352,7 +352,7 @@ def test_basic_tomography_for_quantum_model(circuit: QuantumCircuit) -> None: kwargs = {"n_shots": 100000} estimated_values = model.expectation( inputs, - protocol=Measurements(protocol=Measurements.TOMOGRAPHY, options=kwargs), + measurement=Measurements(measurement=Measurements.TOMOGRAPHY, options=kwargs), ) pyqtorch_backend = backend_factory(backend=backend, diff_mode=diff_mode) (conv_circ, conv_obs, embed, params) = pyqtorch_backend.convert(circuit, observable) @@ -376,7 +376,7 @@ def test_basic_list_observables_tomography_for_quantum_model(circuit: QuantumCir kwargs = {"n_shots": 100000} estimated_values = model.expectation( inputs, - protocol=Measurements(protocol=Measurements.TOMOGRAPHY, options=kwargs), + measurement=Measurements(measurement=Measurements.TOMOGRAPHY, options=kwargs), ) pyqtorch_backend = backend_factory(BackendName.PYQTORCH, diff_mode=DiffMode.GPSR) (conv_circ, conv_obs, embed, params) = pyqtorch_backend.convert( @@ -437,7 +437,7 @@ def test_basic_tomography_for_parametric_circuit_forward_pass( kwargs = {"n_shots": 100000} estimated_values = model.expectation( values=values, - protocol=Measurements(protocol=Measurements.TOMOGRAPHY, options=kwargs), + measurement=Measurements(measurement=Measurements.TOMOGRAPHY, options=kwargs), ) pyqtorch_backend = backend_factory(BackendName.PYQTORCH, diff_mode=DiffMode.GPSR) (conv_circ, conv_obs, embed, params) = pyqtorch_backend.convert(circuit, observable) @@ -483,16 +483,16 @@ def test_forward_and_backward_passes_with_qnn(observable: AbstractBlock, accepta circuit = QuantumCircuit(n_qubits, fm, ansatz) values = {"phi": torch.rand(batch_size, requires_grad=True)} - protocol = Measurements(protocol=Measurements.TOMOGRAPHY, options=kwargs) + protocol = Measurements(measurement=Measurements.TOMOGRAPHY, options=kwargs) model_with_psr = QNN(circuit=circuit, observable=observable, diff_mode=DiffMode.GPSR) model_with_psr_and_init = QNN( - circuit=circuit, observable=observable, diff_mode=DiffMode.GPSR, protocol=protocol + circuit=circuit, observable=observable, diff_mode=DiffMode.GPSR, measurement=measurement ) model_with_psr.zero_grad() expectation_tomo = model_with_psr.expectation( values=values, - protocol=protocol, + measurement=measurement, ) expectation_tomo_init = model_with_psr_and_init.expectation(values=values) assert torch.allclose(expectation_tomo, expectation_tomo_init, atol=acceptance) @@ -540,7 +540,7 @@ def test_partial_derivatives_with_qnn(observable: AbstractBlock, acceptance: flo model_with_psr.zero_grad() expectation_tomo = model_with_psr.expectation( values=values, - protocol=Measurements(protocol=Measurements.TOMOGRAPHY, options=kwargs), + measurement=Measurements(measurement=Measurements.TOMOGRAPHY, options=kwargs), ) dexpval_tomo_phi = torch.autograd.grad( expectation_tomo, @@ -636,7 +636,7 @@ def test_high_order_derivatives_with_qnn(observable: AbstractBlock, acceptance: model_with_psr.zero_grad() expectation_tomo = model_with_psr.expectation( values=values, - protocol=Measurements(protocol=Measurements.TOMOGRAPHY, options=kwargs), + measurement=Measurements(measurement=Measurements.TOMOGRAPHY, options=kwargs), ) dexpval_tomo_phi = torch.autograd.grad( expectation_tomo, @@ -700,6 +700,6 @@ def test_chemistry_hamiltonian() -> None: ) estim = model.expectation( values={}, - protocol=Measurements(protocol=Measurements.TOMOGRAPHY, options=kwargs), + measurement=Measurements(measurement=Measurements.TOMOGRAPHY, options=kwargs), ) assert torch.allclose(estim, exact, atol=LOW_ACCEPTANCE) From 1210b96613cc7362af05d2dc09da48211a18edac Mon Sep 17 00:00:00 2001 From: Gergana Velikova Date: Wed, 25 Oct 2023 09:22:53 +0200 Subject: [PATCH 03/83] protocols renaming --- qadence/backend.py | 1 + qadence/backends/braket/backend.py | 2 ++ qadence/backends/pulser/backend.py | 2 ++ qadence/backends/pyqtorch/backend.py | 1 - qadence/backends/pytorch_wrapper.py | 7 ++++++- qadence/errors/protocols.py | 4 ++-- qadence/exceptions/__init__.py | 1 - qadence/execution.py | 7 +++++-- qadence/measurements/measurement.py | 4 ++-- qadence/measurements/shadow.py | 3 +++ qadence/measurements/tomography.py | 3 +++ qadence/models/qnn.py | 4 +++- tests/qadence/test_measurements/test_shadows.py | 10 +++++----- .../qadence/test_measurements/test_tomography.py | 16 ++++++++-------- 14 files changed, 42 insertions(+), 23 deletions(-) diff --git a/qadence/backend.py b/qadence/backend.py index 8f3d48c6..950a3110 100644 --- a/qadence/backend.py +++ b/qadence/backend.py @@ -229,6 +229,7 @@ def sample( [`embedding`][qadence.blocks.embedding.embedding] for more info. n_shots: Number of shots to sample. state: Initial state. + error: A noise model to use. endianness: Endianness of the resulting bitstrings. """ raise NotImplementedError diff --git a/qadence/backends/braket/backend.py b/qadence/backends/braket/backend.py index 2f456209..8b2784b2 100644 --- a/qadence/backends/braket/backend.py +++ b/qadence/backends/braket/backend.py @@ -15,6 +15,7 @@ from qadence.backends.utils import to_list_of_dicts from qadence.blocks import AbstractBlock, block_to_tensor from qadence.circuit import QuantumCircuit +from qadence.errors import Errors from qadence.measurements import Measurements from qadence.overlap import overlap_exact from qadence.utils import Endianness @@ -122,6 +123,7 @@ def sample( param_values: dict[str, Tensor] = {}, n_shots: int = 1, state: Tensor | None = None, + error: Errors | None = None, endianness: Endianness = Endianness.BIG, ) -> list[Counter]: """Execute the circuit and return samples of the resulting wavefunction.""" diff --git a/qadence/backends/pulser/backend.py b/qadence/backends/pulser/backend.py index 4e0dd100..59c38f82 100644 --- a/qadence/backends/pulser/backend.py +++ b/qadence/backends/pulser/backend.py @@ -19,6 +19,7 @@ from qadence.backends.utils import to_list_of_dicts from qadence.blocks import AbstractBlock from qadence.circuit import QuantumCircuit +from qadence.errors import Errors from qadence.measurements import Measurements from qadence.overlap import overlap_exact from qadence.register import Register @@ -195,6 +196,7 @@ def sample( param_values: dict[str, Tensor] = {}, n_shots: int = 1, state: Tensor | None = None, + error: Errors | None = None, endianness: Endianness = Endianness.BIG, ) -> list[Counter]: if n_shots < 1: diff --git a/qadence/backends/pyqtorch/backend.py b/qadence/backends/pyqtorch/backend.py index c6d2e469..8f3ee5a1 100644 --- a/qadence/backends/pyqtorch/backend.py +++ b/qadence/backends/pyqtorch/backend.py @@ -222,7 +222,6 @@ def _sample(_probs: Tensor, n_shots: int, endianness: Endianness, n_qubits: int) lambda _probs: _sample( _probs=_probs, n_shots=n_shots, - error=error, endianness=endianness, n_qubits=circuit.abstract.n_qubits, ), diff --git a/qadence/backends/pytorch_wrapper.py b/qadence/backends/pytorch_wrapper.py index 637613c5..13cf7975 100644 --- a/qadence/backends/pytorch_wrapper.py +++ b/qadence/backends/pytorch_wrapper.py @@ -271,8 +271,9 @@ def sample( self, circuit: ConvertedCircuit, param_values: dict[str, Tensor], - state: Tensor | None = None, n_shots: int = 1, + state: Tensor | None = None, + error: Errors | None = None, endianness: Endianness = Endianness.BIG, ) -> list[Counter]: """Sample bitstring from the registered circuit. @@ -281,6 +282,9 @@ def sample( circuit: A backend native quantum circuit to be executed. param_values: The values of the parameters after embedding n_shots: The number of shots. Defaults to 1. + state: Initial state. + error: A noise model to use. + endianness: Endianness of the resulting bitstrings. Returns: An iterable with all the sampled bitstrings @@ -292,6 +296,7 @@ def sample( state=state, n_shots=n_shots, endianness=endianness, + error=error, ) def circuit(self, circuit: QuantumCircuit) -> ConvertedCircuit: diff --git a/qadence/errors/protocols.py b/qadence/errors/protocols.py index acb8ab0c..79a8791f 100644 --- a/qadence/errors/protocols.py +++ b/qadence/errors/protocols.py @@ -1,2 +1,2 @@ -class Errors(): - pass # placeholder \ No newline at end of file +class Errors: + pass # placeholder diff --git a/qadence/exceptions/__init__.py b/qadence/exceptions/__init__.py index 36ef4614..5c433408 100644 --- a/qadence/exceptions/__init__.py +++ b/qadence/exceptions/__init__.py @@ -4,4 +4,3 @@ # Modules to be automatically added to the qadence namespace __all__ = ["exceptions"] - diff --git a/qadence/execution.py b/qadence/execution.py index ecbeb2ff..52d826e6 100644 --- a/qadence/execution.py +++ b/qadence/execution.py @@ -13,6 +13,7 @@ from qadence.register import Register from qadence.types import DiffMode from qadence.utils import Endianness +from qadence.errors import Errors # Modules to be automatically added to the qadence namespace __all__ = ["run", "sample", "expectation"] @@ -94,7 +95,7 @@ def sample( configuration: Union[BackendConfiguration, dict, None] = None, ) -> list[Counter]: """Convenience wrapper for the `QuantumModel.sample` method. This is a - `functools.singledispatch`ed function so it can be called with a number of different arguments. + `functools.singledispatch`ed function, so it can be called with a number of different arguments. See the examples of the [`expectation`][qadence.execution.expectation] function. This function works exactly the same. @@ -120,6 +121,7 @@ def _( state: Union[Tensor, None] = None, n_shots: int = 100, backend: BackendName = BackendName.PYQTORCH, + error: Errors | None = None, endianness: Endianness = Endianness.BIG, configuration: Union[BackendConfiguration, dict, None] = None, ) -> list[Counter]: @@ -130,6 +132,7 @@ def _( param_values=conv.embedding_fn(conv.params, values), n_shots=n_shots, state=state, + error=error, endianness=endianness, ) @@ -162,7 +165,7 @@ def expectation( configuration: Union[BackendConfiguration, dict, None] = None, ) -> Tensor: """Convenience wrapper for the `QuantumModel.expectation` method. This is a - `functools.singledispatch`ed function so it can be called with a number of different arguments + `functools.singledispatch`ed function, so it can be called with a number of different arguments (see in the examples). Arguments: diff --git a/qadence/measurements/measurement.py b/qadence/measurements/measurement.py index b0393879..cd2ea6fa 100644 --- a/qadence/measurements/measurement.py +++ b/qadence/measurements/measurement.py @@ -16,8 +16,8 @@ class Measurements: TOMOGRAPHY = "tomography" SHADOW = "shadow" - def __init__(self, measurement: str, options: dict) -> None: - self.measurement: str = protocol + def __init__(self, protocol: str, options: dict) -> None: + self.protocol: str = protocol self.options: dict = options def get_measurement_fn(self) -> Callable: diff --git a/qadence/measurements/shadow.py b/qadence/measurements/shadow.py index f49e7c56..068f9abf 100644 --- a/qadence/measurements/shadow.py +++ b/qadence/measurements/shadow.py @@ -21,6 +21,7 @@ from qadence.blocks.primitive import PrimitiveBlock from qadence.blocks.utils import get_pauli_blocks, unroll_block_with_scaling from qadence.circuit import QuantumCircuit +from qadence.errors import Errors from qadence.operations import X, Y, Z, chain, kron from qadence.states import one_state, zero_state from qadence.types import Endianness @@ -128,6 +129,7 @@ def classical_shadow( state: Tensor | None = None, backend_name: BackendName = BackendName.PYQTORCH, # FIXME: Changed below from Little to Big, double-check when Roland is back + error: Errors | None = None, endianness: Endianness = Endianness.BIG, ) -> list: shadow: list = [] @@ -153,6 +155,7 @@ def classical_shadow( param_values=param_values, n_shots=1, state=state, + error=error, endianness=endianness, ) batched_shadow = [] diff --git a/qadence/measurements/tomography.py b/qadence/measurements/tomography.py index 2a8aa57f..562ff6a5 100644 --- a/qadence/measurements/tomography.py +++ b/qadence/measurements/tomography.py @@ -12,6 +12,7 @@ from qadence.blocks import AbstractBlock, PrimitiveBlock from qadence.blocks.utils import unroll_block_with_scaling from qadence.circuit import QuantumCircuit +from qadence.errors import Errors from qadence.operations import H, SDagger, X, Y, Z, chain from qadence.parameters import evaluate from qadence.utils import Endianness @@ -84,6 +85,7 @@ def iterate_pauli_decomposition( state: Tensor | None = None, backend_name: BackendName = BackendName.PYQTORCH, endianness: Endianness = Endianness.BIG, + error: Errors | None = None, ) -> Tensor: """Estimate total expectation value by averaging all Pauli terms.""" @@ -110,6 +112,7 @@ def iterate_pauli_decomposition( param_values=param_values, n_shots=n_shots, state=state, + error=error, endianness=endianness, ) estim_values = empirical_average(samples=samples, support=support) diff --git a/qadence/models/qnn.py b/qadence/models/qnn.py index 3fddb9cd..36bd3eb4 100644 --- a/qadence/models/qnn.py +++ b/qadence/models/qnn.py @@ -114,7 +114,9 @@ def forward( measurement = self._measurement return self.transform( - self.expectation(values=values, state=state, measurement=measurement, endianness=endianness) + self.expectation( + values=values, state=state, measurement=measurement, endianness=endianness + ) ) def _format_to_dict(self, values: Tensor) -> dict[str, Tensor]: diff --git a/tests/qadence/test_measurements/test_shadows.py b/tests/qadence/test_measurements/test_shadows.py index 990fa4f5..d2c9c107 100644 --- a/tests/qadence/test_measurements/test_shadows.py +++ b/tests/qadence/test_measurements/test_shadows.py @@ -232,12 +232,12 @@ def test_estimations_comparison_tomo_forward_pass(circuit: QuantumCircuit, value options = {"n_shots": 100000} estimated_exp_tomo = model.expectation( values=values, - measurement=Measurements(measurement=Measurements.TOMOGRAPHY, options=options), + measurement=Measurements(protocol=Measurements.TOMOGRAPHY, options=options), ) new_options = {"accuracy": 0.1, "confidence": 0.1} estimated_exp_shadow = model.expectation( values=values, - measurement=Measurements(measurement=Measurements.SHADOW, options=new_options), + measurement=Measurements(protocol=Measurements.SHADOW, options=new_options), ) # N = 54400. assert torch.allclose(estimated_exp_tomo, pyq_exp_exact, atol=1.0e-2) assert torch.allclose(estimated_exp_shadow, pyq_exp_exact, atol=0.1) @@ -265,7 +265,7 @@ def test_chemistry_hamiltonian_1() -> None: exact = model.expectation(values=param_values) estim = model.expectation( values=param_values, - measurement=Measurements(measurement=Measurements.SHADOW, options=kwargs), + measurement=Measurements(protocol=Measurements.SHADOW, options=kwargs), ) assert torch.allclose(estim, exact, atol=0.3) @@ -291,7 +291,7 @@ def test_chemistry_hamiltonian_2() -> None: exact = model.expectation(values=param_values) estim = model.expectation( values=param_values, - measurement=Measurements(measurement=Measurements.SHADOW, options=kwargs), + measurement=Measurements(protocol=Measurements.SHADOW, options=kwargs), ) assert torch.allclose(estim, exact, atol=0.2) @@ -320,6 +320,6 @@ def test_chemistry_hamiltonian_3() -> None: exact = model.expectation(values=param_values) estim = model.expectation( values=param_values, - measurement=Measurements(measurement=Measurements.SHADOW, options=kwargs), + measurement=Measurements(protocol=Measurements.SHADOW, options=kwargs), ) assert torch.allclose(estim, exact, atol=0.3) diff --git a/tests/qadence/test_measurements/test_tomography.py b/tests/qadence/test_measurements/test_tomography.py index 1a06b9b5..d5afbbcf 100644 --- a/tests/qadence/test_measurements/test_tomography.py +++ b/tests/qadence/test_measurements/test_tomography.py @@ -327,7 +327,7 @@ def test_basic_tomography_for_backend_forward_pass(circuit: QuantumCircuit) -> N exp_tomo = qm.expectation( values=inputs, measurement=Measurements( - measurement=Measurements.TOMOGRAPHY, + protocol=Measurements.TOMOGRAPHY, options=kwargs, ), )[0] @@ -352,7 +352,7 @@ def test_basic_tomography_for_quantum_model(circuit: QuantumCircuit) -> None: kwargs = {"n_shots": 100000} estimated_values = model.expectation( inputs, - measurement=Measurements(measurement=Measurements.TOMOGRAPHY, options=kwargs), + measurement=Measurements(protocol=Measurements.TOMOGRAPHY, options=kwargs), ) pyqtorch_backend = backend_factory(backend=backend, diff_mode=diff_mode) (conv_circ, conv_obs, embed, params) = pyqtorch_backend.convert(circuit, observable) @@ -376,7 +376,7 @@ def test_basic_list_observables_tomography_for_quantum_model(circuit: QuantumCir kwargs = {"n_shots": 100000} estimated_values = model.expectation( inputs, - measurement=Measurements(measurement=Measurements.TOMOGRAPHY, options=kwargs), + measurement=Measurements(protocol=Measurements.TOMOGRAPHY, options=kwargs), ) pyqtorch_backend = backend_factory(BackendName.PYQTORCH, diff_mode=DiffMode.GPSR) (conv_circ, conv_obs, embed, params) = pyqtorch_backend.convert( @@ -437,7 +437,7 @@ def test_basic_tomography_for_parametric_circuit_forward_pass( kwargs = {"n_shots": 100000} estimated_values = model.expectation( values=values, - measurement=Measurements(measurement=Measurements.TOMOGRAPHY, options=kwargs), + measurement=Measurements(protocol=Measurements.TOMOGRAPHY, options=kwargs), ) pyqtorch_backend = backend_factory(BackendName.PYQTORCH, diff_mode=DiffMode.GPSR) (conv_circ, conv_obs, embed, params) = pyqtorch_backend.convert(circuit, observable) @@ -483,7 +483,7 @@ def test_forward_and_backward_passes_with_qnn(observable: AbstractBlock, accepta circuit = QuantumCircuit(n_qubits, fm, ansatz) values = {"phi": torch.rand(batch_size, requires_grad=True)} - protocol = Measurements(measurement=Measurements.TOMOGRAPHY, options=kwargs) + measurement = Measurements(protocol=Measurements.TOMOGRAPHY, options=kwargs) model_with_psr = QNN(circuit=circuit, observable=observable, diff_mode=DiffMode.GPSR) model_with_psr_and_init = QNN( @@ -540,7 +540,7 @@ def test_partial_derivatives_with_qnn(observable: AbstractBlock, acceptance: flo model_with_psr.zero_grad() expectation_tomo = model_with_psr.expectation( values=values, - measurement=Measurements(measurement=Measurements.TOMOGRAPHY, options=kwargs), + measurement=Measurements(protocol=Measurements.TOMOGRAPHY, options=kwargs), ) dexpval_tomo_phi = torch.autograd.grad( expectation_tomo, @@ -636,7 +636,7 @@ def test_high_order_derivatives_with_qnn(observable: AbstractBlock, acceptance: model_with_psr.zero_grad() expectation_tomo = model_with_psr.expectation( values=values, - measurement=Measurements(measurement=Measurements.TOMOGRAPHY, options=kwargs), + measurement=Measurements(protocol=Measurements.TOMOGRAPHY, options=kwargs), ) dexpval_tomo_phi = torch.autograd.grad( expectation_tomo, @@ -700,6 +700,6 @@ def test_chemistry_hamiltonian() -> None: ) estim = model.expectation( values={}, - measurement=Measurements(measurement=Measurements.TOMOGRAPHY, options=kwargs), + measurement=Measurements(protocol=Measurements.TOMOGRAPHY, options=kwargs), ) assert torch.allclose(estim, exact, atol=LOW_ACCEPTANCE) From 404ebaabcb1dc28a0024263416cd934bbc3546f8 Mon Sep 17 00:00:00 2001 From: Gergana Velikova Date: Thu, 26 Oct 2023 16:01:48 +0200 Subject: [PATCH 04/83] cleaned outstanding naming issues --- qadence/backend.py | 4 ++-- qadence/backends/braket/backend.py | 7 ++++++- qadence/backends/pulser/backend.py | 7 ++++++- qadence/backends/pulser/pulses.py | 8 ++++---- 4 files changed, 18 insertions(+), 8 deletions(-) diff --git a/qadence/backend.py b/qadence/backend.py index 950a3110..b556382f 100644 --- a/qadence/backend.py +++ b/qadence/backend.py @@ -230,7 +230,7 @@ def sample( n_shots: Number of shots to sample. state: Initial state. error: A noise model to use. - endianness: Endianness of the resulting bitstrings. + endianness: Endianness of the resulting bit strings. """ raise NotImplementedError @@ -276,7 +276,7 @@ def expectation( state: Initial state. measurement: Optional measurement protocol. If None, use exact expectation value with a statevector simulator. - endianness: Endianness of the resulting bitstrings. + endianness: Endianness of the resulting bit strings. """ raise NotImplementedError diff --git a/qadence/backends/braket/backend.py b/qadence/backends/braket/backend.py index 8b2784b2..85d0742a 100644 --- a/qadence/backends/braket/backend.py +++ b/qadence/backends/braket/backend.py @@ -147,7 +147,12 @@ def sample( from qadence.transpile import invert_endianness samples = invert_endianness(samples) - return samples + # return samples + if error is not None: + error_fn = error.get_error_fn() + return error_fn(counters=samples, n_qubits=circuit.abstract.n_qubits) + else: + return samples def expectation( self, diff --git a/qadence/backends/pulser/backend.py b/qadence/backends/pulser/backend.py index 59c38f82..0d5139fc 100644 --- a/qadence/backends/pulser/backend.py +++ b/qadence/backends/pulser/backend.py @@ -214,7 +214,12 @@ def sample( from qadence.transpile import invert_endianness samples = invert_endianness(samples) - return samples + # return samples + if error is not None: + error_fn = error.get_error_fn() + return error_fn(counters=samples, n_qubits=circuit.abstract.n_qubits) + else: + return samples def expectation( self, diff --git a/qadence/backends/pulser/pulses.py b/qadence/backends/pulser/pulses.py index 189b50c6..877346bc 100644 --- a/qadence/backends/pulser/pulses.py +++ b/qadence/backends/pulser/pulses.py @@ -102,17 +102,17 @@ def add_pulses( if block.qubit_support.is_global: pulse = analog_rot_pulse(a, w, p, d, global_channel, config) - sequence.add(pulse, GLOBAL_CHANNEL, measurement="wait-for-all") + sequence.add(pulse, GLOBAL_CHANNEL, protocol="wait-for-all") else: pulse = analog_rot_pulse(a, w, p, d, local_channel, config) sequence.target(qubit_support, LOCAL_CHANNEL) - sequence.add(pulse, LOCAL_CHANNEL, measurement="wait-for-all") + sequence.add(pulse, LOCAL_CHANNEL, protocol="wait-for-all") elif isinstance(block, AnalogEntanglement): (uuid, duration) = block.parameters.uuid_param("duration") t = evaluate(duration) if duration.is_number else sequence.declare_variable(uuid) sequence.add( - entangle_pulse(t, global_channel, config), GLOBAL_CHANNEL, measurement="wait-for-all" + entangle_pulse(t, global_channel, config), GLOBAL_CHANNEL, protocol="wait-for-all" ) elif isinstance(block, (RX, RY)): @@ -120,7 +120,7 @@ def add_pulses( angle = evaluate(p) if p.is_number else sequence.declare_variable(uuid) pulse = rx(angle) if isinstance(block, RX) else ry(angle) sequence.target(qubit_support, LOCAL_CHANNEL) - sequence.add(pulse, LOCAL_CHANNEL, measurement="wait-for-all") + sequence.add(pulse, LOCAL_CHANNEL, protocol="wait-for-all") elif isinstance(block, CompositeBlock) or isinstance(block, AnalogComposite): for block in block.blocks: From 872b5f1db1922dc40db0e46c9aa50dab21556298 Mon Sep 17 00:00:00 2001 From: Gergana Velikova Date: Fri, 27 Oct 2023 14:52:38 +0200 Subject: [PATCH 05/83] draft readout corruption, needs polishing and tests --- qadence/backends/braket/backend.py | 1 + qadence/backends/pyqtorch/backend.py | 2 +- qadence/errors/__init__.py | 2 +- qadence/errors/error.py | 36 +++++++++++ qadence/errors/protocols.py | 2 - qadence/errors/readout.py | 95 ++++++++++++++++++++++++++++ 6 files changed, 134 insertions(+), 4 deletions(-) create mode 100644 qadence/errors/error.py delete mode 100644 qadence/errors/protocols.py create mode 100644 qadence/errors/readout.py diff --git a/qadence/backends/braket/backend.py b/qadence/backends/braket/backend.py index 85d0742a..15ab7745 100644 --- a/qadence/backends/braket/backend.py +++ b/qadence/backends/braket/backend.py @@ -138,6 +138,7 @@ def sample( raise NotImplementedError # loop over all values in the batch + samples = [] for vals in to_list_of_dicts(param_values): final_circuit = self.assign_parameters(circuit, vals) diff --git a/qadence/backends/pyqtorch/backend.py b/qadence/backends/pyqtorch/backend.py index 8f3ee5a1..7dc77f4e 100644 --- a/qadence/backends/pyqtorch/backend.py +++ b/qadence/backends/pyqtorch/backend.py @@ -230,7 +230,7 @@ def _sample(_probs: Tensor, n_shots: int, endianness: Endianness, n_qubits: int) ) if error is not None: error_fn = error.get_error_fn() - return error_fn(counters=counters, n_qubits=circuit.abstract.n_qubits) + return error_fn(counters=counters, n_qubits=circuit.abstract.n_qubits, shots=n_shots) else: return counters diff --git a/qadence/errors/__init__.py b/qadence/errors/__init__.py index 8044d360..241d6e65 100644 --- a/qadence/errors/__init__.py +++ b/qadence/errors/__init__.py @@ -1,6 +1,6 @@ from __future__ import annotations -from .protocols import Errors +from .error import Errors # Modules to be automatically added to the qadence namespace __all__ = ["Errors"] diff --git a/qadence/errors/error.py b/qadence/errors/error.py new file mode 100644 index 00000000..57072c21 --- /dev/null +++ b/qadence/errors/error.py @@ -0,0 +1,36 @@ +from __future__ import annotations + +import importlib +from dataclasses import dataclass +from typing import Callable, cast + +PROTOCOL_TO_MODULE = { + "readout": "qadence.errors.readout", +} + + +# TODO: make this a StrEnum to keep consistency with the rest of the interface +@dataclass +class Errors: + READOUT = "readout" + + def __init__(self, protocol: str, options: dict) -> None: + self.protocol: str = protocol + self.options: dict | None = options + + def get_error_fn(self) -> Callable: + try: + module = importlib.import_module(PROTOCOL_TO_MODULE[self.protocol]) + except KeyError: + ImportError(f"The module corresponding to the protocol {self.protocol} is not found.") + fn = getattr(module, "readout_error") + return cast(Callable, fn) + + def _to_dict(self) -> dict: + return {"protocol": self.protocol, "options": self.options} + + @classmethod + def _from_dict(cls, d: dict) -> Erros | None: + if d: + return cls(d["protocol"], **d["options"]) + return None diff --git a/qadence/errors/protocols.py b/qadence/errors/protocols.py deleted file mode 100644 index 79a8791f..00000000 --- a/qadence/errors/protocols.py +++ /dev/null @@ -1,2 +0,0 @@ -class Errors: - pass # placeholder diff --git a/qadence/errors/readout.py b/qadence/errors/readout.py new file mode 100644 index 00000000..2ed45fb4 --- /dev/null +++ b/qadence/errors/readout.py @@ -0,0 +1,95 @@ +from __future__ import annotations + +from abc import ABC, abstractmethod +from collections import Counter +from enum import Enum +from itertools import chain + +import numpy as np +import torch +from torch.distributions import normal, poisson, uniform + +from qadence.logger import get_logger + +logger = get_logger(__name__) + + +class WhiteNoise(Enum): + """White noise distributions.""" + + UNIFORM = staticmethod(uniform.Uniform(low=0.0, high=1.0)) + "Uniform white noise." + + GAUSSIAN = staticmethod(normal.Normal(loc=0.0, scale=1.0)) + "Gaussian white noise." + + POISSON = staticmethod(poisson.Poisson(rate=0.1)) + "Poisson white noise." + + +def bitstring_to_array(bitstring: str) -> np.array: + return np.array([int(i) for i in bitstring]) + + +def array_to_bitstring(bitstring: np.array) -> str: + return "".join(([str(i) for i in bitstring])) + + +def bit_flip(qubit: int) -> int: + return 1 if qubit == 0 else 0 + + +def bs_corruption( + bitstring: str, + shots: int, + fidelity: float, + n_qubits: int, + noise_distribution: Enum = WhiteNoise.UNIFORM, +) -> list: + # the noise_matrix should be available to the user if they want to do error correction + noise_matrix = noise_distribution.sample([shots, n_qubits]) # type: ignore[attr-defined] + + # simplest approach - en event occurs if its probability is higher than expected + # by random chance + err_idx = torch.nonzero((noise_matrix < fidelity), as_tuple=True)[1] + all_bitstrings = [bitstring] * (shots - len(err_idx)) # add the majority correct bit strings + + def func_distort(idx: int) -> str: + bitstring_copy = bitstring_to_array(bitstring) + bitstring_copy[idx] = bit_flip(bitstring_copy[idx]) + return array_to_bitstring(bitstring_copy) + + all_bitstrings.extend([func_distort(idx) for idx in err_idx]) + return all_bitstrings + + +def readout_error( + counters: Counter, + n_qubits: int, + shots: int = 1000, + seed: int | None = None, + fidelity: float = 0.1, + noise_distribution: Enum = WhiteNoise.UNIFORM, +) -> list[Counter]: + # for ensuring reproducibility + if seed is not None: + torch.manual_seed(seed) + + return [ + Counter( + list( + chain( + *[ + bs_corruption( + bitstring=bitstring, + shots=shots, + fidelity=fidelity, + noise_distribution=noise_distribution, + n_qubits=n_qubits, + ) + for bitstring, shots in counters[0].items() + ] + ) + ) + ) + ] From bf1d15b2a70432ce98ac649776857fd07ceccf53 Mon Sep 17 00:00:00 2001 From: Gergana Velikova Date: Fri, 27 Oct 2023 17:48:01 +0200 Subject: [PATCH 06/83] added a simple test --- qadence/backends/braket/backend.py | 1 - qadence/backends/pulser/backend.py | 1 - qadence/errors/error.py | 2 +- qadence/errors/readout.py | 27 ++++++++++++------------ tests/qadence/test_error_models.py | 34 ++++++++++++++++++++++++++++++ 5 files changed, 48 insertions(+), 17 deletions(-) create mode 100644 tests/qadence/test_error_models.py diff --git a/qadence/backends/braket/backend.py b/qadence/backends/braket/backend.py index 15ab7745..1ee3cd7a 100644 --- a/qadence/backends/braket/backend.py +++ b/qadence/backends/braket/backend.py @@ -148,7 +148,6 @@ def sample( from qadence.transpile import invert_endianness samples = invert_endianness(samples) - # return samples if error is not None: error_fn = error.get_error_fn() return error_fn(counters=samples, n_qubits=circuit.abstract.n_qubits) diff --git a/qadence/backends/pulser/backend.py b/qadence/backends/pulser/backend.py index 0d5139fc..7772efbb 100644 --- a/qadence/backends/pulser/backend.py +++ b/qadence/backends/pulser/backend.py @@ -214,7 +214,6 @@ def sample( from qadence.transpile import invert_endianness samples = invert_endianness(samples) - # return samples if error is not None: error_fn = error.get_error_fn() return error_fn(counters=samples, n_qubits=circuit.abstract.n_qubits) diff --git a/qadence/errors/error.py b/qadence/errors/error.py index 57072c21..ca205bfe 100644 --- a/qadence/errors/error.py +++ b/qadence/errors/error.py @@ -16,7 +16,7 @@ class Errors: def __init__(self, protocol: str, options: dict) -> None: self.protocol: str = protocol - self.options: dict | None = options + self.options: dict | None = None def get_error_fn(self) -> Callable: try: diff --git a/qadence/errors/readout.py b/qadence/errors/readout.py index 2ed45fb4..5a0348ff 100644 --- a/qadence/errors/readout.py +++ b/qadence/errors/readout.py @@ -71,25 +71,24 @@ def readout_error( fidelity: float = 0.1, noise_distribution: Enum = WhiteNoise.UNIFORM, ) -> list[Counter]: - # for ensuring reproducibility + # option for reproducibility if seed is not None: torch.manual_seed(seed) return [ Counter( - list( - chain( - *[ - bs_corruption( - bitstring=bitstring, - shots=shots, - fidelity=fidelity, - noise_distribution=noise_distribution, - n_qubits=n_qubits, - ) - for bitstring, shots in counters[0].items() - ] - ) + chain( + *[ + bs_corruption( + bitstring=bitstring, + shots=shots, + fidelity=fidelity, + noise_distribution=noise_distribution, + n_qubits=n_qubits, + ) + for bitstring, shots in counter.items() + ] ) ) + for counter in counters ] diff --git a/tests/qadence/test_error_models.py b/tests/qadence/test_error_models.py new file mode 100644 index 00000000..030b88a1 --- /dev/null +++ b/tests/qadence/test_error_models.py @@ -0,0 +1,34 @@ +from __future__ import annotations + +import pytest +import torch +from sympy import acos +import qadence as qd +from qadence.operations import * +from qadence import BackendName +from qadence.errors import Errors + + +@pytest.mark.parametrize("backend", [BackendName.BRAKET, BackendName.PYQTORCH, BackendName.PULSER]) +def test_readout_error(backend) -> None: + n_qubits = 5 + fidelity = 0.1 + fp = qd.FeatureParameter("phi") + feature_map = qd.kron(RX(i, 2 * acos(fp)) for i in range(n_qubits)) + inputs = {"phi": torch.rand(1)} + # sample + samples = qd.sample(feature_map, n_shots=1000, values=inputs, backend=backend, error=None) + # introduce errors + error = Errors(protocol=Errors.READOUT, options=None).get_error_fn() + noisy_samples = error(counters=samples, n_qubits=n_qubits, fidelity=fidelity) + # compare that the results are with an error of 10% (the default fidelity) + assert all( + [ + True + for bitstring, count in noisy_samples[0].items() + if ( + samples[0]["bitstring"] <= int(count + count * fidelity) + or samples[0]["bitstring"] >= int(count - count * fidelity) + ) + ] + ) From 70103e08660255d4ed39d194bd0563a56d84e172 Mon Sep 17 00:00:00 2001 From: Gergana Velikova Date: Fri, 27 Oct 2023 18:04:15 +0200 Subject: [PATCH 07/83] slight change to the test --- tests/qadence/test_error_models.py | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/tests/qadence/test_error_models.py b/tests/qadence/test_error_models.py index 030b88a1..02e3d29d 100644 --- a/tests/qadence/test_error_models.py +++ b/tests/qadence/test_error_models.py @@ -25,10 +25,11 @@ def test_readout_error(backend) -> None: assert all( [ True - for bitstring, count in noisy_samples[0].items() if ( - samples[0]["bitstring"] <= int(count + count * fidelity) - or samples[0]["bitstring"] >= int(count - count * fidelity) + samples[0]["bitstring"] < int(count + count * fidelity) + or samples[0]["bitstring"] > int(count - count * fidelity) ) + else False + for bitstring, count in noisy_samples[0].items() ] ) From 8656c770f2b87212956d6d09b8cab83e60531662 Mon Sep 17 00:00:00 2001 From: Gergana Velikova Date: Fri, 27 Oct 2023 18:32:48 +0200 Subject: [PATCH 08/83] some non-ideal fixes for mypy --- qadence/backends/braket/backend.py | 2 +- qadence/backends/pulser/backend.py | 2 +- qadence/backends/pyqtorch/backend.py | 2 +- qadence/backends/pytorch_wrapper.py | 1 + qadence/errors/error.py | 4 ++-- qadence/errors/readout.py | 3 ++- qadence/execution.py | 4 +++- tests/qadence/test_error_models.py | 2 +- 8 files changed, 12 insertions(+), 8 deletions(-) diff --git a/qadence/backends/braket/backend.py b/qadence/backends/braket/backend.py index 1ee3cd7a..699cc7db 100644 --- a/qadence/backends/braket/backend.py +++ b/qadence/backends/braket/backend.py @@ -150,7 +150,7 @@ def sample( samples = invert_endianness(samples) if error is not None: error_fn = error.get_error_fn() - return error_fn(counters=samples, n_qubits=circuit.abstract.n_qubits) + return error_fn(counters=samples, n_qubits=circuit.abstract.n_qubits) # type: ignore else: return samples diff --git a/qadence/backends/pulser/backend.py b/qadence/backends/pulser/backend.py index 7772efbb..9fc02f78 100644 --- a/qadence/backends/pulser/backend.py +++ b/qadence/backends/pulser/backend.py @@ -216,7 +216,7 @@ def sample( samples = invert_endianness(samples) if error is not None: error_fn = error.get_error_fn() - return error_fn(counters=samples, n_qubits=circuit.abstract.n_qubits) + return error_fn(counters=samples, n_qubits=circuit.abstract.n_qubits) # type: ignore else: return samples diff --git a/qadence/backends/pyqtorch/backend.py b/qadence/backends/pyqtorch/backend.py index 7dc77f4e..8422ca2d 100644 --- a/qadence/backends/pyqtorch/backend.py +++ b/qadence/backends/pyqtorch/backend.py @@ -230,7 +230,7 @@ def _sample(_probs: Tensor, n_shots: int, endianness: Endianness, n_qubits: int) ) if error is not None: error_fn = error.get_error_fn() - return error_fn(counters=counters, n_qubits=circuit.abstract.n_qubits, shots=n_shots) + return error_fn(counters=counters, n_qubits=circuit.abstract.n_qubits, shots=n_shots) # type: ignore else: return counters diff --git a/qadence/backends/pytorch_wrapper.py b/qadence/backends/pytorch_wrapper.py index 13cf7975..2c3ee46f 100644 --- a/qadence/backends/pytorch_wrapper.py +++ b/qadence/backends/pytorch_wrapper.py @@ -15,6 +15,7 @@ from qadence.blocks import AbstractBlock, PrimitiveBlock from qadence.blocks.utils import uuid_to_block, uuid_to_eigen from qadence.circuit import QuantumCircuit +from qadence.errors import Errors from qadence.extensions import get_gpsr_fns from qadence.measurements import Measurements from qadence.ml_tools import promote_to_tensor diff --git a/qadence/errors/error.py b/qadence/errors/error.py index ca205bfe..e0656895 100644 --- a/qadence/errors/error.py +++ b/qadence/errors/error.py @@ -14,7 +14,7 @@ class Errors: READOUT = "readout" - def __init__(self, protocol: str, options: dict) -> None: + def __init__(self, protocol: str, options: dict | None) -> None: self.protocol: str = protocol self.options: dict | None = None @@ -30,7 +30,7 @@ def _to_dict(self) -> dict: return {"protocol": self.protocol, "options": self.options} @classmethod - def _from_dict(cls, d: dict) -> Erros | None: + def _from_dict(cls, d: dict) -> Errors | None: if d: return cls(d["protocol"], **d["options"]) return None diff --git a/qadence/errors/readout.py b/qadence/errors/readout.py index 5a0348ff..0f184b4a 100644 --- a/qadence/errors/readout.py +++ b/qadence/errors/readout.py @@ -8,6 +8,7 @@ import numpy as np import torch from torch.distributions import normal, poisson, uniform +from typing import Any from qadence.logger import get_logger @@ -70,7 +71,7 @@ def readout_error( seed: int | None = None, fidelity: float = 0.1, noise_distribution: Enum = WhiteNoise.UNIFORM, -) -> list[Counter]: +) -> list[Counter[Any]]: # option for reproducibility if seed is not None: torch.manual_seed(seed) diff --git a/qadence/execution.py b/qadence/execution.py index 52d826e6..18272b44 100644 --- a/qadence/execution.py +++ b/qadence/execution.py @@ -10,10 +10,10 @@ from qadence.backend import BackendConfiguration, BackendName from qadence.blocks import AbstractBlock from qadence.circuit import QuantumCircuit +from qadence.errors import Errors from qadence.register import Register from qadence.types import DiffMode from qadence.utils import Endianness -from qadence.errors import Errors # Modules to be automatically added to the qadence namespace __all__ = ["run", "sample", "expectation"] @@ -92,6 +92,7 @@ def sample( n_shots: int = 100, backend: BackendName = BackendName.PYQTORCH, endianness: Endianness = Endianness.BIG, + error: Error | None = None, configuration: Union[BackendConfiguration, dict, None] = None, ) -> list[Counter]: """Convenience wrapper for the `QuantumModel.sample` method. This is a @@ -106,6 +107,7 @@ def sample( n_shots: Number of shots per element in the batch. backend: Name of the backend to run on. endianness: The target device endianness. + error: The error model to use if any. configuration: The backend configuration. Returns: diff --git a/tests/qadence/test_error_models.py b/tests/qadence/test_error_models.py index 02e3d29d..09ac1973 100644 --- a/tests/qadence/test_error_models.py +++ b/tests/qadence/test_error_models.py @@ -10,7 +10,7 @@ @pytest.mark.parametrize("backend", [BackendName.BRAKET, BackendName.PYQTORCH, BackendName.PULSER]) -def test_readout_error(backend) -> None: +def test_readout_error(backend: BackendName) -> None: n_qubits = 5 fidelity = 0.1 fp = qd.FeatureParameter("phi") From 5c81f01e8bdf6a9aececd744ed4a3fedbe58a352 Mon Sep 17 00:00:00 2001 From: Gergana Velikova Date: Fri, 27 Oct 2023 18:58:03 +0200 Subject: [PATCH 09/83] linting --- qadence/backends/pyqtorch/backend.py | 4 +++- qadence/errors/readout.py | 3 +-- qadence/execution.py | 2 +- tests/qadence/test_error_models.py | 3 ++- 4 files changed, 7 insertions(+), 5 deletions(-) diff --git a/qadence/backends/pyqtorch/backend.py b/qadence/backends/pyqtorch/backend.py index 8422ca2d..8770d149 100644 --- a/qadence/backends/pyqtorch/backend.py +++ b/qadence/backends/pyqtorch/backend.py @@ -230,7 +230,9 @@ def _sample(_probs: Tensor, n_shots: int, endianness: Endianness, n_qubits: int) ) if error is not None: error_fn = error.get_error_fn() - return error_fn(counters=counters, n_qubits=circuit.abstract.n_qubits, shots=n_shots) # type: ignore + return error_fn( # type: ignore + counters=counters, n_qubits=circuit.abstract.n_qubits, shots=n_shots + ) else: return counters diff --git a/qadence/errors/readout.py b/qadence/errors/readout.py index 0f184b4a..4c0d8c33 100644 --- a/qadence/errors/readout.py +++ b/qadence/errors/readout.py @@ -1,14 +1,13 @@ from __future__ import annotations -from abc import ABC, abstractmethod from collections import Counter from enum import Enum from itertools import chain +from typing import Any import numpy as np import torch from torch.distributions import normal, poisson, uniform -from typing import Any from qadence.logger import get_logger diff --git a/qadence/execution.py b/qadence/execution.py index 18272b44..7fcd9c3d 100644 --- a/qadence/execution.py +++ b/qadence/execution.py @@ -92,7 +92,7 @@ def sample( n_shots: int = 100, backend: BackendName = BackendName.PYQTORCH, endianness: Endianness = Endianness.BIG, - error: Error | None = None, + error: Errors | None = None, configuration: Union[BackendConfiguration, dict, None] = None, ) -> list[Counter]: """Convenience wrapper for the `QuantumModel.sample` method. This is a diff --git a/tests/qadence/test_error_models.py b/tests/qadence/test_error_models.py index 09ac1973..1c170d60 100644 --- a/tests/qadence/test_error_models.py +++ b/tests/qadence/test_error_models.py @@ -3,10 +3,11 @@ import pytest import torch from sympy import acos + import qadence as qd -from qadence.operations import * from qadence import BackendName from qadence.errors import Errors +from qadence.operations import * @pytest.mark.parametrize("backend", [BackendName.BRAKET, BackendName.PYQTORCH, BackendName.PULSER]) From d84bfe486538667fb3043e3999fb762ff771ee3b Mon Sep 17 00:00:00 2001 From: Gergana Velikova Date: Fri, 27 Oct 2023 19:04:18 +0200 Subject: [PATCH 10/83] linting --- tests/qadence/test_error_models.py | 1 + 1 file changed, 1 insertion(+) diff --git a/tests/qadence/test_error_models.py b/tests/qadence/test_error_models.py index 1c170d60..9a86fdd7 100644 --- a/tests/qadence/test_error_models.py +++ b/tests/qadence/test_error_models.py @@ -8,6 +8,7 @@ from qadence import BackendName from qadence.errors import Errors from qadence.operations import * +from qadence.operations import RX @pytest.mark.parametrize("backend", [BackendName.BRAKET, BackendName.PYQTORCH, BackendName.PULSER]) From a0c54704b884616020e3b816b5886d0b8475ba03 Mon Sep 17 00:00:00 2001 From: Gergana Velikova Date: Fri, 27 Oct 2023 19:19:46 +0200 Subject: [PATCH 11/83] error definition --- qadence/execution.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/qadence/execution.py b/qadence/execution.py index 7fcd9c3d..9b119bbd 100644 --- a/qadence/execution.py +++ b/qadence/execution.py @@ -92,7 +92,7 @@ def sample( n_shots: int = 100, backend: BackendName = BackendName.PYQTORCH, endianness: Endianness = Endianness.BIG, - error: Errors | None = None, + error: Union[Errors, None] = None, configuration: Union[BackendConfiguration, dict, None] = None, ) -> list[Counter]: """Convenience wrapper for the `QuantumModel.sample` method. This is a From b2842ddf9ed7da3ad2e44fd72c3e55623e2fb1ce Mon Sep 17 00:00:00 2001 From: Gergana Velikova Date: Mon, 30 Oct 2023 19:44:29 +0100 Subject: [PATCH 12/83] cleaning up --- qadence/backends/pyqtorch/backend.py | 4 +- qadence/errors/__init__.py | 2 +- qadence/errors/{error.py => protocols.py} | 0 qadence/errors/readout.py | 77 +++++++++------ qadence/measurements/__init__.py | 2 +- .../{measurement.py => protocols.py} | 0 tests/qadence/test_error_models.py | 97 +++++++++++++++++-- 7 files changed, 140 insertions(+), 42 deletions(-) rename qadence/errors/{error.py => protocols.py} (100%) rename qadence/measurements/{measurement.py => protocols.py} (100%) diff --git a/qadence/backends/pyqtorch/backend.py b/qadence/backends/pyqtorch/backend.py index 8770d149..d8130bef 100644 --- a/qadence/backends/pyqtorch/backend.py +++ b/qadence/backends/pyqtorch/backend.py @@ -230,9 +230,7 @@ def _sample(_probs: Tensor, n_shots: int, endianness: Endianness, n_qubits: int) ) if error is not None: error_fn = error.get_error_fn() - return error_fn( # type: ignore - counters=counters, n_qubits=circuit.abstract.n_qubits, shots=n_shots - ) + return error_fn(counters=counters, n_qubits=circuit.abstract.n_qubits) # type: ignore else: return counters diff --git a/qadence/errors/__init__.py b/qadence/errors/__init__.py index 241d6e65..8044d360 100644 --- a/qadence/errors/__init__.py +++ b/qadence/errors/__init__.py @@ -1,6 +1,6 @@ from __future__ import annotations -from .error import Errors +from .protocols import Errors # Modules to be automatically added to the qadence namespace __all__ = ["Errors"] diff --git a/qadence/errors/error.py b/qadence/errors/protocols.py similarity index 100% rename from qadence/errors/error.py rename to qadence/errors/protocols.py diff --git a/qadence/errors/readout.py b/qadence/errors/readout.py index 4c0d8c33..e8c30189 100644 --- a/qadence/errors/readout.py +++ b/qadence/errors/readout.py @@ -28,65 +28,84 @@ class WhiteNoise(Enum): def bitstring_to_array(bitstring: str) -> np.array: + """A helper function to convert bit strings to numpy arrays.""" return np.array([int(i) for i in bitstring]) def array_to_bitstring(bitstring: np.array) -> str: + """A helper function to convert numpy arrays to bit strings.""" return "".join(([str(i) for i in bitstring])) def bit_flip(qubit: int) -> int: + """A helper function that reverses the states 0 and 1 in the bit string.""" return 1 if qubit == 0 else 0 -def bs_corruption( - bitstring: str, - shots: int, - fidelity: float, - n_qubits: int, - noise_distribution: Enum = WhiteNoise.UNIFORM, -) -> list: - # the noise_matrix should be available to the user if they want to do error correction - noise_matrix = noise_distribution.sample([shots, n_qubits]) # type: ignore[attr-defined] - - # simplest approach - en event occurs if its probability is higher than expected - # by random chance - err_idx = torch.nonzero((noise_matrix < fidelity), as_tuple=True)[1] - all_bitstrings = [bitstring] * (shots - len(err_idx)) # add the majority correct bit strings - - def func_distort(idx: int) -> str: - bitstring_copy = bitstring_to_array(bitstring) - bitstring_copy[idx] = bit_flip(bitstring_copy[idx]) - return array_to_bitstring(bitstring_copy) - - all_bitstrings.extend([func_distort(idx) for idx in err_idx]) - return all_bitstrings - - def readout_error( counters: Counter, n_qubits: int, - shots: int = 1000, seed: int | None = None, - fidelity: float = 0.1, + error_probability: float = 0.1, noise_distribution: Enum = WhiteNoise.UNIFORM, ) -> list[Counter[Any]]: + """ + Implements a simple uniform readout error model for position-independent bit string + corruption. + + Args: + counters: Samples of bit string as Counters. + n_qubits: Number of shots to sample. + seed: Random seed value if any. + error_probability: Uniform error probability of wrong readout at any position + in the bit strings. + noise_distribution: Noise distribution. + + Returns: + Samples of corrupted bit strings as list[Counter]. + """ + # option for reproducibility if seed is not None: torch.manual_seed(seed) + def bs_corruption( + bitstring: str, + n_shots: int, + error_probability: float, + n_qubits: int, + noise_distribution: Enum = WhiteNoise.UNIFORM, + ) -> list: + # the noise_matrix should be available to the user if they want to do error correction + noise_matrix = noise_distribution.sample([n_shots, n_qubits]) # type: ignore[attr-defined] + + # simplest approach - en event occurs if its probability is higher than expected + # by random chance + err_idx = torch.nonzero((noise_matrix < error_probability), as_tuple=True)[1] + all_bitstrings = [bitstring] * ( + n_shots - len(err_idx) + ) # add the majority correct bit strings + + def func_distort(idx: int) -> str: + bitstring_copy = bitstring_to_array(bitstring) + bitstring_copy[idx] = bit_flip(bitstring_copy[idx]) + return array_to_bitstring(bitstring_copy) + + all_bitstrings.extend([func_distort(idx) for idx in err_idx]) + return all_bitstrings + return [ Counter( chain( *[ bs_corruption( bitstring=bitstring, - shots=shots, - fidelity=fidelity, + n_shots=n_shots, + error_probability=error_probability, noise_distribution=noise_distribution, n_qubits=n_qubits, ) - for bitstring, shots in counter.items() + for bitstring, n_shots in counter.items() ] ) ) diff --git a/qadence/measurements/__init__.py b/qadence/measurements/__init__.py index 348aedcb..dde75742 100644 --- a/qadence/measurements/__init__.py +++ b/qadence/measurements/__init__.py @@ -1,6 +1,6 @@ from __future__ import annotations -from .measurement import Measurements +from .protocols import Measurements # Modules to be automatically added to the qadence namespace __all__ = ["Measurements"] diff --git a/qadence/measurements/measurement.py b/qadence/measurements/protocols.py similarity index 100% rename from qadence/measurements/measurement.py rename to qadence/measurements/protocols.py diff --git a/tests/qadence/test_error_models.py b/tests/qadence/test_error_models.py index 9a86fdd7..f83b2abb 100644 --- a/tests/qadence/test_error_models.py +++ b/tests/qadence/test_error_models.py @@ -6,15 +6,96 @@ import qadence as qd from qadence import BackendName +from qadence.blocks import ( + AbstractBlock, + add, + kron, +) +from qadence.circuit import QuantumCircuit +from qadence.constructors import ( + total_magnetization, +) from qadence.errors import Errors -from qadence.operations import * -from qadence.operations import RX +from qadence.models import QuantumModel +from qadence.operations import ( + CNOT, + RX, + RZ, + HamEvo, + X, + Y, + Z, +) + + +@pytest.mark.parametrize( + "error_probability, block, backend", + [ + (0.1, kron(X(0), X(1)), BackendName.BRAKET), + (0.1, kron(Z(0), Z(1), Z(2)) + kron(X(0), Y(1), Z(2)), BackendName.BRAKET), + (0.15, add(Z(0), Z(1), Z(2)), BackendName.BRAKET), + (0.01, kron(X(0), X(1)) + kron(Z(0), Z(1)) + kron(X(2), X(3)), BackendName.BRAKET), + (0.1, add(Z(0), Z(1), kron(X(2), X(3))) + add(X(2), X(3)), BackendName.BRAKET), + (0.1, add(kron(Z(0), Z(1)), kron(X(2), X(3))), BackendName.BRAKET), + (0.1, kron(Z(0), Z(1)) + CNOT(0, 1), BackendName.BRAKET), + ( + 0.05, + kron(RZ(0, parameter=0.01), RZ(1, parameter=0.01)) + + kron(RX(0, parameter=0.01), RX(1, parameter=0.01)), + BackendName.PULSER, + ), + (0.001, HamEvo(generator=kron(Z(0), Z(1)), parameter=0.05), BackendName.BRAKET), + (0.12, HamEvo(generator=kron(Z(0), Z(1), Z(2)), parameter=0.001), BackendName.BRAKET), + ( + 0.1, + HamEvo(generator=kron(Z(0), Z(1)) + kron(Z(0), Z(1), Z(2)), parameter=0.005), + BackendName.BRAKET, + ), + (0.1, kron(X(0), X(1)), BackendName.PYQTORCH), + (0.01, kron(Z(0), Z(1), Z(2)) + kron(X(0), Y(1), Z(2)), BackendName.PYQTORCH), + (0.01, add(Z(0), Z(1), Z(2)), BackendName.PYQTORCH), + ( + 0.1, + HamEvo( + generator=kron(X(0), X(1)) + kron(Z(0), Z(1)) + kron(X(2), X(3)), parameter=0.005 + ), + BackendName.PYQTORCH, + ), + (0.1, add(Z(0), Z(1), kron(X(2), X(3))) + add(X(2), X(3)), BackendName.PYQTORCH), + (0.05, add(kron(Z(0), Z(1)), kron(X(2), X(3))), BackendName.PYQTORCH), + (0.2, total_magnetization(4), BackendName.PYQTORCH), + (0.1, kron(Z(0), Z(1)) + CNOT(0, 1), BackendName.PYQTORCH), + ], +) +def test_readout_error_quantum_model( + error_probability: float, block: AbstractBlock, backend: BackendName +) -> None: + diff_mode = "ad" if backend == BackendName.PYQTORCH else "gpsr" + err_free = QuantumModel( + QuantumCircuit(block.n_qubits, block), backend=backend, diff_mode=diff_mode + ).sample() + noisy = QuantumModel( + QuantumCircuit(block.n_qubits, block), backend=backend, diff_mode=diff_mode + ).sample(error=Errors(protocol=Errors.READOUT, options=None)) + + assert len(noisy[0]) <= 2 ** block.n_qubits and len(noisy[0]) > len(err_free[0]) + assert all( + [ + True + if ( + err_free[0]["bitstring"] < int(count + count * error_probability) + or err_free[0]["bitstring"] > int(count - count * error_probability) + ) + else False + for bitstring, count in noisy[0].items() + ] + ) @pytest.mark.parametrize("backend", [BackendName.BRAKET, BackendName.PYQTORCH, BackendName.PULSER]) -def test_readout_error(backend: BackendName) -> None: +def test_readout_error_backends(backend: BackendName) -> None: n_qubits = 5 - fidelity = 0.1 + error_probability = 0.1 fp = qd.FeatureParameter("phi") feature_map = qd.kron(RX(i, 2 * acos(fp)) for i in range(n_qubits)) inputs = {"phi": torch.rand(1)} @@ -22,14 +103,14 @@ def test_readout_error(backend: BackendName) -> None: samples = qd.sample(feature_map, n_shots=1000, values=inputs, backend=backend, error=None) # introduce errors error = Errors(protocol=Errors.READOUT, options=None).get_error_fn() - noisy_samples = error(counters=samples, n_qubits=n_qubits, fidelity=fidelity) - # compare that the results are with an error of 10% (the default fidelity) + noisy_samples = error(counters=samples, n_qubits=n_qubits, error_probability=error_probability) + # compare that the results are with an error of 10% (the default error_probability) assert all( [ True if ( - samples[0]["bitstring"] < int(count + count * fidelity) - or samples[0]["bitstring"] > int(count - count * fidelity) + samples[0]["bitstring"] < int(count + count * error_probability) + or samples[0]["bitstring"] > int(count - count * error_probability) ) else False for bitstring, count in noisy_samples[0].items() From 9cee4409fd184d524da533b1e8195a21982f848e Mon Sep 17 00:00:00 2001 From: Gergana Velikova Date: Mon, 30 Oct 2023 20:35:04 +0100 Subject: [PATCH 13/83] tidying --- qadence/execution.py | 2 +- tests/qadence/test_error_models.py | 2 ++ 2 files changed, 3 insertions(+), 1 deletion(-) diff --git a/qadence/execution.py b/qadence/execution.py index 9b119bbd..ee906125 100644 --- a/qadence/execution.py +++ b/qadence/execution.py @@ -123,7 +123,7 @@ def _( state: Union[Tensor, None] = None, n_shots: int = 100, backend: BackendName = BackendName.PYQTORCH, - error: Errors | None = None, + error: Union[Errors, None] = None, endianness: Endianness = Endianness.BIG, configuration: Union[BackendConfiguration, dict, None] = None, ) -> list[Counter]: diff --git a/tests/qadence/test_error_models.py b/tests/qadence/test_error_models.py index f83b2abb..e7c753e0 100644 --- a/tests/qadence/test_error_models.py +++ b/tests/qadence/test_error_models.py @@ -71,9 +71,11 @@ def test_readout_error_quantum_model( error_probability: float, block: AbstractBlock, backend: BackendName ) -> None: diff_mode = "ad" if backend == BackendName.PYQTORCH else "gpsr" + err_free = QuantumModel( QuantumCircuit(block.n_qubits, block), backend=backend, diff_mode=diff_mode ).sample() + noisy = QuantumModel( QuantumCircuit(block.n_qubits, block), backend=backend, diff_mode=diff_mode ).sample(error=Errors(protocol=Errors.READOUT, options=None)) From 54d644ad99185ff59bccd35648aca3916afaccaa Mon Sep 17 00:00:00 2001 From: Roland Guichard Date: Wed, 1 Nov 2023 10:28:54 +0000 Subject: [PATCH 14/83] Add error option for expectation. --- qadence/backend.py | 1 + qadence/backends/pytorch_wrapper.py | 7 +++++++ qadence/models/quantum_model.py | 5 ++++- 3 files changed, 12 insertions(+), 1 deletion(-) diff --git a/qadence/backend.py b/qadence/backend.py index b556382f..3e59d214 100644 --- a/qadence/backend.py +++ b/qadence/backend.py @@ -265,6 +265,7 @@ def expectation( param_values: dict[str, Tensor] = {}, state: Tensor | None = None, measurement: Measurements | None = None, + error: Errors | None = None, endianness: Endianness = Endianness.BIG, ) -> Tensor: """Compute the expectation value of the `circuit` with the given `observable`. diff --git a/qadence/backends/pytorch_wrapper.py b/qadence/backends/pytorch_wrapper.py index 2c3ee46f..be0eb973 100644 --- a/qadence/backends/pytorch_wrapper.py +++ b/qadence/backends/pytorch_wrapper.py @@ -86,6 +86,7 @@ class DifferentiableExpectation: param_values: dict[str, Tensor] state: Tensor | None = None measurement: Measurements | None = None + error: Errors | None = None endianness: Endianness = Endianness.BIG def ad(self) -> Tensor: @@ -100,6 +101,7 @@ def ad(self) -> Tensor: param_values=self.param_values, options=self.measurement.options, state=self.state, + error=self.error, endianness=self.endianness, ) else: @@ -108,6 +110,7 @@ def ad(self) -> Tensor: observable=self.observable, param_values=self.param_values, state=self.state, + error=self.error, endianness=self.endianness, ) return promote_to_tensor( @@ -132,6 +135,7 @@ def psr(self, psr_fn: Callable, **psr_args: int | float | None) -> Tensor: observables=[obs.original for obs in self.observable], options=self.measurement.options, state=self.state, + error=self.error, endianness=self.endianness, ) else: @@ -140,6 +144,7 @@ def psr(self, psr_fn: Callable, **psr_args: int | float | None) -> Tensor: circuit=self.circuit, observable=self.observable, state=self.state, + error=self.error, endianness=self.endianness, ) # PSR only applies to parametric circuits. @@ -231,6 +236,7 @@ def expectation( param_values: dict[str, Tensor] = {}, state: Tensor | None = None, measurement: Measurements | None = None, + error: Errors | None = None, endianness: Endianness = Endianness.BIG, ) -> Tensor: """Compute the expectation value of a given observable. @@ -254,6 +260,7 @@ def expectation( param_values=param_values, state=state, measurement=measurement, + error=error, endianness=endianness, ) diff --git a/qadence/models/quantum_model.py b/qadence/models/quantum_model.py index b2609983..5de7fca2 100644 --- a/qadence/models/quantum_model.py +++ b/qadence/models/quantum_model.py @@ -181,6 +181,7 @@ def expectation( observable: list[ConvertedObservable] | ConvertedObservable | None = None, state: Optional[Tensor] = None, measurement: Measurements | None = None, + error: Errors | None = None, endianness: Endianness = Endianness.BIG, ) -> Tensor: """Compute expectation using the given backend. @@ -200,13 +201,15 @@ def expectation( params = self.embedding_fn(self._params, values) if measurement is None: measurement = self._measurement - + if error is None: + error = self._error return self.backend.expectation( circuit=self._circuit, observable=observable, param_values=params, state=state, measurement=measurement, + error=error, endianness=endianness, ) From db9ce1ef75762add5bfb90e0b55d835bb72fa465 Mon Sep 17 00:00:00 2001 From: Roland Guichard Date: Wed, 1 Nov 2023 10:29:36 +0000 Subject: [PATCH 15/83] Add error for expectation and warning in no measurement case. --- qadence/backends/pyqtorch/backend.py | 10 ++++++++++ 1 file changed, 10 insertions(+) diff --git a/qadence/backends/pyqtorch/backend.py b/qadence/backends/pyqtorch/backend.py index d8130bef..a60f0719 100644 --- a/qadence/backends/pyqtorch/backend.py +++ b/qadence/backends/pyqtorch/backend.py @@ -6,6 +6,7 @@ from typing import Any import pyqtorch.modules as pyq +from qadence import logger import torch from torch import Tensor @@ -15,6 +16,7 @@ from qadence.blocks import AbstractBlock from qadence.circuit import QuantumCircuit from qadence.errors import Errors +from qadence.logger import get_logger from qadence.measurements import Measurements from qadence.overlap import overlap_exact from qadence.states import zero_state @@ -32,6 +34,9 @@ from .convert_ops import convert_block, convert_observable +logger = get_logger(__name__) + + @dataclass(frozen=True, eq=True) class Backend(BackendInterface): """PyQTorch backend.""" @@ -178,8 +183,13 @@ def expectation( param_values: dict[str, Tensor] = {}, state: Tensor | None = None, measurement: Measurements | None = None, + error: Errors | None = None, endianness: Endianness = Endianness.BIG, ) -> Tensor: + if error is not None: + logger.warning( + f"Errors of type {error} are not implemented for expectation yet. " + "This is ignored for now.") fn = self._looped_expectation if self.config.loop_expectation else self._batched_expectation return fn( circuit=circuit, From 9f4219b3736b349ef2527f9d77edd86439c7c794 Mon Sep 17 00:00:00 2001 From: Roland Guichard Date: Wed, 1 Nov 2023 10:30:18 +0000 Subject: [PATCH 16/83] Make options optional. --- qadence/errors/protocols.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/qadence/errors/protocols.py b/qadence/errors/protocols.py index e0656895..0ec391a2 100644 --- a/qadence/errors/protocols.py +++ b/qadence/errors/protocols.py @@ -14,7 +14,7 @@ class Errors: READOUT = "readout" - def __init__(self, protocol: str, options: dict | None) -> None: + def __init__(self, protocol: str, options: dict | None = None) -> None: self.protocol: str = protocol self.options: dict | None = None From fd62673a199af3d0187f90b638fda44f5367be0b Mon Sep 17 00:00:00 2001 From: Roland Guichard Date: Wed, 1 Nov 2023 10:30:51 +0000 Subject: [PATCH 17/83] Pass readout error for tomography. --- qadence/measurements/tomography.py | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/qadence/measurements/tomography.py b/qadence/measurements/tomography.py index 562ff6a5..73542b27 100644 --- a/qadence/measurements/tomography.py +++ b/qadence/measurements/tomography.py @@ -84,8 +84,8 @@ def iterate_pauli_decomposition( n_shots: int, state: Tensor | None = None, backend_name: BackendName = BackendName.PYQTORCH, - endianness: Endianness = Endianness.BIG, error: Errors | None = None, + endianness: Endianness = Endianness.BIG, ) -> Tensor: """Estimate total expectation value by averaging all Pauli terms.""" @@ -131,6 +131,7 @@ def compute_expectation( options: dict, state: Tensor | None = None, backend_name: BackendName = BackendName.PYQTORCH, + error: Error | None = None, endianness: Endianness = Endianness.BIG, ) -> Tensor: """Basic tomography protocol with rotations @@ -158,6 +159,7 @@ def compute_expectation( n_shots=n_shots, state=state, backend_name=backend_name, + error=error, endianness=endianness, ) ) From 8935b3d6c6b29b4c81060583c9c820f5f4a4db2f Mon Sep 17 00:00:00 2001 From: Roland Guichard Date: Wed, 1 Nov 2023 10:35:03 +0000 Subject: [PATCH 18/83] Correct logger import. --- qadence/backends/pyqtorch/backend.py | 9 +++------ 1 file changed, 3 insertions(+), 6 deletions(-) diff --git a/qadence/backends/pyqtorch/backend.py b/qadence/backends/pyqtorch/backend.py index a60f0719..61e3fea9 100644 --- a/qadence/backends/pyqtorch/backend.py +++ b/qadence/backends/pyqtorch/backend.py @@ -6,17 +6,16 @@ from typing import Any import pyqtorch.modules as pyq -from qadence import logger import torch from torch import Tensor +from qadence import logger from qadence.backend import Backend as BackendInterface from qadence.backend import BackendName, ConvertedCircuit, ConvertedObservable from qadence.backends.utils import to_list_of_dicts from qadence.blocks import AbstractBlock from qadence.circuit import QuantumCircuit from qadence.errors import Errors -from qadence.logger import get_logger from qadence.measurements import Measurements from qadence.overlap import overlap_exact from qadence.states import zero_state @@ -34,9 +33,6 @@ from .convert_ops import convert_block, convert_observable -logger = get_logger(__name__) - - @dataclass(frozen=True, eq=True) class Backend(BackendInterface): """PyQTorch backend.""" @@ -189,7 +185,8 @@ def expectation( if error is not None: logger.warning( f"Errors of type {error} are not implemented for expectation yet. " - "This is ignored for now.") + "This is ignored for now." + ) fn = self._looped_expectation if self.config.loop_expectation else self._batched_expectation return fn( circuit=circuit, From b2d6647707370861ea686eb986f418dd77a3c893 Mon Sep 17 00:00:00 2001 From: Roland Guichard Date: Wed, 1 Nov 2023 10:35:31 +0000 Subject: [PATCH 19/83] Fix name. --- qadence/measurements/tomography.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/qadence/measurements/tomography.py b/qadence/measurements/tomography.py index 73542b27..a8d8e48b 100644 --- a/qadence/measurements/tomography.py +++ b/qadence/measurements/tomography.py @@ -131,7 +131,7 @@ def compute_expectation( options: dict, state: Tensor | None = None, backend_name: BackendName = BackendName.PYQTORCH, - error: Error | None = None, + error: Errors | None = None, endianness: Endianness = Endianness.BIG, ) -> Tensor: """Basic tomography protocol with rotations From d63e9b1e40e3d48a69239c97a4a96205c23cfd89 Mon Sep 17 00:00:00 2001 From: Roland Guichard Date: Wed, 1 Nov 2023 10:35:45 +0000 Subject: [PATCH 20/83] First attempt to test readout errors with tomography. --- tests/qadence/test_error_models.py | 36 ++++++++++++++++++++++++++++++ 1 file changed, 36 insertions(+) diff --git a/tests/qadence/test_error_models.py b/tests/qadence/test_error_models.py index e7c753e0..1c26221f 100644 --- a/tests/qadence/test_error_models.py +++ b/tests/qadence/test_error_models.py @@ -15,12 +15,15 @@ from qadence.constructors import ( total_magnetization, ) +from qadence.constructors.hamiltonians import hamiltonian_factory from qadence.errors import Errors +from qadence.measurements.protocols import Measurements from qadence.models import QuantumModel from qadence.operations import ( CNOT, RX, RZ, + H, HamEvo, X, Y, @@ -118,3 +121,36 @@ def test_readout_error_backends(backend: BackendName) -> None: for bitstring, count in noisy_samples[0].items() ] ) + + +@pytest.mark.parametrize( + "block, observable, backend", + [ + (kron(H(0), Z(1)), hamiltonian_factory(2, detuning=Z), BackendName.PYQTORCH), + # (kron(Z(0), Z(1), Z(2)) + kron(X(0), Y(1), Z(2)), BackendName.PYQTORCH), + # (add(Z(0), Z(1), Z(2)), BackendName.PYQTORCH), + # ( + # HamEvo( + # generator=kron(X(0), X(1)) + kron(Z(0), Z(1)) + kron(X(2), X(3)), parameter=0.005 + # ), + # BackendName.PYQTORCH, + # ), + # (add(Z(0), Z(1), kron(X(2), X(3))) + add(X(2), X(3)), BackendName.PYQTORCH), + # (dd(kron(Z(0), Z(1)), kron(X(2), X(3))), BackendName.PYQTORCH), + # (total_magnetization(4), BackendName.PYQTORCH), + # (kron(Z(0), Z(1)) + CNOT(0, 1), BackendName.PYQTORCH), + ], +) +def test_readout_error_with_measurements( + block: AbstractBlock, observable: AbstractBlock, backend: BackendName +): + circuit = QuantumCircuit(block.n_qubits, block) + model = QuantumModel(circuit=circuit, observable=observable, backend=backend) + + error = Errors(protocol=Errors.READOUT) + measurement = Measurements(protocol=Measurements.TOMOGRAPHY, options={"n_shots": 1000}) + + measured = model.expectation(measurement=measurement) + noisy = model.expectation(measurement=measurement, error=error) + exact = model.expectation() + print(f"noisy {noisy} exact {exact}") From f6360b658c81db4a607592f79acee6348e718e27 Mon Sep 17 00:00:00 2001 From: Roland Guichard Date: Thu, 2 Nov 2023 13:45:39 +0000 Subject: [PATCH 21/83] Harmonise subclasses to base class. --- qadence/backends/braket/backend.py | 1 + qadence/backends/pulser/backend.py | 1 + 2 files changed, 2 insertions(+) diff --git a/qadence/backends/braket/backend.py b/qadence/backends/braket/backend.py index 699cc7db..486aa21e 100644 --- a/qadence/backends/braket/backend.py +++ b/qadence/backends/braket/backend.py @@ -161,6 +161,7 @@ def expectation( param_values: dict[str, Tensor] = {}, state: Tensor | None = None, measurement: Measurements | None = None, + error: Errors | None = None, endianness: Endianness = Endianness.BIG, ) -> Tensor: # Do not flip endianness here because then we would have to reverse the observable diff --git a/qadence/backends/pulser/backend.py b/qadence/backends/pulser/backend.py index 16928fdd..342401a6 100644 --- a/qadence/backends/pulser/backend.py +++ b/qadence/backends/pulser/backend.py @@ -235,6 +235,7 @@ def expectation( param_values: dict[str, Tensor] = {}, state: Tensor | None = None, measurement: Measurements | None = None, + errors: Errors | None = None, endianness: Endianness = Endianness.BIG, ) -> Tensor: state = self.run(circuit, param_values=param_values, state=state, endianness=endianness) From b4f542f34a33a1c53182031f92849cf956ac5ee7 Mon Sep 17 00:00:00 2001 From: Roland Guichard Date: Thu, 2 Nov 2023 13:46:04 +0000 Subject: [PATCH 22/83] Correct logging. --- qadence/backends/pyqtorch/backend.py | 9 +++++++-- 1 file changed, 7 insertions(+), 2 deletions(-) diff --git a/qadence/backends/pyqtorch/backend.py b/qadence/backends/pyqtorch/backend.py index 61e3fea9..77b63a64 100644 --- a/qadence/backends/pyqtorch/backend.py +++ b/qadence/backends/pyqtorch/backend.py @@ -9,13 +9,13 @@ import torch from torch import Tensor -from qadence import logger from qadence.backend import Backend as BackendInterface from qadence.backend import BackendName, ConvertedCircuit, ConvertedObservable from qadence.backends.utils import to_list_of_dicts from qadence.blocks import AbstractBlock from qadence.circuit import QuantumCircuit from qadence.errors import Errors +from qadence.logger import get_logger from qadence.measurements import Measurements from qadence.overlap import overlap_exact from qadence.states import zero_state @@ -32,6 +32,8 @@ from .config import Configuration from .convert_ops import convert_block, convert_observable +logger = get_logger(__name__) + @dataclass(frozen=True, eq=True) class Backend(BackendInterface): @@ -237,7 +239,10 @@ def _sample(_probs: Tensor, n_shots: int, endianness: Endianness, n_qubits: int) ) if error is not None: error_fn = error.get_error_fn() - return error_fn(counters=counters, n_qubits=circuit.abstract.n_qubits) # type: ignore + corrupted_counters: list = error_fn( + counters=counters, n_qubits=circuit.abstract.n_qubits, options=error.options + ) + return corrupted_counters else: return counters From 770e13d2513d7b042170ffd64a521b1d7cf9c6fb Mon Sep 17 00:00:00 2001 From: Roland Guichard Date: Thu, 2 Nov 2023 13:46:51 +0000 Subject: [PATCH 23/83] Default options and correct function name. --- qadence/errors/protocols.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/qadence/errors/protocols.py b/qadence/errors/protocols.py index 0ec391a2..a9d4a6b3 100644 --- a/qadence/errors/protocols.py +++ b/qadence/errors/protocols.py @@ -14,16 +14,16 @@ class Errors: READOUT = "readout" - def __init__(self, protocol: str, options: dict | None = None) -> None: + def __init__(self, protocol: str, options: dict = dict()) -> None: self.protocol: str = protocol - self.options: dict | None = None + self.options: dict = options def get_error_fn(self) -> Callable: try: module = importlib.import_module(PROTOCOL_TO_MODULE[self.protocol]) except KeyError: ImportError(f"The module corresponding to the protocol {self.protocol} is not found.") - fn = getattr(module, "readout_error") + fn = getattr(module, "error") return cast(Callable, fn) def _to_dict(self) -> dict: From 1049895113c5eaabaabfad3a33390cd2a965b29b Mon Sep 17 00:00:00 2001 From: Roland Guichard Date: Thu, 2 Nov 2023 13:47:24 +0000 Subject: [PATCH 24/83] Extract bitstring corruption functionality and pass options. --- qadence/errors/readout.py | 91 ++++++++++++++++++++------------------- 1 file changed, 47 insertions(+), 44 deletions(-) diff --git a/qadence/errors/readout.py b/qadence/errors/readout.py index e8c30189..6b26b5b5 100644 --- a/qadence/errors/readout.py +++ b/qadence/errors/readout.py @@ -42,12 +42,34 @@ def bit_flip(qubit: int) -> int: return 1 if qubit == 0 else 0 -def readout_error( - counters: Counter, +def bs_corruption( + bitstring: str, + n_shots: int, + error_probability: float, n_qubits: int, - seed: int | None = None, - error_probability: float = 0.1, noise_distribution: Enum = WhiteNoise.UNIFORM, +) -> list: + # the noise_matrix should be available to the user if they want to do error correction + noise_matrix = noise_distribution.sample([n_shots, n_qubits]) # type: ignore[attr-defined] + + # simplest approach - en event occurs if its probability is higher than expected + # by random chance + err_idx = torch.nonzero((noise_matrix < error_probability), as_tuple=True)[1] + all_bitstrings = [bitstring] * (n_shots - len(err_idx)) # add the majority correct bit strings + + def func_distort(idx: int) -> str: + bitstring_copy = bitstring_to_array(bitstring) + bitstring_copy[idx] = bit_flip(bitstring_copy[idx]) + return array_to_bitstring(bitstring_copy) + + all_bitstrings.extend([func_distort(idx) for idx in err_idx]) + return all_bitstrings + + +def error( + counters: Counter, + n_qubits: int, + options: dict = dict(), ) -> list[Counter[Any]]: """ Implements a simple uniform readout error model for position-independent bit string @@ -65,49 +87,30 @@ def readout_error( Samples of corrupted bit strings as list[Counter]. """ + seed = options.get("seed") + error_probability = options.get("error_probability", 0.1) + noise_distribution = options.get("noise_distribution", WhiteNoise.UNIFORM) + # option for reproducibility if seed is not None: torch.manual_seed(seed) - def bs_corruption( - bitstring: str, - n_shots: int, - error_probability: float, - n_qubits: int, - noise_distribution: Enum = WhiteNoise.UNIFORM, - ) -> list: - # the noise_matrix should be available to the user if they want to do error correction - noise_matrix = noise_distribution.sample([n_shots, n_qubits]) # type: ignore[attr-defined] - - # simplest approach - en event occurs if its probability is higher than expected - # by random chance - err_idx = torch.nonzero((noise_matrix < error_probability), as_tuple=True)[1] - all_bitstrings = [bitstring] * ( - n_shots - len(err_idx) - ) # add the majority correct bit strings - - def func_distort(idx: int) -> str: - bitstring_copy = bitstring_to_array(bitstring) - bitstring_copy[idx] = bit_flip(bitstring_copy[idx]) - return array_to_bitstring(bitstring_copy) - - all_bitstrings.extend([func_distort(idx) for idx in err_idx]) - return all_bitstrings - - return [ - Counter( - chain( - *[ - bs_corruption( - bitstring=bitstring, - n_shots=n_shots, - error_probability=error_probability, - noise_distribution=noise_distribution, - n_qubits=n_qubits, - ) - for bitstring, n_shots in counter.items() - ] + corrupted_bitstrings = [] + for counter in counters: + corrupted_bitstrings.append( + Counter( + chain( + *[ + bs_corruption( + bitstring=bitstring, + n_shots=n_shots, + error_probability=error_probability, + noise_distribution=noise_distribution, + n_qubits=n_qubits, + ) + for bitstring, n_shots in counter.items() + ] + ) ) ) - for counter in counters - ] + return corrupted_bitstrings From f3a32754cc81bed3b1e451e5943b6d1661d8d2ea Mon Sep 17 00:00:00 2001 From: Roland Guichard Date: Thu, 2 Nov 2023 13:48:02 +0000 Subject: [PATCH 25/83] Test bitstring corruption and measurement protocols. --- tests/qadence/test_error_models.py | 90 +++++++++++++++++++----------- 1 file changed, 56 insertions(+), 34 deletions(-) diff --git a/tests/qadence/test_error_models.py b/tests/qadence/test_error_models.py index 1c26221f..33075386 100644 --- a/tests/qadence/test_error_models.py +++ b/tests/qadence/test_error_models.py @@ -1,8 +1,14 @@ from __future__ import annotations +from itertools import chain +from collections import Counter import pytest +from qadence.errors.readout import bs_corruption +import strategies as st import torch +from hypothesis import given, settings from sympy import acos +from torch import Tensor import qadence as qd from qadence import BackendName @@ -12,9 +18,6 @@ kron, ) from qadence.circuit import QuantumCircuit -from qadence.constructors import ( - total_magnetization, -) from qadence.constructors.hamiltonians import hamiltonian_factory from qadence.errors import Errors from qadence.measurements.protocols import Measurements @@ -23,7 +26,6 @@ CNOT, RX, RZ, - H, HamEvo, X, Y, @@ -31,6 +33,39 @@ ) +@pytest.mark.parametrize( + "error_probability, counters, exp_corrupted_counters, n_qubits", + [ + ( + 1.0, + [Counter({"00": 27, "01": 23, "10": 24, "11": 26})], + [Counter({"11": 27, "10": 23, "01": 24, "00": 26})], + 2 + ), + ( + 1.0, + [Counter({"001": 27, "010": 23, "101": 24, "110": 26})], + [Counter({"110": 27, "101": 23, "010": 24, "001": 26})], + 3 + ) + ] +) +def test_bitstring_corruption(error_probability: float, counters: list, exp_corrupted_counters: list, n_qubits: int) -> None: + corrupted_bitstrings = [ + bs_corruption( + bitstring=bitstring, + n_shots=n_shots, + error_probability=error_probability, + n_qubits=n_qubits + ) + for bitstring, n_shots in counters[0].items() + ] + corrupted_counters = [Counter(chain(*corrupted_bitstrings))] + breakpoint() + assert corrupted_counters == exp_corrupted_counters + + + @pytest.mark.parametrize( "error_probability, block, backend", [ @@ -66,7 +101,7 @@ ), (0.1, add(Z(0), Z(1), kron(X(2), X(3))) + add(X(2), X(3)), BackendName.PYQTORCH), (0.05, add(kron(Z(0), Z(1)), kron(X(2), X(3))), BackendName.PYQTORCH), - (0.2, total_magnetization(4), BackendName.PYQTORCH), + (0.2, hamiltonian_factory(4, detuning=Z), BackendName.PYQTORCH), (0.1, kron(Z(0), Z(1)) + CNOT(0, 1), BackendName.PYQTORCH), ], ) @@ -81,7 +116,7 @@ def test_readout_error_quantum_model( noisy = QuantumModel( QuantumCircuit(block.n_qubits, block), backend=backend, diff_mode=diff_mode - ).sample(error=Errors(protocol=Errors.READOUT, options=None)) + ).sample(error=Errors(protocol=Errors.READOUT)) assert len(noisy[0]) <= 2 ** block.n_qubits and len(noisy[0]) > len(err_free[0]) assert all( @@ -107,8 +142,9 @@ def test_readout_error_backends(backend: BackendName) -> None: # sample samples = qd.sample(feature_map, n_shots=1000, values=inputs, backend=backend, error=None) # introduce errors - error = Errors(protocol=Errors.READOUT, options=None).get_error_fn() - noisy_samples = error(counters=samples, n_qubits=n_qubits, error_probability=error_probability) + options = {"error_probability": error_probability} + error = Errors(protocol=Errors.READOUT, options=options).get_error_fn() + noisy_samples = error(counters=samples, n_qubits=n_qubits) # compare that the results are with an error of 10% (the default error_probability) assert all( [ @@ -123,34 +159,20 @@ def test_readout_error_backends(backend: BackendName) -> None: ) -@pytest.mark.parametrize( - "block, observable, backend", - [ - (kron(H(0), Z(1)), hamiltonian_factory(2, detuning=Z), BackendName.PYQTORCH), - # (kron(Z(0), Z(1), Z(2)) + kron(X(0), Y(1), Z(2)), BackendName.PYQTORCH), - # (add(Z(0), Z(1), Z(2)), BackendName.PYQTORCH), - # ( - # HamEvo( - # generator=kron(X(0), X(1)) + kron(Z(0), Z(1)) + kron(X(2), X(3)), parameter=0.005 - # ), - # BackendName.PYQTORCH, - # ), - # (add(Z(0), Z(1), kron(X(2), X(3))) + add(X(2), X(3)), BackendName.PYQTORCH), - # (dd(kron(Z(0), Z(1)), kron(X(2), X(3))), BackendName.PYQTORCH), - # (total_magnetization(4), BackendName.PYQTORCH), - # (kron(Z(0), Z(1)) + CNOT(0, 1), BackendName.PYQTORCH), - ], -) +# @pytest.mark.parametrize("measurement_proto", [Measurements.TOMOGRAPHY]) +@given(st.restricted_batched_circuits()) +@settings(deadline=None) def test_readout_error_with_measurements( - block: AbstractBlock, observable: AbstractBlock, backend: BackendName -): - circuit = QuantumCircuit(block.n_qubits, block) - model = QuantumModel(circuit=circuit, observable=observable, backend=backend) + circ_and_vals: tuple[QuantumCircuit, dict[str, Tensor]] +) -> None: + circuit, inputs = circ_and_vals + observable = hamiltonian_factory(circuit.n_qubits, detuning=Z) + model = QuantumModel(circuit=circuit, observable=observable) error = Errors(protocol=Errors.READOUT) measurement = Measurements(protocol=Measurements.TOMOGRAPHY, options={"n_shots": 1000}) - measured = model.expectation(measurement=measurement) - noisy = model.expectation(measurement=measurement, error=error) - exact = model.expectation() - print(f"noisy {noisy} exact {exact}") + measured = model.expectation(values=inputs, measurement=measurement) + noisy = model.expectation(values=inputs, measurement=measurement, error=error) + exact = model.expectation(values=inputs) + assert torch.allclose(noisy, exact, atol=2.0e-2) From 0f3a9ecc6ca593ff8baf15fa0588cd25eb129e6b Mon Sep 17 00:00:00 2001 From: Roland Guichard Date: Thu, 2 Nov 2023 15:31:08 +0000 Subject: [PATCH 26/83] Tests for integration with measurements and bitstring corruption. --- tests/qadence/test_error_models.py | 46 +++++++++++++++++++++--------- 1 file changed, 32 insertions(+), 14 deletions(-) diff --git a/tests/qadence/test_error_models.py b/tests/qadence/test_error_models.py index 33075386..8f160991 100644 --- a/tests/qadence/test_error_models.py +++ b/tests/qadence/test_error_models.py @@ -1,9 +1,9 @@ from __future__ import annotations -from itertools import chain from collections import Counter +from itertools import chain + import pytest -from qadence.errors.readout import bs_corruption import strategies as st import torch from hypothesis import given, settings @@ -20,6 +20,7 @@ from qadence.circuit import QuantumCircuit from qadence.constructors.hamiltonians import hamiltonian_factory from qadence.errors import Errors +from qadence.errors.readout import bs_corruption from qadence.measurements.protocols import Measurements from qadence.models import QuantumModel from qadence.operations import ( @@ -31,6 +32,7 @@ Y, Z, ) +from qadence.types import DiffMode @pytest.mark.parametrize( @@ -40,31 +42,32 @@ 1.0, [Counter({"00": 27, "01": 23, "10": 24, "11": 26})], [Counter({"11": 27, "10": 23, "01": 24, "00": 26})], - 2 + 2, ), ( 1.0, [Counter({"001": 27, "010": 23, "101": 24, "110": 26})], [Counter({"110": 27, "101": 23, "010": 24, "001": 26})], - 3 - ) - ] + 3, + ), + ], ) -def test_bitstring_corruption(error_probability: float, counters: list, exp_corrupted_counters: list, n_qubits: int) -> None: +def test_bitstring_corruption( + error_probability: float, counters: list, exp_corrupted_counters: list, n_qubits: int +) -> None: corrupted_bitstrings = [ bs_corruption( bitstring=bitstring, n_shots=n_shots, error_probability=error_probability, - n_qubits=n_qubits + n_qubits=n_qubits, ) for bitstring, n_shots in counters[0].items() ] corrupted_counters = [Counter(chain(*corrupted_bitstrings))] breakpoint() - assert corrupted_counters == exp_corrupted_counters + assert corrupted_counters == exp_corrupted_counters - @pytest.mark.parametrize( "error_probability, block, backend", @@ -159,15 +162,17 @@ def test_readout_error_backends(backend: BackendName) -> None: ) -# @pytest.mark.parametrize("measurement_proto", [Measurements.TOMOGRAPHY]) +@pytest.mark.parametrize("measurement_proto", [Measurements.TOMOGRAPHY, Measurements.SHADOW]) @given(st.restricted_batched_circuits()) @settings(deadline=None) def test_readout_error_with_measurements( - circ_and_vals: tuple[QuantumCircuit, dict[str, Tensor]] + measurement_proto: Measurements, circ_and_vals: tuple[QuantumCircuit, dict[str, Tensor]] ) -> None: circuit, inputs = circ_and_vals + # print(circuit, inputs) observable = hamiltonian_factory(circuit.n_qubits, detuning=Z) - model = QuantumModel(circuit=circuit, observable=observable) + model = QuantumModel(circuit=circuit, observable=observable, diff_mode=DiffMode.GPSR) + # model.backend.backend.config._use_gate_params = True error = Errors(protocol=Errors.READOUT) measurement = Measurements(protocol=Measurements.TOMOGRAPHY, options={"n_shots": 1000}) @@ -175,4 +180,17 @@ def test_readout_error_with_measurements( measured = model.expectation(values=inputs, measurement=measurement) noisy = model.expectation(values=inputs, measurement=measurement, error=error) exact = model.expectation(values=inputs) - assert torch.allclose(noisy, exact, atol=2.0e-2) + # breakpoint() + if exact.numel() > 1: + exact_values = torch.abs(exact) + for noisy_value, exact_value in zip(noisy, exact): + noisy_val = noisy_value.item() + exact_val = exact_value.item() + atol = exact_val / 3.0 if exact_val != 0.0 else 0.33 + assert torch.allclose(noisy_value, exact_value, atol=atol) + + else: + exact_value = torch.abs(exact).item() + # print(f"exact {exact_value}") + atol = exact_value / 3.0 if exact_value != 0.0 else 0.33 + assert torch.allclose(noisy, exact, atol=atol) From 44332cc3a1a07a345c3bea0846560a5fefbb18e7 Mon Sep 17 00:00:00 2001 From: Gergana Velikova Date: Thu, 2 Nov 2023 19:09:24 +0100 Subject: [PATCH 27/83] fix incorrect counts after readout --- qadence/backend.py | 1 + qadence/backends/braket/backend.py | 8 ++++++++ qadence/backends/pulser/backend.py | 11 ++++++++++- qadence/backends/pyqtorch/backend.py | 3 +++ qadence/errors/readout.py | 16 ++++++++++------ qadence/execution.py | 3 +++ tests/backends/test_backends.py | 9 +++++++-- tests/qadence/test_error_models.py | 9 ++++++--- 8 files changed, 48 insertions(+), 12 deletions(-) diff --git a/qadence/backend.py b/qadence/backend.py index 3e59d214..0d06a614 100644 --- a/qadence/backend.py +++ b/qadence/backend.py @@ -277,6 +277,7 @@ def expectation( state: Initial state. measurement: Optional measurement protocol. If None, use exact expectation value with a statevector simulator. + error: A noise model to use. endianness: Endianness of the resulting bit strings. """ raise NotImplementedError diff --git a/qadence/backends/braket/backend.py b/qadence/backends/braket/backend.py index 486aa21e..faf46b25 100644 --- a/qadence/backends/braket/backend.py +++ b/qadence/backends/braket/backend.py @@ -16,6 +16,7 @@ from qadence.blocks import AbstractBlock, block_to_tensor from qadence.circuit import QuantumCircuit from qadence.errors import Errors +from qadence.logger import get_logger from qadence.measurements import Measurements from qadence.overlap import overlap_exact from qadence.utils import Endianness @@ -23,6 +24,8 @@ from .config import Configuration from .convert_ops import convert_block +logger = get_logger(__name__) + def promote_parameters(parameters: dict[str, Tensor | float]) -> dict[str, float]: float_params = {} @@ -164,6 +167,11 @@ def expectation( error: Errors | None = None, endianness: Endianness = Endianness.BIG, ) -> Tensor: + if error is not None: + logger.warning( + f"Errors of type {error} are not implemented for expectation yet. " + "This is ignored for now." + ) # Do not flip endianness here because then we would have to reverse the observable wfs = self.run(circuit, param_values, state=state, endianness=Endianness.BIG) diff --git a/qadence/backends/pulser/backend.py b/qadence/backends/pulser/backend.py index 342401a6..f2bf8103 100644 --- a/qadence/backends/pulser/backend.py +++ b/qadence/backends/pulser/backend.py @@ -20,6 +20,7 @@ from qadence.blocks import AbstractBlock from qadence.circuit import QuantumCircuit from qadence.errors import Errors +from qadence.logger import get_logger from qadence.measurements import Measurements from qadence.overlap import overlap_exact from qadence.register import Register @@ -31,6 +32,8 @@ from .devices import Device, IdealDevice, RealisticDevice from .pulses import add_pulses +logger = get_logger(__name__) + WEAK_COUPLING_CONST = 1.2 DEFAULT_SPACING = 8.0 # µm (standard value) @@ -235,9 +238,15 @@ def expectation( param_values: dict[str, Tensor] = {}, state: Tensor | None = None, measurement: Measurements | None = None, - errors: Errors | None = None, + error: Errors | None = None, endianness: Endianness = Endianness.BIG, ) -> Tensor: + if error is not None: + logger.warning( + f"Errors of type {error} are not implemented for expectation yet. " + "This is ignored for now." + ) + state = self.run(circuit, param_values=param_values, state=state, endianness=endianness) observables = observable if isinstance(observable, list) else [observable] diff --git a/qadence/backends/pyqtorch/backend.py b/qadence/backends/pyqtorch/backend.py index 3bc8e9de..ae6b7b52 100644 --- a/qadence/backends/pyqtorch/backend.py +++ b/qadence/backends/pyqtorch/backend.py @@ -131,6 +131,7 @@ def _batched_expectation( param_values: dict[str, Tensor] = {}, state: Tensor | None = None, measurement: Measurements | None = None, + error: Errors | None = None, endianness: Endianness = Endianness.BIG, ) -> Tensor: state = self.run( @@ -155,6 +156,7 @@ def _looped_expectation( param_values: dict[str, Tensor] = {}, state: Tensor | None = None, measurement: Measurements | None = None, + error: Errors | None = None, endianness: Endianness = Endianness.BIG, ) -> Tensor: state = zero_state(circuit.abstract.n_qubits, batch_size=1) if state is None else state @@ -196,6 +198,7 @@ def expectation( param_values=param_values, state=state, measurement=measurement, + error=error, endianness=endianness, ) diff --git a/qadence/errors/readout.py b/qadence/errors/readout.py index 6b26b5b5..8a2f45ee 100644 --- a/qadence/errors/readout.py +++ b/qadence/errors/readout.py @@ -52,17 +52,21 @@ def bs_corruption( # the noise_matrix should be available to the user if they want to do error correction noise_matrix = noise_distribution.sample([n_shots, n_qubits]) # type: ignore[attr-defined] - # simplest approach - en event occurs if its probability is higher than expected + # the simplest approach - en event occurs if its probability is higher than expected # by random chance - err_idx = torch.nonzero((noise_matrix < error_probability), as_tuple=True)[1] - all_bitstrings = [bitstring] * (n_shots - len(err_idx)) # add the majority correct bit strings + err_idx = [(item) for i, item in enumerate(noise_matrix < error_probability) if any(item)] - def func_distort(idx: int) -> str: + def func_distort(idx: tuple) -> str: bitstring_copy = bitstring_to_array(bitstring) - bitstring_copy[idx] = bit_flip(bitstring_copy[idx]) + for id in range(n_qubits): + if idx[id] is True: + bitstring_copy[id] = bit_flip(bitstring_copy[id]) return array_to_bitstring(bitstring_copy) - all_bitstrings.extend([func_distort(idx) for idx in err_idx]) + all_bitstrings = [func_distort(idx) for idx in err_idx] + all_bitstrings.extend( + [bitstring] * (n_shots - len(all_bitstrings)) + ) # add the error-free bit strings return all_bitstrings diff --git a/qadence/execution.py b/qadence/execution.py index ee906125..2c37ec30 100644 --- a/qadence/execution.py +++ b/qadence/execution.py @@ -163,6 +163,7 @@ def expectation( state: Tensor = None, backend: BackendName = BackendName.PYQTORCH, diff_mode: Union[DiffMode, str, None] = None, + error: Union[Errors, None] = None, endianness: Endianness = Endianness.BIG, configuration: Union[BackendConfiguration, dict, None] = None, ) -> Tensor: @@ -216,6 +217,7 @@ def _( state: Tensor = None, backend: BackendName = BackendName.PYQTORCH, diff_mode: Union[DiffMode, str, None] = None, + error: Union[Errors, None] = None, endianness: Endianness = Endianness.BIG, configuration: Union[BackendConfiguration, dict, None] = None, ) -> Tensor: @@ -229,6 +231,7 @@ def _expectation() -> Tensor: observable=conv.observable, # type: ignore[arg-type] param_values=conv.embedding_fn(conv.params, values), state=state, + error=error, endianness=endianness, ) diff --git a/tests/backends/test_backends.py b/tests/backends/test_backends.py index 3b038340..b3f059f9 100644 --- a/tests/backends/test_backends.py +++ b/tests/backends/test_backends.py @@ -165,11 +165,16 @@ def test_backend_sampling(circ: QuantumCircuit) -> None: (circ_pyqtorch, _, _, _) = bknd_pyqtorch.convert(circ) (circ_braket, _, embed, params) = bknd_braket.convert(circ) - # braket doesnt support custom initial states so we use state=None for the zero state + # braket doesn't support custom initial states, so we use state=None for the zero state pyqtorch_samples = bknd_pyqtorch.sample( circ_pyqtorch, embed(params, {}), state=None, n_shots=100 ) - braket_samples = bknd_braket.sample(circ_braket, embed(params, {}), state=None, n_shots=100) + braket_samples = bknd_braket.sample( + circ_braket, + embed(params, {}), + state=None, + n_shots=100, + ) for pyqtorch_sample, braket_sample in zip(pyqtorch_samples, braket_samples): assert js_divergence(pyqtorch_sample, braket_sample) < JS_ACCEPTANCE diff --git a/tests/qadence/test_error_models.py b/tests/qadence/test_error_models.py index 8f160991..c23e0734 100644 --- a/tests/qadence/test_error_models.py +++ b/tests/qadence/test_error_models.py @@ -65,7 +65,8 @@ def test_bitstring_corruption( for bitstring, n_shots in counters[0].items() ] corrupted_counters = [Counter(chain(*corrupted_bitstrings))] - breakpoint() + # breakpoint() + print(corrupted_counters, exp_corrupted_counters) assert corrupted_counters == exp_corrupted_counters @@ -111,17 +112,19 @@ def test_bitstring_corruption( def test_readout_error_quantum_model( error_probability: float, block: AbstractBlock, backend: BackendName ) -> None: + n_shots = 2000 diff_mode = "ad" if backend == BackendName.PYQTORCH else "gpsr" err_free = QuantumModel( QuantumCircuit(block.n_qubits, block), backend=backend, diff_mode=diff_mode - ).sample() + ).sample(n_shots=n_shots) noisy = QuantumModel( QuantumCircuit(block.n_qubits, block), backend=backend, diff_mode=diff_mode - ).sample(error=Errors(protocol=Errors.READOUT)) + ).sample(error=Errors(protocol=Errors.READOUT), n_shots=n_shots) assert len(noisy[0]) <= 2 ** block.n_qubits and len(noisy[0]) > len(err_free[0]) + assert sum(noisy[0].values()) == sum(err_free[0].values()) == n_shots assert all( [ True From aa306dc7bee8508e9773c10cbf5fe285f9f68830 Mon Sep 17 00:00:00 2001 From: Gergana Velikova Date: Thu, 2 Nov 2023 19:39:50 +0100 Subject: [PATCH 28/83] fixes --- qadence/errors/readout.py | 2 +- qadence/measurements/shadow.py | 3 +++ qadence/measurements/tomography.py | 13 +++++++++++++ tests/qadence/test_error_models.py | 4 +--- 4 files changed, 18 insertions(+), 4 deletions(-) diff --git a/qadence/errors/readout.py b/qadence/errors/readout.py index 8a2f45ee..c77edac8 100644 --- a/qadence/errors/readout.py +++ b/qadence/errors/readout.py @@ -59,7 +59,7 @@ def bs_corruption( def func_distort(idx: tuple) -> str: bitstring_copy = bitstring_to_array(bitstring) for id in range(n_qubits): - if idx[id] is True: + if idx[id]: bitstring_copy[id] = bit_flip(bitstring_copy[id]) return array_to_bitstring(bitstring_copy) diff --git a/qadence/measurements/shadow.py b/qadence/measurements/shadow.py index 068f9abf..5b50b0a3 100644 --- a/qadence/measurements/shadow.py +++ b/qadence/measurements/shadow.py @@ -311,6 +311,7 @@ def compute_expectation( options: dict, state: Tensor | None = None, backend_name: BackendName = BackendName.PYQTORCH, + error: Errors | None = None, endianness: Endianness = Endianness.BIG, ) -> Tensor: """ @@ -326,6 +327,8 @@ def compute_expectation( Here, shadow_size (int), accuracy (float) and confidence (float) are supported. state (Tensor | None): an initial input state. backend_name (BackendName): a backend name to retrieve computations from. + error: A noise model to use. + endianness: Endianness of the observable estimate. Returns: expectations (Tensor): an estimation of the expectation values. diff --git a/qadence/measurements/tomography.py b/qadence/measurements/tomography.py index a8d8e48b..b4de9256 100644 --- a/qadence/measurements/tomography.py +++ b/qadence/measurements/tomography.py @@ -138,6 +138,19 @@ def compute_expectation( Given a circuit and a list of observables, apply basic tomography protocol to estimate the expectation values. + + Args: + circuit (QuantumCircuit): a circuit to prepare the state. + observables (list[AbstractBlock]): a list of observables + to estimate the expectation values from. + param_values (dict): a dict of values to substitute the + symbolic parameters for. + options (dict): a dict of options for the measurement protocol. + Here, shadow_size (int), accuracy (float) and confidence (float) are supported. + state (Tensor | None): an initial input state. + backend_name (BackendName): a backend name to retrieve computations from. + error: A noise model to use. + endianness: Endianness of the observable estimate. """ if not isinstance(observables, list): raise TypeError( diff --git a/tests/qadence/test_error_models.py b/tests/qadence/test_error_models.py index c23e0734..a65fd700 100644 --- a/tests/qadence/test_error_models.py +++ b/tests/qadence/test_error_models.py @@ -59,14 +59,13 @@ def test_bitstring_corruption( bs_corruption( bitstring=bitstring, n_shots=n_shots, - error_probability=error_probability, + error_probability=1.0, n_qubits=n_qubits, ) for bitstring, n_shots in counters[0].items() ] corrupted_counters = [Counter(chain(*corrupted_bitstrings))] # breakpoint() - print(corrupted_counters, exp_corrupted_counters) assert corrupted_counters == exp_corrupted_counters @@ -123,7 +122,6 @@ def test_readout_error_quantum_model( QuantumCircuit(block.n_qubits, block), backend=backend, diff_mode=diff_mode ).sample(error=Errors(protocol=Errors.READOUT), n_shots=n_shots) - assert len(noisy[0]) <= 2 ** block.n_qubits and len(noisy[0]) > len(err_free[0]) assert sum(noisy[0].values()) == sum(err_free[0].values()) == n_shots assert all( [ From 8267b82a4ca256e36262d1ce180476bb6f9df0e9 Mon Sep 17 00:00:00 2001 From: Roland Guichard Date: Fri, 3 Nov 2023 13:33:43 +0000 Subject: [PATCH 29/83] Moving old bitstring corruption functionality. --- qadence/errors/readout.py | 29 +++++++++++++++++++++++++++-- 1 file changed, 27 insertions(+), 2 deletions(-) diff --git a/qadence/errors/readout.py b/qadence/errors/readout.py index c77edac8..bdae4e44 100644 --- a/qadence/errors/readout.py +++ b/qadence/errors/readout.py @@ -2,11 +2,10 @@ from collections import Counter from enum import Enum -from itertools import chain -from typing import Any import numpy as np import torch +from torch import rand, where from torch.distributions import normal, poisson, uniform from qadence.logger import get_logger @@ -70,6 +69,32 @@ def func_distort(idx: tuple) -> str: return all_bitstrings +def corrupt(bitflip_proba: float, counters: list[Counter], n_qubits: int) -> list[Counter]: + def flip_bits(bitstring: str, corruption: int, n_qubits: int) -> str: + """Flip bits for the corruption int in bitstring.""" + str_format = "{:0" + str(n_qubits) + "b}" + return str_format.format(int(bitstring, 2) ^ corruption) + + # Get a tensor of random bit indices. + rands = where(rand(n_qubits) < bitflip_proba)[0] + + if rands.numel(): + # Corruption int. + corruption = torch.tensor([2**n for n in rands], dtype=torch.int64).sum().item() + corrupted_counters = [] + for counter in counters: + corrupted_counter: Counter = Counter() + for bitstring, count in counter.items(): + corrupted_bitstring = flip_bits( + bitstring=bitstring, corruption=corruption, n_qubits=n_qubits + ) + corrupted_counter[corrupted_bitstring] = count + corrupted_counters.append(corrupted_counter) + return corrupted_counters + + return counters + + def error( counters: Counter, n_qubits: int, From bc5756ad037ed8edabd9a0aac3a298be06b76df3 Mon Sep 17 00:00:00 2001 From: Roland Guichard Date: Fri, 3 Nov 2023 13:34:26 +0000 Subject: [PATCH 30/83] Simplify types and use old bitstring corruption. --- qadence/errors/readout.py | 48 ++++++++++++++++++++++----------------- 1 file changed, 27 insertions(+), 21 deletions(-) diff --git a/qadence/errors/readout.py b/qadence/errors/readout.py index bdae4e44..c5849602 100644 --- a/qadence/errors/readout.py +++ b/qadence/errors/readout.py @@ -96,10 +96,10 @@ def flip_bits(bitstring: str, corruption: int, n_qubits: int) -> str: def error( - counters: Counter, + counters: list[Counter], n_qubits: int, options: dict = dict(), -) -> list[Counter[Any]]: +) -> list[Counter]: """ Implements a simple uniform readout error model for position-independent bit string corruption. @@ -124,22 +124,28 @@ def error( if seed is not None: torch.manual_seed(seed) - corrupted_bitstrings = [] - for counter in counters: - corrupted_bitstrings.append( - Counter( - chain( - *[ - bs_corruption( - bitstring=bitstring, - n_shots=n_shots, - error_probability=error_probability, - noise_distribution=noise_distribution, - n_qubits=n_qubits, - ) - for bitstring, n_shots in counter.items() - ] - ) - ) - ) - return corrupted_bitstrings + # corrupted_bitstrings = [] + # for counter in counters: + # corrupted_bitstrings.append( + # Counter( + # chain( + # *[ + # bs_corruption( + # bitstring=bitstring, + # n_shots=n_shots, + # error_probability=error_probability, + # noise_distribution=noise_distribution, + # n_qubits=n_qubits, + # ) + # for bitstring, n_shots in counter.items() + # ] + # ) + # ) + # ) + + # bitflip_proba = options.get("bitflip_proba") + # breakpoint() + # if bitflip_proba is None: + # KeyError("Readout error protocol requires a 'bitflip_proba' option of type 'float'.") + return corrupt(bitflip_proba=error_probability, counters=counters, n_qubits=n_qubits) + # return corrupted_bitstrings From 022c78811bb3429459c5fec4889da140bf22d0b1 Mon Sep 17 00:00:00 2001 From: Roland Guichard Date: Fri, 3 Nov 2023 13:35:17 +0000 Subject: [PATCH 31/83] Propagate error protocol for shadows. --- qadence/measurements/shadow.py | 3 +++ 1 file changed, 3 insertions(+) diff --git a/qadence/measurements/shadow.py b/qadence/measurements/shadow.py index 5b50b0a3..92a0c4fd 100644 --- a/qadence/measurements/shadow.py +++ b/qadence/measurements/shadow.py @@ -260,6 +260,7 @@ def estimations( confidence: float = 0.1, state: Tensor | None = None, backend_name: BackendName = BackendName.PYQTORCH, + error: Errors | None = None, endianness: Endianness = Endianness.BIG, ) -> Tensor: """Compute expectation values for all local observables using median of means.""" @@ -275,6 +276,7 @@ def estimations( param_values=param_values, state=state, backend_name=backend_name, + error=error, endianness=endianness, ) estimations = [] @@ -358,5 +360,6 @@ def compute_expectation( confidence=confidence, state=state, backend_name=backend_name, + error=error, endianness=endianness, ) From b47dbd675a1d67e01662cb9708d5225b573e4932 Mon Sep 17 00:00:00 2001 From: Roland Guichard Date: Fri, 3 Nov 2023 13:41:46 +0000 Subject: [PATCH 32/83] Add tests for bitstring corruption and integration with measurements. --- tests/qadence/test_error_models.py | 119 +++++++++++++++-------------- 1 file changed, 63 insertions(+), 56 deletions(-) diff --git a/tests/qadence/test_error_models.py b/tests/qadence/test_error_models.py index a65fd700..5f7f5d2c 100644 --- a/tests/qadence/test_error_models.py +++ b/tests/qadence/test_error_models.py @@ -1,14 +1,10 @@ from __future__ import annotations from collections import Counter -from itertools import chain import pytest -import strategies as st import torch -from hypothesis import given, settings from sympy import acos -from torch import Tensor import qadence as qd from qadence import BackendName @@ -20,13 +16,14 @@ from qadence.circuit import QuantumCircuit from qadence.constructors.hamiltonians import hamiltonian_factory from qadence.errors import Errors -from qadence.errors.readout import bs_corruption +from qadence.errors.readout import corrupt from qadence.measurements.protocols import Measurements from qadence.models import QuantumModel from qadence.operations import ( CNOT, RX, RZ, + H, HamEvo, X, Y, @@ -55,17 +52,12 @@ def test_bitstring_corruption( error_probability: float, counters: list, exp_corrupted_counters: list, n_qubits: int ) -> None: - corrupted_bitstrings = [ - bs_corruption( - bitstring=bitstring, - n_shots=n_shots, - error_probability=1.0, - n_qubits=n_qubits, - ) - for bitstring, n_shots in counters[0].items() - ] - corrupted_counters = [Counter(chain(*corrupted_bitstrings))] - # breakpoint() + corrupted_counters = corrupt( + bitflip_proba=error_probability, + counters=counters, + n_qubits=n_qubits, + ) + assert corrupted_counters[0].total() == 100 assert corrupted_counters == exp_corrupted_counters @@ -114,26 +106,30 @@ def test_readout_error_quantum_model( n_shots = 2000 diff_mode = "ad" if backend == BackendName.PYQTORCH else "gpsr" - err_free = QuantumModel( + noiseless_samples: list[Counter] = QuantumModel( QuantumCircuit(block.n_qubits, block), backend=backend, diff_mode=diff_mode ).sample(n_shots=n_shots) - noisy = QuantumModel( + noisy_samples: list[Counter] = QuantumModel( QuantumCircuit(block.n_qubits, block), backend=backend, diff_mode=diff_mode ).sample(error=Errors(protocol=Errors.READOUT), n_shots=n_shots) - assert sum(noisy[0].values()) == sum(err_free[0].values()) == n_shots - assert all( - [ - True - if ( - err_free[0]["bitstring"] < int(count + count * error_probability) - or err_free[0]["bitstring"] > int(count - count * error_probability) - ) - else False - for bitstring, count in noisy[0].items() - ] - ) + # breakpoint() + for noiseless, noisy in zip(noiseless_samples, noisy_samples): + assert noiseless.total() == noisy.total() + # print(js_divergence(noiseless, noisy)) + # assert len(noisy[0]) <= 2 ** block.n_qubits and len(noisy[0]) > len(err_free[0]) + # assert all( + # [ + # True + # if ( + # err_free[0]["bitstring"] < int(count + count * error_probability) + # or err_free[0]["bitstring"] > int(count - count * error_probability) + # ) + # else False + # for bitstring, count in noisy[0].items() + # ] + # ) @pytest.mark.parametrize("backend", [BackendName.BRAKET, BackendName.PYQTORCH, BackendName.PULSER]) @@ -149,49 +145,60 @@ def test_readout_error_backends(backend: BackendName) -> None: options = {"error_probability": error_probability} error = Errors(protocol=Errors.READOUT, options=options).get_error_fn() noisy_samples = error(counters=samples, n_qubits=n_qubits) + # breakpoint() # compare that the results are with an error of 10% (the default error_probability) - assert all( - [ - True - if ( - samples[0]["bitstring"] < int(count + count * error_probability) - or samples[0]["bitstring"] > int(count - count * error_probability) - ) - else False - for bitstring, count in noisy_samples[0].items() - ] - ) - - -@pytest.mark.parametrize("measurement_proto", [Measurements.TOMOGRAPHY, Measurements.SHADOW]) -@given(st.restricted_batched_circuits()) -@settings(deadline=None) + for sample, noisy_sample in zip(samples, noisy_samples): + assert sample.total() == noisy_sample.total() + # print(js_divergence(sample, noisy_sample)) + # assert all( + # [ + # True + # if ( + # samples[0]["bitstring"] < int(count + count * error_probability) + # or samples[0]["bitstring"] > int(count - count * error_probability) + # ) + # else False + # for bitstring, count in noisy_samples[0].items() + # ] + # ) + + +# @pytest.mark.flaky(max_runs=5) +@pytest.mark.parametrize( + "measurement_proto, options", + [ + (Measurements.TOMOGRAPHY, {"n_shots": 10000}), + (Measurements.SHADOW, {"accuracy": 0.1, "confidence": 0.1}), + ], +) +# @given(st.restricted_batched_circuits()) +# @settings(deadline=None) def test_readout_error_with_measurements( - measurement_proto: Measurements, circ_and_vals: tuple[QuantumCircuit, dict[str, Tensor]] + measurement_proto: Measurements, + options: dict, + # circ_and_vals: tuple[QuantumCircuit, dict[str, Tensor]] ) -> None: - circuit, inputs = circ_and_vals - # print(circuit, inputs) + # circuit, inputs = circ_and_vals + circuit = QuantumCircuit(2, kron(H(0), Z(1))) + inputs: dict = dict() observable = hamiltonian_factory(circuit.n_qubits, detuning=Z) + model = QuantumModel(circuit=circuit, observable=observable, diff_mode=DiffMode.GPSR) # model.backend.backend.config._use_gate_params = True error = Errors(protocol=Errors.READOUT) - measurement = Measurements(protocol=Measurements.TOMOGRAPHY, options={"n_shots": 1000}) + measurement = Measurements(protocol=str(measurement_proto), options=options) - measured = model.expectation(values=inputs, measurement=measurement) + # measured = model.expectation(values=inputs, measurement=measurement) noisy = model.expectation(values=inputs, measurement=measurement, error=error) exact = model.expectation(values=inputs) - # breakpoint() if exact.numel() > 1: - exact_values = torch.abs(exact) for noisy_value, exact_value in zip(noisy, exact): - noisy_val = noisy_value.item() - exact_val = exact_value.item() + exact_val = torch.abs(exact_value).item() atol = exact_val / 3.0 if exact_val != 0.0 else 0.33 assert torch.allclose(noisy_value, exact_value, atol=atol) else: exact_value = torch.abs(exact).item() - # print(f"exact {exact_value}") atol = exact_value / 3.0 if exact_value != 0.0 else 0.33 assert torch.allclose(noisy, exact, atol=atol) From bae8eb357cd94b82f1f7ebec50a9094fe074483c Mon Sep 17 00:00:00 2001 From: Roland Guichard Date: Fri, 3 Nov 2023 13:49:46 +0000 Subject: [PATCH 33/83] Fix conflicts. --- qadence/backend.py | 3 --- qadence/backends/pulser/backend.py | 6 +----- qadence/execution.py | 3 --- 3 files changed, 1 insertion(+), 11 deletions(-) diff --git a/qadence/backend.py b/qadence/backend.py index 891a8b5e..765230fd 100644 --- a/qadence/backend.py +++ b/qadence/backend.py @@ -20,11 +20,8 @@ ) from qadence.blocks.analog import ConstantAnalogRotation, WaitBlock from qadence.circuit import QuantumCircuit -<<<<<<< HEAD from qadence.errors import Errors -======= from qadence.logger import get_logger ->>>>>>> main from qadence.measurements import Measurements from qadence.parameters import stringify from qadence.types import BackendName, DiffMode, Endianness diff --git a/qadence/backends/pulser/backend.py b/qadence/backends/pulser/backend.py index c6baaeea..958622c9 100644 --- a/qadence/backends/pulser/backend.py +++ b/qadence/backends/pulser/backend.py @@ -25,7 +25,7 @@ from qadence.overlap import overlap_exact from qadence.register import Register from qadence.transpile import transpile -from qadence.utils import Endianness, get_logger +from qadence.utils import Endianness from .channels import GLOBAL_CHANNEL, LOCAL_CHANNEL from .cloud import get_client @@ -34,11 +34,7 @@ from .devices import Device, IdealDevice, RealisticDevice from .pulses import add_pulses -<<<<<<< HEAD logger = get_logger(__name__) -======= -logger = get_logger(__file__) ->>>>>>> main WEAK_COUPLING_CONST = 1.2 diff --git a/qadence/execution.py b/qadence/execution.py index 5c4a3862..85a57b96 100644 --- a/qadence/execution.py +++ b/qadence/execution.py @@ -10,11 +10,8 @@ from qadence.backend import BackendConfiguration, BackendName from qadence.blocks import AbstractBlock from qadence.circuit import QuantumCircuit -<<<<<<< HEAD from qadence.errors import Errors -======= from qadence.qubit_support import QubitSupport ->>>>>>> main from qadence.register import Register from qadence.types import DiffMode from qadence.utils import Endianness From 439f9f8f72560c10279181d6c48a045fccf8fdd9 Mon Sep 17 00:00:00 2001 From: Gergana Velikova Date: Fri, 3 Nov 2023 18:10:09 +0100 Subject: [PATCH 34/83] changed back the readout incorporation function and tests --- qadence/errors/readout.py | 41 ++++++------- tests/qadence/test_error_models.py | 96 +++++++++++++++++++++--------- 2 files changed, 88 insertions(+), 49 deletions(-) diff --git a/qadence/errors/readout.py b/qadence/errors/readout.py index c5849602..b6fdd740 100644 --- a/qadence/errors/readout.py +++ b/qadence/errors/readout.py @@ -2,6 +2,7 @@ from collections import Counter from enum import Enum +from itertools import chain import numpy as np import torch @@ -124,28 +125,28 @@ def error( if seed is not None: torch.manual_seed(seed) - # corrupted_bitstrings = [] - # for counter in counters: - # corrupted_bitstrings.append( - # Counter( - # chain( - # *[ - # bs_corruption( - # bitstring=bitstring, - # n_shots=n_shots, - # error_probability=error_probability, - # noise_distribution=noise_distribution, - # n_qubits=n_qubits, - # ) - # for bitstring, n_shots in counter.items() - # ] - # ) - # ) - # ) + corrupted_bitstrings = [] + for counter in counters: + corrupted_bitstrings.append( + Counter( + chain( + *[ + bs_corruption( + bitstring=bitstring, + n_shots=n_shots, + error_probability=error_probability, + noise_distribution=noise_distribution, + n_qubits=n_qubits, + ) + for bitstring, n_shots in counter.items() + ] + ) + ) + ) # bitflip_proba = options.get("bitflip_proba") # breakpoint() # if bitflip_proba is None: # KeyError("Readout error protocol requires a 'bitflip_proba' option of type 'float'.") - return corrupt(bitflip_proba=error_probability, counters=counters, n_qubits=n_qubits) - # return corrupted_bitstrings + # return corrupt(bitflip_proba=error_probability, counters=counters, n_qubits=n_qubits) + return corrupted_bitstrings diff --git a/tests/qadence/test_error_models.py b/tests/qadence/test_error_models.py index 5f7f5d2c..68b19805 100644 --- a/tests/qadence/test_error_models.py +++ b/tests/qadence/test_error_models.py @@ -1,6 +1,7 @@ from __future__ import annotations from collections import Counter +from itertools import chain import pytest import torch @@ -15,8 +16,9 @@ ) from qadence.circuit import QuantumCircuit from qadence.constructors.hamiltonians import hamiltonian_factory +from qadence.divergences import js_divergence from qadence.errors import Errors -from qadence.errors.readout import corrupt +from qadence.errors.readout import bs_corruption from qadence.measurements.protocols import Measurements from qadence.models import QuantumModel from qadence.operations import ( @@ -52,58 +54,80 @@ def test_bitstring_corruption( error_probability: float, counters: list, exp_corrupted_counters: list, n_qubits: int ) -> None: - corrupted_counters = corrupt( - bitflip_proba=error_probability, - counters=counters, - n_qubits=n_qubits, - ) - assert corrupted_counters[0].total() == 100 + # corrupted_counters = corrupt( + # bitflip_proba=error_probability, + # counters=counters, + # n_qubits=n_qubits, + # ) + corrupted_bitstrings = [ + bs_corruption( + bitstring=bitstring, + n_shots=n_shots, + error_probability=error_probability, + n_qubits=n_qubits, + ) + for bitstring, n_shots in counters[0].items() + ] + + corrupted_counters = [Counter(chain(*corrupted_bitstrings))] + # assert corrupted_counters[0].total() == 100 # python3.9 complains about .total in Counter + assert sum(corrupted_counters[0].values()) == 100 assert corrupted_counters == exp_corrupted_counters + assert torch.allclose( + torch.tensor(1.0 - js_divergence(corrupted_counters[0], counters[0])), + torch.ones(1), + atol=1e-3, + ) @pytest.mark.parametrize( - "error_probability, block, backend", + "error_probability, n_shots, block, backend", [ - (0.1, kron(X(0), X(1)), BackendName.BRAKET), - (0.1, kron(Z(0), Z(1), Z(2)) + kron(X(0), Y(1), Z(2)), BackendName.BRAKET), - (0.15, add(Z(0), Z(1), Z(2)), BackendName.BRAKET), - (0.01, kron(X(0), X(1)) + kron(Z(0), Z(1)) + kron(X(2), X(3)), BackendName.BRAKET), - (0.1, add(Z(0), Z(1), kron(X(2), X(3))) + add(X(2), X(3)), BackendName.BRAKET), - (0.1, add(kron(Z(0), Z(1)), kron(X(2), X(3))), BackendName.BRAKET), - (0.1, kron(Z(0), Z(1)) + CNOT(0, 1), BackendName.BRAKET), + (0.1, 100, kron(X(0), X(1)), BackendName.BRAKET), + (0.1, 1000, kron(Z(0), Z(1), Z(2)) + kron(X(0), Y(1), Z(2)), BackendName.BRAKET), + (0.15, 1000, add(Z(0), Z(1), Z(2)), BackendName.BRAKET), + (0.1, 5000, kron(X(0), X(1)) + kron(Z(0), Z(1)) + kron(X(2), X(3)), BackendName.BRAKET), + (0.1, 500, add(Z(0), Z(1), kron(X(2), X(3))) + add(X(2), X(3)), BackendName.BRAKET), + (0.1, 2000, add(kron(Z(0), Z(1)), kron(X(2), X(3))), BackendName.BRAKET), + (0.1, 1300, kron(Z(0), Z(1)) + CNOT(0, 1), BackendName.BRAKET), ( 0.05, + 1500, kron(RZ(0, parameter=0.01), RZ(1, parameter=0.01)) + kron(RX(0, parameter=0.01), RX(1, parameter=0.01)), BackendName.PULSER, ), - (0.001, HamEvo(generator=kron(Z(0), Z(1)), parameter=0.05), BackendName.BRAKET), - (0.12, HamEvo(generator=kron(Z(0), Z(1), Z(2)), parameter=0.001), BackendName.BRAKET), + (0.001, 5000, HamEvo(generator=kron(Z(0), Z(1)), parameter=0.05), BackendName.BRAKET), + (0.12, 2000, HamEvo(generator=kron(Z(0), Z(1), Z(2)), parameter=0.001), BackendName.BRAKET), ( 0.1, + 1000, HamEvo(generator=kron(Z(0), Z(1)) + kron(Z(0), Z(1), Z(2)), parameter=0.005), BackendName.BRAKET, ), - (0.1, kron(X(0), X(1)), BackendName.PYQTORCH), - (0.01, kron(Z(0), Z(1), Z(2)) + kron(X(0), Y(1), Z(2)), BackendName.PYQTORCH), - (0.01, add(Z(0), Z(1), Z(2)), BackendName.PYQTORCH), + (0.1, 100, kron(X(0), X(1)), BackendName.PYQTORCH), + (0.1, 200, kron(Z(0), Z(1), Z(2)) + kron(X(0), Y(1), Z(2)), BackendName.PYQTORCH), + (0.01, 1000, add(Z(0), Z(1), Z(2)), BackendName.PYQTORCH), ( 0.1, + 2000, HamEvo( generator=kron(X(0), X(1)) + kron(Z(0), Z(1)) + kron(X(2), X(3)), parameter=0.005 ), BackendName.PYQTORCH, ), - (0.1, add(Z(0), Z(1), kron(X(2), X(3))) + add(X(2), X(3)), BackendName.PYQTORCH), - (0.05, add(kron(Z(0), Z(1)), kron(X(2), X(3))), BackendName.PYQTORCH), - (0.2, hamiltonian_factory(4, detuning=Z), BackendName.PYQTORCH), - (0.1, kron(Z(0), Z(1)) + CNOT(0, 1), BackendName.PYQTORCH), + (0.1, 500, add(Z(0), Z(1), kron(X(2), X(3))) + add(X(2), X(3)), BackendName.PYQTORCH), + (0.05, 10000, add(kron(Z(0), Z(1)), kron(X(2), X(3))), BackendName.PYQTORCH), + (0.2, 1000, hamiltonian_factory(4, detuning=Z), BackendName.PYQTORCH), + (0.1, 500, kron(Z(0), Z(1)) + CNOT(0, 1), BackendName.PYQTORCH), ], ) def test_readout_error_quantum_model( - error_probability: float, block: AbstractBlock, backend: BackendName + error_probability: float, + n_shots: int, + block: AbstractBlock, + backend: BackendName, ) -> None: - n_shots = 2000 diff_mode = "ad" if backend == BackendName.PYQTORCH else "gpsr" noiseless_samples: list[Counter] = QuantumModel( @@ -116,7 +140,14 @@ def test_readout_error_quantum_model( # breakpoint() for noiseless, noisy in zip(noiseless_samples, noisy_samples): - assert noiseless.total() == noisy.total() + assert sum(noiseless.values()) == sum(noisy.values()) == n_shots + # assert noiseless.total() == noisy.total() # python3.9 complains about .total in Counter + assert js_divergence(noiseless, noisy) > 0.0 + assert torch.allclose( + torch.tensor(1.0 - js_divergence(noiseless, noisy)), + torch.ones(1) - error_probability, + atol=1e-1, + ) # print(js_divergence(noiseless, noisy)) # assert len(noisy[0]) <= 2 ** block.n_qubits and len(noisy[0]) > len(err_free[0]) # assert all( @@ -148,8 +179,15 @@ def test_readout_error_backends(backend: BackendName) -> None: # breakpoint() # compare that the results are with an error of 10% (the default error_probability) for sample, noisy_sample in zip(samples, noisy_samples): - assert sample.total() == noisy_sample.total() - # print(js_divergence(sample, noisy_sample)) + assert sum(sample.values()) == sum(noisy_sample.values()) + # python3.9 complains about .total in Counter + # assert sample.total() == noisy_sample.total() + assert js_divergence(sample, noisy_sample) > 0.0 + assert torch.allclose( + torch.tensor(1.0 - js_divergence(sample, noisy_sample)), + torch.ones(1) - error_probability, + atol=1e-1, + ) # assert all( # [ # True From d6f0bee4db607c3f5788a31a10cc38c6dbab1e77 Mon Sep 17 00:00:00 2001 From: Roland Guichard Date: Fri, 3 Nov 2023 18:50:33 +0000 Subject: [PATCH 35/83] Add docs section about protocols for realistic simulations. --- mkdocs.yml | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/mkdocs.yml b/mkdocs.yml index faf74e56..928d27f4 100644 --- a/mkdocs.yml +++ b/mkdocs.yml @@ -37,6 +37,12 @@ nav: - Quantum circuits differentiation: advanced_tutorials/differentiability.md - Custom quantum models: advanced_tutorials/custom-models.md + - Realistic simulations: + - realistic_sims/index.md + - Measurement protocols: realistic_sims/measurements.md + - Error models: realistic_sims/errors.md + - Error mitigation: realistic_sims/mitigation.md + - API: - Block system: qadence/blocks.md - Operations: qadence/operations.md From fb51ed380e08e5ac644f5dd334e8fbb591f7c691 Mon Sep 17 00:00:00 2001 From: Roland Guichard Date: Fri, 3 Nov 2023 19:20:49 +0000 Subject: [PATCH 36/83] Fix lint. --- mkdocs.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/mkdocs.yml b/mkdocs.yml index 928d27f4..e083ab3a 100644 --- a/mkdocs.yml +++ b/mkdocs.yml @@ -42,7 +42,7 @@ nav: - Measurement protocols: realistic_sims/measurements.md - Error models: realistic_sims/errors.md - Error mitigation: realistic_sims/mitigation.md - + - API: - Block system: qadence/blocks.md - Operations: qadence/operations.md From 4b265020f18b3873b52523447d2e389d686957e6 Mon Sep 17 00:00:00 2001 From: Roland Guichard Date: Fri, 3 Nov 2023 19:24:41 +0000 Subject: [PATCH 37/83] Remove obsolete code. --- qadence/errors/readout.py | 32 ---------------------------- tests/qadence/test_error_models.py | 34 ------------------------------ 2 files changed, 66 deletions(-) diff --git a/qadence/errors/readout.py b/qadence/errors/readout.py index b6fdd740..75bd0ad2 100644 --- a/qadence/errors/readout.py +++ b/qadence/errors/readout.py @@ -6,7 +6,6 @@ import numpy as np import torch -from torch import rand, where from torch.distributions import normal, poisson, uniform from qadence.logger import get_logger @@ -70,32 +69,6 @@ def func_distort(idx: tuple) -> str: return all_bitstrings -def corrupt(bitflip_proba: float, counters: list[Counter], n_qubits: int) -> list[Counter]: - def flip_bits(bitstring: str, corruption: int, n_qubits: int) -> str: - """Flip bits for the corruption int in bitstring.""" - str_format = "{:0" + str(n_qubits) + "b}" - return str_format.format(int(bitstring, 2) ^ corruption) - - # Get a tensor of random bit indices. - rands = where(rand(n_qubits) < bitflip_proba)[0] - - if rands.numel(): - # Corruption int. - corruption = torch.tensor([2**n for n in rands], dtype=torch.int64).sum().item() - corrupted_counters = [] - for counter in counters: - corrupted_counter: Counter = Counter() - for bitstring, count in counter.items(): - corrupted_bitstring = flip_bits( - bitstring=bitstring, corruption=corruption, n_qubits=n_qubits - ) - corrupted_counter[corrupted_bitstring] = count - corrupted_counters.append(corrupted_counter) - return corrupted_counters - - return counters - - def error( counters: list[Counter], n_qubits: int, @@ -144,9 +117,4 @@ def error( ) ) - # bitflip_proba = options.get("bitflip_proba") - # breakpoint() - # if bitflip_proba is None: - # KeyError("Readout error protocol requires a 'bitflip_proba' option of type 'float'.") - # return corrupt(bitflip_proba=error_probability, counters=counters, n_qubits=n_qubits) return corrupted_bitstrings diff --git a/tests/qadence/test_error_models.py b/tests/qadence/test_error_models.py index 68b19805..c3dd8c56 100644 --- a/tests/qadence/test_error_models.py +++ b/tests/qadence/test_error_models.py @@ -54,11 +54,6 @@ def test_bitstring_corruption( error_probability: float, counters: list, exp_corrupted_counters: list, n_qubits: int ) -> None: - # corrupted_counters = corrupt( - # bitflip_proba=error_probability, - # counters=counters, - # n_qubits=n_qubits, - # ) corrupted_bitstrings = [ bs_corruption( bitstring=bitstring, @@ -70,7 +65,6 @@ def test_bitstring_corruption( ] corrupted_counters = [Counter(chain(*corrupted_bitstrings))] - # assert corrupted_counters[0].total() == 100 # python3.9 complains about .total in Counter assert sum(corrupted_counters[0].values()) == 100 assert corrupted_counters == exp_corrupted_counters assert torch.allclose( @@ -141,26 +135,12 @@ def test_readout_error_quantum_model( # breakpoint() for noiseless, noisy in zip(noiseless_samples, noisy_samples): assert sum(noiseless.values()) == sum(noisy.values()) == n_shots - # assert noiseless.total() == noisy.total() # python3.9 complains about .total in Counter assert js_divergence(noiseless, noisy) > 0.0 assert torch.allclose( torch.tensor(1.0 - js_divergence(noiseless, noisy)), torch.ones(1) - error_probability, atol=1e-1, ) - # print(js_divergence(noiseless, noisy)) - # assert len(noisy[0]) <= 2 ** block.n_qubits and len(noisy[0]) > len(err_free[0]) - # assert all( - # [ - # True - # if ( - # err_free[0]["bitstring"] < int(count + count * error_probability) - # or err_free[0]["bitstring"] > int(count - count * error_probability) - # ) - # else False - # for bitstring, count in noisy[0].items() - # ] - # ) @pytest.mark.parametrize("backend", [BackendName.BRAKET, BackendName.PYQTORCH, BackendName.PULSER]) @@ -176,29 +156,15 @@ def test_readout_error_backends(backend: BackendName) -> None: options = {"error_probability": error_probability} error = Errors(protocol=Errors.READOUT, options=options).get_error_fn() noisy_samples = error(counters=samples, n_qubits=n_qubits) - # breakpoint() # compare that the results are with an error of 10% (the default error_probability) for sample, noisy_sample in zip(samples, noisy_samples): assert sum(sample.values()) == sum(noisy_sample.values()) - # python3.9 complains about .total in Counter - # assert sample.total() == noisy_sample.total() assert js_divergence(sample, noisy_sample) > 0.0 assert torch.allclose( torch.tensor(1.0 - js_divergence(sample, noisy_sample)), torch.ones(1) - error_probability, atol=1e-1, ) - # assert all( - # [ - # True - # if ( - # samples[0]["bitstring"] < int(count + count * error_probability) - # or samples[0]["bitstring"] > int(count - count * error_probability) - # ) - # else False - # for bitstring, count in noisy_samples[0].items() - # ] - # ) # @pytest.mark.flaky(max_runs=5) From 23989c5df449ff126e20c51c9f5889773ebf4221 Mon Sep 17 00:00:00 2001 From: Gergana Velikova Date: Mon, 6 Nov 2023 08:54:49 +0100 Subject: [PATCH 38/83] added placeholder readmes on noisy simulations/error mitigation --- docs/realistic_sims/errors.md | 1 + docs/realistic_sims/measurements.md | 1 + docs/realistic_sims/mitigation.md | 1 + 3 files changed, 3 insertions(+) create mode 100644 docs/realistic_sims/errors.md create mode 100644 docs/realistic_sims/measurements.md create mode 100644 docs/realistic_sims/mitigation.md diff --git a/docs/realistic_sims/errors.md b/docs/realistic_sims/errors.md new file mode 100644 index 00000000..8d1c8b69 --- /dev/null +++ b/docs/realistic_sims/errors.md @@ -0,0 +1 @@ + diff --git a/docs/realistic_sims/measurements.md b/docs/realistic_sims/measurements.md new file mode 100644 index 00000000..8d1c8b69 --- /dev/null +++ b/docs/realistic_sims/measurements.md @@ -0,0 +1 @@ + diff --git a/docs/realistic_sims/mitigation.md b/docs/realistic_sims/mitigation.md new file mode 100644 index 00000000..8d1c8b69 --- /dev/null +++ b/docs/realistic_sims/mitigation.md @@ -0,0 +1 @@ + From 35f95d3f6a44ba338d6e28f047caac7261fa5e5b Mon Sep 17 00:00:00 2001 From: Gergana Velikova Date: Mon, 6 Nov 2023 09:19:10 +0100 Subject: [PATCH 39/83] adressing minor issues --- docs/realistic_sims/errors.md | 1 - docs/realistic_sims/measurements.md | 1 - docs/realistic_sims/mitigation.md | 1 - mkdocs.yml | 2 +- qadence/backend.py | 2 +- qadence/models/quantum_model.py | 1 + tests/qadence/test_error_models.py | 1 - 7 files changed, 3 insertions(+), 6 deletions(-) diff --git a/docs/realistic_sims/errors.md b/docs/realistic_sims/errors.md index 8d1c8b69..e69de29b 100644 --- a/docs/realistic_sims/errors.md +++ b/docs/realistic_sims/errors.md @@ -1 +0,0 @@ - diff --git a/docs/realistic_sims/measurements.md b/docs/realistic_sims/measurements.md index 8d1c8b69..e69de29b 100644 --- a/docs/realistic_sims/measurements.md +++ b/docs/realistic_sims/measurements.md @@ -1 +0,0 @@ - diff --git a/docs/realistic_sims/mitigation.md b/docs/realistic_sims/mitigation.md index 8d1c8b69..e69de29b 100644 --- a/docs/realistic_sims/mitigation.md +++ b/docs/realistic_sims/mitigation.md @@ -1 +0,0 @@ - diff --git a/mkdocs.yml b/mkdocs.yml index e083ab3a..3136c760 100644 --- a/mkdocs.yml +++ b/mkdocs.yml @@ -40,7 +40,7 @@ nav: - Realistic simulations: - realistic_sims/index.md - Measurement protocols: realistic_sims/measurements.md - - Error models: realistic_sims/errors.md + - Simulated errors: realistic_sims/errors.md - Error mitigation: realistic_sims/mitigation.md - API: diff --git a/qadence/backend.py b/qadence/backend.py index 765230fd..cbfb8317 100644 --- a/qadence/backend.py +++ b/qadence/backend.py @@ -26,7 +26,7 @@ from qadence.parameters import stringify from qadence.types import BackendName, DiffMode, Endianness -logger = get_logger(__name__) +logger = get_logger(__file__) @dataclass diff --git a/qadence/models/quantum_model.py b/qadence/models/quantum_model.py index 5de7fca2..1bde7db6 100644 --- a/qadence/models/quantum_model.py +++ b/qadence/models/quantum_model.py @@ -64,6 +64,7 @@ def __init__( measurement: Optional measurement protocol. If None, use exact expectation value with a statevector simulator. configuration: Configuration for the backend. + error: A noise model to use. Raises: ValueError: if the `diff_mode` argument is set to None diff --git a/tests/qadence/test_error_models.py b/tests/qadence/test_error_models.py index c3dd8c56..199ff9c9 100644 --- a/tests/qadence/test_error_models.py +++ b/tests/qadence/test_error_models.py @@ -167,7 +167,6 @@ def test_readout_error_backends(backend: BackendName) -> None: ) -# @pytest.mark.flaky(max_runs=5) @pytest.mark.parametrize( "measurement_proto, options", [ From 83257deb49fb179dc6d73caadaef9887d7ece777 Mon Sep 17 00:00:00 2001 From: Gergana Velikova Date: Mon, 6 Nov 2023 10:25:26 +0100 Subject: [PATCH 40/83] placeholder index.md --- docs/realistic_sims/index.md | 1 + 1 file changed, 1 insertion(+) create mode 100644 docs/realistic_sims/index.md diff --git a/docs/realistic_sims/index.md b/docs/realistic_sims/index.md new file mode 100644 index 00000000..8d1c8b69 --- /dev/null +++ b/docs/realistic_sims/index.md @@ -0,0 +1 @@ + From 4538497d5f520d5890d8b734c5e5298b902ec5ce Mon Sep 17 00:00:00 2001 From: Gergana Velikova Date: Mon, 6 Nov 2023 16:25:57 +0100 Subject: [PATCH 41/83] some cleaning up --- docs/realistic_sims/index.md | 1 - qadence/backends/braket/backend.py | 7 +- qadence/backends/pulser/backend.py | 7 +- qadence/backends/pyqtorch/backend.py | 5 +- qadence/errors/readout.py | 107 ++++++++++++++++----------- tests/qadence/test_error_models.py | 33 +++++---- 6 files changed, 101 insertions(+), 59 deletions(-) diff --git a/docs/realistic_sims/index.md b/docs/realistic_sims/index.md index 8d1c8b69..e69de29b 100644 --- a/docs/realistic_sims/index.md +++ b/docs/realistic_sims/index.md @@ -1 +0,0 @@ - diff --git a/qadence/backends/braket/backend.py b/qadence/backends/braket/backend.py index c823eefb..deac338e 100644 --- a/qadence/backends/braket/backend.py +++ b/qadence/backends/braket/backend.py @@ -157,7 +157,12 @@ def sample( samples = invert_endianness(samples) if error is not None: error_fn = error.get_error_fn() - return error_fn(counters=samples, n_qubits=circuit.abstract.n_qubits) # type: ignore + return error_fn( # type: ignore[no-any-return] + counters=samples, + n_qubits=circuit.abstract.n_qubits, + options=error.options, + n_shots=n_shots, + ) else: return samples diff --git a/qadence/backends/pulser/backend.py b/qadence/backends/pulser/backend.py index 958622c9..91a7d900 100644 --- a/qadence/backends/pulser/backend.py +++ b/qadence/backends/pulser/backend.py @@ -269,7 +269,12 @@ def sample( samples = invert_endianness(samples) if error is not None: error_fn = error.get_error_fn() - return error_fn(counters=samples, n_qubits=circuit.abstract.n_qubits) # type: ignore + return error_fn( # type: ignore[no-any-return] + counters=samples, + n_qubits=circuit.abstract.n_qubits, + options=error.options, + n_shots=n_shots, + ) else: return samples diff --git a/qadence/backends/pyqtorch/backend.py b/qadence/backends/pyqtorch/backend.py index 39375063..cc84660e 100644 --- a/qadence/backends/pyqtorch/backend.py +++ b/qadence/backends/pyqtorch/backend.py @@ -240,7 +240,10 @@ def _sample(_probs: Tensor, n_shots: int, endianness: Endianness, n_qubits: int) if error is not None: error_fn = error.get_error_fn() corrupted_counters: list = error_fn( - counters=counters, n_qubits=circuit.abstract.n_qubits, options=error.options + counters=counters, + n_qubits=circuit.abstract.n_qubits, + options=error.options, + n_shots=n_shots, ) return corrupted_counters else: diff --git a/qadence/errors/readout.py b/qadence/errors/readout.py index 75bd0ad2..9018bc37 100644 --- a/qadence/errors/readout.py +++ b/qadence/errors/readout.py @@ -28,12 +28,12 @@ class WhiteNoise(Enum): def bitstring_to_array(bitstring: str) -> np.array: """A helper function to convert bit strings to numpy arrays.""" - return np.array([int(i) for i in bitstring]) + return np.array(list(bitstring)).astype(int) def array_to_bitstring(bitstring: np.array) -> str: """A helper function to convert numpy arrays to bit strings.""" - return "".join(([str(i) for i in bitstring])) + return "".join(bitstring.astype("str")) def bit_flip(qubit: int) -> int: @@ -41,37 +41,54 @@ def bit_flip(qubit: int) -> int: return 1 if qubit == 0 else 0 -def bs_corruption( - bitstring: str, - n_shots: int, - error_probability: float, - n_qubits: int, - noise_distribution: Enum = WhiteNoise.UNIFORM, -) -> list: - # the noise_matrix should be available to the user if they want to do error correction - noise_matrix = noise_distribution.sample([n_shots, n_qubits]) # type: ignore[attr-defined] +def sample_to_matrix(sample: dict) -> np.array: + """A helper function that maps a sample dict to a bit string array.""" + return np.array( + [ + i + for i in chain.from_iterable( + [sample[bitstring] * [bitstring_to_array(bitstring)] for bitstring in sample.keys()] + ) + ] + ) - # the simplest approach - en event occurs if its probability is higher than expected - # by random chance - err_idx = [(item) for i, item in enumerate(noise_matrix < error_probability) if any(item)] - def func_distort(idx: tuple) -> str: - bitstring_copy = bitstring_to_array(bitstring) - for id in range(n_qubits): - if idx[id]: - bitstring_copy[id] = bit_flip(bitstring_copy[id]) - return array_to_bitstring(bitstring_copy) +def create_noise_matrix( + noise_distribution: torch.distributions, n_shots: int, n_qubits: int +) -> np.array: + """A helper function that creates a noise matrix for bit string corruption. + NB: The noise matrix is not square, as all bits are considered independent. + """ + # the noise_matrix should be available to the user if they want to do error correction + return noise_distribution.sample([n_shots, n_qubits]) + - all_bitstrings = [func_distort(idx) for idx in err_idx] - all_bitstrings.extend( - [bitstring] * (n_shots - len(all_bitstrings)) - ) # add the error-free bit strings - return all_bitstrings +def bs_corruption( + n_shots: int, + n_qubits: int, + err_idx: list, + sample: np.array, +) -> Counter: + all_bitstrings = [] + for i in range(n_shots): + all_bitstrings.append( + [ + bit_flip(sample[i, n]) + if err_idx[i, n] # type: ignore[call-overload] + else sample[i, n] + for n in range(n_qubits) + ] + ) + all_bitstrings = np.array(all_bitstrings) + return Counter( + ["".join(i) for i in all_bitstrings.astype(str).tolist()] # type: ignore[attr-defined] + ) def error( counters: list[Counter], n_qubits: int, + n_shots: int = 1000, options: dict = dict(), ) -> list[Counter]: """ @@ -80,7 +97,8 @@ def error( Args: counters: Samples of bit string as Counters. - n_qubits: Number of shots to sample. + n_qubits: Number of qubits in the bit string. + n_shots: Number of shots to sample. seed: Random seed value if any. error_probability: Uniform error probability of wrong readout at any position in the bit strings. @@ -90,31 +108,36 @@ def error( Samples of corrupted bit strings as list[Counter]. """ - seed = options.get("seed") + seed = options.get("seed", None) error_probability = options.get("error_probability", 0.1) noise_distribution = options.get("noise_distribution", WhiteNoise.UNIFORM) + noise_matrix = options.get("noise_matrix") # option for reproducibility if seed is not None: torch.manual_seed(seed) + if noise_matrix is None: + # assumes that all bits can be flipped independently of each other + noise_matrix = create_noise_matrix(noise_distribution, n_shots, n_qubits) + else: + # check noise_matrix shape and values + assert ( + noise_matrix.shape[0] == noise_matrix.shape[1] + ), "The error probabilities matrix needs to be square." + assert noise_matrix.shape == ( + 2**n_qubits, + 2**n_qubits, + ), "The error probabilities matrix needs to be 2 ^ n_qubits x 2 ^ n_qubits." + + # the simplest approach - en event occurs if its probability is higher than expected + # by random chance + err_idx = np.array([(item).numpy() for i, item in enumerate(noise_matrix < error_probability)]) + corrupted_bitstrings = [] for counter in counters: + sample = sample_to_matrix(counter) corrupted_bitstrings.append( - Counter( - chain( - *[ - bs_corruption( - bitstring=bitstring, - n_shots=n_shots, - error_probability=error_probability, - noise_distribution=noise_distribution, - n_qubits=n_qubits, - ) - for bitstring, n_shots in counter.items() - ] - ) - ) + bs_corruption(n_shots=n_shots, err_idx=err_idx, sample=sample, n_qubits=n_qubits) ) - return corrupted_bitstrings diff --git a/tests/qadence/test_error_models.py b/tests/qadence/test_error_models.py index 199ff9c9..f70db89f 100644 --- a/tests/qadence/test_error_models.py +++ b/tests/qadence/test_error_models.py @@ -1,8 +1,8 @@ from __future__ import annotations from collections import Counter -from itertools import chain +import numpy as np import pytest import torch from sympy import acos @@ -18,7 +18,7 @@ from qadence.constructors.hamiltonians import hamiltonian_factory from qadence.divergences import js_divergence from qadence.errors import Errors -from qadence.errors.readout import bs_corruption +from qadence.errors.readout import WhiteNoise, bs_corruption, create_noise_matrix, sample_to_matrix from qadence.measurements.protocols import Measurements from qadence.models import QuantumModel from qadence.operations import ( @@ -54,18 +54,25 @@ def test_bitstring_corruption( error_probability: float, counters: list, exp_corrupted_counters: list, n_qubits: int ) -> None: - corrupted_bitstrings = [ - bs_corruption( - bitstring=bitstring, - n_shots=n_shots, - error_probability=error_probability, - n_qubits=n_qubits, - ) - for bitstring, n_shots in counters[0].items() + # corrupted_bitstrings = [ + # bs_corruption( + # bitstring=bitstring, + # n_shots=n_shots, + # error_probability=error_probability, + # n_qubits=n_qubits, + # ) + # for bitstring, n_shots in counters[0].items() + # ] + + # corrupted_counters = [Counter(chain(*corrupted_bitstrings))] + n_shots = 100 + noise_matrix = create_noise_matrix(WhiteNoise.UNIFORM, n_shots, n_qubits) + err_idx = np.array([(item).numpy() for i, item in enumerate(noise_matrix < error_probability)]) + sample = sample_to_matrix(counters[0]) + corrupted_counters = [ + bs_corruption(n_shots=n_shots, err_idx=err_idx, sample=sample, n_qubits=n_qubits) ] - - corrupted_counters = [Counter(chain(*corrupted_bitstrings))] - assert sum(corrupted_counters[0].values()) == 100 + assert sum(corrupted_counters[0].values()) == n_shots assert corrupted_counters == exp_corrupted_counters assert torch.allclose( torch.tensor(1.0 - js_divergence(corrupted_counters[0], counters[0])), From e8e27736c41eeccbfff261993d0602b271b7063d Mon Sep 17 00:00:00 2001 From: Roland Guichard Date: Mon, 6 Nov 2023 15:41:37 +0000 Subject: [PATCH 42/83] Add doc files. --- docs/realistic_sims/errors.md | 1 + docs/realistic_sims/index.md | 1 + docs/realistic_sims/measurements.md | 1 + docs/realistic_sims/mitigation.md | 1 + 4 files changed, 4 insertions(+) create mode 100644 docs/realistic_sims/index.md diff --git a/docs/realistic_sims/errors.md b/docs/realistic_sims/errors.md index e69de29b..6c71c088 100644 --- a/docs/realistic_sims/errors.md +++ b/docs/realistic_sims/errors.md @@ -0,0 +1 @@ +This section introduces error models. diff --git a/docs/realistic_sims/index.md b/docs/realistic_sims/index.md new file mode 100644 index 00000000..19f48402 --- /dev/null +++ b/docs/realistic_sims/index.md @@ -0,0 +1 @@ +This section describes how to perform realistic simulations in Qadence. diff --git a/docs/realistic_sims/measurements.md b/docs/realistic_sims/measurements.md index e69de29b..d039d48e 100644 --- a/docs/realistic_sims/measurements.md +++ b/docs/realistic_sims/measurements.md @@ -0,0 +1 @@ +This section introduces the various measurement protocols. diff --git a/docs/realistic_sims/mitigation.md b/docs/realistic_sims/mitigation.md index e69de29b..9adb3e06 100644 --- a/docs/realistic_sims/mitigation.md +++ b/docs/realistic_sims/mitigation.md @@ -0,0 +1 @@ +This section introduces mitigation protocols. From 64b7a0d08e2daa4c9d549ef1431f9814361ba391 Mon Sep 17 00:00:00 2001 From: Gergana Velikova Date: Mon, 6 Nov 2023 21:04:39 +0100 Subject: [PATCH 43/83] simplify corruption function --- qadence/errors/readout.py | 34 +++++++++++++++++++--------------- 1 file changed, 19 insertions(+), 15 deletions(-) diff --git a/qadence/errors/readout.py b/qadence/errors/readout.py index 9018bc37..addd5cf2 100644 --- a/qadence/errors/readout.py +++ b/qadence/errors/readout.py @@ -69,19 +69,23 @@ def bs_corruption( err_idx: list, sample: np.array, ) -> Counter: - all_bitstrings = [] - for i in range(n_shots): - all_bitstrings.append( - [ - bit_flip(sample[i, n]) - if err_idx[i, n] # type: ignore[call-overload] - else sample[i, n] - for n in range(n_qubits) - ] - ) - all_bitstrings = np.array(all_bitstrings) + + def vflip(sample: int, err_idx: bool, idx: int) -> np.array: + if err_idx: + return bit_flip(sample) + else: + return sample + + vbit_ind = np.vectorize(vflip) + return Counter( - ["".join(i) for i in all_bitstrings.astype(str).tolist()] # type: ignore[attr-defined] + [ + "".join(k) + for k in map( + lambda i: vbit_ind(sample[i], err_idx[i], range(n_qubits)).astype(str).tolist(), + range(n_shots), + ) + ] ) @@ -126,9 +130,9 @@ def error( noise_matrix.shape[0] == noise_matrix.shape[1] ), "The error probabilities matrix needs to be square." assert noise_matrix.shape == ( - 2**n_qubits, - 2**n_qubits, - ), "The error probabilities matrix needs to be 2 ^ n_qubits x 2 ^ n_qubits." + n_qubits, + n_qubits, + ), "The error probabilities matrix needs to be n_qubits x n_qubits." # the simplest approach - en event occurs if its probability is higher than expected # by random chance From 1d170c852a2b12ba945e01b484fefdb20a1d75c3 Mon Sep 17 00:00:00 2001 From: Gergana Velikova Date: Mon, 6 Nov 2023 21:21:38 +0100 Subject: [PATCH 44/83] improve on docstrings --- qadence/errors/readout.py | 71 ++++++++++++++++++++++++++++++++++----- 1 file changed, 63 insertions(+), 8 deletions(-) diff --git a/qadence/errors/readout.py b/qadence/errors/readout.py index addd5cf2..51db4608 100644 --- a/qadence/errors/readout.py +++ b/qadence/errors/readout.py @@ -27,22 +27,54 @@ class WhiteNoise(Enum): def bitstring_to_array(bitstring: str) -> np.array: - """A helper function to convert bit strings to numpy arrays.""" + """ + A helper function to convert bit strings to numpy arrays. + + Args: + bitstring: A str format of a bit string. + + Returns: + A numpy array out of the input bit string. + """ return np.array(list(bitstring)).astype(int) def array_to_bitstring(bitstring: np.array) -> str: - """A helper function to convert numpy arrays to bit strings.""" + """ + A helper function to convert numpy arrays to bit strings. + + Args: + bitstring: A numpy array format of a bit string. + + Returns: + A str out of the input bit string. + """ return "".join(bitstring.astype("str")) -def bit_flip(qubit: int) -> int: - """A helper function that reverses the states 0 and 1 in the bit string.""" - return 1 if qubit == 0 else 0 +def bit_flip(bit: int) -> int: + """ + A helper function that reverses the states 0 and 1 in the bit string. + + Args: + bit: A integer-value bit in a bitstring to be inverted. + + Returns: + The inverse value of the input bit + """ + return 1 if bit == 0 else 0 def sample_to_matrix(sample: dict) -> np.array: - """A helper function that maps a sample dict to a bit string array.""" + """ + A helper function that maps a sample dict to a bit string array. + + Args: + sample: A dictionary with bit stings as keys and values + as their counts. + + Returns: A numpy array (matrix) of bit strings n_shots x n_qubits. + """ return np.array( [ i @@ -56,8 +88,18 @@ def sample_to_matrix(sample: dict) -> np.array: def create_noise_matrix( noise_distribution: torch.distributions, n_shots: int, n_qubits: int ) -> np.array: - """A helper function that creates a noise matrix for bit string corruption. + """ + A helper function that creates a noise matrix for bit string corruption. NB: The noise matrix is not square, as all bits are considered independent. + + Args: + noise_distribution: Torch statistical distribution one of Gaussian, + Uniform, of Poisson. + n_shots: Number of shots/samples. + n_qubits: Number of qubits + + Returns: + A sample out of the requested distribution given the number of shots/samples. """ # the noise_matrix should be available to the user if they want to do error correction return noise_distribution.sample([n_shots, n_qubits]) @@ -69,6 +111,19 @@ def bs_corruption( err_idx: list, sample: np.array, ) -> Counter: + """ + A function that incorporates the expected readout error in a sample of bit strings + given a noise matrix. + + Args: + n_qubits: Number of qubits in the bit string. + n_shots: Number of shots to sample. + err_idx: A Boolean array of bit string indices that need to be corrupted. + sample: A matrix of bit strings n_shots x n_qubits. + + Returns: + A counter of bit strings after readout corruption. + """ def vflip(sample: int, err_idx: bool, idx: int) -> np.array: if err_idx: @@ -134,7 +189,7 @@ def error( n_qubits, ), "The error probabilities matrix needs to be n_qubits x n_qubits." - # the simplest approach - en event occurs if its probability is higher than expected + # the simplest approach - an event occurs if its probability is higher than expected # by random chance err_idx = np.array([(item).numpy() for i, item in enumerate(noise_matrix < error_probability)]) From bd0d046d801823ae3cc92e80a504b76e0ccc1b9a Mon Sep 17 00:00:00 2001 From: Gergana Velikova Date: Mon, 6 Nov 2023 22:08:09 +0100 Subject: [PATCH 45/83] renamed errors to noise --- mkdocs.yml | 2 +- qadence/__init__.py | 4 ++-- qadence/backend.py | 6 +++--- qadence/backends/braket/backend.py | 8 ++++---- qadence/backends/pulser/backend.py | 8 ++++---- qadence/backends/pyqtorch/backend.py | 12 ++++++------ qadence/backends/pytorch_wrapper.py | 8 ++++---- qadence/execution.py | 10 +++++----- qadence/measurements/shadow.py | 8 ++++---- qadence/measurements/tomography.py | 6 +++--- qadence/models/quantum_model.py | 8 ++++---- qadence/{errors => noise}/__init__.py | 4 ++-- qadence/{errors => noise}/protocols.py | 6 +++--- qadence/{errors => noise}/readout.py | 0 tests/qadence/test_error_models.py | 12 ++++++------ 15 files changed, 51 insertions(+), 51 deletions(-) rename qadence/{errors => noise}/__init__.py (65%) rename qadence/{errors => noise}/protocols.py (89%) rename qadence/{errors => noise}/readout.py (100%) diff --git a/mkdocs.yml b/mkdocs.yml index 3136c760..dfa615f8 100644 --- a/mkdocs.yml +++ b/mkdocs.yml @@ -40,7 +40,7 @@ nav: - Realistic simulations: - realistic_sims/index.md - Measurement protocols: realistic_sims/measurements.md - - Simulated errors: realistic_sims/errors.md + - Simulated errors: realistic_sims/noise.md - Error mitigation: realistic_sims/mitigation.md - API: diff --git a/qadence/__init__.py b/qadence/__init__.py index 9b6eba99..ea1cacdc 100644 --- a/qadence/__init__.py +++ b/qadence/__init__.py @@ -10,12 +10,12 @@ from .blocks import * from .circuit import * from .constructors import * -from .errors import * from .exceptions import * from .execution import * from .measurements import * from .ml_tools import * from .models import * +from .noise import * from .operations import * from .overlap import * from .parameters import * @@ -44,7 +44,7 @@ ".blocks", ".circuit", ".constructors", - ".errors", + ".noise", ".exceptions", ".execution", ".measurements", diff --git a/qadence/backend.py b/qadence/backend.py index cbfb8317..15329b39 100644 --- a/qadence/backend.py +++ b/qadence/backend.py @@ -20,9 +20,9 @@ ) from qadence.blocks.analog import ConstantAnalogRotation, WaitBlock from qadence.circuit import QuantumCircuit -from qadence.errors import Errors from qadence.logger import get_logger from qadence.measurements import Measurements +from qadence.noise import Noise from qadence.parameters import stringify from qadence.types import BackendName, DiffMode, Endianness @@ -229,7 +229,7 @@ def sample( param_values: dict[str, Tensor] = {}, n_shots: int = 1000, state: Tensor | None = None, - error: Errors | None = None, + error: Noise | None = None, endianness: Endianness = Endianness.BIG, ) -> list[Counter]: """Sample bit strings. @@ -276,7 +276,7 @@ def expectation( param_values: dict[str, Tensor] = {}, state: Tensor | None = None, measurement: Measurements | None = None, - error: Errors | None = None, + error: Noise | None = None, endianness: Endianness = Endianness.BIG, ) -> Tensor: """Compute the expectation value of the `circuit` with the given `observable`. diff --git a/qadence/backends/braket/backend.py b/qadence/backends/braket/backend.py index deac338e..f17b081a 100644 --- a/qadence/backends/braket/backend.py +++ b/qadence/backends/braket/backend.py @@ -15,9 +15,9 @@ from qadence.backends.utils import to_list_of_dicts from qadence.blocks import AbstractBlock, block_to_tensor from qadence.circuit import QuantumCircuit -from qadence.errors import Errors from qadence.logger import get_logger from qadence.measurements import Measurements +from qadence.noise import Noise from qadence.overlap import overlap_exact from qadence.transpile import transpile from qadence.utils import Endianness @@ -130,7 +130,7 @@ def sample( param_values: dict[str, Tensor] = {}, n_shots: int = 1, state: Tensor | None = None, - error: Errors | None = None, + error: Noise | None = None, endianness: Endianness = Endianness.BIG, ) -> list[Counter]: """Execute the circuit and return samples of the resulting wavefunction.""" @@ -173,12 +173,12 @@ def expectation( param_values: dict[str, Tensor] = {}, state: Tensor | None = None, measurement: Measurements | None = None, - error: Errors | None = None, + error: Noise | None = None, endianness: Endianness = Endianness.BIG, ) -> Tensor: if error is not None: logger.warning( - f"Errors of type {error} are not implemented for expectation yet. " + f"Noise of type {error} are not implemented for expectation yet. " "This is ignored for now." ) # Do not flip endianness here because then we would have to reverse the observable diff --git a/qadence/backends/pulser/backend.py b/qadence/backends/pulser/backend.py index 91a7d900..c845c41d 100644 --- a/qadence/backends/pulser/backend.py +++ b/qadence/backends/pulser/backend.py @@ -19,9 +19,9 @@ from qadence.backends.utils import to_list_of_dicts from qadence.blocks import AbstractBlock from qadence.circuit import QuantumCircuit -from qadence.errors import Errors from qadence.logger import get_logger from qadence.measurements import Measurements +from qadence.noise import Noise from qadence.overlap import overlap_exact from qadence.register import Register from qadence.transpile import transpile @@ -249,7 +249,7 @@ def sample( param_values: dict[str, Tensor] = {}, n_shots: int = 1, state: Tensor | None = None, - error: Errors | None = None, + error: Noise | None = None, endianness: Endianness = Endianness.BIG, ) -> list[Counter]: if n_shots < 1: @@ -285,12 +285,12 @@ def expectation( param_values: dict[str, Tensor] = {}, state: Tensor | None = None, measurement: Measurements | None = None, - error: Errors | None = None, + error: Noise | None = None, endianness: Endianness = Endianness.BIG, ) -> Tensor: if error is not None: logger.warning( - f"Errors of type {error} are not implemented for expectation yet. " + f"Noise of type {error} are not implemented for expectation yet. " "This is ignored for now." ) diff --git a/qadence/backends/pyqtorch/backend.py b/qadence/backends/pyqtorch/backend.py index cc84660e..b3301cff 100644 --- a/qadence/backends/pyqtorch/backend.py +++ b/qadence/backends/pyqtorch/backend.py @@ -14,9 +14,9 @@ from qadence.backends.utils import to_list_of_dicts from qadence.blocks import AbstractBlock from qadence.circuit import QuantumCircuit -from qadence.errors import Errors from qadence.logger import get_logger from qadence.measurements import Measurements +from qadence.noise import Noise from qadence.overlap import overlap_exact from qadence.states import zero_state from qadence.transpile import ( @@ -128,7 +128,7 @@ def _batched_expectation( param_values: dict[str, Tensor] = {}, state: Tensor | None = None, measurement: Measurements | None = None, - error: Errors | None = None, + error: Noise | None = None, endianness: Endianness = Endianness.BIG, ) -> Tensor: state = self.run( @@ -153,7 +153,7 @@ def _looped_expectation( param_values: dict[str, Tensor] = {}, state: Tensor | None = None, measurement: Measurements | None = None, - error: Errors | None = None, + error: Noise | None = None, endianness: Endianness = Endianness.BIG, ) -> Tensor: state = zero_state(circuit.abstract.n_qubits, batch_size=1) if state is None else state @@ -180,12 +180,12 @@ def expectation( param_values: dict[str, Tensor] = {}, state: Tensor | None = None, measurement: Measurements | None = None, - error: Errors | None = None, + error: Noise | None = None, endianness: Endianness = Endianness.BIG, ) -> Tensor: if error is not None: logger.warning( - f"Errors of type {error} are not implemented for expectation yet. " + f"Noise of type {error} are not implemented for expectation yet. " "This is ignored for now." ) fn = self._looped_expectation if self.config.loop_expectation else self._batched_expectation @@ -205,7 +205,7 @@ def sample( param_values: dict[str, Tensor] = {}, n_shots: int = 1, state: Tensor | None = None, - error: Errors | None = None, + error: Noise | None = None, endianness: Endianness = Endianness.BIG, ) -> list[Counter]: if n_shots < 1: diff --git a/qadence/backends/pytorch_wrapper.py b/qadence/backends/pytorch_wrapper.py index f010c7b0..5c6df878 100644 --- a/qadence/backends/pytorch_wrapper.py +++ b/qadence/backends/pytorch_wrapper.py @@ -15,10 +15,10 @@ from qadence.blocks import AbstractBlock, PrimitiveBlock from qadence.blocks.utils import uuid_to_block, uuid_to_eigen from qadence.circuit import QuantumCircuit -from qadence.errors import Errors from qadence.extensions import get_gpsr_fns from qadence.measurements import Measurements from qadence.ml_tools import promote_to_tensor +from qadence.noise import Noise from qadence.types import DiffMode, Endianness @@ -86,7 +86,7 @@ class DifferentiableExpectation: param_values: dict[str, Tensor] state: Tensor | None = None measurement: Measurements | None = None - error: Errors | None = None + error: Noise | None = None endianness: Endianness = Endianness.BIG def ad(self) -> Tensor: @@ -236,7 +236,7 @@ def expectation( param_values: dict[str, Tensor] = {}, state: Tensor | None = None, measurement: Measurements | None = None, - error: Errors | None = None, + error: Noise | None = None, endianness: Endianness = Endianness.BIG, ) -> Tensor: """Compute the expectation value of a given observable. @@ -281,7 +281,7 @@ def sample( param_values: dict[str, Tensor], n_shots: int = 1, state: Tensor | None = None, - error: Errors | None = None, + error: Noise | None = None, endianness: Endianness = Endianness.BIG, ) -> list[Counter]: """Sample bitstring from the registered circuit. diff --git a/qadence/execution.py b/qadence/execution.py index 45075382..d0d20d65 100644 --- a/qadence/execution.py +++ b/qadence/execution.py @@ -10,7 +10,7 @@ from qadence.backend import BackendConfiguration, BackendName from qadence.blocks import AbstractBlock from qadence.circuit import QuantumCircuit -from qadence.errors import Errors +from qadence.noise import Noise from qadence.qubit_support import QubitSupport from qadence.register import Register from qadence.types import DiffMode @@ -106,7 +106,7 @@ def sample( n_shots: int = 100, backend: BackendName = BackendName.PYQTORCH, endianness: Endianness = Endianness.BIG, - error: Union[Errors, None] = None, + error: Union[Noise, None] = None, configuration: Union[BackendConfiguration, dict, None] = None, ) -> list[Counter]: """Convenience wrapper for the `QuantumModel.sample` method. This is a @@ -137,7 +137,7 @@ def _( state: Union[Tensor, None] = None, n_shots: int = 100, backend: BackendName = BackendName.PYQTORCH, - error: Union[Errors, None] = None, + error: Union[Noise, None] = None, endianness: Endianness = Endianness.BIG, configuration: Union[BackendConfiguration, dict, None] = None, ) -> list[Counter]: @@ -177,7 +177,7 @@ def expectation( state: Tensor = None, backend: BackendName = BackendName.PYQTORCH, diff_mode: Union[DiffMode, str, None] = None, - error: Union[Errors, None] = None, + error: Union[Noise, None] = None, endianness: Endianness = Endianness.BIG, configuration: Union[BackendConfiguration, dict, None] = None, ) -> Tensor: @@ -231,7 +231,7 @@ def _( state: Tensor = None, backend: BackendName = BackendName.PYQTORCH, diff_mode: Union[DiffMode, str, None] = None, - error: Union[Errors, None] = None, + error: Union[Noise, None] = None, endianness: Endianness = Endianness.BIG, configuration: Union[BackendConfiguration, dict, None] = None, ) -> Tensor: diff --git a/qadence/measurements/shadow.py b/qadence/measurements/shadow.py index 92a0c4fd..fca7d361 100644 --- a/qadence/measurements/shadow.py +++ b/qadence/measurements/shadow.py @@ -21,7 +21,7 @@ from qadence.blocks.primitive import PrimitiveBlock from qadence.blocks.utils import get_pauli_blocks, unroll_block_with_scaling from qadence.circuit import QuantumCircuit -from qadence.errors import Errors +from qadence.noise import Noise from qadence.operations import X, Y, Z, chain, kron from qadence.states import one_state, zero_state from qadence.types import Endianness @@ -129,7 +129,7 @@ def classical_shadow( state: Tensor | None = None, backend_name: BackendName = BackendName.PYQTORCH, # FIXME: Changed below from Little to Big, double-check when Roland is back - error: Errors | None = None, + error: Noise | None = None, endianness: Endianness = Endianness.BIG, ) -> list: shadow: list = [] @@ -260,7 +260,7 @@ def estimations( confidence: float = 0.1, state: Tensor | None = None, backend_name: BackendName = BackendName.PYQTORCH, - error: Errors | None = None, + error: Noise | None = None, endianness: Endianness = Endianness.BIG, ) -> Tensor: """Compute expectation values for all local observables using median of means.""" @@ -313,7 +313,7 @@ def compute_expectation( options: dict, state: Tensor | None = None, backend_name: BackendName = BackendName.PYQTORCH, - error: Errors | None = None, + error: Noise | None = None, endianness: Endianness = Endianness.BIG, ) -> Tensor: """ diff --git a/qadence/measurements/tomography.py b/qadence/measurements/tomography.py index b4de9256..a8855004 100644 --- a/qadence/measurements/tomography.py +++ b/qadence/measurements/tomography.py @@ -12,7 +12,7 @@ from qadence.blocks import AbstractBlock, PrimitiveBlock from qadence.blocks.utils import unroll_block_with_scaling from qadence.circuit import QuantumCircuit -from qadence.errors import Errors +from qadence.noise import Noise from qadence.operations import H, SDagger, X, Y, Z, chain from qadence.parameters import evaluate from qadence.utils import Endianness @@ -84,7 +84,7 @@ def iterate_pauli_decomposition( n_shots: int, state: Tensor | None = None, backend_name: BackendName = BackendName.PYQTORCH, - error: Errors | None = None, + error: Noise | None = None, endianness: Endianness = Endianness.BIG, ) -> Tensor: """Estimate total expectation value by averaging all Pauli terms.""" @@ -131,7 +131,7 @@ def compute_expectation( options: dict, state: Tensor | None = None, backend_name: BackendName = BackendName.PYQTORCH, - error: Errors | None = None, + error: Noise | None = None, endianness: Endianness = Endianness.BIG, ) -> Tensor: """Basic tomography protocol with rotations diff --git a/qadence/models/quantum_model.py b/qadence/models/quantum_model.py index 1bde7db6..58fd359e 100644 --- a/qadence/models/quantum_model.py +++ b/qadence/models/quantum_model.py @@ -20,9 +20,9 @@ from qadence.backends.pytorch_wrapper import DiffMode from qadence.blocks import AbstractBlock from qadence.circuit import QuantumCircuit -from qadence.errors import Errors from qadence.logger import get_logger from qadence.measurements import Measurements +from qadence.noise import Noise from qadence.utils import Endianness logger = get_logger(__name__) @@ -50,7 +50,7 @@ def __init__( diff_mode: DiffMode = DiffMode.AD, measurement: Measurements | None = None, configuration: BackendConfiguration | dict | None = None, - error: Errors | None = None, + error: Noise | None = None, ): """Initialize a generic QuantumModel instance. @@ -166,7 +166,7 @@ def sample( values: dict[str, torch.Tensor] = {}, n_shots: int = 1000, state: torch.Tensor | None = None, - error: Errors | None = None, + error: Noise | None = None, endianness: Endianness = Endianness.BIG, ) -> list[Counter]: params = self.embedding_fn(self._params, values) @@ -182,7 +182,7 @@ def expectation( observable: list[ConvertedObservable] | ConvertedObservable | None = None, state: Optional[Tensor] = None, measurement: Measurements | None = None, - error: Errors | None = None, + error: Noise | None = None, endianness: Endianness = Endianness.BIG, ) -> Tensor: """Compute expectation using the given backend. diff --git a/qadence/errors/__init__.py b/qadence/noise/__init__.py similarity index 65% rename from qadence/errors/__init__.py rename to qadence/noise/__init__.py index 8044d360..66c19260 100644 --- a/qadence/errors/__init__.py +++ b/qadence/noise/__init__.py @@ -1,6 +1,6 @@ from __future__ import annotations -from .protocols import Errors +from .protocols import Noise # Modules to be automatically added to the qadence namespace -__all__ = ["Errors"] +__all__ = ["Noise"] diff --git a/qadence/errors/protocols.py b/qadence/noise/protocols.py similarity index 89% rename from qadence/errors/protocols.py rename to qadence/noise/protocols.py index a9d4a6b3..ea848291 100644 --- a/qadence/errors/protocols.py +++ b/qadence/noise/protocols.py @@ -5,13 +5,13 @@ from typing import Callable, cast PROTOCOL_TO_MODULE = { - "readout": "qadence.errors.readout", + "readout": "qadence.noise.readout", } # TODO: make this a StrEnum to keep consistency with the rest of the interface @dataclass -class Errors: +class Noise: READOUT = "readout" def __init__(self, protocol: str, options: dict = dict()) -> None: @@ -30,7 +30,7 @@ def _to_dict(self) -> dict: return {"protocol": self.protocol, "options": self.options} @classmethod - def _from_dict(cls, d: dict) -> Errors | None: + def _from_dict(cls, d: dict) -> Noise | None: if d: return cls(d["protocol"], **d["options"]) return None diff --git a/qadence/errors/readout.py b/qadence/noise/readout.py similarity index 100% rename from qadence/errors/readout.py rename to qadence/noise/readout.py diff --git a/tests/qadence/test_error_models.py b/tests/qadence/test_error_models.py index f70db89f..97a4c4a0 100644 --- a/tests/qadence/test_error_models.py +++ b/tests/qadence/test_error_models.py @@ -17,10 +17,10 @@ from qadence.circuit import QuantumCircuit from qadence.constructors.hamiltonians import hamiltonian_factory from qadence.divergences import js_divergence -from qadence.errors import Errors -from qadence.errors.readout import WhiteNoise, bs_corruption, create_noise_matrix, sample_to_matrix from qadence.measurements.protocols import Measurements from qadence.models import QuantumModel +from qadence.noise import Noise +from qadence.noise.readout import WhiteNoise, bs_corruption, create_noise_matrix, sample_to_matrix from qadence.operations import ( CNOT, RX, @@ -137,7 +137,7 @@ def test_readout_error_quantum_model( noisy_samples: list[Counter] = QuantumModel( QuantumCircuit(block.n_qubits, block), backend=backend, diff_mode=diff_mode - ).sample(error=Errors(protocol=Errors.READOUT), n_shots=n_shots) + ).sample(error=Noise(protocol=Noise.READOUT), n_shots=n_shots) # breakpoint() for noiseless, noisy in zip(noiseless_samples, noisy_samples): @@ -159,9 +159,9 @@ def test_readout_error_backends(backend: BackendName) -> None: inputs = {"phi": torch.rand(1)} # sample samples = qd.sample(feature_map, n_shots=1000, values=inputs, backend=backend, error=None) - # introduce errors + # introduce noise options = {"error_probability": error_probability} - error = Errors(protocol=Errors.READOUT, options=options).get_error_fn() + error = Noise(protocol=Noise.READOUT, options=options).get_error_fn() noisy_samples = error(counters=samples, n_qubits=n_qubits) # compare that the results are with an error of 10% (the default error_probability) for sample, noisy_sample in zip(samples, noisy_samples): @@ -196,7 +196,7 @@ def test_readout_error_with_measurements( model = QuantumModel(circuit=circuit, observable=observable, diff_mode=DiffMode.GPSR) # model.backend.backend.config._use_gate_params = True - error = Errors(protocol=Errors.READOUT) + error = Noise(protocol=Noise.READOUT) measurement = Measurements(protocol=str(measurement_proto), options=options) # measured = model.expectation(values=inputs, measurement=measurement) From 7f09e26ce066ca4f88d1fbfc636889045151938f Mon Sep 17 00:00:00 2001 From: Roland Guichard Date: Mon, 6 Nov 2023 15:56:08 +0000 Subject: [PATCH 46/83] Comment and better logging condition. --- qadence/backends/braket/backend.py | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/qadence/backends/braket/backend.py b/qadence/backends/braket/backend.py index f17b081a..dd8d6c3e 100644 --- a/qadence/backends/braket/backend.py +++ b/qadence/backends/braket/backend.py @@ -25,7 +25,7 @@ from .config import Configuration, default_passes from .convert_ops import convert_block -logger = get_logger(__name__) +logger = get_logger(__file__) def promote_parameters(parameters: dict[str, Tensor | float]) -> dict[str, float]: @@ -176,7 +176,8 @@ def expectation( error: Noise | None = None, endianness: Endianness = Endianness.BIG, ) -> Tensor: - if error is not None: + # Noise is ignored if measurement protocol is not provided. + if error is not None and measurement is None: logger.warning( f"Noise of type {error} are not implemented for expectation yet. " "This is ignored for now." From 9038c095bb6fecbbf8e0d9210d28e5c8e2dd4f62 Mon Sep 17 00:00:00 2001 From: Roland Guichard Date: Mon, 6 Nov 2023 15:58:47 +0000 Subject: [PATCH 47/83] Better logging. --- qadence/backends/pyqtorch/backend.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/qadence/backends/pyqtorch/backend.py b/qadence/backends/pyqtorch/backend.py index b3301cff..a6fcbe49 100644 --- a/qadence/backends/pyqtorch/backend.py +++ b/qadence/backends/pyqtorch/backend.py @@ -183,7 +183,8 @@ def expectation( error: Noise | None = None, endianness: Endianness = Endianness.BIG, ) -> Tensor: - if error is not None: + # Noise is ignored if measurement protocol is not provided. + if error is not None and measurement is None: logger.warning( f"Noise of type {error} are not implemented for expectation yet. " "This is ignored for now." From ccf5b6de6394073407df1e5d707a5792bdf5e53e Mon Sep 17 00:00:00 2001 From: Roland Guichard Date: Mon, 6 Nov 2023 15:59:49 +0000 Subject: [PATCH 48/83] Better logging. --- qadence/backends/pulser/backend.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/qadence/backends/pulser/backend.py b/qadence/backends/pulser/backend.py index c845c41d..5e0d8e74 100644 --- a/qadence/backends/pulser/backend.py +++ b/qadence/backends/pulser/backend.py @@ -288,7 +288,8 @@ def expectation( error: Noise | None = None, endianness: Endianness = Endianness.BIG, ) -> Tensor: - if error is not None: + # Noise is ignored if measurement protocol is not provided. + if error is not None and measurement is None: logger.warning( f"Noise of type {error} are not implemented for expectation yet. " "This is ignored for now." From c7138087a599963965e029398981b1d8185f4b1f Mon Sep 17 00:00:00 2001 From: Roland Guichard Date: Mon, 6 Nov 2023 21:23:08 +0000 Subject: [PATCH 49/83] Remove unnecessary comment. --- qadence/noise/protocols.py | 1 - 1 file changed, 1 deletion(-) diff --git a/qadence/noise/protocols.py b/qadence/noise/protocols.py index ea848291..ad5f45a4 100644 --- a/qadence/noise/protocols.py +++ b/qadence/noise/protocols.py @@ -9,7 +9,6 @@ } -# TODO: make this a StrEnum to keep consistency with the rest of the interface @dataclass class Noise: READOUT = "readout" From 6b9d806caf16869ce62e52382dd5f9cdebecc65d Mon Sep 17 00:00:00 2001 From: Roland Guichard Date: Mon, 6 Nov 2023 21:28:08 +0000 Subject: [PATCH 50/83] Clean-up. --- tests/qadence/test_error_models.py | 1 - 1 file changed, 1 deletion(-) diff --git a/tests/qadence/test_error_models.py b/tests/qadence/test_error_models.py index 97a4c4a0..47730b4e 100644 --- a/tests/qadence/test_error_models.py +++ b/tests/qadence/test_error_models.py @@ -139,7 +139,6 @@ def test_readout_error_quantum_model( QuantumCircuit(block.n_qubits, block), backend=backend, diff_mode=diff_mode ).sample(error=Noise(protocol=Noise.READOUT), n_shots=n_shots) - # breakpoint() for noiseless, noisy in zip(noiseless_samples, noisy_samples): assert sum(noiseless.values()) == sum(noisy.values()) == n_shots assert js_divergence(noiseless, noisy) > 0.0 From 2338fdba2f87803dad7aa2616c089a06b9d3b7d3 Mon Sep 17 00:00:00 2001 From: Gergana Velikova Date: Mon, 6 Nov 2023 22:30:50 +0100 Subject: [PATCH 51/83] rename the remaining errors to noise --- qadence/backend.py | 8 ++++---- qadence/backends/braket/backend.py | 16 ++++++++-------- qadence/backends/pulser/backend.py | 16 ++++++++-------- qadence/backends/pyqtorch/backend.py | 22 +++++++++++----------- qadence/backends/pytorch_wrapper.py | 18 +++++++++--------- qadence/execution.py | 14 +++++++------- qadence/measurements/shadow.py | 14 +++++++------- qadence/measurements/tomography.py | 10 +++++----- qadence/models/quantum_model.py | 22 +++++++++++----------- qadence/noise/protocols.py | 2 +- tests/qadence/test_error_models.py | 12 ++++++------ 11 files changed, 77 insertions(+), 77 deletions(-) diff --git a/qadence/backend.py b/qadence/backend.py index 15329b39..f8c5ad6c 100644 --- a/qadence/backend.py +++ b/qadence/backend.py @@ -229,7 +229,7 @@ def sample( param_values: dict[str, Tensor] = {}, n_shots: int = 1000, state: Tensor | None = None, - error: Noise | None = None, + noise: Noise | None = None, endianness: Endianness = Endianness.BIG, ) -> list[Counter]: """Sample bit strings. @@ -240,7 +240,7 @@ def sample( [`embedding`][qadence.blocks.embedding.embedding] for more info. n_shots: Number of shots to sample. state: Initial state. - error: A noise model to use. + noise: A noise model to use. endianness: Endianness of the resulting bit strings. """ raise NotImplementedError @@ -276,7 +276,7 @@ def expectation( param_values: dict[str, Tensor] = {}, state: Tensor | None = None, measurement: Measurements | None = None, - error: Noise | None = None, + noise: Noise | None = None, endianness: Endianness = Endianness.BIG, ) -> Tensor: """Compute the expectation value of the `circuit` with the given `observable`. @@ -288,7 +288,7 @@ def expectation( state: Initial state. measurement: Optional measurement protocol. If None, use exact expectation value with a statevector simulator. - error: A noise model to use. + noise: A noise model to use. endianness: Endianness of the resulting bit strings. """ raise NotImplementedError diff --git a/qadence/backends/braket/backend.py b/qadence/backends/braket/backend.py index f17b081a..0048bd82 100644 --- a/qadence/backends/braket/backend.py +++ b/qadence/backends/braket/backend.py @@ -130,7 +130,7 @@ def sample( param_values: dict[str, Tensor] = {}, n_shots: int = 1, state: Tensor | None = None, - error: Noise | None = None, + noise: Noise | None = None, endianness: Endianness = Endianness.BIG, ) -> list[Counter]: """Execute the circuit and return samples of the resulting wavefunction.""" @@ -155,12 +155,12 @@ def sample( from qadence.transpile import invert_endianness samples = invert_endianness(samples) - if error is not None: - error_fn = error.get_error_fn() - return error_fn( # type: ignore[no-any-return] + if noise is not None: + noise_fn = noise.get_noise_fn() + return noise_fn( # type: ignore[no-any-return] counters=samples, n_qubits=circuit.abstract.n_qubits, - options=error.options, + options=noise.options, n_shots=n_shots, ) else: @@ -173,12 +173,12 @@ def expectation( param_values: dict[str, Tensor] = {}, state: Tensor | None = None, measurement: Measurements | None = None, - error: Noise | None = None, + noise: Noise | None = None, endianness: Endianness = Endianness.BIG, ) -> Tensor: - if error is not None: + if noise is not None: logger.warning( - f"Noise of type {error} are not implemented for expectation yet. " + f"Noise of type {noise} are not implemented for expectation yet. " "This is ignored for now." ) # Do not flip endianness here because then we would have to reverse the observable diff --git a/qadence/backends/pulser/backend.py b/qadence/backends/pulser/backend.py index c845c41d..71664e18 100644 --- a/qadence/backends/pulser/backend.py +++ b/qadence/backends/pulser/backend.py @@ -249,7 +249,7 @@ def sample( param_values: dict[str, Tensor] = {}, n_shots: int = 1, state: Tensor | None = None, - error: Noise | None = None, + noise: Noise | None = None, endianness: Endianness = Endianness.BIG, ) -> list[Counter]: if n_shots < 1: @@ -267,12 +267,12 @@ def sample( from qadence.transpile import invert_endianness samples = invert_endianness(samples) - if error is not None: - error_fn = error.get_error_fn() - return error_fn( # type: ignore[no-any-return] + if noise is not None: + noise_fn = noise.get_noise_fn() + return noise_fn( # type: ignore[no-any-return] counters=samples, n_qubits=circuit.abstract.n_qubits, - options=error.options, + options=noise.options, n_shots=n_shots, ) else: @@ -285,12 +285,12 @@ def expectation( param_values: dict[str, Tensor] = {}, state: Tensor | None = None, measurement: Measurements | None = None, - error: Noise | None = None, + noise: Noise | None = None, endianness: Endianness = Endianness.BIG, ) -> Tensor: - if error is not None: + if noise is not None: logger.warning( - f"Noise of type {error} are not implemented for expectation yet. " + f"Noise of type {noise} are not implemented for expectation yet. " "This is ignored for now." ) diff --git a/qadence/backends/pyqtorch/backend.py b/qadence/backends/pyqtorch/backend.py index b3301cff..7b1247f5 100644 --- a/qadence/backends/pyqtorch/backend.py +++ b/qadence/backends/pyqtorch/backend.py @@ -128,7 +128,7 @@ def _batched_expectation( param_values: dict[str, Tensor] = {}, state: Tensor | None = None, measurement: Measurements | None = None, - error: Noise | None = None, + noise: Noise | None = None, endianness: Endianness = Endianness.BIG, ) -> Tensor: state = self.run( @@ -153,7 +153,7 @@ def _looped_expectation( param_values: dict[str, Tensor] = {}, state: Tensor | None = None, measurement: Measurements | None = None, - error: Noise | None = None, + noise: Noise | None = None, endianness: Endianness = Endianness.BIG, ) -> Tensor: state = zero_state(circuit.abstract.n_qubits, batch_size=1) if state is None else state @@ -180,12 +180,12 @@ def expectation( param_values: dict[str, Tensor] = {}, state: Tensor | None = None, measurement: Measurements | None = None, - error: Noise | None = None, + noise: Noise | None = None, endianness: Endianness = Endianness.BIG, ) -> Tensor: - if error is not None: + if noise is not None: logger.warning( - f"Noise of type {error} are not implemented for expectation yet. " + f"Noise of type {noise} are not implemented for expectation yet. " "This is ignored for now." ) fn = self._looped_expectation if self.config.loop_expectation else self._batched_expectation @@ -195,7 +195,7 @@ def expectation( param_values=param_values, state=state, measurement=measurement, - error=error, + noise=noise, endianness=endianness, ) @@ -205,7 +205,7 @@ def sample( param_values: dict[str, Tensor] = {}, n_shots: int = 1, state: Tensor | None = None, - error: Noise | None = None, + noise: Noise | None = None, endianness: Endianness = Endianness.BIG, ) -> list[Counter]: if n_shots < 1: @@ -237,12 +237,12 @@ def _sample(_probs: Tensor, n_shots: int, endianness: Endianness, n_qubits: int) probs, ) ) - if error is not None: - error_fn = error.get_error_fn() - corrupted_counters: list = error_fn( + if noise is not None: + noise_fn = noise.get_noise_fn() + corrupted_counters: list = noise_fn( counters=counters, n_qubits=circuit.abstract.n_qubits, - options=error.options, + options=noise.options, n_shots=n_shots, ) return corrupted_counters diff --git a/qadence/backends/pytorch_wrapper.py b/qadence/backends/pytorch_wrapper.py index 5c6df878..2abd3e54 100644 --- a/qadence/backends/pytorch_wrapper.py +++ b/qadence/backends/pytorch_wrapper.py @@ -86,7 +86,7 @@ class DifferentiableExpectation: param_values: dict[str, Tensor] state: Tensor | None = None measurement: Measurements | None = None - error: Noise | None = None + noise: Noise | None = None endianness: Endianness = Endianness.BIG def ad(self) -> Tensor: @@ -101,7 +101,7 @@ def ad(self) -> Tensor: param_values=self.param_values, options=self.measurement.options, state=self.state, - error=self.error, + noise=self.noise, endianness=self.endianness, ) else: @@ -110,7 +110,7 @@ def ad(self) -> Tensor: observable=self.observable, param_values=self.param_values, state=self.state, - error=self.error, + noise=self.noise, endianness=self.endianness, ) return promote_to_tensor( @@ -135,7 +135,7 @@ def psr(self, psr_fn: Callable, **psr_args: int | float | None) -> Tensor: observables=[obs.original for obs in self.observable], options=self.measurement.options, state=self.state, - error=self.error, + noise=self.noise, endianness=self.endianness, ) else: @@ -144,7 +144,7 @@ def psr(self, psr_fn: Callable, **psr_args: int | float | None) -> Tensor: circuit=self.circuit, observable=self.observable, state=self.state, - error=self.error, + noise=self.noise, endianness=self.endianness, ) # PSR only applies to parametric circuits. @@ -236,7 +236,7 @@ def expectation( param_values: dict[str, Tensor] = {}, state: Tensor | None = None, measurement: Measurements | None = None, - error: Noise | None = None, + noise: Noise | None = None, endianness: Endianness = Endianness.BIG, ) -> Tensor: """Compute the expectation value of a given observable. @@ -260,7 +260,7 @@ def expectation( param_values=param_values, state=state, measurement=measurement, - error=error, + noise=noise, endianness=endianness, ) @@ -281,7 +281,7 @@ def sample( param_values: dict[str, Tensor], n_shots: int = 1, state: Tensor | None = None, - error: Noise | None = None, + noise: Noise | None = None, endianness: Endianness = Endianness.BIG, ) -> list[Counter]: """Sample bitstring from the registered circuit. @@ -304,7 +304,7 @@ def sample( state=state, n_shots=n_shots, endianness=endianness, - error=error, + noise=noise, ) def circuit(self, circuit: QuantumCircuit) -> ConvertedCircuit: diff --git a/qadence/execution.py b/qadence/execution.py index d0d20d65..5a3c7efd 100644 --- a/qadence/execution.py +++ b/qadence/execution.py @@ -106,7 +106,7 @@ def sample( n_shots: int = 100, backend: BackendName = BackendName.PYQTORCH, endianness: Endianness = Endianness.BIG, - error: Union[Noise, None] = None, + noise: Union[Noise, None] = None, configuration: Union[BackendConfiguration, dict, None] = None, ) -> list[Counter]: """Convenience wrapper for the `QuantumModel.sample` method. This is a @@ -121,7 +121,7 @@ def sample( n_shots: Number of shots per element in the batch. backend: Name of the backend to run on. endianness: The target device endianness. - error: The error model to use if any. + noise: The noise model to use if any. configuration: The backend configuration. Returns: @@ -137,7 +137,7 @@ def _( state: Union[Tensor, None] = None, n_shots: int = 100, backend: BackendName = BackendName.PYQTORCH, - error: Union[Noise, None] = None, + noise: Union[Noise, None] = None, endianness: Endianness = Endianness.BIG, configuration: Union[BackendConfiguration, dict, None] = None, ) -> list[Counter]: @@ -148,7 +148,7 @@ def _( param_values=conv.embedding_fn(conv.params, values), n_shots=n_shots, state=state, - error=error, + noise=noise, endianness=endianness, ) @@ -177,7 +177,7 @@ def expectation( state: Tensor = None, backend: BackendName = BackendName.PYQTORCH, diff_mode: Union[DiffMode, str, None] = None, - error: Union[Noise, None] = None, + noise: Union[Noise, None] = None, endianness: Endianness = Endianness.BIG, configuration: Union[BackendConfiguration, dict, None] = None, ) -> Tensor: @@ -231,7 +231,7 @@ def _( state: Tensor = None, backend: BackendName = BackendName.PYQTORCH, diff_mode: Union[DiffMode, str, None] = None, - error: Union[Noise, None] = None, + noise: Union[Noise, None] = None, endianness: Endianness = Endianness.BIG, configuration: Union[BackendConfiguration, dict, None] = None, ) -> Tensor: @@ -245,7 +245,7 @@ def _expectation() -> Tensor: observable=conv.observable, # type: ignore[arg-type] param_values=conv.embedding_fn(conv.params, values), state=state, - error=error, + noise=noise, endianness=endianness, ) diff --git a/qadence/measurements/shadow.py b/qadence/measurements/shadow.py index fca7d361..a0cc1ccc 100644 --- a/qadence/measurements/shadow.py +++ b/qadence/measurements/shadow.py @@ -129,7 +129,7 @@ def classical_shadow( state: Tensor | None = None, backend_name: BackendName = BackendName.PYQTORCH, # FIXME: Changed below from Little to Big, double-check when Roland is back - error: Noise | None = None, + noise: Noise | None = None, endianness: Endianness = Endianness.BIG, ) -> list: shadow: list = [] @@ -155,7 +155,7 @@ def classical_shadow( param_values=param_values, n_shots=1, state=state, - error=error, + noise=noise, endianness=endianness, ) batched_shadow = [] @@ -260,7 +260,7 @@ def estimations( confidence: float = 0.1, state: Tensor | None = None, backend_name: BackendName = BackendName.PYQTORCH, - error: Noise | None = None, + noise: Noise | None = None, endianness: Endianness = Endianness.BIG, ) -> Tensor: """Compute expectation values for all local observables using median of means.""" @@ -276,7 +276,7 @@ def estimations( param_values=param_values, state=state, backend_name=backend_name, - error=error, + noise=noise, endianness=endianness, ) estimations = [] @@ -313,7 +313,7 @@ def compute_expectation( options: dict, state: Tensor | None = None, backend_name: BackendName = BackendName.PYQTORCH, - error: Noise | None = None, + noise: Noise | None = None, endianness: Endianness = Endianness.BIG, ) -> Tensor: """ @@ -329,7 +329,7 @@ def compute_expectation( Here, shadow_size (int), accuracy (float) and confidence (float) are supported. state (Tensor | None): an initial input state. backend_name (BackendName): a backend name to retrieve computations from. - error: A noise model to use. + noise: A noise model to use. endianness: Endianness of the observable estimate. Returns: @@ -360,6 +360,6 @@ def compute_expectation( confidence=confidence, state=state, backend_name=backend_name, - error=error, + noise=noise, endianness=endianness, ) diff --git a/qadence/measurements/tomography.py b/qadence/measurements/tomography.py index a8855004..b2b487fe 100644 --- a/qadence/measurements/tomography.py +++ b/qadence/measurements/tomography.py @@ -84,7 +84,7 @@ def iterate_pauli_decomposition( n_shots: int, state: Tensor | None = None, backend_name: BackendName = BackendName.PYQTORCH, - error: Noise | None = None, + noise: Noise | None = None, endianness: Endianness = Endianness.BIG, ) -> Tensor: """Estimate total expectation value by averaging all Pauli terms.""" @@ -112,7 +112,7 @@ def iterate_pauli_decomposition( param_values=param_values, n_shots=n_shots, state=state, - error=error, + noise=noise, endianness=endianness, ) estim_values = empirical_average(samples=samples, support=support) @@ -131,7 +131,7 @@ def compute_expectation( options: dict, state: Tensor | None = None, backend_name: BackendName = BackendName.PYQTORCH, - error: Noise | None = None, + noise: Noise | None = None, endianness: Endianness = Endianness.BIG, ) -> Tensor: """Basic tomography protocol with rotations @@ -149,7 +149,7 @@ def compute_expectation( Here, shadow_size (int), accuracy (float) and confidence (float) are supported. state (Tensor | None): an initial input state. backend_name (BackendName): a backend name to retrieve computations from. - error: A noise model to use. + noise: A noise model to use. endianness: Endianness of the observable estimate. """ if not isinstance(observables, list): @@ -172,7 +172,7 @@ def compute_expectation( n_shots=n_shots, state=state, backend_name=backend_name, - error=error, + noise=noise, endianness=endianness, ) ) diff --git a/qadence/models/quantum_model.py b/qadence/models/quantum_model.py index 58fd359e..c2daec4a 100644 --- a/qadence/models/quantum_model.py +++ b/qadence/models/quantum_model.py @@ -50,7 +50,7 @@ def __init__( diff_mode: DiffMode = DiffMode.AD, measurement: Measurements | None = None, configuration: BackendConfiguration | dict | None = None, - error: Noise | None = None, + noise: Noise | None = None, ): """Initialize a generic QuantumModel instance. @@ -64,7 +64,7 @@ def __init__( measurement: Optional measurement protocol. If None, use exact expectation value with a statevector simulator. configuration: Configuration for the backend. - error: A noise model to use. + noise: A noise model to use. Raises: ValueError: if the `diff_mode` argument is set to None @@ -96,7 +96,7 @@ def __init__( self._backend_name = backend self._diff_mode = diff_mode self._measurement = measurement - self._error = error + self._noise = noise self._params = nn.ParameterDict( { @@ -166,14 +166,14 @@ def sample( values: dict[str, torch.Tensor] = {}, n_shots: int = 1000, state: torch.Tensor | None = None, - error: Noise | None = None, + noise: Noise | None = None, endianness: Endianness = Endianness.BIG, ) -> list[Counter]: params = self.embedding_fn(self._params, values) - if error is None: - error = self._error + if noise is None: + noise = self._noise return self.backend.sample( - self._circuit, params, n_shots=n_shots, state=state, error=error, endianness=endianness + self._circuit, params, n_shots=n_shots, state=state, noise=noise, endianness=endianness ) def expectation( @@ -182,7 +182,7 @@ def expectation( observable: list[ConvertedObservable] | ConvertedObservable | None = None, state: Optional[Tensor] = None, measurement: Measurements | None = None, - error: Noise | None = None, + noise: Noise | None = None, endianness: Endianness = Endianness.BIG, ) -> Tensor: """Compute expectation using the given backend. @@ -202,15 +202,15 @@ def expectation( params = self.embedding_fn(self._params, values) if measurement is None: measurement = self._measurement - if error is None: - error = self._error + if noise is None: + noise = self._noise return self.backend.expectation( circuit=self._circuit, observable=observable, param_values=params, state=state, measurement=measurement, - error=error, + noise=noise, endianness=endianness, ) diff --git a/qadence/noise/protocols.py b/qadence/noise/protocols.py index ea848291..734cf4e5 100644 --- a/qadence/noise/protocols.py +++ b/qadence/noise/protocols.py @@ -18,7 +18,7 @@ def __init__(self, protocol: str, options: dict = dict()) -> None: self.protocol: str = protocol self.options: dict = options - def get_error_fn(self) -> Callable: + def get_noise_fn(self) -> Callable: try: module = importlib.import_module(PROTOCOL_TO_MODULE[self.protocol]) except KeyError: diff --git a/tests/qadence/test_error_models.py b/tests/qadence/test_error_models.py index 97a4c4a0..ab285f3f 100644 --- a/tests/qadence/test_error_models.py +++ b/tests/qadence/test_error_models.py @@ -137,7 +137,7 @@ def test_readout_error_quantum_model( noisy_samples: list[Counter] = QuantumModel( QuantumCircuit(block.n_qubits, block), backend=backend, diff_mode=diff_mode - ).sample(error=Noise(protocol=Noise.READOUT), n_shots=n_shots) + ).sample(noise=Noise(protocol=Noise.READOUT), n_shots=n_shots) # breakpoint() for noiseless, noisy in zip(noiseless_samples, noisy_samples): @@ -158,11 +158,11 @@ def test_readout_error_backends(backend: BackendName) -> None: feature_map = qd.kron(RX(i, 2 * acos(fp)) for i in range(n_qubits)) inputs = {"phi": torch.rand(1)} # sample - samples = qd.sample(feature_map, n_shots=1000, values=inputs, backend=backend, error=None) + samples = qd.sample(feature_map, n_shots=1000, values=inputs, backend=backend, noise=None) # introduce noise options = {"error_probability": error_probability} - error = Noise(protocol=Noise.READOUT, options=options).get_error_fn() - noisy_samples = error(counters=samples, n_qubits=n_qubits) + noise = Noise(protocol=Noise.READOUT, options=options).get_noise_fn() + noisy_samples = noise(counters=samples, n_qubits=n_qubits) # compare that the results are with an error of 10% (the default error_probability) for sample, noisy_sample in zip(samples, noisy_samples): assert sum(sample.values()) == sum(noisy_sample.values()) @@ -196,11 +196,11 @@ def test_readout_error_with_measurements( model = QuantumModel(circuit=circuit, observable=observable, diff_mode=DiffMode.GPSR) # model.backend.backend.config._use_gate_params = True - error = Noise(protocol=Noise.READOUT) + noise = Noise(protocol=Noise.READOUT) measurement = Measurements(protocol=str(measurement_proto), options=options) # measured = model.expectation(values=inputs, measurement=measurement) - noisy = model.expectation(values=inputs, measurement=measurement, error=error) + noisy = model.expectation(values=inputs, measurement=measurement, noise=noise) exact = model.expectation(values=inputs) if exact.numel() > 1: for noisy_value, exact_value in zip(noisy, exact): From 9c5a10f50b02e5a1d98858b9d83365f5c6d6766d Mon Sep 17 00:00:00 2001 From: Gergana Velikova Date: Mon, 6 Nov 2023 22:45:25 +0100 Subject: [PATCH 52/83] adding noise to models --- qadence/ml_tools/models.py | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/qadence/ml_tools/models.py b/qadence/ml_tools/models.py index af047f1f..d85c837a 100644 --- a/qadence/ml_tools/models.py +++ b/qadence/ml_tools/models.py @@ -12,6 +12,7 @@ from qadence.measurements import Measurements from qadence.ml_tools import promote_to_tensor from qadence.models import QNN, QuantumModel +from qadence.noise import Noise from qadence.utils import Endianness logger = get_logger(__name__) @@ -200,6 +201,7 @@ def sample( values: dict[str, torch.Tensor], n_shots: int = 1000, state: torch.Tensor | None = None, + noise: Noise | None = None, endianness: Endianness = Endianness.BIG, ) -> list[Counter]: return self.model.sample( # type: ignore[no-any-return] @@ -207,6 +209,7 @@ def sample( n_shots=n_shots, state=state, endianness=endianness, + noise=noise, ) def expectation( @@ -215,6 +218,7 @@ def expectation( observable: List[ConvertedObservable] | ConvertedObservable | None = None, state: torch.Tensor | None = None, measurement: Measurements | None = None, + noise: Noise | None = None, endianness: Endianness = Endianness.BIG, ) -> Tensor: """ @@ -229,6 +233,7 @@ def expectation( observable=observable if observable is not None else self.model._observable, state=state, measurement=measurement, + noise=noise, endianness=endianness, ) return self._output_scaling * exp + self._output_shifting From 674f1c3370273095c78b088fc0f49cd5abcd4022 Mon Sep 17 00:00:00 2001 From: Gergana Velikova Date: Tue, 7 Nov 2023 07:41:09 +0100 Subject: [PATCH 53/83] adding noise to qnn --- qadence/models/qnn.py | 15 ++++++++++++++- 1 file changed, 14 insertions(+), 1 deletion(-) diff --git a/qadence/models/qnn.py b/qadence/models/qnn.py index 36bd3eb4..cdb1d064 100644 --- a/qadence/models/qnn.py +++ b/qadence/models/qnn.py @@ -10,6 +10,7 @@ from qadence.circuit import QuantumCircuit from qadence.measurements import Measurements from qadence.models.quantum_model import QuantumModel +from qadence.noise import Noise from qadence.utils import Endianness @@ -47,6 +48,7 @@ def __init__( backend: BackendName = BackendName.PYQTORCH, diff_mode: DiffMode = DiffMode.AD, measurement: Measurements | None = None, + noise: Noise | None = None, configuration: BackendConfiguration | dict | None = None, ): """Initialize the QNN @@ -62,6 +64,7 @@ def __init__( diff_mode: The differentiation engine to use. Choices 'gpsr' or 'ad'. measurement: optional measurement protocol. If None, use exact expectation value with a statevector simulator + noise: A noise model to use. configuration: optional configuration for the backend """ @@ -84,6 +87,7 @@ def forward( values: dict[str, Tensor] | Tensor = None, state: Tensor | None = None, measurement: Measurements | None = None, + noise: Noise | None = None, endianness: Endianness = Endianness.BIG, ) -> Tensor: """Forward pass of the model @@ -101,6 +105,11 @@ def forward( Args: values (dict[str, Tensor] | Tensor): the values of the feature parameters + state: Initial state. + measurement: optional measurement protocol. If None, + use exact expectation value with a statevector simulator + noise: A noise model to use. + endianness: Endianness of the resulting bit strings. Returns: Tensor: a tensor with the expectation value of the observables passed @@ -115,7 +124,11 @@ def forward( return self.transform( self.expectation( - values=values, state=state, measurement=measurement, endianness=endianness + values=values, + state=state, + measurement=measurement, + endianness=endianness, + noise=noise, ) ) From 99fb61c78d0445dd403abffaa902f91eed6e0474 Mon Sep 17 00:00:00 2001 From: Gergana Velikova Date: Tue, 7 Nov 2023 07:49:49 +0100 Subject: [PATCH 54/83] readme renamed to noise --- docs/realistic_sims/errors.md | 1 - 1 file changed, 1 deletion(-) delete mode 100644 docs/realistic_sims/errors.md diff --git a/docs/realistic_sims/errors.md b/docs/realistic_sims/errors.md deleted file mode 100644 index 6c71c088..00000000 --- a/docs/realistic_sims/errors.md +++ /dev/null @@ -1 +0,0 @@ -This section introduces error models. From 2ca1ae6317cd5af83cfbb21410d0ecc9ac6590d5 Mon Sep 17 00:00:00 2001 From: Gergana Velikova Date: Tue, 7 Nov 2023 07:57:47 +0100 Subject: [PATCH 55/83] changed warning --- qadence/backends/braket/backend.py | 2 +- qadence/backends/pulser/backend.py | 2 +- qadence/backends/pyqtorch/backend.py | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/qadence/backends/braket/backend.py b/qadence/backends/braket/backend.py index 4f6818fb..14216a13 100644 --- a/qadence/backends/braket/backend.py +++ b/qadence/backends/braket/backend.py @@ -179,7 +179,7 @@ def expectation( # Noise is ignored if measurement protocol is not provided. if noise is not None and measurement is None: logger.warning( - f"Noise of type {noise} are not implemented for expectation yet. " + f"Error of type {noise} are not implemented for expectation yet. " "This is ignored for now." ) # Do not flip endianness here because then we would have to reverse the observable diff --git a/qadence/backends/pulser/backend.py b/qadence/backends/pulser/backend.py index 0f4621fd..31cdd7f6 100644 --- a/qadence/backends/pulser/backend.py +++ b/qadence/backends/pulser/backend.py @@ -291,7 +291,7 @@ def expectation( # Noise is ignored if measurement protocol is not provided. if noise is not None and measurement is None: logger.warning( - f"Noise of type {noise} are not implemented for expectation yet. " + f"Error of type {noise} are not implemented for expectation yet. " "This is ignored for now." ) diff --git a/qadence/backends/pyqtorch/backend.py b/qadence/backends/pyqtorch/backend.py index dff8d199..7ca2449f 100644 --- a/qadence/backends/pyqtorch/backend.py +++ b/qadence/backends/pyqtorch/backend.py @@ -186,7 +186,7 @@ def expectation( # Noise is ignored if measurement protocol is not provided. if noise is not None and measurement is None: logger.warning( - f"Noise of type {noise} are not implemented for expectation yet. " + f"Error of type {noise} are not implemented for expectation yet. " "This is ignored for now." ) fn = self._looped_expectation if self.config.loop_expectation else self._batched_expectation From b50c531b7147e6faf145fc19364cefc05954cdfa Mon Sep 17 00:00:00 2001 From: seitzdom <39090287+dominikandreasseitz@users.noreply.github.com> Date: Mon, 6 Nov 2023 14:18:29 +0100 Subject: [PATCH 56/83] [Refactor] Train loop; make sure qadence runs on GPUs correctly (#135) Co-authored-by: Niklas Heim --- .gitignore | 1 + .pre-commit-config.yaml | 2 +- docs/qadence/ml_tools.md | 2 + docs/qml/ml_tools.md | 66 ++++++++++---------- pyproject.toml | 2 +- qadence/ml_tools/__init__.py | 2 +- qadence/ml_tools/data.py | 93 ++++++++++++++++++++++++---- qadence/ml_tools/optimize_step.py | 38 ------------ qadence/ml_tools/train_grad.py | 65 ++++++------------- qadence/ml_tools/train_no_grad.py | 20 +----- tests/ml_tools/test_checkpointing.py | 10 ++- tests/ml_tools/test_train.py | 48 ++++++-------- tests/ml_tools/test_train_no_grad.py | 11 ++-- 13 files changed, 165 insertions(+), 195 deletions(-) diff --git a/.gitignore b/.gitignore index 9d614421..1b864150 100644 --- a/.gitignore +++ b/.gitignore @@ -17,6 +17,7 @@ htmlcov/ .tox/ .coverage .coverage.* +*.lcov .cache nosetests.xml coverage.xml diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index 9feb2c2e..ba0c85f9 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -9,7 +9,7 @@ repos: args: ['--maxkb=600'] - repo: https://github.com/astral-sh/ruff-pre-commit - rev: "v0.0.291" + rev: "v0.1.2" hooks: - id: ruff args: [--fix, --show-fixes, --show-source, --exclude, examples/draw.py] diff --git a/docs/qadence/ml_tools.md b/docs/qadence/ml_tools.md index 0c5ee50b..0dccc339 100644 --- a/docs/qadence/ml_tools.md +++ b/docs/qadence/ml_tools.md @@ -11,3 +11,5 @@ This module implements gradient-free and gradient-based training loops for torch ### ::: qadence.ml_tools.train_grad ### ::: qadence.ml_tools.train_no_grad + +### ::: qadence.ml_tools.data diff --git a/docs/qml/ml_tools.md b/docs/qml/ml_tools.md index 6d7d717b..ebc9f41a 100644 --- a/docs/qml/ml_tools.md +++ b/docs/qml/ml_tools.md @@ -5,46 +5,42 @@ algorithm by using a standard PyTorch `DataLoader` instance. Qadence also provid the `DictDataLoader` convenience class which allows to build dictionaries of `DataLoader`s instances and easily iterate over them. -```python exec="on" source="material-block" +```python exec="on" source="material-block" result="json" import torch from torch.utils.data import DataLoader, TensorDataset -from qadence.ml_tools import DictDataLoader - -def dataloader() -> DataLoader: - batch_size = 5 - x = torch.linspace(0, 1, batch_size).reshape(-1, 1) - y = torch.sin(x) +from qadence.ml_tools import DictDataLoader, to_dataloader - dataset = TensorDataset(x, y) - return DataLoader(dataset, batch_size=batch_size) +def dataloader(data_size: int = 25, batch_size: int = 5, infinite: bool = False) -> DataLoader: + x = torch.linspace(0, 1, data_size).reshape(-1, 1) + y = torch.sin(x) + return to_dataloader(x, y, batch_size=batch_size, infinite=infinite) -def dictdataloader() -> DictDataLoader: - batch_size = 5 - keys = ["y1", "y2"] +def dictdataloader(data_size: int = 25, batch_size: int = 5) -> DictDataLoader: dls = {} - for k in keys: - x = torch.rand(batch_size, 1) + for k in ["y1", "y2"]: + x = torch.rand(data_size, 1) y = torch.sin(x) - dataset = TensorDataset(x, y) - dataloader = DataLoader(dataset, batch_size=batch_size) - dls[k] = dataloader - + dls[k] = to_dataloader(x, y, batch_size=batch_size, infinite=True) return DictDataLoader(dls) -n_epochs = 2 -# iterate standard DataLoader -dl = dataloader() -for i in range(n_epochs): - data = next(iter(dl)) +# iterate over standard DataLoader +for (x,y) in dataloader(data_size=6, batch_size=2): + print(f"Standard {x = }") -# iterate DictDataLoader -ddl = dictdataloader() -for i in range(n_epochs): - data = next(iter(ddl)) +# construct an infinite dataset which will keep sampling indefinitely +n_epochs = 5 +dl = iter(dataloader(data_size=6, batch_size=2, infinite=True)) +for _ in range(n_epochs): + (x, y) = next(dl) + print(f"Infinite {x = }") +# iterate over DictDataLoader +ddl = dictdataloader() +data = next(iter(ddl)) +print(f"{data = }") ``` ## Optimization routines @@ -108,12 +104,13 @@ Let's look at a complete example of how to use `train_with_grad` now. from pathlib import Path import torch from itertools import count -from qadence.constructors import hamiltonian_factory, hea, feature_map -from qadence import chain, Parameter, QuantumCircuit, Z -from qadence.models import QNN -from qadence.ml_tools import train_with_grad, TrainConfig import matplotlib.pyplot as plt +from qadence import Parameter, QuantumCircuit, Z +from qadence import hamiltonian_factory, hea, feature_map, chain +from qadence.models import QNN +from qadence.ml_tools import TrainConfig, train_with_grad, to_dataloader + n_qubits = 2 fm = feature_map(n_qubits) ansatz = hea(n_qubits=n_qubits, depth=3) @@ -152,12 +149,13 @@ batch_size = 25 x = torch.linspace(0, 1, batch_size).reshape(-1, 1) y = torch.sin(x) +data = to_dataloader(x, y, batch_size=batch_size, infinite=True) -train_with_grad(model, (x, y), optimizer, config, loss_fn=loss_fn) +train_with_grad(model, data, optimizer, config, loss_fn=loss_fn) plt.clf() # markdown-exec: hide -plt.plot(x.numpy(), y.numpy()) -plt.plot(x.numpy(), model(x).detach().numpy()) +plt.plot(x, y) +plt.plot(x, model(x).detach()) from docs import docsutils # markdown-exec: hide print(docsutils.fig_to_html(plt.gcf())) # markdown-exec: hide ``` diff --git a/pyproject.toml b/pyproject.toml index e6b09f88..535320b7 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -19,7 +19,7 @@ authors = [ ] requires-python = ">=3.9,<3.11" license = {text = "Apache 2.0"} -version = "1.0.4" +version = "1.0.5" classifiers=[ "License :: OSI Approved :: Apache Software License", "Programming Language :: Python", diff --git a/qadence/ml_tools/__init__.py b/qadence/ml_tools/__init__.py index 6c045f79..9623e59e 100644 --- a/qadence/ml_tools/__init__.py +++ b/qadence/ml_tools/__init__.py @@ -1,7 +1,7 @@ from __future__ import annotations from .config import TrainConfig -from .data import DictDataLoader +from .data import DictDataLoader, InfiniteTensorDataset, to_dataloader from .optimize_step import optimize_step as default_optimize_step from .parameters import get_parameters, num_parameters, set_parameters from .printing import print_metrics, write_tensorboard diff --git a/qadence/ml_tools/data.py b/qadence/ml_tools/data.py index 17d9739b..e1b436d3 100644 --- a/qadence/ml_tools/data.py +++ b/qadence/ml_tools/data.py @@ -1,9 +1,12 @@ from __future__ import annotations from dataclasses import dataclass +from functools import singledispatch +from itertools import cycle +from typing import Any, Iterator -import torch -from torch.utils.data import DataLoader, TensorDataset +from torch import Tensor +from torch.utils.data import DataLoader, IterableDataset, TensorDataset @dataclass @@ -12,21 +15,85 @@ class DictDataLoader: dataloaders: dict[str, DataLoader] - # this flag indicates that the dictionary contains dataloaders - # which can automatically iterate at each epoch without having to - # redefine the iterator itself (so basically no StopIteration exception - # will occur). This is the case of the Flow library where the dataloader - # is actually mostly used, so it is set to True by default - has_automatic_iter: bool = True - def __iter__(self) -> DictDataLoader: self.iters = {key: iter(dl) for key, dl in self.dataloaders.items()} return self - def __next__(self) -> dict[str, torch.Tensor]: + def __next__(self) -> dict[str, Tensor]: return {key: next(it) for key, it in self.iters.items()} -def to_dataloader(x: torch.Tensor, y: torch.Tensor, batch_size: int = 1) -> DataLoader: - """Convert two torch tensors x and y to a Dataloader.""" - return DataLoader(TensorDataset(x, y), batch_size=batch_size) +class InfiniteTensorDataset(IterableDataset): + def __init__(self, *tensors: Tensor): + """Randomly sample points from the first dimension of the given tensors. + Behaves like a normal torch `Dataset` just that we can sample from it as + many times as we want. + + Examples: + ```python exec="on" source="above" result="json" + import torch + from qadence.ml_tools.data import InfiniteTensorDataset + + x_data, y_data = torch.rand(5,2), torch.ones(5,1) + # The dataset accepts any number of tensors with the same batch dimension + ds = InfiniteTensorDataset(x_data, y_data) + + # call `next` to get one sample from each tensor: + xs = next(iter(ds)) + print(str(xs)) # markdown-exec: hide + ``` + """ + self.tensors = tensors + + def __iter__(self) -> Iterator: + if len(set([t.size(0) for t in self.tensors])) != 1: + raise ValueError("Size of first dimension must be the same for all tensors.") + + for idx in cycle(range(self.tensors[0].size(0))): + yield tuple(t[idx] for t in self.tensors) + + +def to_dataloader(*tensors: Tensor, batch_size: int = 1, infinite: bool = False) -> DataLoader: + """Convert torch tensors an (infinite) Dataloader. + + Arguments: + *tensors: Torch tensors to use in the dataloader. + batch_size: batch size of sampled tensors + infinite: if `True`, the dataloader will keep sampling indefinitely even after the whole + dataset was sampled once + + Examples: + + ```python exec="on" source="above" result="json" + import torch + from qadence.ml_tools import to_dataloader + + (x, y, z) = [torch.rand(10) for _ in range(3)] + loader = iter(to_dataloader(x, y, z, batch_size=5, infinite=True)) + print(next(loader)) + print(next(loader)) + print(next(loader)) + ``` + """ + ds = InfiniteTensorDataset(*tensors) if infinite else TensorDataset(*tensors) + return DataLoader(ds, batch_size=batch_size) + + +@singledispatch +def data_to_device(xs: Any, device: str) -> Any: + raise ValueError(f"Cannot move {type(xs)} to a pytorch device.") + + +@data_to_device.register +def _(xs: Tensor, device: str) -> Tensor: + return xs.to(device, non_blocking=True) + + +@data_to_device.register +def _(xs: list, device: str) -> list: + return [data_to_device(x, device) for x in xs] + + +@data_to_device.register +def _(xs: dict, device: str) -> dict: + return {key: data_to_device(val, device) for key, val in xs.items()} diff --git a/qadence/ml_tools/optimize_step.py b/qadence/ml_tools/optimize_step.py index fcc68d94..12486521 100644 --- a/qadence/ml_tools/optimize_step.py +++ b/qadence/ml_tools/optimize_step.py @@ -1,6 +1,5 @@ from __future__ import annotations -from functools import singledispatch from typing import Any, Callable import torch @@ -8,46 +7,11 @@ from torch.optim import Optimizer -@singledispatch -def data_to_model(xs: Any, device: str = "cpu") -> Any: - """Default behavior for single-dispatched function - - Just return the given data independently on the type - - Args: - xs (Any): the input data - device (str, optional): The torch device. Not used in this implementation. - - Returns: - Any: the `xs` argument untouched - """ - return xs - - -@data_to_model.register(list) -def _(xs: list, device: str = "cpu") -> list: - xs_to_device = xs - - for x in xs_to_device: - if torch.is_tensor(x): - x.to(device, non_blocking=True) - - return xs_to_device - - -@data_to_model.register(dict) -def _(xs: dict, device: str = "cpu") -> dict: - # TODO: Make sure that they are tensors before calling .to() method - to_device = {key: [x.to(device, non_blocking=True) for x in val] for key, val in xs.items()} - return to_device - - def optimize_step( model: Module, optimizer: Optimizer, loss_fn: Callable, xs: dict | list | torch.Tensor | None, - device: str = "cpu", ) -> tuple[torch.Tensor | float, dict | None]: """Default Torch optimize step with closure @@ -60,8 +24,6 @@ def optimize_step( loss_fn (Callable): A custom loss function xs (dict | list | torch.Tensor | None): the input data. If None it means that the given model does not require any input data - device (str, optional): The device were computations are executed. - Defaults to "cpu". Returns: tuple: tuple containing the model, the optimizer, a dictionary with diff --git a/qadence/ml_tools/train_grad.py b/qadence/ml_tools/train_grad.py index 44225457..cb6d560b 100644 --- a/qadence/ml_tools/train_grad.py +++ b/qadence/ml_tools/train_grad.py @@ -1,9 +1,8 @@ from __future__ import annotations -from typing import Callable +from typing import Callable, Union from rich.progress import BarColumn, Progress, TaskProgressColumn, TextColumn, TimeRemainingColumn -from torch import Tensor from torch.nn import Module from torch.optim import Optimizer from torch.utils.data import DataLoader @@ -11,7 +10,7 @@ from qadence.logger import get_logger from qadence.ml_tools.config import TrainConfig -from qadence.ml_tools.data import DictDataLoader +from qadence.ml_tools.data import DictDataLoader, data_to_device from qadence.ml_tools.optimize_step import optimize_step from qadence.ml_tools.printing import print_metrics, write_tensorboard from qadence.ml_tools.saveload import load_checkpoint, write_checkpoint @@ -21,7 +20,7 @@ def train( model: Module, - dataloader: DictDataLoader | DataLoader | list[Tensor] | tuple[Tensor, Tensor] | None, + dataloader: Union[None, DataLoader, DictDataLoader], optimizer: Optimizer, config: TrainConfig, loss_fn: Callable, @@ -64,10 +63,10 @@ def train( from pathlib import Path import torch from itertools import count - from qadence.constructors import hamiltonian_factory, hea, feature_map - from qadence import chain, Parameter, QuantumCircuit, Z + from qadence import Parameter, QuantumCircuit, Z + from qadence import hamiltonian_factory, hea, feature_map, chain from qadence.models import QNN - from qadence.ml_tools import train_with_grad, TrainConfig + from qadence.ml_tools import TrainConfig, train_with_grad, to_dataloader n_qubits = 2 fm = feature_map(n_qubits) @@ -92,24 +91,23 @@ def loss_fn(model: torch.nn.Module, data: torch.Tensor) -> tuple[torch.Tensor, d out = model(x) loss = criterion(out, y) return loss, {} + tmp_path = Path("/tmp") n_epochs = 5 + batch_size = 25 config = TrainConfig( folder=tmp_path, max_iter=n_epochs, checkpoint_every=100, write_every=100, - batch_size=batch_size, ) - batch_size = 25 x = torch.linspace(0, 1, batch_size).reshape(-1, 1) y = torch.sin(x) - train_with_grad(model, (x, y), optimizer, config, loss_fn=loss_fn) + data = to_dataloader(x, y, batch_size=batch_size, infinite=True) + train_with_grad(model, data, optimizer, config, loss_fn=loss_fn) ``` """ - assert loss_fn is not None, "Provide a valid loss function" - # Move model to device before optimizer is loaded model = model.to(device) @@ -128,14 +126,9 @@ def loss_fn(model: torch.nn.Module, data: torch.Tensor) -> tuple[torch.Tensor, d TaskProgressColumn(), TimeRemainingColumn(elapsed_when_finished=True), ) - if isinstance(dataloader, (list, tuple)): - from qadence.ml_tools.data import to_dataloader - assert len(dataloader) == 2, "Please provide exactly two torch tensors." - x, y = dataloader - dataloader = to_dataloader(x=x, y=y, batch_size=config.batch_size) with progress: - dl_iter = iter(dataloader) if isinstance(dataloader, DictDataLoader) else None + dl_iter = iter(dataloader) if dataloader is not None else None # outer epoch loop for iteration in progress.track(range(init_iter, init_iter + config.max_iter)): @@ -144,38 +137,18 @@ def loss_fn(model: torch.nn.Module, data: torch.Tensor) -> tuple[torch.Tensor, d # this is the case, for example, of quantum models # which do not have classical input data (e.g. chemistry) if dataloader is None: - loss, metrics = optimize_step( - model, optimizer, loss_fn, dataloader, device=device - ) + loss, metrics = optimize_step(model, optimizer, loss_fn, None) loss = loss.item() - # single epoch with DictDataloader using a single iteration method - # DictDataloader returns a single sample of the data - # with a given batch size decided when the dataloader is defined - elif isinstance(dataloader, DictDataLoader): - # resample all the time from the dataloader - # by creating a fresh iterator if the dataloader - # does not support automatically iterating datasets - if not dataloader.has_automatic_iter: - dl_iter = iter(dataloader) - data = next(dl_iter) # type: ignore[arg-type] - loss, metrics = optimize_step(model, optimizer, loss_fn, data, device=device) - - elif isinstance(dataloader, DataLoader): - # single-epoch with standard DataLoader - # otherwise a standard PyTorch DataLoader behavior - # is assumed with optional mini-batches - running_loss = 0.0 - for i, data in enumerate(dataloader): - # TODO: make sure to average metrics as well - loss, metrics = optimize_step( - model, optimizer, loss_fn, data, device=device - ) - running_loss += loss.item() - loss = running_loss / (i + 1) + elif isinstance(dataloader, (DictDataLoader, DataLoader)): + data = data_to_device(next(dl_iter), device) # type: ignore[arg-type] + loss, metrics = optimize_step(model, optimizer, loss_fn, data) else: - raise NotImplementedError("Unsupported dataloader type!") + raise NotImplementedError( + f"Unsupported dataloader type: {type(dataloader)}. " + "You can use e.g. `qadence.ml_tools.to_dataloader` to build a dataloader." + ) if iteration % config.print_every == 0 and config.verbose: print_metrics(loss, metrics, iteration) diff --git a/qadence/ml_tools/train_no_grad.py b/qadence/ml_tools/train_no_grad.py index 57d269b8..83f23f84 100644 --- a/qadence/ml_tools/train_no_grad.py +++ b/qadence/ml_tools/train_no_grad.py @@ -83,32 +83,16 @@ def _update_parameters( TimeRemainingColumn(elapsed_when_finished=True), ) with progress: - dl_iter = iter(dataloader) if isinstance(dataloader, DictDataLoader) else None + dl_iter = iter(dataloader) if dataloader is not None else None for iteration in progress.track(range(init_iter, init_iter + config.max_iter)): if dataloader is None: loss, metrics, ng_params = _update_parameters(None, ng_params) - elif isinstance(dataloader, DictDataLoader): - # resample all the time from the dataloader - # by creating a fresh iterator if the dataloader - # does not support automatically iterating datasets - if not dataloader.has_automatic_iter: - dl_iter = iter(dataloader) - + elif isinstance(dataloader, (DictDataLoader, DataLoader)): data = next(dl_iter) # type: ignore[arg-type] loss, metrics, ng_params = _update_parameters(data, ng_params) - elif isinstance(dataloader, DataLoader): - # single-epoch with standard DataLoader - # otherwise a standard PyTorch DataLoader behavior - # is assumed with optional mini-batches - running_loss = 0.0 - for i, data in enumerate(dataloader): - loss, metrics, ng_params = _update_parameters(data, ng_params) - running_loss += loss - loss = running_loss / (i + 1) - else: raise NotImplementedError("Unsupported dataloader type!") diff --git a/tests/ml_tools/test_checkpointing.py b/tests/ml_tools/test_checkpointing.py index 00c2ba93..065a5137 100644 --- a/tests/ml_tools/test_checkpointing.py +++ b/tests/ml_tools/test_checkpointing.py @@ -5,7 +5,7 @@ from pathlib import Path import torch -from torch.utils.data import DataLoader, TensorDataset +from torch.utils.data import DataLoader from qadence.ml_tools import ( TrainConfig, @@ -13,19 +13,17 @@ train_with_grad, write_checkpoint, ) +from qadence.ml_tools.data import to_dataloader from qadence.ml_tools.models import TransformedModule from qadence.ml_tools.parameters import get_parameters, set_parameters from qadence.ml_tools.utils import rand_featureparameters from qadence.models import QNN, QuantumModel -def dataloader() -> DataLoader: - batch_size = 25 +def dataloader(batch_size: int = 25) -> DataLoader: x = torch.linspace(0, 1, batch_size).reshape(-1, 1) y = torch.cos(x) - - dataset = TensorDataset(x, y) - return DataLoader(dataset, batch_size=batch_size) + return to_dataloader(x, y, batch_size=batch_size, infinite=True) def test_basic_save_load_ckpts(Basic: torch.nn.Module, tmp_path: Path) -> None: diff --git a/tests/ml_tools/test_train.py b/tests/ml_tools/test_train.py index 8ac8c37c..071d2cbd 100644 --- a/tests/ml_tools/test_train.py +++ b/tests/ml_tools/test_train.py @@ -7,9 +7,9 @@ import numpy as np import pytest import torch -from torch.utils.data import DataLoader, TensorDataset +from torch.utils.data import DataLoader -from qadence.ml_tools import DictDataLoader, TrainConfig, train_with_grad +from qadence.ml_tools import DictDataLoader, TrainConfig, to_dataloader, train_with_grad from qadence.ml_tools.models import TransformedModule from qadence.models import QNN @@ -17,41 +17,27 @@ np.random.seed(42) -def dataloader() -> DataLoader: - batch_size = 25 +def dataloader(batch_size: int = 25) -> DataLoader: x = torch.linspace(0, 1, batch_size).reshape(-1, 1) y = torch.sin(x) + return to_dataloader(x, y, batch_size=batch_size, infinite=True) - dataset = TensorDataset(x, y) - return DataLoader(dataset, batch_size=batch_size) - - -def dictdataloader() -> DictDataLoader: - batch_size = 25 - - keys = ["y1", "y2"] - dls = {} - for k in keys: - x = torch.rand(batch_size, 1) - y = torch.sin(x) - dataset = TensorDataset(x, y) - dataloader = DataLoader(dataset, batch_size=batch_size) - dls[k] = dataloader - return DictDataLoader(dls, has_automatic_iter=False) +def dictdataloader(batch_size: int = 25) -> DictDataLoader: + x = torch.rand(batch_size, 1) + y = torch.sin(x) + dls = { + "y1": to_dataloader(x, y, batch_size=batch_size, infinite=True), + "y2": to_dataloader(x, y, batch_size=batch_size, infinite=True), + } + return DictDataLoader(dls) def FMdictdataloader(param_name: str = "phi", n_qubits: int = 2) -> DictDataLoader: batch_size = 1 - - dls = {} x = torch.rand(batch_size, 1) y = torch.sin(x) - dataset = TensorDataset(x, y) - dataloader = DataLoader(dataset, batch_size=batch_size) - dls[param_name] = dataloader - - return DictDataLoader(dls, has_automatic_iter=False) + return DictDataLoader({param_name: to_dataloader(x, y, batch_size=batch_size, infinite=True)}) @pytest.mark.flaky(max_runs=10) @@ -110,12 +96,13 @@ def loss_fn(model: torch.nn.Module, xs: Any = None) -> tuple[torch.Tensor, dict] @pytest.mark.flaky(max_runs=10) def test_train_dictdataloader(tmp_path: Path, Basic: torch.nn.Module) -> None: - data = dictdataloader() + batch_size = 25 + data = dictdataloader(batch_size=batch_size) model = Basic cnt = count() criterion = torch.nn.MSELoss() - optimizer = torch.optim.Adam(model.parameters(), lr=0.1) + optimizer = torch.optim.Adam(model.parameters(), lr=0.01) def loss_fn(model: torch.nn.Module, data: torch.Tensor) -> tuple[torch.Tensor, dict]: next(cnt) @@ -186,7 +173,8 @@ def loss_fn(model: torch.nn.Module, data: torch.Tensor) -> tuple[torch.Tensor, d write_every=100, batch_size=batch_size, ) - train_with_grad(model, (x, y), optimizer, config, loss_fn=loss_fn) + data = to_dataloader(x, y, batch_size=batch_size, infinite=True) + train_with_grad(model, data, optimizer, config, loss_fn=loss_fn) assert next(cnt) == n_epochs x = torch.rand(5, 1) diff --git a/tests/ml_tools/test_train_no_grad.py b/tests/ml_tools/test_train_no_grad.py index 03553542..dc3ecc09 100644 --- a/tests/ml_tools/test_train_no_grad.py +++ b/tests/ml_tools/test_train_no_grad.py @@ -7,9 +7,9 @@ import nevergrad as ng import numpy as np import torch -from torch.utils.data import DataLoader, TensorDataset +from torch.utils.data import DataLoader -from qadence.ml_tools import TrainConfig, num_parameters, train_gradient_free +from qadence.ml_tools import TrainConfig, num_parameters, to_dataloader, train_gradient_free # ensure reproducibility SEED = 42 @@ -19,13 +19,10 @@ torch.manual_seed(SEED) -def dataloader() -> DataLoader: - batch_size = 25 +def dataloader(batch_size: int = 25) -> DataLoader: x = torch.linspace(0, 1, batch_size).reshape(-1, 1) y = torch.cos(x) - - dataset = TensorDataset(x, y) - return DataLoader(dataset, batch_size=batch_size) + return to_dataloader(x, y, batch_size=batch_size, infinite=True) def test_train_dataloader_default(tmp_path: Path, Basic: torch.nn.Module) -> None: From 69abe6cd313b05c956ad2803241df00a0b1218e4 Mon Sep 17 00:00:00 2001 From: seitzdom <39090287+dominikandreasseitz@users.noreply.github.com> Date: Mon, 6 Nov 2023 17:57:54 +0100 Subject: [PATCH 57/83] [Fix] Braket CPHASE always having fixed parameters (#162) --- qadence/backends/braket/convert_ops.py | 20 +++++++++++++------- tests/backends/test_backends.py | 12 +++++++++++- 2 files changed, 24 insertions(+), 8 deletions(-) diff --git a/qadence/backends/braket/convert_ops.py b/qadence/backends/braket/convert_ops.py index 5517203b..67255caa 100644 --- a/qadence/backends/braket/convert_ops.py +++ b/qadence/backends/braket/convert_ops.py @@ -81,13 +81,19 @@ def BraketOperation(block: PrimitiveBlock) -> Instruction: return two_qubit[operation](block.qubit_support[0], block.qubit_support[1]) elif operation in two_qubit_parametrized: - (expr,) = block.parameters.expressions() # type: ignore [attr-defined] - angle_value = evaluate(expr) - return two_qubit_parametrized[operation]( - control=block.qubit_support[0], - target=block.qubit_support[1], - angle=angle_value, - ) + ((uuid, expr),) = block.parameters.items() # type: ignore [attr-defined] + if expr.is_number: + return two_qubit_parametrized[operation]( + control=block.qubit_support[0], + target=block.qubit_support[1], + angle=evaluate(expr), + ) + else: + return two_qubit_parametrized[operation]( + control=block.qubit_support[0], + target=block.qubit_support[1], + angle=FreeParameter(uuid), + ) elif operation in three_qubit: return three_qubit[operation]( block.qubit_support[0], block.qubit_support[1], block.qubit_support[2] diff --git a/tests/backends/test_backends.py b/tests/backends/test_backends.py index 2296770e..b7f0ced6 100644 --- a/tests/backends/test_backends.py +++ b/tests/backends/test_backends.py @@ -11,13 +11,13 @@ from metrics import ATOL_DICT, JS_ACCEPTANCE # type: ignore from torch import Tensor -from qadence import BackendName, DiffMode from qadence.backend import BackendConfiguration from qadence.backends.api import backend_factory, config_factory from qadence.blocks import AbstractBlock, chain, kron from qadence.circuit import QuantumCircuit from qadence.constructors import total_magnetization from qadence.divergences import js_divergence +from qadence.execution import run from qadence.ml_tools.utils import rand_featureparameters from qadence.models import QuantumModel from qadence.operations import CPHASE, RX, RY, H, I, X @@ -30,6 +30,7 @@ zero_state, ) from qadence.transpile import flatten +from qadence.types import BackendName, DiffMode from qadence.utils import nqubits_to_basis BACKENDS = BackendName.list() @@ -345,3 +346,12 @@ def test_custom_transpilation_passes() -> None: assert conv.circuit.original == conv_no_transp.circuit.original assert conv.circuit.abstract != conv_no_transp.circuit.abstract + + +def test_braket_parametric_cphase() -> None: + param_name = "y" + block = chain(X(0), H(1), CPHASE(0, 1, param_name)) + values = {param_name: torch.rand(1)} + equivalent_state( + run(block, values=values, backend="braket"), run(block, values=values, backend="pyqtorch") + ) From 1164a3176846c8e83383676fca4415ae27aef577 Mon Sep 17 00:00:00 2001 From: Gergana Velikova Date: Tue, 7 Nov 2023 11:34:24 +0100 Subject: [PATCH 58/83] fix documentation warning --- qadence/backends/pytorch_wrapper.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/qadence/backends/pytorch_wrapper.py b/qadence/backends/pytorch_wrapper.py index 2abd3e54..63da4e7f 100644 --- a/qadence/backends/pytorch_wrapper.py +++ b/qadence/backends/pytorch_wrapper.py @@ -291,7 +291,7 @@ def sample( param_values: The values of the parameters after embedding n_shots: The number of shots. Defaults to 1. state: Initial state. - error: A noise model to use. + noise: A noise model to use. endianness: Endianness of the resulting bitstrings. Returns: From 882d7309cd2c151507ef5cc4fd59876975881de9 Mon Sep 17 00:00:00 2001 From: Gergana Velikova Date: Tue, 7 Nov 2023 12:13:52 +0100 Subject: [PATCH 59/83] added a noise.md --- docs/realistic_sims/noise.md | 1 + 1 file changed, 1 insertion(+) create mode 100644 docs/realistic_sims/noise.md diff --git a/docs/realistic_sims/noise.md b/docs/realistic_sims/noise.md new file mode 100644 index 00000000..6c71c088 --- /dev/null +++ b/docs/realistic_sims/noise.md @@ -0,0 +1 @@ +This section introduces error models. From 1c2d2264c2d7c24a4a0417e42751dfdece4ebf9c Mon Sep 17 00:00:00 2001 From: Roland Guichard Date: Tue, 7 Nov 2023 11:37:34 +0000 Subject: [PATCH 60/83] Abstract noise application in module. --- qadence/backends/braket/backend.py | 12 +++--------- qadence/backends/pulser/backend.py | 12 +++--------- qadence/backends/pyqtorch/backend.py | 15 ++++----------- qadence/noise/protocols.py | 15 ++++++++++++++- tests/qadence/test_error_models.py | 11 ----------- 5 files changed, 24 insertions(+), 41 deletions(-) diff --git a/qadence/backends/braket/backend.py b/qadence/backends/braket/backend.py index 14216a13..a063cb48 100644 --- a/qadence/backends/braket/backend.py +++ b/qadence/backends/braket/backend.py @@ -18,6 +18,7 @@ from qadence.logger import get_logger from qadence.measurements import Measurements from qadence.noise import Noise +from qadence.noise.protocols import apply from qadence.overlap import overlap_exact from qadence.transpile import transpile from qadence.utils import Endianness @@ -156,15 +157,8 @@ def sample( samples = invert_endianness(samples) if noise is not None: - noise_fn = noise.get_noise_fn() - return noise_fn( # type: ignore[no-any-return] - counters=samples, - n_qubits=circuit.abstract.n_qubits, - options=noise.options, - n_shots=n_shots, - ) - else: - return samples + samples = apply(noise=noise, samples=samples) + return samples def expectation( self, diff --git a/qadence/backends/pulser/backend.py b/qadence/backends/pulser/backend.py index 31cdd7f6..c938fcb6 100644 --- a/qadence/backends/pulser/backend.py +++ b/qadence/backends/pulser/backend.py @@ -22,6 +22,7 @@ from qadence.logger import get_logger from qadence.measurements import Measurements from qadence.noise import Noise +from qadence.noise.protocols import apply from qadence.overlap import overlap_exact from qadence.register import Register from qadence.transpile import transpile @@ -268,15 +269,8 @@ def sample( samples = invert_endianness(samples) if noise is not None: - noise_fn = noise.get_noise_fn() - return noise_fn( # type: ignore[no-any-return] - counters=samples, - n_qubits=circuit.abstract.n_qubits, - options=noise.options, - n_shots=n_shots, - ) - else: - return samples + samples = apply(noise=noise, samples=samples) + return samples def expectation( self, diff --git a/qadence/backends/pyqtorch/backend.py b/qadence/backends/pyqtorch/backend.py index 7ca2449f..5dfefdaf 100644 --- a/qadence/backends/pyqtorch/backend.py +++ b/qadence/backends/pyqtorch/backend.py @@ -17,6 +17,7 @@ from qadence.logger import get_logger from qadence.measurements import Measurements from qadence.noise import Noise +from qadence.noise.protocols import apply from qadence.overlap import overlap_exact from qadence.states import zero_state from qadence.transpile import ( @@ -227,7 +228,7 @@ def _sample(_probs: Tensor, n_shots: int, endianness: Endianness, n_qubits: int) wf = self.run(circuit=circuit, param_values=param_values, state=state) probs = torch.abs(torch.pow(wf, 2)) - counters = list( + samples = list( map( lambda _probs: _sample( _probs=_probs, @@ -239,16 +240,8 @@ def _sample(_probs: Tensor, n_shots: int, endianness: Endianness, n_qubits: int) ) ) if noise is not None: - noise_fn = noise.get_noise_fn() - corrupted_counters: list = noise_fn( - counters=counters, - n_qubits=circuit.abstract.n_qubits, - options=noise.options, - n_shots=n_shots, - ) - return corrupted_counters - else: - return counters + samples = apply(noise=noise, samples=samples) + return samples def assign_parameters(self, circuit: ConvertedCircuit, param_values: dict[str, Tensor]) -> Any: raise NotImplementedError diff --git a/qadence/noise/protocols.py b/qadence/noise/protocols.py index 8578315d..3f120294 100644 --- a/qadence/noise/protocols.py +++ b/qadence/noise/protocols.py @@ -2,7 +2,7 @@ import importlib from dataclasses import dataclass -from typing import Callable, cast +from typing import Callable, Counter, cast PROTOCOL_TO_MODULE = { "readout": "qadence.noise.readout", @@ -33,3 +33,16 @@ def _from_dict(cls, d: dict) -> Noise | None: if d: return cls(d["protocol"], **d["options"]) return None + + +def apply(noise: Noise, samples: list[Counter]) -> list[Counter]: + """Apply noise to samples.""" + error_fn = noise.get_noise_fn() + # Get the number of qubits from the sample keys. + n_qubits = len(list(samples[0].keys())[0]) + # Get the number of shots from the sample values. + n_shots = sum(samples[0].values()) + noisy_samples: list = error_fn( + counters=samples, n_qubits=n_qubits, options=noise.options, n_shots=n_shots + ) + return noisy_samples diff --git a/tests/qadence/test_error_models.py b/tests/qadence/test_error_models.py index 89c18cf6..1e957d47 100644 --- a/tests/qadence/test_error_models.py +++ b/tests/qadence/test_error_models.py @@ -54,17 +54,6 @@ def test_bitstring_corruption( error_probability: float, counters: list, exp_corrupted_counters: list, n_qubits: int ) -> None: - # corrupted_bitstrings = [ - # bs_corruption( - # bitstring=bitstring, - # n_shots=n_shots, - # error_probability=error_probability, - # n_qubits=n_qubits, - # ) - # for bitstring, n_shots in counters[0].items() - # ] - - # corrupted_counters = [Counter(chain(*corrupted_bitstrings))] n_shots = 100 noise_matrix = create_noise_matrix(WhiteNoise.UNIFORM, n_shots, n_qubits) err_idx = np.array([(item).numpy() for i, item in enumerate(noise_matrix < error_probability)]) From 95a9f4341653249d5d9f7930073e5e052275eca5 Mon Sep 17 00:00:00 2001 From: Roland Guichard Date: Tue, 7 Nov 2023 14:09:13 +0000 Subject: [PATCH 61/83] Add test for non-deterministic bit flipping. --- tests/qadence/test_error_models.py | 33 +++++++++++++++++++++++++++++- 1 file changed, 32 insertions(+), 1 deletion(-) diff --git a/tests/qadence/test_error_models.py b/tests/qadence/test_error_models.py index 1e957d47..be90b350 100644 --- a/tests/qadence/test_error_models.py +++ b/tests/qadence/test_error_models.py @@ -5,6 +5,7 @@ import numpy as np import pytest import torch +from numpy.random import rand from sympy import acos import qadence as qd @@ -51,7 +52,7 @@ ), ], ) -def test_bitstring_corruption( +def test_bitstring_corruption_all_bitflips( error_probability: float, counters: list, exp_corrupted_counters: list, n_qubits: int ) -> None: n_shots = 100 @@ -70,6 +71,36 @@ def test_bitstring_corruption( ) +@pytest.mark.parametrize( + "error_probability, counters, n_qubits", + [ + ( + rand(), + [Counter({"00": 27, "01": 23, "10": 24, "11": 26})], + 2, + ), + ( + rand(), + [Counter({"001": 27, "010": 23, "101": 24, "110": 26})], + 3, + ), + ], +) +def test_bitstring_corruption_mixed_bitflips( + error_probability: float, counters: list, n_qubits: int +) -> None: + n_shots = 100 + noise_matrix = create_noise_matrix(WhiteNoise.UNIFORM, n_shots, n_qubits) + err_idx = np.array([(item).numpy() for i, item in enumerate(noise_matrix < error_probability)]) + sample = sample_to_matrix(counters[0]) + corrupted_counters = [ + bs_corruption(n_shots=n_shots, err_idx=err_idx, sample=sample, n_qubits=n_qubits) + ] + assert sum(corrupted_counters[0].values()) == n_shots + for noiseless, noisy in zip(counters, corrupted_counters): + assert js_divergence(noiseless, noisy) > 0.0 + + @pytest.mark.parametrize( "error_probability, n_shots, block, backend", [ From ab02534b7348b6dbea993a58924111eb1e9462df Mon Sep 17 00:00:00 2001 From: Roland Guichard Date: Tue, 7 Nov 2023 14:14:00 +0000 Subject: [PATCH 62/83] Better tests. --- tests/qadence/test_error_models.py | 11 ++--------- 1 file changed, 2 insertions(+), 9 deletions(-) diff --git a/tests/qadence/test_error_models.py b/tests/qadence/test_error_models.py index be90b350..e6cacc7a 100644 --- a/tests/qadence/test_error_models.py +++ b/tests/qadence/test_error_models.py @@ -96,8 +96,8 @@ def test_bitstring_corruption_mixed_bitflips( corrupted_counters = [ bs_corruption(n_shots=n_shots, err_idx=err_idx, sample=sample, n_qubits=n_qubits) ] - assert sum(corrupted_counters[0].values()) == n_shots for noiseless, noisy in zip(counters, corrupted_counters): + assert sum(noisy.values()) == n_shots assert js_divergence(noiseless, noisy) > 0.0 @@ -193,6 +193,7 @@ def test_readout_error_backends(backend: BackendName) -> None: ) +# TODO: Use strategies to test against randomly generated circuits. @pytest.mark.parametrize( "measurement_proto, options", [ @@ -200,25 +201,18 @@ def test_readout_error_backends(backend: BackendName) -> None: (Measurements.SHADOW, {"accuracy": 0.1, "confidence": 0.1}), ], ) -# @given(st.restricted_batched_circuits()) -# @settings(deadline=None) def test_readout_error_with_measurements( measurement_proto: Measurements, options: dict, - # circ_and_vals: tuple[QuantumCircuit, dict[str, Tensor]] ) -> None: - # circuit, inputs = circ_and_vals circuit = QuantumCircuit(2, kron(H(0), Z(1))) inputs: dict = dict() observable = hamiltonian_factory(circuit.n_qubits, detuning=Z) model = QuantumModel(circuit=circuit, observable=observable, diff_mode=DiffMode.GPSR) - # model.backend.backend.config._use_gate_params = True - noise = Noise(protocol=Noise.READOUT) measurement = Measurements(protocol=str(measurement_proto), options=options) - # measured = model.expectation(values=inputs, measurement=measurement) noisy = model.expectation(values=inputs, measurement=measurement, noise=noise) exact = model.expectation(values=inputs) if exact.numel() > 1: @@ -226,7 +220,6 @@ def test_readout_error_with_measurements( exact_val = torch.abs(exact_value).item() atol = exact_val / 3.0 if exact_val != 0.0 else 0.33 assert torch.allclose(noisy_value, exact_value, atol=atol) - else: exact_value = torch.abs(exact).item() atol = exact_value / 3.0 if exact_value != 0.0 else 0.33 From 74dfcd3f474ab6487db2961626a8470285d3cf9f Mon Sep 17 00:00:00 2001 From: Gergana Velikova Date: Tue, 7 Nov 2023 16:13:11 +0100 Subject: [PATCH 63/83] correct the warning --- qadence/backends/braket/backend.py | 2 +- qadence/backends/pulser/backend.py | 2 +- qadence/backends/pyqtorch/backend.py | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/qadence/backends/braket/backend.py b/qadence/backends/braket/backend.py index 14216a13..1efc0245 100644 --- a/qadence/backends/braket/backend.py +++ b/qadence/backends/braket/backend.py @@ -179,7 +179,7 @@ def expectation( # Noise is ignored if measurement protocol is not provided. if noise is not None and measurement is None: logger.warning( - f"Error of type {noise} are not implemented for expectation yet. " + f"Errors of type {noise} are not implemented for expectation yet. " "This is ignored for now." ) # Do not flip endianness here because then we would have to reverse the observable diff --git a/qadence/backends/pulser/backend.py b/qadence/backends/pulser/backend.py index 31cdd7f6..73022ad3 100644 --- a/qadence/backends/pulser/backend.py +++ b/qadence/backends/pulser/backend.py @@ -291,7 +291,7 @@ def expectation( # Noise is ignored if measurement protocol is not provided. if noise is not None and measurement is None: logger.warning( - f"Error of type {noise} are not implemented for expectation yet. " + f"Errors of type {noise} are not implemented for expectation yet. " "This is ignored for now." ) diff --git a/qadence/backends/pyqtorch/backend.py b/qadence/backends/pyqtorch/backend.py index 7ca2449f..c97770b7 100644 --- a/qadence/backends/pyqtorch/backend.py +++ b/qadence/backends/pyqtorch/backend.py @@ -186,7 +186,7 @@ def expectation( # Noise is ignored if measurement protocol is not provided. if noise is not None and measurement is None: logger.warning( - f"Error of type {noise} are not implemented for expectation yet. " + f"Errors of type {noise} are not implemented for expectation yet. " "This is ignored for now." ) fn = self._looped_expectation if self.config.loop_expectation else self._batched_expectation From 0838d7479e1bcbbae43f3ebeb31eda315a58a475 Mon Sep 17 00:00:00 2001 From: Roland Guichard Date: Tue, 7 Nov 2023 17:20:11 +0000 Subject: [PATCH 64/83] A section about noise usage. --- docs/realistic_sims/noise.md | 75 +++++++++++++++++++++++++++++++++++- 1 file changed, 74 insertions(+), 1 deletion(-) diff --git a/docs/realistic_sims/noise.md b/docs/realistic_sims/noise.md index 6c71c088..60e4bb0e 100644 --- a/docs/realistic_sims/noise.md +++ b/docs/realistic_sims/noise.md @@ -1 +1,74 @@ -This section introduces error models. +Running programs on NISQ devices often leads to partially useful results due to the presence of noise. +In order to perform realistic simulations, a number of noise models are supported in Qadence and +corresponding error mitigation techniques whenever possible. + +## Readout errors + +State Preparation and Measurement (SPAM) in the hardware is a major source of noise in the execution of +quantum programs. Qadence offers to simulate readout errors with the `Noise` protocol to corrupt the output +samples of a simulation, through execution via a `QuantumModel`: + +```python exec="on" source="material-block" result="json" +from qadence import QuantumModel, QuantumCircuit, kron, H, Z +from qadence import hamiltonian_factory +from qadence.noise import Noise + +# Simple circuit and observable construction. +block = kron(H(0), Z(1)) +circuit = QuantumCircuit(2, block) +observable = hamiltonian_factory(circuit.n_qubits, detuning=Z) + +# Construct a quantum model. +model = QuantumModel(circuit=circuit, observable=observable) + +# Define a noise model to use. +noise = Noise(protocol=Noise.READOUT) + +# Run noiseless and noisy simulations. +noiseless_samples = model.sample(n_shots=100) +noisy_samples = model.sample(noise=noise, n_shots=100) + +print(f"noiseless = {noiseless_samples}") # markdown-exec: hide +print(f"noisy = {noisy_samples}") # markdown-exec: hide +``` + +It is possible to pass options to the noise model. In the previous example, a noise matrix is implicitly computed from a +uniform distribution. The `option` dictionary argument accepts the following options: + +- `seed`: defaulted to `None`, for reproducibility purposes +- `error_probability`: defaulted to 0.1, a bit flip probability +- `noise_distribution`: defaulted to `WhiteNoise.UNIFORM`, for non-uniform noise distributions +- `noise_matrix`: defaulted to `None`, if the noise matrix is known from third-party experiments, _i.e._ hardware calibration. + +Noisy simulations go hand-in-hand with measurement protocols discussed in the previous [section](measurements.md), to assess the impact of noise on expectation values. In this case, both measurement and noise protocols have to be defined appropriately. Please note that a noise protocol without a measurement protocol will be ignored for expectation values computations. + + +```python exec="on" source="material-block" result="json" +from qadence import QuantumModel, QuantumCircuit, kron, H, Z +from qadence import hamiltonian_factory +from qadence.noise import Noise +from qadence.measurements import Measurements + +# Simple circuit and observable construction. +block = kron(H(0), Z(1)) +circuit = QuantumCircuit(2, block) +observable = hamiltonian_factory(circuit.n_qubits, detuning=Z) + +# Construct a quantum model. +model = QuantumModel(circuit=circuit, observable=observable) + +# Define a noise model with options. +options = {"error_probability": 0.01} +noise = Noise(protocol=Noise.READOUT, options=options) + +# Define a tomographical measurement protocol with options. +options = {"n_shots": 10000} +measurement = Measurements(protocol=Measurements.TOMOGRAPHY, options=options) + +# Run noiseless and noisy simulations. +noiseless_exp = model.expectation(measurement=measurement) +noisy_exp = model.expectation(measurement=measurement, noise=noise) + +print(f"noiseless = {noiseless_exp}") # markdown-exec: hide +print(f"noisy = {noisy_exp}") # markdown-exec: hide +``` From 8d135319c53d33dd9f81fccf4768407ff513e84b Mon Sep 17 00:00:00 2001 From: Roland Guichard Date: Tue, 7 Nov 2023 17:20:44 +0000 Subject: [PATCH 65/83] Re-run probabilitic test. --- tests/qadence/test_error_models.py | 1 + 1 file changed, 1 insertion(+) diff --git a/tests/qadence/test_error_models.py b/tests/qadence/test_error_models.py index e6cacc7a..d0651c58 100644 --- a/tests/qadence/test_error_models.py +++ b/tests/qadence/test_error_models.py @@ -101,6 +101,7 @@ def test_bitstring_corruption_mixed_bitflips( assert js_divergence(noiseless, noisy) > 0.0 +@pytest.mark.flaky(max_runs=5) @pytest.mark.parametrize( "error_probability, n_shots, block, backend", [ From 72a334b75ec5070f5c893cfe5aa9b5040742676a Mon Sep 17 00:00:00 2001 From: Roland Guichard Date: Tue, 7 Nov 2023 17:21:16 +0000 Subject: [PATCH 66/83] Better docstring. --- qadence/noise/readout.py | 12 +++++++----- 1 file changed, 7 insertions(+), 5 deletions(-) diff --git a/qadence/noise/readout.py b/qadence/noise/readout.py index 51db4608..d3c511de 100644 --- a/qadence/noise/readout.py +++ b/qadence/noise/readout.py @@ -155,13 +155,15 @@ def error( corruption. Args: - counters: Samples of bit string as Counters. + counters (list): Samples of bit string as Counters. n_qubits: Number of qubits in the bit string. n_shots: Number of shots to sample. - seed: Random seed value if any. - error_probability: Uniform error probability of wrong readout at any position - in the bit strings. - noise_distribution: Noise distribution. + options: A dict of options: + seed: Random seed value if any. + error_probability: Uniform error probability of wrong readout at any position + in the bit strings. + noise_distribution: Noise distribution. + noise_matrix: An input noise matrix if known. Returns: Samples of corrupted bit strings as list[Counter]. From 1c6d6c0861b23c53d3f003ccfd059b222f6cba39 Mon Sep 17 00:00:00 2001 From: Roland Guichard Date: Tue, 7 Nov 2023 17:35:21 +0000 Subject: [PATCH 67/83] Correct argument passing to superclass. --- qadence/models/qnn.py | 3 +++ 1 file changed, 3 insertions(+) diff --git a/qadence/models/qnn.py b/qadence/models/qnn.py index cdb1d064..32b55e31 100644 --- a/qadence/models/qnn.py +++ b/qadence/models/qnn.py @@ -75,6 +75,7 @@ def __init__( diff_mode=diff_mode, measurement=measurement, configuration=configuration, + noise=noise ) if self.out_features is None: @@ -121,6 +122,8 @@ def forward( values = self._format_to_dict(values) if measurement is None: measurement = self._measurement + if noise is None: + noise = self._noise return self.transform( self.expectation( From 37f7644c36fca46a351004b43fd7c613650ce973 Mon Sep 17 00:00:00 2001 From: Roland Guichard Date: Tue, 7 Nov 2023 17:38:02 +0000 Subject: [PATCH 68/83] Fix lint. --- qadence/models/qnn.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/qadence/models/qnn.py b/qadence/models/qnn.py index 32b55e31..4405ac2c 100644 --- a/qadence/models/qnn.py +++ b/qadence/models/qnn.py @@ -75,7 +75,7 @@ def __init__( diff_mode=diff_mode, measurement=measurement, configuration=configuration, - noise=noise + noise=noise, ) if self.out_features is None: From a6830f8f98e0089cc8f81abc34154bb94efcfe24 Mon Sep 17 00:00:00 2001 From: Roland Guichard Date: Tue, 7 Nov 2023 18:58:34 +0000 Subject: [PATCH 69/83] Enable sessions for variable reuse. --- docs/realistic_sims/noise.md | 15 ++------------- 1 file changed, 2 insertions(+), 13 deletions(-) diff --git a/docs/realistic_sims/noise.md b/docs/realistic_sims/noise.md index 60e4bb0e..506cd2ae 100644 --- a/docs/realistic_sims/noise.md +++ b/docs/realistic_sims/noise.md @@ -8,7 +8,7 @@ State Preparation and Measurement (SPAM) in the hardware is a major source of no quantum programs. Qadence offers to simulate readout errors with the `Noise` protocol to corrupt the output samples of a simulation, through execution via a `QuantumModel`: -```python exec="on" source="material-block" result="json" +```python exec="on" source="material-block" session="noise" result="json" from qadence import QuantumModel, QuantumCircuit, kron, H, Z from qadence import hamiltonian_factory from qadence.noise import Noise @@ -43,20 +43,9 @@ uniform distribution. The `option` dictionary argument accepts the following opt Noisy simulations go hand-in-hand with measurement protocols discussed in the previous [section](measurements.md), to assess the impact of noise on expectation values. In this case, both measurement and noise protocols have to be defined appropriately. Please note that a noise protocol without a measurement protocol will be ignored for expectation values computations. -```python exec="on" source="material-block" result="json" -from qadence import QuantumModel, QuantumCircuit, kron, H, Z -from qadence import hamiltonian_factory -from qadence.noise import Noise +```python exec="on" source="material-block" session="noise" result="json" from qadence.measurements import Measurements -# Simple circuit and observable construction. -block = kron(H(0), Z(1)) -circuit = QuantumCircuit(2, block) -observable = hamiltonian_factory(circuit.n_qubits, detuning=Z) - -# Construct a quantum model. -model = QuantumModel(circuit=circuit, observable=observable) - # Define a noise model with options. options = {"error_probability": 0.01} noise = Noise(protocol=Noise.READOUT, options=options) From 2d81353b1cff00bf33c125912f82ce63803c43ac Mon Sep 17 00:00:00 2001 From: Roland Guichard Date: Wed, 8 Nov 2023 10:20:04 +0000 Subject: [PATCH 70/83] Fix docstrings. --- qadence/backend.py | 2 +- qadence/execution.py | 2 -- qadence/noise/readout.py | 13 ++++++++----- 3 files changed, 9 insertions(+), 8 deletions(-) diff --git a/qadence/backend.py b/qadence/backend.py index 1b205abc..9689a693 100644 --- a/qadence/backend.py +++ b/qadence/backend.py @@ -168,7 +168,7 @@ def observable(self, observable: AbstractBlock, n_qubits: int) -> ConvertedObser def convert( self, circuit: QuantumCircuit, observable: list[AbstractBlock] | AbstractBlock | None = None ) -> Converted: - """Convert an abstract circuit (and optionally and observable) to their native representation. + """Convert an abstract circuit and an optional observable to their native representation. Additionally this function constructs an embedding function which maps from user-facing parameters to device parameters (read more on parameter embedding diff --git a/qadence/execution.py b/qadence/execution.py index f2aac8da..11d4ce18 100644 --- a/qadence/execution.py +++ b/qadence/execution.py @@ -113,7 +113,6 @@ def sample( ) -> list[Counter]: """Convenience wrapper for the `QuantumModel.sample` method. - Arguments: x: Circuit, block, or (register+block) to run. values: User-facing parameter dict. @@ -183,7 +182,6 @@ def expectation( ) -> Tensor: """Convenience wrapper for the `QuantumModel.expectation` method. - Arguments: x: Circuit, block, or (register+block) to run. observable: Observable(s) w.r.t. which the expectation is computed. diff --git a/qadence/noise/readout.py b/qadence/noise/readout.py index d3c511de..a040f83e 100644 --- a/qadence/noise/readout.py +++ b/qadence/noise/readout.py @@ -17,13 +17,13 @@ class WhiteNoise(Enum): """White noise distributions.""" UNIFORM = staticmethod(uniform.Uniform(low=0.0, high=1.0)) - "Uniform white noise." + """Uniform white noise.""" GAUSSIAN = staticmethod(normal.Normal(loc=0.0, scale=1.0)) - "Gaussian white noise." + """Gaussian white noise.""" POISSON = staticmethod(poisson.Poisson(rate=0.1)) - "Poisson white noise." + """Poisson white noise.""" def bitstring_to_array(bitstring: str) -> np.array: @@ -90,6 +90,7 @@ def create_noise_matrix( ) -> np.array: """ A helper function that creates a noise matrix for bit string corruption. + NB: The noise matrix is not square, as all bits are considered independent. Args: @@ -112,7 +113,8 @@ def bs_corruption( sample: np.array, ) -> Counter: """ - A function that incorporates the expected readout error in a sample of bit strings + A function that incorporates the expected readout error in a sample of bit strings. + given a noise matrix. Args: @@ -151,7 +153,8 @@ def error( options: dict = dict(), ) -> list[Counter]: """ - Implements a simple uniform readout error model for position-independent bit string + Implements a simple uniform readout error model for position-independent bit string. + corruption. Args: From 72e1f7510ce7e81a8ac7b5d0fd36e7933ff77f02 Mon Sep 17 00:00:00 2001 From: Gergana Velikova Date: Wed, 8 Nov 2023 13:48:28 +0100 Subject: [PATCH 71/83] changed to torch.Tensor --- qadence/noise/readout.py | 72 ++++++++++++++++++------------ tests/qadence/test_error_models.py | 5 +-- 2 files changed, 45 insertions(+), 32 deletions(-) diff --git a/qadence/noise/readout.py b/qadence/noise/readout.py index a040f83e..6db7899f 100644 --- a/qadence/noise/readout.py +++ b/qadence/noise/readout.py @@ -2,7 +2,7 @@ from collections import Counter from enum import Enum -from itertools import chain +from typing import Any import numpy as np import torch @@ -26,30 +26,40 @@ class WhiteNoise(Enum): """Poisson white noise.""" -def bitstring_to_array(bitstring: str) -> np.array: +def bitstring_to_tensor(bitstring: str, output_type: str = "torch") -> np.array | torch.Tensor: """ - A helper function to convert bit strings to numpy arrays. + A helper function to convert bit strings to torch.Tensor or numpy.array. Args: bitstring: A str format of a bit string. + output_type: A str torch | numpy for the type of the output. + Default torch. Returns: - A numpy array out of the input bit string. + A torch.Tensor or np.array out of the input bit string. """ - return np.array(list(bitstring)).astype(int) + return ( + torch.as_tensor(list(map(int, bitstring))) + if output_type == "torch" + else np.array(list(bitstring)).astype(int) + ) -def array_to_bitstring(bitstring: np.array) -> str: +def tensor_to_bitstring(bitstring: torch.Tensor | np.array, output_type: str = "torch") -> str: """ - A helper function to convert numpy arrays to bit strings. + A helper function to convert torch.Tensor or numpy.array to bit strings. Args: - bitstring: A numpy array format of a bit string. + bitstring: A torch.Tensor or numpy.array format of a bit string. Returns: A str out of the input bit string. """ - return "".join(bitstring.astype("str")) + return ( + "".join(list(map(str, bitstring.detach().tolist()))) + if output_type == "torch" + else "".join(bitstring.astype("str")) + ) def bit_flip(bit: int) -> int: @@ -65,7 +75,7 @@ def bit_flip(bit: int) -> int: return 1 if bit == 0 else 0 -def sample_to_matrix(sample: dict) -> np.array: +def sample_to_matrix(sample: dict) -> torch.Tensor: """ A helper function that maps a sample dict to a bit string array. @@ -73,21 +83,24 @@ def sample_to_matrix(sample: dict) -> np.array: sample: A dictionary with bit stings as keys and values as their counts. - Returns: A numpy array (matrix) of bit strings n_shots x n_qubits. + Returns: A torch.Tensor of bit strings n_shots x n_qubits. """ - return np.array( - [ - i - for i in chain.from_iterable( - [sample[bitstring] * [bitstring_to_array(bitstring)] for bitstring in sample.keys()] + + return torch.concatenate( + list( + map( + lambda bitstring: torch.broadcast_to( + bitstring_to_tensor(bitstring), [sample[bitstring], len(bitstring)] + ), + sample.keys(), ) - ] + ) ) def create_noise_matrix( noise_distribution: torch.distributions, n_shots: int, n_qubits: int -) -> np.array: +) -> torch.Tensor: """ A helper function that creates a noise matrix for bit string corruption. @@ -110,7 +123,7 @@ def bs_corruption( n_shots: int, n_qubits: int, err_idx: list, - sample: np.array, + sample: torch.Tensor, ) -> Counter: """ A function that incorporates the expected readout error in a sample of bit strings. @@ -121,25 +134,26 @@ def bs_corruption( n_qubits: Number of qubits in the bit string. n_shots: Number of shots to sample. err_idx: A Boolean array of bit string indices that need to be corrupted. - sample: A matrix of bit strings n_shots x n_qubits. + sample: A torch.Tensor of bit strings n_shots x n_qubits. Returns: A counter of bit strings after readout corruption. """ - def vflip(sample: int, err_idx: bool, idx: int) -> np.array: - if err_idx: - return bit_flip(sample) + def vflip(sample: torch.Tensor, err_idx: torch.Tensor) -> int | Any: + if err_idx.item(): + return bit_flip(sample.item()) else: - return sample - - vbit_ind = np.vectorize(vflip) + return sample.item() return Counter( [ - "".join(k) + "".join(list(map(str, k))) for k in map( - lambda i: vbit_ind(sample[i], err_idx[i], range(n_qubits)).astype(str).tolist(), + lambda j: map( + lambda i: vflip(sample[j][i], err_idx[j][i]), + range(n_qubits), + ), range(n_shots), ) ] @@ -196,7 +210,7 @@ def error( # the simplest approach - an event occurs if its probability is higher than expected # by random chance - err_idx = np.array([(item).numpy() for i, item in enumerate(noise_matrix < error_probability)]) + err_idx = torch.as_tensor(noise_matrix < error_probability) corrupted_bitstrings = [] for counter in counters: diff --git a/tests/qadence/test_error_models.py b/tests/qadence/test_error_models.py index d0651c58..46e0ecff 100644 --- a/tests/qadence/test_error_models.py +++ b/tests/qadence/test_error_models.py @@ -2,7 +2,6 @@ from collections import Counter -import numpy as np import pytest import torch from numpy.random import rand @@ -57,7 +56,7 @@ def test_bitstring_corruption_all_bitflips( ) -> None: n_shots = 100 noise_matrix = create_noise_matrix(WhiteNoise.UNIFORM, n_shots, n_qubits) - err_idx = np.array([(item).numpy() for i, item in enumerate(noise_matrix < error_probability)]) + err_idx = torch.as_tensor(noise_matrix < error_probability) sample = sample_to_matrix(counters[0]) corrupted_counters = [ bs_corruption(n_shots=n_shots, err_idx=err_idx, sample=sample, n_qubits=n_qubits) @@ -91,7 +90,7 @@ def test_bitstring_corruption_mixed_bitflips( ) -> None: n_shots = 100 noise_matrix = create_noise_matrix(WhiteNoise.UNIFORM, n_shots, n_qubits) - err_idx = np.array([(item).numpy() for i, item in enumerate(noise_matrix < error_probability)]) + err_idx = torch.as_tensor(noise_matrix < error_probability) sample = sample_to_matrix(counters[0]) corrupted_counters = [ bs_corruption(n_shots=n_shots, err_idx=err_idx, sample=sample, n_qubits=n_qubits) From 211a7b8e07b5d2e40d0e36c35d7ba5a802dba5da Mon Sep 17 00:00:00 2001 From: Gergana Velikova Date: Wed, 8 Nov 2023 16:31:02 +0100 Subject: [PATCH 72/83] refactoring --- qadence/noise/readout.py | 5 +---- 1 file changed, 1 insertion(+), 4 deletions(-) diff --git a/qadence/noise/readout.py b/qadence/noise/readout.py index 6db7899f..473ed5c4 100644 --- a/qadence/noise/readout.py +++ b/qadence/noise/readout.py @@ -141,10 +141,7 @@ def bs_corruption( """ def vflip(sample: torch.Tensor, err_idx: torch.Tensor) -> int | Any: - if err_idx.item(): - return bit_flip(sample.item()) - else: - return sample.item() + return bit_flip(sample.item()) if err_idx.item() else sample.item() return Counter( [ From 000e47b5b7b8b6267608b36e876b48937072527a Mon Sep 17 00:00:00 2001 From: Gergana Velikova Date: Wed, 8 Nov 2023 17:27:03 +0100 Subject: [PATCH 73/83] functorch for bit flipping --- qadence/noise/readout.py | 21 ++++----------------- 1 file changed, 4 insertions(+), 17 deletions(-) diff --git a/qadence/noise/readout.py b/qadence/noise/readout.py index 473ed5c4..9b2fbf18 100644 --- a/qadence/noise/readout.py +++ b/qadence/noise/readout.py @@ -2,7 +2,6 @@ from collections import Counter from enum import Enum -from typing import Any import numpy as np import torch @@ -62,7 +61,7 @@ def tensor_to_bitstring(bitstring: torch.Tensor | np.array, output_type: str = " ) -def bit_flip(bit: int) -> int: +def bit_flip(bit: torch.Tensor, cond: torch.Tensor) -> torch.Tensor: """ A helper function that reverses the states 0 and 1 in the bit string. @@ -72,7 +71,7 @@ def bit_flip(bit: int) -> int: Returns: The inverse value of the input bit """ - return 1 if bit == 0 else 0 + return torch.where(cond, torch.where(bit == 0, 1, 0), bit) def sample_to_matrix(sample: dict) -> torch.Tensor: @@ -140,21 +139,9 @@ def bs_corruption( A counter of bit strings after readout corruption. """ - def vflip(sample: torch.Tensor, err_idx: torch.Tensor) -> int | Any: - return bit_flip(sample.item()) if err_idx.item() else sample.item() + func = torch.func.vmap(bit_flip) - return Counter( - [ - "".join(list(map(str, k))) - for k in map( - lambda j: map( - lambda i: vflip(sample[j][i], err_idx[j][i]), - range(n_qubits), - ), - range(n_shots), - ) - ] - ) + return Counter([tensor_to_bitstring(k) for k in func(sample, err_idx)]) def error( From 8f1685c4b25e38fb79b210e09d6d4f7893ab4e1b Mon Sep 17 00:00:00 2001 From: Gergana Velikova Date: Wed, 8 Nov 2023 17:35:12 +0100 Subject: [PATCH 74/83] docstring --- qadence/noise/readout.py | 1 + 1 file changed, 1 insertion(+) diff --git a/qadence/noise/readout.py b/qadence/noise/readout.py index 9b2fbf18..fbc1c5ce 100644 --- a/qadence/noise/readout.py +++ b/qadence/noise/readout.py @@ -67,6 +67,7 @@ def bit_flip(bit: torch.Tensor, cond: torch.Tensor) -> torch.Tensor: Args: bit: A integer-value bit in a bitstring to be inverted. + cond: A Bool value of whether or not a bit should be flipped. Returns: The inverse value of the input bit From 01be3f7f22935c54255521c11ee31a608e9a5d65 Mon Sep 17 00:00:00 2001 From: Gergana Velikova Date: Wed, 8 Nov 2023 17:43:23 +0100 Subject: [PATCH 75/83] some shortening --- qadence/noise/readout.py | 8 +------- tests/qadence/test_error_models.py | 8 ++------ 2 files changed, 3 insertions(+), 13 deletions(-) diff --git a/qadence/noise/readout.py b/qadence/noise/readout.py index fbc1c5ce..c6f059f5 100644 --- a/qadence/noise/readout.py +++ b/qadence/noise/readout.py @@ -120,8 +120,6 @@ def create_noise_matrix( def bs_corruption( - n_shots: int, - n_qubits: int, err_idx: list, sample: torch.Tensor, ) -> Counter: @@ -131,8 +129,6 @@ def bs_corruption( given a noise matrix. Args: - n_qubits: Number of qubits in the bit string. - n_shots: Number of shots to sample. err_idx: A Boolean array of bit string indices that need to be corrupted. sample: A torch.Tensor of bit strings n_shots x n_qubits. @@ -200,7 +196,5 @@ def error( corrupted_bitstrings = [] for counter in counters: sample = sample_to_matrix(counter) - corrupted_bitstrings.append( - bs_corruption(n_shots=n_shots, err_idx=err_idx, sample=sample, n_qubits=n_qubits) - ) + corrupted_bitstrings.append(bs_corruption(err_idx=err_idx, sample=sample)) return corrupted_bitstrings diff --git a/tests/qadence/test_error_models.py b/tests/qadence/test_error_models.py index 46e0ecff..2757f1b3 100644 --- a/tests/qadence/test_error_models.py +++ b/tests/qadence/test_error_models.py @@ -58,9 +58,7 @@ def test_bitstring_corruption_all_bitflips( noise_matrix = create_noise_matrix(WhiteNoise.UNIFORM, n_shots, n_qubits) err_idx = torch.as_tensor(noise_matrix < error_probability) sample = sample_to_matrix(counters[0]) - corrupted_counters = [ - bs_corruption(n_shots=n_shots, err_idx=err_idx, sample=sample, n_qubits=n_qubits) - ] + corrupted_counters = [bs_corruption(err_idx=err_idx, sample=sample)] assert sum(corrupted_counters[0].values()) == n_shots assert corrupted_counters == exp_corrupted_counters assert torch.allclose( @@ -92,9 +90,7 @@ def test_bitstring_corruption_mixed_bitflips( noise_matrix = create_noise_matrix(WhiteNoise.UNIFORM, n_shots, n_qubits) err_idx = torch.as_tensor(noise_matrix < error_probability) sample = sample_to_matrix(counters[0]) - corrupted_counters = [ - bs_corruption(n_shots=n_shots, err_idx=err_idx, sample=sample, n_qubits=n_qubits) - ] + corrupted_counters = [bs_corruption(err_idx=err_idx, sample=sample)] for noiseless, noisy in zip(counters, corrupted_counters): assert sum(noisy.values()) == n_shots assert js_divergence(noiseless, noisy) > 0.0 From 4ab9c5e50d0222caeeed368e971295a8885e6078 Mon Sep 17 00:00:00 2001 From: Gergana Velikova Date: Wed, 8 Nov 2023 17:47:31 +0100 Subject: [PATCH 76/83] arg type --- qadence/noise/readout.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/qadence/noise/readout.py b/qadence/noise/readout.py index c6f059f5..4c64353b 100644 --- a/qadence/noise/readout.py +++ b/qadence/noise/readout.py @@ -120,7 +120,7 @@ def create_noise_matrix( def bs_corruption( - err_idx: list, + err_idx: torch.Tensor, sample: torch.Tensor, ) -> Counter: """ From 37b2473db6dbd1c22fc02434cb1aab7feb4635ed Mon Sep 17 00:00:00 2001 From: Gergana Velikova Date: Wed, 8 Nov 2023 21:18:34 +0100 Subject: [PATCH 77/83] torch imports --- qadence/noise/readout.py | 15 ++++++++------- 1 file changed, 8 insertions(+), 7 deletions(-) diff --git a/qadence/noise/readout.py b/qadence/noise/readout.py index 4c64353b..2d62fb9c 100644 --- a/qadence/noise/readout.py +++ b/qadence/noise/readout.py @@ -5,6 +5,7 @@ import numpy as np import torch +from torch import Tensor from torch.distributions import normal, poisson, uniform from qadence.logger import get_logger @@ -25,7 +26,7 @@ class WhiteNoise(Enum): """Poisson white noise.""" -def bitstring_to_tensor(bitstring: str, output_type: str = "torch") -> np.array | torch.Tensor: +def bitstring_to_tensor(bitstring: str, output_type: str = "torch") -> np.array | Tensor: """ A helper function to convert bit strings to torch.Tensor or numpy.array. @@ -44,7 +45,7 @@ def bitstring_to_tensor(bitstring: str, output_type: str = "torch") -> np.array ) -def tensor_to_bitstring(bitstring: torch.Tensor | np.array, output_type: str = "torch") -> str: +def tensor_to_bitstring(bitstring: Tensor | np.array, output_type: str = "torch") -> str: """ A helper function to convert torch.Tensor or numpy.array to bit strings. @@ -61,7 +62,7 @@ def tensor_to_bitstring(bitstring: torch.Tensor | np.array, output_type: str = " ) -def bit_flip(bit: torch.Tensor, cond: torch.Tensor) -> torch.Tensor: +def bit_flip(bit: Tensor, cond: Tensor) -> Tensor: """ A helper function that reverses the states 0 and 1 in the bit string. @@ -75,7 +76,7 @@ def bit_flip(bit: torch.Tensor, cond: torch.Tensor) -> torch.Tensor: return torch.where(cond, torch.where(bit == 0, 1, 0), bit) -def sample_to_matrix(sample: dict) -> torch.Tensor: +def sample_to_matrix(sample: dict) -> Tensor: """ A helper function that maps a sample dict to a bit string array. @@ -100,7 +101,7 @@ def sample_to_matrix(sample: dict) -> torch.Tensor: def create_noise_matrix( noise_distribution: torch.distributions, n_shots: int, n_qubits: int -) -> torch.Tensor: +) -> Tensor: """ A helper function that creates a noise matrix for bit string corruption. @@ -120,8 +121,8 @@ def create_noise_matrix( def bs_corruption( - err_idx: torch.Tensor, - sample: torch.Tensor, + err_idx: Tensor, + sample: Tensor, ) -> Counter: """ A function that incorporates the expected readout error in a sample of bit strings. From 57c30cb958a4dd3e32f1ba3655a41fb11b5fa94c Mon Sep 17 00:00:00 2001 From: Gergana Velikova Date: Thu, 9 Nov 2023 10:04:57 +0100 Subject: [PATCH 78/83] minor fixes --- qadence/backends/braket/backend.py | 2 +- qadence/backends/pulser/backend.py | 2 +- qadence/backends/pyqtorch/backend.py | 2 +- qadence/noise/protocols.py | 2 +- qadence/noise/readout.py | 2 +- 5 files changed, 5 insertions(+), 5 deletions(-) diff --git a/qadence/backends/braket/backend.py b/qadence/backends/braket/backend.py index a37ffe6a..fe1b2520 100644 --- a/qadence/backends/braket/backend.py +++ b/qadence/backends/braket/backend.py @@ -173,7 +173,7 @@ def expectation( # Noise is ignored if measurement protocol is not provided. if noise is not None and measurement is None: logger.warning( - f"Errors of type {noise} are not implemented for expectation yet. " + f"Errors of type {noise} are not implemented for exact expectation yet. " "This is ignored for now." ) # Do not flip endianness here because then we would have to reverse the observable diff --git a/qadence/backends/pulser/backend.py b/qadence/backends/pulser/backend.py index 22329737..59ae9934 100644 --- a/qadence/backends/pulser/backend.py +++ b/qadence/backends/pulser/backend.py @@ -277,7 +277,7 @@ def expectation( # Noise is ignored if measurement protocol is not provided. if noise is not None and measurement is None: logger.warning( - f"Errors of type {noise} are not implemented for expectation yet. " + f"Errors of type {noise} are not implemented for exact expectation yet. " "This is ignored for now." ) diff --git a/qadence/backends/pyqtorch/backend.py b/qadence/backends/pyqtorch/backend.py index 56365fe3..faa7b376 100644 --- a/qadence/backends/pyqtorch/backend.py +++ b/qadence/backends/pyqtorch/backend.py @@ -187,7 +187,7 @@ def expectation( # Noise is ignored if measurement protocol is not provided. if noise is not None and measurement is None: logger.warning( - f"Errors of type {noise} are not implemented for expectation yet. " + f"Errors of type {noise} are not implemented for exact expectation yet. " "This is ignored for now." ) fn = self._looped_expectation if self.config.loop_expectation else self._batched_expectation diff --git a/qadence/noise/protocols.py b/qadence/noise/protocols.py index 3f120294..da0ba765 100644 --- a/qadence/noise/protocols.py +++ b/qadence/noise/protocols.py @@ -22,7 +22,7 @@ def get_noise_fn(self) -> Callable: module = importlib.import_module(PROTOCOL_TO_MODULE[self.protocol]) except KeyError: ImportError(f"The module corresponding to the protocol {self.protocol} is not found.") - fn = getattr(module, "error") + fn = getattr(module, "add_noise") return cast(Callable, fn) def _to_dict(self) -> dict: diff --git a/qadence/noise/readout.py b/qadence/noise/readout.py index 2d62fb9c..67358d16 100644 --- a/qadence/noise/readout.py +++ b/qadence/noise/readout.py @@ -142,7 +142,7 @@ def bs_corruption( return Counter([tensor_to_bitstring(k) for k in func(sample, err_idx)]) -def error( +def add_noise( counters: list[Counter], n_qubits: int, n_shots: int = 1000, From c7718a14df56e96f6ee14f304454caf139f3f1d7 Mon Sep 17 00:00:00 2001 From: Gergana Velikova Date: Thu, 9 Nov 2023 10:12:54 +0100 Subject: [PATCH 79/83] change in the bit flipping function for performance --- qadence/noise/readout.py | 10 +++++++++- 1 file changed, 9 insertions(+), 1 deletion(-) diff --git a/qadence/noise/readout.py b/qadence/noise/readout.py index 67358d16..d8839cce 100644 --- a/qadence/noise/readout.py +++ b/qadence/noise/readout.py @@ -73,7 +73,15 @@ def bit_flip(bit: Tensor, cond: Tensor) -> Tensor: Returns: The inverse value of the input bit """ - return torch.where(cond, torch.where(bit == 0, 1, 0), bit) + return torch.where( + cond, + torch.where( + bit == torch.zeros(1, dtype=torch.int64), + torch.ones(1, dtype=torch.int64), + torch.zeros(1, dtype=torch.int64), + ), + bit, + ) def sample_to_matrix(sample: dict) -> Tensor: From 3d6eb93da2e22bb4123060f1ff05fe5ca5664001 Mon Sep 17 00:00:00 2001 From: Roland Guichard Date: Thu, 9 Nov 2023 10:15:03 +0000 Subject: [PATCH 80/83] Log the file path. --- qadence/backends/pulser/backend.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/qadence/backends/pulser/backend.py b/qadence/backends/pulser/backend.py index 59ae9934..d031d8dc 100644 --- a/qadence/backends/pulser/backend.py +++ b/qadence/backends/pulser/backend.py @@ -34,7 +34,7 @@ from .devices import Device, IdealDevice, RealisticDevice from .pulses import add_pulses -logger = get_logger(__name__) +logger = get_logger(__file__) WEAK_COUPLING_CONST = 1.2 From 4072050b91a19c38db6229f1d43f37ac769a8510 Mon Sep 17 00:00:00 2001 From: Roland Guichard Date: Thu, 9 Nov 2023 10:21:19 +0000 Subject: [PATCH 81/83] Fix imports. --- qadence/backends/pulser/backend.py | 3 +-- qadence/measurements/shadow.py | 2 +- qadence/measurements/tomography.py | 2 +- 3 files changed, 3 insertions(+), 4 deletions(-) diff --git a/qadence/backends/pulser/backend.py b/qadence/backends/pulser/backend.py index 46a62620..f92da34e 100644 --- a/qadence/backends/pulser/backend.py +++ b/qadence/backends/pulser/backend.py @@ -25,8 +25,7 @@ from qadence.overlap import overlap_exact from qadence.register import Register from qadence.transpile import transpile -from qadence.types import BackendName -from qadence.utils import Endianness, get_logger +from qadence.types import BackendName, Endianness from .channels import GLOBAL_CHANNEL, LOCAL_CHANNEL from .cloud import get_client diff --git a/qadence/measurements/shadow.py b/qadence/measurements/shadow.py index 9d485095..e3bf7250 100644 --- a/qadence/measurements/shadow.py +++ b/qadence/measurements/shadow.py @@ -18,7 +18,7 @@ ) from qadence.blocks.composite import CompositeBlock from qadence.blocks.primitive import PrimitiveBlock -from qadence.blocks.utils import chain, get_pauli_blocks, kron, unroll_block_with_scaling +from qadence.blocks.utils import get_pauli_blocks, unroll_block_with_scaling from qadence.circuit import QuantumCircuit from qadence.noise import Noise from qadence.operations import X, Y, Z, chain, kron diff --git a/qadence/measurements/tomography.py b/qadence/measurements/tomography.py index 6c9091bc..6882d1d9 100644 --- a/qadence/measurements/tomography.py +++ b/qadence/measurements/tomography.py @@ -9,7 +9,7 @@ from qadence.backends import backend_factory from qadence.blocks import AbstractBlock, PrimitiveBlock -from qadence.blocks.utils import chain, unroll_block_with_scaling +from qadence.blocks.utils import unroll_block_with_scaling from qadence.circuit import QuantumCircuit from qadence.noise import Noise from qadence.operations import H, SDagger, X, Y, Z, chain From 1c600ce1f85bd253b90e79145aa77a13c60c3226 Mon Sep 17 00:00:00 2001 From: Gergana Velikova Date: Thu, 9 Nov 2023 11:36:59 +0100 Subject: [PATCH 82/83] minor docstring-related fixes --- qadence/backend.py | 2 +- qadence/noise/readout.py | 2 ++ 2 files changed, 3 insertions(+), 1 deletion(-) diff --git a/qadence/backend.py b/qadence/backend.py index 9689a693..35506dd4 100644 --- a/qadence/backend.py +++ b/qadence/backend.py @@ -170,7 +170,7 @@ def convert( ) -> Converted: """Convert an abstract circuit and an optional observable to their native representation. - Additionally this function constructs an embedding function which maps from + Additionally, this function constructs an embedding function which maps from user-facing parameters to device parameters (read more on parameter embedding [here][qadence.blocks.embedding.embedding]). """ diff --git a/qadence/noise/readout.py b/qadence/noise/readout.py index d8839cce..437a0191 100644 --- a/qadence/noise/readout.py +++ b/qadence/noise/readout.py @@ -51,6 +51,8 @@ def tensor_to_bitstring(bitstring: Tensor | np.array, output_type: str = "torch" Args: bitstring: A torch.Tensor or numpy.array format of a bit string. + output_type: A str torch | numpy for the type of the output. + Default torch. Returns: A str out of the input bit string. From b77e82036e1fdf9920c3cd95f08f3d5710a84d5e Mon Sep 17 00:00:00 2001 From: Gergana Velikova Date: Thu, 9 Nov 2023 11:51:46 +0100 Subject: [PATCH 83/83] removing numpy.array as an output/input option --- qadence/noise/readout.py | 29 ++++++++--------------------- 1 file changed, 8 insertions(+), 21 deletions(-) diff --git a/qadence/noise/readout.py b/qadence/noise/readout.py index 437a0191..679a1977 100644 --- a/qadence/noise/readout.py +++ b/qadence/noise/readout.py @@ -3,7 +3,6 @@ from collections import Counter from enum import Enum -import numpy as np import torch from torch import Tensor from torch.distributions import normal, poisson, uniform @@ -26,42 +25,30 @@ class WhiteNoise(Enum): """Poisson white noise.""" -def bitstring_to_tensor(bitstring: str, output_type: str = "torch") -> np.array | Tensor: +def bitstring_to_tensor(bitstring: str) -> Tensor: """ - A helper function to convert bit strings to torch.Tensor or numpy.array. + A helper function to convert bit strings to torch.Tensor. Args: bitstring: A str format of a bit string. - output_type: A str torch | numpy for the type of the output. - Default torch. Returns: - A torch.Tensor or np.array out of the input bit string. + A torch.Tensor out of the input bit string. """ - return ( - torch.as_tensor(list(map(int, bitstring))) - if output_type == "torch" - else np.array(list(bitstring)).astype(int) - ) + return torch.as_tensor(list(map(int, bitstring))) -def tensor_to_bitstring(bitstring: Tensor | np.array, output_type: str = "torch") -> str: +def tensor_to_bitstring(bitstring: Tensor) -> str: """ - A helper function to convert torch.Tensor or numpy.array to bit strings. + A helper function to convert torch.Tensor to bit strings. Args: - bitstring: A torch.Tensor or numpy.array format of a bit string. - output_type: A str torch | numpy for the type of the output. - Default torch. + bitstring: A torch.Tensor format of a bit string. Returns: A str out of the input bit string. """ - return ( - "".join(list(map(str, bitstring.detach().tolist()))) - if output_type == "torch" - else "".join(bitstring.astype("str")) - ) + return "".join(list(map(str, bitstring.detach().tolist()))) def bit_flip(bit: Tensor, cond: Tensor) -> Tensor: