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.

@@ -2,11 +2,26 @@
2
2
  #
3
3
  # SPDX-License-Identifier: Apache-2.0
4
4
 
5
- from dataclasses import dataclass
5
+ """Data models for Quantum Processing Units (QPUs) and QPUSystems."""
6
+
7
+ from __future__ import annotations
8
+
9
+ from dataclasses import dataclass, field, replace
10
+
11
+ _AVAILABLE_QPU_SYSTEMS: dict[str, QPUSystem] = {}
6
12
 
7
13
 
8
14
  @dataclass(frozen=True, repr=True)
9
15
  class QPU:
16
+ """Represents a single Quantum Processing Unit (QPU).
17
+
18
+ Attributes:
19
+ nickname: The unique name or identifier for the QPU.
20
+ q_bits: The number of qubits in the QPU.
21
+ status: The current operational status of the QPU.
22
+ system_kind: The type of technology the QPU uses.
23
+ """
24
+
10
25
  nickname: str
11
26
  q_bits: int
12
27
  status: str
@@ -15,6 +30,65 @@ class QPU:
15
30
 
16
31
  @dataclass(frozen=True, repr=True)
17
32
  class QPUSystem:
33
+ """Represents a collection of QPUs that form a quantum computing system.
34
+
35
+ Attributes:
36
+ name: The name of the QPU system.
37
+ qpus: A list of QPU objects that are part of this system.
38
+ access_level: The access level granted to the user for this system (e.g., 'PUBLIC').
39
+ supports_expval: Whether the system supports expectation value jobs.
40
+ """
41
+
18
42
  name: str
19
- qpus: list[QPU]
20
- access_level: str
43
+ qpus: list[QPU] = field(default_factory=list)
44
+ access_level: str = "PUBLIC"
45
+ supports_expval: bool = False
46
+
47
+
48
+ def parse_qpu_systems(json_data: list) -> list[QPUSystem]:
49
+ """Parses a list of QPU system data from JSON into QPUSystem objects."""
50
+ return [
51
+ QPUSystem(
52
+ name=system_data["name"],
53
+ qpus=[QPU(**qpu) for qpu in system_data.get("qpus", [])],
54
+ access_level=system_data["access_level"],
55
+ )
56
+ for system_data in json_data
57
+ ]
58
+
59
+
60
+ def update_qpu_systems_cache(systems: list[QPUSystem]):
61
+ """Updates the cache of available QPU systems."""
62
+ _AVAILABLE_QPU_SYSTEMS.clear()
63
+ for system in systems:
64
+ if system.name == "qoro_maestro":
65
+ system = replace(system, supports_expval=True)
66
+ _AVAILABLE_QPU_SYSTEMS[system.name] = system
67
+
68
+
69
+ def get_qpu_system(name: str) -> QPUSystem:
70
+ """
71
+ Get a QPUSystem object by its name from the cache.
72
+
73
+ Args:
74
+ name: The name of the QPU system to retrieve.
75
+
76
+ Returns:
77
+ The QPUSystem object with the matching name.
78
+
79
+ Raises:
80
+ ValueError: If the cache is empty or the system is not found.
81
+ """
82
+ if not _AVAILABLE_QPU_SYSTEMS:
83
+ raise ValueError(
84
+ "QPU systems cache is empty. Call `QoroService.fetch_qpu_systems()` to populate it."
85
+ )
86
+ try:
87
+ return _AVAILABLE_QPU_SYSTEMS[name]
88
+ except KeyError:
89
+ raise ValueError(f"QPUSystem with name '{name}' not found in cache.") from None
90
+
91
+
92
+ def get_available_qpu_systems() -> list[QPUSystem]:
93
+ """Returns a list of all available QPU systems from the cache."""
94
+ return list(_AVAILABLE_QPU_SYSTEMS.values())
divi/circuits/_core.py CHANGED
@@ -20,6 +20,20 @@ TRANSFORM_PROGRAM.add_transform(qml.transforms.split_non_commuting)
20
20
 
