qadence 1.7.7__py3-none-any.whl → 1.8.0__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (53) hide show
  1. qadence/analog/device.py +1 -1
  2. qadence/backend.py +3 -3
  3. qadence/backends/horqrux/backend.py +3 -3
  4. qadence/backends/pulser/backend.py +16 -17
  5. qadence/backends/pulser/convert_ops.py +2 -2
  6. qadence/backends/pyqtorch/backend.py +7 -7
  7. qadence/backends/pyqtorch/convert_ops.py +191 -240
  8. qadence/backends/utils.py +9 -1
  9. qadence/blocks/abstract.py +1 -1
  10. qadence/blocks/embedding.py +21 -11
  11. qadence/blocks/matrix.py +3 -1
  12. qadence/blocks/primitive.py +36 -11
  13. qadence/circuit.py +1 -1
  14. qadence/constructors/__init__.py +2 -1
  15. qadence/constructors/ansatze.py +176 -0
  16. qadence/engines/differentiable_backend.py +3 -3
  17. qadence/engines/jax/differentiable_backend.py +2 -2
  18. qadence/engines/jax/differentiable_expectation.py +2 -2
  19. qadence/engines/torch/differentiable_backend.py +2 -2
  20. qadence/engines/torch/differentiable_expectation.py +2 -2
  21. qadence/execution.py +14 -14
  22. qadence/extensions.py +1 -1
  23. qadence/measurements/shadow.py +4 -5
  24. qadence/measurements/tomography.py +2 -2
  25. qadence/measurements/utils.py +2 -2
  26. qadence/mitigations/analog_zne.py +8 -7
  27. qadence/mitigations/protocols.py +2 -2
  28. qadence/mitigations/readout.py +8 -5
  29. qadence/ml_tools/config.py +14 -0
  30. qadence/ml_tools/constructors.py +9 -4
  31. qadence/ml_tools/models.py +7 -7
  32. qadence/ml_tools/printing.py +2 -1
  33. qadence/ml_tools/train_grad.py +39 -7
  34. qadence/model.py +5 -5
  35. qadence/noise/__init__.py +2 -2
  36. qadence/noise/protocols.py +216 -29
  37. qadence/operations/control_ops.py +37 -22
  38. qadence/operations/ham_evo.py +1 -0
  39. qadence/operations/parametric.py +32 -10
  40. qadence/operations/primitive.py +61 -29
  41. qadence/overlap.py +0 -6
  42. qadence/parameters.py +3 -2
  43. qadence/transpile/__init__.py +2 -1
  44. qadence/transpile/noise.py +46 -0
  45. qadence/types.py +26 -2
  46. {qadence-1.7.7.dist-info → qadence-1.8.0.dist-info}/METADATA +5 -8
  47. {qadence-1.7.7.dist-info → qadence-1.8.0.dist-info}/RECORD +49 -52
  48. qadence/backends/braket/__init__.py +0 -4
  49. qadence/backends/braket/backend.py +0 -234
  50. qadence/backends/braket/config.py +0 -22
  51. qadence/backends/braket/convert_ops.py +0 -116
  52. {qadence-1.7.7.dist-info → qadence-1.8.0.dist-info}/WHEEL +0 -0
  53. {qadence-1.7.7.dist-info → qadence-1.8.0.dist-info}/licenses/LICENSE +0 -0
@@ -15,7 +15,7 @@ from qadence.blocks.analog import ConstantAnalogRotation, InteractionBlock
15
15
  from qadence.circuit import QuantumCircuit
16
16
  from qadence.measurements import Measurements
17
17
  from qadence.mitigations import Mitigations
18
- from qadence.noise import Noise
18
+ from qadence.noise import NoiseHandler
19
19
  from qadence.operations import AnalogRot
20
20
  from qadence.transpile import apply_fn_to_blocks
21
21
  from qadence.utils import Endianness
