qoro-divi 0.5.0__py3-none-any.whl → 0.6.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.
divi/backends/__init__.py CHANGED
@@ -1,7 +1,8 @@
1
- # SPDX-FileCopyrightText: 2025 Qoro Quantum Ltd <divi@qoroquantum.de>
1
+ # SPDX-FileCopyrightText: 2025-2026 Qoro Quantum Ltd <divi@qoroquantum.de>
2
2
  #
3
3
  # SPDX-License-Identifier: Apache-2.0
4
4
 
5
+ from ._backend_properties_conversion import create_backend_from_properties
5
6
  from ._circuit_runner import CircuitRunner
6
7
  from ._execution_result import ExecutionResult
7
8
  from ._parallel_simulator import ParallelSimulator
@@ -0,0 +1,227 @@
1
+ # SPDX-FileCopyrightText: 2025-2026 Qoro Quantum Ltd <divi@qoroquantum.de>
2
+ #
3
+ # SPDX-License-Identifier: Apache-2.0
4
+
5
+ """Utilities for working with Qiskit BackendProperties and BackendV2 conversion."""
6
+
7
+ import datetime
8
+ from typing import Any
9
+
10
+ from qiskit.providers.fake_provider import GenericBackendV2
11
+ from qiskit_ibm_runtime.models.backend_properties import BackendProperties
12
+
13
+
14
+ def _normalize_properties(
15
+ properties: dict[str, Any],
16
+ default_date: datetime.datetime | None = None,
17
+ ) -> dict[str, Any]:
18
+ """
19
+ Preprocess an incomplete BackendProperties dictionary by filling in missing
20
+ required fields with sensible defaults.
21
+
22
+ This function makes it easier to create BackendProperties dictionaries by
23
+ allowing you to omit fields that have obvious defaults, such as:
24
+ - Missing top-level fields: `backend_name`, `backend_version`, `last_update_date`
25
+ - Missing `unit` field for dimensionless parameters (e.g., gate_error)
26
+ - Missing `general` field (empty list)
27
+ - Missing `gates` field (empty list)
28
+ - Missing `qubits` field (empty list)
29
+ - Missing `date` fields in Nduv objects
30
+
31
+ Args:
32
+ properties: Incomplete BackendProperties dictionary. Can omit:
33
+ - `unit` field in parameter/qubit Nduv objects (defaults to "" for
34
+ dimensionless quantities like gate_error, or inferred from name)
35
+ - `general` field (defaults to empty list)
36
+ - `gates` field (defaults to empty list)
37
+ - `qubits` field (defaults to empty list)
38
+ - `date` field in Nduv objects (defaults to current time or provided default)
39
+ default_date: Optional datetime to use for missing date fields.
40
+ If None, uses current time.
41
+
42
+ Returns:
43
+ Complete BackendProperties dictionary ready for BackendProperties.from_dict()
44
+
45
+ Example:
46
+ >>> props = {
47
+ ... "backend_name": "test",
48
+ ... "gates": [{
49
+ ... "gate": "sx",
50
+ ... "qubits": [0],
51
+ ... "parameters": [{
52
+ ... "name": "gate_error",
53
+ ... "value": 0.01,
54
+ ... # unit and date will be added automatically
55
+ ... }]
56
+ ... }]
57
+ ... }
58
+ >>> normalized = _normalize_properties(props)
59
+ >>> backend_props = BackendProperties.from_dict(normalized)
60
+ """
61
+ if default_date is None:
62
+ default_date = datetime.datetime.now()
63
+
64
+ # Create a shallow copy to avoid mutating the input
65
+ # (nested structures are rebuilt below to ensure no mutation)
66
+ normalized = properties.copy()
67
+
68
+ # Add missing required top-level fields
69
+ if "backend_name" not in normalized:
70
+ normalized["backend_name"] = "custom_backend"
71
+ if "backend_version" not in normalized:
72
+ normalized["backend_version"] = "1.0.0"
73
+ if "last_update_date" not in normalized:
74
+ normalized["last_update_date"] = default_date
75
+
76
+ # Add missing general field
77
+ if "general" not in normalized:
78
+ normalized["general"] = []
79
+
80
+ # Add missing gates field (required by BackendProperties)
81
+ if "gates" not in normalized:
82
+ normalized["gates"] = []
83
+
84
+ # Add missing qubits field (required by BackendProperties)
85
+ if "qubits" not in normalized:
86
+ normalized["qubits"] = []
87
+
88
+ # Normalize qubits (list of lists of Nduv objects)
89
+ if "qubits" in normalized:
90
+ normalized["qubits"] = [
91
+ [_normalize_nduv(param, default_date) for param in qubit_params]
92
+ for qubit_params in normalized["qubits"]
93
+ ]
94
+
95
+ # Normalize gates (list of gate dicts with parameters)
96
+ if "gates" in normalized:
97
+ normalized["gates"] = [
98
+ {
99
+ **gate,
100
+ "parameters": [
101
+ _normalize_nduv(param, default_date)
102
+ for param in gate.get("parameters", [])
103
+ ],
104
+ }
105
+ for gate in normalized["gates"]
106
+ ]
107
+
108
+ # Normalize general (list of Nduv objects)
109
+ if "general" in normalized and normalized["general"]:
110
+ normalized["general"] = [
111
+ _normalize_nduv(param, default_date) for param in normalized["general"]
112
+ ]
113
+
114
+ return normalized
115
+
116
+
117
+ def _normalize_nduv(
118
+ nduv: dict[str, Any], default_date: datetime.datetime
119
+ ) -> dict[str, Any]:
120
+ """
121
+ Normalize a single Nduv (Name, Date, Unit, Value) object by adding
122
+ missing required fields.
123
+
124
+ Args:
125
+ nduv: Nduv dictionary (may be incomplete)
126
+ default_date: Default date to use if missing
127
+
128
+ Returns:
129
+ Complete Nduv dictionary
130
+ """
131
+ normalized = nduv.copy()
132
+
133
+ # Add missing date field
134
+ if "date" not in normalized:
135
+ normalized["date"] = default_date
136
+
137
+ # Add missing unit field
138
+ if "unit" not in normalized:
139
+ name = normalized.get("name", "").lower()
140
+ # Dimensionless quantities
141
+ if name in ("gate_error", "readout_error", "prob"):
142
+ normalized["unit"] = ""
143
+ # Time-based quantities
144
+ elif name in ("t1", "t2", "gate_length", "readout_length"):
145
+ # Infer unit from common patterns, default to "ns" for gate_length
146
+ if name == "gate_length":
147
+ normalized["unit"] = "ns"
148
+ elif name in ("t1", "t2"):
149
+ normalized["unit"] = "us" # microseconds is common
150
+ else:
151
+ normalized["unit"] = "ns"
152
+ # Frequency-based quantities
153
+ elif name in ("frequency", "freq"):
154
+ normalized["unit"] = "GHz"
155
+ # Default to empty string for unknown quantities
156
+ else:
157
+ normalized["unit"] = ""
158
+
159
+ return normalized
160
+
161
+
162
+ def create_backend_from_properties(
163
+ properties: dict[str, Any],
164
+ n_qubits: int | None = None,
165
+ default_date: datetime.datetime | None = None,
166
+ ) -> GenericBackendV2:
167
+ """
168
+ Create a populated GenericBackendV2 from a BackendProperties dictionary.
169
+
170
+ This function handles the complete workflow:
171
+ 1. Normalizes the properties dictionary (fills in missing fields)
172
+ 2. Infers the number of qubits from the properties if not provided
173
+ 3. Creates a GenericBackendV2 backend
174
+ 4. Populates it with the normalized properties
175
+
176
+ Args:
177
+ properties: BackendProperties dictionary.
178
+ Missing fields will be filled automatically.
179
+ n_qubits: Optional number of qubits. If None, will be inferred from the
180
+ length of the "qubits" list in the properties dictionary.
181
+ default_date: Optional datetime to use for missing date fields.
182
+ If None, uses current time.
183
+
184
+ Returns:
185
+ GenericBackendV2 backend populated with the provided properties.
186
+
187
+ Raises:
188
+ ValueError: If n_qubits is not provided and cannot be inferred from properties
189
+ (i.e., qubits list is empty or missing), or if n_qubits is less than 1.
190
+
191
+ Example:
192
+ >>> props = {
193
+ ... "backend_name": "test",
194
+ ... "qubits": [[{"name": "T1", "value": 100.0}]], # 1 qubit
195
+ ... "gates": [{"gate": "sx", "qubits": [0], "parameters": []}]
196
+ ... }
197
+ >>> # Infer qubit count from properties (will be 1)
198
+ >>> backend = create_backend_from_properties(props)
199
+ >>> backend.n_qubits
200
+ 1
201
+ >>> # Override qubit count if needed
202
+ >>> backend_large = create_backend_from_properties(props, n_qubits=120)
203
+ >>> backend_large.n_qubits
204
+ 120
205
+ """
206
+ # Normalize the properties first
207
+ normalized_properties = _normalize_properties(properties, default_date)
208
+
209
+ # Infer number of qubits from qubits list length if not provided
210
+ if n_qubits is None:
211
+ n_qubits = len(normalized_properties.get("qubits", []))
212
+ if n_qubits == 0:
213
+ raise ValueError(
214
+ "n_qubits must be provided when properties dictionary has no qubits, "
215
+ "or qubits list must contain at least one qubit"
216
+ )
217
+
218
+ if n_qubits < 1:
219
+ raise ValueError("n_qubits must be at least 1")
220
+
221
+ # Create the backend
222
+ backend = GenericBackendV2(num_qubits=n_qubits)
223
+
224
+ # Populate with properties
225
+ backend._properties = BackendProperties.from_dict(normalized_properties)
226
+
227
+ return backend
@@ -1,4 +1,4 @@
1
- # SPDX-FileCopyrightText: 2025 Qoro Quantum Ltd <divi@qoroquantum.de>
1
+ # SPDX-FileCopyrightText: 2025-2026 Qoro Quantum Ltd <divi@qoroquantum.de>
2
2
  #
