qoro-divi 0.3.2b0__tar.gz → 0.3.4__tar.gz

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 (70) hide show
  1. {qoro_divi-0.3.2b0 → qoro_divi-0.3.4}/PKG-INFO +2 -2
  2. {qoro_divi-0.3.2b0 → qoro_divi-0.3.4}/divi/__init__.py +1 -2
  3. qoro_divi-0.3.4/divi/backends/__init__.py +7 -0
  4. qoro_divi-0.3.2b0/divi/parallel_simulator.py → qoro_divi-0.3.4/divi/backends/_parallel_simulator.py +4 -3
  5. qoro_divi-0.3.2b0/divi/qoro_service.py → qoro_divi-0.3.4/divi/backends/_qoro_service.py +27 -15
  6. qoro_divi-0.3.4/divi/circuits/__init__.py +5 -0
  7. qoro_divi-0.3.2b0/divi/circuits.py → qoro_divi-0.3.4/divi/circuits/_core.py +6 -20
  8. {qoro_divi-0.3.2b0/divi → qoro_divi-0.3.4/divi/circuits}/qasm.py +2 -2
  9. {qoro_divi-0.3.2b0/divi/exp → qoro_divi-0.3.4/divi/extern}/cirq/__init__.py +1 -1
  10. {qoro_divi-0.3.2b0/divi/exp → qoro_divi-0.3.4/divi/extern}/cirq/_validator.py +10 -8
  11. qoro_divi-0.3.4/divi/qprog/__init__.py +26 -0
  12. qoro_divi-0.3.4/divi/qprog/algorithms/__init__.py +14 -0
  13. qoro_divi-0.3.4/divi/qprog/algorithms/_ansatze.py +215 -0
  14. {qoro_divi-0.3.2b0/divi/qprog → qoro_divi-0.3.4/divi/qprog/algorithms}/_qaoa.py +16 -26
  15. {qoro_divi-0.3.2b0/divi/qprog → qoro_divi-0.3.4/divi/qprog/algorithms}/_vqe.py +35 -133
  16. {qoro_divi-0.3.2b0 → qoro_divi-0.3.4}/divi/qprog/batch.py +25 -19
  17. qoro_divi-0.3.4/divi/qprog/optimizers.py +200 -0
  18. {qoro_divi-0.3.2b0 → qoro_divi-0.3.4}/divi/qprog/quantum_program.py +142 -200
  19. qoro_divi-0.3.4/divi/qprog/workflows/__init__.py +10 -0
  20. {qoro_divi-0.3.2b0/divi/qprog → qoro_divi-0.3.4/divi/qprog/workflows}/_graph_partitioning.py +6 -9
  21. {qoro_divi-0.3.2b0/divi/qprog → qoro_divi-0.3.4/divi/qprog/workflows}/_qubo_partitioning.py +6 -7
  22. {qoro_divi-0.3.2b0/divi/qprog → qoro_divi-0.3.4/divi/qprog/workflows}/_vqe_sweep.py +35 -24
  23. qoro_divi-0.3.4/divi/reporting/__init__.py +7 -0
  24. {qoro_divi-0.3.2b0/divi → qoro_divi-0.3.4/divi/reporting}/_pbar.py +13 -14
  25. qoro_divi-0.3.2b0/divi/qlogger.py → qoro_divi-0.3.4/divi/reporting/_qlogger.py +8 -6
  26. qoro_divi-0.3.2b0/divi/reporter.py → qoro_divi-0.3.4/divi/reporting/_reporter.py +24 -7
  27. {qoro_divi-0.3.2b0 → qoro_divi-0.3.4}/divi/utils.py +14 -6
  28. {qoro_divi-0.3.2b0 → qoro_divi-0.3.4}/pyproject.toml +2 -2
  29. qoro_divi-0.3.2b0/divi/qprog/__init__.py +0 -13
  30. qoro_divi-0.3.2b0/divi/qprog/optimizers.py +0 -75
  31. {qoro_divi-0.3.2b0 → qoro_divi-0.3.4}/LICENSE +0 -0
  32. {qoro_divi-0.3.2b0 → qoro_divi-0.3.4}/LICENSES/.license-header +0 -0
  33. {qoro_divi-0.3.2b0 → qoro_divi-0.3.4}/LICENSES/Apache-2.0.txt +0 -0
  34. {qoro_divi-0.3.2b0 → qoro_divi-0.3.4}/README.md +0 -0
  35. /qoro_divi-0.3.2b0/divi/interfaces.py → /qoro_divi-0.3.4/divi/backends/_circuit_runner.py +0 -0
  36. /qoro_divi-0.3.2b0/divi/qpu_system.py → /qoro_divi-0.3.4/divi/backends/_qpu_system.py +0 -0
  37. {qoro_divi-0.3.2b0/divi → qoro_divi-0.3.4/divi/circuits}/qem.py +0 -0
  38. {qoro_divi-0.3.2b0/divi/exp → qoro_divi-0.3.4/divi/extern}/cirq/_lexer.py +0 -0
  39. {qoro_divi-0.3.2b0/divi/exp → qoro_divi-0.3.4/divi/extern}/cirq/_parser.py +0 -0
  40. {qoro_divi-0.3.2b0/divi/exp → qoro_divi-0.3.4/divi/extern}/cirq/_qasm_export.py +0 -0
  41. {qoro_divi-0.3.2b0/divi/exp → qoro_divi-0.3.4/divi/extern}/cirq/_qasm_import.py +0 -0
  42. {qoro_divi-0.3.2b0/divi/exp → qoro_divi-0.3.4/divi/extern}/cirq/exception.py +0 -0
  43. {qoro_divi-0.3.2b0/divi/exp → qoro_divi-0.3.4/divi/extern}/scipy/_cobyla.py +0 -0
  44. {qoro_divi-0.3.2b0/divi/exp → qoro_divi-0.3.4/divi/extern}/scipy/pyprima/LICENCE.txt +0 -0
  45. {qoro_divi-0.3.2b0/divi/exp → qoro_divi-0.3.4/divi/extern}/scipy/pyprima/__init__.py +0 -0
  46. {qoro_divi-0.3.2b0/divi/exp → qoro_divi-0.3.4/divi/extern}/scipy/pyprima/cobyla/__init__.py +0 -0
  47. {qoro_divi-0.3.2b0/divi/exp → qoro_divi-0.3.4/divi/extern}/scipy/pyprima/cobyla/cobyla.py +0 -0
  48. {qoro_divi-0.3.2b0/divi/exp → qoro_divi-0.3.4/divi/extern}/scipy/pyprima/cobyla/cobylb.py +0 -0
  49. {qoro_divi-0.3.2b0/divi/exp → qoro_divi-0.3.4/divi/extern}/scipy/pyprima/cobyla/geometry.py +0 -0
  50. {qoro_divi-0.3.2b0/divi/exp → qoro_divi-0.3.4/divi/extern}/scipy/pyprima/cobyla/initialize.py +0 -0
  51. {qoro_divi-0.3.2b0/divi/exp → qoro_divi-0.3.4/divi/extern}/scipy/pyprima/cobyla/trustregion.py +0 -0
  52. {qoro_divi-0.3.2b0/divi/exp → qoro_divi-0.3.4/divi/extern}/scipy/pyprima/cobyla/update.py +0 -0
  53. {qoro_divi-0.3.2b0/divi/exp → qoro_divi-0.3.4/divi/extern}/scipy/pyprima/common/__init__.py +0 -0
  54. {qoro_divi-0.3.2b0/divi/exp → qoro_divi-0.3.4/divi/extern}/scipy/pyprima/common/_bounds.py +0 -0
  55. {qoro_divi-0.3.2b0/divi/exp → qoro_divi-0.3.4/divi/extern}/scipy/pyprima/common/_linear_constraints.py +0 -0
  56. {qoro_divi-0.3.2b0/divi/exp → qoro_divi-0.3.4/divi/extern}/scipy/pyprima/common/_nonlinear_constraints.py +0 -0
  57. {qoro_divi-0.3.2b0/divi/exp → qoro_divi-0.3.4/divi/extern}/scipy/pyprima/common/_project.py +0 -0
  58. {qoro_divi-0.3.2b0/divi/exp → qoro_divi-0.3.4/divi/extern}/scipy/pyprima/common/checkbreak.py +0 -0
  59. {qoro_divi-0.3.2b0/divi/exp → qoro_divi-0.3.4/divi/extern}/scipy/pyprima/common/consts.py +0 -0
  60. {qoro_divi-0.3.2b0/divi/exp → qoro_divi-0.3.4/divi/extern}/scipy/pyprima/common/evaluate.py +0 -0
  61. {qoro_divi-0.3.2b0/divi/exp → qoro_divi-0.3.4/divi/extern}/scipy/pyprima/common/history.py +0 -0
  62. {qoro_divi-0.3.2b0/divi/exp → qoro_divi-0.3.4/divi/extern}/scipy/pyprima/common/infos.py +0 -0
  63. {qoro_divi-0.3.2b0/divi/exp → qoro_divi-0.3.4/divi/extern}/scipy/pyprima/common/linalg.py +0 -0
  64. {qoro_divi-0.3.2b0/divi/exp → qoro_divi-0.3.4/divi/extern}/scipy/pyprima/common/message.py +0 -0
  65. {qoro_divi-0.3.2b0/divi/exp → qoro_divi-0.3.4/divi/extern}/scipy/pyprima/common/powalg.py +0 -0
  66. {qoro_divi-0.3.2b0/divi/exp → qoro_divi-0.3.4/divi/extern}/scipy/pyprima/common/preproc.py +0 -0
  67. {qoro_divi-0.3.2b0/divi/exp → qoro_divi-0.3.4/divi/extern}/scipy/pyprima/common/present.py +0 -0
  68. {qoro_divi-0.3.2b0/divi/exp → qoro_divi-0.3.4/divi/extern}/scipy/pyprima/common/ratio.py +0 -0
  69. {qoro_divi-0.3.2b0/divi/exp → qoro_divi-0.3.4/divi/extern}/scipy/pyprima/common/redrho.py +0 -0
  70. {qoro_divi-0.3.2b0/divi/exp → qoro_divi-0.3.4/divi/extern}/scipy/pyprima/common/selectx.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.1
