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/operations.py CHANGED
@@ -31,6 +31,7 @@ from qadence.blocks.analog import (
31
31
  WaitBlock,
32
32
  )
33
33
  from qadence.blocks.block_to_tensor import block_to_tensor
34
+ from qadence.blocks.primitive import ProjectorBlock
34
35
  from qadence.blocks.utils import (
35
36
  add, # noqa
36
37
  block_is_commuting_hamiltonian,
@@ -117,9 +118,6 @@ class X(PrimitiveBlock):
117
118
  def eigenvalues(self) -> Tensor:
118
119
  return tensor([-1, 1], dtype=cdouble)
119
120
 
120
- def dagger(self) -> X:
121
- return self
122
-
123
121
 
124
122
  class Y(PrimitiveBlock):
125
123
  """The Y gate."""
@@ -141,9 +139,6 @@ class Y(PrimitiveBlock):
141
139
  def eigenvalues(self) -> Tensor:
142
140
  return tensor([-1, 1], dtype=cdouble)
143
141
 
144
- def dagger(self) -> Y:
145
- return self
146
-
147
142
 
148
143
  class Z(PrimitiveBlock):
149
144
  """The Z gate."""
@@ -165,17 +160,36 @@ class Z(PrimitiveBlock):
165
160
  def eigenvalues(self) -> Tensor:
166
161
  return tensor([-1, 1], dtype=cdouble)
167
162
 
168
- def dagger(self) -> Z:
169
- return self
170
163
 
164
+ class Projector(ProjectorBlock):
165
+ """The projector operator."""
166
+
167
+ name = OpName.PROJ
168
+
169
+ def __init__(
170
+ self,
171
+ ket: str,
172
+ bra: str,
173
+ qubit_support: int | tuple[int, ...],
174
+ ):
175
+ super().__init__(ket=ket, bra=bra, qubit_support=qubit_support)
176
+
177
+ @property
178
+ def generator(self) -> None:
179
+ raise ValueError("Property `generator` not available for non-unitary operator.")
180
+
181
+ @property
182
+ def eigenvalues_generator(self) -> None:
183
+ raise ValueError("Property `eigenvalues_generator` not available for non-unitary operator.")
171
184
 
172
- class N(PrimitiveBlock):
185
+
186
+ class N(Projector):
173
187
  """The N = (1/2)(I-Z) operator."""
174
188
 
175
189
  name = OpName.N
176
190
 
177
- def __init__(self, target: int):
178
- super().__init__((target,))
191
+ def __init__(self, target: int, state: str = "1"):
192
+ super().__init__(ket=state, bra=state, qubit_support=(target,))
179
193
 
180
194
  @property
181
195
  def generator(self) -> None:
@@ -189,9 +203,6 @@ class N(PrimitiveBlock):
189
203
  def eigenvalues(self) -> Tensor:
190
204
  return tensor([0, 1], dtype=cdouble)
191
205
 
192
- def dagger(self) -> N:
193
- return self
194
-
195
206
 
196
207
  class S(PrimitiveBlock):
197
208
  """The S / Phase gate."""
@@ -296,9 +307,6 @@ class I(PrimitiveBlock):
296
307
  def __ascii__(self, console: Console) -> Padding:
297
308
  return Padding("──────", (1, 1, 1, 1))
298
309
 
299
- def dagger(self) -> I:
300
- return I(*self.qubit_support)
301
-
302
310
 
303
311
  TPauliBlock = Union[X, Y, Z, I, N]
304
312
 
@@ -320,9 +328,6 @@ class H(PrimitiveBlock):
320
328
  def eigenvalues(self) -> Tensor:
321
329
  return torch.tensor([-1, 1], dtype=cdouble)
322
330
 
323
- def dagger(self) -> H:
324
- return H(*self.qubit_support)
325
-
326
331
 
327
332
  class Zero(PrimitiveBlock):
328
333
  name = OpName.ZERO
@@ -363,9 +368,6 @@ class Zero(PrimitiveBlock):
363
368
  def __pow__(self, power: int) -> AbstractBlock:
364
369
  return self
365
370
 
366
- def dagger(self) -> Zero:
367
- return Zero()
368
-
369
371
 
370
372
  class RX(ParametricBlock):
371
373
  """The Rx gate."""
@@ -668,7 +670,7 @@ class CNOT(ControlBlock):
668
670
  name = OpName.CNOT
669
671
 
670
672
  def __init__(self, control: int, target: int) -> None:
671
- self.generator = kron((I(control) - Z(control)) * 0.5, X(target) - I(target))
673
+ self.generator = kron(N(control), X(target) - I(target))
672
674
  super().__init__((control,), X(target))
673
675
 
674
676
  @property
@@ -691,17 +693,12 @@ class CNOT(ControlBlock):
691
693
  tree.add(self._block_title)
692
694
  return tree
693
695
 
694
- def dagger(self) -> CNOT:
695
- return CNOT(*self.qubit_support)
696
-
697
696
 
698
697
  class MCZ(ControlBlock):
699
698
  name = OpName.MCZ
700
699
 
701
700
  def __init__(self, control: tuple[int, ...], target: int) -> None:
702
- self.generator = kron(
703
- *[(I(qubit) - Z(qubit)) * 0.5 for qubit in control], Z(target) - I(target)
704
- )
701
+ self.generator = kron(*[N(qubit) for qubit in control], Z(target) - I(target))
705
702
  super().__init__(control, Z(target))
706
703
 
707
704
  @property
@@ -724,9 +721,6 @@ class MCZ(ControlBlock):
724
721
  tree.add(self._block_title)
725
722
  return tree
726
723
 
727
- def dagger(self) -> MCZ:
728
- return MCZ(self.qubit_support[:-1], self.qubit_support[-1])
729
-
730
724
 
731
725
  class CZ(MCZ):
732
726
  """The CZ gate."""
@@ -736,9 +730,6 @@ class CZ(MCZ):
736
730
  def __init__(self, control: int, target: int) -> None:
737
731
  super().__init__((control,), target)
738
732
 
739
- def dagger(self) -> CZ:
740
- return CZ(self.qubit_support[-2], self.qubit_support[-1])
741
-
742
733
 
743
734
  class MCRX(ParametricControlBlock):
744
735
  name = OpName.MCRX
@@ -749,7 +740,7 @@ class MCRX(ParametricControlBlock):
749
740
  target: int,
750
741
  parameter: Parameter | TNumber | sympy.Expr | str,
751
742
  ) -> None:
752
- self.generator = kron(*[(I(qubit) - Z(qubit)) * 0.5 for qubit in control], X(target))
743
+ self.generator = kron(*[N(qubit) for qubit in control], X(target))
753
744
  super().__init__(control, RX(target, parameter))
754
745
 
755
746
  @classmethod
@@ -792,7 +783,7 @@ class MCRY(ParametricControlBlock):
792
783
  target: int,
793
784
  parameter: Parameter | TNumber | sympy.Expr | str,
794
785
  ) -> None:
795
- self.generator = kron(*[(I(qubit) - Z(qubit)) * 0.5 for qubit in control], Y(target))
786
+ self.generator = kron(*[N(qubit) for qubit in control], Y(target))
796
787
  super().__init__(control, RY(target, parameter))
797
788
 
798
789
  @classmethod
@@ -821,7 +812,7 @@ class CRY(MCRY):
821
812
  self,
822
813
  control: int,
823
814
  target: int,
824
- parameter: Parameter | TNumber | sympy.Expr | str,
815
+ parameter: TParameter,
825
816
  ):
826
817
  super().__init__((control,), target, parameter)
827
818
 
@@ -835,7 +826,7 @@ class MCRZ(ParametricControlBlock):
835
826
  target: int,
836
827
  parameter: Parameter | TNumber | sympy.Expr | str,
837
828
  ) -> None:
838
- self.generator = kron(*[(I(qubit) - Z(qubit)) * 0.5 for qubit in control], Z(target))
829
+ self.generator = kron(*[N(qubit) for qubit in control], Z(target))
839
830
  super().__init__(control, RZ(target, parameter))