@@ -44,7 +44,7 @@ def pulse_experiment(
44
44
  circuit: QuantumCircuit,
45
45
  observable: list[AbstractBlock],
46
46
  param_values: dict[str, Tensor],
47
- noise: Noise,
47
+ noise: NoiseHandler,
48
48
  stretches: Tensor,
49
49
  endianness: Endianness,
50
50
  state: Tensor | None = None,
@@ -116,11 +116,12 @@ def noise_level_experiment(
116
116
  circuit: QuantumCircuit,
117
117
  observable: list[AbstractBlock],
118
118
  param_values: dict[str, Tensor],
119
- noise: Noise,
119
+ noise: NoiseHandler,
120
120
  endianness: Endianness,
121
121
  state: Tensor | None = None,
122
122
  ) -> Tensor:
123
- noise_probs = noise.options.get("noise_probs")
123
+ protocol, options = noise.protocol[-1], noise.options[-1]
124
+ noise_probs = options.get("noise_probs")
124
125
  zne_datasets: list = []
125
126
  # Get noisy density matrices.
126
127
  conv_circuit = backend.circuit(circuit)
@@ -152,7 +153,7 @@ def analog_zne(
152
153
  param_values: dict[str, Tensor] = {},
153
154
  state: Tensor | None = None,
154
155
  measurement: Measurements | None = None,
155
- noise: Noise | None = None,
156
+ noise: NoiseHandler | None = None,
156
157
  mitigation: Mitigations | None = None,
157
158
  endianness: Endianness = Endianness.BIG,
158
159
  ) -> Tensor:
@@ -162,7 +163,7 @@ def analog_zne(
162
163
  backend = cast(Backend, backend)
163
164
  noise_model = mitigation.options.get("noise_model", None)
164
165
  if noise_model is None:
165
- KeyError(f"A noise model should be choosen from {Noise.list()}. Got {noise_model}.")
166
+ raise KeyError("A noise model should be specified.")
166
167
  stretches = mitigation.options.get("stretches", None)
167
168
  if stretches is not None:
168
169
  extrapolated_exp_values = pulse_experiment(
@@ -195,7 +196,7 @@ def mitigate(
195
196
  param_values: dict[str, Tensor] = {},
196
197
  state: Tensor | None = None,
197
198
  measurement: Measurements | None = None,
198
- noise: Noise | None = None,
199
+ noise: NoiseHandler | None = None,
199
200
  mitigation: Mitigations | None = None,
200
201
  endianness: Endianness = Endianness.BIG,
201
202
  ) -> Tensor:
@@ -5,7 +5,7 @@ from collections import Counter
5
5
  from dataclasses import dataclass
6
6
  from typing import Callable, cast
7
7
 
8
- from qadence.noise.protocols import Noise
8
+ from qadence.noise.protocols import NoiseHandler
9
9
 
10
10
  PROTOCOL_TO_MODULE = {
11
11
  "readout": "qadence.mitigations.readout",
@@ -45,7 +45,7 @@ class Mitigations:
45
45
 
46
46
 
47
47
  def apply_mitigation(
48
- noise: Noise, mitigation: Mitigations, samples: list[Counter]
48
+ noise: NoiseHandler, mitigation: Mitigations, samples: list[Counter]
49
49
  ) -> list[Counter]:
50
50
  """Apply mitigation to samples."""
51
51
  mitigation_fn = mitigation.get_mitigation_fn()
@@ -11,8 +11,8 @@ from scipy.linalg import norm
11
11
  from scipy.optimize import LinearConstraint, minimize
12
12
 
13
13
  from qadence.mitigations.protocols import Mitigations
14
- from qadence.noise.protocols import Noise
15
- from qadence.types import ReadOutOptimization
14
+ from qadence.noise.protocols import NoiseHandler
15
+ from qadence.types import NoiseProtocol, ReadOutOptimization
16
16
 
17
17
 
18
18
  def corrected_probas(p_corr: npt.NDArray, T: npt.NDArray, p_raw: npt.NDArray) -> np.double:
@@ -69,7 +69,7 @@ def matrix_inv(K: npt.NDArray) -> npt.NDArray:
69
69
 
70
70
 
71
71
  def mitigation_minimization(
72
- noise: Noise,
72
+ noise: NoiseHandler,
73
73
  mitigation: Mitigations,
74
74
  samples: list[Counter],
75
75
  ) -> list[Counter]:
@@ -88,7 +88,10 @@ def mitigation_minimization(
88
88
  Returns:
89
89
  Mitigated counts computed by the algorithm
90
90
  """
91
- noise_matrices = noise.options.get("noise_matrix", noise.options["confusion_matrices"])
91
+ protocol, options = noise.protocol[-1], noise.options[-1]
92
+ if protocol != NoiseProtocol.READOUT:
93
+ raise ValueError("Specify a noise source of type NoiseProtocol.READOUT.")
94
+ noise_matrices = options.get("noise_matrix", options["confusion_matrices"])
92
95
  optimization_type = mitigation.options.get("optimization_type", ReadOutOptimization.MLE)
93
96
  n_qubits = len(list(samples[0].keys())[0])
94
97
  n_shots = sum(samples[0].values())
@@ -156,5 +159,5 @@ def mitigation_minimization(
156
159
  return corrected_counters
157
160
 
158
161
 
159
- def mitigate(noise: Noise, mitigation: Mitigations, samples: list[Counter]) -> list[Counter]:
162
+ def mitigate(noise: NoiseHandler, mitigation: Mitigations, samples: list[Counter]) -> list[Counter]:
160
163
  return mitigation_minimization(noise=noise, mitigation=mitigation, samples=samples)
@@ -447,6 +447,13 @@ class FeatureMapConfig:
447
447
  assign `t, x = xs[:,0], xs[:,1]`.
448
448
  """
449
449
 
450
+ tag: str | None = None
451
+ """
452
+ String to indicate the name tag of the feature map.
453
+
454
+ Defaults to None, in which case no tag will be applied.
455
+ """
456
+
450
457
  def __post_init__(self) -> None:
451
458
  if self.multivariate_strategy == MultivariateStrategy.PARALLEL and self.num_features > 1:
452
459
  assert (
@@ -606,6 +613,13 @@ class AnsatzConfig:
606
613
  param_prefix: str = "theta"
607
614
  """The base bame of the variational parameter."""
608
615
 
616
+ tag: str | None = None
617
+ """
618
+ String to indicate the name tag of the ansatz.
619
+
620
+ Defaults to None, in which case no tag will be applied.
621
+ """
622
+
609
623
  def __post_init__(self) -> None:
610
624
  if self.ansatz_type == AnsatzType.IIA:
611
625
  assert (
@@ -7,7 +7,7 @@ from qadence.backend import BackendConfiguration
7
7
  from qadence.blocks import chain, kron
8
8
  from qadence.blocks.abstract import AbstractBlock
9
9
  from qadence.blocks.composite import ChainBlock, KronBlock
10
- from qadence.blocks.utils import add
10
+ from qadence.blocks.utils import add, tag
11
11
  from qadence.circuit import QuantumCircuit
12
12
  from qadence.constructors import (
13
13
  analog_feature_map,
@@ -21,7 +21,7 @@ from qadence.constructors import (
21
21
  from qadence.constructors.ansatze import hea_digital, hea_sDAQC
22
22
  from qadence.constructors.hamiltonians import ObservableConfig, TDetuning
23
23
  from qadence.measurements import Measurements
24
- from qadence.noise import Noise
24
+ from qadence.noise import NoiseHandler
25
25
  from qadence.operations import CNOT, RX, RY, I, N, Z
26
26
  from qadence.parameters import Parameter
27
27
  from qadence.register import Register
@@ -740,7 +740,7 @@ def build_qnn_from_configs(
740
740
  backend: BackendName = BackendName.PYQTORCH,
741
741
  diff_mode: DiffMode = DiffMode.AD,
742
742
  measurement: Measurements | None = None,
743
- noise: Noise | None = None,
743
+ noise: NoiseHandler | None = None,
744
744
  configuration: BackendConfiguration | dict | None = None,
745
745
  input_diff_mode: InputDiffMode | str = InputDiffMode.AD,
746
746
  ) -> QNN:
@@ -774,10 +774,15 @@ def build_qnn_from_configs(
774
774
  fm_blocks=fm_blocks,
775
775
  ansatz_config=ansatz_config,
776
776
  )
777
+ if isinstance(fm_config.tag, str):
778
+ tag(full_fm, fm_config.tag)
777
779
  inputs = fm_config.inputs
778
780
  blocks.append(full_fm)
779
781
 
780
- blocks.append(create_ansatz(register=register, config=ansatz_config))
782
+ ansatz = create_ansatz(register=register, config=ansatz_config)
783
+ if isinstance(ansatz_config.tag, str):
784
+ tag(ansatz, ansatz_config.tag)
785
+ blocks.append(ansatz)
781
786
 
782
787
  circ = QuantumCircuit(register, *blocks)
783
788
 
@@ -16,7 +16,7 @@ from qadence.measurements import Measurements
16
16
  from qadence.mitigations import Mitigations
17
17
  from qadence.ml_tools.config import AnsatzConfig, FeatureMapConfig
18
18
  from qadence.model import QuantumModel
19
- from qadence.noise import Noise
19
+ from qadence.noise import NoiseHandler
20
20
  from qadence.register import Register
21
21
  from qadence.types import BackendName, DiffMode, Endianness, InputDiffMode, ParamDictType
22
22
 
@@ -139,7 +139,7 @@ class QNN(QuantumModel):
139
139
  backend: BackendName = BackendName.PYQTORCH,
140
140
  diff_mode: DiffMode = DiffMode.AD,
141
141
  measurement: Measurements | None = None,
142
- noise: Noise | None = None,
142
+ noise: NoiseHandler | None = None,
143
143
  configuration: BackendConfiguration | dict | None = None,
144
144
  inputs: list[sympy.Basic | str] | None = None,
145
145
  input_diff_mode: InputDiffMode | str = InputDiffMode.AD,
@@ -218,7 +218,7 @@ class QNN(QuantumModel):
218
218
  backend: BackendName = BackendName.PYQTORCH,
219
219
  diff_mode: DiffMode = DiffMode.AD,
220
220
  measurement: Measurements | None = None,
221
- noise: Noise | None = None,
221
+ noise: NoiseHandler | None = None,
222
222
  configuration: BackendConfiguration | dict | None = None,
223
223
  input_diff_mode: InputDiffMode | str = InputDiffMode.AD,
224
224
  ) -> QNN:
@@ -311,7 +311,7 @@ class QNN(QuantumModel):
311
311
  values: dict[str, Tensor] | Tensor = None,
312
312
  state: Tensor | None = None,
313
313
  measurement: Measurements | None = None,
314
- noise: Noise | None = None,
314
+ noise: NoiseHandler | None = None,
315
315
  endianness: Endianness = Endianness.BIG,
316
316
  ) -> Tensor:
317
317
  """Forward pass of the model.
@@ -360,7 +360,7 @@ class QNN(QuantumModel):
360
360
  values: Tensor | dict[str, Tensor] = {},
361
361
  n_shots: int = 1000,
362
362
  state: Tensor | None = None,
363
- noise: Noise | None = None,
363
+ noise: NoiseHandler | None = None,
364
364
  mitigation: Mitigations | None = None,
365
365
  endianness: Endianness = Endianness.BIG,
366
366
  ) -> list[Counter]:
@@ -379,7 +379,7 @@ class QNN(QuantumModel):
379
379
  observable: list[ConvertedObservable] | ConvertedObservable | None = None,
380
380
  state: Tensor | None = None,
381
381
  measurement: Measurements | None = None,
382
- noise: Noise | None = None,
382
+ noise: NoiseHandler | None = None,
383
383
  mitigation: Mitigations | None = None,
384
384
  endianness: Endianness = Endianness.BIG,
385
385
  ) -> Tensor:
@@ -423,7 +423,7 @@ class QNN(QuantumModel):
423
423
  backend=qm_dict["backend"],
424
424
  diff_mode=qm_dict["diff_mode"],
425
425
  measurement=Measurements._from_dict(qm_dict["measurement"]),
426
- noise=Noise._from_dict(qm_dict["noise"]),
426
+ noise=NoiseHandler._from_dict(qm_dict["noise"]),
427
427
  configuration=config_factory(qm_dict["backend"], qm_dict["backend_configuration"]),
428
428
  inputs=qm_dict["inputs"],
429
429
  )
@@ -27,8 +27,9 @@ def print_metrics(loss: float | None, metrics: dict, iteration: int) -> None:
27
27
 
28
28
 
29
29
  def write_tensorboard(
30
- writer: SummaryWriter, loss: float = None, metrics: dict = {}, iteration: int = 0
30
+ writer: SummaryWriter, loss: float = None, metrics: dict | None = None, iteration: int = 0
31
31
  ) -> None:
32
+ metrics = metrics or dict()
32
33
  if loss is not None:
33
34
  writer.add_scalar("loss", loss, iteration)
34
35
  for key, arg in metrics.items():
@@ -213,21 +213,48 @@ def train(
213
213
  ]
214
214
 
215
215
  # writing metrics
216
+ # we specify two writers,
217
+ # to write at evaluation time and before evaluation
216
218
  callbacks += [
217
219
  Callback(
218
220
  lambda opt_res: write_tracker(
219
221
  writer,
220
222
  opt_res.loss,
221
223
  opt_res.metrics,
222
- opt_res.iteration,
224
+ opt_res.iteration - 1, # loss returned be optimized_step is at -1
225
+ tracking_tool=config.tracking_tool,
226
+ ),
227
+ called_every=config.write_every,
228
+ call_end_epoch=True,
229
+ ),
230
+ Callback(
231
+ lambda opt_res: write_tracker(
232
+ writer,
233
+ opt_res.loss,
234
+ opt_res.metrics,
235
+ opt_res.iteration, # after_opt we match the right loss function
223
236
  tracking_tool=config.tracking_tool,
224
237
  ),
225
238
  called_every=config.write_every,
226
- call_before_opt=False,
239
+ call_end_epoch=False,
227
240
  call_after_opt=True,
228
- call_during_eval=True,
229
- )
241
+ ),
230
242
  ]
243
+ if perform_val:
244
+ callbacks += [
245
+ Callback(
246
+ lambda opt_res: write_tracker(
247
+ writer,
248
+ None,
249
+ opt_res.metrics,
250
+ opt_res.iteration,
251
+ tracking_tool=config.tracking_tool,
252
+ ),
253
+ called_every=config.write_every,
254
+ call_before_opt=True,
255
+ call_during_eval=True,
256
+ )
257
+ ]
231
258
 
232
259
  # checkpointing
233
260
  if config.folder and config.checkpoint_every > 0 and not config.checkpoint_best_only:
@@ -332,17 +359,22 @@ def train(
332
359
  logger.info("Terminating training gracefully after the current iteration.")
333
360
  break
334
361
 
335
- # Handling printing the last training loss
362
+ # For handling printing/writing the last training loss
336
363
  # as optimize_step does not give the loss value at the last iteration
337
364
  try:
338
365
  loss, metrics, *_ = next_loss_iter(dl_iter)
339
- if iteration % config.print_every == 0 and config.verbose:
340
- print_metrics(loss, metrics, iteration)
366
+ if isinstance(loss, Tensor):
367
+ loss = loss.item()
368
+ if perform_val:
369
+ # reputting val_loss as already evaluated before
370
+ metrics["val_loss"] = val_loss
371
+ print_metrics(loss, metrics, iteration)
341
372
 
342
373
  except KeyboardInterrupt:
343
374
  logger.info("Terminating training gracefully after the current iteration.")
344
375
 
345
376
  # Final callbacks, by default checkpointing and writing
377
+ opt_result = OptimizeResult(iteration, model, optimizer, loss, metrics)
346
378
  callbacks_after_opt = [callback for callback in callbacks if callback.call_after_opt]
347
379
  run_callbacks(callbacks_after_opt, opt_result, is_last_iteration=True)
348
380
 
qadence/model.py CHANGED
@@ -24,7 +24,7 @@ from qadence.circuit import QuantumCircuit
24
24
  from qadence.engines.differentiable_backend import DifferentiableBackend
25
25
  from qadence.measurements import Measurements
26
26
  from qadence.mitigations import Mitigations
27
- from qadence.noise import Noise
27
+ from qadence.noise import NoiseHandler
28
28
  from qadence.parameters import Parameter
29
29
  from qadence.types import DiffMode, Endianness
30
30
 
@@ -83,7 +83,7 @@ class QuantumModel(nn.Module):
83
83
  backend: BackendName | str = BackendName.PYQTORCH,
84
84
  diff_mode: DiffMode = DiffMode.AD,
85
85
  measurement: Measurements | None = None,
86
- noise: Noise | None = None,
86
+ noise: NoiseHandler | None = None,
87
87
  mitigation: Mitigations | None = None,
88
88
  configuration: BackendConfiguration | dict | None = None,
89
89
  ):
@@ -249,7 +249,7 @@ class QuantumModel(nn.Module):
249
249
  values: dict[str, torch.Tensor] = {},
250
250
  n_shots: int = 1000,
251
251
  state: torch.Tensor | None = None,
252
- noise: Noise | None = None,
252
+ noise: NoiseHandler | None = None,
253
253
  mitigation: Mitigations | None = None,
254
254
  endianness: Endianness = Endianness.BIG,
255
255
  ) -> list[Counter]:
@@ -287,7 +287,7 @@ class QuantumModel(nn.Module):
287
287
  observable: list[ConvertedObservable] | ConvertedObservable | None = None,
288
288
  state: Optional[Tensor] = None,
289
289
  measurement: Measurements | None = None,
290
- noise: Noise | None = None,
290
+ noise: NoiseHandler | None = None,
291
291
  mitigation: Mitigations | None = None,
292
292
  endianness: Endianness = Endianness.BIG,
293
293
  ) -> Tensor:
@@ -415,7 +415,7 @@ class QuantumModel(nn.Module):
415
415
  backend=qm_dict["backend"],
416
416
  diff_mode=qm_dict["diff_mode"],
417
417
  measurement=Measurements._from_dict(qm_dict["measurement"]),
418
- noise=Noise._from_dict(qm_dict["noise"]),
418
+ noise=NoiseHandler._from_dict(qm_dict["noise"]),
419
419
  configuration=config_factory(qm_dict["backend"], qm_dict["backend_configuration"]),
420
420
  )
421
421
 
qadence/noise/__init__.py CHANGED
@@ -1,6 +1,6 @@
1
1
  from __future__ import annotations
2
2
 
3
- from .protocols import Noise
3
+ from .protocols import NoiseHandler, apply_readout_noise
4
4
 
5
5
  # Modules to be automatically added to the qadence namespace
6
- __all__ = ["Noise"]
6
+ __all__ = ["NoiseHandler", "apply_readout_noise"]
@@ -1,54 +1,241 @@
1
1
  from __future__ import annotations
2
2
 
3
3
  import importlib
4
- from dataclasses import dataclass
5
- from typing import Callable, Counter, cast
4
+ from itertools import compress
5
+ from typing import Any, Callable, Counter, cast
6
+
7
+ from qadence.types import NoiseEnum, NoiseProtocol
6
8
 
7
9
  PROTOCOL_TO_MODULE = {
8
- "readout": "qadence.noise.readout",
10
+ "Readout": "qadence.noise.readout",
9
11
  }
10
12
 
11
13
 
12
- @dataclass
13
- class Noise:
14
- DEPHASING = "dephasing"
15
- DEPOLARIZING = "depolarizing"
16
- READOUT = "readout"
14
+ class NoiseHandler:
15
+ """A container for multiple sources of noise.
16
+
17
+ Note `NoiseProtocol.ANALOG` and `NoiseProtocol.DIGITAL` sources cannot be both present.
18
+ Also `NoiseProtocol.READOUT` can only be present once as the last noise sources, and only
19
+ exclusively with `NoiseProtocol.DIGITAL` sources.
20
+
21
+ Args:
22
+ protocol: The protocol(s) applied. To be defined from `NoiseProtocol`.
23
+ options: A list of options defining the protocol.
24
+ For `NoiseProtocol.ANALOG`, options should contain a field `noise_probs`.
25
+ For `NoiseProtocol.DIGITAL`, options should contain a field `error_probability`.
26
+
27
+ Examples:
28
+ ```
29
+ from qadence import NoiseProtocol, NoiseHandler
30
+
31
+ analog_options = {"noise_probs": 0.1}
32
+ digital_options = {"error_probability": 0.1}
33
+ readout_options = {"error_probability": 0.1, "seed": 0}
34
+
35
+ # single noise sources
36
+ analog_noise = NoiseHandler(NoiseProtocol.ANALOG.DEPOLARIZING, analog_options)
37
+ digital_depo_noise = NoiseHandler(NoiseProtocol.DIGITAL.DEPOLARIZING, digital_options)
38
+ readout_noise = NoiseHandler(NoiseProtocol.READOUT, readout_options)
39
+
40
+ # init from multiple sources
41
+ protocols: list = [NoiseProtocol.DIGITAL.DEPOLARIZING, NoiseProtocol.READOUT]
42
+ options: list = [digital_options, readout_noise]
43
+ noise_combination = NoiseHandler(protocols, options)
44
+
45
+ # Appending noise sources
46
+ noise_combination = NoiseHandler(NoiseProtocol.DIGITAL.BITFLIP, digital_options)
47
+ noise_combination.append([digital_depo_noise, readout_noise])
48
+ ```
49
+ """
50
+
51
+ def __init__(
52
+ self,
53
+ protocol: NoiseEnum | list[NoiseEnum],
54
+ options: dict | list[dict] = dict(),
55
+ ) -> None:
56
+ self.protocol = protocol if isinstance(protocol, list) else [protocol]
57
+ self.options = options if isinstance(options, list) else [options] * len(self.protocol)
58
+ self.verify_all_protocols()
17
59
 
18
- def __init__(self, protocol: str, options: dict = dict()) -> None:
19
- self.protocol: str = protocol
20
- self.options: dict = options
60
+ def _verify_single_protocol(self, protocol: NoiseEnum, option: dict) -> None:
61
+ if protocol != NoiseProtocol.READOUT:
62
+ name_mandatory_option = (
63
+ "noise_probs" if isinstance(protocol, NoiseProtocol.ANALOG) else "error_probability"
64
+ )
65
+ noise_probs = option.get(name_mandatory_option, None)
66
+ if noise_probs is None:
67
+ error_txt = f"A `{name_mandatory_option}` option"
68
+ error_txt += f"should be passed for protocol {protocol}."
69
+ raise KeyError(error_txt)
21
70
 
22
- def get_noise_fn(self) -> Callable:
71
+ def verify_all_protocols(self) -> None:
72
+ """Make sure all protocols are correct in terms and their combination too."""
73
+
74
+ if len(self.protocol) == 0:
75
+ raise ValueError("NoiseHandler should be specified with one valid configuration.")
76
+
77
+ if len(self.protocol) != len(self.options):
78
+ raise ValueError("Specify lists of same length when defining noises.")
79
+
80
+ for protocol, option in zip(self.protocol, self.options):
81
+ self._verify_single_protocol(protocol, option)
82
+
83
+ types = [type(p) for p in self.protocol]
84
+ unique_types = set(types)
85
+ if NoiseProtocol.DIGITAL in unique_types and NoiseProtocol.ANALOG in unique_types:
86
+ raise ValueError("Cannot define a config with both Digital and Analog noises.")
87
+
88
+ if NoiseProtocol.ANALOG in unique_types:
89
+ if NoiseProtocol.READOUT in unique_types:
90
+ raise ValueError("Cannot define a config with both READOUT and Analog noises.")
91
+ if types.count(NoiseProtocol.ANALOG) > 1:
92
+ raise ValueError("Multiple Analog Noises are not supported yet.")
93
+
94
+ if NoiseProtocol.READOUT in self.protocol:
95
+ if (
96
+ self.protocol[-1] != NoiseProtocol.READOUT
97
+ or self.protocol.count(NoiseProtocol.READOUT) > 1
98
+ ):
99
+ raise ValueError("Only define a NoiseHandler with one READOUT as the last Noise.")
100
+
101
+ def __repr__(self) -> str:
102
+ return "\n".join(
103
+ [
104
+ f"Noise({protocol}, {str(option)})"
105
+ for protocol, option in zip(self.protocol, self.options)
106
+ ]
107
+ )
108
+
109
+ def get_noise_fn(self, index_protocol: int) -> Callable:
23
110
  try:
24
- module = importlib.import_module(PROTOCOL_TO_MODULE[self.protocol])
111
+ module = importlib.import_module(PROTOCOL_TO_MODULE[self.protocol[index_protocol]])
25
112
  except KeyError:
26
- ImportError(f"The module corresponding to the protocol {self.protocol} is not found.")
113
+ ImportError(
114
+ f"The module for the protocol {self.protocol[index_protocol]} is not found."
115
+ )
27
116
  fn = getattr(module, "add_noise")
28
117
  return cast(Callable, fn)
29
118
 
119
+ def append(self, other: NoiseHandler | list[NoiseHandler]) -> None:
120
+ """Append noises.
121
+
122
+ Args:
123
+ other (NoiseHandler | list[NoiseHandler]): The noises to add.
124
+ """
125
+ # To avoid overwriting the noise_sources list if an error is raised, make a copy
126
+ other_list = other if isinstance(other, list) else [other]
127
+ protocols = self.protocol[:]
128
+ options = self.options[:]
129
+
130
+ for noise in other_list:
131
+ protocols += noise.protocol
132
+ options += noise.options
133
+
134
+ # init may raise an error
135
+ temp_handler = NoiseHandler(protocols, options)
136
+ # if verify passes, replace protocols and options
137
+ self.protocol = temp_handler.protocol
138
+ self.options = temp_handler.options
139
+
140
+ def __eq__(self, other: object) -> bool:
141
+ if not isinstance(other, NoiseHandler):
142
+ raise TypeError(f"Cant compare {type(self)} to {type(other)}")
143
+ if isinstance(other, type(self)):
144
+ protocols_equal = all([p1 == p2 for p1, p2 in zip(self.protocol, other.protocol)])
145
+ options_equal = all([o1 == o2 for o1, o2 in zip(self.options, other.options)])
146
+ return protocols_equal and options_equal
147
+
148
+ return False
149
+
30
150
  def _to_dict(self) -> dict:
31
- return {"protocol": self.protocol, "options": self.options}
151
+ return {
152
+ "protocol": self.protocol,
153
+ "options": self.options,
154
+ }
32
155
 
33
156
  @classmethod
34
- def _from_dict(cls, d: dict) -> Noise | None:
35
- if d:
36
- return cls(d["protocol"], **d["options"])
157
+ def _from_dict(cls, d: dict | None) -> NoiseHandler | None:
158
+ if d is not None and d.get("protocol", None):
159
+ return cls(d["protocol"], d["options"])
37
160
  return None
38
161
 
39
162
  @classmethod
40
163
  def list(cls) -> list:
41
164
  return list(filter(lambda el: not el.startswith("__"), dir(cls)))
42
165
 
166
+ def filter(self, protocol: NoiseEnum) -> NoiseHandler | None:
167
+ is_protocol: list = [isinstance(p, protocol) for p in self.protocol] # type: ignore[arg-type]
168
+ return (
169
+ NoiseHandler(
170
+ list(compress(self.protocol, is_protocol)),
171
+ list(compress(self.options, is_protocol)),
172
+ )
173
+ if len(is_protocol) > 0
174
+ else None
175
+ )
176
+
177
+ def bitflip(self, *args: Any, **kwargs: Any) -> NoiseHandler:
178
+ self.append(NoiseHandler(NoiseProtocol.DIGITAL.BITFLIP, *args, **kwargs))
179
+ return self
180
+
181
+ def phaseflip(self, *args: Any, **kwargs: Any) -> NoiseHandler:
182
+ self.append(NoiseHandler(NoiseProtocol.DIGITAL.PHASEFLIP, *args, **kwargs))
183
+ return self
184
+
185
+ def digital_depolarizing(self, *args: Any, **kwargs: Any) -> NoiseHandler:
186
+ self.append(NoiseHandler(NoiseProtocol.DIGITAL.DEPOLARIZING, *args, **kwargs))
187
+ return self
188
+
189
+ def pauli_channel(self, *args: Any, **kwargs: Any) -> NoiseHandler:
190
+ self.append(NoiseHandler(NoiseProtocol.DIGITAL.PAULI_CHANNEL, *args, **kwargs))
191
+ return self
192
+
193
+ def amplitude_damping(self, *args: Any, **kwargs: Any) -> NoiseHandler:
194
+ self.append(NoiseHandler(NoiseProtocol.DIGITAL.AMPLITUDE_DAMPING, *args, **kwargs))
195
+ return self
196
+
197
+ def phase_damping(self, *args: Any, **kwargs: Any) -> NoiseHandler:
198
+ self.append(NoiseHandler(NoiseProtocol.DIGITAL.PHASE_DAMPING, *args, **kwargs))
199
+ return self
200
+
201
+ def generalized_amplitude_damping(self, *args: Any, **kwargs: Any) -> NoiseHandler:
202
+ self.append(
203
+ NoiseHandler(NoiseProtocol.DIGITAL.GENERALIZED_AMPLITUDE_DAMPING, *args, **kwargs)
204
+ )
205
+ return self
206
+
207
+ def analog_depolarizing(self, *args: Any, **kwargs: Any) -> NoiseHandler:
208
+ self.append(NoiseHandler(NoiseProtocol.ANALOG.DEPOLARIZING, *args, **kwargs))
209
+ return self
210
+
211
+ def dephasing(self, *args: Any, **kwargs: Any) -> NoiseHandler:
212
+ self.append(NoiseHandler(NoiseProtocol.ANALOG.DEPHASING, *args, **kwargs))
213
+ return self
214
+
215
+ def readout(self, *args: Any, **kwargs: Any) -> NoiseHandler:
216
+ self.append(NoiseHandler(NoiseProtocol.READOUT, *args, **kwargs))
217
+ return self
218
+
219
+
220
+ def apply_readout_noise(noise: NoiseHandler, samples: list[Counter]) -> list[Counter]:
221
+ """Apply readout noise to samples if provided.
222
+
223
+ Args:
224
+ noise (NoiseHandler): Noise to apply.
225
+ samples (list[Counter]): Samples to alter
43
226
 
44
- def apply_noise(noise: Noise, samples: list[Counter]) -> list[Counter]:
45
- """Apply noise to samples."""
46
- error_fn = noise.get_noise_fn()
47
- # Get the number of qubits from the sample keys.
48
- n_qubits = len(list(samples[0].keys())[0])
49
- # Get the number of shots from the sample values.
50
- n_shots = sum(samples[0].values())
51
- noisy_samples: list = error_fn(
52
- counters=samples, n_qubits=n_qubits, options=noise.options, n_shots=n_shots
53
- )
54
- return noisy_samples
227
+ Returns:
228
+ list[Counter]: Altered samples.
229
+ """
230
+ if noise.protocol[-1] == NoiseProtocol.READOUT:
231
+ error_fn = noise.get_noise_fn(-1)
232
+ # Get the number of qubits from the sample keys.
233
+ n_qubits = len(list(samples[0].keys())[0])
234
+ # Get the number of shots from the sample values.
235
+ n_shots = sum(samples[0].values())
236
+ noisy_samples: list = error_fn(
237
+ counters=samples, n_qubits=n_qubits, options=noise.options[-1], n_shots=n_shots
238
+ )
239
+ return noisy_samples
240
+ else:
241
+ return samples