qadence 1.1.0__py3-none-any.whl → 1.2.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 (46) hide show
  1. qadence/analog/__init__.py +4 -2
  2. qadence/analog/addressing.py +167 -0
  3. qadence/analog/constants.py +59 -0
  4. qadence/analog/device.py +82 -0
  5. qadence/analog/hamiltonian_terms.py +101 -0
  6. qadence/analog/parse_analog.py +120 -0
  7. qadence/backend.py +27 -1
  8. qadence/backends/braket/backend.py +1 -1
  9. qadence/backends/pulser/__init__.py +0 -1
  10. qadence/backends/pulser/backend.py +30 -15
  11. qadence/backends/pulser/config.py +19 -10
  12. qadence/backends/pulser/devices.py +57 -63
  13. qadence/backends/pulser/pulses.py +70 -12
  14. qadence/backends/pyqtorch/backend.py +2 -3
  15. qadence/backends/pyqtorch/config.py +18 -12
  16. qadence/backends/pyqtorch/convert_ops.py +12 -4
  17. qadence/backends/pytorch_wrapper.py +2 -1
  18. qadence/backends/utils.py +1 -10
  19. qadence/blocks/abstract.py +5 -1
  20. qadence/blocks/analog.py +18 -9
  21. qadence/blocks/block_to_tensor.py +11 -0
  22. qadence/blocks/primitive.py +81 -9
  23. qadence/constructors/__init__.py +4 -0
  24. qadence/constructors/feature_maps.py +84 -60
  25. qadence/constructors/hamiltonians.py +27 -98
  26. qadence/constructors/rydberg_feature_maps.py +113 -0
  27. qadence/divergences.py +12 -0
  28. qadence/draw/utils.py +1 -1
  29. qadence/extensions.py +1 -6
  30. qadence/finitediff.py +47 -0
  31. qadence/mitigations/readout.py +92 -25
  32. qadence/models/qnn.py +88 -23
  33. qadence/operations.py +55 -70
  34. qadence/parameters.py +10 -2
  35. qadence/register.py +91 -43
  36. qadence/transpile/__init__.py +1 -0
  37. qadence/transpile/apply_fn.py +40 -0
  38. qadence/transpile/block.py +15 -7
  39. qadence/types.py +19 -1
  40. qadence/utils.py +35 -0
  41. {qadence-1.1.0.dist-info → qadence-1.2.0.dist-info}/METADATA +2 -2
  42. {qadence-1.1.0.dist-info → qadence-1.2.0.dist-info}/RECORD +44 -38
  43. {qadence-1.1.0.dist-info → qadence-1.2.0.dist-info}/WHEEL +1 -1
  44. qadence/analog/interaction.py +0 -198
  45. qadence/analog/utils.py +0 -132
  46. {qadence-1.1.0.dist-info → qadence-1.2.0.dist-info}/licenses/LICENSE +0 -0
qadence/blocks/analog.py CHANGED
@@ -59,8 +59,8 @@ class AnalogBlock(AbstractBlock):
59
59
  @property
60
60
  def eigenvalues_generator(self) -> torch.Tensor:
61
61
  msg = (
62
- "Eigenvalues of analog blocks can be computed via "
63
- "`add_interaction(register, block).eigenvalues`"
62
+ "Eigenvalues of for generator of analog blocks can be computed via "
63
+ "`add_background_hamiltonian(block, register).eigenvalues_generator`. "
64
64
  )
65
65
  raise NotImplementedError(msg)
66
66
 
@@ -68,7 +68,7 @@ class AnalogBlock(AbstractBlock):
68
68
  def eigenvalues(self) -> torch.Tensor:
69
69
  msg = (
70
70
  "Eigenvalues of analog blocks can be computed via "
71
- "`add_interaction(register, block).eigenvalues`"
71
+ "`add_background_hamiltonian(block, register).eigenvalues`. "
72
72
  )
73
73
  raise NotImplementedError(msg)
74
74
 
@@ -83,11 +83,19 @@ class AnalogBlock(AbstractBlock):
83
83
  return s
84
84
 
85
85
  def compute_eigenvalues_generator(
86
- self, register: Register, block: AbstractBlock
86
+ self,
87
+ block: AbstractBlock,
88
+ register: Register,
87
89
  ) -> torch.Tensor:
