qoro-divi 0.3.4__py3-none-any.whl → 0.4.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.

Potentially problematic release.


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

divi/backends/__init__.py CHANGED
@@ -4,4 +4,4 @@
4
4
 
5
5
  from ._circuit_runner import CircuitRunner
6
6
  from ._parallel_simulator import ParallelSimulator
7
- from ._qoro_service import JobStatus, JobType, QoroService
7
+ from ._qoro_service import JobConfig, JobStatus, JobType, QoroService
@@ -18,8 +18,50 @@ class CircuitRunner(ABC):
18
18
 
19
19
  @property
20
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
+ """
21
27
  return self._shots
22
28
 
29
+ @property
30
+ @abstractmethod
31
+ def supports_expval(self) -> bool:
32
+ """
33
+ Whether the backend supports expectation value measurements.
34
+ """
35
+ return False
36
+
37
+ @property
38
+ @abstractmethod
39
+ def is_async(self) -> bool:
40
+ """
41
+ Whether the backend executes circuits asynchronously.
42
+
43
+ Returns:
44
+ bool: True if the backend returns a job ID and requires polling
45
+ for results (e.g., QoroService). False if the backend
46
+ returns results immediately (e.g., ParallelSimulator).
47
+ """
48
+ return False
49
+
23
50
  @abstractmethod
24
51
  def submit_circuits(self, circuits: dict[str, str], **kwargs):
52
+ """
53
+ Submit quantum circuits for execution.
54
+
55
+ This abstract method must be implemented by subclasses to define how
56
+ circuits are executed on their respective backends (simulator, hardware, etc.).
57
+
58
+ Args:
59
+ circuits (dict[str, str]): Dictionary mapping circuit labels to their
60
+ OpenQASM string representations.
61
+ **kwargs: Additional backend-specific parameters for circuit execution.
62
+
63
+ Returns:
64
+ The return type depends on the backend implementation. Typically returns
65
+ measurement results or a job identifier.
66
+ """
25
67
  pass
@@ -66,12 +66,15 @@ 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
80
  qiskit_backend (Backend | Literal["auto"] | None, optional): A Qiskit backend to initiate the simulator from.
@@ -92,62 +95,152 @@ class ParallelSimulator(CircuitRunner):
92
95
  self.simulation_seed = simulation_seed
93
96
  self.qiskit_backend = qiskit_backend
94
97
  self.noise_model = noise_model
98
+ self._deterministic_execution = _deterministic_execution
95
99
 
96
- @staticmethod
97
- def simulate_circuit(
98
- circuit_data: tuple[str, str],
99
- shots: int,
100
- simulation_seed: int | None = None,
101
- qiskit_backend: Backend | None = None,
102
- noise_model: NoiseModel | None = None,
103
- ):
104
- circuit_label, circuit = circuit_data
100
+ def set_seed(self, seed: int):
101
+ """
102
+ Set the random seed for circuit simulation.
105
103
 
106
- qiskit_circuit = QuantumCircuit.from_qasm_str(circuit)
104
+ Args:
105
+ seed (int): Seed value for the random number generator used in simulation.
106
+ """
107
+ self.simulation_seed = seed
107
108
 
108
- resolved_backend = (
109
- _find_best_fake_backend(qiskit_circuit)[-1]()
110
- if qiskit_backend == "auto"
111
- else qiskit_backend
112
- )
109
+ @property
110
+ def supports_expval(self) -> bool:
111
+ """
112
+ Whether the backend supports expectation value measurements.
113
+ """
114
+ return False
113
115
 
114
- aer_simulator = (
115
- AerSimulator.from_backend(resolved_backend)
116
- if qiskit_backend
117
- else AerSimulator(noise_model=noise_model)
118
- )
119
- transpiled_circuit = transpile(qiskit_circuit, aer_simulator)
116
+ @property
117
+ def is_async(self) -> bool:
118
+ """
119
+ Whether the backend executes circuits asynchronously.
120
+ """
121
+ return False
120
122
 
121
- aer_simulator.set_option("seed_simulator", simulation_seed)
122
- job = aer_simulator.run(transpiled_circuit, shots=shots)
123
+ def _execute_circuits_deterministically(
124
+ self, circuit_labels: list[str], transpiled_circuits: list, resolved_backend
125
+ ) -> list[dict]:
126
+ """
127
+ Execute circuits individually for debugging purposes.
123
128
 
