qadence 1.7.8__py3-none-any.whl → 1.9.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 (75) hide show
  1. qadence/__init__.py +1 -1
  2. qadence/analog/device.py +1 -1
  3. qadence/analog/parse_analog.py +1 -2
  4. qadence/backend.py +3 -3
  5. qadence/backends/gpsr.py +8 -2
  6. qadence/backends/horqrux/backend.py +3 -3
  7. qadence/backends/pulser/backend.py +21 -38
  8. qadence/backends/pulser/convert_ops.py +2 -2
  9. qadence/backends/pyqtorch/backend.py +85 -10
  10. qadence/backends/pyqtorch/config.py +10 -3
  11. qadence/backends/pyqtorch/convert_ops.py +245 -233
  12. qadence/backends/utils.py +9 -1
  13. qadence/blocks/abstract.py +1 -1
  14. qadence/blocks/embedding.py +21 -11
  15. qadence/blocks/matrix.py +3 -1
  16. qadence/blocks/primitive.py +37 -11
  17. qadence/circuit.py +1 -1
  18. qadence/constructors/__init__.py +2 -1
  19. qadence/constructors/ansatze.py +176 -0
  20. qadence/engines/differentiable_backend.py +3 -3
  21. qadence/engines/jax/differentiable_backend.py +2 -2
  22. qadence/engines/jax/differentiable_expectation.py +2 -2
  23. qadence/engines/torch/differentiable_backend.py +2 -2
  24. qadence/engines/torch/differentiable_expectation.py +2 -2
  25. qadence/execution.py +14 -16
  26. qadence/extensions.py +1 -1
  27. qadence/log_config.yaml +10 -0
  28. qadence/measurements/shadow.py +101 -133
  29. qadence/measurements/tomography.py +2 -2
  30. qadence/measurements/utils.py +4 -4
  31. qadence/mitigations/analog_zne.py +8 -7
  32. qadence/mitigations/protocols.py +2 -2
  33. qadence/mitigations/readout.py +14 -5
  34. qadence/ml_tools/__init__.py +4 -8
  35. qadence/ml_tools/callbacks/__init__.py +30 -0
  36. qadence/ml_tools/callbacks/callback.py +451 -0
  37. qadence/ml_tools/callbacks/callbackmanager.py +214 -0
  38. qadence/ml_tools/{saveload.py → callbacks/saveload.py} +11 -11
  39. qadence/ml_tools/callbacks/writer_registry.py +430 -0
  40. qadence/ml_tools/config.py +132 -258
  41. qadence/ml_tools/constructors.py +2 -2
  42. qadence/ml_tools/data.py +7 -3
  43. qadence/ml_tools/loss/__init__.py +10 -0
  44. qadence/ml_tools/loss/loss.py +87 -0
  45. qadence/ml_tools/models.py +7 -7
  46. qadence/ml_tools/optimize_step.py +45 -10
  47. qadence/ml_tools/stages.py +46 -0
  48. qadence/ml_tools/train_utils/__init__.py +7 -0
  49. qadence/ml_tools/train_utils/base_trainer.py +548 -0
  50. qadence/ml_tools/train_utils/config_manager.py +184 -0
  51. qadence/ml_tools/trainer.py +692 -0
  52. qadence/model.py +6 -6
  53. qadence/noise/__init__.py +2 -2
  54. qadence/noise/protocols.py +188 -36
  55. qadence/operations/control_ops.py +37 -22
  56. qadence/operations/ham_evo.py +88 -26
  57. qadence/operations/parametric.py +32 -10
  58. qadence/operations/primitive.py +61 -29
  59. qadence/overlap.py +0 -6
  60. qadence/parameters.py +3 -2
  61. qadence/transpile/__init__.py +2 -1
  62. qadence/transpile/noise.py +53 -0
  63. qadence/types.py +39 -3
  64. {qadence-1.7.8.dist-info → qadence-1.9.0.dist-info}/METADATA +5 -9
  65. {qadence-1.7.8.dist-info → qadence-1.9.0.dist-info}/RECORD +67 -63
  66. {qadence-1.7.8.dist-info → qadence-1.9.0.dist-info}/WHEEL +1 -1
  67. qadence/backends/braket/__init__.py +0 -4
  68. qadence/backends/braket/backend.py +0 -234
  69. qadence/backends/braket/config.py +0 -22
  70. qadence/backends/braket/convert_ops.py +0 -116
  71. qadence/ml_tools/printing.py +0 -153
  72. qadence/ml_tools/train_grad.py +0 -395
  73. qadence/ml_tools/train_no_grad.py +0 -199
  74. qadence/noise/readout.py +0 -218
  75. {qadence-1.7.8.dist-info → qadence-1.9.0.dist-info}/licenses/LICENSE +0 -0
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
 
@@ -514,7 +514,7 @@ class QuantumModel(nn.Module):
514
514
  if isinstance(file_path, str):
515
515
  file_path = Path(file_path)
516
516
  if os.path.isdir(file_path):
517
- from qadence.ml_tools.saveload import get_latest_checkpoint_name
517
+ from qadence.ml_tools.callbacks.saveload import get_latest_checkpoint_name
518
518
 
519
519
  file_path = file_path / get_latest_checkpoint_name(file_path, "model")
520
520
 
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
4
4
 
5
5
  # Modules to be automatically added to the qadence namespace
6
- __all__ = ["Noise"]
6
+ __all__ = ["NoiseHandler"]
@@ -1,54 +1,206 @@
1
1
  from __future__ import annotations
2
2
 
3
- import importlib
4
- from dataclasses import dataclass
5
- from typing import Callable, Counter, cast
3
+ from itertools import compress
4
+ from typing import Any
6
5
 
7
- PROTOCOL_TO_MODULE = {
8
- "readout": "qadence.noise.readout",
9
- }
6
+ from qadence.types import NoiseEnum, NoiseProtocol
10
7
 
11
8
 
12
- @dataclass
13
- class Noise:
14
- DEPHASING = "dephasing"
15
- DEPOLARIZING = "depolarizing"
16
- READOUT = "readout"
9
+ class NoiseHandler:
10
+ """A container for multiple sources of noise.
17
11
 
18
- def __init__(self, protocol: str, options: dict = dict()) -> None:
19
- self.protocol: str = protocol
20
- self.options: dict = options
12
+ Note `NoiseProtocol.ANALOG` and `NoiseProtocol.DIGITAL` sources cannot be both present.
13
+ Also `NoiseProtocol.READOUT` can only be present once as the last noise sources, and only
14
+ exclusively with `NoiseProtocol.DIGITAL` sources.
21
15
 
22
- def get_noise_fn(self) -> Callable:
23
- try:
24
- module = importlib.import_module(PROTOCOL_TO_MODULE[self.protocol])
25
- except KeyError:
26
- ImportError(f"The module corresponding to the protocol {self.protocol} is not found.")
27
- fn = getattr(module, "add_noise")
28
- return cast(Callable, fn)
16
+ Args:
17
+ protocol: The protocol(s) applied. To be defined from `NoiseProtocol`.
18
+ options: A list of options defining the protocol.
19
+ For `NoiseProtocol.ANALOG`, options should contain a field `noise_probs`.
20
+ For `NoiseProtocol.DIGITAL`, options should contain a field `error_probability`.
21
+
22
+ Examples:
23
+ ```
24
+ from qadence import NoiseProtocol, NoiseHandler
25
+
26
+ analog_options = {"noise_probs": 0.1}
27
+ digital_options = {"error_probability": 0.1}
28
+ readout_options = {"error_probability": 0.1, "seed": 0}
29
+
30
+ # single noise sources
31
+ analog_noise = NoiseHandler(NoiseProtocol.ANALOG.DEPOLARIZING, analog_options)
32
+ digital_depo_noise = NoiseHandler(NoiseProtocol.DIGITAL.DEPOLARIZING, digital_options)
33
+ readout_noise = NoiseHandler(NoiseProtocol.READOUT, readout_options)
34
+
35
+ # init from multiple sources
36
+ protocols: list = [NoiseProtocol.DIGITAL.DEPOLARIZING, NoiseProtocol.READOUT]
37
+ options: list = [digital_options, readout_noise]
38
+ noise_combination = NoiseHandler(protocols, options)
39
+
40
+ # Appending noise sources
41
+ noise_combination = NoiseHandler(NoiseProtocol.DIGITAL.BITFLIP, digital_options)
42
+ noise_combination.append([digital_depo_noise, readout_noise])
43
+ ```
44
+ """
45
+
46
+ def __init__(
47
+ self,
48
+ protocol: NoiseEnum | list[NoiseEnum],
49
+ options: dict | list[dict] = dict(),
50
+ ) -> None:
51
+ self.protocol = protocol if isinstance(protocol, list) else [protocol]
52
+ self.options = options if isinstance(options, list) else [options] * len(self.protocol)
53
+ self.verify_all_protocols()
54
+
55
+ def _verify_single_protocol(self, protocol: NoiseEnum, option: dict) -> None:
56
+ if not isinstance(protocol, NoiseProtocol.READOUT): # type: ignore[arg-type]
57
+ name_mandatory_option = (
58
+ "noise_probs" if isinstance(protocol, NoiseProtocol.ANALOG) else "error_probability"
59
+ )
60
+ noise_probs = option.get(name_mandatory_option, None)
61
+ if noise_probs is None:
62
+ error_txt = f"A `{name_mandatory_option}` option"
63
+ error_txt += f"should be passed for protocol {protocol}."
64
+ raise KeyError(error_txt)
65
+
66
+ def verify_all_protocols(self) -> None:
67
+ """Make sure all protocols are correct in terms and their combination too."""
68
+
69
+ if len(self.protocol) == 0:
70
+ raise ValueError("NoiseHandler should be specified with one valid configuration.")
71
+
72
+ if len(self.protocol) != len(self.options):
73
+ raise ValueError("Specify lists of same length when defining noises.")
74
+
75
+ for protocol, option in zip(self.protocol, self.options):
76
+ self._verify_single_protocol(protocol, option)
77
+
78
+ types = [type(p) for p in self.protocol]
79
+ unique_types = set(types)
80
+ if NoiseProtocol.DIGITAL in unique_types and NoiseProtocol.ANALOG in unique_types:
81
+ raise ValueError("Cannot define a config with both Digital and Analog noises.")
82
+
83
+ if NoiseProtocol.ANALOG in unique_types:
84
+ if NoiseProtocol.READOUT in unique_types:
85
+ raise ValueError("Cannot define a config with both READOUT and Analog noises.")
86
+ if types.count(NoiseProtocol.ANALOG) > 1:
87
+ raise ValueError("Multiple Analog Noises are not supported yet.")
88
+
89
+ if NoiseProtocol.READOUT in unique_types:
90
+ if (
91
+ not isinstance(self.protocol[-1], NoiseProtocol.READOUT)
92
+ or types.count(NoiseProtocol.READOUT) > 1
93
+ ):
94
+ raise ValueError("Only define a NoiseHandler with one READOUT as the last Noise.")
95
+
96
+ def __repr__(self) -> str:
97
+ return "\n".join(
98
+ [
99
+ f"Noise({protocol}, {str(option)})"
100
+ for protocol, option in zip(self.protocol, self.options)
101
+ ]
102
+ )
103
+
104
+ def append(self, other: NoiseHandler | list[NoiseHandler]) -> None:
105
+ """Append noises.
106
+
107
+ Args:
108
+ other (NoiseHandler | list[NoiseHandler]): The noises to add.
109
+ """
110
+ # To avoid overwriting the noise_sources list if an error is raised, make a copy
111
+ other_list = other if isinstance(other, list) else [other]
112
+ protocols = self.protocol[:]
113
+ options = self.options[:]
114
+
115
+ for noise in other_list:
116
+ protocols += noise.protocol
117
+ options += noise.options
118
+
119
+ # init may raise an error
120
+ temp_handler = NoiseHandler(protocols, options)
121
+ # if verify passes, replace protocols and options
122
+ self.protocol = temp_handler.protocol
123
+ self.options = temp_handler.options
124
+
125
+ def __eq__(self, other: object) -> bool:
126
+ if not isinstance(other, NoiseHandler):
127
+ raise TypeError(f"Cant compare {type(self)} to {type(other)}")
128
+ if isinstance(other, type(self)):
129
+ protocols_equal = all([p1 == p2 for p1, p2 in zip(self.protocol, other.protocol)])
130
+ options_equal = all([o1 == o2 for o1, o2 in zip(self.options, other.options)])
131
+ return protocols_equal and options_equal
132
+
133
+ return False
29
134
 
