amazon-braket-algorithm-library 1.7.3__tar.gz → 1.7.4__tar.gz

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 (50) hide show
  1. {amazon_braket_algorithm_library-1.7.3/src/amazon_braket_algorithm_library.egg-info → amazon_braket_algorithm_library-1.7.4}/PKG-INFO +2 -1
  2. {amazon_braket_algorithm_library-1.7.3 → amazon_braket_algorithm_library-1.7.4}/README.md +1 -0
  3. {amazon_braket_algorithm_library-1.7.3 → amazon_braket_algorithm_library-1.7.4/src/amazon_braket_algorithm_library.egg-info}/PKG-INFO +2 -1
  4. {amazon_braket_algorithm_library-1.7.3 → amazon_braket_algorithm_library-1.7.4}/src/amazon_braket_algorithm_library.egg-info/SOURCES.txt +2 -0
  5. {amazon_braket_algorithm_library-1.7.3 → amazon_braket_algorithm_library-1.7.4}/src/braket/_algos/_version.py +1 -1
  6. {amazon_braket_algorithm_library-1.7.3 → amazon_braket_algorithm_library-1.7.4}/src/braket/experimental/algorithms/adaptive_shot_allocation/adaptive_allocator.py +105 -74
  7. {amazon_braket_algorithm_library-1.7.3 → amazon_braket_algorithm_library-1.7.4}/src/braket/experimental/algorithms/adaptive_shot_allocation/adaptive_allocator_braket_helpers.py +25 -27
  8. amazon_braket_algorithm_library-1.7.4/src/braket/experimental/algorithms/quantum_counting/__init__.py +20 -0
  9. amazon_braket_algorithm_library-1.7.4/src/braket/experimental/algorithms/quantum_counting/quantum_counting.py +483 -0
  10. {amazon_braket_algorithm_library-1.7.3 → amazon_braket_algorithm_library-1.7.4}/LICENSE +0 -0
  11. {amazon_braket_algorithm_library-1.7.3 → amazon_braket_algorithm_library-1.7.4}/NOTICE +0 -0
  12. {amazon_braket_algorithm_library-1.7.3 → amazon_braket_algorithm_library-1.7.4}/pyproject.toml +0 -0
  13. {amazon_braket_algorithm_library-1.7.3 → amazon_braket_algorithm_library-1.7.4}/setup.cfg +0 -0
  14. {amazon_braket_algorithm_library-1.7.3 → amazon_braket_algorithm_library-1.7.4}/setup.py +0 -0
  15. {amazon_braket_algorithm_library-1.7.3 → amazon_braket_algorithm_library-1.7.4}/src/amazon_braket_algorithm_library.egg-info/dependency_links.txt +0 -0
  16. {amazon_braket_algorithm_library-1.7.3 → amazon_braket_algorithm_library-1.7.4}/src/amazon_braket_algorithm_library.egg-info/requires.txt +0 -0
  17. {amazon_braket_algorithm_library-1.7.3 → amazon_braket_algorithm_library-1.7.4}/src/amazon_braket_algorithm_library.egg-info/top_level.txt +0 -0
  18. {amazon_braket_algorithm_library-1.7.3 → amazon_braket_algorithm_library-1.7.4}/src/braket/experimental/__init__.py +0 -0
  19. {amazon_braket_algorithm_library-1.7.3 → amazon_braket_algorithm_library-1.7.4}/src/braket/experimental/algorithms/__init__.py +0 -0
  20. {amazon_braket_algorithm_library-1.7.3 → amazon_braket_algorithm_library-1.7.4}/src/braket/experimental/algorithms/adaptive_shot_allocation/__init__.py +0 -0
  21. {amazon_braket_algorithm_library-1.7.3 → amazon_braket_algorithm_library-1.7.4}/src/braket/experimental/algorithms/bells_inequality/__init__.py +0 -0
  22. {amazon_braket_algorithm_library-1.7.3 → amazon_braket_algorithm_library-1.7.4}/src/braket/experimental/algorithms/bells_inequality/bells_inequality.py +0 -0
  23. {amazon_braket_algorithm_library-1.7.3 → amazon_braket_algorithm_library-1.7.4}/src/braket/experimental/algorithms/bernstein_vazirani/__init__.py +0 -0
  24. {amazon_braket_algorithm_library-1.7.3 → amazon_braket_algorithm_library-1.7.4}/src/braket/experimental/algorithms/bernstein_vazirani/bernstein_vazirani.py +0 -0
  25. {amazon_braket_algorithm_library-1.7.3 → amazon_braket_algorithm_library-1.7.4}/src/braket/experimental/algorithms/chsh_inequality/__init__.py +0 -0
  26. {amazon_braket_algorithm_library-1.7.3 → amazon_braket_algorithm_library-1.7.4}/src/braket/experimental/algorithms/chsh_inequality/chsh_inequality.py +0 -0
  27. {amazon_braket_algorithm_library-1.7.3 → amazon_braket_algorithm_library-1.7.4}/src/braket/experimental/algorithms/deutsch_jozsa/__init__.py +0 -0
  28. {amazon_braket_algorithm_library-1.7.3 → amazon_braket_algorithm_library-1.7.4}/src/braket/experimental/algorithms/deutsch_jozsa/deutsch_jozsa.py +0 -0
  29. {amazon_braket_algorithm_library-1.7.3 → amazon_braket_algorithm_library-1.7.4}/src/braket/experimental/algorithms/grovers_search/__init__.py +0 -0
  30. {amazon_braket_algorithm_library-1.7.3 → amazon_braket_algorithm_library-1.7.4}/src/braket/experimental/algorithms/grovers_search/grovers_search.py +0 -0
  31. {amazon_braket_algorithm_library-1.7.3 → amazon_braket_algorithm_library-1.7.4}/src/braket/experimental/algorithms/qc_qmc/__init__.py +0 -0
  32. {amazon_braket_algorithm_library-1.7.3 → amazon_braket_algorithm_library-1.7.4}/src/braket/experimental/algorithms/qc_qmc/classical_qmc.py +0 -0
  33. {amazon_braket_algorithm_library-1.7.3 → amazon_braket_algorithm_library-1.7.4}/src/braket/experimental/algorithms/qc_qmc/qc_qmc.py +0 -0
  34. {amazon_braket_algorithm_library-1.7.3 → amazon_braket_algorithm_library-1.7.4}/src/braket/experimental/algorithms/quantum_approximate_optimization/__init__.py +0 -0
  35. {amazon_braket_algorithm_library-1.7.3 → amazon_braket_algorithm_library-1.7.4}/src/braket/experimental/algorithms/quantum_approximate_optimization/quantum_approximate_optimization.py +0 -0
  36. {amazon_braket_algorithm_library-1.7.3 → amazon_braket_algorithm_library-1.7.4}/src/braket/experimental/algorithms/quantum_circuit_born_machine/__init__.py +0 -0
  37. {amazon_braket_algorithm_library-1.7.3 → amazon_braket_algorithm_library-1.7.4}/src/braket/experimental/algorithms/quantum_circuit_born_machine/qcbm.py +0 -0
  38. {amazon_braket_algorithm_library-1.7.3 → amazon_braket_algorithm_library-1.7.4}/src/braket/experimental/algorithms/quantum_fourier_transform/__init__.py +0 -0
  39. {amazon_braket_algorithm_library-1.7.3 → amazon_braket_algorithm_library-1.7.4}/src/braket/experimental/algorithms/quantum_fourier_transform/quantum_fourier_transform.py +0 -0
  40. {amazon_braket_algorithm_library-1.7.3 → amazon_braket_algorithm_library-1.7.4}/src/braket/experimental/algorithms/quantum_phase_estimation/__init__.py +0 -0
  41. {amazon_braket_algorithm_library-1.7.3 → amazon_braket_algorithm_library-1.7.4}/src/braket/experimental/algorithms/quantum_phase_estimation/quantum_phase_estimation.py +0 -0
  42. {amazon_braket_algorithm_library-1.7.3 → amazon_braket_algorithm_library-1.7.4}/src/braket/experimental/algorithms/quantum_walk/__init__.py +0 -0
  43. {amazon_braket_algorithm_library-1.7.3 → amazon_braket_algorithm_library-1.7.4}/src/braket/experimental/algorithms/quantum_walk/quantum_walk.py +0 -0
  44. {amazon_braket_algorithm_library-1.7.3 → amazon_braket_algorithm_library-1.7.4}/src/braket/experimental/algorithms/shors/__init__.py +0 -0
  45. {amazon_braket_algorithm_library-1.7.3 → amazon_braket_algorithm_library-1.7.4}/src/braket/experimental/algorithms/shors/shors.py +0 -0
  46. {amazon_braket_algorithm_library-1.7.3 → amazon_braket_algorithm_library-1.7.4}/src/braket/experimental/algorithms/simons/__init__.py +0 -0
  47. {amazon_braket_algorithm_library-1.7.3 → amazon_braket_algorithm_library-1.7.4}/src/braket/experimental/algorithms/simons/simons.py +0 -0
  48. {amazon_braket_algorithm_library-1.7.3 → amazon_braket_algorithm_library-1.7.4}/src/braket/experimental/auxiliary_functions/__init__.py +0 -0
  49. {amazon_braket_algorithm_library-1.7.3 → amazon_braket_algorithm_library-1.7.4}/src/braket/experimental/auxiliary_functions/random_circuit/__init__.py +0 -0
  50. {amazon_braket_algorithm_library-1.7.3 → amazon_braket_algorithm_library-1.7.4}/src/braket/experimental/auxiliary_functions/random_circuit/random_circuit.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: amazon-braket-algorithm-library