88
- from qadence import add_interaction
90
+ # FIXME: Revisit analog blocks eigenvalues
91
+ from qadence.analog import add_background_hamiltonian
92
+
93
+ return add_background_hamiltonian(block, register).eigenvalues_generator # type: ignore [union-attr]
89
94
 
90
- return add_interaction(register, block).eigenvalues_generator
95
+ def dagger(self) -> AbstractBlock:
96
+ raise NotImplementedError(
97
+ f"Hermitian adjoint of block type {type(self)} is not implemented yet."
98
+ )
91
99
 
92
100
 
93
101
  @dataclass(eq=False, repr=False)
@@ -108,8 +116,6 @@ class WaitBlock(AnalogBlock):
108
116
  with `nᵢ = (1-Zᵢ)/2`.
109
117
 
110
118
  To construct this block, use the [`wait`][qadence.operations.wait] function.
111
-
112
- Can be used with `add_interaction`.
113
119
  """
114
120
 
115
121
  _eigenvalues_generator: torch.Tensor | None = None
@@ -117,6 +123,8 @@ class WaitBlock(AnalogBlock):
117
123
  parameters: ParamMap = ParamMap(duration=1000.0) # ns
118
124
  qubit_support: QubitSupport = QubitSupport("global")
119
125
 
126
+ add_pattern: bool = True
127
+
120
128
  @property
121
129
  def eigenvalues_generator(self) -> torch.Tensor | None:
122
130
  return self._eigenvalues_generator
@@ -145,7 +153,6 @@ class ConstantAnalogRotation(AnalogBlock):
145
153
  [`AnalogRY`][qadence.operations.AnalogRY],
146
154
  [`AnalogRZ`][qadence.operations.AnalogRZ]
147
155
 
148
- Can be used with `add_interaction`.
149
156
  WARNING: do not use `ConstantAnalogRotation` with `alpha` as differentiable parameter - use
150
157
  the convenience wrappers mentioned above.
151
158
  """
@@ -161,6 +168,8 @@ class ConstantAnalogRotation(AnalogBlock):
161
168
  )
162
169
  qubit_support: QubitSupport = QubitSupport("global")
163
170
 
171
+ add_pattern: bool = True
172
+
164
173
  @property
165
174
  def _block_title(self) -> str:
166
175
  a = self.parameters.alpha
@@ -15,8 +15,11 @@ from qadence.blocks import (
15
15
  PrimitiveBlock,
16
16
  ScaleBlock,
17
17
  )
18
+ from qadence.blocks.primitive import ProjectorBlock
18
19
  from qadence.blocks.utils import chain, kron, uuid_to_expression
19
20
  from qadence.parameters import evaluate, stringify
21
+
22
+ # from qadence.states import product_state
20
23
  from qadence.types import Endianness, TensorType, TNumber
21
24
 
22
25
  J = torch.tensor(1j)