30
135
  def _to_dict(self) -> dict:
31
- return {"protocol": self.protocol, "options": self.options}
136
+ return {
137
+ "protocol": self.protocol,
138
+ "options": self.options,
139
+ }
32
140
 
33
141
  @classmethod
34
- def _from_dict(cls, d: dict) -> Noise | None:
35
- if d:
36
- return cls(d["protocol"], **d["options"])
142
+ def _from_dict(cls, d: dict | None) -> NoiseHandler | None:
143
+ if d is not None and d.get("protocol", None):
144
+ return cls(d["protocol"], d["options"])
37
145
  return None
38
146
 
39
147
  @classmethod
40
148
  def list(cls) -> list:
41
149
  return list(filter(lambda el: not el.startswith("__"), dir(cls)))
42
150
 
151
+ def filter(self, protocol: NoiseEnum) -> NoiseHandler | None:
152
+ protocol_matches: list = [isinstance(p, protocol) for p in self.protocol] # type: ignore[arg-type]
153
+
154
+ # if we have at least a match
155
+ if True in protocol_matches:
156
+ return NoiseHandler(
157
+ list(compress(self.protocol, protocol_matches)),
158
+ list(compress(self.options, protocol_matches)),
159
+ )
160
+ return None
161
+
162
+ def bitflip(self, *args: Any, **kwargs: Any) -> NoiseHandler:
163
+ self.append(NoiseHandler(NoiseProtocol.DIGITAL.BITFLIP, *args, **kwargs))
164
+ return self
165
+
166
+ def phaseflip(self, *args: Any, **kwargs: Any) -> NoiseHandler:
167
+ self.append(NoiseHandler(NoiseProtocol.DIGITAL.PHASEFLIP, *args, **kwargs))
168
+ return self
169
+
170
+ def digital_depolarizing(self, *args: Any, **kwargs: Any) -> NoiseHandler:
171
+ self.append(NoiseHandler(NoiseProtocol.DIGITAL.DEPOLARIZING, *args, **kwargs))
172
+ return self
173
+
174
+ def pauli_channel(self, *args: Any, **kwargs: Any) -> NoiseHandler:
175
+ self.append(NoiseHandler(NoiseProtocol.DIGITAL.PAULI_CHANNEL, *args, **kwargs))
176
+ return self
177
+
178
+ def amplitude_damping(self, *args: Any, **kwargs: Any) -> NoiseHandler:
179
+ self.append(NoiseHandler(NoiseProtocol.DIGITAL.AMPLITUDE_DAMPING, *args, **kwargs))
180
+ return self
181
+
182
+ def phase_damping(self, *args: Any, **kwargs: Any) -> NoiseHandler:
183
+ self.append(NoiseHandler(NoiseProtocol.DIGITAL.PHASE_DAMPING, *args, **kwargs))
184
+ return self
185
+
186
+ def generalized_amplitude_damping(self, *args: Any, **kwargs: Any) -> NoiseHandler:
187
+ self.append(
188
+ NoiseHandler(NoiseProtocol.DIGITAL.GENERALIZED_AMPLITUDE_DAMPING, *args, **kwargs)
189
+ )
190
+ return self
191
+
192
+ def analog_depolarizing(self, *args: Any, **kwargs: Any) -> NoiseHandler:
193
+ self.append(NoiseHandler(NoiseProtocol.ANALOG.DEPOLARIZING, *args, **kwargs))
194
+ return self
195
+
196
+ def dephasing(self, *args: Any, **kwargs: Any) -> NoiseHandler:
197
+ self.append(NoiseHandler(NoiseProtocol.ANALOG.DEPHASING, *args, **kwargs))
198
+ return self
199
+
200
+ def readout_independent(self, *args: Any, **kwargs: Any) -> NoiseHandler:
201
+ self.append(NoiseHandler(NoiseProtocol.READOUT.INDEPENDENT, *args, **kwargs))
202
+ return self
43
203
 
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
204
+ def readout_correlated(self, *args: Any, **kwargs: Any) -> NoiseHandler:
205
+ self.append(NoiseHandler(NoiseProtocol.READOUT.CORRELATED, *args, **kwargs))
206
+ return self
@@ -18,6 +18,7 @@ from qadence.blocks.utils import (
18
18
  chain,
19
19
  kron,
20
20
  )