3
3
  # SPDX-License-Identifier: Apache-2.0
4
4
 
@@ -16,6 +16,7 @@ from qiskit import QuantumCircuit, transpile
16
16
  from qiskit.converters import circuit_to_dag
17
17
  from qiskit.dagcircuit import DAGOpNode
18
18
  from qiskit.providers import Backend
19
+ from qiskit.transpiler.exceptions import TranspilerError
19
20
  from qiskit_aer import AerSimulator
20
21
  from qiskit_aer.noise import NoiseModel
21
22
 
@@ -270,7 +271,7 @@ class ParallelSimulator(CircuitRunner):
270
271
  circuit_simulator = self._create_simulator(resolved_backend)
271
272
 
272
273
  if self.simulation_seed is not None:
273
- circuit_simulator.set_option("seed_simulator", self.simulation_seed)
274
+ circuit_simulator.set_options(seed_simulator=self.simulation_seed)
274
275
 
275
276
  # Run the single circuit
276
277
  job = circuit_simulator.run(transpiled_circuit, shots=self.shots)
@@ -421,7 +422,7 @@ class ParallelSimulator(CircuitRunner):
421
422
  )
422
423
 
423
424
  total_run_time_s = 0.0
424
- durations = resolved_backend.instruction_durations
425
+ durations = resolved_backend.target.durations()
425
426
 