2
2
  Name: qoro-divi
3
- Version: 0.3.2b0
3
+ Version: 0.3.4
4
4
  Summary: A Python library to automate generating, parallelizing, and executing quantum programs.
5
5
  Author: Ahmed Darwish
6
6
  Author-email: ahmed@qoroquantum.de
@@ -21,7 +21,7 @@ Requires-Dist: python-dotenv (>=1.1.1,<2.0.0)
21
21
  Requires-Dist: qiskit (<2.0)
22
22
  Requires-Dist: qiskit-aer (>=0.17.1,<0.18.0)
23
23
  Requires-Dist: qiskit-ibm-runtime (>=0.37,<0.38)
24
- Requires-Dist: qiskit-optimization (>=0.6.1,<0.7.0)
24
+ Requires-Dist: qiskit-optimization[cplex] (>=0.7.0,<0.8.0)
25
25
  Requires-Dist: requests (>=2.32.4,<3.0.0)
26
26
  Requires-Dist: rich (>=14.0.0,<15.0.0)
27
27
  Requires-Dist: scikit-learn (>=1.7.0,<2.0.0)
@@ -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
@@ -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
 
@@ -74,8 +74,9 @@ class ParallelSimulator(CircuitRunner):
74
74
  n_processes (int, optional): Number of parallel processes to use for simulation. Defaults to 2.
