Skip to content

Commit

Permalink
[Refactoring] [Testing] [Feature] NoiseHandler (#591)
Browse files Browse the repository at this point in the history
Co-authored-by: RolandMacDoland <[email protected]>
  • Loading branch information
chMoussa and RolandMacDoland authored Oct 30, 2024
1 parent 713f8ee commit 87912c7
Show file tree
Hide file tree
Showing 39 changed files with 701 additions and 226 deletions.
3 changes: 3 additions & 0 deletions docs/api/noise.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
## Noise for simulations

### ::: qadence.noise.protocols
6 changes: 3 additions & 3 deletions docs/tutorials/realistic_sims/mitigation.md
Original file line number Diff line number Diff line change
Expand Up @@ -32,9 +32,9 @@ subjected to physicality constraints $0 \leq p_{corr}(x) \leq 1$ and $\lVert p_{
```python exec="on" source="material-block" session="mitigation" result="json"
from qadence import QuantumModel, QuantumCircuit, kron, H, Z
from qadence import hamiltonian_factory
from qadence.noise import Noise
from qadence.noise import NoiseHandler
from qadence.mitigations import Mitigations
from qadence.types import ReadOutOptimization
from qadence.types import ReadOutOptimization, NoiseProtocol

# Simple circuit and observable construction.
block = kron(H(0), Z(1))
Expand All @@ -45,7 +45,7 @@ observable = hamiltonian_factory(circuit.n_qubits, detuning=Z)
model = QuantumModel(circuit=circuit, observable=observable)

# Define a noise model to use:
noise = Noise(protocol=Noise.READOUT)
noise = NoiseHandler(NoiseProtocol.READOUT)
# Define the mitigation method solving the minimization problem:
options={"optimization_type": ReadOutOptimization.CONSTRAINED} # ReadOutOptimization.MLE for the alternative method.
mitigation = Mitigations(protocol=Mitigations.READOUT, options=options)
Expand Down
112 changes: 101 additions & 11 deletions docs/tutorials/realistic_sims/noise.md
Original file line number Diff line number Diff line change
@@ -1,7 +1,61 @@
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
In order to perform realistic simulations, a number of noise models (for digital operations, analog operations and simulated readout errors) are supported in Qadence through their implementation in backends and
corresponding error mitigation techniques whenever possible.

# NoiseHandler

Noise models can be defined via the `NoiseHandler`. It is a container of several noise instances which require to specify a `protocols` and
a dictionary of `options` (or lists). The `protocol` field is to be instantiated from `NoiseProtocol`.

```python exec="on" source="material-block" session="noise" result="json"
from qadence import NoiseHandler
from qadence.types import NoiseProtocol

analog_noise = NoiseHandler(protocol=NoiseProtocol.ANALOG.DEPOLARIZING, options={"noise_probs": 0.1})
digital_noise = NoiseHandler(protocol=NoiseProtocol.DIGITAL.DEPOLARIZING, options={"error_probability": 0.1})
readout_noise = NoiseHandler(protocol=NoiseProtocol.READOUT, options={"error_probability": 0.1, "seed": 0})
```

One can also define a `NoiseHandler` passing a list of protocols and a list of options (careful with the order):

```python exec="on" source="material-block" session="noise" result="json"
from qadence import NoiseHandler
from qadence.types import NoiseProtocol

protocols = [NoiseProtocol.DIGITAL.DEPOLARIZING, NoiseProtocol.READOUT]
options = [{"error_probability": 0.1}, {"error_probability": 0.1, "seed": 0}]

noise_combination = NoiseHandler(protocols, options)
print(noise_combination)
```

One can also append to a `NoiseHandler` other `NoiseHandler` instances:

```python exec="on" source="material-block" session="noise" result="json"
from qadence import NoiseHandler
from qadence.types import NoiseProtocol

depo_noise = NoiseHandler(protocol=NoiseProtocol.DIGITAL.DEPOLARIZING, options={"error_probability": 0.1})
readout_noise = NoiseHandler(protocol=NoiseProtocol.READOUT, options={"error_probability": 0.1, "seed": 0})

noise_combination = NoiseHandler(protocol=NoiseProtocol.DIGITAL.BITFLIP, options={"error_probability": 0.1})
noise_combination.append([depo_noise, readout_noise])
print(noise_combination)
```

Finally, one can add directly a few pre-defined types using several `NoiseHandler` methods:

```python exec="on" source="material-block" session="noise" result="json"
from qadence import NoiseHandler
from qadence.types import NoiseProtocol
noise_combination = NoiseHandler(protocol=NoiseProtocol.DIGITAL.BITFLIP, options={"error_probability": 0.1})
noise_combination.digital_depolarizing({"error_probability": 0.1}).readout({"error_probability": 0.1, "seed": 0})
print(noise_combination)
```

!!! warning "NoiseHandler scope"
Note it is not possible to define a `NoiseHandler` instances with both digital and analog noises, both readout and analog noises, several analog noises, several readout noises, or a readout noise that is not the last defined protocol within `NoiseHandler`.

## Readout errors

State Preparation and Measurement (SPAM) in the hardware is a major source of noise in the execution of
Expand All @@ -12,13 +66,12 @@ T(x|x')=\delta_{xx'}
$$


Qadence offers to simulate readout errors with the `Noise` protocol to corrupt the output
Qadence offers to simulate readout errors with the `NoiseHandler` to corrupt the output
samples of a simulation, through execution via a `QuantumModel`:

```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

# Simple circuit and observable construction.
block = kron(H(0), Z(1))
Expand All @@ -29,7 +82,7 @@ observable = hamiltonian_factory(circuit.n_qubits, detuning=Z)
model = QuantumModel(circuit=circuit, observable=observable)

# Define a noise model to use.
noise = Noise(protocol=Noise.READOUT)
noise = NoiseHandler(protocol=NoiseProtocol.READOUT)

# Run noiseless and noisy simulations.
noiseless_samples = model.sample(n_shots=100)
Expand All @@ -39,6 +92,16 @@ 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:

Expand All @@ -55,7 +118,7 @@ from qadence.measurements import Measurements

# Define a noise model with options.
options = {"error_probability": 0.01}
noise = Noise(protocol=Noise.READOUT, options=options)
noise = NoiseHandler(protocol=NoiseProtocol.READOUT, options=options)

# Define a tomographical measurement protocol with options.
options = {"n_shots": 10000}
Expand All @@ -69,17 +132,44 @@ print(f"noiseless = {noiseless_exp}") # markdown-exec: hide
print(f"noisy = {noisy_exp}") # markdown-exec: hide
```

## Analog noisy simulation

At the moment, analog noisy simulations are only compatable with the Pulser backend.
```python exec="on" source="material-block" session="noise" result="json"
from qadence import DiffMode, NoiseHandler, QuantumModel
from qadence.blocks import chain, kron
from qadence.circuit import QuantumCircuit
from qadence.operations import AnalogRX, AnalogRZ, Z
from qadence.types import PI, BackendName, NoiseProtocol


analog_block = chain(AnalogRX(PI / 2.0), AnalogRZ(PI))
observable = Z(0) + Z(1)
circuit = QuantumCircuit(2, analog_block)

options = {"noise_probs": 0.1}
noise = NoiseHandler(protocol=NoiseProtocol.ANALOG.DEPOLARIZING, options=options)
model_noisy = QuantumModel(
circuit=circuit,
observable=observable,
backend=BackendName.PULSER,
diff_mode=DiffMode.GPSR,
noise=noise,
)
noisy_expectation = model_noisy.expectation()
print(f"noisy = {noisy_expectation}") # markdown-exec: hide
```


## Digital noisy simulation

When dealing with programs involving only digital operations, several options are made available from [PyQTorch](https://pasqal-io.github.io/pyqtorch/latest/noise/) via the `NoiseType`. One can define noisy digital operations with `DigitalNoise`as follows:
When dealing with programs involving only digital operations, several options are made available from [PyQTorch](https://pasqal-io.github.io/pyqtorch/latest/noise/) via the `NoiseProtocol.DIGITAL`. One can define noisy digital operations as follows:

```python exec="on" source="material-block" session="noise" result="json"
from qadence import NoiseType, DigitalNoise, RX, run
from qadence import NoiseProtocol, RX, run
import torch

noise = DigitalNoise(NoiseType.BITFLIP, error_probability = 0.2)
noise = DigitalNoise.bitflip(error_probability = 0.2) # equivalent

noise = NoiseHandler(NoiseProtocol.DIGITAL.BITFLIP, {"error_probability": 0.2})
op = RX(0, torch.pi, noise = noise)

print(run(op))
Expand All @@ -94,7 +184,7 @@ n_qubits = 2

block = chain(RX(i, f"theta_{i}") for i in range(n_qubits))

noise = DigitalNoise.bitflip(error_probability = 0.1)
noise = NoiseHandler(NoiseProtocol.DIGITAL.BITFLIP, {"error_probability": 0.1})

# The function changes the block in place:
set_noise(block, noise)
Expand Down
1 change: 1 addition & 0 deletions mkdocs.yml
Original file line number Diff line number Diff line change
Expand Up @@ -72,6 +72,7 @@ nav:
- QuantumCircuit: api/quantumcircuit.md
- Parameters: api/parameters.md
- State preparation: api/states.md
- Noise: api/noise.md
- Constructors: api/constructors.md
- Transpilation: api/transpile.md
- Execution: api/execution.md
Expand Down
2 changes: 1 addition & 1 deletion pyproject.toml
Original file line number Diff line number Diff line change
Expand Up @@ -26,7 +26,7 @@ authors = [
]
requires-python = ">=3.9"
license = { text = "Apache 2.0" }
version = "1.7.8"
version = "1.8.0"
classifiers = [
"License :: OSI Approved :: Apache Software License",
"Programming Language :: Python",
Expand Down
6 changes: 3 additions & 3 deletions qadence/backend.py
Original file line number Diff line number Diff line change
Expand Up @@ -23,7 +23,7 @@
from qadence.circuit import QuantumCircuit
from qadence.measurements import Measurements
from qadence.mitigations import Mitigations
from qadence.noise import Noise
from qadence.noise import NoiseHandler
from qadence.parameters import stringify
from qadence.types import ArrayLike, BackendName, DiffMode, Endianness, Engine, ParamDictType

Expand Down Expand Up @@ -240,7 +240,7 @@ def sample(
param_values: dict[str, Tensor] = {},
n_shots: int = 1000,
state: ArrayLike | None = None,
noise: Noise | None = None,
noise: NoiseHandler | None = None,
mitigation: Mitigations | None = None,
endianness: Endianness = Endianness.BIG,
) -> list[Counter]:
Expand Down Expand Up @@ -290,7 +290,7 @@ def expectation(
param_values: ParamDictType = {},
state: ArrayLike | None = None,
measurement: Measurements | None = None,
noise: Noise | None = None,
noise: NoiseHandler | None = None,
mitigation: Mitigations | None = None,
endianness: Endianness = Endianness.BIG,
) -> ArrayLike:
Expand Down
10 changes: 5 additions & 5 deletions qadence/backends/braket/backend.py
Original file line number Diff line number Diff line change
Expand Up @@ -19,8 +19,8 @@
from qadence.measurements import Measurements
from qadence.mitigations import Mitigations
from qadence.mitigations.protocols import apply_mitigation
from qadence.noise import Noise
from qadence.noise.protocols import apply_noise
from qadence.noise import NoiseHandler
from qadence.noise.protocols import apply_readout_noise
from qadence.overlap import overlap_exact
from qadence.transpile import transpile
from qadence.types import BackendName, Engine
Expand Down Expand Up @@ -137,7 +137,7 @@ def sample(
param_values: dict[str, Tensor] = {},
n_shots: int = 1,
state: Tensor | None = None,
noise: Noise | None = None,
noise: NoiseHandler | None = None,
mitigation: Mitigations | None = None,
endianness: Endianness = Endianness.BIG,
) -> list[Counter]:
Expand All @@ -164,7 +164,7 @@ def sample(

samples = invert_endianness(samples)
if noise is not None:
samples = apply_noise(noise=noise, samples=samples)
samples = apply_readout_noise(noise=noise, samples=samples)
if mitigation is not None:
logger.warning(
"Mitigation protocol is deprecated. Use qadence-protocols instead.",
Expand All @@ -180,7 +180,7 @@ def expectation(
param_values: dict[str, Tensor] = {},
state: Tensor | None = None,
measurement: Measurements | None = None,
noise: Noise | None = None,
noise: NoiseHandler | None = None,
mitigation: Mitigations | None = None,
endianness: Endianness = Endianness.BIG,
) -> Tensor:
Expand Down
6 changes: 3 additions & 3 deletions qadence/backends/horqrux/backend.py
Original file line number Diff line number Diff line change
Expand Up @@ -21,7 +21,7 @@
from qadence.circuit import QuantumCircuit
from qadence.measurements import Measurements
from qadence.mitigations import Mitigations
from qadence.noise import Noise
from qadence.noise import NoiseHandler
from qadence.transpile import flatten, scale_primitive_blocks_only, transpile
from qadence.types import BackendName, Endianness, Engine, ParamDictType
from qadence.utils import int_to_basis
Expand Down Expand Up @@ -114,7 +114,7 @@ def expectation(
param_values: ParamDictType = {},
state: ArrayLike | None = None,
measurement: Measurements | None = None,
noise: Noise | None = None,
noise: NoiseHandler | None = None,
mitigation: Mitigations | None = None,
endianness: Endianness = Endianness.BIG,
) -> ArrayLike:
Expand Down Expand Up @@ -163,7 +163,7 @@ def sample(
param_values: ParamDictType = {},
n_shots: int = 1,
state: ArrayLike | None = None,
noise: Noise | None = None,
noise: NoiseHandler | None = None,
mitigation: Mitigations | None = None,
endianness: Endianness = Endianness.BIG,
) -> list[Counter]:
Expand Down
33 changes: 16 additions & 17 deletions qadence/backends/pulser/backend.py
Original file line number Diff line number Diff line change
Expand Up @@ -24,12 +24,12 @@
from qadence.measurements import Measurements
from qadence.mitigations import Mitigations
from qadence.mitigations.protocols import apply_mitigation
from qadence.noise import Noise
from qadence.noise.protocols import apply_noise
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
from qadence.types import BackendName, DeviceType, Endianness, Engine
from qadence.types import BackendName, DeviceType, Endianness, Engine, NoiseProtocol

from .channels import GLOBAL_CHANNEL, LOCAL_CHANNEL
from .cloud import get_client
Expand Down Expand Up @@ -187,7 +187,7 @@ def run(
param_values: dict[str, Tensor] = {},
state: Tensor | None = None,
endianness: Endianness = Endianness.BIG,
noise: Noise | None = None,
noise: NoiseHandler | None = None,
) -> Tensor:
vals = to_list_of_dicts(param_values)

Expand Down Expand Up @@ -235,27 +235,26 @@ def run(
def _run_noisy(
self,
circuit: ConvertedCircuit,
noise: Noise,
noise: NoiseHandler,
param_values: dict[str, Tensor] = dict(),
state: Tensor | None = None,
endianness: Endianness = Endianness.BIG,
) -> Tensor:
vals = to_list_of_dicts(param_values)
noise_probs = noise.options.get("noise_probs", None)
if noise_probs is None:
KeyError("A `noise probs` option should be passed to the <class QuantumModel>.")
if not (isinstance(noise_probs, float) or isinstance(noise_probs, Iterable)):
KeyError(
"A single or a range of noise probabilities"
" should be passed. Got {type(noise_probs)}."
)
if not isinstance(noise.protocol[-1], NoiseProtocol.ANALOG):
raise TypeError("Noise must be of type `NoiseProtocol.ANALOG`.")
noise_probs = noise.options[-1].get("noise_probs", None)

def run_noisy_sim(noise_prob: float) -> Tensor:
batched_dm = np.zeros(
(len(vals), 2**circuit.abstract.n_qubits, 2**circuit.abstract.n_qubits),
dtype=np.complex128,
)
sim_config = {"noise": noise.protocol, noise.protocol + "_rate": noise_prob}
# pulser requires lower letters
sim_config = {
"noise": noise.protocol[-1].lower(),
noise.protocol[-1].lower() + "_rate": noise_prob,
}
self.config.sim_config = SimConfig(**sim_config)

for i, param_values_el in enumerate(vals):
Expand Down Expand Up @@ -289,7 +288,7 @@ def sample(
param_values: dict[str, Tensor] = {},
n_shots: int = 1,
state: Tensor | None = None,
noise: Noise | None = None,
noise: NoiseHandler | None = None,
mitigation: Mitigations | None = None,
endianness: Endianness = Endianness.BIG,
) -> list[Counter]:
Expand All @@ -313,7 +312,7 @@ def sample(

samples = invert_endianness(samples)
if noise is not None:
samples = apply_noise(noise=noise, samples=samples)
samples = apply_readout_noise(noise=noise, samples=samples)
if mitigation is not None:
logger.warning(
"Mitigation protocol is deprecated. Use qadence-protocols instead.",
Expand All @@ -329,7 +328,7 @@ def expectation(
param_values: dict[str, Tensor] = {},
state: Tensor | None = None,
measurement: Measurements | None = None,
noise: Noise | None = None,
noise: NoiseHandler | None = None,
mitigation: Mitigations | None = None,
endianness: Endianness = Endianness.BIG,
) -> Tensor:
Expand Down
Loading

0 comments on commit 87912c7

Please sign in to comment.