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.
- divi/__init__.py +1 -2
- divi/backends/__init__.py +7 -0
- divi/backends/_circuit_runner.py +46 -0
- divi/{parallel_simulator.py → backends/_parallel_simulator.py} +136 -53
- divi/backends/_qoro_service.py +531 -0
- divi/circuits/__init__.py +5 -0
- divi/circuits/_core.py +226 -0
- divi/{qasm.py → circuits/qasm.py} +21 -2
- divi/{exp → extern}/cirq/_validator.py +9 -7
- divi/qprog/__init__.py +18 -5
- divi/qprog/algorithms/__init__.py +14 -0
- divi/qprog/algorithms/_ansatze.py +311 -0
- divi/qprog/{_qaoa.py → algorithms/_qaoa.py} +69 -41
- divi/qprog/{_vqe.py → algorithms/_vqe.py} +79 -135
- divi/qprog/batch.py +239 -55
- divi/qprog/exceptions.py +9 -0
- divi/qprog/optimizers.py +219 -18
- divi/qprog/quantum_program.py +389 -57
- divi/qprog/workflows/__init__.py +10 -0
- divi/qprog/{_graph_partitioning.py → workflows/_graph_partitioning.py} +3 -34
- divi/qprog/{_qubo_partitioning.py → workflows/_qubo_partitioning.py} +42 -25
- divi/qprog/{_vqe_sweep.py → workflows/_vqe_sweep.py} +59 -26
- divi/reporting/__init__.py +7 -0
- divi/reporting/_pbar.py +112 -0
- divi/{qlogger.py → reporting/_qlogger.py} +37 -2
- divi/{reporter.py → reporting/_reporter.py} +8 -14
- divi/utils.py +49 -10
- {qoro_divi-0.3.3.dist-info → qoro_divi-0.3.5.dist-info}/METADATA +2 -1
- qoro_divi-0.3.5.dist-info/RECORD +69 -0
- divi/_pbar.py +0 -70
- divi/circuits.py +0 -139
- divi/interfaces.py +0 -25
- divi/qoro_service.py +0 -425
- qoro_divi-0.3.3.dist-info/RECORD +0 -62
- /divi/{qpu_system.py → backends/_qpu_system.py} +0 -0
- /divi/{qem.py → circuits/qem.py} +0 -0
- /divi/{exp → extern}/cirq/__init__.py +0 -0
- /divi/{exp → extern}/cirq/_lexer.py +0 -0
- /divi/{exp → extern}/cirq/_parser.py +0 -0
- /divi/{exp → extern}/cirq/_qasm_export.py +0 -0
- /divi/{exp → extern}/cirq/_qasm_import.py +0 -0
- /divi/{exp → extern}/cirq/exception.py +0 -0
- /divi/{exp → extern}/scipy/_cobyla.py +0 -0
- /divi/{exp → extern}/scipy/pyprima/LICENCE.txt +0 -0
- /divi/{exp → extern}/scipy/pyprima/__init__.py +0 -0
- /divi/{exp → extern}/scipy/pyprima/cobyla/__init__.py +0 -0
- /divi/{exp → extern}/scipy/pyprima/cobyla/cobyla.py +0 -0
- /divi/{exp → extern}/scipy/pyprima/cobyla/cobylb.py +0 -0
- /divi/{exp → extern}/scipy/pyprima/cobyla/geometry.py +0 -0
- /divi/{exp → extern}/scipy/pyprima/cobyla/initialize.py +0 -0
- /divi/{exp → extern}/scipy/pyprima/cobyla/trustregion.py +0 -0
- /divi/{exp → extern}/scipy/pyprima/cobyla/update.py +0 -0
- /divi/{exp → extern}/scipy/pyprima/common/__init__.py +0 -0
- /divi/{exp → extern}/scipy/pyprima/common/_bounds.py +0 -0
- /divi/{exp → extern}/scipy/pyprima/common/_linear_constraints.py +0 -0
- /divi/{exp → extern}/scipy/pyprima/common/_nonlinear_constraints.py +0 -0
- /divi/{exp → extern}/scipy/pyprima/common/_project.py +0 -0
- /divi/{exp → extern}/scipy/pyprima/common/checkbreak.py +0 -0
- /divi/{exp → extern}/scipy/pyprima/common/consts.py +0 -0
- /divi/{exp → extern}/scipy/pyprima/common/evaluate.py +0 -0
- /divi/{exp → extern}/scipy/pyprima/common/history.py +0 -0
- /divi/{exp → extern}/scipy/pyprima/common/infos.py +0 -0
- /divi/{exp → extern}/scipy/pyprima/common/linalg.py +0 -0
- /divi/{exp → extern}/scipy/pyprima/common/message.py +0 -0
- /divi/{exp → extern}/scipy/pyprima/common/powalg.py +0 -0
- /divi/{exp → extern}/scipy/pyprima/common/preproc.py +0 -0
- /divi/{exp → extern}/scipy/pyprima/common/present.py +0 -0
- /divi/{exp → extern}/scipy/pyprima/common/ratio.py +0 -0
- /divi/{exp → extern}/scipy/pyprima/common/redrho.py +0 -0
- /divi/{exp → extern}/scipy/pyprima/common/selectx.py +0 -0
- {qoro_divi-0.3.3.dist-info → qoro_divi-0.3.5.dist-info}/LICENSE +0 -0
- {qoro_divi-0.3.3.dist-info → qoro_divi-0.3.5.dist-info}/LICENSES/.license-header +0 -0
- {qoro_divi-0.3.3.dist-info → qoro_divi-0.3.5.dist-info}/LICENSES/Apache-2.0.txt +0 -0
- {qoro_divi-0.3.3.dist-info → qoro_divi-0.3.5.dist-info}/WHEEL +0 -0
divi/__init__.py
CHANGED
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
# SPDX-FileCopyrightText: 2025 Qoro Quantum Ltd <divi@qoroquantum.de>
|
|
2
|
+
#
|
|
3
|
+
# SPDX-License-Identifier: Apache-2.0
|
|
4
|
+
|
|
5
|
+
from ._circuit_runner import CircuitRunner
|
|
6
|
+
from ._parallel_simulator import ParallelSimulator
|
|
7
|
+
from ._qoro_service import JobStatus, JobType, QoroService
|
|
@@ -0,0 +1,46 @@
|
|
|
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
|
+
|
|
7
|
+
|
|
8
|
+
class CircuitRunner(ABC):
|
|
9
|
+
"""
|
|
10
|
+
A generic interface for anything that can "run" quantum circuits.
|
|
11
|
+
"""
|
|
12
|
+
|
|
13
|
+
def __init__(self, shots: int):
|
|
14
|
+
if shots <= 0:
|
|
15
|
+
raise ValueError(f"Shots must be a positive integer. Got {shots}.")
|
|
16
|
+
|
|
17
|
+
self._shots = shots
|
|
18
|
+
|
|
19
|
+
@property
|
|
20
|
+
def shots(self):
|
|
21
|
+
"""
|
|
22
|
+
Get the number of measurement shots for circuit execution.
|
|
23
|
+
|
|
24
|
+
Returns:
|
|
25
|
+
int: Number of shots configured for this runner.
|
|
26
|
+
"""
|
|
27
|
+
return self._shots
|
|
28
|
+
|
|
29
|
+
@abstractmethod
|
|
30
|
+
def submit_circuits(self, circuits: dict[str, str], **kwargs):
|
|
31
|
+
"""
|
|
32
|
+
Submit quantum circuits for execution.
|
|
33
|
+
|
|
34
|
+
This abstract method must be implemented by subclasses to define how
|
|
35
|
+
circuits are executed on their respective backends (simulator, hardware, etc.).
|
|
36
|
+
|
|
37
|
+
Args:
|
|
38
|
+
circuits (dict[str, str]): Dictionary mapping circuit labels to their
|
|
39
|
+
OpenQASM string representations.
|
|
40
|
+
**kwargs: Additional backend-specific parameters for circuit execution.
|
|
41
|
+
|
|
42
|
+
Returns:
|
|
43
|
+
The return type depends on the backend implementation. Typically returns
|
|
44
|
+
measurement results or a job identifier.
|
|
45
|
+
"""
|
|
46
|
+
pass
|
|
@@ -18,7 +18,7 @@ from qiskit.providers import Backend
|
|
|
18
18
|
from qiskit_aer import AerSimulator
|
|
19
19
|
from qiskit_aer.noise import NoiseModel
|
|
20
20
|
|
|
21
|
-
from divi.
|
|
21
|
+
from divi.backends import CircuitRunner
|
|
22
22
|
|
|
23
23
|
logger = logging.getLogger(__name__)
|
|
24
24
|
|
|
@@ -66,16 +66,20 @@ class ParallelSimulator(CircuitRunner):
|
|
|
66
66
|
simulation_seed: int | None = None,
|
|
67
67
|
qiskit_backend: Backend | Literal["auto"] | None = None,
|
|
68
68
|
noise_model: NoiseModel | None = None,
|
|
69
|
+
_deterministic_execution: bool = False,
|
|
69
70
|
):
|
|
70
71
|
"""
|
|
71
|
-
A
|
|
72
|
+
A parallel wrapper around Qiskit's AerSimulator using Qiskit's built-in parallelism.
|
|
72
73
|
|
|
73
74
|
Args:
|
|
74
|
-
n_processes (int, optional): Number of parallel processes to use for
|
|
75
|
+
n_processes (int, optional): Number of parallel processes to use for transpilation and
|
|
76
|
+
simulation. Defaults to 2. This sets both the transpile num_processes and
|
|
77
|
+
AerSimulator's max_parallel_experiments.
|
|
75
78
|
shots (int, optional): Number of shots to perform. Defaults to 5000.
|
|
76
79
|
simulation_seed (int, optional): Seed for the random number generator to ensure reproducibility. Defaults to None.
|
|
77
|
-
|
|
78
|
-
|
|
80
|
+
qiskit_backend (Backend | Literal["auto"] | None, optional): A Qiskit backend to initiate the simulator from.
|
|
81
|
+
If "auto" is passed, the best-fit most recent fake backend will be chosen for the given circuit.
|
|
82
|
+
Defaults to None, resulting in noiseless simulation.
|
|
79
83
|
noise_model (NoiseModel, optional): Qiskit noise model to use in simulation. Defaults to None.
|
|
80
84
|
"""
|
|
81
85
|
super().__init__(shots=shots)
|
|
@@ -91,62 +95,138 @@ class ParallelSimulator(CircuitRunner):
|
|
|
91
95
|
self.simulation_seed = simulation_seed
|
|
92
96
|
self.qiskit_backend = qiskit_backend
|
|
93
97
|
self.noise_model = noise_model
|
|
98
|
+
self._deterministic_execution = _deterministic_execution
|
|
94
99
|
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
shots: int,
|
|
99
|
-
simulation_seed: int | None = None,
|
|
100
|
-
qiskit_backend: Backend | None = None,
|
|
101
|
-
noise_model: NoiseModel | None = None,
|
|
102
|
-
):
|
|
103
|
-
circuit_label, circuit = circuit_data
|
|
104
|
-
|
|
105
|
-
qiskit_circuit = QuantumCircuit.from_qasm_str(circuit)
|
|
100
|
+
def set_seed(self, seed: int):
|
|
101
|
+
"""
|
|
102
|
+
Set the random seed for circuit simulation.
|
|
106
103
|
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
)
|
|
104
|
+
Args:
|
|
105
|
+
seed (int): Seed value for the random number generator used in simulation.
|
|
106
|
+
"""
|
|
107
|
+
self.simulation_seed = seed
|
|
112
108
|
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
transpiled_circuit = transpile(qiskit_circuit, aer_simulator)
|
|
109
|
+
def _execute_circuits_deterministically(
|
|
110
|
+
self, circuit_labels: list[str], transpiled_circuits: list, resolved_backend
|
|
111
|
+
) -> list[dict]:
|
|
112
|
+
"""
|
|
113
|
+
Execute circuits individually for debugging purposes.
|
|
119
114
|
|
|
120
|
-
|
|
121
|
-
|
|
115
|
+
This method ensures deterministic results by running each circuit with its own
|
|
116
|
+
simulator instance and the same seed. Used internally for debugging non-deterministic
|
|
117
|
+
behavior in batch execution.
|
|
122
118
|
|
|
123
|
-
|
|
124
|
-
|
|
119
|
+
Args:
|
|
120
|
+
circuit_labels: List of circuit labels
|
|
121
|
+
transpiled_circuits: List of transpiled QuantumCircuit objects
|
|
122
|
+
resolved_backend: Resolved backend for simulator creation
|
|
125
123
|
|
|
126
|
-
|
|
124
|
+
Returns:
|
|
125
|
+
List of result dictionaries
|
|
126
|
+
"""
|
|
127
|
+
results = []
|
|
128
|
+
for i, (label, transpiled_circuit) in enumerate(
|
|
129
|
+
zip(circuit_labels, transpiled_circuits)
|
|
130
|
+
):
|
|
131
|
+
# Create a new simulator instance for each circuit with the same seed
|
|
132
|
+
if resolved_backend is not None:
|
|
133
|
+
circuit_simulator = AerSimulator.from_backend(resolved_backend)
|
|
134
|
+
else:
|
|
135
|
+
circuit_simulator = AerSimulator(noise_model=self.noise_model)
|
|
136
|
+
|
|
137
|
+
if self.simulation_seed is not None:
|
|
138
|
+
circuit_simulator.set_option("seed_simulator", self.simulation_seed)
|
|
139
|
+
|
|
140
|
+
# Run the single circuit
|
|
141
|
+
job = circuit_simulator.run(transpiled_circuit, shots=self.shots)
|
|
142
|
+
circuit_result = job.result()
|
|
143
|
+
counts = circuit_result.get_counts(0)
|
|
144
|
+
results.append({"label": label, "results": dict(counts)})
|
|
127
145
|
|
|
128
|
-
|
|
129
|
-
self.simulation_seed = seed
|
|
146
|
+
return results
|
|
130
147
|
|
|
131
148
|
def submit_circuits(self, circuits: dict[str, str]):
|
|
149
|
+
"""
|
|
150
|
+
Submit multiple circuits for parallel simulation using Qiskit's built-in parallelism.
|
|
151
|
+
|
|
152
|
+
Uses Qiskit's native batch transpilation and execution, which handles parallelism
|
|
153
|
+
internally.
|
|
154
|
+
|
|
155
|
+
Args:
|
|
156
|
+
circuits (dict[str, str]): Dictionary mapping circuit labels to OpenQASM
|
|
157
|
+
string representations.
|
|
158
|
+
|
|
159
|
+
Returns:
|
|
160
|
+
list[dict]: List of result dictionaries, each containing:
|
|
161
|
+
- 'label' (str): Circuit identifier
|
|
162
|
+
- 'results' (dict): Measurement counts as {bitstring: count}
|
|
163
|
+
"""
|
|
132
164
|
logger.debug(
|
|
133
165
|
f"Simulating {len(circuits)} circuits with {self.n_processes} processes"
|
|
134
166
|
)
|
|
135
167
|
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
168
|
+
# Convert QASM strings to QuantumCircuit objects
|
|
169
|
+
circuit_labels = list(circuits.keys())
|
|
170
|
+
qiskit_circuits = [
|
|
171
|
+
QuantumCircuit.from_qasm_str(qasm) for qasm in circuits.values()
|
|
172
|
+
]
|
|
173
|
+
|
|
174
|
+
# Determine backend for transpilation
|
|
175
|
+
if self.qiskit_backend == "auto":
|
|
176
|
+
# For "auto", find the maximum number of qubits across all circuits to determine backend
|
|
177
|
+
max_qubits_circ = max(qiskit_circuits, key=lambda x: x.num_qubits)
|
|
178
|
+
resolved_backend = _find_best_fake_backend(max_qubits_circ)[-1]()
|
|
179
|
+
elif self.qiskit_backend is not None:
|
|
180
|
+
resolved_backend = self.qiskit_backend
|
|
181
|
+
else:
|
|
182
|
+
resolved_backend = None
|
|
183
|
+
|
|
184
|
+
# Create simulator
|
|
185
|
+
if resolved_backend is not None:
|
|
186
|
+
aer_simulator = AerSimulator.from_backend(resolved_backend)
|
|
187
|
+
else:
|
|
188
|
+
aer_simulator = AerSimulator(noise_model=self.noise_model)
|
|
189
|
+
|
|
190
|
+
# Set simulator options for parallelism
|
|
191
|
+
# Note: We don't set seed_simulator here because we need different seeds for each circuit
|
|
192
|
+
# to ensure deterministic results when running multiple circuits in parallel
|
|
193
|
+
aer_simulator.set_options(max_parallel_experiments=self.n_processes)
|
|
194
|
+
|
|
195
|
+
# Batch transpile all circuits (Qiskit handles parallelism internally)
|
|
196
|
+
transpiled_circuits = transpile(
|
|
197
|
+
qiskit_circuits, aer_simulator, num_processes=self.n_processes
|
|
198
|
+
)
|
|
199
|
+
|
|
200
|
+
# Use deterministic execution for debugging if enabled
|
|
201
|
+
if self._deterministic_execution:
|
|
202
|
+
return self._execute_circuits_deterministically(
|
|
203
|
+
circuit_labels, transpiled_circuits, resolved_backend
|
|
149
204
|
)
|
|
205
|
+
|
|
206
|
+
# Batch execution with metadata checking for non-deterministic behavior
|
|
207
|
+
job = aer_simulator.run(transpiled_circuits, shots=self.shots)
|
|
208
|
+
batch_result = job.result()
|
|
209
|
+
|
|
210
|
+
# Check metadata to detect non-deterministic behavior
|
|
211
|
+
metadata = batch_result.metadata
|
|
212
|
+
parallel_experiments = metadata.get("parallel_experiments", 1)
|
|
213
|
+
omp_nested = metadata.get("omp_nested", False)
|
|
214
|
+
|
|
215
|
+
# If parallel execution is detected and we have a seed, warn about potential non-determinism
|
|
216
|
+
if parallel_experiments > 1 and self.simulation_seed is not None:
|
|
217
|
+
logger.warning(
|
|
218
|
+
f"Parallel execution detected (parallel_experiments={parallel_experiments}, "
|
|
219
|
+
f"omp_nested={omp_nested}). Results may not be deterministic across different "
|
|
220
|
+
"grouping strategies. Consider enabling deterministic mode for "
|
|
221
|
+
"deterministic results."
|
|
222
|
+
)
|
|
223
|
+
|
|
224
|
+
# Extract results and match with labels
|
|
225
|
+
results = []
|
|
226
|
+
for i, label in enumerate(circuit_labels):
|
|
227
|
+
counts = batch_result.get_counts(i)
|
|
228
|
+
results.append({"label": label, "results": dict(counts)})
|
|
229
|
+
|
|
150
230
|
return results
|
|
151
231
|
|
|
152
232
|
@staticmethod
|
|
@@ -186,15 +266,18 @@ class ParallelSimulator(CircuitRunner):
|
|
|
186
266
|
|
|
187
267
|
op_name = node.name
|
|
188
268
|
|
|
269
|
+
# Determine qubit indices for the operation
|
|
189
270
|
if node.num_clbits == 1:
|
|
190
271
|
idx = (node.cargs[0]._index,)
|
|
191
|
-
|
|
192
|
-
if op_name != "measure" and node.num_qubits > 0:
|
|
272
|
+
elif op_name != "measure" and node.num_qubits > 0:
|
|
193
273
|
idx = tuple(qarg._index for qarg in node.qargs)
|
|
274
|
+
else:
|
|
275
|
+
# Skip operations without qubits or measurements without classical bits
|
|
276
|
+
continue
|
|
194
277
|
|
|
195
278
|
try:
|
|
196
279
|
total_run_time_s += (
|
|
197
|
-
|
|
280
|
+
resolved_backend.instruction_durations.duration_by_name_qubits[
|
|
198
281
|
(op_name, idx)
|
|
199
282
|
][0]
|
|
200
283
|
)
|
|
@@ -208,7 +291,7 @@ class ParallelSimulator(CircuitRunner):
|
|
|
208
291
|
@staticmethod
|
|
209
292
|
def estimate_run_time_batch(
|
|
210
293
|
circuits: list[str] | None = None,
|
|
211
|
-
|
|
294
|
+
precomputed_durations: list[float] | None = None,
|
|
212
295
|
n_qpus: int = 5,
|
|
213
296
|
**transpilation_kwargs,
|
|
214
297
|
) -> float:
|
|
@@ -225,7 +308,7 @@ class ParallelSimulator(CircuitRunner):
|
|
|
225
308
|
"""
|
|
226
309
|
|
|
227
310
|
# Compute the run time estimates for each given circuit, in descending order
|
|
228
|
-
if
|
|
311
|
+
if precomputed_durations is None:
|
|
229
312
|
with Pool() as p:
|
|
230
313
|
estimated_run_times = p.map(
|
|
231
314
|
partial(
|
|
@@ -237,7 +320,7 @@ class ParallelSimulator(CircuitRunner):
|
|
|
237
320
|
)
|
|
238
321
|
estimated_run_times_sorted = sorted(estimated_run_times, reverse=True)
|
|
239
322
|
else:
|
|
240
|
-
estimated_run_times_sorted = sorted(
|
|
323
|
+
estimated_run_times_sorted = sorted(precomputed_durations, reverse=True)
|
|
241
324
|
|
|
242
325
|
# Just return the longest run time if there are enough QPUs
|
|
243
326
|
if n_qpus >= len(estimated_run_times_sorted):
|