@@ -463,6 +466,14 @@ def _block_to_tensor_embedded(
463
466
  # add missing identities on unused qubits
464
467
  mat = _fill_identities(block_mat, block.qubit_support, qubit_support, endianness=endianness)
465
468
 
469
+ elif isinstance(block, ProjectorBlock):
470
+ from qadence.states import product_state
471
+
472
+ bra = product_state(block.bra)
473
+ ket = product_state(block.ket)
474
+
475
+ mat = torch.kron(ket, bra.T)
476
+
466
477
  else:
467
478
  raise TypeError(f"Conversion for block type {type(block)} not supported.")
468
479
 
@@ -1,6 +1,7 @@
1
1
  from __future__ import annotations
2
2
 
3
3
  from abc import abstractmethod
4
+ from copy import deepcopy
4
5
  from typing import Any, Iterable, Tuple
5
6
 
6
7
  import sympy
@@ -13,6 +14,7 @@ from qadence.blocks.abstract import AbstractBlock
13
14
  from qadence.parameters import (
14
15
  Parameter,
15
16
  ParamMap,
17
+ dagger_expression,
16
18
  evaluate,
17
19
  extract_original_param_entry,
18
20
  stringify,
@@ -101,6 +103,9 @@ class PrimitiveBlock(AbstractBlock):
101
103
  def n_supports(self) -> int:
102
104
  return len(self.qubit_support)
103
105
 
106
+ def dagger(self) -> PrimitiveBlock:
107
+ return self
108
+
104
109
 
105
110
  class ParametricBlock(PrimitiveBlock):
106
111
  """Parameterized primitive blocks."""
@@ -200,11 +205,10 @@ class ParametricBlock(PrimitiveBlock):
200
205
  target = d["qubit_support"][0]
201
206
  return cls(target, params) # type: ignore[call-arg]
202
207
 
203
- def dagger(self) -> ParametricBlock: # type: ignore[override]
208
+ def dagger(self) -> ParametricBlock:
204
209
  exprs = self.parameters.expressions()
205
- args = tuple(-extract_original_param_entry(param) for param in exprs)
206
- args = args if -1 in self.qubit_support else (*self.qubit_support, *args)
207
- return self.__class__(*args) # type: ignore[arg-type]
210
+ params = tuple(-extract_original_param_entry(param) for param in exprs)
211
+ return type(self)(*self.qubit_support, *params) # type: ignore[arg-type]
208
212
 
209
213
 
210
214
  class ScaleBlock(ParametricBlock):
@@ -304,9 +308,8 @@ class ScaleBlock(ParametricBlock):
304
308
  )
305
309
 
306
310
  def dagger(self) -> ScaleBlock:
307
- return self.__class__(
308
- self.block, Parameter(-extract_original_param_entry(self.parameters.parameter))
309
- )
311
+ p = list(self.parameters.expressions())[0]
312
+ return self.__class__(self.block.dagger(), dagger_expression(p))
310
313
 
311
314
  def _to_dict(self) -> dict:
312
315
  return {
@@ -350,13 +353,25 @@ class ControlBlock(PrimitiveBlock):
350
353
  """The abstract ControlBlock."""
351
354
 
352
355
  name = "Control"
356
+ control: tuple[int, ...]
357
+ target: tuple[int, ...]
353
358
 
354
359
  def __init__(self, control: tuple[int, ...], target_block: PrimitiveBlock) -> None:
360
+ self.control = control
355
361
  self.blocks = (target_block,)
362
+ self.target = target_block.qubit_support
356
363
 
357
364
  # using tuple expansion because some control operations could
358
365
  # have multiple targets, e.g. CSWAP
359
- super().__init__((*control, *target_block.qubit_support)) # target_block.qubit_support[0]))
366
+ super().__init__((*control, *self.target)) # target_block.qubit_support[0]))
367
+
368
+ @property
369
+ def n_controls(self) -> int:
370
+ return len(self.control)
371
+
372
+ @property
373
+ def n_targets(self) -> int:
374
+ return len(self.target)
360
375
 
361
376
  @property
362
377
  def _block_title(self) -> str:
@@ -391,16 +406,28 @@ class ControlBlock(PrimitiveBlock):
391
406
  target = d["qubit_support"][1]
392
407
  return cls(control, target)
393
408
 
409
+ def dagger(self) -> ControlBlock:
410
+ blk = deepcopy(self)
411
+ blk.blocks = (self.blocks[0].dagger(),)
412
+ return blk
413
+
394
414
 
395
415
  class ParametricControlBlock(ParametricBlock):
396
416
  """The abstract parametrized ControlBlock."""
397
417
 
398
418
  name = "ParameterizedControl"
419
+ control: tuple[int, ...] = ()
420
+ blocks: tuple[ParametricBlock, ...]
399
421
 
400
422
  def __init__(self, control: tuple[int, ...], target_block: ParametricBlock) -> None:
401
423
  self.blocks = (target_block,)
424
+ self.control = control
402
425
  self.parameters = target_block.parameters
403
- super().__init__((*control, target_block.qubit_support[0]))
426
+ super().__init__((*control, *target_block.qubit_support))
427
+
428
+ @property
429
+ def n_controls(self) -> int:
430
+ return len(self.control)
404
431
 
405
432
  @property
406
433
  def eigenvalues_generator(self) -> torch.Tensor:
@@ -454,3 +481,48 @@ class ParametricControlBlock(ParametricBlock):
454
481
 
455
482
  s += rf" \[params: {params_str}]"
456
483
  return s if self.tag is None else (s + rf" \[tag: {self.tag}]")
484
+
485
+ def dagger(self) -> ParametricControlBlock:
486
+ blk = deepcopy(self)
487
+ blocks = tuple(b.dagger() for b in blk.blocks)
488
+ blk.blocks = blocks
489
+ blk.parameters = blocks[0].parameters
490
+ return blk
491
+
492
+
493
+ class ProjectorBlock(PrimitiveBlock):
494
+ """The abstract ProjectorBlock."""
495
+
496
+ name = "ProjectorBlock"
497
+
498
+ def __init__(
499
+ self,
500
+ ket: str,
501
+ bra: str,
502
+ qubit_support: int | tuple[int, ...],
503
+ ) -> None:
504
+ """
505
+ Arguments:
506
+
507
+ ket (str): The ket given as a bitstring.
508
+ bra (str): The bra given as a bitstring.
509
+ qubit_support (int | tuple[int]): The qubit_support of the block.
510
+ """
511
+ if isinstance(qubit_support, int):
512
+ qubit_support = (qubit_support,)
513
+ if len(bra) != len(ket):
514
+ raise ValueError(
515
+ "Bra and ket must be bitstrings of same length in the 'Projector' definition."
516
+ )
517
+ elif len(bra) != len(qubit_support):
518
+ raise ValueError("Bra or ket must be of same length as the 'qubit_support'")
519
+ for wf in [bra, ket]:
520
+ if not all(int(item) == 0 or int(item) == 1 for item in wf):
521
+ raise ValueError(
522
+ "All qubits must be either in the '0' or '1' state"
523
+ " in the 'ProjectorBlock' definition."
524
+ )
525
+
526
+ self.ket = ket
527
+ self.bra = bra
528
+ super().__init__(qubit_support)
@@ -23,6 +23,7 @@ from .hamiltonians import (
23
23
  )
24
24
 
25
25
  from .rydberg_hea import rydberg_hea, rydberg_hea_layer
26
+ from .rydberg_feature_maps import rydberg_feature_map, analog_feature_map, rydberg_tower_feature_map
26
27
 
27
28
  from .qft import qft
28
29
 
@@ -45,4 +46,7 @@ __all__ = [
45
46
  "daqc_transform",
46
47
  "rydberg_hea",
47
48
  "rydberg_hea_layer",
49
+ "rydberg_feature_map",
50
+ "analog_feature_map",
51
+ "rydberg_tower_feature_map",
48
52
  ]
@@ -6,7 +6,7 @@ from collections.abc import Callable
6
6
  from math import isclose, pi
7
7
  from typing import Union
8
8
 
9
- from sympy import Function, acos
9
+ from sympy import Basic, Function, acos
10
10
 
11
11
  from qadence.blocks import AbstractBlock, KronBlock, chain, kron, tag
12
12
  from qadence.logger import get_logger
@@ -36,65 +36,12 @@ RS_FUNC_DICT = {
36
36
  }
37
37
 
38
38
 
39
- def feature_map(
40
- n_qubits: int,
41
- support: tuple[int, ...] | None = None,
39
+ def fm_parameter(
40
+ fm_type: BasisSet | type[Function] | str,
42
41
  param: Parameter | str = "phi",
43
- op: RotationTypes = RX,
44
- fm_type: BasisSet | type[Function] | str = BasisSet.FOURIER,
45
- reupload_scaling: ReuploadScaling | Callable | str = ReuploadScaling.CONSTANT,
46
42
  feature_range: tuple[float, float] | None = None,
47
43
  target_range: tuple[float, float] | None = None,
48
- multiplier: Parameter | TParameter | None = None,
49
- ) -> KronBlock:
50
- """Construct a feature map of a given type.
51
-
52
- Arguments:
53
- n_qubits: Number of qubits the feature map covers. Results in `support=range(n_qubits)`.
54
- support: Puts one feature-encoding rotation gate on every qubit in `support`. n_qubits in
55
- this case specifies the total overall qubits of the circuit, which may be wider than the
56
- support itself, but not narrower.
57
- param: Parameter of the feature map; you can pass a string or Parameter;
58
- it will be set as non-trainable (FeatureParameter) regardless.
59
- op: Rotation operation of the feature map; choose from RX, RY, RZ or PHASE.
60
- fm_type: Basis set for data encoding; choose from `BasisSet.FOURIER` for Fourier
61
- encoding, or `BasisSet.CHEBYSHEV` for Chebyshev polynomials of the first kind.
62
- reupload_scaling: how the feature map scales the data that is re-uploaded for each qubit.
63
- choose from `ReuploadScaling` enumeration or provide your own function with a single
64
- int as input and int or float as output.
65
- feature_range: range of data that the input data is assumed to come from.
66
- target_range: range of data the data encoder assumes as the natural range. For example,
67
- in Chebyshev polynomials it is (-1, 1), while for Fourier it may be chosen as (0, 2*pi).
68
- multiplier: overall multiplier; this is useful for reuploading the feature map serially with
69
- different scalings; can be a number or parameter/expression.
70
-
71
- Example:
72
- ```python exec="on" source="material-block" result="json"
73
- from qadence import feature_map, BasisSet, ReuploadScaling
74
-
75
- fm = feature_map(3, fm_type=BasisSet.FOURIER)
76
- print(f"{fm = }")
77
-
78
- fm = feature_map(3, fm_type=BasisSet.CHEBYSHEV)
79
- print(f"{fm = }")
80
-
81
- fm = feature_map(3, fm_type=BasisSet.FOURIER, reupload_scaling = ReuploadScaling.TOWER)
82
- print(f"{fm = }")
83
- ```
84
- """
85
-
86
- # Process input
87
- if support is None:
88
- support = tuple(range(n_qubits))
89
- elif len(support) != n_qubits:
90
- raise ValueError("Wrong qubit support supplied")
91
-
92
- if op not in ROTATIONS:
93
- raise ValueError(
94
- f"Operation {op} not supported. "
95
- f"Please provide one from {[rot.__name__ for rot in ROTATIONS]}."
96
- )
97
-
44
+ ) -> Parameter | Basic:
98
45
  # Backwards compatibility
99
46
  if fm_type in ("fourier", "chebyshev", "tower"):
100
47
  logger.warning(
@@ -108,7 +55,6 @@ def feature_map(
108
55
  fm_type = BasisSet.CHEBYSHEV
109
56
  elif fm_type == "tower":
110
57
  fm_type = BasisSet.CHEBYSHEV
111
- reupload_scaling = ReuploadScaling.TOWER
112
58
 
113
59
  if isinstance(param, Parameter):
114
60
  fparam = param
@@ -144,8 +90,12 @@ def feature_map(
144
90
  "the given feature parameter with."
145
91
  )
146
92
 
147
- basis_tag = fm_type.value if isinstance(fm_type, BasisSet) else str(fm_type)
93
+ return transformed_feature
94
+
148
95
 
96
+ def fm_reupload_scaling_fn(
97
+ reupload_scaling: ReuploadScaling | Callable | str = ReuploadScaling.CONSTANT,
98
+ ) -> tuple[Callable, str]:
149
99
  # Set reupload scaling function
150
100
  if callable(reupload_scaling):
151
101
  rs_func = reupload_scaling
@@ -163,8 +113,82 @@ def feature_map(
163
113
  else:
164
114
  rs_tag = reupload_scaling
165
115
 
116
+ return rs_func, rs_tag
117
+
118
+
119
+ def feature_map(
120
+ n_qubits: int,
121
+ support: tuple[int, ...] | None = None,
122
+ param: Parameter | str = "phi",
123
+ op: RotationTypes = RX,
124
+ fm_type: BasisSet | type[Function] | str = BasisSet.FOURIER,
125
+ reupload_scaling: ReuploadScaling | Callable | str = ReuploadScaling.CONSTANT,
126
+ feature_range: tuple[float, float] | None = None,
127
+ target_range: tuple[float, float] | None = None,
128
+ multiplier: Parameter | TParameter | None = None,
129
+ ) -> KronBlock:
130
+ """Construct a feature map of a given type.
131
+
132
+ Arguments:
133
+ n_qubits: Number of qubits the feature map covers. Results in `support=range(n_qubits)`.
134
+ support: Puts one feature-encoding rotation gate on every qubit in `support`. n_qubits in
135
+ this case specifies the total overall qubits of the circuit, which may be wider than the
136
+ support itself, but not narrower.
137
+ param: Parameter of the feature map; you can pass a string or Parameter;
138
+ it will be set as non-trainable (FeatureParameter) regardless.
139
+ op: Rotation operation of the feature map; choose from RX, RY, RZ or PHASE.
140
+ fm_type: Basis set for data encoding; choose from `BasisSet.FOURIER` for Fourier
141
+ encoding, or `BasisSet.CHEBYSHEV` for Chebyshev polynomials of the first kind.
142
+ reupload_scaling: how the feature map scales the data that is re-uploaded for each qubit.
143
+ choose from `ReuploadScaling` enumeration or provide your own function with a single
144
+ int as input and int or float as output.
145
+ feature_range: range of data that the input data is assumed to come from.
146
+ target_range: range of data the data encoder assumes as the natural range. For example,
147
+ in Chebyshev polynomials it is (-1, 1), while for Fourier it may be chosen as (0, 2*pi).
148
+ multiplier: overall multiplier; this is useful for reuploading the feature map serially with
149
+ different scalings; can be a number or parameter/expression.
150
+
151
+ Example:
152
+ ```python exec="on" source="material-block" result="json"
153
+ from qadence import feature_map, BasisSet, ReuploadScaling
154
+
155
+ fm = feature_map(3, fm_type=BasisSet.FOURIER)
156
+ print(f"{fm = }")
157
+
158
+ fm = feature_map(3, fm_type=BasisSet.CHEBYSHEV)
159
+ print(f"{fm = }")
160
+
161
+ fm = feature_map(3, fm_type=BasisSet.FOURIER, reupload_scaling = ReuploadScaling.TOWER)
162
+ print(f"{fm = }")
163
+ ```
164
+ """
165
+
166
+ # Process input
167
+ if support is None:
168
+ support = tuple(range(n_qubits))
169
+ elif len(support) != n_qubits:
170
+ raise ValueError("Wrong qubit support supplied")
171
+
172
+ if op not in ROTATIONS:
173
+ raise ValueError(
174
+ f"Operation {op} not supported. "
175
+ f"Please provide one from {[rot.__name__ for rot in ROTATIONS]}."
176
+ )
177
+
178
+ transformed_feature = fm_parameter(
179
+ fm_type, param, feature_range=feature_range, target_range=target_range
180
+ )
181
+
182
+ # Backwards compatibility
183
+ if fm_type == "tower":
184
+ logger.warning("Forcing reupload scaling strategy to TOWER")
185
+ reupload_scaling = ReuploadScaling.TOWER
186
+
187
+ basis_tag = fm_type.value if isinstance(fm_type, BasisSet) else str(fm_type)
188
+ rs_func, rs_tag = fm_reupload_scaling_fn(reupload_scaling)
189
+
166
190
  # Set overall multiplier
167
- multiplier = 1 if multiplier is None else multiplier
191
+ multiplier = 1 if multiplier is None else Parameter(multiplier)
168
192
 
169
193
  # Build feature map
170
194
  op_list = []
@@ -1,7 +1,6 @@
1
1
  from __future__ import annotations
2
2
 
3
- import warnings
4
- from typing import List, Tuple, Type, Union
3
+ from typing import List, Type, Union
5
4
 
6
5
  import numpy as np
7
6
  from torch import Tensor, double, ones, rand
@@ -57,8 +56,7 @@ def hamiltonian_factory(
57
56
  interaction_strength: TArray | str | None = None,
58
57
  detuning_strength: TArray | str | None = None,
59
58
  random_strength: bool = False,
60
- force_update: bool = False,
61
- use_complete_graph: bool = False,
59
+ use_all_node_pairs: bool = False,
62
60
  ) -> AbstractBlock:
63
61
  """
64
62
  General Hamiltonian creation function.
@@ -80,9 +78,8 @@ def hamiltonian_factory(
80
78
  Alternatively, some string "x" can be passed, which will create a parameterized
81
79
  detuning for each qubit, each labelled as `"x_i"`.
82
80
  random_strength: set random interaction and detuning strengths between -1 and 1.
83
- force_update: force override register detuning and interaction strengths.
84
- use_complete_graph: computes an interaction for every edge in a complete graph,
85
- independent of the edges in the register. Useful for defining Hamiltonians
81
+ use_all_node_pairs: computes an interaction term for every pair of nodes in the graph,
82
+ independent of the edge topology in the register. Useful for defining Hamiltonians
86
83
  where the interaction strength decays with the distance.
87
84
 
88
85
  Examples:
@@ -121,12 +118,9 @@ def hamiltonian_factory(
121
118
  register = Register(register) if isinstance(register, int) else register
122
119
 
123
120
  # Get interaction function
124
- try:
125
- int_fn = INTERACTION_DICT[interaction] # type: ignore [index]
126
- except (KeyError, ValueError) as error:
127
- if interaction is None:
128
- pass
129
- else:
121
+ if interaction is not None:
122
+ int_fn = INTERACTION_DICT.get(interaction, None)
123
+ if int_fn is None:
130
124
  raise KeyError(f"Interaction {interaction} not supported.")
131
125
 
132
126
  # Check single-qubit detuning
@@ -134,37 +128,27 @@ def hamiltonian_factory(
134
128
  raise TypeError(f"Detuning of type {type(detuning)} not supported.")
135
129
 
136
130
  # Pre-process detuning and interaction strengths and update register
137
- has_detuning_strength, detuning_strength = _preprocess_strengths(
138
- register, detuning_strength, "nodes", force_update, random_strength
131
+ detuning_strength_array = _preprocess_strengths(
132
+ register, detuning_strength, "nodes", random_strength
139
133
  )
140
134
 
141
- edge_str = "all_edges" if use_complete_graph else "edges"
142
- has_interaction_strength, interaction_strength = _preprocess_strengths(
143
- register, interaction_strength, edge_str, force_update, random_strength
135
+ edge_str = "all_node_pairs" if use_all_node_pairs else "edges"
136
+ interaction_strength_array = _preprocess_strengths(
137
+ register, interaction_strength, edge_str, random_strength
144
138
  )
145
139
 
146
- if (not has_detuning_strength) or force_update:
147
- register = _update_detuning_strength(register, detuning_strength)
148
-
149
- if (not has_interaction_strength) or force_update:
150
- register = _update_interaction_strength(register, interaction_strength, use_complete_graph)
151
-
152
140
  # Create single-qubit detunings:
153
141
  single_qubit_terms: List[AbstractBlock] = []
154
142
  if detuning is not None:
155
- for node in register.nodes:
156
- block_sq = detuning(node) # type: ignore [operator]
157
- strength_sq = register.nodes[node]["strength"]
158
- single_qubit_terms.append(strength_sq * block_sq)
143
+ for strength, node in zip(detuning_strength_array, register.nodes):
144
+ single_qubit_terms.append(strength * detuning(node))
159
145
 
160
146
  # Create two-qubit interactions:
161
147
  two_qubit_terms: List[AbstractBlock] = []
162
- edge_data = register.all_edges if use_complete_graph else register.edges
163
- if interaction is not None:
164
- for edge in edge_data:
165
- block_tq = int_fn(*edge) # type: ignore [operator]
166
- strength_tq = edge_data[edge]["strength"]
167
- two_qubit_terms.append(strength_tq * block_tq)
148
+ edge_data = register.all_node_pairs if use_all_node_pairs else register.edges
149
+ if interaction is not None and int_fn is not None:
150
+ for strength, edge in zip(interaction_strength_array, edge_data):
151
+ two_qubit_terms.append(strength * int_fn(*edge))
168
152
 
169
153
  return add(*single_qubit_terms, *two_qubit_terms)
170
154
 
@@ -173,22 +157,13 @@ def _preprocess_strengths(
173
157
  register: Register,
174
158
  strength: TArray | str | None,
175
159
  nodes_or_edges: str,
176
- force_update: bool,
177
160
  random_strength: bool,
178
- ) -> Tuple[bool, Union[TArray | str]]:
161
+ ) -> Tensor | list:
179
162
  data = getattr(register, nodes_or_edges)
180
163
 
181
164
  # Useful for error messages:
182
165
  strength_target = "detuning" if nodes_or_edges == "nodes" else "interaction"
183
166
 
184
- # First we check if strength values already exist in the register
185
- has_strength = any(["strength" in data[i] for i in data])
186
- if has_strength and not force_update:
187
- if strength is not None:
188
- logger.warning(
189
- "Register already includes " + strength_target + " strengths. "
190
- "Skipping update. Use `force_update = True` to override them."
191
- )
192
167
  # Next we process the strength given in the input arguments
193
168
  if strength is None:
194
169
  if random_strength:
@@ -202,8 +177,11 @@ def _preprocess_strengths(
202
177
  message = "Array of " + strength_target + " strengths has incorrect size."
203
178
  raise ValueError(message)
204
179
  elif isinstance(strength, str):
205
- # Any string will be used as a prefix to variational parameters
206
- pass
180
+ prefix = strength
181
+ if nodes_or_edges == "nodes":
182
+ strength = [prefix + f"_{node}" for node in data]
183
+ if nodes_or_edges in ["edges", "all_node_pairs"]:
184
+ strength = [prefix + f"_{edge[0]}{edge[1]}" for edge in data]
207
185
  else:
208
186
  # If not of the accepted types ARRAYS or str, we error out
209
187
  raise TypeError(
@@ -212,69 +190,27 @@ def _preprocess_strengths(
212
190
  "parameterized " + strength_target + "s."
213
191
  )
214
192
 
215
- return has_strength, strength
216
-
217
-
218
- def _update_detuning_strength(register: Register, detuning_strength: TArray | str) -> Register:
219
- for node in register.nodes:
220
- if isinstance(detuning_strength, str):
221
- register.nodes[node]["strength"] = detuning_strength + f"_{node}"
222
- elif isinstance(detuning_strength, ARRAYS):
223
- register.nodes[node]["strength"] = detuning_strength[node]
224
- return register
225
-
226
-
227
- def _update_interaction_strength(
228
- register: Register, interaction_strength: TArray | str, use_complete_graph: bool
229
- ) -> Register:
230
- edge_data = register.all_edges if use_complete_graph else register.edges
231
- for idx, edge in enumerate(edge_data):
232
- if isinstance(interaction_strength, str):
233
- edge_data[edge]["strength"] = interaction_strength + f"_{edge[0]}{edge[1]}"
234
- elif isinstance(interaction_strength, ARRAYS):
235
- edge_data[edge]["strength"] = interaction_strength[idx]
236
- return register
237
-
193
+ return strength
238
194
 
239
- # FIXME: Previous hamiltonian / observable functions, now refactored, to be deprecated:
240
195
 
241
- DEPRECATION_MESSAGE = "This function will be removed in the future. "
196
+ def total_magnetization(n_qubits: int, z_terms: np.ndarray | list | None = None) -> AbstractBlock:
197
+ return hamiltonian_factory(n_qubits, detuning=Z, detuning_strength=z_terms)
242
198
 
243
199
 
244
200
  def single_z(qubit: int = 0, z_coefficient: float = 1.0) -> AbstractBlock:
245
- message = DEPRECATION_MESSAGE + "Please use `z_coefficient * Z(qubit)` directly."
246
- warnings.warn(message, FutureWarning)
247
201
  return Z(qubit) * z_coefficient
248
202
 
249
203
 
250
- def total_magnetization(n_qubits: int, z_terms: np.ndarray | list | None = None) -> AbstractBlock:
251
- message = (
252
- DEPRECATION_MESSAGE
253
- + "Please use `hamiltonian_factory(n_qubits, detuning=Z, node_coeff=z_terms)`."
254
- )
255
- warnings.warn(message, FutureWarning)
256
- return hamiltonian_factory(n_qubits, detuning=Z, detuning_strength=z_terms)
257
-
258
-
259
204
  def zz_hamiltonian(
260
205
  n_qubits: int,
261
206
  z_terms: np.ndarray | None = None,
262
207
  zz_terms: np.ndarray | None = None,
263
208
  ) -> AbstractBlock:
264
- message = (
265
- DEPRECATION_MESSAGE
266
- + """
267
- Please use `hamiltonian_factory(n_qubits, Interaction.ZZ, Z, interaction_strength, z_terms)`. \
268
- Note that the argument `zz_terms` in this function is a 2D array of size `(n_qubits, n_qubits)`, \
269
- while `interaction_strength` is expected as a 1D array of size `0.5 * n_qubits * (n_qubits - 1)`."""
270
- )
271
- warnings.warn(message, FutureWarning)
272
209
  if zz_terms is not None:
273
210
  register = Register(n_qubits)
274
211
  interaction_strength = [zz_terms[edge[0], edge[1]] for edge in register.edges]
275
212
  else:
276
213
  interaction_strength = None
277
-
278
214
  return hamiltonian_factory(n_qubits, Interaction.ZZ, Z, interaction_strength, z_terms)
279
215
 
280
216
 
@@ -284,13 +220,6 @@ def ising_hamiltonian(
284
220
  z_terms: np.ndarray | None = None,
285
221
  zz_terms: np.ndarray | None = None,
286
222
  ) -> AbstractBlock:
287
- message = (
288
- DEPRECATION_MESSAGE
289
- + """
290
- You can build a general transverse field ising model with the `hamiltonian_factory` function. \
291
- Check the hamiltonian construction tutorial in the documentation for more information."""
292
- )
293
- warnings.warn(message, FutureWarning)
294
223
  zz_ham = zz_hamiltonian(n_qubits, z_terms=z_terms, zz_terms=zz_terms)
295
224
  x_ham = hamiltonian_factory(n_qubits, detuning=X, detuning_strength=x_terms)
296
225
  return zz_ham + x_ham