21
+ from qadence.noise import NoiseHandler
21
22
  from qadence.parameters import (
22
23
  Parameter,
23
24
  evaluate,
@@ -35,9 +36,9 @@ class CNOT(ControlBlock):
35
36
 
36
37
  name = OpName.CNOT
37
38
 
38
- def __init__(self, control: int, target: int) -> None:
39
+ def __init__(self, control: int, target: int, noise: NoiseHandler | None = None) -> None:
39
40
  self.generator = kron(N(control), X(target) - I(target))
40
- super().__init__((control,), X(target))
41
+ super().__init__((control,), X(target), noise=noise)
41
42
 
42
43
  @property
43
44
  def eigenvalues_generator(self) -> Tensor:
@@ -63,9 +64,11 @@ class CNOT(ControlBlock):
63
64
  class MCZ(ControlBlock):
64
65
  name = OpName.MCZ
65
66
 
66
- def __init__(self, control: tuple[int, ...], target: int) -> None:
67
+ def __init__(
68
+ self, control: tuple[int, ...], target: int, noise: NoiseHandler | None = None
69
+ ) -> None:
67
70
  self.generator = kron(*[N(qubit) for qubit in control], Z(target) - I(target))
68
- super().__init__(control, Z(target))
71
+ super().__init__(control, Z(target), noise=noise)
69
72
 
70
73
  @property
71
74
  def eigenvalues_generator(self) -> Tensor:
@@ -93,8 +96,8 @@ class CZ(MCZ):
93
96
 
94
97
  name = OpName.CZ
95
98
 
96
- def __init__(self, control: int, target: int) -> None:
97
- super().__init__((control,), target)
99
+ def __init__(self, control: int, target: int, noise: NoiseHandler | None = None) -> None:
100
+ super().__init__((control,), target, noise=noise)
98
101
 
99
102
 
100
103
  class MCRX(ParametricControlBlock):
@@ -105,9 +108,10 @@ class MCRX(ParametricControlBlock):
105
108
  control: tuple[int, ...],
106
109
  target: int,
107
110
  parameter: Parameter | TNumber | sympy.Expr | str,
111
+ noise: NoiseHandler | None = None,
108
112
  ) -> None:
