qoro-divi 0.3.3__py3-none-any.whl → 0.3.5__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.

Potentially problematic release.


This version of qoro-divi might be problematic. Click here for more details.

Files changed (74) hide show
  1. divi/__init__.py +1 -2
  2. divi/backends/__init__.py +7 -0
  3. divi/backends/_circuit_runner.py +46 -0
  4. divi/{parallel_simulator.py → backends/_parallel_simulator.py} +136 -53
  5. divi/backends/_qoro_service.py +531 -0
  6. divi/circuits/__init__.py +5 -0
  7. divi/circuits/_core.py +226 -0
  8. divi/{qasm.py → circuits/qasm.py} +21 -2
  9. divi/{exp → extern}/cirq/_validator.py +9 -7
  10. divi/qprog/__init__.py +18 -5
  11. divi/qprog/algorithms/__init__.py +14 -0
  12. divi/qprog/algorithms/_ansatze.py +311 -0
  13. divi/qprog/{_qaoa.py → algorithms/_qaoa.py} +69 -41
  14. divi/qprog/{_vqe.py → algorithms/_vqe.py} +79 -135
  15. divi/qprog/batch.py +239 -55
  16. divi/qprog/exceptions.py +9 -0
  17. divi/qprog/optimizers.py +219 -18
  18. divi/qprog/quantum_program.py +389 -57
  19. divi/qprog/workflows/__init__.py +10 -0
  20. divi/qprog/{_graph_partitioning.py → workflows/_graph_partitioning.py} +3 -34
  21. divi/qprog/{_qubo_partitioning.py → workflows/_qubo_partitioning.py} +42 -25
  22. divi/qprog/{_vqe_sweep.py → workflows/_vqe_sweep.py} +59 -26
  23. divi/reporting/__init__.py +7 -0
  24. divi/reporting/_pbar.py +112 -0
  25. divi/{qlogger.py → reporting/_qlogger.py} +37 -2
  26. divi/{reporter.py → reporting/_reporter.py} +8 -14
  27. divi/utils.py +49 -10
  28. {qoro_divi-0.3.3.dist-info → qoro_divi-0.3.5.dist-info}/METADATA +2 -1
  29. qoro_divi-0.3.5.dist-info/RECORD +69 -0
  30. divi/_pbar.py +0 -70
  31. divi/circuits.py +0 -139
  32. divi/interfaces.py +0 -25
  33. divi/qoro_service.py +0 -425
  34. qoro_divi-0.3.3.dist-info/RECORD +0 -62
  35. /divi/{qpu_system.py → backends/_qpu_system.py} +0 -0
  36. /divi/{qem.py → circuits/qem.py} +0 -0
  37. /divi/{exp → extern}/cirq/__init__.py +0 -0
  38. /divi/{exp → extern}/cirq/_lexer.py +0 -0
  39. /divi/{exp → extern}/cirq/_parser.py +0 -0
  40. /divi/{exp → extern}/cirq/_qasm_export.py +0 -0
  41. /divi/{exp → extern}/cirq/_qasm_import.py +0 -0
  42. /divi/{exp → extern}/cirq/exception.py +0 -0
  43. /divi/{exp → extern}/scipy/_cobyla.py +0 -0
  44. /divi/{exp → extern}/scipy/pyprima/LICENCE.txt +0 -0
  45. /divi/{exp → extern}/scipy/pyprima/__init__.py +0 -0
  46. /divi/{exp → extern}/scipy/pyprima/cobyla/__init__.py +0 -0
  47. /divi/{exp → extern}/scipy/pyprima/cobyla/cobyla.py +0 -0
  48. /divi/{exp → extern}/scipy/pyprima/cobyla/cobylb.py +0 -0
  49. /divi/{exp → extern}/scipy/pyprima/cobyla/geometry.py +0 -0
  50. /divi/{exp → extern}/scipy/pyprima/cobyla/initialize.py +0 -0
  51. /divi/{exp → extern}/scipy/pyprima/cobyla/trustregion.py +0 -0
  52. /divi/{exp → extern}/scipy/pyprima/cobyla/update.py +0 -0
  53. /divi/{exp → extern}/scipy/pyprima/common/__init__.py +0 -0
  54. /divi/{exp → extern}/scipy/pyprima/common/_bounds.py +0 -0
  55. /divi/{exp → extern}/scipy/pyprima/common/_linear_constraints.py +0 -0
  56. /divi/{exp → extern}/scipy/pyprima/common/_nonlinear_constraints.py +0 -0
  57. /divi/{exp → extern}/scipy/pyprima/common/_project.py +0 -0
  58. /divi/{exp → extern}/scipy/pyprima/common/checkbreak.py +0 -0
  59. /divi/{exp → extern}/scipy/pyprima/common/consts.py +0 -0
  60. /divi/{exp → extern}/scipy/pyprima/common/evaluate.py +0 -0
  61. /divi/{exp → extern}/scipy/pyprima/common/history.py +0 -0
  62. /divi/{exp → extern}/scipy/pyprima/common/infos.py +0 -0
  63. /divi/{exp → extern}/scipy/pyprima/common/linalg.py +0 -0
  64. /divi/{exp → extern}/scipy/pyprima/common/message.py +0 -0
  65. /divi/{exp → extern}/scipy/pyprima/common/powalg.py +0 -0
  66. /divi/{exp → extern}/scipy/pyprima/common/preproc.py +0 -0
  67. /divi/{exp → extern}/scipy/pyprima/common/present.py +0 -0
  68. /divi/{exp → extern}/scipy/pyprima/common/ratio.py +0 -0
  69. /divi/{exp → extern}/scipy/pyprima/common/redrho.py +0 -0
  70. /divi/{exp → extern}/scipy/pyprima/common/selectx.py +0 -0
  71. {qoro_divi-0.3.3.dist-info → qoro_divi-0.3.5.dist-info}/LICENSE +0 -0
  72. {qoro_divi-0.3.3.dist-info → qoro_divi-0.3.5.dist-info}/LICENSES/.license-header +0 -0
  73. {qoro_divi-0.3.3.dist-info → qoro_divi-0.3.5.dist-info}/LICENSES/Apache-2.0.txt +0 -0
  74. {qoro_divi-0.3.3.dist-info → qoro_divi-0.3.5.dist-info}/WHEEL +0 -0