75
75
  shots (int, optional): Number of shots to perform. Defaults to 5000.
76
76
  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.
77
+ qiskit_backend (Backend | Literal["auto"] | None, optional): A Qiskit backend to initiate the simulator from.
78
+ If "auto" is passed, the best-fit most recent fake backend will be chosen for the given circuit.
79
+ Defaults to None, resulting in noiseless simulation.
79
80
  noise_model (NoiseModel, optional): Qiskit noise model to use in simulation. Defaults to None.
80
81
  """
81
82
  super().__init__(shots=shots)
@@ -15,9 +15,9 @@ import requests
15
15
  from dotenv import dotenv_values
16
16
  from requests.adapters import HTTPAdapter, Retry
17
17
 
18
- from divi.exp.cirq import is_valid_qasm
19
- from divi.interfaces import CircuitRunner
20
- from divi.qpu_system import QPU, QPUSystem
18
+ from divi.backends import CircuitRunner
19
+ from divi.backends._qpu_system import QPU, QPUSystem
20
+ from divi.extern.cirq import is_valid_qasm
21
21
 
22
22
  API_URL = "https://app.qoroquantum.net/api"
23
23
  MAX_PAYLOAD_SIZE_MB = 0.95
@@ -241,8 +241,8 @@ class QoroService(CircuitRunner):
241
241
  raise ValueError("Only one circuit allowed for circuit-cutting jobs.")
242
242
 
243
243
  for key, circuit in circuits.items():
244
- if not is_valid_qasm(circuit):
245
- raise ValueError(f"Circuit {key} is not a valid QASM string.")
244
+ if not (err := is_valid_qasm(circuit)):
245
+ raise ValueError(f"Circuit '{key}' is not a valid QASM: {err}")
246
246
 
247
247
  circuit_chunks = self._split_circuits(circuits)
248
248
 
@@ -342,7 +342,7 @@ class QoroService(CircuitRunner):
342
342
  loop_until_complete: bool = False,
343
343
  on_complete: Callable | None = None,
344
344
  verbose: bool = True,
345
- pbar_update_fn: Callable | None = None,
345
+ poll_callback: Callable[[int, str], None] | None = None,
346
346
  ):
347
347
  """