109
113
  self.generator = kron(*[N(qubit) for qubit in control], X(target))
110
- super().__init__(control, RX(target, parameter))
114
+ super().__init__(control, RX(target, parameter), noise=noise)
111
115
 
112
116
  @classmethod
113
117
  def num_parameters(cls) -> int:
@@ -136,8 +140,9 @@ class CRX(MCRX):
136
140
  control: int,
137
141
  target: int,
138
142
  parameter: Parameter | TNumber | sympy.Expr | str,
143
+ noise: NoiseHandler | None = None,
139
144
  ):
140
- super().__init__((control,), target, parameter)
145
+ super().__init__((control,), target, parameter, noise=noise)
141
146
 
142
147
 
143
148
  class MCRY(ParametricControlBlock):
@@ -148,9 +153,10 @@ class MCRY(ParametricControlBlock):
148
153
  control: tuple[int, ...],
149
154
  target: int,
150
155
  parameter: Parameter | TNumber | sympy.Expr | str,
156
+ noise: NoiseHandler | None = None,
151
157
  ) -> None:
152
158
  self.generator = kron(*[N(qubit) for qubit in control], Y(target))
153
- super().__init__(control, RY(target, parameter))
159
+ super().__init__(control, RY(target, parameter), noise=noise)
154
160
 