426
427
  for node in circuit_to_dag(transpiled_circuit).longest_path():
427
428
  if not isinstance(node, DAGOpNode) or not node.num_qubits:
@@ -429,10 +430,9 @@ class ParallelSimulator(CircuitRunner):
429
430
 
430
431
  try:
431
432
  idx = tuple(q._index for q in node.qargs)
432
- total_run_time_s += durations.duration_by_name_qubits[(node.name, idx)][
433
- 0
434
- ]
435
- except KeyError:
433
+ duration = durations.get(node.name, idx, unit="s")
434
+ total_run_time_s += duration
435
+ except TranspilerError:
436
436
  if node.name != "barrier":
437
437
  warn(f"Instruction duration not found: {node.name}")
438
438
 
divi/circuits/__init__.py CHANGED
@@ -1,8 +1,13 @@
1
- # SPDX-FileCopyrightText: 2025 Qoro Quantum Ltd <divi@qoroquantum.de>
1
+ # SPDX-FileCopyrightText: 2025-2026 Qoro Quantum Ltd <divi@qoroquantum.de>
2
2
  #
3
3
  # SPDX-License-Identifier: Apache-2.0
4
4
 
5
- # isort: skip_file
6
5
  from ._qasm_conversion import to_openqasm
7
6
  from ._qasm_validation import is_valid_qasm, validate_qasm, validate_qasm_count_qubits
