qoro-divi 0.2.0b1__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.
Files changed (58) hide show
  1. divi/__init__.py +8 -0
  2. divi/_pbar.py +73 -0
  3. divi/circuits.py +139 -0
  4. divi/exp/cirq/__init__.py +7 -0
  5. divi/exp/cirq/_lexer.py +126 -0
  6. divi/exp/cirq/_parser.py +889 -0
  7. divi/exp/cirq/_qasm_export.py +37 -0
  8. divi/exp/cirq/_qasm_import.py +35 -0
  9. divi/exp/cirq/exception.py +21 -0
  10. divi/exp/scipy/_cobyla.py +342 -0
  11. divi/exp/scipy/pyprima/LICENCE.txt +28 -0
  12. divi/exp/scipy/pyprima/__init__.py +263 -0
  13. divi/exp/scipy/pyprima/cobyla/__init__.py +0 -0
  14. divi/exp/scipy/pyprima/cobyla/cobyla.py +599 -0
  15. divi/exp/scipy/pyprima/cobyla/cobylb.py +849 -0
  16. divi/exp/scipy/pyprima/cobyla/geometry.py +240 -0
  17. divi/exp/scipy/pyprima/cobyla/initialize.py +269 -0
  18. divi/exp/scipy/pyprima/cobyla/trustregion.py +540 -0
  19. divi/exp/scipy/pyprima/cobyla/update.py +331 -0
  20. divi/exp/scipy/pyprima/common/__init__.py +0 -0
  21. divi/exp/scipy/pyprima/common/_bounds.py +41 -0
  22. divi/exp/scipy/pyprima/common/_linear_constraints.py +46 -0
  23. divi/exp/scipy/pyprima/common/_nonlinear_constraints.py +64 -0
  24. divi/exp/scipy/pyprima/common/_project.py +224 -0
  25. divi/exp/scipy/pyprima/common/checkbreak.py +107 -0
  26. divi/exp/scipy/pyprima/common/consts.py +48 -0
  27. divi/exp/scipy/pyprima/common/evaluate.py +101 -0
  28. divi/exp/scipy/pyprima/common/history.py +39 -0
  29. divi/exp/scipy/pyprima/common/infos.py +30 -0
  30. divi/exp/scipy/pyprima/common/linalg.py +452 -0
  31. divi/exp/scipy/pyprima/common/message.py +336 -0
  32. divi/exp/scipy/pyprima/common/powalg.py +131 -0
  33. divi/exp/scipy/pyprima/common/preproc.py +393 -0
  34. divi/exp/scipy/pyprima/common/present.py +5 -0
  35. divi/exp/scipy/pyprima/common/ratio.py +56 -0
  36. divi/exp/scipy/pyprima/common/redrho.py +49 -0
  37. divi/exp/scipy/pyprima/common/selectx.py +346 -0
  38. divi/interfaces.py +25 -0
  39. divi/parallel_simulator.py +258 -0
  40. divi/qasm.py +220 -0
  41. divi/qem.py +191 -0
  42. divi/qlogger.py +119 -0
  43. divi/qoro_service.py +343 -0
  44. divi/qprog/__init__.py +13 -0
  45. divi/qprog/_graph_partitioning.py +619 -0
  46. divi/qprog/_mlae.py +182 -0
  47. divi/qprog/_qaoa.py +440 -0
  48. divi/qprog/_vqe.py +275 -0
  49. divi/qprog/_vqe_sweep.py +144 -0
  50. divi/qprog/batch.py +235 -0
  51. divi/qprog/optimizers.py +75 -0
  52. divi/qprog/quantum_program.py +493 -0
  53. divi/utils.py +116 -0
  54. qoro_divi-0.2.0b1.dist-info/LICENSE +190 -0
  55. qoro_divi-0.2.0b1.dist-info/LICENSES/Apache-2.0.txt +73 -0
  56. qoro_divi-0.2.0b1.dist-info/METADATA +57 -0
  57. qoro_divi-0.2.0b1.dist-info/RECORD +58 -0
  58. qoro_divi-0.2.0b1.dist-info/WHEEL +4 -0