155
161
  @classmethod
156
162
  def num_parameters(cls) -> int:
@@ -175,12 +181,9 @@ class CRY(MCRY):
175
181
  name = OpName.CRY
176
182
 
177
183
  def __init__(
178
- self,
179
- control: int,
180
- target: int,
181
- parameter: TParameter,
184
+ self, control: int, target: int, parameter: TParameter, noise: NoiseHandler | None = None
182
185
  ):
183
- super().__init__((control,), target, parameter)
186
+ super().__init__((control,), target, parameter, noise=noise)
184
187
 
185
188
 
186
189
  class MCRZ(ParametricControlBlock):
@@ -191,9 +194,10 @@ class MCRZ(ParametricControlBlock):
191
194
  control: tuple[int, ...],
192
195
  target: int,
193
196
  parameter: Parameter | TNumber | sympy.Expr | str,
197
+ noise: NoiseHandler | None = None,
194
198
  ) -> None:
195
199
  self.generator = kron(*[N(qubit) for qubit in control], Z(target))
196
- super().__init__(control, RZ(target, parameter))
200
+ super().__init__(control, RZ(target, parameter), noise=noise)
197
201
 
198
202
  @classmethod
199
203
  def num_parameters(cls) -> int:
@@ -222,8 +226,9 @@ class CRZ(MCRZ):
222
226
  control: int,
223
227
  target: int,
224
228
  parameter: Parameter | TNumber | sympy.Expr | str,
229
+ noise: NoiseHandler | None = None,
225
230
  ):
226
- super().__init__((control,), target, parameter)
231
+ super().__init__((control,), target, parameter, noise=noise)
227
232
 
228
233
 
229
234
  class MCPHASE(ParametricControlBlock):
@@ -234,9 +239,10 @@ class MCPHASE(ParametricControlBlock):
234
239
  control: tuple[int, ...],
235
240
  target: int,
236
241
  parameter: Parameter | TNumber | sympy.Expr | str,
242
+ noise: NoiseHandler | None = None,
237
243
  ) -> None:
238
244
  self.generator = kron(*[N(qubit) for qubit in control], Z(target) - I(target))
239
- super().__init__(control, PHASE(target, parameter))
245
+ super().__init__(control, PHASE(target, parameter), noise=noise)
240
246
 
241
247
  @classmethod
242
248
  def num_parameters(cls) -> int:
@@ -276,8 +282,9 @@ class CPHASE(MCPHASE):
276
282
  control: int,
277
283
  target: int,
278
284
  parameter: Parameter | TNumber | sympy.Expr | str,
285
+ noise: NoiseHandler | None = None,
279
286
  ):
280
- super().__init__((control,), target, parameter)
287
+ super().__init__((control,), target, parameter, noise=noise)
281
288
 
282
289
 
283
290
  class CSWAP(ControlBlock):
@@ -285,7 +292,13 @@ class CSWAP(ControlBlock):
285
292
 
286
293
  name = OpName.CSWAP
287
294
 
288
- def __init__(self, control: int | tuple[int, ...], target1: int, target2: int) -> None:
295
+ def __init__(
296
+ self,
297
+ control: int | tuple[int, ...],
298
+ target1: int,
299
+ target2: int,
300
+ noise: NoiseHandler | None = None,
301
+ ) -> None:
289
302
  if isinstance(control, tuple):
290
303
  control = control[0]
291
304
 
@@ -303,7 +316,7 @@ class CSWAP(ControlBlock):
303
316
  + kron(a00p, a21, a12)
304
317
  )
305
318
  self.generator = no_effect + swap_effect
306
- super().__init__((control,), SWAP(target1, target2))
319
+ super().__init__((control,), SWAP(target1, target2), noise=noise)
307
320
 
308
321
  @property
309
322
  def eigenvalues_generator(self) -> Tensor:
@@ -321,9 +334,11 @@ class CSWAP(ControlBlock):
321
334
  class Toffoli(ControlBlock):
