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
@@ -1,37 +1,23 @@
1
1
  from __future__ import annotations
2
2
 
3
+ import re
4
+ from functools import partial, reduce
3
5
  from itertools import chain as flatten
4
- from math import prod
5
- from typing import Any, Sequence, Tuple
6
+ from typing import Any, Callable, Sequence
6
7
 
7
8
  import pyqtorch as pyq
8
9
  import sympy
9
10
  import torch
10
- from pyqtorch.embed import Embedding
11
- from pyqtorch.matrices import _dagger
12
- from pyqtorch.time_dependent.sesolve import sesolve
13
- from pyqtorch.utils import is_diag
11
+ from pyqtorch.embed import ConcretizedCallable
14
12
  from torch import (
15
13
  Tensor,
16
14
  cdouble,
17
15
  complex64,
18
- diag_embed,
19
- diagonal,
20
- exp,
21
16
  float64,
22
- linalg,
23
17
  tensor,
24
- transpose,
25
- )
26
- from torch import device as torch_device
27
- from torch import dtype as torch_dtype
28
- from torch.nn import Module, ParameterDict
29
-
30
- from qadence.backends.utils import (
31
- finitediff,
32
- pyqify,
33
- unpyqify,
34
18
  )
19
+ from torch.nn import Module
20
+
35
21
  from qadence.blocks import (
36
22
  AbstractBlock,
37
23
  AddBlock,
@@ -43,11 +29,8 @@ from qadence.blocks import (
43
29
  ScaleBlock,
44
30
  TimeEvolutionBlock,
45
31
  )
46
- from qadence.blocks.block_to_tensor import (
47
- _block_to_tensor_embedded,
48
- )
49
32
  from qadence.blocks.primitive import ProjectorBlock
50
- from qadence.blocks.utils import parameters
33
+ from qadence.noise import NoiseHandler
51
34
  from qadence.operations import (
52
35
  U,
53
36
  multi_qubit_gateset,
@@ -56,10 +39,28 @@ from qadence.operations import (
56
39
  three_qubit_gateset,
57
40
  two_qubit_gateset,
58
41
  )
59
- from qadence.types import OpName
42
+ from qadence.types import NoiseProtocol, OpName
60
43
 
61
44
  from .config import Configuration
62
45
 
46
+ SYMPY_TO_PYQ_MAPPING = {
47
+ sympy.Pow: "pow",
48
+ sympy.cos: "cos",
49
+ sympy.Add: "add",
50
+ sympy.Mul: "mul",
51
+ sympy.sin: "sin",
52
+ sympy.log: "log",
53
+ sympy.tan: "tan",
54
+ sympy.tanh: "tanh",
55
+ sympy.Heaviside: "hs",
56
+ sympy.Abs: "abs",
57
+ sympy.exp: "exp",
58
+ sympy.acos: "acos",
59
+ sympy.asin: "asin",
60
+ sympy.atan: "atan",
61
+ }
62
+
63
+
63
64
  # Tdagger is not supported currently
64
65
  supported_gates = list(set(OpName.list()) - set([OpName.TDAGGER]))
65
66
  """The set of supported gates.
@@ -98,9 +99,99 @@ def extract_parameter(block: ScaleBlock | ParametricBlock, config: Configuration
98
99
  return config.get_param_name(block)[0]
99
100
 
100
101
 
102
+ def replace_underscore_floats(s: str) -> str:
103
+ """Replace underscores with periods for all floats in given string.
104
+
105
+ Needed for correct parsing of string by sympy parser.
106
+
107
+ Args:
108
+ s (str): string expression
109
+
110
+ Returns:
111
+ str: transformed string expression
112
+ """
113
+
114
+ # Regular expression to match floats written with underscores instead of dots
115
+ float_with_underscore_pattern = r"""
116
+ (?<!\w) # Negative lookbehind to ensure not part of a word
117
+ -? # Optional negative sign
118
+ \d+ # One or more digits (before underscore)
119
+ _ # The underscore acting as decimal separator
120
+ \d+ # One or more digits (after underscore)
121
+ ([eE][-+]?\d+)? # Optional exponent part for scientific notation
122
+ (?!\w) # Negative lookahead to ensure not part of a word
123
+ """
124
+
125
+ # Function to replace the underscore with a dot
126
+ def underscore_to_dot(match: re.Match) -> Any:
127
+ return match.group(0).replace("_", ".")
128
+
129
+ # Compile the regular expression
130
+ pattern = re.compile(float_with_underscore_pattern, re.VERBOSE)
131
+
132
+ return pattern.sub(underscore_to_dot, s)
133
+
134
+
135
+ def sympy_to_pyq(expr: sympy.Expr) -> ConcretizedCallable | Tensor:
136
+ """Convert sympy expression to pyqtorch ConcretizedCallable object.
137
+
138
+ Args:
139
+ expr (sympy.Expr): sympy expression
140
+
141
+ Returns:
142
+ ConcretizedCallable: expression encoded as ConcretizedCallable
143
+ """
144
+
145
+ # base case - independent argument
146
+ if len(expr.args) == 0:
147
+ try:
148
+ res = torch.as_tensor(float(expr))
149
+ except Exception as e:
150
+ res = str(expr)
151
+
152
+ if "/" in res: # Found a rational
153
+ res = torch.as_tensor(float(sympy.Rational(res).evalf()))
154
+ return res
155
+
156
+ # Recursively iterate through current function arguments
157
+ all_results = []
158
+ for arg in expr.args:
159
+ res = sympy_to_pyq(arg)
160
+ all_results.append(res)
161
+
162
+ # deal with multi-argument (>2) sympy functions: converting to nested
163
+ # ConcretizedCallable objects
164
+ if len(all_results) > 2:
165
+
166
+ def fn(x: str | ConcretizedCallable, y: str | ConcretizedCallable) -> Callable:
167
+ return partial(ConcretizedCallable, call_name=SYMPY_TO_PYQ_MAPPING[expr.func])( # type: ignore [no-any-return]
168
+ abstract_args=[x, y]
169
+ )
170
+
171
+ concretized_callable = reduce(fn, all_results)
172
+ else:
173
+ concretized_callable = ConcretizedCallable(SYMPY_TO_PYQ_MAPPING[expr.func], all_results)
174
+ return concretized_callable
175
+
176
+
101
177
  def convert_block(
102
- block: AbstractBlock, n_qubits: int = None, config: Configuration = None
178
+ block: AbstractBlock,
179
+ n_qubits: int = None,
180
+ config: Configuration = None,
103
181
  ) -> Sequence[Module | Tensor | str | sympy.Expr]:
182
+ """Convert block to native Pyqtorch representation.
183
+
184
+ Args:
185
+ block (AbstractBlock): Block to convert.
186
+ n_qubits (int, optional): Number of qubits. Defaults to None.
187
+ config (Configuration, optional): Backend configuration instance. Defaults to None.
188
+
189
+ Raises:
190
+ NotImplementedError: For non supported blocks.
191
+
192
+ Returns:
193
+ Sequence[Module | Tensor | str | sympy.Expr]: List of native operations.
194
+ """
104
195
  if isinstance(block, (Tensor, str, sympy.Expr)): # case for hamevo generators
105
196
  if isinstance(block, Tensor):
106
197
  block = block.permute(1, 2, 0) # put batch size in the back
@@ -112,41 +203,73 @@ def convert_block(
112
203
  if config is None:
113
204
  config = Configuration()
114
205
 
206
+ noise: NoiseHandler | None = None
207
+ if hasattr(block, "noise") and block.noise:
208
+ noise = convert_digital_noise(block.noise)
209
+
115
210
  if isinstance(block, ScaleBlock):
116
211
  scaled_ops = convert_block(block.block, n_qubits, config)
117
- scale = extract_parameter(block, config)
118
- return [pyq.Scale(pyq.Sequence(scaled_ops), scale)]
212
+ scale = extract_parameter(block, config=config)
213
+
214
+ # replace underscore by dot when underscore is between two numbers in string
215
+ if isinstance(scale, str):
216
+ scale = replace_underscore_floats(scale)
217
+
218
+ if isinstance(scale, str) and not config._use_gate_params:
219
+ param = sympy_to_pyq(sympy.parse_expr(scale))
220
+ else:
221
+ param = scale
222
+
223
+ return [pyq.Scale(pyq.Sequence(scaled_ops), param)]
119
224
 
120
225
  elif isinstance(block, TimeEvolutionBlock):
226
+ duration = block.duration # type: ignore [attr-defined]
121
227
  if getattr(block.generator, "is_time_dependent", False):
122
- return [PyQTimeDependentEvolution(qubit_support, n_qubits, block, config)]
123
- else:
124
- if isinstance(block.generator, sympy.Basic):
125
- generator = config.get_param_name(block)[1]
126
- elif isinstance(block.generator, Tensor):
127
- m = block.generator.to(dtype=cdouble)
128
- generator = convert_block(
129
- MatrixBlock(
130
- m,
131
- qubit_support=qubit_support,
132
- check_unitary=False,
133
- check_hermitian=True,
134
- )
135
- )[0]
136
- else:
137
- generator = convert_block(block.generator, n_qubits, config)[0] # type: ignore[arg-type]
138
- time_param = config.get_param_name(block)[0]
139
- return [
140
- pyq.HamiltonianEvolution(
228
+ config._use_gate_params = False
229
+ duration = config.get_param_name(block)[1]
230
+ generator = convert_block(block.generator, config=config)[0] # type: ignore [arg-type]
231
+ elif isinstance(block.generator, sympy.Basic):
232
+ generator = config.get_param_name(block)[1]
233
+
234
+ elif isinstance(block.generator, Tensor):
235
+ m = block.generator.to(dtype=cdouble)
236
+ generator = convert_block(
237
+ MatrixBlock(
238
+ m,
141
239
  qubit_support=qubit_support,
142
- generator=generator,
143
- time=time_param,
144
- cache_length=0,
240
+ check_unitary=False,
241
+ check_hermitian=True,
145
242
  )
243
+ )[0]
244
+ else:
245
+ generator = convert_block(block.generator, n_qubits, config)[0] # type: ignore[arg-type]
246
+ time_param = config.get_param_name(block)[0]
247
+
248
+ # convert noise operators here
249
+ noise_operators: list = [
250
+ convert_block(noise_block, config=config)[0] for noise_block in block.noise_operators
251
+ ]
252
+ if len(noise_operators) > 0:
253
+ # squeeze batch size for noise operators
254
+ noise_operators = [
255
+ pyq_op.tensor(full_support=qubit_support).squeeze(-1) for pyq_op in noise_operators
146
256
  ]
147
257
 
258
+ return [
259
+ pyq.HamiltonianEvolution(
260
+ qubit_support=qubit_support,
261
+ generator=generator,
262
+ time=time_param,
263
+ cache_length=0,
264
+ duration=duration,
265
+ solver=config.ode_solver,
266
+ steps=config.n_steps_hevo,
267
+ noise_operators=noise_operators,
268
+ )
269
+ ]
270
+
148
271
  elif isinstance(block, MatrixBlock):
149
- return [pyq.primitives.Primitive(block.matrix, block.qubit_support)]
272
+ return [pyq.primitives.Primitive(block.matrix, block.qubit_support, noise=noise)]
150
273
  elif isinstance(block, CompositeBlock):
151
274
  ops = list(flatten(*(convert_block(b, n_qubits, config) for b in block.blocks)))
152
275
  if isinstance(block, AddBlock):
@@ -159,38 +282,66 @@ def convert_block(
159
282
  if isinstance(block, ProjectorBlock):
160
283
  projector = getattr(pyq, block.name)
161
284
  if block.name == OpName.N:
162
- return [projector(target=qubit_support)]
285
+ return [projector(target=qubit_support, noise=noise)]
163
286
  else:
164
- return [projector(qubit_support=qubit_support, ket=block.ket, bra=block.bra)]
287
+ return [
288
+ projector(
289
+ qubit_support=qubit_support,
290
+ ket=block.ket,
291
+ bra=block.bra,
292
+ noise=noise,
293
+ )
294
+ ]
165
295
  else:
166
296
  return [getattr(pyq, block.name)(qubit_support[0])]
167
297
  elif isinstance(block, tuple(single_qubit_gateset)):
168
298
  pyq_cls = getattr(pyq, block.name)
169
299
  if isinstance(block, ParametricBlock):
170
300
  if isinstance(block, U):
171
- op = pyq_cls(qubit_support[0], *config.get_param_name(block))
301
+ op = pyq_cls(
302
+ qubit_support[0],
303
+ *config.get_param_name(block),
304
+ noise=noise,
305
+ )
172
306
  else:
173
- op = pyq_cls(qubit_support[0], extract_parameter(block, config))
307
+ param = extract_parameter(block, config)
308
+ op = pyq_cls(qubit_support[0], param, noise=noise)
174
309
  else:
175
- op = pyq_cls(qubit_support[0])
310
+ op = pyq_cls(qubit_support[0], noise=noise) # type: ignore [attr-defined]
176
311
  return [op]
177
312
  elif isinstance(block, tuple(two_qubit_gateset)):
178
313
  pyq_cls = getattr(pyq, block.name)
179
314
  if isinstance(block, ParametricBlock):
180
- op = pyq_cls(qubit_support[0], qubit_support[1], extract_parameter(block, config))
315
+ op = pyq_cls(
316
+ qubit_support[0],
317
+ qubit_support[1],
318
+ extract_parameter(block, config),
319
+ noise=noise,
320
+ )
181
321
  else:
182
- op = pyq_cls(qubit_support[0], qubit_support[1])
322
+ op = pyq_cls(
323
+ qubit_support[0], qubit_support[1], noise=noise # type: ignore [attr-defined]
324
+ )
183
325
  return [op]
184
326
  elif isinstance(block, tuple(three_qubit_gateset) + tuple(multi_qubit_gateset)):
185
327
  block_name = block.name[1:] if block.name.startswith("M") else block.name
186
328
  pyq_cls = getattr(pyq, block_name)
187
329
  if isinstance(block, ParametricBlock):
188
- op = pyq_cls(qubit_support[:-1], qubit_support[-1], extract_parameter(block, config))
330
+ op = pyq_cls(
331
+ qubit_support[:-1],
332
+ qubit_support[-1],
333
+ extract_parameter(block, config),
334
+ noise=noise,
335
+ )
189
336
  else:
190
337
  if "CSWAP" in block_name:
191
- op = pyq_cls(qubit_support[:-2], qubit_support[-2:])
338
+ op = pyq_cls(
339
+ qubit_support[:-2], qubit_support[-2:], noise=noise # type: ignore [attr-defined]
340
+ )
192
341
  else:
193
- op = pyq_cls(qubit_support[:-1], qubit_support[-1])
342
+ op = pyq_cls(
343
+ qubit_support[:-1], qubit_support[-1], noise=noise # type: ignore [attr-defined]
344
+ )
194
345
  return [op]
195
346
  else:
196
347
  raise NotImplementedError(
@@ -200,182 +351,43 @@ def convert_block(
200
351
  )
201
352
 
202
353
 
203
- class PyQTimeDependentEvolution(Module):
204
- def __init__(
205
- self,
206
- qubit_support: Tuple[int, ...],
207
- n_qubits: int,
208
- block: TimeEvolutionBlock,
209
- config: Configuration,
210
- ):
211
- super().__init__()
212
- self.qubit_support = qubit_support
213
- self.n_qubits = n_qubits
214
- self.param_names = config.get_param_name(block)
215
- self.block = block
216
- self.hmat: Tensor
217
- self.config = config
218
-
219
- def _hamiltonian(self: PyQTimeDependentEvolution, values: dict[str, Tensor]) -> Tensor:
220
- hmat = _block_to_tensor_embedded(
221
- block.generator, # type: ignore[arg-type]
222
- values=values,
223
- qubit_support=self.qubit_support,
224
- use_full_support=False,
225
- device=self.device,
226
- )
227
- return hmat.permute(1, 2, 0)
228
-
229
- self._hamiltonian = _hamiltonian
230
-
231
- self._time_evolution = lambda values: values[self.param_names[0]]
232
- self._device: torch_device = (
233
- self.hmat.device if hasattr(self, "hmat") else torch_device("cpu")
234
- )
235
- self._dtype: torch_dtype = self.hmat.dtype if hasattr(self, "hmat") else cdouble
354
+ def convert_digital_noise(noise: NoiseHandler) -> pyq.noise.NoiseProtocol | None:
355
+ """Convert the digital noise into pyqtorch NoiseProtocol.
236
356
 
237
- def _unitary(self, hamiltonian: Tensor, time_evolution: Tensor) -> Tensor:
238
- self.batch_size = max(hamiltonian.size()[2], len(time_evolution))
239
- diag_check = tensor(
240
- [is_diag(hamiltonian[..., i]) for i in range(hamiltonian.size()[2])], device=self.device
241
- )
242
-
243
- def _evolve_diag_operator(hamiltonian: Tensor, time_evolution: Tensor) -> Tensor:
244
- evol_operator = diagonal(hamiltonian) * (-1j * time_evolution).view((-1, 1))
245
- evol_operator = diag_embed(exp(evol_operator))
246
- return transpose(evol_operator, 0, -1)
247
-
248
- def _evolve_matrixexp_operator(hamiltonian: Tensor, time_evolution: Tensor) -> Tensor:
249
- evol_operator = transpose(hamiltonian, 0, -1) * (-1j * time_evolution).view((-1, 1, 1))
250
- evol_operator = linalg.matrix_exp(evol_operator)
251
- return transpose(evol_operator, 0, -1)
252
-
253
- evolve_operator = (
254
- _evolve_diag_operator if bool(prod(diag_check)) else _evolve_matrixexp_operator
255
- )
256
- return evolve_operator(hamiltonian, time_evolution)
257
-
258
- def unitary(self, values: dict[str, Tensor]) -> Tensor:
259
- """The evolved operator given current parameter values for generator and time evolution."""
260
- return self._unitary(self._hamiltonian(self, values), self._time_evolution(values))
261
-
262
- def jacobian_time(self, values: dict[str, Tensor]) -> Tensor:
263
- """Approximate jacobian of the evolved operator with respect to time evolution."""
264
- return finitediff(
265
- lambda t: self._unitary(time_evolution=t, hamiltonian=self._hamiltonian(self, values)),
266
- values[self.param_names[0]].reshape(-1, 1),
267
- (0,),
268
- )
269
-
270
- def jacobian_generator(self, values: dict[str, Tensor]) -> Tensor:
271
- """Approximate jacobian of the evolved operator with respect to generator parameter(s)."""
272
- if len(self.param_names) > 2:
273
- raise NotImplementedError(
274
- "jacobian_generator does not support generators\
275
- with more than 1 parameter."
276
- )
357
+ Args:
358
+ noise (NoiseHandler): Noise to convert.
277
359
 
278
- def _generator(val: Tensor) -> Tensor:
279
- val_copy = values.copy()
280
- val_copy[self.param_names[1]] = val
281
- hmat = _block_to_tensor_embedded(
282
- self.block.generator, # type: ignore[arg-type]
283
- values=val_copy,
284
- qubit_support=self.qubit_support,
285
- use_full_support=False,
286
- device=self.device,
287
- )
288
- return hmat.permute(1, 2, 0)
289
-
290
- return finitediff(
291
- lambda v: self._unitary(
292
- time_evolution=self._time_evolution(values), hamiltonian=_generator(v)
293
- ),
294
- values[self.param_names[1]].reshape(-1, 1),
295
- (0,),
296
- )
360
+ Returns:
361
+ pyq.noise.NoiseProtocol | None: Pyqtorch native noise protocol
362
+ if there are any digital noise protocols.
363
+ """
364
+ digital_part = noise.filter(NoiseProtocol.DIGITAL)
365
+ if digital_part is None:
366
+ return None
367
+ return pyq.noise.NoiseProtocol(
368
+ [
369
+ pyq.noise.NoiseProtocol(proto, option.get("error_probability"))
370
+ for proto, option in zip(digital_part.protocol, digital_part.options)
371
+ ]
372
+ )
297
373
 
298
- def dagger(self, values: dict[str, Tensor]) -> Tensor:
299
- """Dagger of the evolved operator given the current parameter values."""
300
- return _dagger(self.unitary(values))
301
-
302
- def _get_time_parameter(self) -> str:
303
- # get unique time parameters
304
- unique_time_params = set()
305
- for p in parameters(self.block.generator): # type: ignore [arg-type]
306
- if getattr(p, "is_time", False):
307
- unique_time_params.add(str(p))
308
-
309
- if len(unique_time_params) > 1:
310
- raise Exception("Only a single time parameter is supported.")
311
-
312
- return unique_time_params.pop()
313
-
314
- def forward(
315
- self,
316
- state: Tensor,
317
- values: dict[str, Tensor] | ParameterDict = dict(),
318
- embedding: Embedding | None = None,
319
- ) -> Tensor:
320
- def Ht(t: Tensor | float) -> Tensor:
321
- # values dict has to change with new value of t
322
- # initial value of a feature parameter inside generator block
323
- # has to be inferred here
324
- new_vals = dict()
325
- for str_expr, val in values.items():
326
- expr = sympy.sympify(str_expr)
327
- t_symb = sympy.Symbol(self._get_time_parameter())
328
- free_symbols = expr.free_symbols
329
- if t_symb in free_symbols:
330
- # create substitution list for time and feature params
331
- subs_list = [(t_symb, t)]
332
-
333
- if len(free_symbols) > 1:
334
- # get feature param symbols
335
- feat_symbols = free_symbols.difference(set([t_symb]))
336
-
337
- # get feature param values
338
- feat_vals = values["orig_param_values"]
339
-
340
- # update substitution list with feature param values
341
- for fs in feat_symbols:
342
- subs_list.append((fs, feat_vals[str(fs)]))
343
-
344
- # evaluate expression with new time param value
345
- new_vals[str_expr] = torch.tensor(float(expr.subs(subs_list)))
346
- else:
347
- # expression doesn't contain time parameter - copy it as is
348
- new_vals[str_expr] = val
349
-
350
- # get matrix form of generator
351
- hmat = _block_to_tensor_embedded(
352
- self.block.generator, # type: ignore[arg-type]
353
- values=new_vals,
354
- qubit_support=self.qubit_support,
355
- use_full_support=False,
356
- device=self.device,
357
- ).squeeze(0)
358
-
359
- return hmat
360
-
361
- tsave = torch.linspace(0, self.block.duration, self.config.n_steps_hevo) # type: ignore [attr-defined]
362
- result = pyqify(
363
- sesolve(Ht, unpyqify(state).T[:, 0:1], tsave, self.config.ode_solver).states[-1].T
364
- )
365
374
 
366
- return result
375
+ def convert_readout_noise(n_qubits: int, noise: NoiseHandler) -> pyq.noise.ReadoutNoise | None:
376
+ """Convert the readout noise into pyqtorch ReadoutNoise.
367
377
 
368
- @property
369
- def device(self) -> torch_device:
370
- return self._device
378
+ Args:
379
+ n_qubits (int): Number of qubits
380
+ noise (NoiseHandler): Noise to convert.
371
381
 
372
- @property
373
- def dtype(self) -> torch_dtype:
374
- return self._dtype
382
+ Returns:
383
+ pyq.noise.ReadoutNoise | None: Pyqtorch native ReadoutNoise instance
384
+ if readout is is noise.
385
+ """
386
+ readout_part = noise.filter(NoiseProtocol.READOUT)
387
+ if readout_part is None:
388
+ return None
375
389
 
376
- def to(self, *args: Any, **kwargs: Any) -> PyQTimeDependentEvolution:
377
- if hasattr(self, "hmat"):
378
- self.hmat = self.hmat.to(*args, **kwargs)
379
- self._device = self.hmat.device
380
- self._dtype = self.hmat.dtype
381
- return self
390
+ if readout_part.protocol[0] == NoiseProtocol.READOUT.INDEPENDENT:
391
+ return pyq.noise.ReadoutNoise(n_qubits, **readout_part.options[0])
392
+ else:
393
+ return pyq.noise.CorrelatedReadoutNoise(**readout_part.options[0])
qadence/backends/utils.py CHANGED
@@ -10,6 +10,7 @@ import torch
10
10
  from numpy.typing import ArrayLike
11
11
  from pyqtorch.apply import apply_operator
12
12
  from pyqtorch.primitives import Parametric as PyQParametric
13
+ from pyqtorch.utils import DensityMatrix
13
14
  from torch import (
14
15
  Tensor,
15
16
  cat,
@@ -121,7 +122,14 @@ def pyqify(state: Tensor, n_qubits: int = None) -> ArrayLike:
121
122
 
122
123
 
123
124
  def unpyqify(state: Tensor) -> Tensor:
124
- """Convert a state of shape [2] * n_qubits + [batch_size] to (batch_size, 2**n_qubits)."""
125
+ """
126
+ Convert a state of shape [2] * n_qubits + [batch_size] to (batch_size, 2**n_qubits).
127
+
128
+ Convert a density matrix of shape (2**n_qubits, 2**n_qubits, batch_size)
129
+ to (batch_size, 2**n_qubits, 2**n_qubits)
130
+ """
131
+ if isinstance(state, DensityMatrix):
132
+ return torch.einsum("ijk->kij", state)
125
133
  return torch.flatten(state, start_dim=0, end_dim=-2).t()
126
134
 
127
135
 
@@ -263,7 +263,7 @@ class AbstractBlock(ABC):
263
263
 
264
264
  @classmethod
265
265
  def _from_json(cls, path: str | Path) -> AbstractBlock:
266
- d: dict = {}
266
+ d: dict = dict()
267
267
  if isinstance(path, str):
268
268
  path = Path(path)
269
269
  try:
@@ -5,7 +5,7 @@ from typing import Callable, Iterable, List
5
5
  import sympy
6
6
  from numpy import array as nparray
7
7
  from numpy import cdouble as npcdouble
8
- from torch import tensor
8
+ from torch import as_tensor, tensor
9
9
 
10
10
  from qadence.blocks import (
11
11
  AbstractBlock,
@@ -111,11 +111,13 @@ def embedding(
111
111
  angle: ArrayLike
112
112
  values = {}
113
113
  for symbol in expr.free_symbols:
114
- if not symbol.is_time:
115
- if symbol.name in inputs:
116
- value = inputs[symbol.name]
117
- elif symbol.name in params:
118
- value = params[symbol.name]
114
+ if symbol.name in inputs:
115
+ value = inputs[symbol.name]
116
+ elif symbol.name in params:
117
+ value = params[symbol.name]
118
+ else:
119
+ if symbol.is_time:
120
+ value = tensor(1.0)
119
121
  else:
120
122
  msg_trainable = "Trainable" if symbol.trainable else "Non-trainable"
121
123
  raise KeyError(
@@ -123,9 +125,7 @@ def embedding(
123
125
  f"inputs list: {list(inputs.keys())} nor the "
124
126
  f"params list: {list(params.keys())}."
125
127
  )
126
- values[symbol.name] = value
127
- else:
128
- values[symbol.name] = tensor(1.0)
128
+ values[symbol.name] = value
129
129
  angle = fn(**values)
130
130
  # do not reshape parameters which are multi-dimensional
131
131
  # tensors, such as for example generator matrices
@@ -142,8 +142,18 @@ def embedding(
142
142
  gate_lvl_params[uuid] = embedded_params[e]
143
143
  return gate_lvl_params
144
144
  else:
145
- out = {stringify(k): v for k, v in embedded_params.items()}
146
- out.update({"orig_param_values": inputs})
145
+ embedded_params.update(inputs)
146
+ for k, v in params.items():
147
+ if k not in embedded_params:
148
+ embedded_params[k] = v
149
+ out = {
150
+ stringify(k)
151
+ if not isinstance(k, str)
152
+ else k: as_tensor(v)[None]
153
+ if as_tensor(v).ndim == 0
154
+ else v
155
+ for k, v in embedded_params.items()
156
+ }
147
157
  return out
148
158
 
149
159
  params: ParamDictType