qoro-divi 0.2.0b1__py3-none-any.whl → 0.6.0__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (92) hide show
  1. divi/__init__.py +1 -2
  2. divi/backends/__init__.py +10 -0
  3. divi/backends/_backend_properties_conversion.py +227 -0
  4. divi/backends/_circuit_runner.py +70 -0
  5. divi/backends/_execution_result.py +70 -0
  6. divi/backends/_parallel_simulator.py +486 -0
  7. divi/backends/_qoro_service.py +663 -0
  8. divi/backends/_qpu_system.py +101 -0
  9. divi/backends/_results_processing.py +133 -0
  10. divi/circuits/__init__.py +13 -0
  11. divi/{exp/cirq → circuits/_cirq}/__init__.py +1 -2
  12. divi/circuits/_cirq/_parser.py +110 -0
  13. divi/circuits/_cirq/_qasm_export.py +78 -0
  14. divi/circuits/_core.py +391 -0
  15. divi/{qasm.py → circuits/_qasm_conversion.py} +73 -14
  16. divi/circuits/_qasm_validation.py +694 -0
  17. divi/qprog/__init__.py +27 -8
  18. divi/qprog/_expectation.py +181 -0
  19. divi/qprog/_hamiltonians.py +281 -0
  20. divi/qprog/algorithms/__init__.py +16 -0
  21. divi/qprog/algorithms/_ansatze.py +368 -0
  22. divi/qprog/algorithms/_custom_vqa.py +263 -0
  23. divi/qprog/algorithms/_pce.py +262 -0
  24. divi/qprog/algorithms/_qaoa.py +579 -0
  25. divi/qprog/algorithms/_vqe.py +262 -0
  26. divi/qprog/batch.py +387 -74
  27. divi/qprog/checkpointing.py +556 -0
  28. divi/qprog/exceptions.py +9 -0
  29. divi/qprog/optimizers.py +1014 -43
  30. divi/qprog/quantum_program.py +243 -412
  31. divi/qprog/typing.py +62 -0
  32. divi/qprog/variational_quantum_algorithm.py +1208 -0
  33. divi/qprog/workflows/__init__.py +10 -0
  34. divi/qprog/{_graph_partitioning.py → workflows/_graph_partitioning.py} +139 -95
  35. divi/qprog/workflows/_qubo_partitioning.py +221 -0
  36. divi/qprog/workflows/_vqe_sweep.py +560 -0
  37. divi/reporting/__init__.py +7 -0
  38. divi/reporting/_pbar.py +127 -0
  39. divi/reporting/_qlogger.py +68 -0
  40. divi/reporting/_reporter.py +155 -0
  41. {qoro_divi-0.2.0b1.dist-info → qoro_divi-0.6.0.dist-info}/METADATA +43 -15
  42. qoro_divi-0.6.0.dist-info/RECORD +47 -0
  43. {qoro_divi-0.2.0b1.dist-info → qoro_divi-0.6.0.dist-info}/WHEEL +1 -1
  44. qoro_divi-0.6.0.dist-info/licenses/LICENSES/.license-header +3 -0
  45. divi/_pbar.py +0 -73
  46. divi/circuits.py +0 -139
  47. divi/exp/cirq/_lexer.py +0 -126
  48. divi/exp/cirq/_parser.py +0 -889
  49. divi/exp/cirq/_qasm_export.py +0 -37
  50. divi/exp/cirq/_qasm_import.py +0 -35
  51. divi/exp/cirq/exception.py +0 -21
  52. divi/exp/scipy/_cobyla.py +0 -342
  53. divi/exp/scipy/pyprima/LICENCE.txt +0 -28
  54. divi/exp/scipy/pyprima/__init__.py +0 -263
  55. divi/exp/scipy/pyprima/cobyla/__init__.py +0 -0
  56. divi/exp/scipy/pyprima/cobyla/cobyla.py +0 -599
  57. divi/exp/scipy/pyprima/cobyla/cobylb.py +0 -849
  58. divi/exp/scipy/pyprima/cobyla/geometry.py +0 -240
  59. divi/exp/scipy/pyprima/cobyla/initialize.py +0 -269
  60. divi/exp/scipy/pyprima/cobyla/trustregion.py +0 -540
  61. divi/exp/scipy/pyprima/cobyla/update.py +0 -331
  62. divi/exp/scipy/pyprima/common/__init__.py +0 -0
  63. divi/exp/scipy/pyprima/common/_bounds.py +0 -41
  64. divi/exp/scipy/pyprima/common/_linear_constraints.py +0 -46
  65. divi/exp/scipy/pyprima/common/_nonlinear_constraints.py +0 -64
  66. divi/exp/scipy/pyprima/common/_project.py +0 -224
  67. divi/exp/scipy/pyprima/common/checkbreak.py +0 -107
  68. divi/exp/scipy/pyprima/common/consts.py +0 -48
  69. divi/exp/scipy/pyprima/common/evaluate.py +0 -101
  70. divi/exp/scipy/pyprima/common/history.py +0 -39
  71. divi/exp/scipy/pyprima/common/infos.py +0 -30
  72. divi/exp/scipy/pyprima/common/linalg.py +0 -452
  73. divi/exp/scipy/pyprima/common/message.py +0 -336
  74. divi/exp/scipy/pyprima/common/powalg.py +0 -131
  75. divi/exp/scipy/pyprima/common/preproc.py +0 -393
  76. divi/exp/scipy/pyprima/common/present.py +0 -5
  77. divi/exp/scipy/pyprima/common/ratio.py +0 -56
  78. divi/exp/scipy/pyprima/common/redrho.py +0 -49
  79. divi/exp/scipy/pyprima/common/selectx.py +0 -346
  80. divi/interfaces.py +0 -25
  81. divi/parallel_simulator.py +0 -258
  82. divi/qlogger.py +0 -119
  83. divi/qoro_service.py +0 -343
  84. divi/qprog/_mlae.py +0 -182
  85. divi/qprog/_qaoa.py +0 -440
  86. divi/qprog/_vqe.py +0 -275
  87. divi/qprog/_vqe_sweep.py +0 -144
  88. divi/utils.py +0 -116
  89. qoro_divi-0.2.0b1.dist-info/RECORD +0 -58
  90. /divi/{qem.py → circuits/qem.py} +0 -0
  91. {qoro_divi-0.2.0b1.dist-info → qoro_divi-0.6.0.dist-info/licenses}/LICENSE +0 -0
  92. {qoro_divi-0.2.0b1.dist-info → qoro_divi-0.6.0.dist-info/licenses}/LICENSES/Apache-2.0.txt +0 -0