3
- Version: 1.7.3
3
+ Version: 1.7.4
4
4
  Summary: An open source library of quantum computing algorithms implemented on Amazon Braket
5
5
  Home-page: https://github.com/amazon-braket/amazon-braket-algorithm-library
6
6
  Author: Amazon Web Services
@@ -70,6 +70,7 @@ Running notebooks locally requires additional dependencies located in [notebooks
70
70
  | Quantum Circuit Born Machine | [Quantum_Circuit_Born_Machine.ipynb](notebooks/textbook/Quantum_Circuit_Born_Machine.ipynb) | [Benedetti2019](https://www.nature.com/articles/s41534-019-0157-8), [Liu2018](https://journals.aps.org/pra/abstract/10.1103/PhysRevA.98.062324) |
71
71
  | QFT | [Quantum_Fourier_Transform.ipynb](notebooks/textbook/Quantum_Fourier_Transform.ipynb) | [Coppersmith2002](https://arxiv.org/abs/quant-ph/0201067) |
72
72
  | QPE | [Quantum_Phase_Estimation_Algorithm.ipynb](notebooks/textbook/Quantum_Phase_Estimation_Algorithm.ipynb) | [Kitaev1995](https://arxiv.org/abs/quant-ph/9511026) |
73
+ | Quantum Counting | [Quantum_Counting_Algorithm.ipynb](notebooks/textbook/Quantum_Counting_Algorithm.ipynb) | [Brassard1998](https://arxiv.org/abs/quant-ph/9805082) |
73
74
  | Quantum Walk | [Quantum_Walk.ipynb](notebooks/textbook/Quantum_Walk.ipynb) | [Childs2002](https://arxiv.org/abs/quant-ph/0209131) |
74
75
  |Shor's| [Shors_Algorithm.ipynb](notebooks/textbook/Shors_Algorithm.ipynb) | [Shor1998](https://arxiv.org/abs/quant-ph/9508027) |
75
76
  | Simon's | [Simons_Algorithm.ipynb](notebooks/textbook/Simons_Algorithm.ipynb) | [Simon1997](https://epubs.siam.org/doi/10.1137/S0097539796298637) |
@@ -22,6 +22,7 @@ Running notebooks locally requires additional dependencies located in [notebooks
22
22
  | Quantum Circuit Born Machine | [Quantum_Circuit_Born_Machine.ipynb](notebooks/textbook/Quantum_Circuit_Born_Machine.ipynb) | [Benedetti2019](https://www.nature.com/articles/s41534-019-0157-8), [Liu2018](https://journals.aps.org/pra/abstract/10.1103/PhysRevA.98.062324) |
23
23
  | QFT | [Quantum_Fourier_Transform.ipynb](notebooks/textbook/Quantum_Fourier_Transform.ipynb) | [Coppersmith2002](https://arxiv.org/abs/quant-ph/0201067) |
24
24
  | QPE | [Quantum_Phase_Estimation_Algorithm.ipynb](notebooks/textbook/Quantum_Phase_Estimation_Algorithm.ipynb) | [Kitaev1995](https://arxiv.org/abs/quant-ph/9511026) |
25
+ | Quantum Counting | [Quantum_Counting_Algorithm.ipynb](notebooks/textbook/Quantum_Counting_Algorithm.ipynb) | [Brassard1998](https://arxiv.org/abs/quant-ph/9805082) |
25
26
  | Quantum Walk | [Quantum_Walk.ipynb](notebooks/textbook/Quantum_Walk.ipynb) | [Childs2002](https://arxiv.org/abs/quant-ph/0209131) |
26
27
  |Shor's| [Shors_Algorithm.ipynb](notebooks/textbook/Shors_Algorithm.ipynb) | [Shor1998](https://arxiv.org/abs/quant-ph/9508027) |
27
28
  | Simon's | [Simons_Algorithm.ipynb](notebooks/textbook/Simons_Algorithm.ipynb) | [Simon1997](https://epubs.siam.org/doi/10.1137/S0097539796298637) |
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: amazon-braket-algorithm-library
3
- Version: 1.7.3
3
+ Version: 1.7.4
4
4
  Summary: An open source library of quantum computing algorithms implemented on Amazon Braket
5
5
  Home-page: https://github.com/amazon-braket/amazon-braket-algorithm-library
6
6
  Author: Amazon Web Services
@@ -70,6 +70,7 @@ Running notebooks locally requires additional dependencies located in [notebooks
70
70
  | Quantum Circuit Born Machine | [Quantum_Circuit_Born_Machine.ipynb](notebooks/textbook/Quantum_Circuit_Born_Machine.ipynb) | [Benedetti2019](https://www.nature.com/articles/s41534-019-0157-8), [Liu2018](https://journals.aps.org/pra/abstract/10.1103/PhysRevA.98.062324) |
71
71
  | QFT | [Quantum_Fourier_Transform.ipynb](notebooks/textbook/Quantum_Fourier_Transform.ipynb) | [Coppersmith2002](https://arxiv.org/abs/quant-ph/0201067) |
72
72
  | QPE | [Quantum_Phase_Estimation_Algorithm.ipynb](notebooks/textbook/Quantum_Phase_Estimation_Algorithm.ipynb) | [Kitaev1995](https://arxiv.org/abs/quant-ph/9511026) |
73
+ | Quantum Counting | [Quantum_Counting_Algorithm.ipynb](notebooks/textbook/Quantum_Counting_Algorithm.ipynb) | [Brassard1998](https://arxiv.org/abs/quant-ph/9805082) |
73
74
  | Quantum Walk | [Quantum_Walk.ipynb](notebooks/textbook/Quantum_Walk.ipynb) | [Childs2002](https://arxiv.org/abs/quant-ph/0209131) |
74
75
  |Shor's| [Shors_Algorithm.ipynb](notebooks/textbook/Shors_Algorithm.ipynb) | [Shor1998](https://arxiv.org/abs/quant-ph/9508027) |
75
76
  | Simon's | [Simons_Algorithm.ipynb](notebooks/textbook/Simons_Algorithm.ipynb) | [Simon1997](https://epubs.siam.org/doi/10.1137/S0097539796298637) |
@@ -32,6 +32,8 @@ src/braket/experimental/algorithms/quantum_approximate_optimization/__init__.py
32
32
  src/braket/experimental/algorithms/quantum_approximate_optimization/quantum_approximate_optimization.py
33
33
  src/braket/experimental/algorithms/quantum_circuit_born_machine/__init__.py
34
34
  src/braket/experimental/algorithms/quantum_circuit_born_machine/qcbm.py
35
+ src/braket/experimental/algorithms/quantum_counting/__init__.py
36
+ src/braket/experimental/algorithms/quantum_counting/quantum_counting.py
35
37
  src/braket/experimental/algorithms/quantum_fourier_transform/__init__.py
36
38
  src/braket/experimental/algorithms/quantum_fourier_transform/quantum_fourier_transform.py
37
39
  src/braket/experimental/algorithms/quantum_phase_estimation/__init__.py
@@ -16,4 +16,4 @@
16
16
  Version number (major.minor.patch[-label])
17
17
  """
18
18
 
19
- __version__ = "1.7.3"
19
+ __version__ = "1.7.4"
@@ -33,7 +33,7 @@ def commute(a: str, b: str, qwc: bool = True) -> bool:
33
33
  Raises:
34
34
  ValueError: If the Pauli strigns are of different length.
35
35
  """
36
- if len(a)!=len(b):
36
+ if len(a) != len(b):
37
37
  raise ValueError("Pauli strings must be of the same length.")
38
38
  count = 0
39
39
  for i in zip(a, b):
@@ -42,15 +42,17 @@ def commute(a: str, b: str, qwc: bool = True) -> bool:
42
42
 
43
43
 
44
44
  # Partial functions for specific commutation checks
45
- qwc_commute = partial(commute, qwc=True) # Qubit-wise commutation
46
- gen_commute = partial(commute, qwc=False) # General commutation
45
+ qwc_commute = partial(commute, qwc=True) # Qubit-wise commutation
46
+ gen_commute = partial(commute, qwc=False) # General commutation
47
47
 
48
48
  # BAYESIAN STATISTICS (closed formulas from Appendix B of arXiv:2110.15339v6)
49
49
 
50
50
 
51
- def term_variance_estimate(term_idx: int, measurements: Union[MeasurementData, None] = None) -> float:
51
+ def term_variance_estimate(
52
+ term_idx: int, measurements: Union[MeasurementData, None] = None
53
+ ) -> float:
52
54
  """
53
- Estimate variance for a single Pauli term.
55
+ Estimate variance for a single Pauli term.
54
56
  See Eq 14 in Appendix B of "Adaptive Estimation of Quantum Observables (arXiv:2110.15339v6)
55
57
 
56
58
  Args:
@@ -64,10 +66,12 @@ def term_variance_estimate(term_idx: int, measurements: Union[MeasurementData, N
64
66
  if measurements:
65
67
  x0 = measurements[term_idx][term_idx][(1, 1)]
66
68
  x1 = measurements[term_idx][term_idx][(-1, -1)]
67
- return 4*((x0+1)*(x1+1))/((x0+x1+2)*(x0+x1+3))
69
+ return 4 * ((x0 + 1) * (x1 + 1)) / ((x0 + x1 + 2) * (x0 + x1 + 3))
68
70
 
69
71
 
70
- def terms_covariance_estimate(i: int, j: int, measurements: Union[MeasurementData, None] = None) -> float:
72
+ def terms_covariance_estimate(
73
+ i: int, j: int, measurements: Union[MeasurementData, None] = None
74
+ ) -> float:
71
75
  """
72
76
  Estimate covariance between two Pauli terms.
73
77
  See Eq 25-6 in Appendix B of "Adaptive Estimation of Quantum Observables (arXiv:2110.15339v6)
@@ -93,14 +97,17 @@ def terms_covariance_estimate(i: int, j: int, measurements: Union[MeasurementDat
93
97
  xy11 = measurements[i][j][(-1, -1)]
94
98
 
95
99
  # Calculate prior probabilities
96
- p00 = 4*((x0+1)*(y0+1))/((x0+x1+2)*(y0+y1+2))
97
- p01 = 4*((x0+1)*(y1+1))/((x0+x1+2)*(y0+y1+2))
98
- p10 = 4*((x1+1)*(y0+1))/((x0+x1+2)*(y0+y1+2))
99
- p11 = 4*((x1+1)*(y1+1))/((x0+x1+2)*(y0+y1+2))
100
+ p00 = 4 * ((x0 + 1) * (y0 + 1)) / ((x0 + x1 + 2) * (y0 + y1 + 2))
101
+ p01 = 4 * ((x0 + 1) * (y1 + 1)) / ((x0 + x1 + 2) * (y0 + y1 + 2))
102
+ p10 = 4 * ((x1 + 1) * (y0 + 1)) / ((x0 + x1 + 2) * (y0 + y1 + 2))
103
+ p11 = 4 * ((x1 + 1) * (y1 + 1)) / ((x0 + x1 + 2) * (y0 + y1 + 2))
100
104
 
101
105
  # Return Bayesian covariance estimate
102
- return 4*((xy00+p00)*(xy11+p11) - (xy01+p01)*(xy10+p10)) / \
103
- ((xy00+xy01+xy10+xy11+4)*(xy00+xy01+xy10+xy11+5))
106
+ return (
107
+ 4
108
+ * ((xy00 + p00) * (xy11 + p11) - (xy01 + p01) * (xy10 + p10))
109
+ / ((xy00 + xy01 + xy10 + xy11 + 4) * (xy00 + xy01 + xy10 + xy11 + 5))
110
+ )
104
111
 
105
112
 
106
113
  class AdaptiveShotAllocator:
@@ -156,11 +163,10 @@ class AdaptiveShotAllocator:
156
163
  raise ValueError("Number of Paulis must match coefficients")
157
164
 
158
165
  # Validate Pauli strings
159
- valid_chars = set('IXYZ')
166
+ valid_chars = set("IXYZ")
160
167
  for pauli in paulis:
161
168
  if not set(pauli).issubset(valid_chars):
162
- raise ValueError(
163
- f"Invalid Pauli string: {pauli}. Must only contain I, X, Y, or Z")
169
+ raise ValueError(f"Invalid Pauli string: {pauli}. Must only contain I, X, Y, or Z")
164
170
  self.paulis = paulis
165
171
  self.coeffs = coeffs
166
172
  self.graph = self._generate_graph()
@@ -179,9 +185,10 @@ class AdaptiveShotAllocator:
179
185
  - Updates graph weights to initial values
180
186
  """
181
187
  # Initialize measurement counts for all term pairs
182
- self.measurements = [[{(1, 1): 0, (1, -1): 0, (-1, 1): 0, (-1, -1): 0}
183
- for _ in range(self.num_terms)]
184
- for _ in range(self.num_terms)]
188
+ self.measurements = [
189
+ [{(1, 1): 0, (1, -1): 0, (-1, 1): 0, (-1, -1): 0} for _ in range(self.num_terms)]
190
+ for _ in range(self.num_terms)
191
+ ]
185
192
  self.shots = None # Clear shot allocation history
186
193
  self._update_graph_weights() # Reset graph weights
187
194
 
@@ -201,7 +208,9 @@ class AdaptiveShotAllocator:
201
208
  Edges are added between terms that commute according to the provided function.
202
209
  """
203
210
  if commute != qwc_commute:
204
- warnings.warn("Braket only supports simultaneous measurement of qubit-wise commuting operators.")
211
+ warnings.warn(
212
+ "Braket only supports simultaneous measurement of qubit-wise commuting operators."
213
+ )
205
214
 
206
215
  # Create graph and add nodes with Pauli string labels
207
216
  self.graph = nx.Graph()
@@ -229,8 +238,9 @@ class AdaptiveShotAllocator:
229
238
  _, cliq = approximation.clique_removal(self.graph)
230
239
  self.cliq = [sorted(i) for i in cliq] # Sort cliques for consistency
231
240
 
232
- def visualize_graph(self, node_size: int = 1230, font_size: int = 10,
233
- show_cliques: bool = True) -> None:
241
+ def visualize_graph(
242
+ self, node_size: int = 1230, font_size: int = 10, show_cliques: bool = True
243
+ ) -> None:
234
244
  """
235
245
  Visualize the graph with colored edges based on clique membership.
236
246
 
@@ -242,15 +252,17 @@ class AdaptiveShotAllocator:
242
252
  """
243
253
 
244
254
  # Generate random colors for each clique
245
- cliq_colors = ["#"+"".join([hex(random.randint(0, 255))[2:].zfill(2)
246
- for _ in range(3)]) for _ in self.cliq]
255
+ cliq_colors = [
256
+ "#" + "".join([hex(random.randint(0, 255))[2:].zfill(2) for _ in range(3)])
257
+ for _ in self.cliq
258
+ ]
247
259
 
248
260
  # Build edge list and colors
249
261
  if show_cliques:
250
262
  el = []
251
263
  ec = []
252
264
  for e in self.graph.edges:
253
- for i,c in enumerate(self.cliq):
265
+ for i, c in enumerate(self.cliq):
254
266
  if (e[0] in c) and (e[1] in c):
255
267
  el.append(e)
256
268
  ec.append(cliq_colors[i])
@@ -258,23 +270,32 @@ class AdaptiveShotAllocator:
258
270
  else:
259
271
  el = list(self.graph.edges)
260
272
  # Use gray for all edges when showing full graph
261
- ec = ['gray' for _ in el]
273
+ ec = ["gray" for _ in el]
262
274
 
263
275
  # Create layout
264
276
  pos = nx.circular_layout(self.graph)
265
277
 
266
278
  # Draw the graph
267
279
  plt.figure(figsize=(10, 10))
268
- nx.draw(self.graph, pos=pos, with_labels=False,
269
- node_color='white', node_size=node_size,
270
- edgelist=el, edge_color=ec)
280
+ nx.draw(
281
+ self.graph,
282
+ pos=pos,
283
+ with_labels=False,
284
+ node_color="white",
285
+ node_size=node_size,
286
+ edgelist=el,
287
+ edge_color=ec,
288
+ )
271
289
 
272
290
  # Add labels
273
- nx.draw_networkx_labels(self.graph, pos,
274
- labels=nx.get_node_attributes(
275
- self.graph, 'label'),
276
- font_size=font_size, font_color="black",
277
- font_family="Times")
291
+ nx.draw_networkx_labels(
292
+ self.graph,
293
+ pos,
294
+ labels=nx.get_node_attributes(self.graph, "label"),
295
+ font_size=font_size,
296
+ font_color="black",
297
+ font_family="Times",
298
+ )
278
299
 
279
300
  # Set edge colors
280
301
  ax = plt.gca()
@@ -299,9 +320,12 @@ class AdaptiveShotAllocator:
299
320
  # Base weight from coefficients
300
321
  weight = self.coeffs[i] * self.coeffs[j]
301
322
  # Multiply by variance/covariance estimate
302
- weight *= (term_variance_estimate(i, measurements) if i == j
303
- else terms_covariance_estimate(i, j, measurements))
304
- self.graph[i][j]['weight'] = weight
323
+ weight *= (
324
+ term_variance_estimate(i, measurements)
325
+ if i == j
326
+ else terms_covariance_estimate(i, j, measurements)
327
+ )
328
+ self.graph[i][j]["weight"] = weight
305
329
 
306
330
  def incremental_shot_allocation(self, num_shots: int) -> List[int]:
307
331
  """
@@ -337,13 +361,14 @@ class AdaptiveShotAllocator:
337
361
  current_shots = self.shots if self.shots else [0 for c in self.cliq]
338
362
 
339
363
  # Calculate weighted covariance matrix for error estimates
340
- weighted_covariance = nx.adjacency_matrix(
341
- self.graph, weight="weight").toarray()
364
+ weighted_covariance = nx.adjacency_matrix(self.graph, weight="weight").toarray()
342
365
 
343
366
  # Initialize error estimates for each clique using current shots
344
367
  # Error = sqrt(sum of covariances) / number of shots
345
- clique_error = [np.sqrt(weighted_covariance[c, c].sum())/(current_shots[e] or not current_shots[e])
346
- for e, c in enumerate(self.cliq)]
368
+ clique_error = [
369
+ np.sqrt(weighted_covariance[c, c].sum()) / (current_shots[e] or not current_shots[e])
370
+ for e, c in enumerate(self.cliq)
371
+ ]
347
372
 
348
373
  # Allocate shots one at a time
349
374
  for _ in range(num_shots):
@@ -356,8 +381,7 @@ class AdaptiveShotAllocator:
356
381
  # Update error estimate for the chosen clique
357
382
  # New error = Old error * (n/(n+1)) where n+1 is the updated shot count.
358
383
  total_shots = current_shots[cliq_id] + proposed_allocation[cliq_id]
359
- clique_error[cliq_id] *= ((total_shots-1)
360
- or not (total_shots-1))/(total_shots)
384
+ clique_error[cliq_id] *= ((total_shots - 1) or not (total_shots - 1)) / (total_shots)
361
385
 
362
386
  return proposed_allocation
363
387
 
@@ -379,18 +403,23 @@ class AdaptiveShotAllocator:
379
403
  ValueError: If graph weights have not been properly initialized
380
404
  """
381
405
  # Get weighted covariance matrix
382
- weighted_covariance = nx.adjacency_matrix(
383
- self.graph, weight="weight").toarray()
406
+ weighted_covariance = nx.adjacency_matrix(self.graph, weight="weight").toarray()
384
407
 
385
408
  # Sum variance contributions from each clique
386
- variance_estimate = sum([(weighted_covariance[c, c].sum())/(self.shots[e] or not self.shots[e])
387
- for e, c in enumerate(self.cliq)])
409
+ variance_estimate = sum(
410
+ [
411
+ (weighted_covariance[c, c].sum()) / (self.shots[e] or not self.shots[e])
412
+ for e, c in enumerate(self.cliq)
413
+ ]
414
+ )
388
415
 
389
416
  return np.sqrt(variance_estimate)
390
417
 
391
- def expectation_from_measurements(self, measurements: Union[MeasurementData, None] = None) -> float:
418
+ def expectation_from_measurements(
419
+ self, measurements: Union[MeasurementData, None] = None
420
+ ) -> float:
392
421
  """
393
- Calculate the energy expectation value from measurement results
422
+ Calculate the energy expectation value from measurement results
394
423
  for the different Pauli string observables.
395
424
 
396
425
  For each Pauli term, computes <P> = (N++ - N--)/N_total where:
@@ -414,17 +443,15 @@ class AdaptiveShotAllocator:
414
443
  expectation = 0.0
415
444
  for i in range(len(self.coeffs)):
416
445
  # Verify no invalid measurements
417
- assert measurements[i][i][(
418
- 1, -1)] == 0, "Invalid measurement detected: (+1,-1)"
419
- assert measurements[i][i][(-1, 1)
420
- ] == 0, "Invalid measurement detected: (-1,+1)"
446
+ assert measurements[i][i][(1, -1)] == 0, "Invalid measurement detected: (+1,-1)"
447
+ assert measurements[i][i][(-1, 1)] == 0, "Invalid measurement detected: (-1,+1)"
421
448
 
422
449
  # Calculate expectation for this term
423
- term_shots = measurements[i][i][(
424
- 1, 1)] + measurements[i][i][(-1, -1)]
450
+ term_shots = measurements[i][i][(1, 1)] + measurements[i][i][(-1, -1)]
425
451
  if term_shots:
426
452
  term_expect = (
427
- measurements[i][i][(1, 1)] - measurements[i][i][(-1, -1)]) / term_shots
453
+ measurements[i][i][(1, 1)] - measurements[i][i][(-1, -1)]
454
+ ) / term_shots
428
455
  expectation += self.coeffs[i] * term_expect
429
456
 
430
457
  return expectation
@@ -447,13 +474,11 @@ class AdaptiveShotAllocator:
447
474
  Raises:
448
475
  AssertionError: If any validation check fails
449
476
  """
450
- assert len(
451
- measurements) == self.num_terms, "Wrong number of measurement records"
477
+ assert len(measurements) == self.num_terms, "Wrong number of measurement records"
452
478
 
453
479
  for c in self.cliq:
454
480
  # Get total shots for this clique
455
- m_cliq = (measurements[c[0]][c[0]][(1, 1)] +
456
- measurements[c[0]][c[0]][(-1, -1)])
481
+ m_cliq = measurements[c[0]][c[0]][(1, 1)] + measurements[c[0]][c[0]][(-1, -1)]
457
482
 
458
483
  # Check consistency within clique
459
484
  for i in c:
@@ -463,25 +488,31 @@ class AdaptiveShotAllocator:
463
488
  assert v >= 0, "Measurement counts should not be negative"
464
489
 
465
490
  # Measurements should be symmetric
466
- assert measurements[i][j][(1,1)]==measurements[j][i][(1,1)],\
467
- "Measurement should be symmetric"
468
- assert measurements[i][j][(-1,-1)]==measurements[j][i][(-1,-1)],\
469
- "Measurement should be symmetric"
470
- assert measurements[i][j][(1,-1)]==measurements[j][i][(-1,1)],\
471
- "Measurement should be symmetric"
472
- assert measurements[i][j][(-1,1)]==measurements[j][i][(1,-1)],\
473
- "Measurement should be symmetric"
491
+ assert measurements[i][j][(1, 1)] == measurements[j][i][(1, 1)], (
492
+ "Measurement should be symmetric"
493
+ )
494
+ assert measurements[i][j][(-1, -1)] == measurements[j][i][(-1, -1)], (
495
+ "Measurement should be symmetric"
496
+ )
497
+ assert measurements[i][j][(1, -1)] == measurements[j][i][(-1, 1)], (
498
+ "Measurement should be symmetric"
499
+ )
500
+ assert measurements[i][j][(-1, 1)] == measurements[j][i][(1, -1)], (
501
+ "Measurement should be symmetric"
502
+ )
474
503
 
475
504
  # All pairs in clique should have same total measurements
476
- assert m_cliq == sum(measurements[i][j].values()), \
477
- (f"The number of times {i} and {j} were measured together should be "
478
- "equal to the number of measurements of their clique.")
505
+ assert m_cliq == sum(measurements[i][j].values()), (
506
+ f"The number of times {i} and {j} were measured together should be "
507
+ "equal to the number of measurements of their clique."
508
+ )
479
509
 
480
510
  # Diagonal elements should not have invalid combinations
481
511
  if i == j:
482
- assert measurements[i][i][(1, -1)] == measurements[i][i][(-1, 1)] == 0, \
483
- ("A measurement of a single term can only contribute to the "
484
- "(1,1) or (-1,-1) counts.")
512
+ assert measurements[i][i][(1, -1)] == measurements[i][i][(-1, 1)] == 0, (
513
+ "A measurement of a single term can only contribute to the "
514
+ "(1,1) or (-1,-1) counts."
515
+ )
485
516
  return True
486
517
 
487
518
  def shots_from_measurements(self, measurements: MeasurementData) -> List[int]:
@@ -1,5 +1,5 @@
1
1
  """
2
- Helper functions to handle quantum measurements and adaptive shot allocation
2
+ Helper functions to handle quantum measurements and adaptive shot allocation
3
3
  experiments on Amazon Braket.
4
4
  """
5
5
 
@@ -13,6 +13,7 @@ from braket.experimental.algorithms.adaptive_shot_allocation.adaptive_allocator
13
13
  MeasurementData,
14
14
  )
15
15
 
16
+
16
17
  def observable_from_string(pauli_string: str) -> Observable:
17
18
  """
18
19
  Convert Pauli string to Braket observable.
@@ -23,15 +24,15 @@ def observable_from_string(pauli_string: str) -> Observable:
23
24
  Returns:
24
25
  Observable: Corresponding Braket observable
25
26
  """
26
- gates = {"I": Observable.I, "X": Observable.X,
27
- "Y": Observable.Y, "Z": Observable.Z}
27
+ gates = {"I": Observable.I, "X": Observable.X, "Y": Observable.Y, "Z": Observable.Z}
28
28
  return Observable.TensorProduct([gates[i[1]](i[0]) for i in enumerate(pauli_string)])
29
29
 
30
+
30
31
  def run_fixed_allocation(
31
32
  device: Union[LocalSimulator, AwsDevice],
32
33
  circuit: Circuit,
33
34
  estimator: AdaptiveShotAllocator,
34
- shot_allocation: List[int]
35
+ shot_allocation: List[int],
35
36
  ) -> MeasurementData:
36
37
  """
37
38
  Run experiment with a specific shot allocation.
@@ -45,7 +46,7 @@ def run_fixed_allocation(
45
46
  Returns:
46
47
  MeasurementData: Measurement outcomes for each term pair
47
48
  """
48
-
49
+
49
50
  # Step 1. Submit all tasks.
50
51
  tasks = {}
51
52
  for c_idx, c in enumerate(estimator.cliq):
@@ -54,41 +55,40 @@ def run_fixed_allocation(
54
55
 
55
56
  measurement_circ = circuit.copy()
56
57
  for p in c:
57
- measurement_circ.sample(
58
- observable_from_string(estimator.paulis[p]))
58
+ measurement_circ.sample(observable_from_string(estimator.paulis[p]))
59
+
60
+ tasks[c_idx] = device.run(measurement_circ, shots=shot_allocation[c_idx])
59
61
 
60
- tasks[c_idx] = device.run(
61
- measurement_circ, shots=shot_allocation[c_idx])
62
-
63
62
  # Step 2. Post-process results.
64
- measurements = [[{(1, 1): 0, (1, -1): 0, (-1, 1): 0, (-1, -1): 0}
65
- for _ in range(len(estimator.paulis))]
66
- for _ in range(len(estimator.paulis))]
67
-
68
- while(tasks):
63
+ measurements = [
64
+ [{(1, 1): 0, (1, -1): 0, (-1, 1): 0, (-1, -1): 0} for _ in range(len(estimator.paulis))]
65
+ for _ in range(len(estimator.paulis))
66
+ ]
67
+
68
+ while tasks:
69
69
  task_to_process = None
70
-
70
+
71
71
  for c_idx in tasks:
72
72
  # Check task status
73
73
  state = tasks[c_idx].state()
74
- assert state in ["CREATED", "QUEUED", "RUNNING", "COMPLETED"], \
74
+ assert state in ["CREATED", "QUEUED", "RUNNING", "COMPLETED"], (
75
75
  f"Encountered quantum task failure (status: {state})."
76
+ )
76
77
 
77
78
  if state == "COMPLETED":
78
79
  task_to_process = c_idx
79
80
  break
80
-
81
+
81
82
  if task_to_process is None:
82
83
  continue
83
-
84
+
84
85
  # Task is ready for post-processing
85
86
  result = tasks[task_to_process].result()
86
87
  c = estimator.cliq[task_to_process]
87
88
  for i_idx, i in enumerate(c):
88
89
  for j_idx, j in enumerate(c):
89
90
  for s in range(len(result.values[i_idx])):
90
- measurements[i][j][(result.values[i_idx][s],
91
- result.values[j_idx][s])] += 1
91
+ measurements[i][j][(result.values[i_idx][s], result.values[j_idx][s])] += 1
92
92
  # Remove task from the queue
93
93
  tasks.pop(task_to_process)
94
94
 
@@ -101,7 +101,7 @@ def run_adaptive_allocation(
101
101
  estimator: AdaptiveShotAllocator,
102
102
  shots_per_round: int,
103
103
  num_rounds: int,
104
- verbose: bool = False
104
+ verbose: bool = False,
105
105
  ) -> MeasurementData:
106
106
  """
107
107
  Run adaptive shot allocation process.
@@ -118,14 +118,12 @@ def run_adaptive_allocation(
118
118
  MeasurementData: Final measurement outcomes
119
119
  """
120
120
  if verbose:
121
- print(
122
- f"Running {num_rounds} rounds with {shots_per_round} shots each:")
121
+ print(f"Running {num_rounds} rounds with {shots_per_round} shots each:")
123
122
  for i in range(num_rounds):
124
123
  if verbose:
125
- print(f"Round {i+1}/{num_rounds}...")
124
+ print(f"Round {i + 1}/{num_rounds}...")
126
125
  shots_to_run = estimator.incremental_shot_allocation(shots_per_round)
127
- new_measurements = run_fixed_allocation(
128
- device, circuit, estimator, shots_to_run)
126
+ new_measurements = run_fixed_allocation(device, circuit, estimator, shots_to_run)
129
127
  estimator.update_measurements(new_measurements)
130
128
 
131
129
  return estimator.measurements
@@ -0,0 +1,20 @@
1
+ # Copyright Amazon.com Inc. or its affiliates. All Rights Reserved.
2
+ #
3
+ # Licensed under the Apache License, Version 2.0 (the "License"). You
4
+ # may not use this file except in compliance with the License. A copy of
5
+ # the License is located at
6
+ #
7
+ # http://aws.amazon.com/apache2.0/
8
+ #
9
+ # or in the "license" file accompanying this file. This file is
10
+ # distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF
11
+ # ANY KIND, either express or implied. See the License for the specific
12
+ # language governing permissions and limitations under the License.
13
+
14
+ from braket.experimental.algorithms.quantum_counting.quantum_counting import ( # noqa: F401, E501
15
+ build_grover_circuit,
16
+ build_oracle_circuit,
17
+ get_quantum_counting_results,
18
+ quantum_counting_circuit,
19
+ run_quantum_counting,
20
+ )
@@ -0,0 +1,483 @@
1
+ """Quantum Counting Algorithm implementation using the Amazon Braket SDK.
2
+
3
+ The quantum counting algorithm combines Grover's search operator with Quantum
4
+ Phase Estimation (QPE) to count the number of marked items in an unstructured
5
+ search space of N = 2^n elements.
6
+
7
+ Reference:
8
+ G. Brassard, P. Høyer, and A. Tapp, "Quantum Counting",
9
+ Proceedings of ICALP 1998. arXiv:quant-ph/9805082
10
+ """
11
+
12
+ import math
13
+ from typing import Any, Dict, List, Tuple
14
+
15
+ import numpy as np
16
+
17
+ from braket.circuits import Circuit
18
+ from braket.circuits.qubit_set import QubitSetInput
19
+ from braket.devices import Device
20
+ from braket.experimental.algorithms.grovers_search.grovers_search import amplify, build_oracle
21
+ from braket.tasks import QuantumTask
22
+
23
+
24
+ def build_oracle_circuit(
25
+ n_qubits: int,
26
+ marked_states: List[int],
27
+ decompose_ccnot: bool = False,
28
+ ) -> Circuit:
29
+ """Build an oracle circuit that flips the phase of marked states.
30
+
31
+ Composes individual oracle circuits from grovers_search.build_oracle
32
+ for each marked state. Each call flips the phase of one basis state,
33
+ and their composition marks all specified states.
34
+
35
+ Args:
36
+ n_qubits (int): Number of qubits in the search register.
37
+ marked_states (List[int]): Indices of marked computational-basis states.
38
+ decompose_ccnot (bool): Whether to decompose CCNOT (Toffoli) gates.
39
+
40
+ Returns:
41
+ Circuit: Oracle circuit that flips the phase of all marked states.
42
+
43
+ Raises:
44
+ ValueError: If a marked state index is out of range.
45
+ """
46
+ dim = 2**n_qubits
47
+ for state in marked_states:
48
+ if state < 0 or state >= dim:
49
+ raise ValueError(
50
+ f"Marked state {state} is out of range for {n_qubits} qubits "
51
+ f"(must be in [0, {dim - 1}])."
52
+ )
53
+
54
+ oracle_circ = Circuit()
55
+ for state in marked_states:
56
+ bitstring = format(state, f"0{n_qubits}b")
57
+ oracle_circ.add_circuit(build_oracle(bitstring, decompose_ccnot))
58
+ return oracle_circ
59
+
60
+
61
+ def build_grover_circuit(
62
+ n_qubits: int,
63
+ marked_states: List[int],
64
+ decompose_ccnot: bool = False,
65
+ ) -> Circuit:
66
+ """Build the Grover operator G = D · O as a circuit.
67
+
68
+ Uses circuit primitives from grovers_search: build_oracle for the phase
69
+ oracle and amplify for the diffusion operator (H · oracle_0 · H).
70
+
71
+ Args:
72
+ n_qubits (int): Number of qubits in the search register.
73
+ marked_states (List[int]): Indices of marked states.
74
+ decompose_ccnot (bool): Whether to decompose CCNOT (Toffoli) gates.
75
+
76
+ Returns:
77
+ Circuit: Circuit implementing the Grover operator G.
78
+ """
79
+ oracle_circ = build_oracle_circuit(n_qubits, marked_states, decompose_ccnot)
80
+ diffusion_circ = amplify(n_qubits, decompose_ccnot)
81
+
82
+ grover_circ = Circuit()
83
+ grover_circ.add_circuit(oracle_circ)
84
+ grover_circ.add_circuit(diffusion_circ)
85
+ return grover_circ
86
+
87
+
88
+ def _controlled_oracle_circuit(
89
+ control: int,
90
+ search_qubits: QubitSetInput,
91
+ marked_states: List[int],
92
+ decompose_ccnot: bool = False,
93
+ ) -> Circuit:
94
+ """Build a controlled oracle circuit using gate-level primitives.
95
+
96
+ Makes the phase-flip oracle controlled on the given qubit by prepending
97
+ "1" to each marked-state bitstring, so the MCZ gate gains an extra
98
+ control qubit.
99
+
100
+ Args:
101
+ control (int): Index of the control qubit.
102
+ search_qubits (QubitSetInput): Indices of the search register qubits.
103
+ marked_states (List[int]): Indices of marked computational-basis states.
104
+ decompose_ccnot (bool): Whether to decompose CCNOT (Toffoli) gates.
105
+
106
+ Returns:
107
+ Circuit: Controlled oracle circuit.
108
+ """
109
+ n_search = len(search_qubits)
110
+ circ = Circuit()
111
+
112
+ for state in marked_states:
113
+ bitstring = format(state, f"0{n_search}b")
114
+ # Prepend "1" so the MCZ is controlled on the QPE qubit
115
+ controlled_bitstring = "1" + bitstring
116
+ oracle_sub = build_oracle(controlled_bitstring, decompose_ccnot)
117
+ # Map the oracle qubits: qubit 0 -> control, qubits 1..n -> search_qubits
118
+ target_mapping = [control] + list(search_qubits)
119
+ # The oracle may use ancilla qubits beyond n_search+1; they are
120
+ # automatically placed by add_circuit's target mapping.
121
+ all_targets = target_mapping + list(
122
+ range(
123
+ max(target_mapping) + 1,
124
+ max(target_mapping) + 1 + oracle_sub.qubit_count - len(target_mapping),
125
+ )
126
+ )
127
+ circ.add_circuit(oracle_sub, target=all_targets)
128
+
129
+ return circ
130
+
131
+
132
+ def _controlled_diffusion_circuit(
133
+ control: int,
134
+ search_qubits: QubitSetInput,
135
+ decompose_ccnot: bool = False,
136
+ ) -> Circuit:
137
+ """Build a controlled diffusion (amplitude amplification) circuit.
138
+
139
+ The diffusion operator is D = H^⊗n · O_0 · H^⊗n, where O_0 flips the
140
+ phase of the |0⟩ state. To make it controlled:
141
+ - H gates become controlled-H (CH) gates
142
+ - O_0 becomes controlled by prepending "1" to the "00...0" bitstring
143
+
144
+ Args:
145
+ control (int): Index of the control qubit.
146
+ search_qubits (QubitSetInput): Indices of the search register qubits.
147
+ decompose_ccnot (bool): Whether to decompose CCNOT (Toffoli) gates.
148
+
149
+ Returns:
150
+ Circuit: Controlled diffusion circuit.
151
+ """
152
+ n_search = len(search_qubits)
153
+ circ = Circuit()
154
+
155
+ # First set of controlled-H gates on search qubits
156
+ for sq in search_qubits:
157
+ _add_controlled_h(circ, control, sq)
158
+
159
+ # Controlled O_0: flip phase of |0⟩, controlled on QPE qubit
160
+ # O_0 uses build_oracle("00...0"). Controlled version: build_oracle("1" + "00...0")
161
+ controlled_zero_bitstring = "1" + "0" * n_search
162
+ zero_oracle = build_oracle(controlled_zero_bitstring, decompose_ccnot)
163
+ target_mapping = [control] + list(search_qubits)
164
+ all_targets = target_mapping + list(
165
+ range(
166
+ max(target_mapping) + 1,
167
+ max(target_mapping) + 1 + zero_oracle.qubit_count - len(target_mapping),
168
+ )
169
+ )
170
+ circ.add_circuit(zero_oracle, target=all_targets)
171
+
172
+ # Second set of controlled-H gates on search qubits
173
+ for sq in search_qubits:
174
+ _add_controlled_h(circ, control, sq)
175
+
176
+ # Apply Z gate to correct the -1 global phase difference between
177
+ # I - 2|s><s| and 2|s><s| - I when controlled.
178
+ circ.z(control)
179
+
180
+ return circ
181
+
182
+
183
+ def _add_controlled_h(circ: Circuit, control: int, target: int) -> None:
184
+ """Add a controlled-Hadamard gate to the circuit.
185
+
186
+ Decomposition: CH(c, t) = S†(t) · H(t) · T†(t) · CX(c,t) · T(t) · H(t) · S(t)
187
+
188
+ Args:
189
+ circ (Circuit): Circuit to append the controlled-H to.
190
+ control (int): Control qubit index.
191
+ target (int): Target qubit index.
192
+ """
193
+ circ.s(target)
194
+ circ.h(target)
195
+ circ.t(target)
196
+ circ.cnot(control, target)
197
+ circ.ti(target)
198
+ circ.h(target)
199
+ circ.si(target)
200
+
201
+
202
+ def controlled_grover_circuit(
203
+ control: int,
204
+ search_qubits: QubitSetInput,
205
+ marked_states: List[int],
206
+ power: int = 1,
207
+ decompose_ccnot: bool = False,
208
+ ) -> Circuit:
209
+ """Build a controlled Grover operator G^power as a gate-level circuit.
210
+
211
+ Constructs the controlled Grover operator C-G = C-D · C-O entirely from
212
+ gate-level primitives:
213
+ - The oracle is made controlled by prepending "1" to each marked-state
214
+ bitstring, adding the control qubit as an extra MCZ control.
215
+ - The diffusion operator is made controlled by using controlled-H (CH)
216
+ gates, a controlled zero-oracle, and a Z gate on the control qubit
217
+ to correct the inherent -1 global phase of the diffusion operator.
218
+
219
+ Args:
220
+ control (int): Index of the QPE control qubit.
221
+ search_qubits (QubitSetInput): Indices of the search register qubits.
222
+ marked_states (List[int]): Indices of marked computational-basis states.
223
+ power (int): Number of times to apply the Grover operator (default 1).
224
+ decompose_ccnot (bool): Whether to decompose CCNOT (Toffoli) gates.
225
+
226
+ Returns:
227
+ Circuit: Circuit implementing the controlled G^power operator.
228
+ """
229
+ circ = Circuit()
230
+
231
+ for _ in range(power):
232
+ # Controlled oracle
233
+ circ.add_circuit(
234
+ _controlled_oracle_circuit(control, search_qubits, marked_states, decompose_ccnot)
235
+ )
236
+
237
+ # Controlled diffusion
238
+ circ.add_circuit(
239
+ _controlled_diffusion_circuit(control, search_qubits, decompose_ccnot)
240
+ )
241
+
242
+ return circ
243
+
244
+
245
+ def inverse_qft_for_counting(qubits: QubitSetInput) -> Circuit:
246
+ """Inverse Quantum Fourier Transform applied to the given qubits.
247
+
248
+ Args:
249
+ qubits (QubitSetInput): Qubits on which to apply the inverse QFT.
250
+
251
+ Returns:
252
+ Circuit: Circuit implementing the inverse QFT.
253
+ """
254
+ qft_circ = Circuit()
255
+ num_qubits = len(qubits)
256
+
257
+ # SWAP gates to reverse qubit order
258
+ for i in range(math.floor(num_qubits / 2)):
259
+ qft_circ.swap(qubits[i], qubits[-i - 1])
260
+
261
+ # Controlled phase rotations + Hadamard
262
+ for k in reversed(range(num_qubits)):
263
+ for j in reversed(range(1, num_qubits - k)):
264
+ angle = -2 * math.pi / (2 ** (j + 1))
265
+ qft_circ.cphaseshift(qubits[k + j], qubits[k], angle)
266
+ qft_circ.h(qubits[k])
267
+
268
+ return qft_circ
269
+
270
+
271
+ def quantum_counting_circuit(
272
+ counting_circ: Circuit,
273
+ counting_qubits: QubitSetInput,
274
+ search_qubits: QubitSetInput,
275
+ marked_states: List[int],
276
+ decompose_ccnot: bool = False,
277
+ ) -> Circuit:
278
+ """Create the full quantum counting circuit with result types.
279
+
280
+ Builds the controlled Grover operator as a gate-level circuit and
281
+ applies QPE.
282
+
283
+ Args:
284
+ counting_circ (Circuit): Initial circuit (may contain setup operations).
285
+ counting_qubits (QubitSetInput): Qubits for the counting (precision) register.
286
+ search_qubits (QubitSetInput): Qubits for the search register.
287
+ marked_states (List[int]): Indices of marked computational-basis states.
288
+ decompose_ccnot (bool): Whether to decompose CCNOT (Toffoli) gates.
289
+
290
+ Returns:
291
+ Circuit: The complete quantum counting circuit with result types.
292
+ """
293
+ new_circuit = Circuit().add_circuit(counting_circ)
294
+ new_circuit.add_circuit(
295
+ quantum_counting(
296
+ counting_qubits, search_qubits, marked_states, decompose_ccnot
297
+ )
298
+ )
299
+ return new_circuit.probability(counting_qubits)
300
+
301
+
302
+ def quantum_counting(
303
+ counting_qubits: QubitSetInput,
304
+ search_qubits: QubitSetInput,
305
+ marked_states: List[int],
306
+ decompose_ccnot: bool = False,
307
+ ) -> Circuit:
308
+ """Build the core quantum counting circuit using QPE on the Grover operator.
309
+
310
+ Constructs the controlled Grover operator as a gate-level circuit from
311
+ grovers_search primitives (build_oracle for the phase oracle and
312
+ controlled-H gates for the diffusion operator).
313
+
314
+ The circuit structure:
315
+ 1. Apply H to all counting qubits
316
+ 2. Apply H to all search qubits (prepare uniform superposition |s⟩)
317
+ 3. Apply controlled-G^(2^k) for each counting qubit k
318
+ 4. Apply inverse QFT to counting qubits
319
+
320
+ Args:
321
+ counting_qubits (QubitSetInput): Qubits for the counting (precision) register.
322
+ search_qubits (QubitSetInput): Qubits for the search register.
323
+ marked_states (List[int]): Indices of marked computational-basis states.
324
+ decompose_ccnot (bool): Whether to decompose CCNOT (Toffoli) gates.
325
+
326
+ Returns:
327
+ Circuit: Circuit implementing the quantum counting algorithm.
328
+ """
329
+ qc_circ = Circuit()
330
+
331
+ # Hadamard on counting qubits
332
+ qc_circ.h(counting_qubits)
333
+
334
+ # Hadamard on search qubits (prepare |s⟩)
335
+ qc_circ.h(search_qubits)
336
+
337
+ # Controlled-G^(2^k)
338
+ for ii, qubit in enumerate(reversed(counting_qubits)):
339
+ power = 2**ii
340
+ qc_circ.add_circuit(
341
+ controlled_grover_circuit(
342
+ qubit, search_qubits, marked_states, power, decompose_ccnot
343
+ )
344
+ )
345
+
346
+ # Inverse QFT on counting qubits
347
+ qc_circ.add_circuit(inverse_qft_for_counting(counting_qubits))
348
+
349
+ return qc_circ
350
+
351
+
352
+ def run_quantum_counting(
353
+ circuit: Circuit,
354
+ device: Device,
355
+ shots: int = 1000,
356
+ ) -> QuantumTask:
357
+ """Run the quantum counting circuit on a device.
358
+
359
+ Args:
360
+ circuit (Circuit): The quantum counting circuit to run.
361
+ device (Device): Braket device backend.
362
+ shots (int): Number of measurement shots (default is 1000).
363
+
364
+ Returns:
365
+ QuantumTask: Task from running quantum counting.
366
+ """
367
+ task = device.run(circuit, shots=shots)
368
+ return task
369
+
370
+
371
+ def get_quantum_counting_results(
372
+ task,
373
+ counting_qubits: QubitSetInput,
374
+ search_qubits: QubitSetInput,
375
+ verbose: bool = False,
376
+ ) -> Dict[str, Any]:
377
+ """Post-process quantum counting results to estimate the number of marked items.
378
+
379
+ After measuring the counting qubits, the most likely outcome y gives
380
+ an estimate of the phase φ = y / 2^t. The number of marked items M is:
381
+ M = N · sin²(π · φ)
382
+ where N = 2^n_search.
383
+
384
+ Args:
385
+ task (QuantumTask): The task holding quantum counting results.
386
+ counting_qubits (QubitSetInput): Qubits of the counting register.
387
+ search_qubits (QubitSetInput): Qubits of the search register.
388
+ verbose (bool): If True, print detailed results (default False).
389
+
390
+ Returns:
391
+ Dict[str, Any]: Aggregate measurement results including:
392
+ - measurement_counts: raw measurement counts
393
+ - counting_register_results: counts collapsed to the counting register
394
+ - phases: estimated phases φ for each measured bitstring
395
+ - estimated_counts: estimated M for each measured bitstring
396
+ - best_estimate: best estimate of M (from most frequent outcome)
397
+ - search_space_size: N = 2^n_search
398
+ """
399
+ result = task.result()
400
+ measurement_counts = result.measurement_counts
401
+ n_counting = len(counting_qubits)
402
+ n_search = len(search_qubits)
403
+ N = 2**n_search
404
+
405
+ # Aggregate results on counting register (trace out search qubits)
406
+ counting_register_results: Dict[str, int] = {}
407
+ if measurement_counts:
408
+ for key in measurement_counts.keys():
409
+ counting_bits = key[:n_counting]
410
+ counting_register_results[counting_bits] = (
411
+ counting_register_results.get(counting_bits, 0) + measurement_counts[key]
412
+ )
413
+
414
+ # Convert counting register bitstrings to phase estimates and M estimates
415
+ phases, estimated_counts = _get_counting_estimates(counting_register_results, n_counting, N)
416
+
417
+ # Best estimate from most frequent outcome
418
+ if counting_register_results:
419
+ best_key = max(counting_register_results, key=counting_register_results.get)
420
+ best_y = int(best_key, 2)
421
+ raw_phase = best_y / (2**n_counting)
422
+ best_phase = raw_phase
423
+ best_M = N * (np.sin(np.pi * best_phase) ** 2)
424
+ else:
425
+ best_key = None
426
+ best_M = None
427
+
428
+ aggregate_results = {
429
+ "measurement_counts": measurement_counts,
430
+ "counting_register_results": counting_register_results,
431
+ "phases": phases,
432
+ "estimated_counts": estimated_counts,
433
+ "best_estimate": best_M,
434
+ "search_space_size": N,
435
+ }
436
+
437
+ if verbose:
438
+ print(f"Search space size N = {N}")
439
+ sorted_cr = sorted(
440
+ counting_register_results.items(), key=lambda x: x[1], reverse=True
441
+ )
442
+ print("\nCounting register distribution (top outcomes):")
443
+ for bitstring, count in sorted_cr[:6]:
444
+ y_val = int(bitstring, 2)
445
+ raw_ph = y_val / (2**n_counting)
446
+ ph = raw_ph
447
+ m_est = N * (np.sin(np.pi * ph) ** 2)
448
+ print(f" |{bitstring}>: {count} counts -> phase = {ph:.4f}, M ~ {m_est:.4f}")
449
+ if len(sorted_cr) > 6:
450
+ print(f" ... ({len(sorted_cr) - 6} more outcomes)")
451
+ print(f"\nBest estimate of M: {best_M}")
452
+
453
+ return aggregate_results
454
+
455
+
456
+ def _get_counting_estimates(
457
+ counting_register_results: Dict[str, int],
458
+ n_counting: int,
459
+ N: int,
460
+ ) -> Tuple[List[float], List[float]]:
461
+ """Convert counting register bitstrings to phase and count estimates.
462
+
463
+ Args:
464
+ counting_register_results (Dict[str, int]): Measurement results on the counting register.
465
+ n_counting (int): Number of counting qubits.
466
+ N (int): Search space size.
467
+
468
+ Returns:
469
+ Tuple[List[float], List[float]]: (phases, estimated_counts) lists, one entry per unique
470
+ measured bitstring with counts > 0.
471
+ """
472
+ phases = []
473
+ estimated_counts = []
474
+
475
+ for bitstring in counting_register_results:
476
+ y = int(bitstring, 2)
477
+ raw_phase = y / (2**n_counting)
478
+ phase = raw_phase
479
+ M_est = N * (np.sin(np.pi * phase) ** 2)
480
+ phases.append(phase)
481
+ estimated_counts.append(M_est)
482
+
483
+ return phases, estimated_counts