qoro-divi 0.2.0b1__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/__init__.py +1 -2
- divi/backends/__init__.py +10 -0
- divi/backends/_backend_properties_conversion.py +227 -0
- divi/backends/_circuit_runner.py +70 -0
- divi/backends/_execution_result.py +70 -0
- divi/backends/_parallel_simulator.py +486 -0
- divi/backends/_qoro_service.py +663 -0
- divi/backends/_qpu_system.py +101 -0
- divi/backends/_results_processing.py +133 -0
- divi/circuits/__init__.py +13 -0
- divi/{exp/cirq → circuits/_cirq}/__init__.py +1 -2
- divi/circuits/_cirq/_parser.py +110 -0
- divi/circuits/_cirq/_qasm_export.py +78 -0
- divi/circuits/_core.py +391 -0
- divi/{qasm.py → circuits/_qasm_conversion.py} +73 -14
- divi/circuits/_qasm_validation.py +694 -0
- divi/qprog/__init__.py +27 -8
- divi/qprog/_expectation.py +181 -0
- divi/qprog/_hamiltonians.py +281 -0
- divi/qprog/algorithms/__init__.py +16 -0
- divi/qprog/algorithms/_ansatze.py +368 -0
- divi/qprog/algorithms/_custom_vqa.py +263 -0
- divi/qprog/algorithms/_pce.py +262 -0
- divi/qprog/algorithms/_qaoa.py +579 -0
- divi/qprog/algorithms/_vqe.py +262 -0
- divi/qprog/batch.py +387 -74
- divi/qprog/checkpointing.py +556 -0
- divi/qprog/exceptions.py +9 -0
- divi/qprog/optimizers.py +1014 -43
- divi/qprog/quantum_program.py +243 -412
- divi/qprog/typing.py +62 -0
- divi/qprog/variational_quantum_algorithm.py +1208 -0
- divi/qprog/workflows/__init__.py +10 -0
- divi/qprog/{_graph_partitioning.py → workflows/_graph_partitioning.py} +139 -95
- divi/qprog/workflows/_qubo_partitioning.py +221 -0
- divi/qprog/workflows/_vqe_sweep.py +560 -0
- divi/reporting/__init__.py +7 -0
- divi/reporting/_pbar.py +127 -0
- divi/reporting/_qlogger.py +68 -0
- divi/reporting/_reporter.py +155 -0
- {qoro_divi-0.2.0b1.dist-info → qoro_divi-0.6.0.dist-info}/METADATA +43 -15
- qoro_divi-0.6.0.dist-info/RECORD +47 -0
- {qoro_divi-0.2.0b1.dist-info → qoro_divi-0.6.0.dist-info}/WHEEL +1 -1
- qoro_divi-0.6.0.dist-info/licenses/LICENSES/.license-header +3 -0
- divi/_pbar.py +0 -73
- divi/circuits.py +0 -139
- divi/exp/cirq/_lexer.py +0 -126
- divi/exp/cirq/_parser.py +0 -889
- divi/exp/cirq/_qasm_export.py +0 -37
- divi/exp/cirq/_qasm_import.py +0 -35
- divi/exp/cirq/exception.py +0 -21
- divi/exp/scipy/_cobyla.py +0 -342
- divi/exp/scipy/pyprima/LICENCE.txt +0 -28
- divi/exp/scipy/pyprima/__init__.py +0 -263
- divi/exp/scipy/pyprima/cobyla/__init__.py +0 -0
- divi/exp/scipy/pyprima/cobyla/cobyla.py +0 -599
- divi/exp/scipy/pyprima/cobyla/cobylb.py +0 -849
- divi/exp/scipy/pyprima/cobyla/geometry.py +0 -240
- divi/exp/scipy/pyprima/cobyla/initialize.py +0 -269
- divi/exp/scipy/pyprima/cobyla/trustregion.py +0 -540
- divi/exp/scipy/pyprima/cobyla/update.py +0 -331
- divi/exp/scipy/pyprima/common/__init__.py +0 -0
- divi/exp/scipy/pyprima/common/_bounds.py +0 -41
- divi/exp/scipy/pyprima/common/_linear_constraints.py +0 -46
- divi/exp/scipy/pyprima/common/_nonlinear_constraints.py +0 -64
- divi/exp/scipy/pyprima/common/_project.py +0 -224
- divi/exp/scipy/pyprima/common/checkbreak.py +0 -107
- divi/exp/scipy/pyprima/common/consts.py +0 -48
- divi/exp/scipy/pyprima/common/evaluate.py +0 -101
- divi/exp/scipy/pyprima/common/history.py +0 -39
- divi/exp/scipy/pyprima/common/infos.py +0 -30
- divi/exp/scipy/pyprima/common/linalg.py +0 -452
- divi/exp/scipy/pyprima/common/message.py +0 -336
- divi/exp/scipy/pyprima/common/powalg.py +0 -131
- divi/exp/scipy/pyprima/common/preproc.py +0 -393
- divi/exp/scipy/pyprima/common/present.py +0 -5
- divi/exp/scipy/pyprima/common/ratio.py +0 -56
- divi/exp/scipy/pyprima/common/redrho.py +0 -49
- divi/exp/scipy/pyprima/common/selectx.py +0 -346
- divi/interfaces.py +0 -25
- divi/parallel_simulator.py +0 -258
- divi/qlogger.py +0 -119
- divi/qoro_service.py +0 -343
- divi/qprog/_mlae.py +0 -182
- divi/qprog/_qaoa.py +0 -440
- divi/qprog/_vqe.py +0 -275
- divi/qprog/_vqe_sweep.py +0 -144
- divi/utils.py +0 -116
- qoro_divi-0.2.0b1.dist-info/RECORD +0 -58
- /divi/{qem.py → circuits/qem.py} +0 -0
- {qoro_divi-0.2.0b1.dist-info → qoro_divi-0.6.0.dist-info/licenses}/LICENSE +0 -0
- {qoro_divi-0.2.0b1.dist-info → qoro_divi-0.6.0.dist-info/licenses}/LICENSES/Apache-2.0.txt +0 -0
|
@@ -0,0 +1,101 @@
|
|
|
1
|
+
# SPDX-FileCopyrightText: 2025 Qoro Quantum Ltd <divi@qoroquantum.de>
|
|
2
|
+
#
|
|
3
|
+
# SPDX-License-Identifier: Apache-2.0
|
|
4
|
+
|
|
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
|
+
from threading import RLock
|
|
11
|
+
|
|
12
|
+
_AVAILABLE_QPU_SYSTEMS: dict[str, QPUSystem] = {}
|
|
13
|
+
_CACHE_LOCK = RLock()
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
@dataclass(frozen=True, repr=True)
|
|
17
|
+
class QPU:
|
|
18
|
+
"""Represents a single Quantum Processing Unit (QPU).
|
|
19
|
+
|
|
20
|
+
Attributes:
|
|
21
|
+
nickname: The unique name or identifier for the QPU.
|
|
22
|
+
q_bits: The number of qubits in the QPU.
|
|
23
|
+
status: The current operational status of the QPU.
|
|
24
|
+
system_kind: The type of technology the QPU uses.
|
|
25
|
+
"""
|
|
26
|
+
|
|
27
|
+
nickname: str
|
|
28
|
+
q_bits: int
|
|
29
|
+
status: str
|
|
30
|
+
system_kind: str
|
|
31
|
+
|
|
32
|
+
|
|
33
|
+
@dataclass(frozen=True, repr=True)
|
|
34
|
+
class QPUSystem:
|
|
35
|
+
"""Represents a collection of QPUs that form a quantum computing system.
|
|
36
|
+
|
|
37
|
+
Attributes:
|
|
38
|
+
name: The name of the QPU system.
|
|
39
|
+
qpus: A list of QPU objects that are part of this system.
|
|
40
|
+
access_level: The access level granted to the user for this system (e.g., 'PUBLIC').
|
|
41
|
+
supports_expval: Whether the system supports expectation value jobs.
|
|
42
|
+
"""
|
|
43
|
+
|
|
44
|
+
name: str
|
|
45
|
+
qpus: list[QPU] = field(default_factory=list)
|
|
46
|
+
access_level: str = "PUBLIC"
|
|
47
|
+
supports_expval: bool = False
|
|
48
|
+
|
|
49
|
+
|
|
50
|
+
def parse_qpu_systems(json_data: list) -> list[QPUSystem]:
|
|
51
|
+
"""Parses a list of QPU system data from JSON into QPUSystem objects."""
|
|
52
|
+
return [
|
|
53
|
+
QPUSystem(
|
|
54
|
+
name=system_data["name"],
|
|
55
|
+
qpus=[QPU(**qpu) for qpu in system_data.get("qpus", [])],
|
|
56
|
+
access_level=system_data["access_level"],
|
|
57
|
+
)
|
|
58
|
+
for system_data in json_data
|
|
59
|
+
]
|
|
60
|
+
|
|
61
|
+
|
|
62
|
+
def update_qpu_systems_cache(systems: list[QPUSystem]):
|
|
63
|
+
"""Updates the cache of available QPU systems."""
|
|
64
|
+
with _CACHE_LOCK:
|
|
65
|
+
_AVAILABLE_QPU_SYSTEMS.clear()
|
|
66
|
+
for system in systems:
|
|
67
|
+
if system.name == "qoro_maestro":
|
|
68
|
+
system = replace(system, supports_expval=True)
|
|
69
|
+
_AVAILABLE_QPU_SYSTEMS[system.name] = system
|
|
70
|
+
|
|
71
|
+
|
|
72
|
+
def get_qpu_system(name: str) -> QPUSystem:
|
|
73
|
+
"""
|
|
74
|
+
Get a QPUSystem object by its name from the cache.
|
|
75
|
+
|
|
76
|
+
Args:
|
|
77
|
+
name: The name of the QPU system to retrieve.
|
|
78
|
+
|
|
79
|
+
Returns:
|
|
80
|
+
The QPUSystem object with the matching name.
|
|
81
|
+
|
|
82
|
+
Raises:
|
|
83
|
+
ValueError: If the cache is empty or the system is not found.
|
|
84
|
+
"""
|
|
85
|
+
with _CACHE_LOCK:
|
|
86
|
+
if not _AVAILABLE_QPU_SYSTEMS:
|
|
87
|
+
raise ValueError(
|
|
88
|
+
"QPU systems cache is empty. Call `QoroService.fetch_qpu_systems()` to populate it."
|
|
89
|
+
)
|
|
90
|
+
try:
|
|
91
|
+
return _AVAILABLE_QPU_SYSTEMS[name]
|
|
92
|
+
except KeyError:
|
|
93
|
+
raise ValueError(
|
|
94
|
+
f"QPUSystem with name '{name}' not found in cache."
|
|
95
|
+
) from None
|
|
96
|
+
|
|
97
|
+
|
|
98
|
+
def get_available_qpu_systems() -> list[QPUSystem]:
|
|
99
|
+
"""Returns a list of all available QPU systems from the cache."""
|
|
100
|
+
with _CACHE_LOCK:
|
|
101
|
+
return list(_AVAILABLE_QPU_SYSTEMS.values())
|
|
@@ -0,0 +1,133 @@
|
|
|
1
|
+
# SPDX-FileCopyrightText: 2025 Qoro Quantum Ltd <divi@qoroquantum.de>
|
|
2
|
+
#
|
|
3
|
+
# SPDX-License-Identifier: Apache-2.0
|
|
4
|
+
|
|
5
|
+
import base64
|
|
6
|
+
|
|
7
|
+
|
|
8
|
+
def _decode_qh1_b64(encoded: dict) -> dict[str, int]:
|
|
9
|
+
"""
|
|
10
|
+
Decode a {'encoding':'qh1','n_bits':N,'payload':base64} histogram
|
|
11
|
+
into a dict with bitstring keys -> int counts.
|
|
12
|
+
|
|
13
|
+
If `encoded` is None, returns None.
|
|
14
|
+
If `encoded` is an empty dict or has a missing/empty payload, returns `encoded` unchanged.
|
|
15
|
+
Otherwise, decodes the payload and returns a dict mapping bitstrings to counts.
|
|
16
|
+
"""
|
|
17
|
+
if not encoded or not encoded.get("payload"):
|
|
18
|
+
return encoded
|
|
19
|
+
|
|
20
|
+
if encoded.get("encoding") != "qh1":
|
|
21
|
+
raise ValueError(f"Unsupported encoding: {encoded.get('encoding')}")
|
|
22
|
+
|
|
23
|
+
blob = base64.b64decode(encoded["payload"])
|
|
24
|
+
hist_int = _decompress_histogram(blob)
|
|
25
|
+
return {str(k): v for k, v in hist_int.items()}
|
|
26
|
+
|
|
27
|
+
|
|
28
|
+
def _uleb128_decode(data: bytes, pos: int = 0) -> tuple[int, int]:
|
|
29
|
+
x = 0
|
|
30
|
+
shift = 0
|
|
31
|
+
while True:
|
|
32
|
+
if pos >= len(data):
|
|
33
|
+
raise ValueError("truncated varint")
|
|
34
|
+
b = data[pos]
|
|
35
|
+
pos += 1
|
|
36
|
+
x |= (b & 0x7F) << shift
|
|
37
|
+
if (b & 0x80) == 0:
|
|
38
|
+
break
|
|
39
|
+
shift += 7
|
|
40
|
+
return x, pos
|
|
41
|
+
|
|
42
|
+
|
|
43
|
+
def _int_to_bitstr(x: int, n_bits: int) -> str:
|
|
44
|
+
return format(x, f"0{n_bits}b")
|
|
45
|
+
|
|
46
|
+
|
|
47
|
+
def _rle_bool_decode(data: bytes, pos=0) -> tuple[list[bool], int]:
|
|
48
|
+
num_runs, pos = _uleb128_decode(data, pos)
|
|
49
|
+
if num_runs == 0:
|
|
50
|
+
return [], pos
|
|
51
|
+
first_val = data[pos] != 0
|
|
52
|
+
pos += 1
|
|
53
|
+
total, val = [], first_val
|
|
54
|
+
for _ in range(num_runs):
|
|
55
|
+
ln, pos = _uleb128_decode(data, pos)
|
|
56
|
+
total.extend([val] * ln)
|
|
57
|
+
val = not val
|
|
58
|
+
return total, pos
|
|
59
|
+
|
|
60
|
+
|
|
61
|
+
def _decompress_histogram(buf: bytes) -> dict[str, int]:
|
|
62
|
+
if not buf:
|
|
63
|
+
return {}
|
|
64
|
+
pos = 0
|
|
65
|
+
if buf[pos : pos + 3] != b"QH1":
|
|
66
|
+
raise ValueError("bad magic")
|
|
67
|
+
pos += 3
|
|
68
|
+
n_bits = buf[pos]
|
|
69
|
+
pos += 1
|
|
70
|
+
unique, pos = _uleb128_decode(buf, pos)
|
|
71
|
+
total_shots, pos = _uleb128_decode(buf, pos)
|
|
72
|
+
|
|
73
|
+
num_gaps, pos = _uleb128_decode(buf, pos)
|
|
74
|
+
gaps = []
|
|
75
|
+
for _ in range(num_gaps):
|
|
76
|
+
g, pos = _uleb128_decode(buf, pos)
|
|
77
|
+
gaps.append(g)
|
|
78
|
+
|
|
79
|
+
idxs, acc = [], 0
|
|
80
|
+
for i, g in enumerate(gaps):
|
|
81
|
+
acc = g if i == 0 else acc + g
|
|
82
|
+
idxs.append(acc)
|
|
83
|
+
|
|
84
|
+
rb_len, pos = _uleb128_decode(buf, pos)
|
|
85
|
+
is_one, _ = _rle_bool_decode(buf[pos : pos + rb_len], 0)
|
|
86
|
+
pos += rb_len
|
|
87
|
+
|
|
88
|
+
extras_len, pos = _uleb128_decode(buf, pos)
|
|
89
|
+
extras = []
|
|
90
|
+
for _ in range(extras_len):
|
|
91
|
+
e, pos = _uleb128_decode(buf, pos)
|
|
92
|
+
extras.append(e)
|
|
93
|
+
|
|
94
|
+
counts, it = [], iter(extras)
|
|
95
|
+
for flag in is_one:
|
|
96
|
+
counts.append(1 if flag else next(it) + 2)
|
|
97
|
+
|
|
98
|
+
hist = {_int_to_bitstr(i, n_bits): c for i, c in zip(idxs, counts)}
|
|
99
|
+
|
|
100
|
+
# optional integrity check
|
|
101
|
+
if sum(counts) != total_shots:
|
|
102
|
+
raise ValueError("corrupt stream: shot sum mismatch")
|
|
103
|
+
if len(counts) != unique:
|
|
104
|
+
raise ValueError("corrupt stream: unique mismatch")
|
|
105
|
+
return hist
|
|
106
|
+
|
|
107
|
+
|
|
108
|
+
def reverse_dict_endianness(
|
|
109
|
+
probs_dict: dict[str, dict[str, float]],
|
|
110
|
+
) -> dict[str, dict[str, float]]:
|
|
111
|
+
"""Reverse endianness of all bitstrings in a dictionary of probability distributions."""
|
|
112
|
+
return {
|
|
113
|
+
tag: {bitstring[::-1]: prob for bitstring, prob in probs.items()}
|
|
114
|
+
for tag, probs in probs_dict.items()
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
|
|
118
|
+
def convert_counts_to_probs(
|
|
119
|
+
counts: dict[str, dict[str, int]], shots: int
|
|
120
|
+
) -> dict[str, dict[str, float]]:
|
|
121
|
+
"""Convert raw counts to probability distributions.
|
|
122
|
+
|
|
123
|
+
Args:
|
|
124
|
+
counts (dict[str, dict[str, int]]): The counts to convert to probabilities.
|
|
125
|
+
shots (int): The number of shots.
|
|
126
|
+
|
|
127
|
+
Returns:
|
|
128
|
+
dict[str, dict[str, float]]: The probability distributions.
|
|
129
|
+
"""
|
|
130
|
+
return {
|
|
131
|
+
tag: {bitstring: count / shots for bitstring, count in probs.items()}
|
|
132
|
+
for tag, probs in counts.items()
|
|
133
|
+
}
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
# SPDX-FileCopyrightText: 2025-2026 Qoro Quantum Ltd <divi@qoroquantum.de>
|
|
2
|
+
#
|
|
3
|
+
# SPDX-License-Identifier: Apache-2.0
|
|
4
|
+
|
|
5
|
+
from ._qasm_conversion import to_openqasm
|
|
6
|
+
from ._qasm_validation import is_valid_qasm, validate_qasm, validate_qasm_count_qubits
|
|
7
|
+
from ._core import (
|
|
8
|
+
CircuitBundle,
|
|
9
|
+
ExecutableQASMCircuit,
|
|
10
|
+
MetaCircuit,
|
|
11
|
+
CircuitTag,
|
|
12
|
+
format_circuit_tag,
|
|
13
|
+
)
|
|
@@ -2,6 +2,5 @@
|
|
|
2
2
|
#
|
|
3
3
|
# SPDX-License-Identifier: Apache-2.0
|
|
4
4
|
|
|
5
|
-
# TODO: delete whole module once Cirq properly supports parameters in openqasm 3.0
|
|
6
5
|
from . import _qasm_export # Does nothing, just initiates the patch
|
|
7
|
-
from .
|
|
6
|
+
from ._parser import ExtendedQasmParser
|
|
@@ -0,0 +1,110 @@
|
|
|
1
|
+
# SPDX-FileCopyrightText: 2025 Qoro Quantum Ltd <divi@qoroquantum.de>
|
|
2
|
+
#
|
|
3
|
+
# SPDX-License-Identifier: Apache-2.0
|
|
4
|
+
|
|
5
|
+
from __future__ import annotations
|
|
6
|
+
|
|
7
|
+
import sympy
|
|
8
|
+
from cirq.contrib.qasm_import._lexer import QasmLexer
|
|
9
|
+
from cirq.contrib.qasm_import._parser import QasmParser
|
|
10
|
+
from cirq.contrib.qasm_import.exception import QasmException
|
|
11
|
+
from ply import yacc
|
|
12
|
+
|
|
13
|
+
|
|
14
|
+
class ExtendedQasmLexer(QasmLexer):
|
|
15
|
+
"""Extended lexer with INPUT and ANGLE keywords."""
|
|
16
|
+
|
|
17
|
+
reserved = {
|
|
18
|
+
**QasmLexer.reserved,
|
|
19
|
+
"input": "INPUT",
|
|
20
|
+
"angle": "ANGLE",
|
|
21
|
+
}
|
|
22
|
+
# Rebuild tokens list to include new reserved keywords
|
|
23
|
+
tokens = [
|
|
24
|
+
"FORMAT_SPEC",
|
|
25
|
+
"NUMBER",
|
|
26
|
+
"NATURAL_NUMBER",
|
|
27
|
+
"STDGATESINC",
|
|
28
|
+
"QELIBINC",
|
|
29
|
+
"ID",
|
|
30
|
+
"ARROW",
|
|
31
|
+
"EQ",
|
|
32
|
+
] + list(reserved.values())
|
|
33
|
+
|
|
34
|
+
# PLY quirk: When overriding t_ID, PLY's method discovery doesn't find inherited
|
|
35
|
+
# token methods. These three have multi-character patterns that MUST be checked
|
|
36
|
+
# before t_ID (PLY matches longest patterns first). Without explicit definitions,
|
|
37
|
+
# "OPENQASM" and "include" would be tokenized as ID instead of their proper types.
|
|
38
|
+
# We keep the regex in the docstring (required by PLY) but delegate to parent.
|
|
39
|
+
def t_FORMAT_SPEC(self, t):
|
|
40
|
+
r"""OPENQASM(\s+)([^\s\t\;]*);"""
|
|
41
|
+
return super().t_FORMAT_SPEC(t)
|
|
42
|
+
|
|
43
|
+
def t_QELIBINC(self, t):
|
|
44
|
+
r"""include(\s+)"qelib1.inc";"""
|
|
45
|
+
return super().t_QELIBINC(t)
|
|
46
|
+
|
|
47
|
+
def t_STDGATESINC(self, t):
|
|
48
|
+
r"""include(\s+)"stdgates.inc";"""
|
|
49
|
+
return super().t_STDGATESINC(t)
|
|
50
|
+
|
|
51
|
+
# Override t_ID to check both parent and extended reserved dicts
|
|
52
|
+
def t_ID(self, t):
|
|
53
|
+
r"""[^\W\d_][\w_]*"""
|
|
54
|
+
# This regex matches any Unicode letter (not digit/underscore) at the start,
|
|
55
|
+
# followed by any number of Unicode word characters or underscores.
|
|
56
|
+
# Check extended reserved first, then parent's
|
|
57
|
+
if t.value in self.reserved:
|
|
58
|
+
t.type = self.reserved[t.value]
|
|
59
|
+
elif t.value in QasmLexer.reserved:
|
|
60
|
+
t.type = QasmLexer.reserved[t.value]
|
|
61
|
+
return t
|
|
62
|
+
|
|
63
|
+
|
|
64
|
+
class ExtendedQasmParser(QasmParser):
|
|
65
|
+
"""Extended parser with QASM 3.0 parameter support.
|
|
66
|
+
|
|
67
|
+
Only adds support for: input angle[32] param_name;
|
|
68
|
+
"""
|
|
69
|
+
|
|
70
|
+
# Add new tokens
|
|
71
|
+
tokens = QasmParser.tokens + ["INPUT", "ANGLE"]
|
|
72
|
+
|
|
73
|
+
def __init__(self) -> None:
|
|
74
|
+
# Initialize parameter storage
|
|
75
|
+
self.input_params: dict[str, sympy.Symbol] = {}
|
|
76
|
+
|
|
77
|
+
# Call parent to set up everything (this will create its own lexer and parser)
|
|
78
|
+
super().__init__()
|
|
79
|
+
|
|
80
|
+
self.lexer = ExtendedQasmLexer()
|
|
81
|
+
|
|
82
|
+
self.parser = yacc.yacc(module=self, debug=False, write_tables=False)
|
|
83
|
+
|
|
84
|
+
# Add parser rule for input angle declarations
|
|
85
|
+
def p_circuit_input_angle(self, p):
|
|
86
|
+
"""circuit : input_angle circuit"""
|
|
87
|
+
p[0] = self.circuit
|
|
88
|
+
|
|
89
|
+
def p_input_angle(self, p):
|
|
90
|
+
"""input_angle : INPUT ANGLE '[' NATURAL_NUMBER ']' ID ';'"""
|
|
91
|
+
param_name = p[6]
|
|
92
|
+
self.input_params[param_name] = sympy.Symbol(param_name)
|
|
93
|
+
p[0] = None
|
|
94
|
+
|
|
95
|
+
# Override to check input_params first
|
|
96
|
+
def p_expr_identifier(self, p):
|
|
97
|
+
"""expr : ID"""
|
|
98
|
+
# Check input_params first (QASM 3.0 parameters)
|
|
99
|
+
if p[1] in self.input_params:
|
|
100
|
+
p[0] = self.input_params[p[1]]
|
|
101
|
+
return
|
|
102
|
+
|
|
103
|
+
# Fall back to parent logic (custom gate parameters)
|
|
104
|
+
if not self.in_custom_gate_scope:
|
|
105
|
+
raise QasmException(
|
|
106
|
+
f"Parameter '{p[1]}' in line {p.lineno(1)} not supported"
|
|
107
|
+
)
|
|
108
|
+
if p[1] not in self.custom_gate_scoped_params:
|
|
109
|
+
raise QasmException(f"Undefined parameter '{p[1]}' in line {p.lineno(1)}'")
|
|
110
|
+
p[0] = sympy.Symbol(p[1])
|
|
@@ -0,0 +1,78 @@
|
|
|
1
|
+
# SPDX-FileCopyrightText: 2025 Qoro Quantum Ltd <divi@qoroquantum.de>
|
|
2
|
+
#
|
|
3
|
+
# SPDX-License-Identifier: Apache-2.0
|
|
4
|
+
|
|
5
|
+
"""
|
|
6
|
+
Patch for Cirq's QasmArgs.format_field to support symbolic parameters in QASM export.
|
|
7
|
+
|
|
8
|
+
This module patches Cirq's QasmArgs.format_field method to properly handle:
|
|
9
|
+
1. Symbolic parameters (Sympy expressions) with "half_turns" spec for rotation gates
|
|
10
|
+
2. Float precision rounding for numeric parameters
|
|
11
|
+
3. Qubit ID mapping for Qid objects
|
|
12
|
+
|
|
13
|
+
Note: Measurement keys are handled directly by Cirq's qasm() function and do not
|
|
14
|
+
require special handling in format_field.
|
|
15
|
+
|
|
16
|
+
The patch is essential for QEM (Quantum Error Mitigation) protocols that modify circuits
|
|
17
|
+
with symbolic parameters. When Cirq exports QASM code using cirq.qasm(), it formats rotation
|
|
18
|
+
gate angles using the "half_turns" spec. Without this patch, Sympy expressions would not be
|
|
19
|
+
properly converted to QASM format (e.g., "theta*pi" instead of just "theta").
|
|
20
|
+
|
|
21
|
+
Example:
|
|
22
|
+
When a circuit contains a rotation gate with a Sympy symbol:
|
|
23
|
+
rz(theta * pi) q[0]
|
|
24
|
+
|
|
25
|
+
Cirq's QASM export calls format_field with the Sympy expression and spec="half_turns".
|
|
26
|
+
This patch ensures the expression is properly formatted for QASM output.
|
|
27
|
+
"""
|
|
28
|
+
|
|
29
|
+
from typing import Any
|
|
30
|
+
|
|
31
|
+
from cirq import ops
|
|
32
|
+
from cirq.protocols.qasm import QasmArgs
|
|
33
|
+
from sympy import Expr, pi
|
|
34
|
+
|
|
35
|
+
|
|
36
|
+
def patched_format_field(self, value: Any, spec: str) -> str:
|
|
37
|
+
"""Patched version of QasmArgs.format_field for symbolic parameter support.
|
|
38
|
+
|
|
39
|
+
This method extends Cirq's QasmArgs.format_field to handle:
|
|
40
|
+
- Sympy expressions with "half_turns" spec (for rotation gate angles)
|
|
41
|
+
- Float precision rounding
|
|
42
|
+
- Qubit ID mapping
|
|
43
|
+
|
|
44
|
+
Args:
|
|
45
|
+
self: QasmArgs instance
|
|
46
|
+
value: The value to format (can be float, int, Sympy Expr, or Qid)
|
|
47
|
+
spec: Format specifier (e.g., "half_turns" or empty string)
|
|
48
|
+
|
|
49
|
+
Returns:
|
|
50
|
+
Formatted string representation of the value suitable for QASM output
|
|
51
|
+
"""
|
|
52
|
+
# Handle numeric values (floats and integers)
|
|
53
|
+
if isinstance(value, (float, int)):
|
|
54
|
+
if isinstance(value, float):
|
|
55
|
+
value = round(value, self.precision)
|
|
56
|
+
if spec == "half_turns":
|
|
57
|
+
# Format as "pi*value" for QASM rotation gates
|
|
58
|
+
value = f"pi*{value}" if value != 0 else "0"
|
|
59
|
+
spec = ""
|
|
60
|
+
|
|
61
|
+
# Handle Cirq Qid objects (qubits)
|
|
62
|
+
elif isinstance(value, ops.Qid):
|
|
63
|
+
value = self.qubit_id_map[value]
|
|
64
|
+
|
|
65
|
+
# Handle Sympy expressions (symbolic parameters)
|
|
66
|
+
if isinstance(value, Expr):
|
|
67
|
+
if spec == "half_turns":
|
|
68
|
+
# Multiply by pi for rotation gates (Cirq uses half-turns internally)
|
|
69
|
+
value *= pi
|
|
70
|
+
return str(value)
|
|
71
|
+
|
|
72
|
+
# Fall back to parent implementation for other cases
|
|
73
|
+
return super(QasmArgs, self).format_field(value, spec)
|
|
74
|
+
|
|
75
|
+
|
|
76
|
+
# Apply the patch to QasmArgs.format_field
|
|
77
|
+
# This is done at module import time so all QASM exports use the patched version
|
|
78
|
+
QasmArgs.format_field = patched_format_field
|