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

[Feature] Correlated readout #620

Merged
merged 26 commits into from
Nov 25, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
26 commits
Select commit Hold shift + click to select a range
43a0351
add try except when appending config noise
Nov 12, 2024
6f4ef20
set_noise in backend circuit method
Nov 12, 2024
12ebe26
add new test for expectation of quantum model and digital noise
Nov 12, 2024
508230e
Update qadence/backends/pyqtorch/backend.py
chMoussa Nov 12, 2024
5d070b8
Update qadence/backends/pyqtorch/backend.py
chMoussa Nov 12, 2024
42840f6
Merge remote-tracking branch 'origin/main' into cm/call_setnoise_in_b…
Nov 12, 2024
3202838
fix typo
Nov 12, 2024
6260945
more comment
Nov 12, 2024
1c0d450
testing passing noise backend
Nov 13, 2024
0a8f059
docstring converted_circuit_with_noise
Nov 13, 2024
93ddb17
separate set_noise functions
Nov 13, 2024
ec4f614
add if readout
Nov 13, 2024
0a34191
Merge remote-tracking branch 'origin/main' into cm/call_setnoise_in_b…
Nov 15, 2024
5efe7c1
Merge remote-tracking branch 'origin/main' into cm/call_setnoise_in_b…
Nov 21, 2024
0e49465
use filter function in noise filter
Nov 21, 2024
5e5735e
set_noise with if elif else
Nov 21, 2024
b1127ec
rm filter
Nov 21, 2024
834f88e
add type readoutnoise for correlated readout
Nov 21, 2024
52d3030
add test serialization
Nov 21, 2024
bc6d17e
add correlatedreadout in tests
Nov 21, 2024
daf73b6
fix isinstance when veryfing noise protocol
Nov 21, 2024
dd32238
fix test corr readout
Nov 21, 2024
68b969c
improve corr test
Nov 21, 2024
dbbfc7e
fix docs
Nov 21, 2024
363e413
Merge remote-tracking branch 'origin/main' into cm/correlated_readout
Nov 23, 2024
328ee63
change nomenclature readout
Nov 25, 2024
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
2 changes: 1 addition & 1 deletion docs/tutorials/realistic_sims/mitigation.md
Original file line number Diff line number Diff line change
Expand Up @@ -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 = NoiseHandler(NoiseProtocol.READOUT)
noise = NoiseHandler(NoiseProtocol.READOUT.INDEPENDENT)
# 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
27 changes: 20 additions & 7 deletions docs/tutorials/realistic_sims/noise.md
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,7 @@ 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})
readout_noise = NoiseHandler(protocol=NoiseProtocol.READOUT.INDEPENDENT, 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):
Expand All @@ -36,7 +36,7 @@ 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})
readout_noise = NoiseHandler(protocol=NoiseProtocol.READOUT.INDEPENDENT, 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])
Expand All @@ -49,7 +49,7 @@ Finally, one can add directly a few pre-defined types using several `NoiseHandle
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})
noise_combination.digital_depolarizing({"error_probability": 0.1}).readout_independent({"error_probability": 0.1, "seed": 0})
print(noise_combination)
```

Expand All @@ -65,6 +65,10 @@ $$
T(x|x')=\delta_{xx'}
$$

Two types of readout protocols are available:
- `NoiseProtocol.READOUT.INDEPENDENT` where each bit can be corrupted independently of each other.
- `NoiseProtocol.READOUT.CORRELATED` where we can define of confusion matrix of corruption between each
possible bitstrings.

Qadence offers to simulate readout errors with the `NoiseHandler` to corrupt the output
samples of a simulation, through execution via a `QuantumModel`:
Expand All @@ -82,7 +86,7 @@ observable = hamiltonian_factory(circuit.n_qubits, detuning=Z)
model = QuantumModel(circuit=circuit, observable=observable)

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

# Run noiseless and noisy simulations.
noiseless_samples = model.sample(n_shots=100)
Expand All @@ -93,12 +97,21 @@ 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:
uniform distribution.

