qoro-divi 0.2.0b1__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 (58) hide show
  1. divi/__init__.py +8 -0
  2. divi/_pbar.py +73 -0
  3. divi/circuits.py +139 -0
  4. divi/exp/cirq/__init__.py +7 -0
  5. divi/exp/cirq/_lexer.py +126 -0
  6. divi/exp/cirq/_parser.py +889 -0
  7. divi/exp/cirq/_qasm_export.py +37 -0
  8. divi/exp/cirq/_qasm_import.py +35 -0
  9. divi/exp/cirq/exception.py +21 -0
  10. divi/exp/scipy/_cobyla.py +342 -0
  11. divi/exp/scipy/pyprima/LICENCE.txt +28 -0
  12. divi/exp/scipy/pyprima/__init__.py +263 -0
  13. divi/exp/scipy/pyprima/cobyla/__init__.py +0 -0
  14. divi/exp/scipy/pyprima/cobyla/cobyla.py +599 -0
  15. divi/exp/scipy/pyprima/cobyla/cobylb.py +849 -0
  16. divi/exp/scipy/pyprima/cobyla/geometry.py +240 -0
  17. divi/exp/scipy/pyprima/cobyla/initialize.py +269 -0
  18. divi/exp/scipy/pyprima/cobyla/trustregion.py +540 -0
  19. divi/exp/scipy/pyprima/cobyla/update.py +331 -0
  20. divi/exp/scipy/pyprima/common/__init__.py +0 -0
  21. divi/exp/scipy/pyprima/common/_bounds.py +41 -0
  22. divi/exp/scipy/pyprima/common/_linear_constraints.py +46 -0
  23. divi/exp/scipy/pyprima/common/_nonlinear_constraints.py +64 -0
  24. divi/exp/scipy/pyprima/common/_project.py +224 -0
  25. divi/exp/scipy/pyprima/common/checkbreak.py +107 -0
  26. divi/exp/scipy/pyprima/common/consts.py +48 -0
  27. divi/exp/scipy/pyprima/common/evaluate.py +101 -0
  28. divi/exp/scipy/pyprima/common/history.py +39 -0
  29. divi/exp/scipy/pyprima/common/infos.py +30 -0
  30. divi/exp/scipy/pyprima/common/linalg.py +452 -0
  31. divi/exp/scipy/pyprima/common/message.py +336 -0
  32. divi/exp/scipy/pyprima/common/powalg.py +131 -0
  33. divi/exp/scipy/pyprima/common/preproc.py +393 -0
  34. divi/exp/scipy/pyprima/common/present.py +5 -0
  35. divi/exp/scipy/pyprima/common/ratio.py +56 -0
  36. divi/exp/scipy/pyprima/common/redrho.py +49 -0
  37. divi/exp/scipy/pyprima/common/selectx.py +346 -0
  38. divi/interfaces.py +25 -0
  39. divi/parallel_simulator.py +258 -0
  40. divi/qasm.py +220 -0
  41. divi/qem.py +191 -0
  42. divi/qlogger.py +119 -0
  43. divi/qoro_service.py +343 -0
  44. divi/qprog/__init__.py +13 -0
  45. divi/qprog/_graph_partitioning.py +619 -0
  46. divi/qprog/_mlae.py +182 -0
  47. divi/qprog/_qaoa.py +440 -0
  48. divi/qprog/_vqe.py +275 -0
  49. divi/qprog/_vqe_sweep.py +144 -0
  50. divi/qprog/batch.py +235 -0
  51. divi/qprog/optimizers.py +75 -0
  52. divi/qprog/quantum_program.py +493 -0
  53. divi/utils.py +116 -0
  54. qoro_divi-0.2.0b1.dist-info/LICENSE +190 -0
  55. qoro_divi-0.2.0b1.dist-info/LICENSES/Apache-2.0.txt +73 -0
  56. qoro_divi-0.2.0b1.dist-info/METADATA +57 -0
  57. qoro_divi-0.2.0b1.dist-info/RECORD +58 -0
  58. qoro_divi-0.2.0b1.dist-info/WHEEL +4 -0