divi/circuits/_core.py ADDED
@@ -0,0 +1,226 @@
1
+ # SPDX-FileCopyrightText: 2025 Qoro Quantum Ltd <divi@qoroquantum.de>
2
+ #
3
+ # SPDX-License-Identifier: Apache-2.0
4
+
5
+ import re
6
+ from copy import deepcopy
7
+ from itertools import product
8
+ from typing import Literal
9
+
10
+ import dill
11
+ import pennylane as qml
12
+ from pennylane.transforms.core.transform_program import TransformProgram
13
+
14
+ from divi.circuits.qasm import to_openqasm
15
+ from divi.circuits.qem import QEMProtocol
16
+
17
+ TRANSFORM_PROGRAM = TransformProgram()
18
+ TRANSFORM_PROGRAM.add_transform(qml.transforms.split_to_single_terms)
19
+ TRANSFORM_PROGRAM.add_transform(qml.transforms.split_non_commuting)
20
+
21
+
22
+ class Circuit:
23
+ """
24
+ Represents a quantum circuit with its QASM representation and metadata.
25
+
26
+ This class encapsulates a PennyLane quantum circuit along with its OpenQASM
27
+ serialization and associated tags for identification. Each circuit instance
28
+ is assigned a unique ID for tracking purposes.
29
+
30
+ Attributes:
31
+ main_circuit: The PennyLane quantum circuit/tape object.
32
+ tags (list[str]): List of string tags for circuit identification.
33
+ qasm_circuits (list[str]): List of OpenQASM string representations.
34
+ circuit_id (int): Unique identifier for this circuit instance.
35
+ """
36
+
37
+ _id_counter = 0
38
+
39
+ def __init__(
40
+ self,
41
+ main_circuit,
42
+ tags: list[str],
43
+ qasm_circuits: list[str] = None,
44
+ ):
45
+ """
46
+ Initialize a Circuit instance.
47
+
48
+ Args:
49
+ main_circuit: A PennyLane quantum circuit or tape object to be wrapped.
50
+ tags (list[str]): List of string tags for identifying this circuit.
51
+ qasm_circuits (list[str], optional): Pre-computed OpenQASM string
52
+ representations. If None, they will be generated from main_circuit.
53
+ Defaults to None.
54
+ """
55
+ self.main_circuit = main_circuit
56
+ self.tags = tags
57
+
58
+ self.qasm_circuits = qasm_circuits
59
+
60
+ if self.qasm_circuits is None:
61
+ self.qasm_circuits = to_openqasm(
62
+ self.main_circuit,
63
+ measurement_groups=[self.main_circuit.measurements],
64
+ return_measurements_separately=False,
65
+ )
66
+
67
+ self.circuit_id = Circuit._id_counter
68
+ Circuit._id_counter += 1
69
+
70
+ def __str__(self):
71
+ """
72
+ Return a string representation of the circuit.
73
+
74
+ Returns:
75
+ str: String in format "Circuit: {circuit_id}".
76
+ """
77
+ return f"Circuit: {self.circuit_id}"
78
+
79
+
80
+ class MetaCircuit:
81
+ """
82
+ A parameterized quantum circuit template for batch circuit generation.
83
+
84
+ MetaCircuit represents a symbolic quantum circuit that can be instantiated
85
+ multiple times with different parameter values. It handles circuit compilation,
86
+ observable grouping, and measurement decomposition for efficient execution.
87
+
88
+ Attributes:
89
+ main_circuit: The PennyLane quantum circuit with symbolic parameters.
90
+ symbols: Array of sympy symbols used as circuit parameters.
91
+ qem_protocol (QEMProtocol): Quantum error mitigation protocol to apply.
92
+ compiled_circuits_bodies (list[str]): QASM bodies without measurements.
93
+ measurements (list[str]): QASM measurement strings.
94
+ measurement_groups (list[list]): Grouped observables for each circuit variant.
95
+ postprocessing_fn: Function to combine measurement results.
96
+ """
97
+
98
+ def __init__(
99
+ self,
100
+ main_circuit,
101
+ symbols,
102
+ grouping_strategy: Literal["wires", "default", "qwc"] | None = None,
103
+ qem_protocol: QEMProtocol | None = None,
104
+ ):
105
+ """
106
+ Initialize a MetaCircuit with symbolic parameters.
107
+
108
+ Args:
109
+ main_circuit: A PennyLane quantum circuit/tape with symbolic parameters.
110
+ symbols: Array of sympy Symbol objects representing circuit parameters.
111
+ grouping_strategy (str, optional): Strategy for grouping commuting
112
+ observables. Options are "wires", "default", or "qwc" (qubit-wise
113
+ commuting). Defaults to None.
114
+ qem_protocol (QEMProtocol, optional): Quantum error mitigation protocol
115
+ to apply to the circuits. Defaults to None.
116
+ """
117
+ self.main_circuit = main_circuit
118
+ self.symbols = symbols
119
+ self.qem_protocol = qem_protocol
120
+
121
+ transform_program = deepcopy(TRANSFORM_PROGRAM)
122
+ transform_program[1].kwargs["grouping_strategy"] = grouping_strategy
123
+
124
+ qscripts, self.postprocessing_fn = transform_program((main_circuit,))
125
+
126
+ self.compiled_circuits_bodies, self.measurements = to_openqasm(
127
+ main_circuit,
128
+ measurement_groups=[qsc.measurements for qsc in qscripts],
129
+ return_measurements_separately=True,
130
+ # TODO: optimize later
131
+ measure_all=True,
132
+ symbols=self.symbols,
133
+ qem_protocol=qem_protocol,
134
+ )
135
+
136
+ # Need to store the measurement groups for computing
137
+ # expectation values later on, stripped of the `qml.expval` wrapper
138
+ self.measurement_groups = [
139
+ [meas.obs for meas in qsc.measurements] for qsc in qscripts
140
+ ]
141
+
142
+ def __getstate__(self):
143
+ """
144
+ Prepare the MetaCircuit for pickling.
145
+
146
+ Serializes the postprocessing function using dill since regular pickle
147
+ cannot handle certain PennyLane function objects.
148
+
149
+ Returns:
150
+ dict: State dictionary with serialized postprocessing function.
151
+ """
152
+ state = self.__dict__.copy()
153
+ state["postprocessing_fn"] = dill.dumps(self.postprocessing_fn)
154
+ return state
155
+
156
+ def __setstate__(self, state):
157
+ """
158
+ Restore the MetaCircuit from a pickled state.
159
+
160
+ Deserializes the postprocessing function that was serialized with dill
161
+ during pickling.
162
+
163
+ Args:
164
+ state (dict): State dictionary from pickling with serialized
165
+ postprocessing function.
166
+ """
167
+ state["postprocessing_fn"] = dill.loads(state["postprocessing_fn"])
168
+
169
+ self.__dict__.update(state)
170
+
171
+ def initialize_circuit_from_params(
172
+ self, param_list, tag_prefix: str = "", precision: int = 8
173
+ ) -> Circuit:
174
+ """
175
+ Instantiate a concrete Circuit by substituting symbolic parameters with values.
176
+
177
+ Takes a list of parameter values and creates a fully instantiated Circuit
178
+ by replacing all symbolic parameters in the QASM representations with their
179
+ concrete numerical values.
180
+
181
+ Args:
182
+ param_list: Array of numerical parameter values to substitute for symbols.
183
+ Must match the length and order of self.symbols.
184
+ tag_prefix (str, optional): Prefix to prepend to circuit tags for
185
+ identification. Defaults to "".
186
+ precision (int, optional): Number of decimal places for parameter values
187
+ in the QASM output. Defaults to 8.
188
+
189
+ Returns:
190
+ Circuit: A new Circuit instance with parameters substituted and proper
191
+ tags for identification.
192
+
193
+ Note:
194
+ The main_circuit attribute in the returned Circuit still contains
195
+ symbolic parameters. Only the QASM representations have concrete values.
196
+ """
197
+ mapping = dict(
198
+ zip(
199
+ map(lambda x: re.escape(str(x)), self.symbols),
200
+ map(lambda x: f"{x:.{precision}f}", param_list),
201
+ )
202
+ )
203
+ pattern = re.compile("|".join(k for k in mapping.keys()))
204
+
205
+ final_qasm_strs = []
206
+ for circuit_body in self.compiled_circuits_bodies:
207
+ final_qasm_strs.append(
208
+ pattern.sub(lambda match: mapping[match.group(0)], circuit_body)
209
+ )
210
+
211
+ tags = []
212
+ qasm_circuits = []
213
+ for (i, body_str), (j, meas_str) in product(
214
+ enumerate(final_qasm_strs), enumerate(self.measurements)
215
+ ):
216
+ qasm_circuits.append(body_str + meas_str)
217
+
218
+ nonempty_subtags = filter(
219
+ None,
220
+ [tag_prefix, f"{self.qem_protocol.name}:{i}", str(j)],
221
+ )
222
+ tags.append("_".join(nonempty_subtags))
223
+
224
+ # Note: The main circuit's parameters are still in symbol form.
225
+ # Not sure if it is necessary for any useful application to parameterize them.
226
+ return Circuit(self.main_circuit, qasm_circuits=qasm_circuits, tags=tags)
@@ -14,8 +14,8 @@ from pennylane.tape import QuantumScript
14
14
  from pennylane.wires import Wires