322
335
  name = OpName.TOFFOLI
323
336
 
324
- def __init__(self, control: tuple[int, ...], target: int) -> None:
337
+ def __init__(
338
+ self, control: tuple[int, ...], target: int, noise: NoiseHandler | None = None
339
+ ) -> None:
325
340
  self.generator = kron(*[N(qubit) for qubit in control], X(target) - I(target))
326
- super().__init__(control, X(target))
341
+ super().__init__(control, X(target), noise=noise)
327
342
 
328
343
  @property
329
344
  def n_qubits(self) -> int:
@@ -33,29 +33,59 @@ logger = getLogger(__name__)
33
33
 
34
34
  class HamEvo(TimeEvolutionBlock):
35
35
  """
36
- A block implementing the Hamiltonian evolution operation H where:
36
+ The Hamiltonian evolution operator U(t).
37
37
 
38
- H = exp(-iG, t)
39
- where G represents a square generator and t represents the time parameter
40
- which can be parametrized.
38
+ For time-independent Hamiltonians the solution is exact:
39
+
40
+ U(t) = exp(-iGt)
41
+
42
+ where G represents an Hermitian generator, or Hamiltonian and t represents the
43
+ time parameter. For time-dependent Hamiltonians, the solution is obtained by
44
+ numerical integration of the Schrodinger equation.
41
45
 
42
46
  Arguments:
43
- generator: Either a AbstractBlock, torch.Tensor or numpy.ndarray.
44
- parameter: A scalar or vector of numeric or torch.Tensor type.
45
- qubit_support: The qubits on which the evolution will be performed on.
46
- duration: duration of evolution in case of time-dependent generator
47
+ generator: Hamiltonian generator, either symbolic as an AbstractBlock,
48
+ or as a torch.Tensor or numpy.ndarray.
49
+ parameter: The time parameter for evolution operator. For the time-independent
50
+ case, it represents the actual value for which the evolution will be
51
+ evaluated. For the time-dependent case, it should be an instance of
52
+ TimeParameter to signal the solver the variable that will be integrated over.
53
+ qubit_support: The qubits on which the evolution will be performed on. Only
54
+ required for generators that are not a composition of blocks.
55
+ duration: (optional) duration of the evolution in case of time-dependent
56
+ generator. By default, a FeatureParameter with tag "duration" will
57
+ be initialized, and the value will then be required in the values dict.
58
+ noise_operators: (optional) the list of jump operators to use when using
59
+ a shrodinger solver, allowing to perform noisy simulations.
47
60
 
48
61
  Examples:
49
62
 
50
63
  ```python exec="on" source="material-block" result="json"
51
- from qadence import RX, HamEvo, run, PI
64
+ from qadence import X, HamEvo, PI, add, run
65
+ from qadence import FeatureParameter, TimeParameter
52
66
  import torch
53
- hevo = HamEvo(generator=RX(0, PI), parameter=torch.rand(2))
54
- print(run(hevo))
55
- # Now lets use a torch.Tensor as a generator, Now we have to pass the support
56
- gen = torch.rand(2,2, dtype=torch.complex128)
57
- hevo = HamEvo(generator=gen, parameter=torch.rand(2), qubit_support=(0,))
58
- print(run(hevo))
67
+
68
+ n_qubits = 3
69
+
70
+ # Hamiltonian as a block composition
71
+ hamiltonian = add(X(i) for i in range(n_qubits))
72
+ hevo = HamEvo(hamiltonian, parameter=torch.rand(2))
73
+ state = run(hevo)
74
+
75
+ # Hamiltonian as a random matrix
76
+ hamiltonian = torch.rand(2, 2, dtype=torch.complex128)
77
+ hevo = HamEvo(hamiltonian, parameter=torch.rand(2), qubit_support=(0,))
78
+ state = run(hevo)
79
+
80
+ # Time-dependent Hamiltonian
81
+ t = TimeParameter("t")
82
+ hamiltonian = t * add(X(i) for i in range(n_qubits))
83
+ hevo = HamEvo(hamiltonian, parameter=t)
84
+ state = run(hevo, values = {"duration": torch.tensor(1.0)})
85
+
86
+ # Adding noise operators
87
+ noise_ops = [X(0)]
88
+ hevo = HamEvo(hamiltonian, parameter=t, noise_operators=noise_ops)
59
89
  ```
60
90
  """