divi/qprog/_vqe.py ADDED
@@ -0,0 +1,275 @@
1
+ # SPDX-FileCopyrightText: 2025 Qoro Quantum Ltd <divi@qoroquantum.de>
2
+ #
3
+ # SPDX-License-Identifier: Apache-2.0
4
+
5
+ from enum import Enum
6
+
7
+ import numpy as np
8
+ import pennylane as qml
9
+ import sympy as sp
10
+
11
+ from divi.circuits import MetaCircuit
12
+ from divi.qprog import QuantumProgram
13
+ from divi.qprog.optimizers import Optimizer
14
+
15
+
16
+ class VQEAnsatz(Enum):
17
+ UCCSD = "UCCSD"
18
+ RY = "RY"
19
+ RYRZ = "RYRZ"
20
+ HW_EFFICIENT = "HW_EFFICIENT"
21
+ QAOA = "QAOA"
22
+ HARTREE_FOCK = "HF"
23
+
24
+ def describe(self):
25
+ return self.name, self.value
26
+
27
+ def n_params(self, n_qubits, **kwargs):
28
+ if self in (VQEAnsatz.UCCSD, VQEAnsatz.HARTREE_FOCK):
29
+ singles, doubles = qml.qchem.excitations(
30
+ kwargs.pop("n_electrons"), n_qubits
31
+ )
32
+ s_wires, d_wires = qml.qchem.excitations_to_wires(singles, doubles)
33
+
34
+ return len(s_wires) + len(d_wires)
35
+ elif self == VQEAnsatz.RY:
36
+ return n_qubits
37
+ elif self == VQEAnsatz.RYRZ:
38
+ return 2 * n_qubits
39
+ elif self == VQEAnsatz.HW_EFFICIENT:
40
+ raise NotImplementedError
41
+ elif self == VQEAnsatz.QAOA:
42
+ return qml.QAOAEmbedding.shape(n_layers=1, n_wires=n_qubits)[1]
43
+
44
+
45
+ class VQE(QuantumProgram):
46
+ def __init__(
47
+ self,
48
+ symbols,
49
+ bond_length: float,
50
+ coordinate_structure: list[tuple[float, float, float]],
51
+ charge: float = 0,
52
+ n_layers: int = 1,
53
+ ansatz=VQEAnsatz.HARTREE_FOCK,
54
+ optimizer=Optimizer.MONTE_CARLO,
55
+ max_iterations=10,
56
+ **kwargs,
57
+ ) -> None:
58
+ """
59
+ Initialize the VQE problem.
60
+
61
+ Args:
62
+ symbols (list): The symbols of the atoms in the molecule
63
+ bond_length (float): The bond length to consider
64
+ coordinate_structure (list): The coordinate structure of the molecule, represented in unit lengths
65
+ charge (float): the charge of the molecule. Defaults to 0.
66
+ ansatz (VQEAnsatz): The ansatz to use for the VQE problem
67
+ optimizer (Optimizers): The optimizer to use.
68
+ max_iterations (int): Maximum number of iteration optimizers.
69
+ """
70
+
71
+ # Local Variables
72
+ self.symbols = symbols
73
+ self.coordinate_structure = coordinate_structure
74
+ self.bond_length = bond_length
75
+ self.charge = charge
76
+ if len(self.coordinate_structure) != len(self.symbols):
77
+ raise ValueError(
78
+ "The number of symbols must match the number of coordinates"
79
+ )
80
+
81
+ self.n_layers = n_layers
82
+ self.results = {}
83
+ self.ansatz = ansatz
84
+ self.optimizer = optimizer
85
+ self.max_iterations = max_iterations
86
+ self.current_iteration = 0
87
+
88
+ self.cost_hamiltonian = self._generate_hamiltonian_operations()
89
+
90
+ super().__init__(**kwargs)
91
+
92
+ self._meta_circuits = self._create_meta_circuits_dict()
93
+
94
+ def _generate_hamiltonian_operations(self) -> qml.operation.Operator:
95
+ """
96
+ Generate the Hamiltonian operators for the given bond length.
97
+
98
+ Returns:
99
+ The Hamiltonian corresponding to the VQE problem.
100
+ """
101
+
102
+ coordinates = [
103
+ (
104
+ coord_0 * self.bond_length,
105
+ coord_1 * self.bond_length,
106
+ coord_2 * self.bond_length,
107
+ )
108
+ for (coord_0, coord_1, coord_2) in self.coordinate_structure
109
+ ]
110
+
111
+ coordinates = np.array(coordinates)
112
+ molecule = qml.qchem.Molecule(self.symbols, coordinates, charge=self.charge)
113
+ hamiltonian, qubits = qml.qchem.molecular_hamiltonian(molecule)
114
+
115
+ self.n_qubits = qubits
116
+ self.n_electrons = molecule.n_electrons
117
+
118
+ self.n_params = self.ansatz.n_params(
119
+ self.n_qubits, n_electrons=self.n_electrons
120
+ )
121
+
122
+ constant_terms_idx = list(
123
+ filter(
124
+ lambda x: all(
125
+ isinstance(term, qml.I) for term in hamiltonian[x].terms()[1]
126
+ ),
127
+ range(len(hamiltonian)),
128
+ )
129
+ )
130
+
131
+ self.loss_constant = sum(
132
+ map(lambda x: hamiltonian[x].scalar.item(), constant_terms_idx)
133
+ )
134
+
135
+ for idx in constant_terms_idx:
136
+ hamiltonian -= hamiltonian[idx]
137
+
138
+ return hamiltonian.simplify()
139
+
140
+ def _create_meta_circuits_dict(self) -> dict[str, MetaCircuit]:
141
+ weights_syms = sp.symarray("w", (self.n_layers, self.n_params))
142
+
143
+ def _prepare_circuit(ansatz, hamiltonian, params):
144
+ """
145
+ Prepare the circuit for the VQE problem.
146
+ Args:
147
+ ansatz (Ansatze): The ansatz to use
148
+ hamiltonian (qml.Hamiltonian): The Hamiltonian to use
149
+ params (list): The parameters to use for the ansatz
150
+ """
151
+ self._set_ansatz(ansatz, params)
152
+
153
+ # Even though in principle we want to sample from a state,
154
+ # we are applying an `expval` operation here to make it compatible
155
+ # with the pennylane transforms down the line, which complain about
156
+ # the `sample` operation.
157
+ return qml.expval(hamiltonian)
158
+
159
+ return {
160
+ "cost_circuit": self._meta_circuit_factory(
161
+ qml.tape.make_qscript(_prepare_circuit)(
162
+ self.ansatz, self.cost_hamiltonian, weights_syms
163
+ ),
164
+ symbols=weights_syms.flatten(),
165
+ )
166
+ }
167
+
168
+ def _set_ansatz(self, ansatz: VQEAnsatz, params):
169
+ """
170
+ Set the ansatz for the VQE problem.
171
+ Args:
172
+ ansatz (Ansatze): The ansatz to use
173
+ params (list): The parameters to use for the ansatz
174
+ n_layers (int): The number of layers to use for the ansatz
175
+ """
176
+
177
+ def _add_hw_efficient_ansatz(params):
178
+ raise NotImplementedError
179
+
180
+ def _add_qaoa_ansatz(params):
181
+ # This infers layers automatically from the parameters shape
182
+ qml.QAOAEmbedding(
183
+ features=[],
184
+ weights=params.reshape(self.n_layers, -1),
185
+ wires=range(self.n_qubits),
186
+ )
187
+
188
+ def _add_ry_ansatz(params):
189
+ qml.layer(
190
+ qml.AngleEmbedding,
191
+ self.n_layers,
192
+ params.reshape(self.n_layers, -1),
193
+ wires=range(self.n_qubits),
194
+ rotation="Y",
195
+ )
196
+
197
+ def _add_ryrz_ansatz(params):
198
+ def _ryrz(params, wires):
199
+ ry_rots, rz_rots = params.reshape(2, -1)
200
+ qml.AngleEmbedding(ry_rots, wires=wires, rotation="Y")
201
+ qml.AngleEmbedding(rz_rots, wires=wires, rotation="Z")
202
+
203
+ qml.layer(
204
+ _ryrz,
205
+ self.n_layers,
206
+ params.reshape(self.n_layers, -1),
207
+ wires=range(self.n_qubits),
208
+ )
209
+
210
+ def _add_uccsd_ansatz(params):
211
+ hf_state = qml.qchem.hf_state(self.n_electrons, self.n_qubits)
212
+
213
+ singles, doubles = qml.qchem.excitations(self.n_electrons, self.n_qubits)
214
+ s_wires, d_wires = qml.qchem.excitations_to_wires(singles, doubles)
215
+
216
+ qml.UCCSD(
217
+ params.reshape(self.n_layers, -1),
218
+ wires=range(self.n_qubits),
219
+ s_wires=s_wires,
220
+ d_wires=d_wires,
221
+ init_state=hf_state,
222
+ n_repeats=self.n_layers,
223
+ )
224
+
225
+ def _add_hartree_fock_ansatz(params):
226
+ singles, doubles = qml.qchem.excitations(self.n_electrons, self.n_qubits)
227
+ hf_state = qml.qchem.hf_state(self.n_electrons, self.n_qubits)
228
+
229
+ qml.layer(
230
+ qml.AllSinglesDoubles,
231
+ self.n_layers,
232
+ params.reshape(self.n_layers, -1),
233
+ wires=range(self.n_qubits),
234
+ hf_state=hf_state,
235
+ singles=singles,
236
+ doubles=doubles,
237
+ )
238
+
239
+ # Reset the BasisState operations after the first layer
240
+ # for behaviour similar to UCCSD ansatz
241
+ for op in qml.QueuingManager.active_context().queue[1:]:
242
+ op._hyperparameters["hf_state"] = 0
243
+
244
+ if ansatz in VQEAnsatz:
245
+ locals()[f"_add_{ansatz.name.lower()}_ansatz"](params)
246
+ else:
247
+ raise ValueError(f"Invalid Ansatz Value. Got {ansatz}.")
248
+
249
+ def _generate_circuits(self):
250
+ """
251
+ Generate the circuits for the VQE problem.
252
+
253
+ In this method, we generate bulk circuits based on the selected parameters.
254
+ We generate circuits for each bond length and each ansatz and optimization choice.
255
+
256
+ The structure of the circuits is as follows:
257
+ - For each bond length:
258
+ - For each ansatz:
259
+ - Generate the circuit
260
+ """
261
+
262
+ for p, params_group in enumerate(self._curr_params):
263
+ circuit = self._meta_circuits[
264
+ "cost_circuit"
265
+ ].initialize_circuit_from_params(params_group, tag_prefix=f"{p}")
266
+
267
+ self.circuits.append(circuit)
268
+
269
+ def _run_optimization_circuits(self, store_data, data_file):
270
+ if self.cost_hamiltonian is None or len(self.cost_hamiltonian) == 0:
271
+ raise RuntimeError(
272
+ "Hamiltonian operators must be generated before running the VQE"
273
+ )
274
+
275
+ return super()._run_optimization_circuits(store_data, data_file)
@@ -0,0 +1,144 @@
1
+ # SPDX-FileCopyrightText: 2025 Qoro Quantum Ltd <divi@qoroquantum.de>
2
+ #
3
+ # SPDX-License-Identifier: Apache-2.0
4
+
5
+ from functools import partial
6
+ from itertools import product
7
+ from typing import Literal
8
+
9
+ import matplotlib.pyplot as plt
10
+
11
+ from divi.qprog import VQE, ProgramBatch, VQEAnsatz
12
+
13
+ from .optimizers import Optimizer
14
+
15
+
16
+ class VQEHyperparameterSweep(ProgramBatch):
17
+ """Allows user to carry out a grid search across different values
18
+ for the ansatz and the bond length used in a VQE program.
19
+ """
20
+
21
+ def __init__(
22
+ self,
23
+ bond_lengths: list[float],
24
+ ansatze: list[VQEAnsatz],
25
+ symbols: list[str],
26
+ coordinate_structure: list[tuple[float, float, float]],
27
+ charge: float = 0,
28
+ optimizer: Optimizer = Optimizer.MONTE_CARLO,
29
+ max_iterations: int = 10,
30
+ **kwargs,
31
+ ):
32
+ """Initiates the class.
33
+
34
+ Args:
35
+ bond_lengths (list): The bond lengths to consider.
36
+ ansatze (list): The ansatze to use for the VQE problem.
37
+ symbols (list): The symbols of the atoms in the molecule.
38
+ coordinate_structure (list): The coordinate structure of the molecule.
39
+ optimizer (Optimizers): The optimizer to use.
40
+ max_iterations (int): Maximum number of iteration optimizers.
41
+ """
42
+ super().__init__(backend=kwargs.pop("backend"))
43
+
44
+ self.ansatze = ansatze
45
+ self.bond_lengths = [round(bnd, 9) for bnd in bond_lengths]
46
+ self.max_iterations = max_iterations
47
+
48
+ self._constructor = partial(
49
+ VQE,
50
+ symbols=symbols,
51
+ coordinate_structure=coordinate_structure,
52
+ charge=charge,
53
+ optimizer=optimizer,
54
+ max_iterations=self.max_iterations,
55
+ backend=self.backend,
56
+ **kwargs,
57
+ )
58
+
59
+ def create_programs(self):
60
+ if len(self.programs) > 0:
61
+ raise RuntimeError(
62
+ "Some programs already exist. "
63
+ "Clear the program dictionary before creating new ones by using batch.reset()."
64
+ )
65
+
66
+ super().create_programs()
67
+
68
+ for ansatz, bond_length in product(self.ansatze, self.bond_lengths):
69
+ _job_id = (ansatz, bond_length)
70
+ self.programs[_job_id] = self._constructor(
71
+ job_id=_job_id,
72
+ bond_length=bond_length,
73
+ ansatz=ansatz,
74
+ losses=self._manager.list(),
75
+ final_params=self._manager.list(),
76
+ progress_queue=self._queue,
77
+ )
78
+
79
+ def aggregate_results(self):
80
+ if len(self.programs) == 0:
81
+ raise RuntimeError("No programs to aggregate. Run create_programs() first.")
82
+
83
+ if self._executor is not None:
84
+ self.wait_for_all()
85
+
86
+ if any(len(program.losses) == 0 for program in self.programs.values()):
87
+ raise RuntimeError(
88
+ "Some/All programs have empty losses. Did you call run()?"
89
+ )
90
+
91
+ all_energies = {key: prog.losses[-1] for key, prog in self.programs.items()}
92
+
93
+ smallest_key = min(all_energies, key=lambda k: min(all_energies[k].values()))
94
+ smallest_value = min(all_energies[smallest_key].values())
95
+
96
+ return smallest_key, smallest_value
97
+
98
+ def visualize_results(self, graph_type: Literal["line", "scatter"] = "line"):
99
+ """
100
+ Visualize the results of the VQE problem.
101
+ """
102
+ if graph_type not in ["line", "scatter"]:
103
+ raise ValueError(
104
+ f"Invalid graph type: {graph_type}. Choose between 'line' and 'scatter'."
105
+ )
106
+
107
+ if self._executor is not None:
108
+ self.wait_for_all()
109
+
110
+ data = []
111
+ colors = ["blue", "g", "r", "c", "m", "y", "k"]
112
+
113
+ ansatz_list = list(VQEAnsatz)
114
+
115
+ if graph_type == "scatter":
116
+ for ansatz, bond_length in self.programs.keys():
117
+ min_energies = []
118
+
119
+ curr_energies = self.programs[(ansatz, bond_length)].losses[-1]
120
+ min_energies.append(
121
+ (
122
+ bond_length,
123
+ min(curr_energies.values()),
124
+ colors[ansatz_list.index(ansatz)],
125
+ )
126
+ )
127
+ data.extend(min_energies)
128
+
129
+ x, y, z = zip(*data)
130
+ plt.scatter(x, y, color=z, label=ansatz)
131
+
132
+ elif graph_type == "line":
133
+ for ansatz in self.ansatze:
134
+ energies = []
135
+ for bond_length in self.bond_lengths:
136
+ energies.append(
137
+ min(self.programs[(ansatz, bond_length)].losses[-1].values())
138
+ )
139
+ plt.plot(self.bond_lengths, energies, label=ansatz)
140
+
141
+ plt.xlabel("Bond length")
142
+ plt.ylabel("Energy level")
143
+ plt.legend()
144
+ plt.show()
divi/qprog/batch.py ADDED
@@ -0,0 +1,235 @@
1
+ # SPDX-FileCopyrightText: 2025 Qoro Quantum Ltd <divi@qoroquantum.de>
2
+ #
3
+ # SPDX-License-Identifier: Apache-2.0
4
+
5
+ import traceback
6
+ from abc import ABC, abstractmethod
7
+ from concurrent.futures import ProcessPoolExecutor, as_completed
8
+ from multiprocessing import Event, Manager
9
+ from multiprocessing.synchronize import Event as EventClass
10
+ from queue import Empty, Queue
11
+ from threading import Lock, Thread
12
+ from warnings import warn
13
+
14
+ from rich.console import Console
15
+ from rich.progress import Progress, TaskID
16
+
17
+ from divi._pbar import make_progress_bar
18
+ from divi.interfaces import CircuitRunner
19
+ from divi.parallel_simulator import ParallelSimulator
20
+ from divi.qlogger import disable_logging
21
+ from divi.qprog.quantum_program import QuantumProgram
22
+
23
+
24
+ def queue_listener(
25
+ queue: Queue,
26
+ progress_bar: Progress,
27
+ pb_task_map: dict[QuantumProgram, TaskID],
28
+ done_event: EventClass,
29
+ is_jupyter: bool,
30
+ lock: Lock,
31
+ ):
32
+ while not done_event.is_set():
33
+ try:
34
+ msg = queue.get(timeout=0.1)
35
+ except Empty:
36
+ continue
37
+ except Exception as e:
38
+ progress_bar.console.log(f"[queue_listener] Unexpected exception: {e}")
39
+ continue
40
+
41
+ with lock:
42
+ task_id = pb_task_map[msg["job_id"]]
43
+
44
+ progress_bar.update(
45
+ task_id,
46
+ advance=msg["progress"],
47
+ poll_attempt=msg.get("poll_attempt", 0),
48
+ message=msg.get("message", ""),
49
+ final_status=msg.get("final_status", ""),
50
+ refresh=is_jupyter,
51
+ )
52
+
53
+
54
+ class ProgramBatch(ABC):
55
+ """This abstract class provides the basic scaffolding for higher-order
56
+ computations that require more than one quantum program to achieve its goal.
57
+
58
+ Each implementation of this class has to have an implementation of two functions:
59
+ 1. `create_programs`: This function generates the independent programs that
60
+ are needed to achieve the objective of the job. The creation of those
61
+ programs can utilize the instance variables of the class to initialize
62
+ their parameters. The programs should be stored in a key-value store
63
+ where the keys represent the identifier of the program, whether random
64
+ or identificatory.
65
+
66
+ 2. `aggregate_results`: This function aggregates the results of the programs
67
+ after they are done executing. This function should be aware of the different
68
+ formats the programs might have (counts dictionary, expectation value, etc) and
69
+ handle such cases accordingly.
70
+ """
71
+
72
+ def __init__(self, backend: CircuitRunner):
73
+ super().__init__()
74
+
75
+ self.backend = backend
76
+ self._executor = None
77
+ self.programs = {}
78
+
79
+ self._total_circuit_count = 0
80
+ self._total_run_time = 0.0
81
+
82
+ self._is_local = isinstance(backend, ParallelSimulator)
83
+ self._is_jupyter = Console().is_jupyter
84
+
85
+ # Disable logging since we already have the bars to track progress
86
+ disable_logging()
87
+
88
+ @property
89
+ def total_circuit_count(self):
90
+ return self._total_circuit_count
91
+
92
+ @property
93
+ def total_run_time(self):
94
+ return self._total_run_time
95
+
96
+ @abstractmethod
97
+ def create_programs(self):
98
+ self._manager = Manager()
99
+ self._queue = self._manager.Queue()
100
+
101
+ if hasattr(self, "max_iterations"):
102
+ self._done_event = Event()
103
+
104
+ def reset(self):
105
+ self.programs.clear()
106
+
107
+ # Stop any active executor
108
+ if self._executor is not None:
109
+ self._executor.shutdown(wait=False)
110
+ self._executor = None
111
+ self.futures = None
112
+
113
+ # Signal and wait for listener thread to stop
114
+ if hasattr(self, "_done_event") and self._done_event is not None:
115
+ self._done_event.set()
116
+ self._done_event = None
117
+
118
+ if getattr(self, "_listener_thread", None) is not None:
119
+ self._listener_thread.join(timeout=1)
120
+ if self._listener_thread.is_alive():
121
+ warn(
122
+ "Listener thread did not terminate within timeout and may still be running."
123
+ )
124
+ else:
125
+ self._listener_thread = None
126
+ self._queue.close()
127
+ self._queue.join_thread()
128
+ self._queue = None
129
+
130
+ # Stop the progress bar if it's still active
131
+ if getattr(self, "_progress_bar", None) is not None:
132
+ try:
133
+ self._progress_bar.stop()
134
+ self._progress_bar = None
135
+ except Exception:
136
+ pass # Already stopped or not running
137
+
138
+ self._pb_task_map.clear()
139
+
140
+ def add_program_to_executor(self, program):
141
+ self.futures.append(self._executor.submit(program.run))
142
+
143
+ if self._progress_bar is not None:
144
+ with self._pb_lock:
145
+ self._pb_task_map[program.job_id] = self._progress_bar.add_task(
146
+ "",
147
+ job_name=f"Job {program.job_id}",
148
+ total=self.max_iterations,
149
+ completed=0,
150
+ poll_attempt=0,
151
+ message="",
152
+ final_status="",
153
+ mode=("simulation" if self._is_local else "network"),
154
+ )
155
+
156
+ def run(self):
157
+ if self._executor is not None:
158
+ raise RuntimeError("A batch is already being run.")
159
+
160
+ if len(self.programs) == 0:
161
+ raise RuntimeError("No programs to run.")
162
+
163
+ self._progress_bar = (
164
+ make_progress_bar(
165
+ max_retries=None if self._is_local else self.backend.max_retries,
166
+ is_jupyter=self._is_jupyter,
167
+ )
168
+ if hasattr(self, "max_iterations")
169
+ else None
170
+ )
171
+
172
+ self._executor = ProcessPoolExecutor()
173
+ self.futures = []
174
+ self._pb_task_map = {}
175
+ self._pb_lock = Lock()
176
+
177
+ if self._progress_bar is not None:
178
+ self._progress_bar.start()
179
+ self._listener_thread = Thread(
180
+ target=queue_listener,
181
+ args=(
182
+ self._queue,
183
+ self._progress_bar,
184
+ self._pb_task_map,
185
+ self._done_event,
186
+ self._is_jupyter,
187
+ self._pb_lock,
188
+ ),
189
+ daemon=True,
190
+ )
191
+ self._listener_thread.start()
192
+
193
+ for program in self.programs.values():
194
+ self.add_program_to_executor(program)
195
+
196
+ def check_all_done(self):
197
+ return all(future.done() for future in self.futures)
198
+
199
+ def wait_for_all(self):
200
+ if self._executor is None:
201
+ return
202
+
203
+ exceptions = []
204
+ try:
205
+ # Ensure all futures are completed and handle exceptions.
206
+ for future in as_completed(self.futures):
207
+ try:
208
+ future.result() # Raises an exception if the task failed.
209
+ except Exception as e:
210
+ exceptions.append(e)
211
+
212
+ finally:
213
+ self._executor.shutdown(wait=True, cancel_futures=False)
214
+ self._executor = None
215
+
216
+ if self._progress_bar is not None:
217
+ self._done_event.set()
218
+ self._listener_thread.join()
219
+
220
+ if exceptions:
221
+ for i, exc in enumerate(exceptions, 1):
222
+ print(f"Task {i} failed with exception:")
223
+ traceback.print_exception(type(exc), exc, exc.__traceback__)
224
+ raise RuntimeError("One or more tasks failed. Check logs for details.")
225
+
226
+ if self._progress_bar is not None:
227
+ self._progress_bar.stop()
228
+
229
+ self._total_circuit_count += sum(future.result()[0] for future in self.futures)
230
+ self._total_run_time += sum(future.result()[1] for future in self.futures)
231
+ self.futures = []
232
+
233
+ @abstractmethod
234
+ def aggregate_results(self):
235
+ raise NotImplementedError