@@ -0,0 +1,368 @@
1
+ # SPDX-FileCopyrightText: 2025-2026 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
+ def _require_trainable_params(n_params: int, ansatz_name: str) -> int:
14
+ if n_params <= 0:
15
+ raise ValueError(
16
+ f"{ansatz_name} must define at least one trainable parameter. "
17
+ "Parameter-free circuits are not supported."
18
+ )
19
+ return n_params
20
+
21
+
22
+ class Ansatz(ABC):
23
+ """Abstract base class for all VQE ansätze."""
24
+
25
+ @property
26
+ def name(self) -> str:
27
+ """Returns the human-readable name of the ansatz."""
28
+ return self.__class__.__name__
29
+
30
+ @staticmethod
31
+ @abstractmethod
32
+ def n_params_per_layer(n_qubits: int, **kwargs) -> int:
33
+ """Returns the number of parameters required by the ansatz for one layer."""
34
+ raise NotImplementedError
35
+
36
+ @abstractmethod
37
+ def build(
38
+ self, params, n_qubits: int, n_layers: int, **kwargs
39
+ ) -> list[qml.operation.Operator]:
40
+ """
41
+ Builds the ansatz circuit and returns a list of operations.
42
+
43
+ Args:
44
+ params: Parameter array for the ansatz.
45
+ n_qubits (int): Number of qubits in the circuit.
46
+ n_layers (int): Number of ansatz layers.
47
+ **kwargs: Additional arguments specific to the ansatz.
48
+
49
+ Returns:
50
+ list[qml.operation.Operator]: List of PennyLane operations representing the ansatz.
51
+ """
52
+ raise NotImplementedError
53
+
54
+
55
+ # --- Template Ansaetze ---
56
+
57
+
58
+ class GenericLayerAnsatz(Ansatz):
59
+ """
60
+ A flexible ansatz alternating single-qubit gates with optional entanglers.
61
+ """
62
+
63
+ def __init__(
64
+ self,
65
+ gate_sequence: list[qml.operation.Operator],
66
+ entangler: qml.operation.Operator | None = None,
67
+ entangling_layout: (
68
+ Literal["linear", "brick", "circular", "all-to-all"]
69
+ | Sequence[tuple[int, int]]
70
+ | None
71
+ ) = None,
72
+ ):
73
+ """
74
+ Args:
75
+ gate_sequence (list[Callable]): List of one-qubit gate classes (e.g., qml.RY, qml.Rot).
76
+ entangler (Callable): Two-qubit entangling gate class (e.g., qml.CNOT, qml.CZ).
77
+ If None, no entanglement is applied.
78
+ entangling_layout (str): Layout for entangling layer ("linear", "all_to_all", etc.).
79
+ """
80
+ if not all(
81
+ issubclass(g, qml.operation.Operator) and g.num_wires == 1
82
+ for g in gate_sequence
83
+ ):
84
+ raise ValueError(
85
+ "All elements in gate_sequence must be PennyLane one-qubit gate classes."
86
+ )
87
+ self.gate_sequence = gate_sequence
88
+
89
+ if entangler not in (None, qml.CNOT, qml.CZ):
90
+ raise ValueError("Only qml.CNOT and qml.CZ are supported as entanglers.")
91
+ self.entangler = entangler
92
+
93
+ self.entangling_layout = entangling_layout
94
+ if entangler is None and self.entangling_layout is not None:
95
+ warn("`entangling_layout` provided but `entangler` is None.")
96
+ match self.entangling_layout:
97
+ case None | "linear":
98
+ self.entangling_layout = "linear"
99
+
100
+ self._layout_fn = lambda n_qubits: zip(
101
+ range(n_qubits), range(1, n_qubits)
102
+ )
103
+ case "brick":
104
+ self._layout_fn = lambda n_qubits: [
105
+ (i, i + 1) for r in range(2) for i in range(r, n_qubits - 1, 2)
106
+ ]
107
+ case "circular":
108
+ self._layout_fn = lambda n_qubits: zip(
109
+ range(n_qubits), [(i + 1) % n_qubits for i in range(n_qubits)]
110
+ )
111
+ case "all-to-all":
112
+ self._layout_fn = lambda n_qubits: (
113
+ (i, j) for i in range(n_qubits) for j in range(i + 1, n_qubits)
114
+ )
115
+ case _:
116
+ if not all(
117
+ isinstance(ent, tuple)
118
+ and len(ent) == 2
119
+ and isinstance(ent[0], int)
120
+ and isinstance(ent[1], int)
121
+ for ent in entangling_layout
122
+ ):
123
+ raise ValueError(
124
+ "entangling_layout must be 'linear', 'circular', "
125
+ "'all_to_all', or a Sequence of tuples of integers."
126
+ )
127
+
128
+ self._layout_fn = lambda _: entangling_layout
129
+
130
+ def n_params_per_layer(self, n_qubits: int, **kwargs) -> int:
131
+ """Total parameters = sum of gate.num_params per qubit per layer."""
132
+ per_qubit = sum(getattr(g, "num_params", 1) for g in self.gate_sequence)
133
+ return _require_trainable_params(per_qubit * n_qubits, self.name)
134
+
135
+ def build(
136
+ self, params, n_qubits: int, n_layers: int, **kwargs
137
+ ) -> list[qml.operation.Operator]:
138
+ # calculate how many params each gate needs per qubit
139
+ gate_param_counts = [getattr(g, "num_params", 1) for g in self.gate_sequence]
140
+ per_qubit = sum(gate_param_counts)
141
+
142
+ # reshape into [layers, qubits, per_qubit]
143
+ params = params.reshape(n_layers, n_qubits, per_qubit)
144
+ layout_gen = iter(tee(self._layout_fn(n_qubits), n_layers))
145
+
146
+ operations = []
147
+ wires = list(range(n_qubits))
148
+
149
+ for layer_idx in range(n_layers):
150
+ layer_params = params[layer_idx]
151
+ # Single-qubit gates
152
+ for w, qubit_params in zip(wires, layer_params):
153
+ idx = 0
154
+ for gate, n_p in zip(self.gate_sequence, gate_param_counts):
155
+ theta = qubit_params[idx : idx + n_p]
156
+ if n_p == 0:
157
+ op = gate(wires=w)
158
+ elif n_p == 1:
159
+ op = gate(theta[0], wires=w)
160
+ else:
161
+ op = gate(*theta, wires=w)
162
+ operations.append(op)
163
+ idx += n_p
164
+
165
+ # Entangling gates
166
+ if self.entangler is not None:
167
+ for wire_a, wire_b in next(layout_gen):
168
+ op = self.entangler(wires=[wire_a, wire_b])
169
+ operations.append(op)
170
+
171
+ return operations
172
+
173
+
174
+ class QAOAAnsatz(Ansatz):
175
+ """
176
+ QAOA-style ansatz using PennyLane's QAOAEmbedding.
177
+
178
+ Implements a parameterized ansatz based on the Quantum Approximate Optimization
179
+ Algorithm structure, alternating between problem and mixer Hamiltonians.
180
+ """
181
+
182
+ @staticmethod
183
+ def n_params_per_layer(n_qubits: int, **kwargs) -> int:
184
+ """
185
+ Calculate the number of parameters per layer for QAOA ansatz.
186
+
187
+ Args:
188
+ n_qubits (int): Number of qubits in the circuit.
189
+ **kwargs: Additional unused arguments.
190
+
191
+ Returns:
192
+ int: Number of parameters needed per layer.
193
+ """
194
+ n_params = qml.QAOAEmbedding.shape(n_layers=1, n_wires=n_qubits)[1]
195
+ return _require_trainable_params(n_params, QAOAAnsatz.__name__)
196
+
197
+ def build(
198
+ self, params, n_qubits: int, n_layers: int, **kwargs
199
+ ) -> list[qml.operation.Operator]:
200
+ """
201
+ Build the QAOA ansatz circuit.
202
+
203
+ Args:
204
+ params: Parameter array to use for the ansatz.
205
+ n_qubits (int): Number of qubits.
206
+ n_layers (int): Number of QAOA layers.
207
+ **kwargs: Additional unused arguments.
208
+
209
+ Returns:
210
+ list[qml.operation.Operator]: List of operations representing the QAOA ansatz.
211
+ """
212
+ return qml.QAOAEmbedding.compute_decomposition(
213
+ features=[],
214
+ weights=params.reshape(n_layers, -1),
215
+ wires=range(n_qubits),
216
+ local_field=qml.RY,
217
+ )
218
+
219
+
220
+ class HardwareEfficientAnsatz(Ansatz):
221
+ """
222
+ Hardware-efficient ansatz (not yet implemented).
223
+
224
+ This ansatz is designed to be easily implementable on near-term quantum hardware,
225
+ typically using native gate sets and connectivity patterns.
226
+
227
+ Note:
228
+ This class is a placeholder for future implementation.
229
+ """
230
+
231
+ @staticmethod
232
+ def n_params_per_layer(n_qubits: int, **kwargs) -> int:
233
+ """Not yet implemented."""
234
+ raise NotImplementedError("HardwareEfficientAnsatz is not yet implemented.")
235
+
236
+ def build(
237
+ self, params, n_qubits: int, n_layers: int, **kwargs
238
+ ) -> list[qml.operation.Operator]:
239
+ """Not yet implemented."""
240
+ raise NotImplementedError("HardwareEfficientAnsatz is not yet implemented.")
241
+
242
+
243
+ # --- Chemistry Ansaetze ---
244
+
245
+
246
+ class UCCSDAnsatz(Ansatz):
247
+ """
248
+ Unitary Coupled Cluster Singles and Doubles (UCCSD) ansatz.
249
+
250
+ This ansatz is specifically designed for quantum chemistry calculations,
251
+ implementing the UCCSD approximation which includes all single and double
252
+ electron excitations from a reference state.
253
+ """
254
+
255
+ @staticmethod
256
+ def n_params_per_layer(n_qubits: int, n_electrons: int, **kwargs) -> int:
257
+ """
258
+ Calculate the number of parameters per layer for UCCSD ansatz.
259
+
260
+ Args:
261
+ n_qubits (int): Number of qubits in the circuit.
262
+ n_electrons (int): Number of electrons in the system.
263
+ **kwargs: Additional unused arguments.
264
+
265
+ Returns:
266
+ int: Number of parameters (number of single + double excitations).
267
+ """
268
+ singles, doubles = qml.qchem.excitations(n_electrons, n_qubits)
269
+ s_wires, d_wires = qml.qchem.excitations_to_wires(singles, doubles)
270
+ n_params = len(s_wires) + len(d_wires)
271
+ return _require_trainable_params(n_params, UCCSDAnsatz.__name__)
272
+
273
+ def build(
274
+ self, params, n_qubits: int, n_layers: int, **kwargs
275
+ ) -> list[qml.operation.Operator]:
276
+ """
277
+ Build the UCCSD ansatz circuit.
278
+
279
+ Args:
280
+ params: Parameter array for excitation amplitudes.
281
+ n_qubits (int): Number of qubits.
282
+ n_layers (int): Number of UCCSD layers (repeats).
283
+ **kwargs: Additional arguments:
284
+ n_electrons (int): Number of electrons in the system (required).
285
+
286
+ Returns:
287
+ list[qml.operation.Operator]: List of operations representing the UCCSD ansatz.
288
+ """
289
+ n_electrons = kwargs.pop("n_electrons")
290
+ singles, doubles = qml.qchem.excitations(n_electrons, n_qubits)
291
+ s_wires, d_wires = qml.qchem.excitations_to_wires(singles, doubles)
292
+ hf_state = qml.qchem.hf_state(n_electrons, n_qubits)
293
+
294
+ return qml.UCCSD.compute_decomposition(
295
+ params.reshape(n_layers, -1),
296
+ wires=range(n_qubits),
297
+ s_wires=s_wires,
298
+ d_wires=d_wires,
299
+ init_state=hf_state,
300
+ n_repeats=n_layers,
301
+ )
302
+
303
+
304
+ class HartreeFockAnsatz(Ansatz):
305
+ """
306
+ Hartree-Fock-based ansatz for quantum chemistry.
307
+
308
+ This ansatz prepares the Hartree-Fock reference state and applies
309
+ parameterized single and double excitation gates. It's a simplified
310
+ alternative to UCCSD, often used as a starting point for VQE calculations.
311
+ """
312
+
313
+ @staticmethod
314
+ def n_params_per_layer(n_qubits: int, n_electrons: int, **kwargs) -> int:
315
+ """
316
+ Calculate the number of parameters per layer for Hartree-Fock ansatz.
317
+
318
+ Args:
319
+ n_qubits (int): Number of qubits in the circuit.
320
+ n_electrons (int): Number of electrons in the system.
321
+ **kwargs: Additional unused arguments.
322
+
323
+ Returns:
324
+ int: Number of parameters (number of single + double excitations).
325
+ """
326
+ singles, doubles = qml.qchem.excitations(n_electrons, n_qubits)
327
+ n_params = len(singles) + len(doubles)
328
+ return _require_trainable_params(n_params, HartreeFockAnsatz.__name__)
329
+
330
+ def build(
331
+ self, params, n_qubits: int, n_layers: int, **kwargs
332
+ ) -> list[qml.operation.Operator]:
333
+ """
334
+ Build the Hartree-Fock ansatz circuit.
335
+
336
+ Args:
337
+ params: Parameter array for excitation amplitudes.
338
+ n_qubits (int): Number of qubits.
339
+ n_layers (int): Number of ansatz layers.
340
+ **kwargs: Additional arguments:
341
+ n_electrons (int): Number of electrons in the system (required).
342
+
343
+ Returns:
344
+ list[qml.operation.Operator]: List of operations representing the Hartree-Fock ansatz.
345
+ """
346
+ n_electrons = kwargs.pop("n_electrons")
347
+ singles, doubles = qml.qchem.excitations(n_electrons, n_qubits)
348
+ hf_state = qml.qchem.hf_state(n_electrons, n_qubits)
349
+
350
+ operations = []
351
+ for layer_params in params.reshape(n_layers, -1):
352
+ operations.extend(
353
+ qml.AllSinglesDoubles.compute_decomposition(
354
+ layer_params,
355
+ wires=range(n_qubits),
356
+ hf_state=hf_state,
357
+ singles=singles,
358
+ doubles=doubles,
359
+ )
360
+ )
361
+
362
+ # Reset the BasisState operations after the first layer
363
+ # for behaviour similar to UCCSD ansatz
364
+ for op in operations[len(operations) // 2 :]:
365
+ if hasattr(op, "_hyperparameters") and "hf_state" in op._hyperparameters:
366
+ op._hyperparameters["hf_state"] = 0
367
+
368
+ return operations
@@ -0,0 +1,263 @@
1
+ # SPDX-FileCopyrightText: 2025-2026 Qoro Quantum Ltd <divi@qoroquantum.de>
2
+ #
3
+ # SPDX-License-Identifier: Apache-2.0
4
+
5
+ from typing import Any
6
+ from warnings import warn
7
+
8
+ import numpy as np
9
+ import pennylane as qml
10
+ import sympy as sp
11
+ from qiskit import QuantumCircuit
12
+
13
+ from divi.circuits import CircuitBundle, MetaCircuit
14
+ from divi.qprog._hamiltonians import _clean_hamiltonian
15
+ from divi.qprog.variational_quantum_algorithm import VariationalQuantumAlgorithm
16
+
17
+
18
+ class CustomVQA(VariationalQuantumAlgorithm):
19
+ """Custom variational algorithm for a parameterized QuantumScript.
20
+
21
+ This implementation wraps a PennyLane QuantumScript (or converts a Qiskit
22
+ QuantumCircuit into one) and optimizes its trainable parameters to minimize
23
+ a single expectation-value measurement. Qiskit measurements are converted
24
+ into a PauliZ expectation on the measured wires. Parameters are bound to sympy
25
+ symbols to enable QASM substitution and reuse of MetaCircuit templates
26
+ during optimization.
27
+
28
+ Attributes:
29
+ qscript (qml.tape.QuantumScript): The parameterized QuantumScript.
30
+ param_shape (tuple[int, ...]): Shape of a single parameter set.
31
+ n_qubits (int): Number of qubits in the script.
32
+ n_layers (int): Layer count (fixed to 1 for this wrapper).
33
+ cost_hamiltonian (qml.operation.Operator): Observable being minimized.
34
+ loss_constant (float): Constant term extracted from the observable.
35
+ optimizer (Optimizer): Classical optimizer for parameter updates.
36
+ max_iterations (int): Maximum number of optimization iterations.
37
+ current_iteration (int): Current optimization iteration.
38
+ """
39
+
40
+ def __init__(
41
+ self,
42
+ qscript: qml.tape.QuantumScript | QuantumCircuit,
43
+ *,
44
+ param_shape: tuple[int, ...] | int | None = None,
45
+ max_iterations: int = 10,
46
+ **kwargs,
47
+ ) -> None:
48
+ """Initialize a CustomVQA instance.
49
+
50
+ Args:
51
+ qscript (qml.tape.QuantumScript | QuantumCircuit): A parameterized QuantumScript with a
52
+ single expectation-value measurement, or a Qiskit QuantumCircuit with
53
+ computational basis measurements.
54
+ param_shape (tuple[int, ...] | int | None): Shape of a single parameter
55
+ set. If None, uses a flat shape inferred from trainable parameters.
56
+ max_iterations (int): Maximum number of optimization iterations.
57
+ **kwargs: Additional keyword arguments passed to the parent class, including
58
+ backend and optimizer.
59
+
60
+ Raises:
61
+ TypeError: If qscript is not a supported PennyLane QuantumScript or Qiskit QuantumCircuit.
62
+ ValueError: If the script has an invalid measurement or no trainable parameters.
63
+ """
64
+ super().__init__(**kwargs)
65
+
66
+ self._qiskit_param_names = (
67
+ [param.name for param in qscript.parameters]
68
+ if isinstance(qscript, QuantumCircuit)
69
+ else None
70
+ )
71
+ self.qscript = self._coerce_to_quantum_script(qscript)
72
+
73
+ if len(self.qscript.measurements) != 1:
74
+ raise ValueError(
75
+ "QuantumScript must contain exactly one measurement for optimization."
76
+ )
77
+
78
+ measurement = self.qscript.measurements[0]
79
+ if not hasattr(measurement, "obs") or measurement.obs is None:
80
+ raise ValueError(
81
+ "QuantumScript must contain a single expectation-value measurement."
82
+ )
83
+
84
+ self._cost_hamiltonian, self.loss_constant = _clean_hamiltonian(measurement.obs)
85
+ if (
86
+ isinstance(self._cost_hamiltonian, qml.Hamiltonian)
87
+ and not self._cost_hamiltonian.operands
88
+ ):
89
+ raise ValueError("Hamiltonian contains only constant terms.")
90
+
91
+ self.n_qubits = self.qscript.num_wires
92
+ self.n_layers = 1
93
+ self.max_iterations = max_iterations
94
+ self.current_iteration = 0
95
+
96
+ trainable_param_indices = (
97
+ list(self.qscript.trainable_params)
98
+ if self.qscript.trainable_params
99
+ else list(range(len(self.qscript.get_parameters())))
100
+ )
101
+ if not trainable_param_indices:
102
+ raise ValueError("QuantumScript does not contain any trainable parameters.")
103
+
104
+ self._param_shape = self._resolve_param_shape(
105
+ param_shape, len(trainable_param_indices)
106
+ )
107
+ self._n_params = int(np.prod(self._param_shape))
108
+
109
+ self._trainable_param_indices = trainable_param_indices
110
+ self._param_symbols = (
111
+ np.array(
112
+ [sp.Symbol(name) for name in self._qiskit_param_names], dtype=object
113
+ ).reshape(self._param_shape)
114
+ if self._qiskit_param_names is not None
115
+ else sp.symarray("p", self._param_shape)
116
+ )
117
+
118
+ flat_symbols = self._param_symbols.flatten().tolist()
119
+ self._qscript = self.qscript.bind_new_parameters(
120
+ flat_symbols, self._trainable_param_indices
121
+ )
122
+
123
+ @property
124
+ def cost_hamiltonian(self) -> qml.operation.Operator:
125
+ """The cost Hamiltonian for the QuantumScript optimization."""
126
+ return self._cost_hamiltonian
127
+
128
+ @property
129
+ def param_shape(self) -> tuple[int, ...]:
130
+ """Shape of a single parameter set."""
131
+ return self._param_shape
132
+
133
+ def _resolve_param_shape(
134
+ self, param_shape: tuple[int, ...] | int | None, n_params: int
135
+ ) -> tuple[int, ...]:
136
+ """Validate and normalize the parameter shape.
137
+
138
+ Args:
139
+ param_shape (tuple[int, ...] | int | None): User-provided parameter shape.
140
+ n_params (int): Number of trainable parameters in the script.
141
+
142
+ Returns:
143
+ tuple[int, ...]: Normalized parameter shape.
144
+
145
+ Raises:
146
+ ValueError: If the shape is invalid or does not match n_params.
147
+ """
148
+ if param_shape is None:
149
+ return (n_params,)
150
+
151
+ param_shape = (param_shape,) if isinstance(param_shape, int) else param_shape
152
+
153
+ if any(dim <= 0 for dim in param_shape):
154
+ raise ValueError(
155
+ f"param_shape entries must be positive, got {param_shape}."
156
+ )
157
+
158
+ if int(np.prod(param_shape)) != n_params:
159
+ raise ValueError(
160
+ f"param_shape does not match the number of trainable parameters. "
161
+ f"Expected product {n_params}, got {int(np.prod(param_shape))}."
162
+ )
163
+
164
+ return tuple(param_shape)
165
+
166
+ def _coerce_to_quantum_script(
167
+ self,
168
+ qscript: qml.tape.QuantumScript | QuantumCircuit,
169
+ ) -> qml.tape.QuantumScript:
170
+ """Convert supported inputs into a PennyLane QuantumScript.
171
+
172
+ Args:
173
+ qscript (qml.tape.QuantumScript): Input QuantumScript or Qiskit QuantumCircuit.
174
+
175
+ Returns:
176
+ qml.tape.QuantumScript: The converted QuantumScript.
177
+
178
+ Raises:
179
+ TypeError: If the input type is unsupported.
180
+ """
181
+ if isinstance(qscript, qml.tape.QuantumScript):
182
+ return qscript
183
+
184
+ if isinstance(qscript, QuantumCircuit):
185
+ measured_wires = sorted(
186
+ {
187
+ qscript.qubits.index(qubit)
188
+ for instruction in qscript.data
189
+ if instruction.operation.name == "measure"
190
+ for qubit in instruction.qubits
191
+ }
192
+ )
193
+ if not measured_wires:
194
+ warn(
195
+ "Provided QuantumCircuit has no measurement operations. "
196
+ "Defaulting to measuring all wires with PauliZ.",
197
+ UserWarning,
198
+ )
199
+ measured_wires = list(range(len(qscript.qubits)))
200
+
201
+ obs = (
202
+ qml.Z(measured_wires[0])
203
+ if len(measured_wires) == 1
204
+ else qml.sum(*(qml.Z(wire) for wire in measured_wires))
205
+ )
206
+ # Remove measurements before conversion to avoid MidMeasureMP issues
207
+ qc_no_measure = QuantumCircuit(qscript.num_qubits)
208
+ for instruction in qscript.data:
209
+ if instruction.operation.name != "measure":
210
+ qc_no_measure.append(
211
+ instruction.operation, instruction.qubits, instruction.clbits
212
+ )
213
+ qfunc = qml.from_qiskit(qc_no_measure)
214
+ qiskit_params = [
215
+ qml.numpy.array(0.0, requires_grad=True) for _ in qscript.parameters
216
+ ]
217
+
218
+ def qfunc_with_measurement(*params):
219
+ qfunc(*params)
220
+ return qml.expval(obs)
221
+
222
+ return qml.tape.make_qscript(qfunc_with_measurement)(*qiskit_params)
223
+
224
+ raise TypeError(
225
+ "qscript must be a PennyLane QuantumScript or a Qiskit QuantumCircuit."
226
+ )
227
+
228
+ def _create_meta_circuits_dict(self) -> dict[str, MetaCircuit]:
229
+ """Create the meta-circuit dictionary for CustomVQA.
230
+
231
+ Returns:
232
+ dict[str, MetaCircuit]: Dictionary containing the cost circuit template.
233
+ """
234
+ return {
235
+ "cost_circuit": self._meta_circuit_factory(
236
+ self._qscript, symbols=self._param_symbols.flatten()
237
+ )
238
+ }
239
+
240
+ def _generate_circuits(self) -> list[CircuitBundle]:
241
+ """Generate circuits for the current parameter sets.
242
+
243
+ Returns:
244
+ list[CircuitBundle]: Circuit bundles tagged by parameter index.
245
+ """
246
+ return [
247
+ self.meta_circuits["cost_circuit"].initialize_circuit_from_params(
248
+ params_group, param_idx=p
249
+ )
250
+ for p, params_group in enumerate(self._curr_params)
251
+ ]
252
+
253
+ def _perform_final_computation(self, **kwargs) -> None:
254
+ """No-op by default for custom QuantumScript optimization."""
255
+ pass
256
+
257
+ def _save_subclass_state(self) -> dict[str, Any]:
258
+ """Save subclass-specific state for checkpointing."""
259
+ return {}
260
+
261
+ def _load_subclass_state(self, state: dict[str, Any]) -> None:
262
+ """Load subclass-specific state from a checkpoint."""
263
+ pass