For `NoiseProtocol.READOUT.INDEPENDENT`, 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
- `error_probability`: If float, the same probability is applied to every bit. By default, this is 0.1.
If a 1D tensor with the number of elements equal to the number of qubits, a different probability can be set for each qubit. If a tensor of shape (n_qubits, 2, 2) is passed, that is a confusion matrix obtained from experiments, we extract the error_probability.
and do not compute internally the confusion matrix as in the other cases.
- `noise_distribution`: defaulted to `WhiteNoise.UNIFORM`, for non-uniform noise distributions

For `NoiseProtocol.READOUT.CORRELATED`, the `option` dictionary argument accepts the following options:
- `confusion_matrix`: The square matrix representing $T(x|x')$ for each possible bitstring of length `n` qubits. Should be of size (2**n, 2**n).
- `seed`: defaulted to `None`, for reproducibility purposes


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 All @@ -107,7 +120,7 @@ from qadence.measurements import Measurements

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

# Define a tomographical measurement protocol with options.
options = {"n_shots": 10000}
Expand Down
6 changes: 5 additions & 1 deletion qadence/backends/pyqtorch/convert_ops.py
Original file line number Diff line number Diff line change
Expand Up @@ -373,4 +373,8 @@ def convert_readout_noise(n_qubits: int, noise: NoiseHandler) -> pyq.noise.Reado
readout_part = noise.filter(NoiseProtocol.READOUT)
if readout_part is None:
return None
return pyq.noise.ReadoutNoise(n_qubits, **readout_part.options[0])

if readout_part.protocol[0] == NoiseProtocol.READOUT.INDEPENDENT:
return pyq.noise.ReadoutNoise(n_qubits, **readout_part.options[0])
else:
return pyq.noise.CorrelatedReadoutNoise(**readout_part.options[0])
22 changes: 11 additions & 11 deletions qadence/noise/protocols.py
Original file line number Diff line number Diff line change
Expand Up @@ -53,7 +53,7 @@ def __init__(
self.verify_all_protocols()

def _verify_single_protocol(self, protocol: NoiseEnum, option: dict) -> None:
if protocol != NoiseProtocol.READOUT:
if not isinstance(protocol, NoiseProtocol.READOUT): # type: ignore[arg-type]
name_mandatory_option = (
"noise_probs" if isinstance(protocol, NoiseProtocol.ANALOG) else "error_probability"
)
Expand Down Expand Up @@ -86,10 +86,10 @@ def verify_all_protocols(self) -> None:
if types.count(NoiseProtocol.ANALOG) > 1:
raise ValueError("Multiple Analog Noises are not supported yet.")

if NoiseProtocol.READOUT in self.protocol:
if NoiseProtocol.READOUT in unique_types:
if (
self.protocol[-1] != NoiseProtocol.READOUT
or self.protocol.count(NoiseProtocol.READOUT) > 1
not isinstance(self.protocol[-1], NoiseProtocol.READOUT)
or types.count(NoiseProtocol.READOUT) > 1
):
raise ValueError("Only define a NoiseHandler with one READOUT as the last Noise.")

Expand Down Expand Up @@ -149,11 +149,7 @@ def list(cls) -> list:
return list(filter(lambda el: not el.startswith("__"), dir(cls)))

def filter(self, protocol: NoiseEnum) -> NoiseHandler | None:
protocol_matches: list = list()
if protocol == NoiseProtocol.READOUT:
protocol_matches = [p == protocol for p in self.protocol]
else:
protocol_matches = [isinstance(p, protocol) for p in self.protocol] # type: ignore[arg-type]
protocol_matches: list = [isinstance(p, protocol) for p in self.protocol] # type: ignore[arg-type]

# if we have at least a match
if True in protocol_matches:
Expand Down Expand Up @@ -201,6 +197,10 @@ def dephasing(self, *args: Any, **kwargs: Any) -> NoiseHandler:
self.append(NoiseHandler(NoiseProtocol.ANALOG.DEPHASING, *args, **kwargs))
return self

def readout(self, *args: Any, **kwargs: Any) -> NoiseHandler:
self.append(NoiseHandler(NoiseProtocol.READOUT, *args, **kwargs))
def readout_independent(self, *args: Any, **kwargs: Any) -> NoiseHandler:
self.append(NoiseHandler(NoiseProtocol.READOUT.INDEPENDENT, *args, **kwargs))
return self

def readout_correlated(self, *args: Any, **kwargs: Any) -> NoiseHandler:
self.append(NoiseHandler(NoiseProtocol.READOUT.CORRELATED, *args, **kwargs))
return self
13 changes: 11 additions & 2 deletions qadence/types.py
Original file line number Diff line number Diff line change
Expand Up @@ -470,16 +470,25 @@ class AnalogNoise(StrEnum):
DEPHASING = "Dephasing"


class ReadoutNoise(StrEnum):
"""Type of readout protocol."""

INDEPENDENT = "Independent Readout"
"""Simple readout protocols where each qubit is corrupted independently."""
CORRELATED = "Correlated Readout"
"""Using a confusion matrix (2**n, 2**n) for corrupting bitstrings values."""


@dataclass
class NoiseProtocol:
"""Type of noise protocol."""

ANALOG = AnalogNoise
"""Noise applied in analog blocks."""
READOUT = "Readout"
READOUT = ReadoutNoise
"""Noise applied on outputs of quantum programs."""
DIGITAL = DigitalNoise
"""Noise applied to digital blocks."""


NoiseEnum = Union[DigitalNoise, AnalogNoise, str]
NoiseEnum = Union[DigitalNoise, AnalogNoise, ReadoutNoise]
102 changes: 78 additions & 24 deletions tests/qadence/test_noise/test_readout.py
Original file line number Diff line number Diff line change
Expand Up @@ -32,40 +32,57 @@

@pytest.mark.flaky(max_runs=5)
@pytest.mark.parametrize(
"error_probability, n_shots, block, backend",
"error_probability, n_shots, block",
[
(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,
100,
kron(X(0), X(1)),
),
(
0.1,
200,
kron(Z(0), Z(1), Z(2)) + kron(X(0), Y(1), Z(2)),
),
(
0.01,
1000,
add(Z(0), Z(1), Z(2)),
),
(
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, 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),
(
0.1,
500,
add(Z(0), Z(1), kron(X(2), X(3))) + add(X(2), X(3)),
),
(0.05, 10000, add(kron(Z(0), Z(1)), kron(X(2), X(3)))),
(0.2, 1000, hamiltonian_factory(4, detuning=Z)),
(0.1, 500, kron(Z(0), Z(1)) + CNOT(0, 1)),
],
)
def test_readout_error_quantum_model(
error_probability: float,
n_shots: int,
block: AbstractBlock,
backend: BackendName,
) -> None:
diff_mode = "ad" if backend == BackendName.PYQTORCH else "gpsr"

noiseless_samples: list[Counter] = QuantumModel(
backend = BackendName.PYQTORCH
diff_mode = "ad"
model = QuantumModel(
QuantumCircuit(block.n_qubits, block), backend=backend, diff_mode=diff_mode
).sample(n_shots=n_shots)
)
noiseless_samples: list[Counter] = model.sample(n_shots=n_shots)

noisy_samples: list[Counter] = QuantumModel(
QuantumCircuit(block.n_qubits, block), backend=backend, diff_mode=diff_mode
).sample(noise=NoiseHandler(protocol=NoiseProtocol.READOUT), n_shots=n_shots)
noise_protocol: NoiseHandler = NoiseHandler(
protocol=NoiseProtocol.READOUT.INDEPENDENT,
options={"error_probability": error_probability},
)
noisy_samples: list[Counter] = model.sample(noise=noise_protocol, n_shots=n_shots)

for noiseless, noisy in zip(noiseless_samples, noisy_samples):
assert sum(noiseless.values()) == sum(noisy.values()) == n_shots
Expand All @@ -76,6 +93,23 @@ def test_readout_error_quantum_model(
atol=1e-1,
)

rand_confusion = torch.rand(2**block.n_qubits, 2**block.n_qubits)
rand_confusion = rand_confusion / rand_confusion.sum(dim=1, keepdim=True)
corr_noise_protocol: NoiseHandler = NoiseHandler(
protocol=NoiseProtocol.READOUT.CORRELATED,
options={"confusion_matrix": rand_confusion},
)
# assert difference with noiseless samples
corr_noisy_samples: list[Counter] = model.sample(noise=corr_noise_protocol, n_shots=n_shots)
for noiseless, noisy in zip(noiseless_samples, corr_noisy_samples):
assert sum(noiseless.values()) == sum(noisy.values()) == n_shots
assert js_divergence(noiseless, noisy) > 0.0

# assert difference noisy samples
for noisy, corr_noisy in zip(noisy_samples, corr_noisy_samples):
assert sum(noisy.values()) == sum(corr_noisy.values()) == n_shots
assert js_divergence(noisy, corr_noisy) > 0.0


@pytest.mark.parametrize("backend", [BackendName.PYQTORCH, BackendName.PULSER])
def test_readout_error_backends(backend: BackendName) -> None:
Expand All @@ -88,7 +122,7 @@ def test_readout_error_backends(backend: BackendName) -> None:
samples = qd.sample(feature_map, n_shots=1000, values=inputs, backend=backend, noise=None)
# introduce noise
options = {"error_probability": error_probability}
noise = NoiseHandler(protocol=NoiseProtocol.READOUT, options=options)
noise = NoiseHandler(protocol=NoiseProtocol.READOUT.INDEPENDENT, options=options)
noisy_samples = qd.sample(
feature_map, n_shots=1000, values=inputs, backend=backend, noise=noise
)
Expand Down Expand Up @@ -120,7 +154,7 @@ def test_readout_error_with_measurements(
observable = hamiltonian_factory(circuit.n_qubits, detuning=Z)

model = QuantumModel(circuit=circuit, observable=observable, diff_mode=DiffMode.GPSR)
noise = NoiseHandler(protocol=NoiseProtocol.READOUT)
noise = NoiseHandler(protocol=NoiseProtocol.READOUT.INDEPENDENT)
measurement = Measurements(protocol=str(measurement_proto), options=options)

noisy = model.expectation(values=inputs, measurement=measurement, noise=noise)
Expand All @@ -137,7 +171,16 @@ def test_readout_error_with_measurements(


def test_serialization() -> None:
noise = NoiseHandler(protocol=NoiseProtocol.READOUT)
noise = NoiseHandler(protocol=NoiseProtocol.READOUT.INDEPENDENT)
serialized_noise = NoiseHandler._from_dict(noise._to_dict())
assert noise == serialized_noise

rand_confusion = torch.rand(4, 4)
rand_confusion = rand_confusion / rand_confusion.sum(dim=1, keepdim=True)
noise = NoiseHandler(
protocol=NoiseProtocol.READOUT.CORRELATED,
options={"seed": 0, "confusion_matrix": rand_confusion},
)
serialized_noise = NoiseHandler._from_dict(noise._to_dict())
assert noise == serialized_noise

Expand All @@ -150,10 +193,21 @@ def test_serialization() -> None:
[NoiseProtocol.DIGITAL.BITFLIP, NoiseProtocol.DIGITAL.PHASEFLIP],
],
)
def test_append(noise_config: NoiseProtocol | list[NoiseProtocol]) -> None:
noise = NoiseHandler(protocol=NoiseProtocol.READOUT)
@pytest.mark.parametrize(
"initial_noise",
[
NoiseHandler(protocol=NoiseProtocol.READOUT.INDEPENDENT),
NoiseHandler(protocol=NoiseProtocol.READOUT.CORRELATED, options=torch.rand((4, 4))),
],
)
def test_append(
initial_noise: NoiseHandler, noise_config: NoiseProtocol | list[NoiseProtocol]
) -> None:
options = {"error_probability": 0.1}
with pytest.raises(ValueError):
noise.append(NoiseHandler(noise_config, options))
initial_noise.append(NoiseHandler(noise_config, options))
with pytest.raises(ValueError):
initial_noise.readout_independent(options)

with pytest.raises(ValueError):
noise.readout(options)
initial_noise.readout_correlated({"confusion_matrix": torch.rand(4, 4)})