qadence 1.1.1__py3-none-any.whl → 1.2.1__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 (64) hide show
  1. qadence/__init__.py +1 -0
  2. qadence/analog/__init__.py +4 -2
  3. qadence/analog/addressing.py +167 -0
  4. qadence/analog/constants.py +59 -0
  5. qadence/analog/device.py +82 -0
  6. qadence/analog/hamiltonian_terms.py +101 -0
  7. qadence/analog/parse_analog.py +120 -0
  8. qadence/backend.py +42 -12
  9. qadence/backends/__init__.py +1 -2
  10. qadence/backends/api.py +27 -9
  11. qadence/backends/braket/backend.py +3 -2
  12. qadence/backends/horqrux/__init__.py +5 -0
  13. qadence/backends/horqrux/backend.py +216 -0
  14. qadence/backends/horqrux/config.py +26 -0
  15. qadence/backends/horqrux/convert_ops.py +273 -0
  16. qadence/backends/jax_utils.py +45 -0
  17. qadence/backends/pulser/__init__.py +0 -1
  18. qadence/backends/pulser/backend.py +31 -15
  19. qadence/backends/pulser/config.py +19 -10
  20. qadence/backends/pulser/devices.py +57 -63
  21. qadence/backends/pulser/pulses.py +70 -12
  22. qadence/backends/pyqtorch/backend.py +4 -4
  23. qadence/backends/pyqtorch/config.py +18 -12
  24. qadence/backends/pyqtorch/convert_ops.py +15 -7
  25. qadence/backends/utils.py +5 -9
  26. qadence/blocks/abstract.py +5 -1
  27. qadence/blocks/analog.py +18 -9
  28. qadence/blocks/block_to_tensor.py +11 -0
  29. qadence/blocks/embedding.py +46 -24
  30. qadence/blocks/primitive.py +81 -9
  31. qadence/blocks/utils.py +20 -1
  32. qadence/circuit.py +3 -9
  33. qadence/constructors/__init__.py +4 -0
  34. qadence/constructors/feature_maps.py +84 -60
  35. qadence/constructors/hamiltonians.py +27 -98
  36. qadence/constructors/rydberg_feature_maps.py +113 -0
  37. qadence/divergences.py +12 -0
  38. qadence/engines/__init__.py +0 -0
  39. qadence/engines/differentiable_backend.py +152 -0
  40. qadence/engines/jax/__init__.py +8 -0
  41. qadence/engines/jax/differentiable_backend.py +73 -0
  42. qadence/engines/jax/differentiable_expectation.py +94 -0
  43. qadence/engines/torch/__init__.py +4 -0
  44. qadence/engines/torch/differentiable_backend.py +85 -0
  45. qadence/extensions.py +21 -9
  46. qadence/finitediff.py +47 -0
  47. qadence/mitigations/readout.py +92 -25
  48. qadence/ml_tools/models.py +10 -3
  49. qadence/models/qnn.py +88 -23
  50. qadence/models/quantum_model.py +13 -2
  51. qadence/operations.py +55 -70
  52. qadence/parameters.py +24 -13
  53. qadence/register.py +91 -43
  54. qadence/transpile/__init__.py +1 -0
  55. qadence/transpile/apply_fn.py +40 -0
  56. qadence/types.py +32 -2
  57. qadence/utils.py +35 -0
  58. {qadence-1.1.1.dist-info → qadence-1.2.1.dist-info}/METADATA +22 -3
  59. {qadence-1.1.1.dist-info → qadence-1.2.1.dist-info}/RECORD +62 -44
  60. {qadence-1.1.1.dist-info → qadence-1.2.1.dist-info}/WHEEL +1 -1
  61. qadence/analog/interaction.py +0 -198
  62. qadence/analog/utils.py +0 -132
  63. /qadence/{backends/pytorch_wrapper.py → engines/torch/differentiable_expectation.py} +0 -0
  64. {qadence-1.1.1.dist-info → qadence-1.2.1.dist-info}/licenses/LICENSE +0 -0