840
831
 
841
832
  @classmethod
@@ -878,10 +869,10 @@ class CSWAP(ControlBlock):
878
869
  if isinstance(control, tuple):
879
870
  control = control[0]
880
871
 
881
- a00m = 0.5 * (Z(control) - I(control))
882
- a00p = -0.5 * (Z(control) + I(control))
883
- a11 = 0.5 * (Z(target1) - I(target1))
884
- a22 = -0.5 * (Z(target2) + I(target2))
872
+ a00m = -N(target=control)
873
+ a00p = -N(target=control, state="0")
874
+ a11 = -N(target=target1)
875
+ a22 = -N(target=target2, state="0")
885
876
  a12 = 0.5 * (chain(X(target1), Z(target1)) + X(target1))
886
877
  a21 = 0.5 * (chain(Z(target2), X(target2)) + X(target2))
887
878
  no_effect = kron(a00m, I(target1), I(target2))
@@ -906,9 +897,6 @@ class CSWAP(ControlBlock):
906
897
  def nqubits(self) -> int:
907
898
  return 3
908
899
 
909
- def dagger(self) -> CSWAP:
910
- return CSWAP(*self.qubit_support)
911
-
912
900
 
913
901
  class T(PrimitiveBlock):
914
902
  """The T gate."""
@@ -994,9 +982,6 @@ class SWAP(PrimitiveBlock):
994
982
  s = f"{self.name}({c}, {t})"
995
983
  return s if self.tag is None else (s + rf" \[tag: {self.tag}]")
996
984
 
997
- def dagger(self) -> SWAP:
998
- return SWAP(*self.qubit_support)
999
-
1000
985
 
1001
986
  class AnalogSWAP(HamEvo):
1002
987
  """
@@ -1031,9 +1016,7 @@ class MCPHASE(ParametricControlBlock):
1031
1016
  target: int,
1032
1017
  parameter: Parameter | TNumber | sympy.Expr | str,
1033
1018
  ) -> None:
1034
- self.generator = kron(
1035
- *[(I(qubit) - Z(qubit)) * 0.5 for qubit in control], Z(target) - I(target)
1036
- )
1019
+ self.generator = kron(*[N(qubit) for qubit in control], Z(target) - I(target))
1037
1020
  super().__init__(control, PHASE(target, parameter))
1038
1021
 
1039
1022
  @classmethod
@@ -1082,14 +1065,9 @@ class Toffoli(ControlBlock):
1082
1065
  name = OpName.TOFFOLI
1083
1066
 
1084
1067
  def __init__(self, control: tuple[int, ...], target: int) -> None:
1085
- self.generator = kron(
1086
- *[(I(qubit) - Z(qubit)) * 0.5 for qubit in control], X(target) - I(target)
1087
- )
1068
+ self.generator = kron(*[N(qubit) for qubit in control], X(target) - I(target))
1088
1069
  super().__init__(control, X(target))
1089
1070
 
1090
- def dagger(self) -> Toffoli:
1091
- return Toffoli(self.qubit_support[:-1], self.qubit_support[-1])
1092
-
1093
1071
  @property
1094
1072
  def n_qubits(self) -> int:
1095
1073
  return len(self.qubit_support)
@@ -1129,6 +1107,7 @@ def _cast(T: Any, val: Any) -> Any:
1129
1107
  def wait(
1130
1108
  duration: TNumber | sympy.Basic,
1131
1109
  qubit_support: str | QubitSupport | tuple = "global",
1110
+ add_pattern: bool = True,
1132
1111
  ) -> WaitBlock:
1133
1112
  """Constructs a [`WaitBlock`][qadence.blocks.analog.WaitBlock].
1134
1113
 