21
21
 
22
22
  class Circuit:
23
+ """
24
+ Represents a quantum circuit with its QASM representation and metadata.
25
+
26
+ This class encapsulates a PennyLane quantum circuit along with its OpenQASM
27
+ serialization and associated tags for identification. Each circuit instance
28
+ is assigned a unique ID for tracking purposes.
29
+
30
+ Attributes:
31
+ main_circuit: The PennyLane quantum circuit/tape object.
32
+ tags (list[str]): List of string tags for circuit identification.
33
+ qasm_circuits (list[str]): List of OpenQASM string representations.
34
+ circuit_id (int): Unique identifier for this circuit instance.
35
+ """
36
+
23
37
  _id_counter = 0
24
38
 
25
39
  def __init__(
@@ -28,6 +42,16 @@ class Circuit:
28
42
  tags: list[str],
29
43
  qasm_circuits: list[str] = None,
30
44
  ):
45
+ """
46
+ Initialize a Circuit instance.
47
+
48
+ Args:
49
+ main_circuit: A PennyLane quantum circuit or tape object to be wrapped.
50
+ tags (list[str]): List of string tags for identifying this circuit.
51
+ qasm_circuits (list[str], optional): Pre-computed OpenQASM string
52
+ representations. If None, they will be generated from main_circuit.
53
+ Defaults to None.
54
+ """
31
55
  self.main_circuit = main_circuit
32
56
  self.tags = tags
33
57
 
@@ -44,29 +68,83 @@ class Circuit:
44
68
  Circuit._id_counter += 1
45
69
 
46
70
  def __str__(self):
71
+ """
72
+ Return a string representation of the circuit.
73
+
74
+ Returns:
75
+ str: String in format "Circuit: {circuit_id}".
76
+ """
47
77
  return f"Circuit: {self.circuit_id}"
48
78
 
49
79
 
50
80
  class MetaCircuit:
81
+ """
82
+ A parameterized quantum circuit template for batch circuit generation.
83
+
84
+ MetaCircuit represents a symbolic quantum circuit that can be instantiated
85
+ multiple times with different parameter values. It handles circuit compilation,
86
+ observable grouping, and measurement decomposition for efficient execution.
87
+
88
+ Attributes:
89
+ main_circuit: The PennyLane quantum circuit with symbolic parameters.
90
+ symbols: Array of sympy symbols used as circuit parameters.
91
+ qem_protocol (QEMProtocol): Quantum error mitigation protocol to apply.
92
+ compiled_circuits_bodies (list[str]): QASM bodies without measurements.
93
+ measurements (list[str]): QASM measurement strings.
94
+ measurement_groups (list[list]): Grouped observables for each circuit variant.
95
+ postprocessing_fn: Function to combine measurement results.
96
+ """
97
+
51
98
  def __init__(
52
99
  self,
53
100
  main_circuit,
54
101
  symbols,
55
- grouping_strategy: Literal["wires", "default", "qwc"] | None = None,
102
+ grouping_strategy: (
103
+ Literal["wires", "default", "qwc", "_backend_expval"] | None
104
+ ) = None,
56
105
  qem_protocol: QEMProtocol | None = None,
57
106
  ):
107
+ """
108
+ Initialize a MetaCircuit with symbolic parameters.
109
+
110
+ Args:
111
+ main_circuit: A PennyLane quantum circuit/tape with symbolic parameters.
112
+ symbols: Array of sympy Symbol objects representing circuit parameters.
113
+ grouping_strategy (str, optional): Strategy for grouping commuting
114
+ observables. Options are "wires", "default", or "qwc" (qubit-wise
115
+ commuting). If the backend supports expectation value measurements,
116
+ "_backend_expval" to place all observables in the same measurement group.
117
+ Defaults to None.
118
+ qem_protocol (QEMProtocol, optional): Quantum error mitigation protocol
119
+ to apply to the circuits. Defaults to None.
120
+ """
58
121
  self.main_circuit = main_circuit
59
122
  self.symbols = symbols