divi/qasm.py ADDED
@@ -0,0 +1,220 @@
1
+ # SPDX-FileCopyrightText: 2025 Qoro Quantum Ltd <divi@qoroquantum.de>
2
+ #
3
+ # SPDX-License-Identifier: Apache-2.0
4
+
5
+ import re
6
+ from functools import partial
7
+ from itertools import product
8
+ from typing import Optional
9
+ from warnings import warn
10
+
11
+ import cirq
12
+ import numpy as np
13
+ import pennylane as qml
14
+ from pennylane.tape import QuantumScript
15
+ from pennylane.wires import Wires
16
+ from sympy import Symbol
17
+
18
+ from divi.exp.cirq import cirq_circuit_from_qasm
19
+ from divi.qem import QEMProtocol
20
+
21
+ OPENQASM_GATES = {
22
+ "CNOT": "cx",
23
+ "CZ": "cz",
24
+ "U3": "u3",
25
+ "U2": "u2",
26
+ "U1": "u1",
27
+ "Identity": "id",
28
+ "PauliX": "x",
29
+ "PauliY": "y",
30
+ "PauliZ": "z",
31
+ "Hadamard": "h",
32
+ "S": "s",
33
+ "Adjoint(S)": "sdg",
34
+ "T": "t",
35
+ "Adjoint(T)": "tdg",
36
+ "RX": "rx",
37
+ "RY": "ry",
38
+ "RZ": "rz",
39
+ "CRX": "crx",
40
+ "CRY": "cry",
41
+ "CRZ": "crz",
42
+ "SWAP": "swap",
43
+ "Toffoli": "ccx",
44
+ "CSWAP": "cswap",
45
+ "PhaseShift": "u1",
46
+ }
47
+
48
+
49
+ def _ops_to_qasm(operations, precision, wires):
50
+ # create the QASM code representing the operations
51
+ qasm_str = ""
52
+
53
+ for op in operations:
54
+ try:
55
+ gate = OPENQASM_GATES[op.name]
56
+ except KeyError as e:
57
+ raise ValueError(
58
+ f"Operation {op.name} not supported by the QASM serializer"
59
+ ) from e
60
+
61
+ wire_labels = ",".join([f"q[{wires.index(w)}]" for w in op.wires.tolist()])
62
+ params = ""
63
+
64
+ if op.num_params > 0:
65
+ # If the operation takes parameters, construct a string
66
+ # with parameter values.
67
+ if precision is not None:
68
+ params = (
69
+ "(" + ",".join([f"{p:.{precision}}" for p in op.parameters]) + ")"
70
+ )
71
+ else:
72
+ # use default precision
73
+ params = "(" + ",".join([str(p) for p in op.parameters]) + ")"
74
+
75
+ qasm_str += f"{gate}{params} {wire_labels};\n"
76
+
77
+ return qasm_str
78
+
79
+
80
+ def to_openqasm(
81
+ main_qscript,
82
+ measurement_groups: list[list[qml.measurements.ExpectationMP]],
83
+ measure_all: bool = True,
84
+ precision: Optional[int] = None,
85
+ return_measurements_separately: bool = False,
86
+ symbols: list[Symbol] = None,
87
+ qem_protocol: Optional[QEMProtocol] = None,
88
+ ) -> list[str] | tuple[str, list[str]]:
89
+ """
90
+ Serialize the circuit as an OpenQASM 2.0 program.
91
+
92
+ A modified version of PennyLane's function that is more compatible with having
93
+ several measurements and incorporates modifications introduced by splitting transforms,
94
+ as well as error mitigation through folding.
95
+
96
+ The measurement outputs can be restricted to only those specified in the script by
97
+ setting ``measure_all=False``.
98
+
99
+ .. note::
100
+
101
+ The serialized OpenQASM program assumes that gate definitions
102
+ in ``qelib1.inc`` are available.
103
+
104
+ Args:
105
+ main_qscript (QuantumScript): the quantum circuit to be converted, as a QuantumScript/QuantumTape object.
106
+ measurement_groups (list[list]): A list of list of commuting observables, generated by the grouping Pennylane transformation.
107
+ measure_all (bool): whether to perform a computational basis measurement on all qubits
108
+ or just those specified in the script
109
+ precision (int): decimal digits to display for parameters
110
+ return_measurements_separately (bool): whether to not append the measurement instructions
111
+ and their diagonalizations to the main circuit QASM code and return separately.
112
+ symbols (list): Sympy symbols present in the circuit. Needed for some QEM routines.
113
+ qem_protocol (QEMProtocol): An optional QEMProtocol object for error mitigation, which may modify the circuit.
114
+
115
+ Returns:
116
+ list[str] or tuple[str, list[str]]: OpenQASM serialization of the circuit
117
+ """
118
+
119
+ if qem_protocol and symbols is None:
120
+ raise ValueError(
121
+ "When passing a QEMProtocol instance, the Sympy symbols in the circuit should be provided for the openqasm 3 conversion."
122
+ )
123
+
124
+ wires = main_qscript.wires
125
+
126
+ _to_qasm = partial(_ops_to_qasm, precision=precision, wires=wires)
127
+
128
+ # Add the QASM headers
129
+ main_qasm_str = (
130
+ 'OPENQASM 3.0;\ninclude "stdgates.inc";\n'
131
+ if qem_protocol
132
+ else 'OPENQASM 2.0;\ninclude "qelib1.inc";\n'
133
+ )
134
+
135
+ if main_qscript.num_wires == 0:
136
+ # empty circuit
137
+ return main_qasm_str
138
+
139
+ if qem_protocol:
140
+ for symbol in symbols:
141
+ main_qasm_str += f"input angle[32] {str(symbol)};\n"
142
+
143
+ # create the quantum and classical registers
144
+ main_qasm_str += (
145
+ f"qubit[{len(wires)}] q;\n" if qem_protocol else f"qreg q[{len(wires)}];\n"
146
+ )
147
+ main_qasm_str += (
148
+ f"bit[{len(wires)}] c;\n" if qem_protocol else f"creg c[{len(wires)}];\n"
149
+ )
150
+
151
+ # Wrapping Sympy Symbols in a numpy object to bypass
152
+ # Pennylane's sanitization
153
+ for op in main_qscript.operations:
154
+ if qml.math.get_interface(*op.data) == "sympy":
155
+ op.data = np.array(op.data)
156
+
157
+ [transformed_tape], _ = qml.transforms.convert_to_numpy_parameters(main_qscript)
158
+ operations = transformed_tape.operations
159
+
160
+ # Decompose the queue
161
+ just_ops = QuantumScript(operations)
162
+ [decomposed_tape], _ = qml.transforms.decompose(
163
+ just_ops, gate_set=lambda obj: obj.name in OPENQASM_GATES
164
+ )
165
+
166
+ main_qasm_str += _to_qasm(decomposed_tape.operations)
167
+
168
+ main_qasm_strs = []
169
+ if qem_protocol:
170
+ for circ in qem_protocol.modify_circuit(cirq_circuit_from_qasm(main_qasm_str)):
171
+ # Convert back to QASM2.0 code, with the symbolic parameters
172
+ qasm_str = cirq.qasm(circ)
173
+ # Remove redundant newlines
174
+ qasm_str = re.sub(r"\n+", "\n", qasm_str)
175
+ # Remove comments
176
+ qasm_str = re.sub(r"^//.*\n?", "", qasm_str, flags=re.MULTILINE)
177
+ # Add missing classical reg
178
+ qasm_str = re.sub(r"qreg q\[(\d+)\];", r"qreg q[\1];creg c[\1];", qasm_str)
179
+
180
+ main_qasm_strs.append(qasm_str)
181
+ else:
182
+ main_qasm_strs.append(main_qasm_str)
183
+
184
+ qasm_circuits = []
185
+ measurement_qasms = []
186
+
187
+ # Create a copy of the program for every measurement that we have
188
+ for meas_group in measurement_groups:
189
+ curr_diag_qasm_str = (
190
+ _to_qasm(diag_ops)
191
+ if (diag_ops := QuantumScript(measurements=meas_group).diagonalizing_gates)
192
+ else ""
193
+ )
194
+
195
+ measure_qasm_str = ""
196
+ if measure_all:
197
+ for wire in range(len(wires)):
198
+ measure_qasm_str += f"measure q[{wire}] -> c[{wire}];\n"
199
+ else:
200
+ measured_wires = Wires.all_wires(
201
+ [m.wires for m in main_qscript.measurements]
202
+ )
203
+
204
+ for w in measured_wires:
205
+ wire_indx = main_qscript.wires.index(w)
206
+ measure_qasm_str += f"measure q[{wire_indx}] -> c[{wire_indx}];\n"
207
+
208
+ measurement_qasms.append(curr_diag_qasm_str + measure_qasm_str)
209
+
210
+ if not return_measurements_separately:
211
+ qasm_circuits.extend(product(main_qasm_strs, measurement_qasms))
212
+
213
+ if len(measurement_groups) == 0:
214
+ warn(
215
+ "No measurement groups provided. Returning the QASM of the circuit operations only."
216
+ )
217
+ qasm_circuits.extend(np.atleast_1d(main_qasm_strs).tolist())
218
+ return qasm_circuits
219
+
220
+ return qasm_circuits or (np.atleast_1d(main_qasm_strs).tolist(), measurement_qasms)
divi/qem.py ADDED
@@ -0,0 +1,191 @@
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 collections.abc import Callable, Sequence
7
+ from functools import partial
8
+
9
+ from cirq.circuits.circuit import Circuit
10
+ from mitiq.zne import combine_results, construct_circuits
11
+ from mitiq.zne.inference import Factory
12
+
13
+
14
+ class QEMProtocol(ABC):
15
+ """
16
+ Abstract Base Class for Quantum Error Mitigation (QEM) protocols.
17
+
18
+ All concrete QEM protocols should inherit from this class and implement
19
+ the abstract methods and properties. This ensures a consistent interface
20
+ across different mitigation techniques.
21
+ """
22
+
23
+ @property
24
+ @abstractmethod
25
+ def name(self) -> str:
26
+ pass
27
+
28
+ @abstractmethod
29
+ def modify_circuit(self, cirq_circuit: Circuit) -> Sequence[Circuit]:
30
+ """
31
+ Modifies a given Cirq circuit into one or more new circuits
32
+ required by the QEM protocol.
33
+
34
+ For example, a Zero Noise Extrapolation (ZNE) protocol might
35
+ produce multiple scaled versions of the input circuit. A simple
36
+ mitigation protocol might return the original circuit unchanged.
37
+
38
+ Args:
39
+ cirq_circuit (cirq.Circuit): The input quantum circuit to be modified.
40
+
41
+ Returns:
42
+ Sequence[cirq.Circuit]: A sequence (e.g., list or tuple) of
43
+ Cirq circuits to be executed.
44
+ """
45
+ pass
46
+
47
+ @abstractmethod
48
+ def postprocess_results(self, results: Sequence[float]) -> float:
49
+ """
50
+ Applies post-processing (e.g., extrapolation, filtering) to the
51
+ results obtained from executing the modified circuits.
52
+
53
+ This method takes the raw output from quantum circuit executions
54
+ (typically a sequence of expectation values or probabilities) and
55
+ applies the core error mitigation logic to produce a single,
56
+ mitigated result.
57
+
58
+ Args:
59
+ results (Sequence[float]): A sequence of floating-point results,
60
+ corresponding to the executions of the
61
+ circuits returned by `modify_circuit`.
62
+
63
+ Returns:
64
+ float: The single, mitigated result after post-processing.
65
+ """
66
+ pass
67
+
68
+
69
+ class _NoMitigation(QEMProtocol):
70
+ """
71
+ A dummy default mitigation protocol.
72
+ """
73
+
74
+ @property
75
+ def name(self) -> str:
76
+ return "NoMitigation"
77
+
78
+ def modify_circuit(self, cirq_circuit: Circuit) -> Sequence[Circuit]:
79
+ # Identity, do nothing
80
+ return [cirq_circuit]
81
+
82
+ def postprocess_results(self, results: Sequence[float]) -> float:
83
+ """
84
+ Returns the single result provided, ensuring only one result is given.
85
+
86
+ If multiple results are provided, it raises a RuntimeError, as this
87
+ protocol expects a single measurement outcome for its input circuit.
88
+
89
+ Args:
90
+ results (Sequence[float]): A sequence containing a single floating-point result.
91
+
92
+ Returns:
93
+ float: The single result from the sequence.
94
+
95
+ Raises:
96
+ RuntimeError: If more than one result is provided.
97
+ """
98
+ if len(results) > 1:
99
+ raise RuntimeError("NoMitigation class received multiple partial results.")
100
+
101
+ return results[0]
102
+
103
+
104
+ class ZNE(QEMProtocol):
105
+ """
106
+ Implements the Zero Noise Extrapolation (ZNE) quantum error mitigation protocol.
107
+
108
+ This protocol uses `Mitiq`'s functionalities to construct noise-scaled
109
+ circuits and then extrapolate to the zero-noise limit based on the
110
+ obtained results.
111
+ """
112
+
113
+ def __init__(
114
+ self,
115
+ scale_factors: Sequence[float],
116
+ folding_fn: Callable,
117
+ extrapolation_factory: Factory,
118
+ ):
119
+ """
120
+ Initializes a ZNE protocol instance.
121
+
122
+ Args:
123
+ scale_factors (Sequence[float]): A sequence of noise scale factors
124
+ to be applied to the circuits. These
125
+ factors typically range from 1.0 upwards.
126
+ folding_fn (Callable): A callable (e.g., a `functools.partial` object)
127
+ that defines how the circuit should be "folded"
128
+ to increase noise. This function must accept
129
+ a `cirq.Circuit` and a `float` (scale factor)
130
+ as its first two arguments.
131
+ extrapolation_factory (mitiq.zne.inference.Factory): An instance of
132
+ `Mitiq`'s `Factory`
133
+ class, which provides
134
+ the extrapolation method.
135
+
136
+ Raises:
137
+ ValueError: If `scale_factors` is not a sequence of numbers,
138
+ `folding_fn` is not callable, or `extrapolation_factory`
139
+ is not an instance of `mitiq.zne.inference.Factory`.
140
+ """
141
+ if (
142
+ not isinstance(scale_factors, Sequence)
143
+ or not all(isinstance(elem, (int, float)) for elem in scale_factors)
144
+ or not all(elem >= 1.0 for elem in scale_factors)
145
+ ):
146
+ raise ValueError(
147
+ "scale_factors is expected to be a sequence of real numbers >=1."
148
+ )
149
+
150
+ if not isinstance(folding_fn, partial):
151
+ raise ValueError(
152
+ "folding_fn is expected to be of type partial with all parameters "
153
+ "except for the circuit object and the scale factor already set."
154
+ )
155
+
156
+ if not isinstance(extrapolation_factory, Factory):
157
+ raise ValueError("extrapolation_fn is expected to be of Factory.")
158
+
159
+ self._scale_factors = scale_factors
160
+ self._folding_fn = folding_fn
161
+ self._extrapolation_factory = extrapolation_factory
162
+
163
+ @property
164
+ def name(self) -> str:
165
+ return "zne"
166
+
167
+ @property
168
+ def scale_factors(self) -> Sequence[float]:
169
+ return self._scale_factors
170
+
171
+ @property
172
+ def folding_fn(self):
173
+ return self._folding_fn
174
+
175
+ @property
176
+ def extrapolation_factory(self):
177
+ return self._extrapolation_factory
178
+
179
+ def modify_circuit(self, cirq_circuit: Circuit) -> Sequence[Circuit]:
180
+ return construct_circuits(
181
+ cirq_circuit,
182
+ scale_factors=self._scale_factors,
183
+ scale_method=self._folding_fn,
184
+ )
185
+
186
+ def postprocess_results(self, results: Sequence[float]) -> float:
187
+ return combine_results(
188
+ scale_factors=self._scale_factors,
189
+ results=results,
190
+ extrapolation_method=self._extrapolation_factory.extrapolate,
191
+ )
divi/qlogger.py ADDED
@@ -0,0 +1,119 @@
1
+ # SPDX-FileCopyrightText: 2025 Qoro Quantum Ltd <divi@qoroquantum.de>
2
+ #
3
+ # SPDX-License-Identifier: Apache-2.0
4
+
5
+ import logging
6
+ import shutil
7
+ import sys
8
+
9
+
10
+ def _is_jupyter():
11
+ """
12
+ Checks if the code is running inside a Jupyter Notebook or IPython environment.
13
+ """
14
+ try:
15
+ from IPython import get_ipython
16
+
17
+ # Check if get_ipython() returns a shell instance (not None)
18
+ # and if the shell class is 'ZMQInteractiveShell' for Jupyter notebooks/qtconsole
19
+ # or 'TerminalInteractiveShell' for IPython console.
20
+ shell = get_ipython().__class__.__name__
21
+ if shell == "ZMQInteractiveShell":
22
+ return True # Jupyter notebook or qtconsole
23
+ elif shell == "TerminalInteractiveShell":
24
+ return False # IPython terminal
25
+ else:
26
+ return False # Other IPython environment (less common for typical Jupyter detection)
27
+ except NameError:
28
+ return False # Not running in IPython
29
+ except ImportError:
30
+ return False # IPython is not installed
31
+
32
+
33
+ class OverwriteStreamHandler(logging.StreamHandler):
34
+ def __init__(self, stream=None):
35
+ super().__init__(stream)
36
+
37
+ self._last_record = ""
38
+ self._last_message = ""
39
+
40
+ # Worst case: 2 complex emojis (8 chars each) + buffer = 21 extra chars
41
+ self._emoji_buffer = 21
42
+
43
+ self._is_jupyter = _is_jupyter()
44
+
45
+ def emit(self, record):
46
+ msg = self.format(record)
47
+
48
+ if record.message.startswith(r"\c"):
49
+ sep = r"\c"
50
+ msg = f"{msg.split(sep)[0]}{self._last_record} [{record.message[2:-2]}]\r"
51
+
52
+ if msg.endswith("\r\n"):
53
+ overwrite_and_newline = True
54
+ clean_msg = msg[:-2]
55
+
56
+ if not record.message.startswith("\c"):
57
+ self._last_record = record.message[:-2]
58
+ elif msg.endswith("\r"):
59
+ overwrite_and_newline = False
60
+ clean_msg = msg[:-1]
61
+
62
+ if not record.message.startswith(r"\c"):
63
+ self._last_record = record.message[:-1]
64
+ else:
65
+ # Normal message - no overwriting
66
+ self.stream.write(msg + "\n")
67
+ self.stream.flush()
68
+ return
69
+
70
+ # Clear previous line if needed
71
+ if len(self._last_message) > 0:
72
+ if self._is_jupyter:
73
+ clear_length = len(self._last_message) + self._emoji_buffer + 50
74
+ else:
75
+ clear_length = min(
76
+ len(self._last_message) + self._emoji_buffer,
77
+ shutil.get_terminal_size().columns,
78
+ )
79
+
80
+ self.stream.write("\r" + " " * clear_length + "\r")
81
+ self.stream.flush()
82
+
83
+ # Write message with appropriate ending
84
+ if overwrite_and_newline:
85
+ self.stream.write(clean_msg + "\n")
86
+ self._last_message = ""
87
+ else:
88
+ self.stream.write(clean_msg + "\r")
89
+ self._last_message = self._strip_ansi(clean_msg)
90
+
91
+ self.stream.flush()
92
+
93
+ def _strip_ansi(self, text):
94
+ """Remove ANSI escape sequences for accurate length calculation"""
95
+ import re
96
+
97
+ ansi_escape = re.compile(r"\x1B(?:[@-Z\\-_]|\[[0-?]*[ -/]*[@-~])")
98
+ return ansi_escape.sub("", text)
99
+
100
+
101
+ def enable_logging(level=logging.INFO):
102
+ root_logger = logging.getLogger(__name__.split(".")[0])
103
+
104
+ formatter = logging.Formatter(
105
+ "%(asctime)s - %(name)s - %(levelname)s - %(message)s"
106
+ )
107
+
108
+ handler = OverwriteStreamHandler(sys.stdout)
109
+ handler.setFormatter(formatter)
110
+
111
+ root_logger.setLevel(level)
112
+ root_logger.handlers.clear()
113
+ root_logger.addHandler(handler)
114
+
115
+
116
+ def disable_logging():
117
+ root_logger = logging.getLogger(__name__.split(".")[0])
118
+ root_logger.handlers.clear()
119
+ root_logger.setLevel(logging.CRITICAL + 1)