@@ -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
@@ -0,0 +1,113 @@
1
+ from __future__ import annotations
2
+
3
+ from typing import Callable
4
+
5
+ import numpy as np
6
+ from sympy import Basic, Function
7
+
8
+ from qadence.blocks import AnalogBlock, KronBlock, kron
9
+ from qadence.constructors.feature_maps import fm_parameter
10
+ from qadence.logger import get_logger
11
+ from qadence.operations import AnalogRot, AnalogRX, AnalogRY, AnalogRZ
12
+ from qadence.parameters import FeatureParameter, Parameter, VariationalParameter
13
+ from qadence.types import BasisSet, ReuploadScaling, TParameter
14
+
15
+ logger = get_logger(__file__)
16
+
17
+ AnalogRotationTypes = [AnalogRX, AnalogRY, AnalogRZ]
18
+
19
+
20
+ def rydberg_feature_map(
21
+ n_qubits: int,
22
+ param: str = "phi",
23
+ max_abs_detuning: float = 2 * np.pi * 10,
24
+ weights: list[float] | None = None,
25
+ ) -> KronBlock:
26
+ """Feature map using semi-local addressing patterns.
27
+
28
+ If not weights are specified, variational parameters are created
29
+ for the pattern
30
+
31
+ Args:
32
+ n_qubits (int): number of qubits
33
+ param: the name of the feature parameter
34
+ max_abs_detuning: maximum value of absolute detuning for each qubit. Defaulted at 10 MHz.
35
+ weights: a list of wegiths to assign to each qubit parameter in the feature map
36
+
37
+ Returns:
38
+ The block representing the feature map
39
+ """
40
+
41
+ tower_coeffs: list[float | Parameter]
42
+ tower_coeffs = (
43
+ [VariationalParameter(f"w_{param}_{i}") for i in range(n_qubits)]
44
+ if weights is None
45
+ else weights
46
+ )
47
+ tower_detuning = max_abs_detuning / (sum(tower_coeffs[i] for i in range(n_qubits)))
48
+
49
+ param = FeatureParameter(param)
50
+ duration = 1000 * param / tower_detuning
51
+ return kron(
52
+ AnalogRot(
53
+ duration=duration,
54
+ delta=-tower_detuning * tower_coeffs[i],
55
+ phase=0.0,
56
+ qubit_support=(i,),
57
+ )
58
+ for i in range(n_qubits)
59
+ )
60
+
61
+
62
+ def rydberg_tower_feature_map(
63
+ n_qubits: int, param: str = "phi", max_abs_detuning: float = 2 * np.pi * 10
64
+ ) -> KronBlock:
65
+ weights = list(np.arange(1, n_qubits + 1))
66
+ return rydberg_feature_map(
67
+ n_qubits, param=param, max_abs_detuning=max_abs_detuning, weights=weights
68
+ )
69
+
70
+
71
+ def analog_feature_map(
72
+ param: str = "phi",
73
+ op: Callable[[Parameter | Basic], AnalogBlock] = AnalogRX,
74
+ fm_type: BasisSet | type[Function] | str = BasisSet.FOURIER,
75
+ reupload_scaling: ReuploadScaling | Callable | str = ReuploadScaling.CONSTANT,
76
+ feature_range: tuple[float, float] | None = None,
77
+ target_range: tuple[float, float] | None = None,
78
+ multiplier: Parameter | TParameter | None = None,
79
+ ) -> AnalogBlock:
80
+ """Generate a fully analog feature map.
81
+
82
+ Args:
83
+ param: Parameter of the feature map; you can pass a string or Parameter;
84
+ it will be set as non-trainable (FeatureParameter) regardless.
85
+ op: type of operation. Choose among AnalogRX, AnalogRY, AnalogRZ or a custom
86
+ callable function returning an AnalogBlock instance
87
+ fm_type: Basis set for data encoding; choose from `BasisSet.FOURIER` for Fourier
88
+ encoding, or `BasisSet.CHEBYSHEV` for Chebyshev polynomials of the first kind.
89
+ reupload_scaling: how the feature map scales the data that is re-uploaded. Given that
90
+ this feature map uses analog rotations, the reuploading works by simply
91
+ adding additional operations with different scaling factors in the parameter.
92
+ Choose from `ReuploadScaling` enumeration, currently only CONSTANT works,
93
+ or provide your own function with the first argument being the given
94
+ operation `op` and the second argument the feature parameter
95
+ feature_range: range of data that the input data is assumed to come from.
96
+ target_range: range of data the data encoder assumes as the natural range. For example,
97
+ in Chebyshev polynomials it is (-1, 1), while for Fourier it may be chosen as (0, 2*pi).
98
+ multiplier: overall multiplier; this is useful for reuploading the feature map serially with
99
+ different scalings; can be a number or parameter/expression.
100
+ """
101
+ transformed_feature = fm_parameter(
102
+ fm_type, param, feature_range=feature_range, target_range=target_range
103
+ )
104
+ multiplier = 1.0 if multiplier is None else Parameter(multiplier)
105
+
106
+ if callable(reupload_scaling):
107
+ return reupload_scaling(op, multiplier * transformed_feature) # type: ignore[no-any-return]
108
+ elif reupload_scaling == ReuploadScaling.CONSTANT:
109
+ return op(multiplier * transformed_feature)
110
+ # TODO: implement tower scaling by reuploading multiple times
111
+ # using different analog rotations
112
+ else:
113
+ raise NotImplementedError(f"Reupload scaling {str(reupload_scaling)} not implemented!")
qadence/divergences.py CHANGED
@@ -36,3 +36,15 @@ def js_divergence(counter_p: Counter, counter_q: Counter) -> float:
36
36
  entropy_p = shannon_entropy(counter_p)
