Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

[Refactoring] Use Readout from Pyqtorch #599

Merged
merged 6 commits into from
Nov 7, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
11 changes: 0 additions & 11 deletions docs/tutorials/realistic_sims/noise.md
Original file line number Diff line number Diff line change
Expand Up @@ -92,23 +92,12 @@ print(f"noiseless = {noiseless_samples}") # markdown-exec: hide
print(f"noisy = {noisy_samples}") # markdown-exec: hide
```

Note we can apply directly the method `apply_readout_noise` to the noiseless samples as follows:

```python exec="on" source="material-block" session="noise" result="json"
from qadence.noise import apply_readout_noise
altered_samples = apply_readout_noise(noise, noiseless_samples)

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.

Expand Down
3 changes: 0 additions & 3 deletions qadence/backends/pulser/backend.py
Original file line number Diff line number Diff line change
Expand Up @@ -25,7 +25,6 @@
from qadence.mitigations import Mitigations
from qadence.mitigations.protocols import apply_mitigation
from qadence.noise import NoiseHandler
from qadence.noise.protocols import apply_readout_noise
from qadence.overlap import overlap_exact
from qadence.register import Register
from qadence.transpile import transpile
Expand Down Expand Up @@ -311,8 +310,6 @@ def sample(
from qadence.transpile import invert_endianness

samples = invert_endianness(samples)
if noise is not None:
samples = apply_readout_noise(noise=noise, samples=samples)
if mitigation is not None:
logger.warning(
"Mitigation protocol is deprecated. Use qadence-protocols instead.",
Expand Down
27 changes: 22 additions & 5 deletions qadence/backends/pyqtorch/backend.py
Original file line number Diff line number Diff line change
Expand Up @@ -23,7 +23,6 @@
from qadence.measurements import Measurements
from qadence.mitigations.protocols import Mitigations, apply_mitigation
from qadence.noise import NoiseHandler
from qadence.noise.protocols import apply_readout_noise
from qadence.transpile import (
chain_single_qubit_ops,
flatten,
Expand All @@ -34,7 +33,7 @@
from qadence.types import BackendName, Endianness, Engine

from .config import Configuration, default_passes
from .convert_ops import convert_block
from .convert_ops import convert_block, convert_readout_noise

logger = getLogger(__name__)

Expand Down Expand Up @@ -65,7 +64,16 @@ def circuit(self, circuit: QuantumCircuit) -> ConvertedCircuit:
circuit = transpile(*passes)(circuit)

ops = convert_block(circuit.block, n_qubits=circuit.n_qubits, config=self.config)
native = pyq.QuantumCircuit(circuit.n_qubits, ops)
readout_noise = (
convert_readout_noise(circuit.n_qubits, self.config.noise)
if self.config.noise
else None
)
native = pyq.QuantumCircuit(
circuit.n_qubits,
ops,
readout_noise,
)
return ConvertedCircuit(native=native, abstract=circuit, original=original_circ)

def observable(self, observable: AbstractBlock, n_qubits: int) -> ConvertedObservable:
Expand Down Expand Up @@ -116,6 +124,9 @@ def _batched_expectation(
noise: NoiseHandler | None = None,
endianness: Endianness = Endianness.BIG,
) -> Tensor:
if noise and circuit.native.readout_noise is None:
readout = convert_readout_noise(circuit.abstract.n_qubits, noise)
circuit.native.readout_noise = readout
state = self.run(
circuit,
param_values=param_values,
Expand Down Expand Up @@ -152,6 +163,11 @@ def _looped_expectation(
"Looping expectation does not make sense with batched initial state. "
"Define your initial state with `batch_size=1`"
)

if noise and circuit.native.readout_noise is None:
readout = convert_readout_noise(circuit.abstract.n_qubits, noise)
circuit.native.readout_noise = readout

list_expvals = []
observables = observable if isinstance(observable, list) else [observable]
for vals in to_list_of_dicts(param_values):
Expand Down Expand Up @@ -206,12 +222,13 @@ def sample(
elif state is not None and pyqify_state:
n_qubits = circuit.abstract.n_qubits
state = pyqify(state, n_qubits) if pyqify_state else state
if noise and circuit.native.readout_noise is None:
readout = convert_readout_noise(circuit.abstract.n_qubits, noise)
circuit.native.readout_noise = readout
samples: list[Counter] = circuit.native.sample(
state=state, values=param_values, n_shots=n_shots
)
samples = invert_endianness(samples) if endianness != Endianness.BIG else samples
if noise is not None:
samples = apply_readout_noise(noise=noise, samples=samples)
if mitigation is not None:
logger.warning(
"Mitigation protocol is deprecated. Use qadence-protocols instead.",
Expand Down
4 changes: 4 additions & 0 deletions qadence/backends/pyqtorch/config.py
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@

from qadence.analog import add_background_hamiltonian
from qadence.backend import BackendConfiguration
from qadence.noise import NoiseHandler
from qadence.transpile import (
blockfn_to_circfn,
chain_single_qubit_ops,
Expand Down Expand Up @@ -63,3 +64,6 @@ class Configuration(BackendConfiguration):

Loop over the batch of parameters to only allocate a single wavefunction at any given time.
"""

noise: NoiseHandler | None = None
"""NoiseHandler containing readout noise applied in backend."""
28 changes: 27 additions & 1 deletion qadence/backends/pyqtorch/convert_ops.py
Original file line number Diff line number Diff line change
Expand Up @@ -320,7 +320,16 @@ def convert_block(
)


def convert_digital_noise(noise: NoiseHandler) -> pyq.noise.NoiseProtocol:
def convert_digital_noise(noise: NoiseHandler) -> pyq.noise.NoiseProtocol | None:
"""Convert the digital noise into pyqtorch NoiseProtocol.

Args:
noise (NoiseHandler): Noise to convert.

Returns:
pyq.noise.NoiseProtocol | None: Pyqtorch native noise protocol
if there are any digital noise protocols.
"""
digital_part = noise.filter(NoiseProtocol.DIGITAL)
if digital_part is None:
return None
Expand All @@ -330,3 +339,20 @@ def convert_digital_noise(noise: NoiseHandler) -> pyq.noise.NoiseProtocol:
for proto, option in zip(digital_part.protocol, digital_part.options)
]
)


def convert_readout_noise(n_qubits: int, noise: NoiseHandler) -> pyq.noise.ReadoutNoise | None:
"""Convert the readout noise into pyqtorch ReadoutNoise.

Args:
n_qubits (int): Number of qubits
noise (NoiseHandler): Noise to convert.

Returns:
pyq.noise.ReadoutNoise | None: Pyqtorch native ReadoutNoise instance
if readout is is noise.
"""
readout_part = noise.filter(NoiseProtocol.READOUT)
if readout_part is None:
return None
return pyq.noise.ReadoutNoise(n_qubits, **readout_part.options[0])
18 changes: 12 additions & 6 deletions qadence/mitigations/readout.py
Original file line number Diff line number Diff line change
Expand Up @@ -10,9 +10,10 @@
from scipy.linalg import norm
from scipy.optimize import LinearConstraint, minimize

from qadence.backends.pyqtorch.convert_ops import convert_readout_noise
from qadence.mitigations.protocols import Mitigations
from qadence.noise.protocols import NoiseHandler
from qadence.types import NoiseProtocol, ReadOutOptimization
from qadence.types import ReadOutOptimization


def corrected_probas(p_corr: npt.NDArray, T: npt.NDArray, p_raw: npt.NDArray) -> np.double:
Expand Down Expand Up @@ -88,13 +89,18 @@ def mitigation_minimization(
Returns:
Mitigated counts computed by the algorithm
"""
protocol, options = noise.protocol[-1], noise.options[-1]
if protocol != NoiseProtocol.READOUT:
raise ValueError("Specify a noise source of type NoiseProtocol.READOUT.")
noise_matrices = options.get("noise_matrix", options["confusion_matrices"])
optimization_type = mitigation.options.get("optimization_type", ReadOutOptimization.MLE)

n_qubits = len(list(samples[0].keys())[0])
readout_noise = convert_readout_noise(n_qubits, noise)
if readout_noise is None:
raise ValueError("Specify a noise source of type NoiseProtocol.READOUT.")
n_shots = sum(samples[0].values())
noise_matrices = readout_noise.confusion_matrices
if readout_noise.confusion_matrices.numel() == 0:
readout_noise.create_noise_matrix(n_shots)
noise_matrices = readout_noise.confusion_matrices
optimization_type = mitigation.options.get("optimization_type", ReadOutOptimization.MLE)

corrected_counters: list[Counter] = []

if optimization_type == ReadOutOptimization.CONSTRAINED:
Expand Down
4 changes: 2 additions & 2 deletions qadence/noise/__init__.py
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
from __future__ import annotations

from .protocols import NoiseHandler, apply_readout_noise
from .protocols import NoiseHandler

# Modules to be automatically added to the qadence namespace
__all__ = ["NoiseHandler", "apply_readout_noise"]
__all__ = ["NoiseHandler"]
45 changes: 3 additions & 42 deletions qadence/noise/protocols.py
Original file line number Diff line number Diff line change
@@ -1,15 +1,10 @@
from __future__ import annotations

import importlib
from itertools import compress
from typing import Any, Callable, Counter, cast
from typing import Any

from qadence.types import NoiseEnum, NoiseProtocol

PROTOCOL_TO_MODULE = {
"Readout": "qadence.noise.readout",
}


class NoiseHandler:
"""A container for multiple sources of noise.
Expand Down Expand Up @@ -106,16 +101,6 @@ def __repr__(self) -> str:
]
)

def get_noise_fn(self, index_protocol: int) -> Callable:
try:
module = importlib.import_module(PROTOCOL_TO_MODULE[self.protocol[index_protocol]])
except KeyError:
ImportError(
f"The module for the protocol {self.protocol[index_protocol]} is not found."
)
fn = getattr(module, "add_noise")
return cast(Callable, fn)

def append(self, other: NoiseHandler | list[NoiseHandler]) -> None:
"""Append noises.

Expand Down Expand Up @@ -163,8 +148,8 @@ def _from_dict(cls, d: dict | None) -> NoiseHandler | None:
def list(cls) -> list:
return list(filter(lambda el: not el.startswith("__"), dir(cls)))

def filter(self, protocol: NoiseEnum) -> NoiseHandler | None:
is_protocol: list = [isinstance(p, protocol) for p in self.protocol] # type: ignore[arg-type]
def filter(self, protocol: NoiseEnum | str) -> NoiseHandler | None:
is_protocol: list = [p == protocol or isinstance(p, protocol) for p in self.protocol] # type: ignore[arg-type]
return (
NoiseHandler(
list(compress(self.protocol, is_protocol)),
Expand Down Expand Up @@ -215,27 +200,3 @@ def dephasing(self, *args: Any, **kwargs: Any) -> NoiseHandler:
def readout(self, *args: Any, **kwargs: Any) -> NoiseHandler:
self.append(NoiseHandler(NoiseProtocol.READOUT, *args, **kwargs))
return self


def apply_readout_noise(noise: NoiseHandler, samples: list[Counter]) -> list[Counter]:
"""Apply readout noise to samples if provided.

Args:
noise (NoiseHandler): Noise to apply.
samples (list[Counter]): Samples to alter

Returns:
list[Counter]: Altered samples.
"""
if noise.protocol[-1] == NoiseProtocol.READOUT:
error_fn = noise.get_noise_fn(-1)
# 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[-1], n_shots=n_shots
)
return noisy_samples
else:
return samples
Loading