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/__init__.py CHANGED
@@ -2,7 +2,6 @@
2
2
  #
3
3
  # SPDX-License-Identifier: Apache-2.0
4
4
 
5
- from .qlogger import enable_logging
6
- from .qoro_service import QoroService
5
+ from .reporting import enable_logging
7
6
 
8
7
  enable_logging()
@@ -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.interfaces import CircuitRunner
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 multi-process wrapper around Qiskit's AerSimulator.
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 simulation. Defaults to 2.
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
- backend (Backend or "auto, optional): A Qiskit backend to initiate the simulator from. If "auto" is passed,
78
- the best-fit most recent fake backend will be chosen for the given circuit. Defaults to None, resulting in noiseless simulation.
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
- @staticmethod
96
- def simulate_circuit(
97
- circuit_data: tuple[str, str],
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
- resolved_backend = (
108
- _find_best_fake_backend(qiskit_circuit)[-1]()
109
- if qiskit_backend == "auto"
110
- else qiskit_backend
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
- aer_simulator = (
114
- AerSimulator.from_backend(resolved_backend)
115
- if qiskit_backend
116
- else AerSimulator(noise_model=noise_model)
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
- aer_simulator.set_option("seed_simulator", simulation_seed)
121
- job = aer_simulator.run(transpiled_circuit, shots=shots)
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
- result = job.result()
124
- counts = result.get_counts(0)
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
- return {"label": circuit_label, "results": dict(counts)}
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
- def set_seed(self, seed: int):
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
- with Pool(processes=self.n_processes) as pool:
137
- results = pool.starmap(
138
- self.simulate_circuit,
139
- [
140
- (
141
- circuit,
142
- self.shots,
143
- self.simulation_seed,
144
- self.qiskit_backend,
145
- self.noise_model,
146
- )
147
- for circuit in circuits.items()
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
- qiskit_backend.instruction_durations.duration_by_name_qubits[
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
- precomputed_duration: list[float] | None = None,
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 precomputed_duration is None:
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(precomputed_duration, reverse=True)
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):