8
- from ._core import CircuitBundle, ExecutableQASMCircuit, MetaCircuit
7
+ from ._core import (
8
+ CircuitBundle,
9
+ ExecutableQASMCircuit,
10
+ MetaCircuit,
11
+ CircuitTag,
12
+ format_circuit_tag,
13
+ )
divi/circuits/_core.py CHANGED
@@ -1,11 +1,12 @@
1
- # SPDX-FileCopyrightText: 2025 Qoro Quantum Ltd <divi@qoroquantum.de>
1
+ # SPDX-FileCopyrightText: 2025-2026 Qoro Quantum Ltd <divi@qoroquantum.de>
2
2
  #
3
3
  # SPDX-License-Identifier: Apache-2.0
4
4
 
5
5
  import re
6
+ from collections.abc import Callable
6
7
  from dataclasses import dataclass, field
7
8
  from itertools import product
8
- from typing import Callable, Literal
9
+ from typing import Literal, NamedTuple
9
10
 
10
11
  import dill
11
12
  import numpy as np
@@ -96,11 +97,25 @@ def _create_final_postprocessing_fn(coefficients, partition_indices, num_total_o
96
97
  return final_postprocessing_fn
97
98
 
98
99
 
100
+ class CircuitTag(NamedTuple):
101
+ """Structured tag for identifying circuit executions."""
102
+
103
+ param_id: int
104
+ qem_name: str
105
+ qem_id: int
106
+ meas_id: int
107
+
108
+
109
+ def format_circuit_tag(tag: CircuitTag) -> str:
110
+ """Format a CircuitTag into its wire-safe string representation."""
111
+ return f"{tag.param_id}_{tag.qem_name}:{tag.qem_id}_{tag.meas_id}"
112
+
113
+
99
114
  @dataclass(frozen=True)
100
115
  class ExecutableQASMCircuit:
101
116
  """Represents a single, executable QASM circuit with its associated tag."""
102
117
 
103
- tag: str
118
+ tag: CircuitTag
104
119
  qasm: str
105
120
 
106
121
 
@@ -129,7 +144,7 @@ class CircuitBundle:
129
144
  return f"CircuitBundle ({len(self.executables)} executables)"
130
145
 
131
146
  @property
132
- def tags(self) -> list[str]:
147
+ def tags(self) -> list[CircuitTag]:
133
148
  """A list of tags for all executables in the bundle."""
134
149
  return [e.tag for e in self.executables]
135
150
 
@@ -312,7 +327,10 @@ class MetaCircuit:
312
327
  self.__dict__.update(state)
313
328
 
314
329
  def initialize_circuit_from_params(
315
- self, param_list, tag_prefix: str = "", precision: int | None = None
330
+ self,
331
+ param_list: npt.NDArray[np.floating] | list[float],
332
+ param_idx: int = 0,
333
+ precision: int | None = None,
316
334
  ) -> CircuitBundle:
317
335
  """
318
336
  Instantiate a concrete CircuitBundle by substituting symbolic parameters with values.
@@ -322,10 +340,11 @@ class MetaCircuit:
322
340
  concrete numerical values.
323
341
 
324
342
  Args:
325
- param_list: Array of numerical parameter values to substitute for symbols.
343
+ param_list (npt.NDArray[np.floating] | list[float]): Array of numerical
344
+ parameter values to substitute for symbols.
326
345
  Must match the length and order of self.symbols.
327
- tag_prefix (str, optional): Prefix to prepend to circuit tags for
328
- identification. Defaults to "".
346
+ param_idx (int, optional): Parameter set index used for structured tags.
347
+ Defaults to 0.
329
348
  precision (int | None, optional): Number of decimal places for parameter values
330
349
  in the QASM output. If None, uses the precision set on this MetaCircuit instance.
331
350
  Defaults to None.
@@ -354,16 +373,19 @@ class MetaCircuit:
354
373
  ]
355
374
 
356
375
  executables = []
376
+ param_id = param_idx
357
377
  for (i, body_str), (j, meas_str) in product(
358
378
  enumerate(final_qasm_bodies), enumerate(self._measurements)
359
379
  ):
360
380
  qasm_circuit = body_str + meas_str
361
- tag_parts = [tag_prefix]
362
- if self.qem_protocol:
363
- tag_parts.append(f"{self.qem_protocol.name}:{i}")
364
- tag_parts.append(str(j))
365
-
366
- tag = "_".join(filter(None, tag_parts))
381
+ tag = CircuitTag(
382
+ param_id=param_id,
383
+ qem_name=(
384
+ self.qem_protocol.name if self.qem_protocol else "NoMitigation"
385
+ ),
386
+ qem_id=i,
387
+ meas_id=j,
388
+ )
367
389
  executables.append(ExecutableQASMCircuit(tag=tag, qasm=qasm_circuit))
368
390
 
369
391
  return CircuitBundle(executables=tuple(executables))
divi/qprog/__init__.py CHANGED
@@ -1,15 +1,16 @@
1
- # SPDX-FileCopyrightText: 2025 Qoro Quantum Ltd <divi@qoroquantum.de>
1
+ # SPDX-FileCopyrightText: 2025-2026 Qoro Quantum Ltd <divi@qoroquantum.de>
2
2
  #
3
3
  # SPDX-License-Identifier: Apache-2.0
4
4
 
5
- # isort: skip_file
6
5
  from .quantum_program import QuantumProgram
7
- from .variational_quantum_algorithm import VariationalQuantumAlgorithm
6
+ from .variational_quantum_algorithm import VariationalQuantumAlgorithm, SolutionEntry
8
7
  from .batch import ProgramBatch
9
8
  from .algorithms import (
10
9
  QAOA,
11
10
  GraphProblem,
12
11
  VQE,
12
+ PCE,
13
+ CustomVQA,
13
14
  Ansatz,
14
15
  UCCSDAnsatz,
15
16
  QAOAAnsatz,
@@ -1,4 +1,4 @@
1
- # SPDX-FileCopyrightText: 2025 Qoro Quantum Ltd <divi@qoroquantum.de>
1
+ # SPDX-FileCopyrightText: 2025-2026 Qoro Quantum Ltd <divi@qoroquantum.de>
2
2
  #
3
3
  # SPDX-License-Identifier: Apache-2.0
4
4
 
@@ -10,5 +10,7 @@ from ._ansatze import (
10
10
  QAOAAnsatz,
11
11
  UCCSDAnsatz,
12
12
  )
13
- from ._qaoa import QAOA, GraphProblem, GraphProblemTypes, QUBOProblemTypes
13
+ from ._custom_vqa import CustomVQA
14
+ from ._qaoa import QAOA, GraphProblem
14
15
  from ._vqe import VQE
16
+ from ._pce import PCE
@@ -1,4 +1,4 @@
1
- # SPDX-FileCopyrightText: 2025 Qoro Quantum Ltd <divi@qoroquantum.de>
1
+ # SPDX-FileCopyrightText: 2025-2026 Qoro Quantum Ltd <divi@qoroquantum.de>
2
2
  #
3
3
  # SPDX-License-Identifier: Apache-2.0
4
4
 
@@ -10,6 +10,15 @@ from warnings import warn
10
10
  import pennylane as qml
11
11
 
12
12
 
13
+ def _require_trainable_params(n_params: int, ansatz_name: str) -> int:
14
+ if n_params <= 0:
15
+ raise ValueError(
16
+ f"{ansatz_name} must define at least one trainable parameter. "
17
+ "Parameter-free circuits are not supported."
18
+ )
19
+ return n_params
20
+
21
+
13
22
  class Ansatz(ABC):
14
23
  """Abstract base class for all VQE ansätze."""
15
24
 
@@ -99,7 +108,7 @@ class GenericLayerAnsatz(Ansatz):
99
108
  self._layout_fn = lambda n_qubits: zip(
100
109
  range(n_qubits), [(i + 1) % n_qubits for i in range(n_qubits)]
101
110
  )
102
- case "all_to_all":
111
+ case "all-to-all":
103
112
  self._layout_fn = lambda n_qubits: (
104
113
  (i, j) for i in range(n_qubits) for j in range(i + 1, n_qubits)
105
114
  )
@@ -121,7 +130,7 @@ class GenericLayerAnsatz(Ansatz):
121
130
  def n_params_per_layer(self, n_qubits: int, **kwargs) -> int:
122
131
  """Total parameters = sum of gate.num_params per qubit per layer."""
123
132
  per_qubit = sum(getattr(g, "num_params", 1) for g in self.gate_sequence)
124
- return per_qubit * n_qubits
133
+ return _require_trainable_params(per_qubit * n_qubits, self.name)
125
134
 
126
135
  def build(
127
136
  self, params, n_qubits: int, n_layers: int, **kwargs
@@ -182,7 +191,8 @@ class QAOAAnsatz(Ansatz):
182
191
  Returns:
183
192
  int: Number of parameters needed per layer.
184
193
  """
185
- return qml.QAOAEmbedding.shape(n_layers=1, n_wires=n_qubits)[1]
194
+ n_params = qml.QAOAEmbedding.shape(n_layers=1, n_wires=n_qubits)[1]
195
+ return _require_trainable_params(n_params, QAOAAnsatz.__name__)
186
196
 
187
197
  def build(
188
198
  self, params, n_qubits: int, n_layers: int, **kwargs
@@ -257,7 +267,8 @@ class UCCSDAnsatz(Ansatz):
257
267
  """
258
268
  singles, doubles = qml.qchem.excitations(n_electrons, n_qubits)
259
269
  s_wires, d_wires = qml.qchem.excitations_to_wires(singles, doubles)
260
- return len(s_wires) + len(d_wires)
270
+ n_params = len(s_wires) + len(d_wires)
271
+ return _require_trainable_params(n_params, UCCSDAnsatz.__name__)
261
272
 
262
273
  def build(
263
274
  self, params, n_qubits: int, n_layers: int, **kwargs
@@ -313,7 +324,8 @@ class HartreeFockAnsatz(Ansatz):
313
324
  int: Number of parameters (number of single + double excitations).
314
325
  """
315
326
  singles, doubles = qml.qchem.excitations(n_electrons, n_qubits)
316
- return len(singles) + len(doubles)
327
+ n_params = len(singles) + len(doubles)
328
+ return _require_trainable_params(n_params, HartreeFockAnsatz.__name__)
317
329
 
318
330
  def build(
319
331
  self, params, n_qubits: int, n_layers: int, **kwargs