15
15
  from sympy import Symbol
16
16
 
17
- from divi.exp.cirq import cirq_circuit_from_qasm
18
- from divi.qem import QEMProtocol
17
+ from divi.circuits.qem import QEMProtocol
18
+ from divi.extern.cirq import cirq_circuit_from_qasm
19
19
 
20
20
  OPENQASM_GATES = {
21
21
  "CNOT": "cx",
@@ -46,6 +46,25 @@ OPENQASM_GATES = {
46
46
 
47
47
 
48
48
  def _ops_to_qasm(operations, precision, wires):
49
+ """
50
+ Convert PennyLane operations to OpenQASM instruction strings.
51
+
52
+ Translates a sequence of PennyLane quantum operations into their OpenQASM
53
+ 2.0 equivalent representations. Each operation is mapped to its corresponding
54
+ QASM gate with appropriate parameters and wire labels.
55
+
56
+ Args:
57
+ operations: Sequence of PennyLane operation objects to convert.
58
+ precision (int | None): Number of decimal places for parameter values.
59
+ If None, uses default Python string formatting.
60
+ wires: Wire labels used in the circuit for indexing.
61
+
62
+ Returns:
63
+ str: OpenQASM instruction string with each operation on a new line.
64
+
65
+ Raises:
66
+ ValueError: If an operation is not supported by the QASM serializer.
67
+ """
49
68
  # create the QASM code representing the operations
50
69
  qasm_str = ""
51
70
 
@@ -289,7 +289,12 @@ class Parser:
289
289
  # ---- gate definitions ----
290
290
  def gate_def(self):
291
291
  self.match("GATE")
292
- gname = self.match("ID").value
292
+ gname_tok = self.match("ID")
293
+ gname = gname_tok.value
294
+ if gname in BUILTINS:
295
+ raise SyntaxError(
296
+ f"Cannot redefine built-in gate '{gname}' at {gname_tok.line}:{gname_tok.col}"
297
+ )
293
298
  if gname in self.user_gates:
294
299
  self._dupe(gname)
295
300
  params: tuple[str, ...] = ()
@@ -570,7 +575,6 @@ class Parser:
570
575
  if t.type == "PI":
571
576
  self.match("PI")
572
577
  return
573
- # ---- NEW BLOCK TO HANDLE MATH FUNCTIONS ----
574
578
  if t.type in _MATH_FUNCS:
575
579
  self.match(t.type) # Consume the function name (e.g., COS)
576
580
  self.match("LPAREN")
@@ -578,13 +582,11 @@ class Parser:
578
582
  # Note: QASM 2.0 math functions only take one argument
579
583
  self.match("RPAREN")
580
584
  return
581
- # --------------------------------------------
582
585
  if t.type == "ID":
583
586
  # function call or plain ID
584
587
  id_tok = self.match("ID")
585
588
  ident = id_tok.value
586
589
  if self.accept("LPAREN"):
587
- # This now correctly handles user-defined functions (if any)
588
590
  if self.peek().type != "RPAREN":
589
591
  self._expr(allow_id)
590
592
  while self.accept("COMMA"):
@@ -637,9 +639,9 @@ def validate_qasm_raise(src: str) -> None:
637
639
  Parser(toks).parse()
638
640
 
639
641
 
640
- def is_valid_qasm(src: str) -> bool:
642
+ def is_valid_qasm(src: str) -> bool | str:
641
643
  try:
642
644
  validate_qasm_raise(src)
643
645
  return True
644
- except SyntaxError:
645
- return False
646
+ except SyntaxError as e:
647
+ return str(e)
divi/qprog/__init__.py CHANGED
@@ -5,9 +5,22 @@
5
5
  # isort: skip_file
6
6
  from .quantum_program import QuantumProgram
7
7
  from .batch import ProgramBatch
8
- from ._qaoa import QAOA, GraphProblem
9
- from ._vqe import VQE, VQEAnsatz
10
- from ._graph_partitioning import GraphPartitioningQAOA, PartitioningConfig
11
- from ._qubo_partitioning import QUBOPartitioningQAOA
12
- from ._vqe_sweep import VQEHyperparameterSweep, MoleculeTransformer
8
+ from .algorithms import (
9
+ QAOA,
10
+ GraphProblem,
11
+ VQE,
12
+ Ansatz,
13
+ UCCSDAnsatz,
14
+ QAOAAnsatz,
15
+ HardwareEfficientAnsatz,
16
+ HartreeFockAnsatz,
17
+ GenericLayerAnsatz,
18
+ )
19
+ from .workflows import (
20
+ GraphPartitioningQAOA,
21
+ PartitioningConfig,
22
+ QUBOPartitioningQAOA,
23
+ VQEHyperparameterSweep,
24
+ MoleculeTransformer,
25
+ )
13
26
  from .optimizers import ScipyOptimizer, ScipyMethod, MonteCarloOptimizer
@@ -0,0 +1,14 @@
1
+ # SPDX-FileCopyrightText: 2025 Qoro Quantum Ltd <divi@qoroquantum.de>
2
+ #
3
+ # SPDX-License-Identifier: Apache-2.0
4
+
5
+ from ._ansatze import (
6
+ Ansatz,
7
+ GenericLayerAnsatz,
8
+ HardwareEfficientAnsatz,
9
+ HartreeFockAnsatz,
10
+ QAOAAnsatz,
11
+ UCCSDAnsatz,
12
+ )
13
+ from ._qaoa import QAOA, GraphProblem, GraphProblemTypes, QUBOProblemTypes
14
+ from ._vqe import VQE
@@ -0,0 +1,311 @@
1
+ # SPDX-FileCopyrightText: 2025 Qoro Quantum Ltd <divi@qoroquantum.de>
2
+ #
3
+ # SPDX-License-Identifier: Apache-2.0
4
+
5
+ from abc import ABC, abstractmethod
6
+ from itertools import tee
7
+ from typing import Literal, Sequence
8
+ from warnings import warn
9
+
10
+ import pennylane as qml
11
+
12
+
13
+ class Ansatz(ABC):
14
+ """Abstract base class for all VQE ansaetze."""
15
+
16
+ @property
17
+ def name(self) -> str:
18
+ """Returns the human-readable name of the ansatz."""
19
+ return self.__class__.__name__
20
+
21
+ @staticmethod
22
+ @abstractmethod
23
+ def n_params_per_layer(n_qubits: int, **kwargs) -> int:
24
+ """Returns the number of parameters required by the ansatz for one layer."""
25
+ raise NotImplementedError
26
+
27
+ @abstractmethod
28
+ def build(self, params, n_qubits: int, n_layers: int, **kwargs):
29
+ """
30
+ Builds the ansatz circuit.
31
+
32
+ Args:
33
+ params (array): The parameters (weights) for the ansatz.
34
+ n_qubits (int): The number of qubits.
35
+ n_layers (int): The number of layers.
36
+ **kwargs: Additional arguments like n_electrons for chemistry ansaetze.
37
+ """
38
+ raise NotImplementedError
39
+
40
+
41
+ # --- Template Ansaetze ---
42
+
43
+
44
+ class GenericLayerAnsatz(Ansatz):
45
+ """
46
+ A flexible ansatz alternating single-qubit gates with optional entanglers.
47
+ """
48
+
49
+ def __init__(
50
+ self,
51
+ gate_sequence: list[qml.operation.Operator],
52
+ entangler: qml.operation.Operator | None = None,
53
+ entangling_layout: (
54
+ Literal["linear", "brick", "circular", "all-to-all"]
55
+ | Sequence[tuple[int, int]]
56
+ | None
57
+ ) = None,
58
+ ):
59
+ """
60
+ Args:
61
+ gate_sequence (list[Callable]): List of one-qubit gate classes (e.g., qml.RY, qml.Rot).
62
+ entangler (Callable): Two-qubit entangling gate class (e.g., qml.CNOT, qml.CZ).
63
+ If None, no entanglement is applied.
64
+ entangling_layout (str): Layout for entangling layer ("linear", "all_to_all", etc.).
65
+ """
66
+ if not all(
67
+ issubclass(g, qml.operation.Operator) and g.num_wires == 1
68
+ for g in gate_sequence
69
+ ):
70
+ raise ValueError(
71
+ "All elements in gate_sequence must be PennyLane one-qubit gate classes."
72
+ )
73
+ self.gate_sequence = gate_sequence
74
+
75
+ if entangler not in (None, qml.CNOT, qml.CZ):
76
+ raise ValueError("Only qml.CNOT and qml.CZ are supported as entanglers.")
77
+ self.entangler = entangler
78
+
79
+ self.entangling_layout = entangling_layout
80
+ if entangler is None and self.entangling_layout is not None:
81
+ warn("`entangling_layout` provided but `entangler` is None.")
82
+ match self.entangling_layout:
83
+ case None | "linear":
84
+ self.entangling_layout = "linear"
85
+
86
+ self._layout_fn = lambda n_qubits: zip(
87
+ range(n_qubits), range(1, n_qubits)
88
+ )
89
+ case "brick":
90
+ self._layout_fn = lambda n_qubits: [
91
+ (i, i + 1) for r in range(2) for i in range(r, n_qubits - 1, 2)
92
+ ]
93
+ case "circular":
94
+ self._layout_fn = lambda n_qubits: zip(
95
+ range(n_qubits), [(i + 1) % n_qubits for i in range(n_qubits)]
96
+ )
97
+ case "all_to_all":
98
+ self._layout_fn = lambda n_qubits: (
99
+ (i, j) for i in range(n_qubits) for j in range(i + 1, n_qubits)
100
+ )
101
+ case _:
102
+ if not all(
103
+ isinstance(ent, tuple)
104
+ and len(ent) == 2
105
+ and isinstance(ent[0], int)
106
+ and isinstance(ent[1], int)
107
+ for ent in entangling_layout
108
+ ):
109
+ raise ValueError(
110
+ "entangling_layout must be 'linear', 'circular', "
111
+ "'all_to_all', or a Sequence of tuples of integers."
112
+ )
113
+
114
+ self._layout_fn = lambda _: entangling_layout
115
+
116
+ def n_params_per_layer(self, n_qubits: int, **kwargs) -> int:
117
+ """Total parameters = sum of gate.num_params per qubit per layer."""
118
+ per_qubit = sum(getattr(g, "num_params", 1) for g in self.gate_sequence)
119
+ return per_qubit * n_qubits
120
+
121
+ def build(self, params, n_qubits: int, n_layers: int, **kwargs):
122
+ # calculate how many params each gate needs per qubit
123
+ gate_param_counts = [getattr(g, "num_params", 1) for g in self.gate_sequence]
124
+ per_qubit = sum(gate_param_counts)
125
+
126
+ # reshape into [layers, qubits, per_qubit]
127
+ params = params.reshape(n_layers, n_qubits, per_qubit)
128
+ layout_gen = iter(tee(self._layout_fn(n_qubits), n_layers))
129
+
130
+ def _layer(layer_params, wires):
131
+ for w, qubit_params in zip(wires, layer_params):
132
+ idx = 0
133
+ for gate, n_p in zip(self.gate_sequence, gate_param_counts):
134
+ theta = qubit_params[idx : idx + n_p]
135
+ gate(*theta, wires=w)
136
+ idx += n_p
137
+
138
+ if self.entangler is not None:
139
+ for wire_a, wire_b in next(layout_gen):
140
+ self.entangler(wires=[wire_a, wire_b])
141
+
142
+ qml.layer(_layer, n_layers, params, wires=range(n_qubits))
143
+
144
+
145
+ class QAOAAnsatz(Ansatz):
146
+ """
147
+ QAOA-style ansatz using PennyLane's QAOAEmbedding.
148
+
149
+ Implements a parameterized ansatz based on the Quantum Approximate Optimization
150
+ Algorithm structure, alternating between problem and mixer Hamiltonians.
151
+ """
152
+
153
+ @staticmethod
154
+ def n_params_per_layer(n_qubits: int, **kwargs) -> int:
155
+ """
156
+ Calculate the number of parameters per layer for QAOA ansatz.
157
+
158
+ Args:
159
+ n_qubits (int): Number of qubits in the circuit.
160
+ **kwargs: Additional unused arguments.
161
+
162
+ Returns:
163
+ int: Number of parameters needed per layer.
164
+ """
165
+ return qml.QAOAEmbedding.shape(n_layers=1, n_wires=n_qubits)[1]
166
+
167
+ def build(self, params, n_qubits: int, n_layers: int, **kwargs):
168
+ """
169
+ Build the QAOA ansatz circuit.
170
+
171
+ Args:
172
+ params: Parameter array to use for the ansatz.
173
+ n_qubits (int): Number of qubits.
174
+ n_layers (int): Number of QAOA layers.
175
+ **kwargs: Additional unused arguments.
176
+ """
177
+ qml.QAOAEmbedding(
178
+ features=[],
179
+ weights=params.reshape(n_layers, -1),
180
+ wires=range(n_qubits),
181
+ )
182
+
183
+
184
+ class HardwareEfficientAnsatz(Ansatz):
185
+ """
186
+ Hardware-efficient ansatz (not yet implemented).
187
+
188
+ This ansatz is designed to be easily implementable on near-term quantum hardware,
189
+ typically using native gate sets and connectivity patterns.
190
+
191
+ Note:
192
+ This class is a placeholder for future implementation.
193
+ """
194
+
195
+ @staticmethod
196
+ def n_params_per_layer(n_qubits: int, **kwargs) -> int:
197
+ """Not yet implemented."""
198
+ raise NotImplementedError("HardwareEfficientAnsatz is not yet implemented.")
199
+
200
+ def build(self, params, n_qubits: int, n_layers: int, **kwargs) -> None:
201
+ """Not yet implemented."""
202
+ raise NotImplementedError("HardwareEfficientAnsatz is not yet implemented.")
203
+
204
+
205
+ # --- Chemistry Ansaetze ---
206
+
207
+
208
+ class UCCSDAnsatz(Ansatz):
209
+ """
210
+ Unitary Coupled Cluster Singles and Doubles (UCCSD) ansatz.
211
+
212
+ This ansatz is specifically designed for quantum chemistry calculations,
213
+ implementing the UCCSD approximation which includes all single and double
214
+ electron excitations from a reference state.
215
+ """
216
+
217
+ @staticmethod
218
+ def n_params_per_layer(n_qubits: int, n_electrons: int, **kwargs) -> int:
219
+ """
220
+ Calculate the number of parameters per layer for UCCSD ansatz.
221
+
222
+ Args:
223
+ n_qubits (int): Number of qubits in the circuit.
224
+ n_electrons (int): Number of electrons in the system.
225
+ **kwargs: Additional unused arguments.
226
+
227
+ Returns:
228
+ int: Number of parameters (number of single + double excitations).
229
+ """
230
+ singles, doubles = qml.qchem.excitations(n_electrons, n_qubits)
231
+ s_wires, d_wires = qml.qchem.excitations_to_wires(singles, doubles)
232
+ return len(s_wires) + len(d_wires)
233
+
234
+ def build(self, params, n_qubits: int, n_layers: int, n_electrons: int, **kwargs):
235
+ """
236
+ Build the UCCSD ansatz circuit.
237
+
238
+ Args:
239
+ params: Parameter array for excitation amplitudes.
240
+ n_qubits (int): Number of qubits.
241
+ n_layers (int): Number of UCCSD layers (repeats).
242
+ n_electrons (int): Number of electrons in the system.
243
+ **kwargs: Additional unused arguments.
244
+ """
245
+ singles, doubles = qml.qchem.excitations(n_electrons, n_qubits)
246
+ s_wires, d_wires = qml.qchem.excitations_to_wires(singles, doubles)
247
+ hf_state = qml.qchem.hf_state(n_electrons, n_qubits)
248
+
249
+ qml.UCCSD(
250
+ params.reshape(n_layers, -1),
251
+ wires=range(n_qubits),
252
+ s_wires=s_wires,
253
+ d_wires=d_wires,
254
+ init_state=hf_state,
255
+ n_repeats=n_layers,
256
+ )
257
+
258
+
259
+ class HartreeFockAnsatz(Ansatz):
260
+ """
261
+ Hartree-Fock-based ansatz for quantum chemistry.
262
+
263
+ This ansatz prepares the Hartree-Fock reference state and applies
264
+ parameterized single and double excitation gates. It's a simplified
265
+ alternative to UCCSD, often used as a starting point for VQE calculations.
266
+ """
267
+
268
+ @staticmethod
269
+ def n_params_per_layer(n_qubits: int, n_electrons: int, **kwargs) -> int:
270
+ """
271
+ Calculate the number of parameters per layer for Hartree-Fock ansatz.
272
+
273
+ Args:
274
+ n_qubits (int): Number of qubits in the circuit.
275
+ n_electrons (int): Number of electrons in the system.
276
+ **kwargs: Additional unused arguments.
277
+
278
+ Returns:
279
+ int: Number of parameters (number of single + double excitations).
280
+ """
281
+ singles, doubles = qml.qchem.excitations(n_electrons, n_qubits)
282
+ return len(singles) + len(doubles)
283
+
284
+ def build(self, params, n_qubits: int, n_layers: int, n_electrons: int, **kwargs):
285
+ """
286
+ Build the Hartree-Fock ansatz circuit.
287
+
288
+ Args:
289
+ params: Parameter array for excitation amplitudes.
290
+ n_qubits (int): Number of qubits.
291
+ n_layers (int): Number of ansatz layers.
292
+ n_electrons (int): Number of electrons in the system.
293
+ **kwargs: Additional unused arguments.
294
+ """
295
+ singles, doubles = qml.qchem.excitations(n_electrons, n_qubits)
296
+ hf_state = qml.qchem.hf_state(n_electrons, n_qubits)
297
+
298
+ qml.layer(
299
+ qml.AllSinglesDoubles,
300
+ n_layers,
301
+ params.reshape(n_layers, -1),
302
+ wires=range(n_qubits),
303
+ hf_state=hf_state,
304
+ singles=singles,
305
+ doubles=doubles,
306
+ )
307
+
308
+ # Reset the BasisState operations after the first layer
309
+ # for behaviour similar to UCCSD ansatz
310
+ for op in qml.QueuingManager.active_context().queue[1:]:
311
+ op._hyperparameters["hf_state"] = 0