qoro-divi 0.3.4__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.

@@ -18,8 +18,29 @@ 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
 
23
29
  @abstractmethod
24
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
+ """
25
46
  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,138 @@ 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
105
-
106
- qiskit_circuit = QuantumCircuit.from_qasm_str(circuit)
100
+ def set_seed(self, seed: int):
101
+ """
102
+ Set the random seed for circuit simulation.
107
103
 
108
- resolved_backend = (
109
- _find_best_fake_backend(qiskit_circuit)[-1]()
110
- if qiskit_backend == "auto"
111
- else qiskit_backend
112
- )
104
+ Args:
105
+ seed (int): Seed value for the random number generator used in simulation.
106
+ """
107
+ self.simulation_seed = seed
113
108
 
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)
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.
120
114
 
121
- aer_simulator.set_option("seed_simulator", simulation_seed)
122
- 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.
123
118
 
124
- result = job.result()
125
- 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
126
123
 
127
- 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)})
128
145
 
129
- def set_seed(self, seed: int):
130
- self.simulation_seed = seed
146
+ return results
131
147
 
132
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
+ """
133
164
  logger.debug(
134
165
  f"Simulating {len(circuits)} circuits with {self.n_processes} processes"
135
166
  )
136
167
 
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
- ],
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
150
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
+
151
230
  return results
152
231
 
153
232
  @staticmethod
@@ -187,15 +266,18 @@ class ParallelSimulator(CircuitRunner):
187
266
 
188
267
  op_name = node.name
189
268
 
269
+ # Determine qubit indices for the operation
190
270
  if node.num_clbits == 1:
191
271
  idx = (node.cargs[0]._index,)
192
-
193
- if op_name != "measure" and node.num_qubits > 0:
272
+ elif op_name != "measure" and node.num_qubits > 0:
194
273
  idx = tuple(qarg._index for qarg in node.qargs)
274
+ else:
275
+ # Skip operations without qubits or measurements without classical bits
276
+ continue
195
277
 
196
278
  try:
197
279
  total_run_time_s += (
198
- qiskit_backend.instruction_durations.duration_by_name_qubits[
280
+ resolved_backend.instruction_durations.duration_by_name_qubits[
199
281
  (op_name, idx)
200
282
  ][0]
201
283
  )
@@ -209,7 +291,7 @@ class ParallelSimulator(CircuitRunner):
209
291
  @staticmethod
210
292
  def estimate_run_time_batch(
211
293
  circuits: list[str] | None = None,
212
- precomputed_duration: list[float] | None = None,
294
+ precomputed_durations: list[float] | None = None,
213
295
  n_qpus: int = 5,
214
296
  **transpilation_kwargs,
215
297
  ) -> float:
@@ -226,7 +308,7 @@ class ParallelSimulator(CircuitRunner):
226
308
  """
227
309
 
228
310
  # Compute the run time estimates for each given circuit, in descending order
229
- if precomputed_duration is None:
311
+ if precomputed_durations is None:
230
312
  with Pool() as p:
231
313
  estimated_run_times = p.map(
232
314
  partial(
@@ -238,7 +320,7 @@ class ParallelSimulator(CircuitRunner):
238
320
  )
239
321
  estimated_run_times_sorted = sorted(estimated_run_times, reverse=True)
240
322
  else:
241
- estimated_run_times_sorted = sorted(precomputed_duration, reverse=True)
323
+ estimated_run_times_sorted = sorted(precomputed_durations, reverse=True)
242
324
 
243
325
  # Just return the longest run time if there are enough QPUs
244
326
  if n_qpus >= len(estimated_run_times_sorted):