60
123
  self.qem_protocol = qem_protocol
124
+ self.grouping_strategy = grouping_strategy
125
+
126
+ # --- VQE Optimization ---
127
+ # Create a lightweight tape with only measurements to avoid deep-copying
128
+ # the circuit's operations inside the split_non_commuting transform.
129
+ measurements_only_tape = qml.tape.QuantumScript(
130
+ measurements=self.main_circuit.measurements
131
+ )
61
132
 
62
133
  transform_program = deepcopy(TRANSFORM_PROGRAM)
63
- transform_program[1].kwargs["grouping_strategy"] = grouping_strategy
134
+ transform_program[1].kwargs["grouping_strategy"] = (
135
+ None if grouping_strategy == "_backend_expval" else grouping_strategy
136
+ )
137
+ # Run the transform on the lightweight tape
138
+ qscripts, self.postprocessing_fn = transform_program((measurements_only_tape,))
64
139
 
65
- qscripts, self.postprocessing_fn = transform_program((main_circuit,))
140
+ if grouping_strategy == "_backend_expval":
141
+ wrapped_measurements_groups = [[]]
142
+ else:
143
+ wrapped_measurements_groups = [qsc.measurements for qsc in qscripts]
66
144
 
67
145
  self.compiled_circuits_bodies, self.measurements = to_openqasm(
68
146
  main_circuit,
69
- measurement_groups=[qsc.measurements for qsc in qscripts],
147
+ measurement_groups=wrapped_measurements_groups,
70
148
  return_measurements_separately=True,
71
149
  # TODO: optimize later
72
150
  measure_all=True,
@@ -81,11 +159,30 @@ class MetaCircuit:
81
159
  ]
82
160
 
83
161
  def __getstate__(self):
162
+ """
163
+ Prepare the MetaCircuit for pickling.
164
+
165
+ Serializes the postprocessing function using dill since regular pickle
166
+ cannot handle certain PennyLane function objects.
167
+
168
+ Returns:
169
+ dict: State dictionary with serialized postprocessing function.
170
+ """
84
171
  state = self.__dict__.copy()
85
172
  state["postprocessing_fn"] = dill.dumps(self.postprocessing_fn)
86
173
  return state
87
174
 
88
175
  def __setstate__(self, state):
176
+ """
177
+ Restore the MetaCircuit from a pickled state.
178
+
179
+ Deserializes the postprocessing function that was serialized with dill
180
+ during pickling.
181
+
182
+ Args:
183
+ state (dict): State dictionary from pickling with serialized
184
+ postprocessing function.
185
+ """
89
186
  state["postprocessing_fn"] = dill.loads(state["postprocessing_fn"])
90
187
 
91
188
  self.__dict__.update(state)
@@ -93,6 +190,29 @@ class MetaCircuit:
93
190
  def initialize_circuit_from_params(
94
191
  self, param_list, tag_prefix: str = "", precision: int = 8
95
192
  ) -> Circuit:
193
+ """
194
+ Instantiate a concrete Circuit by substituting symbolic parameters with values.
195
+
196
+ Takes a list of parameter values and creates a fully instantiated Circuit
197
+ by replacing all symbolic parameters in the QASM representations with their
198
+ concrete numerical values.
199
+
200
+ Args:
201
+ param_list: Array of numerical parameter values to substitute for symbols.
202
+ Must match the length and order of self.symbols.
203
+ tag_prefix (str, optional): Prefix to prepend to circuit tags for
204
+ identification. Defaults to "".
205
+ precision (int, optional): Number of decimal places for parameter values
206
+ in the QASM output. Defaults to 8.
207
+
208
+ Returns:
209
+ Circuit: A new Circuit instance with parameters substituted and proper
210
+ tags for identification.
211
+
212
+ Note:
213
+ The main_circuit attribute in the returned Circuit still contains
214
+ symbolic parameters. Only the QASM representations have concrete values.
215
+ """
96
216
  mapping = dict(
97
217
  zip(
98
218
  map(lambda x: re.escape(str(x)), self.symbols),
divi/circuits/qasm.py CHANGED
@@ -46,6 +46,25 @@ OPENQASM_GATES = {
46
46
 
47
47
 
48
48
  def _ops_to_qasm(operations, precision, wires):
49
+ """
50
+ Convert PennyLane operations to OpenQASM instruction strings.
51
+
52
+ Translates a sequence of PennyLane quantum operations into their OpenQASM
53
+ 2.0 equivalent representations. Each operation is mapped to its corresponding
54
+ QASM gate with appropriate parameters and wire labels.
55
+
56
+ Args:
57
+ operations: Sequence of PennyLane operation objects to convert.
58
+ precision (int | None): Number of decimal places for parameter values.
59
+ If None, uses default Python string formatting.
60
+ wires: Wire labels used in the circuit for indexing.
61
+
62
+ Returns:
63
+ str: OpenQASM instruction string with each operation on a new line.
64
+
65
+ Raises:
66
+ ValueError: If an operation is not supported by the QASM serializer.
67
+ """
49
68
  # create the QASM code representing the operations
50
69
  qasm_str = ""
51
70
 
@@ -196,9 +215,7 @@ def to_openqasm(
196
215
  for wire in range(len(wires)):
197
216
  measure_qasm_str += f"measure q[{wire}] -> c[{wire}];\n"
198
217
  else:
199
- measured_wires = Wires.all_wires(
200
- [m.wires for m in main_qscript.measurements]
201
- )
218
+ measured_wires = Wires.all_wires([m.wires for m in meas_group])
202
219
 
203
220
  for w in measured_wires:
204
221
  wire_indx = main_qscript.wires.index(w)
@@ -639,9 +639,18 @@ def validate_qasm_raise(src: str) -> None:
639
639
  Parser(toks).parse()
640
640
 
641
641
 
642
- def is_valid_qasm(src: str) -> bool | str:
642
+ def validate_qasm_count_qubits(src: str) -> int:
643
+ """Validate QASM and return the total number of qubits."""
644
+ toks = _lex(src)
645
+ parser = Parser(toks)
646
+ parser.parse()
647
+ # Sum all qubit register sizes to get total qubit count
648
+ return sum(parser.qregs.values())
649
+
650
+
651
+ def is_valid_qasm(src: str) -> int | str:
652
+ """Validate QASM and return the number of qubits if valid, or error message if invalid."""
643
653
  try:
644
- validate_qasm_raise(src)
645
- return True
654
+ return validate_qasm_count_qubits(src)
646
655
  except SyntaxError as e:
647
656
  return str(e)
divi/qprog/__init__.py CHANGED
@@ -4,6 +4,7 @@
4
4
 
5
5
  # isort: skip_file
6
6
  from .quantum_program import QuantumProgram
7
+ from .variational_quantum_algorithm import VariationalQuantumAlgorithm
7
8
  from .batch import ProgramBatch
8
9
  from .algorithms import (
9
10
  QAOA,
@@ -11,7 +11,7 @@ import pennylane as qml
11
11
 
12
12
 
13
13
  class Ansatz(ABC):
14
- """Abstract base class for all VQE ansaetze."""
14
+ """Abstract base class for all VQE ansätze."""
15
15
 
16
16
  @property
17
17
  def name(self) -> str:
@@ -25,15 +25,17 @@ class Ansatz(ABC):
25
25
  raise NotImplementedError
26
26
 
27
27
  @abstractmethod
28
- def build(self, params, n_qubits: int, n_layers: int, **kwargs):
28
+ def build(self, params, n_qubits: int, n_layers: int, **kwargs) -> None:
29
29
  """
30
- Builds the ansatz circuit.
30
+ Builds the ansatz circuit by adding operations to the active PennyLane
31
+ quantum function context.
31
32
 
32
- Args:
33
- params (array): The parameters (weights) for the ansatz.
34
- n_qubits (int): The number of qubits.
35
- n_layers (int): The number of layers.
36
- **kwargs: Additional arguments like n_electrons for chemistry ansaetze.
33
+ Note: This method is called within a PennyLane quantum function context
34
+ (qml.qnode or qml.tape.make_qscript). Operations are added via side effects
35
+ to the active quantum tape/script.
36
+
37
+ Returns:
38
+ None: Operations are added to the active PennyLane context
37
39
  """
38
40
  raise NotImplementedError
39
41
 
@@ -118,7 +120,7 @@ class GenericLayerAnsatz(Ansatz):
118
120
  per_qubit = sum(getattr(g, "num_params", 1) for g in self.gate_sequence)
119
121
  return per_qubit * n_qubits
120
122
 
121
- def build(self, params, n_qubits: int, n_layers: int, **kwargs):
123
+ def build(self, params, n_qubits: int, n_layers: int, **kwargs) -> None:
122
124
  # calculate how many params each gate needs per qubit
123
125
  gate_param_counts = [getattr(g, "num_params", 1) for g in self.gate_sequence]
124
126
  per_qubit = sum(gate_param_counts)
@@ -143,11 +145,37 @@ class GenericLayerAnsatz(Ansatz):
143
145
 
144
146
 
145
147
  class QAOAAnsatz(Ansatz):
148
+ """
149
+ QAOA-style ansatz using PennyLane's QAOAEmbedding.
150
+
151
+ Implements a parameterized ansatz based on the Quantum Approximate Optimization
152
+ Algorithm structure, alternating between problem and mixer Hamiltonians.
153
+ """
154
+
146
155
  @staticmethod
147
156
  def n_params_per_layer(n_qubits: int, **kwargs) -> int:
157
+ """
158
+ Calculate the number of parameters per layer for QAOA ansatz.
159
+
160
+ Args:
161
+ n_qubits (int): Number of qubits in the circuit.
162
+ **kwargs: Additional unused arguments.
163
+
164
+ Returns:
165
+ int: Number of parameters needed per layer.
166
+ """
148
167
  return qml.QAOAEmbedding.shape(n_layers=1, n_wires=n_qubits)[1]
149
168
 
150
- def build(self, params, n_qubits: int, n_layers: int, **kwargs):
169
+ def build(self, params, n_qubits: int, n_layers: int, **kwargs) -> None:
170
+ """
171
+ Build the QAOA ansatz circuit.
172
+
173
+ Args:
174
+ params: Parameter array to use for the ansatz.
175
+ n_qubits (int): Number of qubits.
176
+ n_layers (int): Number of QAOA layers.
177
+ **kwargs: Additional unused arguments.
178
+ """
151
179
  qml.QAOAEmbedding(
152
180
  features=[],
153
181
  weights=params.reshape(n_layers, -1),
@@ -156,11 +184,23 @@ class QAOAAnsatz(Ansatz):
156
184
 
157
185
 
158
186
  class HardwareEfficientAnsatz(Ansatz):
187
+ """
188
+ Hardware-efficient ansatz (not yet implemented).
189
+
190
+ This ansatz is designed to be easily implementable on near-term quantum hardware,
191
+ typically using native gate sets and connectivity patterns.
192
+
193
+ Note:
194
+ This class is a placeholder for future implementation.
195
+ """
196
+
159
197
  @staticmethod
160
198
  def n_params_per_layer(n_qubits: int, **kwargs) -> int:
199
+ """Not yet implemented."""
161
200
  raise NotImplementedError("HardwareEfficientAnsatz is not yet implemented.")
162
201
 
163
202
  def build(self, params, n_qubits: int, n_layers: int, **kwargs) -> None:
203
+ """Not yet implemented."""
164
204
  raise NotImplementedError("HardwareEfficientAnsatz is not yet implemented.")
165
205
 
166
206
 
@@ -168,13 +208,43 @@ class HardwareEfficientAnsatz(Ansatz):
168
208
 
169
209
 
170
210
  class UCCSDAnsatz(Ansatz):
211
+ """
212
+ Unitary Coupled Cluster Singles and Doubles (UCCSD) ansatz.
213
+
214
+ This ansatz is specifically designed for quantum chemistry calculations,
215
+ implementing the UCCSD approximation which includes all single and double
216
+ electron excitations from a reference state.
217
+ """
218
+
171
219
  @staticmethod
172
220
  def n_params_per_layer(n_qubits: int, n_electrons: int, **kwargs) -> int:
221
+ """
222
+ Calculate the number of parameters per layer for UCCSD ansatz.
223
+
224
+ Args:
225
+ n_qubits (int): Number of qubits in the circuit.
226
+ n_electrons (int): Number of electrons in the system.
227
+ **kwargs: Additional unused arguments.
228
+
229
+ Returns:
230
+ int: Number of parameters (number of single + double excitations).
231
+ """
173
232
  singles, doubles = qml.qchem.excitations(n_electrons, n_qubits)
174
233
  s_wires, d_wires = qml.qchem.excitations_to_wires(singles, doubles)
175
234
  return len(s_wires) + len(d_wires)
176
235
 
177
- def build(self, params, n_qubits: int, n_layers: int, n_electrons: int, **kwargs):
236
+ def build(self, params, n_qubits: int, n_layers: int, **kwargs) -> None:
237
+ """
238
+ Build the UCCSD ansatz circuit.
239
+
240
+ Args:
241
+ params: Parameter array for excitation amplitudes.
242
+ n_qubits (int): Number of qubits.
243
+ n_layers (int): Number of UCCSD layers (repeats).
244
+ **kwargs: Additional arguments:
245
+ n_electrons (int): Number of electrons in the system (required).
246
+ """
247
+ n_electrons = kwargs.pop("n_electrons")
178
248
  singles, doubles = qml.qchem.excitations(n_electrons, n_qubits)
179
249
  s_wires, d_wires = qml.qchem.excitations_to_wires(singles, doubles)
180
250
  hf_state = qml.qchem.hf_state(n_electrons, n_qubits)
@@ -190,12 +260,42 @@ class UCCSDAnsatz(Ansatz):
190
260
 
191
261
 
192
262
  class HartreeFockAnsatz(Ansatz):
263
+ """
264
+ Hartree-Fock-based ansatz for quantum chemistry.
265
+
266
+ This ansatz prepares the Hartree-Fock reference state and applies
267
+ parameterized single and double excitation gates. It's a simplified
268
+ alternative to UCCSD, often used as a starting point for VQE calculations.
269
+ """
270
+
193
271
  @staticmethod
194
272
  def n_params_per_layer(n_qubits: int, n_electrons: int, **kwargs) -> int:
273
+ """
274
+ Calculate the number of parameters per layer for Hartree-Fock ansatz.
275
+
276
+ Args:
277
+ n_qubits (int): Number of qubits in the circuit.
278
+ n_electrons (int): Number of electrons in the system.
279
+ **kwargs: Additional unused arguments.
280
+
281
+ Returns:
282
+ int: Number of parameters (number of single + double excitations).
283
+ """
195
284
  singles, doubles = qml.qchem.excitations(n_electrons, n_qubits)
196
285
  return len(singles) + len(doubles)
197
286
 
198
- def build(self, params, n_qubits: int, n_layers: int, n_electrons: int, **kwargs):
287
+ def build(self, params, n_qubits: int, n_layers: int, **kwargs) -> None:
288
+ """
289
+ Build the Hartree-Fock ansatz circuit.
290
+
291
+ Args:
292
+ params: Parameter array for excitation amplitudes.
293
+ n_qubits (int): Number of qubits.
294
+ n_layers (int): Number of ansatz layers.
295
+ **kwargs: Additional arguments:
296
+ n_electrons (int): Number of electrons in the system (required).
297
+ """
298
+ n_electrons = kwargs.pop("n_electrons")
199
299
  singles, doubles = qml.qchem.excitations(n_electrons, n_qubits)
200
300
  hf_state = qml.qchem.hf_state(n_electrons, n_qubits)
201
301