61
91
 
@@ -67,21 +97,31 @@ class HamEvo(TimeEvolutionBlock):
67
97
  generator: Union[TGenerator, AbstractBlock],
68
98
  parameter: TParameter,
69
99
  qubit_support: tuple[int, ...] = None,
70
- duration: float | None = None,
100
+ duration: TParameter | None = None,
101
+ noise_operators: list[AbstractBlock] = list(),
71
102
  ):
72
- gen_exprs = {}
103
+ params = {}
73
104
  if qubit_support is None and not isinstance(generator, AbstractBlock):
74
105
  raise ValueError("You have to supply a qubit support for non-block generators.")
75
106
  super().__init__(qubit_support if qubit_support else generator.qubit_support)
76
107
  if isinstance(generator, AbstractBlock):
77
108
  qubit_support = generator.qubit_support
78
109
  if generator.is_parametric:
79
- gen_exprs = {str(e): e for e in expressions(generator)}
80
-
81
- if generator.is_time_dependent and duration is None:
82
- raise ValueError("For time-dependent generators, a duration must be specified.")
83
-
110
+ params = {str(e): e for e in expressions(generator)}
111
+ if generator.is_time_dependent:
112
+ if isinstance(duration, str):
113
+ duration = Parameter(duration, trainable=False)
114
+ elif duration is None:
115
+ duration = Parameter("duration", trainable=False)
116
+ if not generator.is_time_dependent and duration is not None:
117
+ raise TypeError(
118
+ "Duration argument is only supported for time-dependent generators."
119
+ )
84
120
  elif isinstance(generator, torch.Tensor):
121
+ if duration is not None:
122
+ raise TypeError(
123
+ "Duration argument is only supported for time-dependent generators."
124
+ )
85
125
  msg = "Please provide a square generator."
86
126
  if len(generator.shape) == 2:
87
127
  assert generator.shape[0] == generator.shape[1], msg
@@ -94,19 +134,41 @@ class HamEvo(TimeEvolutionBlock):
94
134
  In case of a 3D generator, the batch dim\
95
135
  is expected to be at dim 0."
96
136
  )
97
- gen_exprs = {str(generator.__hash__()): generator}
137
+ params = {str(generator.__hash__()): generator}
98
138
  elif isinstance(generator, (sympy.Basic, sympy.Array)):
99
- gen_exprs = {str(generator): generator}
139
+ if duration is not None:
140
+ raise TypeError(
141
+ "Duration argument is only supported for time-dependent generators."
142
+ )
143
+ params = {str(generator): generator}
100
144
  else:
101
145
  raise TypeError(
102
146
  f"Generator of type {type(generator)} not supported.\
103
147
  If you're using a numpy.ndarray, please cast it to a torch tensor."
104
148
  )
105
- ps = {"parameter": Parameter(parameter), **gen_exprs}
106
- self.parameters = ParamMap(**ps)
149
+ if duration is not None:
150
+ params = {"duration": Parameter(duration), **params}
151
+ params = {"parameter": Parameter(parameter), **params}
152
+ self.parameters = ParamMap(**params)
153
+ self.time_param = parameter
107
154
  self.generator = generator
108
155
  self.duration = duration
109
156
 
157
+ if len(noise_operators) > 0:
158
+ if not all(
159
+ [
160
+ len(set(op.qubit_support + self.qubit_support) - set(self.qubit_support)) == 0
161
+ for op in noise_operators
162
+ ]
163
+ ):
164
+ raise ValueError(
165
+ "Noise operators should be defined"
166
+ " over the same or a subset of the qubit support"
167
+ )
168
+ if True in [op.is_parametric for op in noise_operators]:
169
+ raise ValueError("Parametric operators are not supported")
170
+ self.noise_operators = noise_operators
171
+
110
172
  @classmethod
111
173
  def num_parameters(cls) -> int:
112
174
  return 2