348
348
  Get the status of a job and optionally execute function *on_complete* on the results
@@ -354,13 +354,28 @@ class QoroService(CircuitRunner):
354
354
  on_complete (optional): A function to be called when the job is completed
355
355
  polling_interval (optional): The time to wait between retries
356
356
  verbose (optional): A flag to print the when retrying
357
- pbar_update_fn (optional): A function for updating progress bars while polling.
357
+ poll_callback (optional): A function for updating progress bars while polling.
358
+ Definition should be `poll_callback(retry_count: int, status: str) -> None`.
358
359
  Returns:
359
360
  status: The status of the job
360
361
  """
361
362
  if not isinstance(job_ids, list):
362
363
  job_ids = [job_ids]
363
364
 
365
+ # Decide once at the start
366
+ if poll_callback:
367
+ update_fn = poll_callback
368
+ elif verbose:
369
+ CYAN = "\033[36m"
370
+ RESET = "\033[0m"
371
+
372
+ update_fn = lambda retry_count, status: logger.info(
373
+ rf"Job {CYAN}{job_ids[0].split('-')[0]}{RESET} is {status}. Polling attempt {retry_count} / {self.max_retries}\r",
374
+ extra={"append": True},
375
+ )
376
+ else:
377
+ update_fn = lambda _, __: None
378
+
364
379
  if not loop_until_complete:
365
380
  statuses = [
366
381
  self._make_request(
@@ -386,7 +401,10 @@ class QoroService(CircuitRunner):
386
401
  timeout=200,
387
402
  )
388
403
 
389
- if response.json()["status"] == JobStatus.COMPLETED.value:
404
+ if response.json()["status"] in (
405
+ JobStatus.COMPLETED.value,
406
+ JobStatus.FAILED.value,
407
+ ):
390
408
  pending_job_ids.remove(job_id)
391
409
  responses.append(response)
392
410
 
@@ -396,13 +414,7 @@ class QoroService(CircuitRunner):
396
414
 
397
415
  time.sleep(self.polling_interval)
398
416
 
399
- if verbose:
400
- if pbar_update_fn:
401
- pbar_update_fn(retry_count)
402
- else:
403
- logger.info(
404
- rf"\cPolling {retry_count} / {self.max_retries} retries\r"
405
- )
417
+ update_fn(retry_count, response.json()["status"])
406
418
 
407
419
  if not pending_job_ids:
408
420
  if on_complete:
@@ -0,0 +1,5 @@
1
+ # SPDX-FileCopyrightText: 2025 Qoro Quantum Ltd <divi@qoroquantum.de>
2
+ #
3
+ # SPDX-License-Identifier: Apache-2.0
4
+
5
+ from ._core import Circuit, MetaCircuit
@@ -10,10 +10,9 @@ from typing import Literal
10
10
  import dill
11
11
  import pennylane as qml
12
12
  from pennylane.transforms.core.transform_program import TransformProgram
13
- from qiskit.qasm2 import dumps
14
13
 
15
- from divi.qasm import to_openqasm
16
- from divi.qem import QEMProtocol
14
+ from divi.circuits.qasm import to_openqasm
15
+ from divi.circuits.qem import QEMProtocol
17
16
 
18
17
  TRANSFORM_PROGRAM = TransformProgram()
19
18
  TRANSFORM_PROGRAM.add_transform(qml.transforms.split_to_single_terms)
@@ -30,35 +29,22 @@ class Circuit:
30
29
  qasm_circuits: list[str] = None,
31
30
  ):
32
31
  self.main_circuit = main_circuit
33
- self.circuit_type = main_circuit.__module__.split(".")[0]
34
32
  self.tags = tags
35
33
 
36
34
  self.qasm_circuits = qasm_circuits
37
35
 
38
36
  if self.qasm_circuits is None:
39
- self.convert_to_qasm()
40
-
41
- self.circuit_id = Circuit._id_counter
42
- Circuit._id_counter += 1
43
-
44
- def __str__(self):
45
- return f"Circuit: {self.circuit_id}"
46
-
47
- def convert_to_qasm(self):
48
- if self.circuit_type == "pennylane":
49
37
  self.qasm_circuits = to_openqasm(
50
38
  self.main_circuit,
51
39
  measurement_groups=[self.main_circuit.measurements],
52
40
  return_measurements_separately=False,
53
41
  )
54
42
 
55
- elif self.circuit_type == "qiskit":
56
- self.qasm_circuits = [dumps(self.main_circuit)]
43
+ self.circuit_id = Circuit._id_counter
44
+ Circuit._id_counter += 1
57
45
 
58
- else:
59
- raise ValueError(
60
- f"Invalid circuit type. Circuit type {self.circuit_type} not currently supported."
61
- )
46
+ def __str__(self):
47
+ return f"Circuit: {self.circuit_id}"
62
48
 
63
49
 
64
50
  class MetaCircuit:
@@ -14,8 +14,8 @@ from pennylane.tape import QuantumScript
14
14
  from pennylane.wires import Wires
15
15
  from sympy import Symbol
16
16
 
17
- from divi.exp.cirq import cirq_circuit_from_qasm
18
- from divi.qem import QEMProtocol
17
+ from divi.circuits.qem import QEMProtocol
18
+ from divi.extern.cirq import cirq_circuit_from_qasm
19
19
 
20
20
  OPENQASM_GATES = {
21
21
  "CNOT": "cx",
@@ -5,4 +5,4 @@
5
5
  # TODO: delete whole module once Cirq properly supports parameters in openqasm 3.0
6
6
  from . import _qasm_export # Does nothing, just initiates the patch
7
7
  from ._qasm_import import cirq_circuit_from_qasm
8
- from ._validator import is_valid_qasm
8
+ from ._validator import is_valid_qasm, validate_qasm_raise
@@ -289,7 +289,12 @@ class Parser:
289
289
  # ---- gate definitions ----
290
290
  def gate_def(self):
291
291
  self.match("GATE")
292
- gname = self.match("ID").value
292
+ gname_tok = self.match("ID")
293
+ gname = gname_tok.value
294
+ if gname in BUILTINS:
295
+ raise SyntaxError(
296
+ f"Cannot redefine built-in gate '{gname}' at {gname_tok.line}:{gname_tok.col}"
297
+ )
293
298
  if gname in self.user_gates:
294
299
  self._dupe(gname)
295
300
  params: tuple[str, ...] = ()
@@ -558,7 +563,7 @@ class Parser:
558
563
  self._expr_power(allow_id)
559
564
 
560
565
  def _expr_unary(self, allow_id: bool):
561
- if self.peek().type in ("PLUS", "MINUS"):
566
+ while self.peek().type in ("PLUS", "MINUS"):
562
567
  self.match(self.peek().type)
563
568
  self._expr_atom(allow_id)
564
569
 
@@ -570,7 +575,6 @@ class Parser:
570
575
  if t.type == "PI":
571
576
  self.match("PI")
572
577
  return
573
- # ---- NEW BLOCK TO HANDLE MATH FUNCTIONS ----
574
578
  if t.type in _MATH_FUNCS:
575
579
  self.match(t.type) # Consume the function name (e.g., COS)
576
580
  self.match("LPAREN")
@@ -578,13 +582,11 @@ class Parser:
578
582
  # Note: QASM 2.0 math functions only take one argument
579
583
  self.match("RPAREN")
580
584
  return
581
- # --------------------------------------------
582
585
  if t.type == "ID":
583
586
  # function call or plain ID
584
587
  id_tok = self.match("ID")
585
588
  ident = id_tok.value
586
589
  if self.accept("LPAREN"):
587
- # This now correctly handles user-defined functions (if any)
588
590
  if self.peek().type != "RPAREN":
589
591
  self._expr(allow_id)
590
592
  while self.accept("COMMA"):
@@ -637,9 +639,9 @@ def validate_qasm_raise(src: str) -> None:
637
639
  Parser(toks).parse()
638
640
 
639
641
 
640
- def is_valid_qasm(src: str) -> bool:
642
+ def is_valid_qasm(src: str) -> bool | str:
641
643
  try:
642
644
  validate_qasm_raise(src)
643
645
  return True
644
- except SyntaxError:
645
- return False
646
+ except SyntaxError as e:
647
+ return str(e)
@@ -0,0 +1,26 @@
1
+ # SPDX-FileCopyrightText: 2025 Qoro Quantum Ltd <divi@qoroquantum.de>
2
+ #
3
+ # SPDX-License-Identifier: Apache-2.0
4
+
5
+ # isort: skip_file
6
+ from .quantum_program import QuantumProgram
7
+ from .batch import ProgramBatch
8
+ from .algorithms import (
9
+ QAOA,
10
+ GraphProblem,
11
+ VQE,
12
+ Ansatz,
13
+ UCCSDAnsatz,
14
+ QAOAAnsatz,
15
+ HardwareEfficientAnsatz,
16
+ HartreeFockAnsatz,
17
+ GenericLayerAnsatz,
18
+ )
19
+ from .workflows import (
20
+ GraphPartitioningQAOA,
21
+ PartitioningConfig,
22
+ QUBOPartitioningQAOA,
23
+ VQEHyperparameterSweep,
24
+ MoleculeTransformer,
25
+ )
26
+ from .optimizers import ScipyOptimizer, ScipyMethod, MonteCarloOptimizer
@@ -0,0 +1,14 @@
1
+ # SPDX-FileCopyrightText: 2025 Qoro Quantum Ltd <divi@qoroquantum.de>
2
+ #
3
+ # SPDX-License-Identifier: Apache-2.0
4
+
5
+ from ._ansatze import (
6
+ Ansatz,
7
+ GenericLayerAnsatz,
8
+ HardwareEfficientAnsatz,
9
+ HartreeFockAnsatz,
10
+ QAOAAnsatz,
11
+ UCCSDAnsatz,
12
+ )
13
+ from ._qaoa import QAOA, GraphProblem, GraphProblemTypes, QUBOProblemTypes
14
+ from ._vqe import VQE
@@ -0,0 +1,215 @@
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
+ from itertools import tee
7
+ from typing import Literal, Sequence
8
+ from warnings import warn
9
+
10
+ import pennylane as qml
11
+
12
+
13
+ class Ansatz(ABC):
14
+ """Abstract base class for all VQE ansaetze."""
15
+
16
+ @property
17
+ def name(self) -> str:
18
+ """Returns the human-readable name of the ansatz."""
19
+ return self.__class__.__name__
20
+
21
+ @staticmethod
22
+ @abstractmethod
23
+ def n_params_per_layer(n_qubits: int, **kwargs) -> int:
24
+ """Returns the number of parameters required by the ansatz for one layer."""
25
+ raise NotImplementedError
26
+
27
+ @abstractmethod
28
+ def build(self, params, n_qubits: int, n_layers: int, **kwargs):
29
+ """
30
+ Builds the ansatz circuit.
31
+
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.
37
+ """
38
+ raise NotImplementedError
39
+
40
+
41
+ # --- Template Ansaetze ---
42
+
43
+
44
+ class GenericLayerAnsatz(Ansatz):
45
+ """
46
+ A flexible ansatz alternating single-qubit gates with optional entanglers.
47
+ """
48
+
49
+ def __init__(
50
+ self,
51
+ gate_sequence: list[qml.operation.Operator],
52
+ entangler: qml.operation.Operator | None = None,
53
+ entangling_layout: (
54
+ Literal["linear", "brick", "circular", "all-to-all"]
55
+ | Sequence[tuple[int, int]]
56
+ | None
57
+ ) = None,
58
+ ):
59
+ """
60
+ Args:
61
+ gate_sequence (list[Callable]): List of one-qubit gate classes (e.g., qml.RY, qml.Rot).
62
+ entangler (Callable): Two-qubit entangling gate class (e.g., qml.CNOT, qml.CZ).
63
+ If None, no entanglement is applied.
64
+ entangling_layout (str): Layout for entangling layer ("linear", "all_to_all", etc.).
65
+ """
66
+ if not all(
67
+ issubclass(g, qml.operation.Operator) and g.num_wires == 1
68
+ for g in gate_sequence
69
+ ):
70
+ raise ValueError(
71
+ "All elements in gate_sequence must be PennyLane one-qubit gate classes."
72
+ )
73
+ self.gate_sequence = gate_sequence
74
+
75
+ if entangler not in (None, qml.CNOT, qml.CZ):
76
+ raise ValueError("Only qml.CNOT and qml.CZ are supported as entanglers.")
77
+ self.entangler = entangler
78
+
79
+ self.entangling_layout = entangling_layout
80
+ if entangler is None and self.entangling_layout is not None:
81
+ warn("`entangling_layout` provided but `entangler` is None.")
82
+ match self.entangling_layout:
83
+ case None | "linear":
84
+ self.entangling_layout = "linear"
85
+
86
+ self._layout_fn = lambda n_qubits: zip(
87
+ range(n_qubits), range(1, n_qubits)
88
+ )
89
+ case "brick":
90
+ self._layout_fn = lambda n_qubits: [
91
+ (i, i + 1) for r in range(2) for i in range(r, n_qubits - 1, 2)
92
+ ]
93
+ case "circular":
94
+ self._layout_fn = lambda n_qubits: zip(
95
+ range(n_qubits), [(i + 1) % n_qubits for i in range(n_qubits)]
96
+ )
97
+ case "all_to_all":
98
+ self._layout_fn = lambda n_qubits: (
99
+ (i, j) for i in range(n_qubits) for j in range(i + 1, n_qubits)
100
+ )
101
+ case _:
102
+ if not all(
103
+ isinstance(ent, tuple)
104
+ and len(ent) == 2
105
+ and isinstance(ent[0], int)
106
+ and isinstance(ent[1], int)
107
+ for ent in entangling_layout
108
+ ):
109
+ raise ValueError(
110
+ "entangling_layout must be 'linear', 'circular', "
111
+ "'all_to_all', or a Sequence of tuples of integers."
112
+ )
113
+
114
+ self._layout_fn = lambda _: entangling_layout
115
+
116
+ def n_params_per_layer(self, n_qubits: int, **kwargs) -> int:
117
+ """Total parameters = sum of gate.num_params per qubit per layer."""
118
+ per_qubit = sum(getattr(g, "num_params", 1) for g in self.gate_sequence)
119
+ return per_qubit * n_qubits
120
+
121
+ def build(self, params, n_qubits: int, n_layers: int, **kwargs):
122
+ # calculate how many params each gate needs per qubit
123
+ gate_param_counts = [getattr(g, "num_params", 1) for g in self.gate_sequence]
124
+ per_qubit = sum(gate_param_counts)
125
+
126
+ # reshape into [layers, qubits, per_qubit]
127
+ params = params.reshape(n_layers, n_qubits, per_qubit)
128
+ layout_gen = iter(tee(self._layout_fn(n_qubits), n_layers))
129
+
130
+ def _layer(layer_params, wires):
131
+ for w, qubit_params in zip(wires, layer_params):
132
+ idx = 0
133
+ for gate, n_p in zip(self.gate_sequence, gate_param_counts):
134
+ theta = qubit_params[idx : idx + n_p]
135
+ gate(*theta, wires=w)
136
+ idx += n_p
137
+
138
+ if self.entangler is not None:
139
+ for wire_a, wire_b in next(layout_gen):
140
+ self.entangler(wires=[wire_a, wire_b])
141
+
142
+ qml.layer(_layer, n_layers, params, wires=range(n_qubits))
143
+
144
+
145
+ class QAOAAnsatz(Ansatz):
146
+ @staticmethod
147
+ def n_params_per_layer(n_qubits: int, **kwargs) -> int:
148
+ return qml.QAOAEmbedding.shape(n_layers=1, n_wires=n_qubits)[1]
149
+
150
+ def build(self, params, n_qubits: int, n_layers: int, **kwargs):
151
+ qml.QAOAEmbedding(
152
+ features=[],
153
+ weights=params.reshape(n_layers, -1),
154
+ wires=range(n_qubits),
155
+ )
156
+
157
+
158
+ class HardwareEfficientAnsatz(Ansatz):
159
+ @staticmethod
160
+ def n_params_per_layer(n_qubits: int, **kwargs) -> int:
161
+ raise NotImplementedError("HardwareEfficientAnsatz is not yet implemented.")
162
+
163
+ def build(self, params, n_qubits: int, n_layers: int, **kwargs) -> None:
164
+ raise NotImplementedError("HardwareEfficientAnsatz is not yet implemented.")
165
+
166
+
167
+ # --- Chemistry Ansaetze ---
168
+
169
+
170
+ class UCCSDAnsatz(Ansatz):
171
+ @staticmethod
172
+ def n_params_per_layer(n_qubits: int, n_electrons: int, **kwargs) -> int:
173
+ singles, doubles = qml.qchem.excitations(n_electrons, n_qubits)
174
+ s_wires, d_wires = qml.qchem.excitations_to_wires(singles, doubles)
175
+ return len(s_wires) + len(d_wires)
176
+
177
+ def build(self, params, n_qubits: int, n_layers: int, n_electrons: int, **kwargs):
178
+ singles, doubles = qml.qchem.excitations(n_electrons, n_qubits)
179
+ s_wires, d_wires = qml.qchem.excitations_to_wires(singles, doubles)
180
+ hf_state = qml.qchem.hf_state(n_electrons, n_qubits)
181
+
182
+ qml.UCCSD(
183
+ params.reshape(n_layers, -1),
184
+ wires=range(n_qubits),
185
+ s_wires=s_wires,
186
+ d_wires=d_wires,
187
+ init_state=hf_state,
188
+ n_repeats=n_layers,
189
+ )
190
+
191
+
192
+ class HartreeFockAnsatz(Ansatz):
193
+ @staticmethod
194
+ def n_params_per_layer(n_qubits: int, n_electrons: int, **kwargs) -> int:
195
+ singles, doubles = qml.qchem.excitations(n_electrons, n_qubits)
196
+ return len(singles) + len(doubles)
197
+
198
+ def build(self, params, n_qubits: int, n_layers: int, n_electrons: int, **kwargs):
199
+ singles, doubles = qml.qchem.excitations(n_electrons, n_qubits)
200
+ hf_state = qml.qchem.hf_state(n_electrons, n_qubits)
201
+
202
+ qml.layer(
203
+ qml.AllSinglesDoubles,
204
+ n_layers,
205
+ params.reshape(n_layers, -1),
206
+ wires=range(n_qubits),
207
+ hf_state=hf_state,
208
+ singles=singles,
209
+ doubles=doubles,
210
+ )
211
+
212
+ # Reset the BasisState operations after the first layer
213
+ # for behaviour similar to UCCSD ansatz
214
+ for op in qml.QueuingManager.active_context().queue[1:]:
215
+ op._hyperparameters["hf_state"] = 0
@@ -23,7 +23,7 @@ from qiskit_optimization.problems import VarType
23
23
 
24
24
  from divi.circuits import MetaCircuit
25
25
  from divi.qprog import QuantumProgram
26
- from divi.qprog.optimizers import Optimizer
26
+ from divi.qprog.optimizers import MonteCarloOptimizer, Optimizer
27
27
  from divi.utils import convert_qubo_matrix_to_pennylane_ising
28
28
 
29
29
  logger = logging.getLogger(__name__)
@@ -153,10 +153,11 @@ class QAOA(QuantumProgram):
153
153
  def __init__(
154
154
  self,
155
155
  problem: GraphProblemTypes | QUBOProblemTypes,
156
+ *,
156
157
  graph_problem: GraphProblem | None = None,
157
158
  n_layers: int = 1,
158
159
  initial_state: _SUPPORTED_INITIAL_STATES_LITERAL = "Recommended",
159
- optimizer: Optimizer = Optimizer.MONTE_CARLO,
160
+ optimizer: Optimizer | None = None,
160
161
  max_iterations: int = 10,
161
162
  **kwargs,
162
163
  ):
@@ -221,11 +222,11 @@ class QAOA(QuantumProgram):
221
222
 
222
223
  # Local Variables
223
224
  self.n_layers = n_layers
224
- self.optimizer = optimizer
225
225
  self.max_iterations = max_iterations
226
226
  self.current_iteration = 0
227
- self.n_params = 2
227
+ self._n_params = 2
228
228
  self._is_compute_probabilites = False
229
+ self.optimizer = optimizer if optimizer is not None else MonteCarloOptimizer()
229
230
 
230
231
  # Shared Variables
231
232
  self.probs = kwargs.pop("probs", {})
@@ -245,10 +246,17 @@ class QAOA(QuantumProgram):
245
246
  )
246
247
  self.problem_metadata = problem_metadata[0] if problem_metadata else {}
247
248
 
248
- self.loss_constant = self.problem_metadata.get("constant", 0.0)
249
+ if "constant" in self.problem_metadata:
250
+ self.loss_constant = self.problem_metadata.get("constant")
251
+ try:
252
+ self.loss_constant = self.loss_constant.item()
253
+ except AttributeError:
254
+ pass
255
+ else:
256
+ self.loss_constant = 0.0
249
257
 
250
258
  kwargs.pop("is_constrained", None)
251
- super().__init__(**kwargs)
259
+ super().__init__(has_final_computation=True, **kwargs)
252
260
 
253
261
  self._meta_circuits = self._create_meta_circuits_dict()
254
262
 
@@ -387,16 +395,7 @@ class QAOA(QuantumProgram):
387
395
  - float: The total runtime of the optimization process.
388
396
  """
389
397
 
390
- if self._progress_queue:
391
- self._progress_queue.put(
392
- {
393
- "job_id": self.job_id,
394
- "message": "🏁 Computing Final Solution 🏁",
395
- "progress": 0,
396
- }
397
- )
398
- else:
399
- logger.info("🏁 Computing Final Solution 🏁")
398
+ self.reporter.info(message="🏁 Computing Final Solution 🏁")
400
399
 
401
400
  # Convert losses dict to list to apply ordinal operations
402
401
  final_losses_list = list(self.losses[-1].values())
@@ -440,16 +439,7 @@ class QAOA(QuantumProgram):
440
439
  m.start() for m in re.finditer("1", best_solution_bitstring)
441
440
  ]
442
441
 
443
- if self._progress_queue:
444
- self._progress_queue.put(
445
- {
446
- "job_id": self.job_id,
447
- "progress": 0,
448
- "final_status": "Success",
449
- }
450
- )
451
- else:
452
- logger.info(f"Computed Solution!")
442
+ self.reporter.info(message="Computed Final Solution!")
453
443
 
454
444
  return self._total_circuit_count, self._total_run_time
455
445