@@ -1142,7 +1121,7 @@ def wait(
1142
1121
  """
1143
1122
  q = _cast(QubitSupport, qubit_support)
1144
1123
  ps = ParamMap(duration=duration)
1145
- return WaitBlock(parameters=ps, qubit_support=q)
1124
+ return WaitBlock(parameters=ps, qubit_support=q, add_pattern=add_pattern)
1146
1125
 
1147
1126
 
1148
1127
  def entangle(
@@ -1160,6 +1139,7 @@ def AnalogRot(
1160
1139
  delta: float | str | Parameter = 0,
1161
1140
  phase: float | str | Parameter = 0,
1162
1141
  qubit_support: str | QubitSupport | Tuple = "global",
1142
+ add_pattern: bool = True,
1163
1143
  ) -> ConstantAnalogRotation:
1164
1144
  """General analog rotation operation.
1165
1145
 
@@ -1174,18 +1154,20 @@ def AnalogRot(
1174
1154
  ConstantAnalogRotation
1175
1155
  """
1176
1156
  q = _cast(QubitSupport, qubit_support)
1177
- if isinstance(duration, str):
1178
- duration = Parameter(duration)
1179
- alpha = duration * sympy.sqrt(omega**2 + delta**2) / 1000 # type: ignore [operator]
1180
-
1157
+ duration = Parameter(duration)
1158
+ omega = Parameter(omega)
1159
+ delta = Parameter(delta)
1160
+ phase = Parameter(phase)
1161
+ alpha = duration * sympy.sqrt(omega**2 + delta**2) / 1000
1181
1162
  ps = ParamMap(alpha=alpha, duration=duration, omega=omega, delta=delta, phase=phase)
1182
- return ConstantAnalogRotation(parameters=ps, qubit_support=q)
1163
+ return ConstantAnalogRotation(parameters=ps, qubit_support=q, add_pattern=add_pattern)
1183
1164
 
1184
1165
 
1185
1166
  def _analog_rot(
1186
1167
  angle: float | str | Parameter,
1187
1168
  qubit_support: str | QubitSupport | Tuple,
1188
1169
  phase: float,
1170
+ add_pattern: bool = True,
1189
1171
  ) -> ConstantAnalogRotation:
1190
1172
  q = _cast(QubitSupport, qubit_support)
1191
1173
  # assuming some arbitrary omega = π rad/μs
@@ -1200,12 +1182,13 @@ def _analog_rot(
1200
1182
  # and compute omega like this:
1201
1183
  # omega = alpha / duration * 1000
1202
1184
  ps = ParamMap(alpha=alpha, duration=duration, omega=omega, delta=0, phase=phase)
1203
- return ConstantAnalogRotation(parameters=ps, qubit_support=q)
1185
+ return ConstantAnalogRotation(parameters=ps, qubit_support=q, add_pattern=add_pattern)
1204
1186
 
1205
1187
 
1206
1188
  def AnalogRX(
1207
1189
  angle: float | str | Parameter,
1208
1190
  qubit_support: str | QubitSupport | Tuple = "global",
1191
+ add_pattern: bool = True,
1209
1192
  ) -> ConstantAnalogRotation:
1210
1193
  """Analog X rotation.
1211
1194
 
@@ -1223,12 +1206,13 @@ def AnalogRX(
1223
1206
  Returns:
1224
1207
  ConstantAnalogRotation
1225
1208
  """
1226
- return _analog_rot(angle, qubit_support, phase=0)
1209
+ return _analog_rot(angle, qubit_support, phase=0, add_pattern=add_pattern)
1227
1210
 
1228
1211
 
1229
1212
  def AnalogRY(
1230
1213
  angle: float | str | Parameter,
1231
1214
  qubit_support: str | QubitSupport | Tuple = "global",
1215
+ add_pattern: bool = True,
1232
1216
  ) -> ConstantAnalogRotation:
1233
1217
  """Analog Y rotation.
1234
1218
 
@@ -1245,12 +1229,13 @@ def AnalogRY(
1245
1229
  Returns:
1246
1230
  ConstantAnalogRotation
1247
1231
  """
1248
- return _analog_rot(angle, qubit_support, phase=-np.pi / 2)
1232
+ return _analog_rot(angle, qubit_support, phase=-np.pi / 2, add_pattern=add_pattern)
1249
1233
 
1250
1234
 
1251
1235
  def AnalogRZ(
1252
1236
  angle: float | str | Parameter,
1253
1237
  qubit_support: str | QubitSupport | Tuple = "global",
1238
+ add_pattern: bool = True,
1254
1239
  ) -> ConstantAnalogRotation:
1255
1240
  """Analog Z rotation. Shorthand for [`AnalogRot`][qadence.operations.AnalogRot]:
1256
1241
  ```
@@ -1263,7 +1248,7 @@ def AnalogRZ(
1263
1248
  delta = np.pi
1264
1249
  duration = alpha / delta * 1000
1265
1250
  ps = ParamMap(alpha=alpha, duration=duration, omega=0, delta=delta, phase=0.0)
1266
- return ConstantAnalogRotation(qubit_support=q, parameters=ps)
1251
+ return ConstantAnalogRotation(qubit_support=q, parameters=ps, add_pattern=add_pattern)
1267
1252
 
1268
1253
 
1269
1254
  # gate sets
@@ -1288,4 +1273,4 @@ analog_gateset = [
1288
1273
  entangle,
1289
1274
  wait,
1290
1275
  ]
1291
- non_unitary_gateset = [Zero, N]
1276
+ non_unitary_gateset = [Zero, N, Projector]
qadence/parameters.py CHANGED
@@ -8,8 +8,9 @@ import numpy as np
8
8
  import sympy
9
9
  from sympy import *
10
10
  from sympy import Array, Basic, Expr, Symbol, sympify
11
+ from sympy.physics.quantum.dagger import Dagger
11
12
  from sympytorch import SymPyModule
12
- from torch import Tensor, rand, tensor
13
+ from torch import Tensor, heaviside, no_grad, rand, tensor
13
14
 
14
15
  from qadence.logger import get_logger
15
16
  from qadence.types import TNumber
@@ -19,6 +20,7 @@ __all__ = ["FeatureParameter", "Parameter", "VariationalParameter"]
19
20
 
20
21
  logger = get_logger(__file__)
21
22
 
23
+ dagger_expression = Dagger
22
24
 
23
25
  ParameterJSONSchema = {
24
26
  "$schema": "http://json-schema.org/draft-07/schema#",
@@ -197,7 +199,13 @@ def torchify(expr: Expr) -> SymPyModule:
197
199
  Returns:
198
200
  A torchified, differentiable Expression.
199
201
  """
200
- extra_funcs = {sympy.core.numbers.ImaginaryUnit: 1.0j}
202
+
203
+ def heaviside_func(x: Tensor, _: Any) -> Tensor:
204
+ with no_grad():
205
+ res = heaviside(x, tensor(0.5))
206
+ return res
207
+
208
+ extra_funcs = {sympy.core.numbers.ImaginaryUnit: 1.0j, sympy.Heaviside: heaviside_func}
201
209
  return SymPyModule(expressions=[sympy.N(expr)], extra_funcs=extra_funcs)
202
210
 
203
211
 
qadence/register.py CHANGED
@@ -11,12 +11,16 @@ import numpy as np
11
11
  from deepdiff import DeepDiff
12
12
  from networkx.classes.reportviews import EdgeView, NodeView
13
13
 
14
+ from qadence.analog import IdealDevice, RydbergDevice
14
15
  from qadence.types import LatticeTopology
15
16
 
16
17
  # Modules to be automatically added to the qadence namespace
17
18
  __all__ = ["Register"]
18
19
 
19
20
 
21
+ DEFAULT_DEVICE = IdealDevice()
22
+
23
+
20
24
  def _scale_node_positions(graph: nx.Graph, min_distance: float, spacing: float) -> None:
21
25
  scaled_nodes = {}
22
26
  scale_factor = spacing / min_distance
@@ -27,8 +31,14 @@ def _scale_node_positions(graph: nx.Graph, min_distance: float, spacing: float)
27
31
 
28
32
 
29
33
  class Register:
30
- def __init__(self, support: nx.Graph | int, spacing: float | None = 1.0):
31
- """A 2D register of qubits which includes their coordinates.
34
+ def __init__(
35
+ self,
36
+ support: nx.Graph | int,
37
+ spacing: float | None = 1.0,
38
+ device_specs: RydbergDevice = DEFAULT_DEVICE,
39
+ ):
40
+ """
41
+ A 2D register of qubits which includes their coordinates.
32
42
 
33
43
  It is needed for e.g. analog computing.
34
44
  The coordinates are ignored in backends that don't need them. The easiest
@@ -52,21 +62,16 @@ class Register:
52
62
  reg.draw()
53
63
  ```
54
64
  """
55
- self.graph = support if isinstance(support, nx.Graph) else alltoall_graph(support)
65
+ if device_specs is not None and not isinstance(device_specs, RydbergDevice):
66
+ raise ValueError("Device specs are not valid. Please pass a `RydbergDevice` instance.")
56
67
 
57
- # Auxiliary complete graph
58
- support = self.graph.nodes
59
- all_edges = list(filter(lambda x: x[0] < x[1], product(support, support)))
60
- self.complete_graph = nx.Graph()
61
- self.complete_graph.add_nodes_from(support)
62
- self.complete_graph.add_edges_from(all_edges)
68
+ self.device_specs = device_specs
69
+
70
+ self.graph = support if isinstance(support, nx.Graph) else alltoall_graph(support)
63
71
 
64
72
  if spacing is not None and self.min_distance != 0.0:
65
73
  _scale_node_positions(self.graph, self.min_distance, spacing)
66
74
 
67
- pos_values = nx.get_node_attributes(self.graph, "pos")
68
- nx.set_node_attributes(self.complete_graph, pos_values, "pos")
69
-
70
75
  @property
71
76
  def n_qubits(self) -> int:
72
77
  return len(self.graph)
@@ -77,27 +82,43 @@ class Register:
77
82
  coords: list[tuple],
78
83
  lattice: LatticeTopology | str = LatticeTopology.ARBITRARY,
79
84
  spacing: float | None = None,
85
+ device_specs: RydbergDevice = DEFAULT_DEVICE,
80
86
  ) -> Register:
81
87
  graph = nx.Graph()
82
88
  for i, pos in enumerate(coords):
83
89
  graph.add_node(i, pos=pos)
84
- return cls(graph, spacing)
90
+ return cls(graph, spacing, device_specs)
85
91
 
86
92
  @classmethod
87
- def line(cls, n_qubits: int, spacing: float = 1.0) -> Register:
88
- return cls(line_graph(n_qubits), spacing)
93
+ def line(
94
+ cls,
95
+ n_qubits: int,
96
+ spacing: float = 1.0,
97
+ device_specs: RydbergDevice = DEFAULT_DEVICE,
98
+ ) -> Register:
99
+ return cls(line_graph(n_qubits), spacing, device_specs)
89
100
 
90
101
  @classmethod
91
- def circle(cls, n_qubits: int, spacing: float = 1.0) -> Register:
102
+ def circle(
103
+ cls,
104
+ n_qubits: int,
105
+ spacing: float = 1.0,
106
+ device_specs: RydbergDevice = DEFAULT_DEVICE,
107
+ ) -> Register:
92
108
  graph = nx.grid_2d_graph(n_qubits, 1, periodic=True)
93
109
  graph = nx.relabel_nodes(graph, {(i, 0): i for i in range(n_qubits)})
94
110
  coords = nx.circular_layout(graph)
95
111
  values = {i: {"pos": pos} for i, pos in coords.items()}
96
112
  nx.set_node_attributes(graph, values)
97
- return cls(graph, spacing)
113
+ return cls(graph, spacing, device_specs)
98
114
 
99
115
  @classmethod
100
- def square(cls, qubits_side: int, spacing: float = 1.0) -> Register:
116
+ def square(
117
+ cls,
118
+ qubits_side: int,
119
+ spacing: float = 1.0,
120
+ device_specs: RydbergDevice = DEFAULT_DEVICE,
121
+ ) -> Register:
101
122
  n_points = 4 * (qubits_side - 1)
102
123
 
103
124
  def gen_points() -> np.ndarray:
@@ -122,35 +143,52 @@ class Register:
122
143
  graph = nx.relabel_nodes(graph, {(i, 0): i for i in range(n_points)})
123
144
  values = {i: {"pos": point} for i, point in zip(graph.nodes, gen_points())}
124
145
  nx.set_node_attributes(graph, values)
125
- return cls(graph, spacing)
146
+ return cls(graph, spacing, device_specs)
126
147
 
127
148
  @classmethod
128
- def all_to_all(cls, n_qubits: int, spacing: float = 1.0) -> Register:
129
- return cls(alltoall_graph(n_qubits), spacing)
149
+ def all_to_all(
150
+ cls,
151
+ n_qubits: int,
152
+ spacing: float = 1.0,
153
+ device_specs: RydbergDevice = DEFAULT_DEVICE,
154
+ ) -> Register:
155
+ return cls(alltoall_graph(n_qubits), spacing, device_specs)
130
156
 
131
157
  @classmethod
132
158
  def rectangular_lattice(
133
- cls, qubits_row: int, qubits_col: int, spacing: float = 1.0
159
+ cls,
160
+ qubits_row: int,
161
+ qubits_col: int,
162
+ spacing: float = 1.0,
163
+ device_specs: RydbergDevice = DEFAULT_DEVICE,
134
164
  ) -> Register:
135
165
  graph = nx.grid_2d_graph(qubits_col, qubits_row)
136
166
  values = {i: {"pos": node} for (i, node) in enumerate(graph.nodes)}
137
167
  graph = nx.relabel_nodes(graph, {(i, j): k for k, (i, j) in enumerate(graph.nodes)})
138
168
  nx.set_node_attributes(graph, values)
139
- return cls(graph, spacing)
169
+ return cls(graph, spacing, device_specs)
140
170
 
141
171
  @classmethod
142
172
  def triangular_lattice(
143
- cls, n_cells_row: int, n_cells_col: int, spacing: float = 1.0
173
+ cls,
174
+ n_cells_row: int,
175
+ n_cells_col: int,
176
+ spacing: float = 1.0,
177
+ device_specs: RydbergDevice = DEFAULT_DEVICE,
144
178
  ) -> Register:
145
- return cls(triangular_lattice_graph(n_cells_row, n_cells_col), spacing)
179
+ return cls(triangular_lattice_graph(n_cells_row, n_cells_col), spacing, device_specs)
146
180
 
147
181
  @classmethod
148
182
  def honeycomb_lattice(
149
- cls, n_cells_row: int, n_cells_col: int, spacing: float = 1.0
183
+ cls,
184
+ n_cells_row: int,
185
+ n_cells_col: int,
186
+ spacing: float = 1.0,
187
+ device_specs: RydbergDevice = DEFAULT_DEVICE,
150
188
  ) -> Register:
151
189
  graph = nx.hexagonal_lattice_graph(n_cells_row, n_cells_col)
152
190
  graph = nx.relabel_nodes(graph, {(i, j): k for k, (i, j) in enumerate(graph.nodes)})
153
- return cls(graph, spacing)
191
+ return cls(graph, spacing, device_specs)
154
192
 
155
193
  @classmethod
156
194
  def lattice(cls, topology: LatticeTopology | str, *args: Any, **kwargs: Any) -> Register:
@@ -166,25 +204,29 @@ class Register:
166
204
  return self.graph.nodes[item]
167
205
 
168
206
  @property
169
- def support(self) -> set:
170
- return set(self.graph.nodes)
171
-
172
- @property
173
- def coords(self) -> dict:
174
- return {i: tuple(node.get("pos", ())) for i, node in self.graph.nodes.items()}
207
+ def nodes(self) -> NodeView:
208
+ return self.graph.nodes
175
209
 
176
210
  @property
177
211
  def edges(self) -> EdgeView:
178
212
  return self.graph.edges
179
213
 
180
214
  @property
181
- def all_edges(self) -> EdgeView:
182
- return self.complete_graph.edges
215
+ def support(self) -> set:
216
+ return set(self.nodes)
217
+
218
+ @property
219
+ def coords(self) -> dict:
220
+ return {i: tuple(node.get("pos", ())) for i, node in self.nodes.items()}
221
+
222
+ @property
223
+ def all_node_pairs(self) -> EdgeView:
224
+ return list(filter(lambda x: x[0] < x[1], product(self.support, self.support)))
183
225
 
184
226
  @property
185
227
  def distances(self) -> dict:
186
228
  coords = self.coords
187
- return {edge: dist(coords[edge[0]], coords[edge[1]]) for edge in self.all_edges}
229
+ return {edge: dist(coords[edge[0]], coords[edge[1]]) for edge in self.all_node_pairs}
188
230
 
189
231
  @property
190
232
  def edge_distances(self) -> dict:
@@ -197,21 +239,27 @@ class Register:
197
239
  value: float = min(self.distances.values()) if len(distances) > 0 else 0.0
198
240
  return value
199
241
 
200
- @property
201
- def nodes(self) -> NodeView:
202
- return self.graph.nodes
203
-
204
242
  def rescale_coords(self, scaling: float) -> Register:
205
243
  g = deepcopy(self.graph)
206
244
  _scale_node_positions(g, min_distance=1.0, spacing=scaling)
207
- return Register(g, spacing=None)
245
+ return Register(g, spacing=None, device_specs=self.device_specs)
208
246
 
209
247
  def _to_dict(self) -> dict:
210
- return {"graph": nx.node_link_data(self.graph)}
248
+ return {
249
+ "graph": nx.node_link_data(self.graph),
250
+ "device_specs": self.device_specs._to_dict(),
251
+ }
211
252
 
212
253
  @classmethod
213
254
  def _from_dict(cls, d: dict) -> Register:
214
- return cls(nx.node_link_graph(d["graph"]))
255
+ device_dict = d.get("device_specs", None)
256
+ if device_dict is None:
257
+ device_dict = DEFAULT_DEVICE._to_dict()
258
+
259
+ return cls(
260
+ support=nx.node_link_graph(d["graph"]),
261
+ device_specs=RydbergDevice._from_dict(device_dict),
262
+ )
215
263
 
216
264
  def __eq__(self, other: object) -> bool:
217
265
  if not isinstance(other, Register):
@@ -1,5 +1,6 @@
1
1
  from __future__ import annotations
2
2
 
3
+ from .apply_fn import apply_fn_to_blocks
3
4
  from .block import (
4
5
  chain_single_qubit_ops,
5
6
  repeat,
@@ -0,0 +1,40 @@
1
+ from __future__ import annotations
2
+
3
+ from typing import Any, Callable
4
+
5
+ from qadence.blocks import AbstractBlock, CompositeBlock, add, chain, kron
6
+ from qadence.blocks.analog import AnalogChain
7
+
8
+ COMPOSE_FN_DICT = {
9
+ "ChainBlock": chain,
10
+ "AnalogChain": chain,
11
+ "KronBlock": kron,
12
+ "AnalogKron": kron,
13
+ "AddBlock": add,
14
+ }
15
+
16
+
17
+ def apply_fn_to_blocks(
18
+ input_block: AbstractBlock, block_fn: Callable, *args: Any, **kwargs: Any
19
+ ) -> AbstractBlock:
20
+ """
21
+ Recurses through the block tree and applies a given function to all the leaf blocks.
22
+
23
+ Arguments:
24
+ input_block: tree of blocks on which to apply the recurse.
25
+ block_fn: callable function to apply to each leaf block.
26
+ args: any positional arguments to pass to the leaf function.
27
+ kwargs: any keyword arguments to pass to the leaf function.
28
+ """
29
+
30
+ if isinstance(input_block, (CompositeBlock, AnalogChain)):
31
+ parsed_blocks = [
32
+ apply_fn_to_blocks(block, block_fn, *args, **kwargs) for block in input_block.blocks
33
+ ]
34
+ compose_fn = COMPOSE_FN_DICT[type(input_block).__name__]
35
+ output_block = compose_fn(*parsed_blocks) # type: ignore [arg-type]
36
+ else:
37
+ # AnalogKrons are considered as a leaf block
38
+ output_block = block_fn(input_block, *args, **kwargs)
39
+
40
+ return output_block