37
37
  entropy_q = shannon_entropy(counter_q)
38
38
  return float(average_entropy - (entropy_p + entropy_q) / 2.0)
39
+
40
+
41
+ def norm_difference(counter_p: Counter, counter_q: Counter) -> float:
42
+ # Normalise counters
43
+
44
+ counter_p = np.array([v for v in counter_p.values()])
45
+ counter_q = np.array([v for v in counter_q.values()])
46
+
47
+ prob_p = counter_p / np.sum(counter_p)
48
+ prob_q = counter_q / np.sum(counter_q)
49
+
50
+ return float(np.linalg.norm(prob_p - prob_q))
File without changes
@@ -0,0 +1,152 @@
1
+ from __future__ import annotations
2
+
3
+ from abc import ABC, abstractmethod
4
+ from collections import Counter
5
+ from dataclasses import dataclass
6
+ from typing import Any, Callable
7
+
8
+ from qadence.backend import Backend, Converted, ConvertedCircuit, ConvertedObservable
9
+ from qadence.blocks import AbstractBlock, PrimitiveBlock
10
+ from qadence.blocks.utils import uuid_to_block
11
+ from qadence.circuit import QuantumCircuit
12
+ from qadence.measurements import Measurements
13
+ from qadence.mitigations import Mitigations
14
+ from qadence.noise import Noise
15
+ from qadence.types import ArrayLike, DiffMode, Endianness, Engine, ParamDictType
16
+
17
+
18
+ @dataclass(frozen=True, eq=True)
19
+ class DifferentiableBackend(ABC):
20
+ """The abstract class which wraps any (non)-natively differentiable QuantumBackend.
21
+
22
+ in an automatic differentiation engine.
23
+
24
+ Arguments:
25
+ backend: An instance of the QuantumBackend type perform execution.
26
+ engine: Which automatic differentiation engine the QuantumBackend runs on.
27
+ diff_mode: A differentiable mode supported by the differentiation engine.
28
+ """
29
+
30
+ backend: Backend
31
+ engine: Engine
32
+ diff_mode: DiffMode
33
+
34
+ # TODO: Add differentiable overlap calculation
35
+ _overlap: Callable = None # type: ignore [assignment]
36
+
37
+ def sample(
38
+ self,
39
+ circuit: ConvertedCircuit,
40
+ param_values: ParamDictType = {},
41
+ n_shots: int = 100,
42
+ state: ArrayLike | None = None,
43
+ noise: Noise | None = None,
44
+ mitigation: Mitigations | None = None,
45
+ endianness: Endianness = Endianness.BIG,
46
+ ) -> list[Counter]:
47
+ """Sample bitstring from the registered circuit.
48
+
49
+ Arguments:
50
+ circuit: A backend native quantum circuit to be executed.
51
+ param_values: The values of the parameters after embedding
52
+ n_shots: The number of shots. Defaults to 1.
53
+ state: Initial state.
54
+ noise: A noise model to use.
55
+ mitigation: A mitigation protocol to apply to noisy samples.
56
+ endianness: Endianness of the resulting bitstrings.
57
+
58
+ Returns:
59
+ An iterable with all the sampled bitstrings
60
+ """
61
+
62
+ return self.backend.sample(
63
+ circuit=circuit,
64
+ param_values=param_values,
65
+ n_shots=n_shots,
66
+ state=state,
67
+ noise=noise,
68
+ mitigation=mitigation,
69
+ endianness=endianness,
70
+ )
71
+
72
+ def run(
73
+ self,
74
+ circuit: ConvertedCircuit,
75
+ param_values: ParamDictType = {},
76
+ state: ArrayLike | None = None,
77
+ endianness: Endianness = Endianness.BIG,
78
+ ) -> ArrayLike:
79
+ """Run on the underlying backend."""
80
+ return self.backend.run(
81
+ circuit=circuit, param_values=param_values, state=state, endianness=endianness
82
+ )
83
+
84
+ @abstractmethod
85
+ def expectation(
86
+ self,
87
+ circuit: ConvertedCircuit,
88
+ observable: list[ConvertedObservable] | ConvertedObservable,
89
+ param_values: ParamDictType = {},
90
+ state: ArrayLike | None = None,
91
+ measurement: Measurements | None = None,
92
+ noise: Noise | None = None,
93
+ mitigation: Mitigations | None = None,
94
+ endianness: Endianness = Endianness.BIG,
95
+ ) -> Any:
96
+ """Compute the expectation value of the `circuit` with the given `observable`.
97
+
98
+ Arguments:
99
+ circuit: A converted circuit as returned by `backend.circuit`.
100
+ observable: A converted observable as returned by `backend.observable`.
101
+ param_values: _**Already embedded**_ parameters of the circuit. See
102
+ [`embedding`][qadence.blocks.embedding.embedding] for more info.
103
+ state: Initial state.
104
+ measurement: Optional measurement protocol. If None, use
105
+ exact expectation value with a statevector simulator.
106
+ noise: A noise model to use.
107
+ mitigation: The error mitigation to use.
108
+ endianness: Endianness of the resulting bit strings.
109
+ """
110
+ raise NotImplementedError(
111
+ "A DifferentiableBackend needs to override the expectation method."
112
+ )
113
+
114
+ def default_configuration(self) -> Any:
115
+ return self.backend.default_configuration()
116
+
117
+ def circuit(self, circuit: QuantumCircuit) -> ConvertedCircuit:
118
+ if self.diff_mode == DiffMode.GPSR:
119
+ parametrized_blocks = list(uuid_to_block(circuit.block).values())
120
+ non_prim_blocks = filter(
121
+ lambda b: not isinstance(b, PrimitiveBlock), parametrized_blocks
122
+ )
123
+ if len(list(non_prim_blocks)) > 0:
124
+ raise ValueError(
125
+ "The circuit contains non-primitive blocks that are currently\
126
+ not supported by the PSR differentiable mode."
127
+ )
128
+ return self.backend.circuit(circuit)
129
+
130
+ def observable(self, observable: AbstractBlock, n_qubits: int) -> ConvertedObservable:
131
+ if self.diff_mode != DiffMode.AD and observable is not None:
132
+ msg = (
133
+ f"Differentiation mode '{self.diff_mode}' does not support parametric observables."
134
+ )
135
+ if isinstance(observable, list):
136
+ for obs in observable:
137
+ if obs.is_parametric:
138
+ raise ValueError(msg)
139
+ else:
140
+ if observable.is_parametric:
141
+ raise ValueError(msg)
142
+ return self.backend.observable(observable, n_qubits)
143
+
144
+ def convert(
145
+ self,
146
+ circuit: QuantumCircuit,
147
+ observable: list[AbstractBlock] | AbstractBlock | None = None,
148
+ ) -> Converted:
149
+ return self.backend.convert(circuit, observable)
150
+
151
+ def assign_parameters(self, circuit: ConvertedCircuit, param_values: ParamDictType) -> Any:
152
+ return self.backend.assign_parameters(circuit, param_values)