124
- result = job.result()
125
- counts = result.get_counts(0)
129
+ This method ensures deterministic results by running each circuit with its own
130
+ simulator instance and the same seed. Used internally for debugging non-deterministic
131
+ behavior in batch execution.
126
132
 
127
- return {"label": circuit_label, "results": dict(counts)}
133
+ Args:
134
+ circuit_labels: List of circuit labels
135
+ transpiled_circuits: List of transpiled QuantumCircuit objects
136
+ resolved_backend: Resolved backend for simulator creation
128
137
 
129
- def set_seed(self, seed: int):
130
- self.simulation_seed = seed
138
+ Returns:
139
+ List of result dictionaries
140
+ """
141
+ results = []
142
+ for i, (label, transpiled_circuit) in enumerate(
143
+ zip(circuit_labels, transpiled_circuits)
144
+ ):
145
+ # Create a new simulator instance for each circuit with the same seed
146
+ if resolved_backend is not None:
147
+ circuit_simulator = AerSimulator.from_backend(resolved_backend)
148
+ else:
149
+ circuit_simulator = AerSimulator(noise_model=self.noise_model)
150
+
151
+ if self.simulation_seed is not None:
152
+ circuit_simulator.set_option("seed_simulator", self.simulation_seed)
153
+
154
+ # Run the single circuit
155
+ job = circuit_simulator.run(transpiled_circuit, shots=self.shots)
156
+ circuit_result = job.result()
157
+ counts = circuit_result.get_counts(0)
158
+ results.append({"label": label, "results": dict(counts)})
159
+
160
+ return results
131
161
 
132
162
  def submit_circuits(self, circuits: dict[str, str]):
163
+ """
164
+ Submit multiple circuits for parallel simulation using Qiskit's built-in parallelism.
165
+
166
+ Uses Qiskit's native batch transpilation and execution, which handles parallelism
167
+ internally.
168
+
169
+ Args:
170
+ circuits (dict[str, str]): Dictionary mapping circuit labels to OpenQASM
171
+ string representations.
172
+
173
+ Returns:
174
+ list[dict]: List of result dictionaries, each containing:
175
+ - 'label' (str): Circuit identifier
176
+ - 'results' (dict): Measurement counts as {bitstring: count}
177
+ """
133
178
  logger.debug(
134
179
  f"Simulating {len(circuits)} circuits with {self.n_processes} processes"
135
180
  )
136
181
 
137
- with Pool(processes=self.n_processes) as pool:
138
- results = pool.starmap(
139
- self.simulate_circuit,
140
- [
141
- (
142
- circuit,
143
- self.shots,
144
- self.simulation_seed,
145
- self.qiskit_backend,
146
- self.noise_model,
147
- )
148
- for circuit in circuits.items()
149
- ],
182
+ # Convert QASM strings to QuantumCircuit objects
183
+ circuit_labels = list(circuits.keys())
184
+ qiskit_circuits = [
185
+ QuantumCircuit.from_qasm_str(qasm) for qasm in circuits.values()
186
+ ]
187
+
188
+ # Determine backend for transpilation
189
+ if self.qiskit_backend == "auto":
190
+ # For "auto", find the maximum number of qubits across all circuits to determine backend
191
+ max_qubits_circ = max(qiskit_circuits, key=lambda x: x.num_qubits)
192
+ resolved_backend = _find_best_fake_backend(max_qubits_circ)[-1]()
193
+ elif self.qiskit_backend is not None:
194
+ resolved_backend = self.qiskit_backend
195
+ else:
196
+ resolved_backend = None
197
+
198
+ # Create simulator
199
+ if resolved_backend is not None:
200
+ aer_simulator = AerSimulator.from_backend(resolved_backend)
201
+ else:
202
+ aer_simulator = AerSimulator(noise_model=self.noise_model)
203
+
204
+ # Set simulator options for parallelism
205
+ # Note: We don't set seed_simulator here because we need different seeds for each circuit
206
+ # to ensure deterministic results when running multiple circuits in parallel
207
+ aer_simulator.set_options(max_parallel_experiments=self.n_processes)
208
+
209
+ # Batch transpile all circuits (Qiskit handles parallelism internally)
210
+ transpiled_circuits = transpile(
211
+ qiskit_circuits, aer_simulator, num_processes=self.n_processes
212
+ )
213
+
214
+ # Use deterministic execution for debugging if enabled
215
+ if self._deterministic_execution:
216
+ return self._execute_circuits_deterministically(
217
+ circuit_labels, transpiled_circuits, resolved_backend
150
218
  )
219
+
220
+ # Batch execution with metadata checking for non-deterministic behavior
221
+ job = aer_simulator.run(transpiled_circuits, shots=self.shots)
222
+ batch_result = job.result()
223
+
224
+ # Check metadata to detect non-deterministic behavior
225
+ metadata = batch_result.metadata
226
+ parallel_experiments = metadata.get("parallel_experiments", 1)
227
+ omp_nested = metadata.get("omp_nested", False)
228
+
229
+ # If parallel execution is detected and we have a seed, warn about potential non-determinism
230
+ if parallel_experiments > 1 and self.simulation_seed is not None:
231
+ logger.warning(
232
+ f"Parallel execution detected (parallel_experiments={parallel_experiments}, "
233
+ f"omp_nested={omp_nested}). Results may not be deterministic across different "
234
+ "grouping strategies. Consider enabling deterministic mode for "
235
+ "deterministic results."
236
+ )
237
+
238
+ # Extract results and match with labels
239
+ results = []
240
+ for i, label in enumerate(circuit_labels):
241
+ counts = batch_result.get_counts(i)
242
+ results.append({"label": label, "results": dict(counts)})
243
+
151
244
  return results
152
245
 
153
246
  @staticmethod
@@ -187,15 +280,18 @@ class ParallelSimulator(CircuitRunner):
187
280
 
188
281
  op_name = node.name
189
282
 
283
+ # Determine qubit indices for the operation
190
284
  if node.num_clbits == 1:
191
285
  idx = (node.cargs[0]._index,)
192
-
193
- if op_name != "measure" and node.num_qubits > 0:
286
+ elif op_name != "measure" and node.num_qubits > 0:
194
287
  idx = tuple(qarg._index for qarg in node.qargs)
288
+ else:
289
+ # Skip operations without qubits or measurements without classical bits
290
+ continue
195
291
 
196
292
  try:
197
293
  total_run_time_s += (
198
- qiskit_backend.instruction_durations.duration_by_name_qubits[
294
+ resolved_backend.instruction_durations.duration_by_name_qubits[
199
295
  (op_name, idx)
200
296
  ][0]
201
297
  )
@@ -209,7 +305,7 @@ class ParallelSimulator(CircuitRunner):
209
305
  @staticmethod
210
306
  def estimate_run_time_batch(
211
307
  circuits: list[str] | None = None,
212
- precomputed_duration: list[float] | None = None,
308
+ precomputed_durations: list[float] | None = None,
213
309
  n_qpus: int = 5,
214
310
  **transpilation_kwargs,
215
311
  ) -> float:
@@ -226,7 +322,7 @@ class ParallelSimulator(CircuitRunner):
226
322
  """
227
323
 
228
324
  # Compute the run time estimates for each given circuit, in descending order
229
- if precomputed_duration is None:
325
+ if precomputed_durations is None:
230
326
  with Pool() as p:
231
327
  estimated_run_times = p.map(
232
328
  partial(
@@ -238,7 +334,7 @@ class ParallelSimulator(CircuitRunner):
238
334
  )
239
335
  estimated_run_times_sorted = sorted(estimated_run_times, reverse=True)
240
336
  else:
241
- estimated_run_times_sorted = sorted(precomputed_duration, reverse=True)
337
+ estimated_run_times_sorted = sorted(precomputed_durations, reverse=True)
242
338
 
243
339
  # Just return the longest run time if there are enough QPUs
244
340
  if n_qpus >= len(estimated_run_times_sorted):