qadence 1.7.8__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 (51) 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/constructors.py +2 -2
  30. qadence/ml_tools/models.py +7 -7
  31. qadence/ml_tools/printing.py +2 -1
  32. qadence/model.py +5 -5
  33. qadence/noise/__init__.py +2 -2
  34. qadence/noise/protocols.py +216 -29
  35. qadence/operations/control_ops.py +37 -22
  36. qadence/operations/ham_evo.py +1 -0
  37. qadence/operations/parametric.py +32 -10
  38. qadence/operations/primitive.py +61 -29
  39. qadence/overlap.py +0 -6
  40. qadence/parameters.py +3 -2
  41. qadence/transpile/__init__.py +2 -1
  42. qadence/transpile/noise.py +46 -0
  43. qadence/types.py +26 -2
  44. {qadence-1.7.8.dist-info → qadence-1.8.0.dist-info}/METADATA +5 -8
  45. {qadence-1.7.8.dist-info → qadence-1.8.0.dist-info}/RECORD +47 -50
  46. qadence/backends/braket/__init__.py +0 -4
  47. qadence/backends/braket/backend.py +0 -234
  48. qadence/backends/braket/config.py +0 -22
  49. qadence/backends/braket/convert_ops.py +0 -116
  50. {qadence-1.7.8.dist-info → qadence-1.8.0.dist-info}/WHEEL +0 -0
  51. {qadence-1.7.8.dist-info → qadence-1.8.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,6 +99,81 @@ 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
178
  block: AbstractBlock, n_qubits: int = None, config: Configuration = None
103
179
  ) -> Sequence[Module | Tensor | str | sympy.Expr]:
@@ -112,41 +188,57 @@ def convert_block(
112
188
  if config is None:
113
189
  config = Configuration()
114
190
 
191
+ noise: NoiseHandler | None = None
192
+ if hasattr(block, "noise") and block.noise:
193
+ noise = convert_digital_noise(block.noise)
194
+
115
195
  if isinstance(block, ScaleBlock):
116
196
  scaled_ops = convert_block(block.block, n_qubits, config)
117
- scale = extract_parameter(block, config)
118
- return [pyq.Scale(pyq.Sequence(scaled_ops), scale)]
197
+ scale = extract_parameter(block, config=config)
198
+
199
+ # replace underscore by dot when underscore is between two numbers in string
200
+ if isinstance(scale, str):
201
+ scale = replace_underscore_floats(scale)
202
+
203
+ if isinstance(scale, str) and not config._use_gate_params:
204
+ param = sympy_to_pyq(sympy.parse_expr(scale))
205
+ else:
206
+ param = scale
207
+
208
+ return [pyq.Scale(pyq.Sequence(scaled_ops), param)]
119
209
 
120
210
  elif isinstance(block, TimeEvolutionBlock):
121
211
  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(
212
+ config._use_gate_params = False
213
+ generator = convert_block(block.generator, config=config)[0] # type: ignore [arg-type]
214
+ elif isinstance(block.generator, sympy.Basic):
215
+ generator = config.get_param_name(block)[1]
216
+
217
+ elif isinstance(block.generator, Tensor):
218
+ m = block.generator.to(dtype=cdouble)
219
+ generator = convert_block(
220
+ MatrixBlock(
221
+ m,
141
222
  qubit_support=qubit_support,
142
- generator=generator,
143
- time=time_param,
144
- cache_length=0,
223
+ check_unitary=False,
224
+ check_hermitian=True,
145
225
  )
146
- ]
226
+ )[0]
227
+ else:
228
+ generator = convert_block(block.generator, n_qubits, config)[0] # type: ignore[arg-type]
229
+ time_param = config.get_param_name(block)[0]
230
+
231
+ return [
232
+ pyq.HamiltonianEvolution(
233
+ qubit_support=qubit_support,
234
+ generator=generator,
235
+ time=time_param,
236
+ cache_length=0,
237
+ )
238
+ ]
147
239
 
148
240
  elif isinstance(block, MatrixBlock):
149
- return [pyq.primitives.Primitive(block.matrix, block.qubit_support)]
241
+ return [pyq.primitives.Primitive(block.matrix, block.qubit_support, noise=noise)]
150
242
  elif isinstance(block, CompositeBlock):
151
243
  ops = list(flatten(*(convert_block(b, n_qubits, config) for b in block.blocks)))
152
244
  if isinstance(block, AddBlock):
@@ -159,38 +251,66 @@ def convert_block(
159
251
  if isinstance(block, ProjectorBlock):
160
252
  projector = getattr(pyq, block.name)
161
253
  if block.name == OpName.N:
162
- return [projector(target=qubit_support)]
254
+ return [projector(target=qubit_support, noise=noise)]
163
255
  else:
164
- return [projector(qubit_support=qubit_support, ket=block.ket, bra=block.bra)]
256
+ return [
257
+ projector(
258
+ qubit_support=qubit_support,
259
+ ket=block.ket,
260
+ bra=block.bra,
261
+ noise=noise,
262
+ )
263
+ ]
165
264
  else:
166
265
  return [getattr(pyq, block.name)(qubit_support[0])]
167
266
  elif isinstance(block, tuple(single_qubit_gateset)):
168
267
  pyq_cls = getattr(pyq, block.name)
169
268
  if isinstance(block, ParametricBlock):
170
269
  if isinstance(block, U):
171
- op = pyq_cls(qubit_support[0], *config.get_param_name(block))
270
+ op = pyq_cls(
271
+ qubit_support[0],
272
+ *config.get_param_name(block),
273
+ noise=noise,
274
+ )
172
275
  else:
173
- op = pyq_cls(qubit_support[0], extract_parameter(block, config))
276
+ param = extract_parameter(block, config)
277
+ op = pyq_cls(qubit_support[0], param, noise=noise)
174
278
  else:
175
- op = pyq_cls(qubit_support[0])
279
+ op = pyq_cls(qubit_support[0], noise=noise) # type: ignore [attr-defined]
176
280
  return [op]
177
281
  elif isinstance(block, tuple(two_qubit_gateset)):
178
282
  pyq_cls = getattr(pyq, block.name)
179
283
  if isinstance(block, ParametricBlock):
180
- op = pyq_cls(qubit_support[0], qubit_support[1], extract_parameter(block, config))
284
+ op = pyq_cls(
285
+ qubit_support[0],
286
+ qubit_support[1],
287
+ extract_parameter(block, config),
288
+ noise=noise,
289
+ )
181
290
  else:
182
- op = pyq_cls(qubit_support[0], qubit_support[1])
291
+ op = pyq_cls(
292
+ qubit_support[0], qubit_support[1], noise=noise # type: ignore [attr-defined]
293
+ )
183
294
  return [op]
184
295
  elif isinstance(block, tuple(three_qubit_gateset) + tuple(multi_qubit_gateset)):
185
296
  block_name = block.name[1:] if block.name.startswith("M") else block.name
186
297
  pyq_cls = getattr(pyq, block_name)
187
298
  if isinstance(block, ParametricBlock):
188
- op = pyq_cls(qubit_support[:-1], qubit_support[-1], extract_parameter(block, config))
299
+ op = pyq_cls(
300
+ qubit_support[:-1],
301
+ qubit_support[-1],
302
+ extract_parameter(block, config),
303
+ noise=noise,
304
+ )
189
305
  else:
190
306
  if "CSWAP" in block_name:
191
- op = pyq_cls(qubit_support[:-2], qubit_support[-2:])
307
+ op = pyq_cls(
308
+ qubit_support[:-2], qubit_support[-2:], noise=noise # type: ignore [attr-defined]
309
+ )
192
310
  else:
193
- op = pyq_cls(qubit_support[:-1], qubit_support[-1])
311
+ op = pyq_cls(
312
+ qubit_support[:-1], qubit_support[-1], noise=noise # type: ignore [attr-defined]
313
+ )
194
314
  return [op]
195
315
  else:
196
316
  raise NotImplementedError(
@@ -200,182 +320,13 @@ def convert_block(
200
320
  )
201
321
 
202
322
 
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
236
-
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
- )
277
-
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
- )
297
-
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
-
366
- return result
367
-
368
- @property
369
- def device(self) -> torch_device:
370
- return self._device
371
-
372
- @property
373
- def dtype(self) -> torch_dtype:
374
- return self._dtype
375
-
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
323
+ def convert_digital_noise(noise: NoiseHandler) -> pyq.noise.NoiseProtocol:
324
+ digital_part = noise.filter(NoiseProtocol.DIGITAL)
325
+ if digital_part is None:
326
+ return None
327
+ return pyq.noise.NoiseProtocol(
328
+ [
329
+ pyq.noise.NoiseProtocol(proto, option.get("error_probability"))
330
+ for proto, option in zip(digital_part.protocol, digital_part.options)
331
+ ]
332
+ )
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
qadence/blocks/matrix.py CHANGED
@@ -8,6 +8,7 @@ import torch
8
8
  from torch.linalg import eigvals
9
9
 
10
10
  from qadence.blocks import PrimitiveBlock
11
+ from qadence.noise import NoiseHandler
11
12
 
12
13
  logger = getLogger(__name__)
13
14
 
@@ -64,6 +65,7 @@ class MatrixBlock(PrimitiveBlock):
64
65
  self,
65
66
  matrix: torch.Tensor | np.ndarray,
66
67
  qubit_support: tuple[int, ...],
68
+ noise: NoiseHandler | None = None,
67
69
  check_unitary: bool = True,
68
70
  check_hermitian: bool = False,
69
71
  ) -> None:
@@ -82,7 +84,7 @@ class MatrixBlock(PrimitiveBlock):
82
84
  if not self.is_unitary(matrix):
83
85
  logger.warning("Provided matrix is not unitary.")
84
86
  self.matrix = matrix.clone()
85
- super().__init__(qubit_support)
87
+ super().__init__(qubit_support, noise)
86
88
 
87
89
  @cached_property
88
90
  def eigenvalues_generator(self) -> torch.Tensor: