qilisdk 0.1.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.
Files changed (47) hide show
  1. qilisdk/__init__.py +47 -0
  2. qilisdk/__init__.pyi +30 -0
  3. qilisdk/_optionals.py +105 -0
  4. qilisdk/analog/__init__.py +17 -0
  5. qilisdk/analog/algorithms.py +111 -0
  6. qilisdk/analog/analog_backend.py +43 -0
  7. qilisdk/analog/analog_result.py +114 -0
  8. qilisdk/analog/exceptions.py +19 -0
  9. qilisdk/analog/hamiltonian.py +706 -0
  10. qilisdk/analog/quantum_objects.py +486 -0
  11. qilisdk/analog/schedule.py +311 -0
  12. qilisdk/common/__init__.py +20 -0
  13. qilisdk/common/algorithm.py +17 -0
  14. qilisdk/common/backend.py +16 -0
  15. qilisdk/common/model.py +16 -0
  16. qilisdk/common/optimizer.py +136 -0
  17. qilisdk/common/optimizer_result.py +110 -0
  18. qilisdk/common/result.py +17 -0
  19. qilisdk/digital/__init__.py +66 -0
  20. qilisdk/digital/ansatz.py +143 -0
  21. qilisdk/digital/circuit.py +106 -0
  22. qilisdk/digital/digital_algorithm.py +20 -0
  23. qilisdk/digital/digital_backend.py +90 -0
  24. qilisdk/digital/digital_result.py +145 -0
  25. qilisdk/digital/exceptions.py +31 -0
  26. qilisdk/digital/gates.py +989 -0
  27. qilisdk/digital/vqe.py +165 -0
  28. qilisdk/extras/__init__.py +13 -0
  29. qilisdk/extras/cuda/__init__.py +18 -0
  30. qilisdk/extras/cuda/cuda_analog_result.py +19 -0
  31. qilisdk/extras/cuda/cuda_backend.py +398 -0
  32. qilisdk/extras/cuda/cuda_digital_result.py +19 -0
  33. qilisdk/extras/qaas/__init__.py +13 -0
  34. qilisdk/extras/qaas/keyring.py +54 -0
  35. qilisdk/extras/qaas/models.py +57 -0
  36. qilisdk/extras/qaas/qaas_backend.py +154 -0
  37. qilisdk/extras/qaas/qaas_digital_result.py +20 -0
  38. qilisdk/extras/qaas/qaas_settings.py +23 -0
  39. qilisdk/py.typed +0 -0
  40. qilisdk/utils/__init__.py +27 -0
  41. qilisdk/utils/openqasm2.py +215 -0
  42. qilisdk/utils/serialization.py +128 -0
  43. qilisdk/yaml.py +71 -0
  44. qilisdk-0.1.0.dist-info/METADATA +237 -0
  45. qilisdk-0.1.0.dist-info/RECORD +47 -0
  46. qilisdk-0.1.0.dist-info/WHEEL +4 -0
  47. qilisdk-0.1.0.dist-info/licenses/LICENCE +201 -0
@@ -0,0 +1,54 @@
1
+ # Copyright 2025 Qilimanjaro Quantum Tech
2
+ #
3
+ # Licensed under the Apache License, Version 2.0 (the "License");
4
+ # you may not use this file except in compliance with the License.
5
+ # You may obtain a copy of the License at
6
+ #
7
+ # http://www.apache.org/licenses/LICENSE-2.0
8
+ #
9
+ # Unless required by applicable law or agreed to in writing, software
10
+ # distributed under the License is distributed on an "AS IS" BASIS,
11
+ # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12
+ # See the License for the specific language governing permissions and
13
+ # limitations under the License.
14
+
15
+ import keyring
16
+ from pydantic import ValidationError
17
+
18
+ from .models import Token
19
+
20
+ KEYRING_IDENTIFIER = "QaaSKeyring"
21
+
22
+
23
+ def store_credentials(username: str, token: Token) -> None:
24
+ """
25
+ Store the username and token in the keyring.
26
+ """
27
+ keyring.set_password(KEYRING_IDENTIFIER, "username", username)
28
+ keyring.set_password(KEYRING_IDENTIFIER, "token", token.model_dump_json())
29
+
30
+
31
+ def delete_credentials() -> None:
32
+ """
33
+ Delete username and token from the keyring.
34
+ """
35
+ keyring.delete_password(KEYRING_IDENTIFIER, "username")
36
+ keyring.delete_password(KEYRING_IDENTIFIER, "token")
37
+
38
+
39
+ def load_credentials() -> tuple[str, Token] | None:
40
+ """
41
+ Attempt to load the stored username and token from the keyring.
42
+
43
+ Returns:
44
+ A tuple (username, Token) if both exist and can be parsed; otherwise, None.
45
+ """
46
+ username = keyring.get_password(KEYRING_IDENTIFIER, "username")
47
+ token_json = keyring.get_password(KEYRING_IDENTIFIER, "token")
48
+ if username is None or token_json is None:
49
+ return None
50
+ try:
51
+ token = Token.model_validate_json(token_json)
52
+ return username, token
53
+ except ValidationError:
54
+ return None
@@ -0,0 +1,57 @@
1
+ # Copyright 2025 Qilimanjaro Quantum Tech
2
+ #
3
+ # Licensed under the Apache License, Version 2.0 (the "License");
4
+ # you may not use this file except in compliance with the License.
5
+ # You may obtain a copy of the License at
6
+ #
7
+ # http://www.apache.org/licenses/LICENSE-2.0
8
+ #
9
+ # Unless required by applicable law or agreed to in writing, software
10
+ # distributed under the License is distributed on an "AS IS" BASIS,
11
+ # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12
+ # See the License for the specific language governing permissions and
13
+ # limitations under the License.
14
+
15
+ from enum import Enum
16
+
17
+ from pydantic import BaseModel, ConfigDict, Field
18
+
19
+
20
+ class QaaSModel(BaseModel):
21
+ model_config = ConfigDict(validate_by_name=True, validate_by_alias=True)
22
+
23
+
24
+ class LoginPayload(BaseModel): ...
25
+
26
+
27
+ class Token(QaaSModel):
28
+ """
29
+ Represents the structure of the login response:
30
+ {
31
+ "accessToken": "...",
32
+ "expiresIn": 123456789,
33
+ "issuedAt": "123456789",
34
+ "refreshToken": "...",
35
+ "tokenType": "bearer"
36
+ }
37
+ """
38
+
39
+ access_token: str = Field(alias="accessToken")
40
+ expires_in: int = Field(alias="expiresIn")
41
+ issued_at: str = Field(alias="issuedAt")
42
+ refresh_token: str = Field(alias="refreshToken")
43
+ token_type: str = Field(alias="tokenType")
44
+
45
+
46
+ class DeviceStatus(str, Enum):
47
+ """Device status typing for posting"""
48
+
49
+ ONLINE = "online"
50
+ MAINTENANCE = "maintenance"
51
+ OFFLINE = "offline"
52
+
53
+
54
+ class Device(QaaSModel):
55
+ id: int = Field(...)
56
+ name: str = Field(...)
57
+ status: DeviceStatus = Field(...)
@@ -0,0 +1,154 @@
1
+ # Copyright 2025 Qilimanjaro Quantum Tech
2
+ #
3
+ # Licensed under the Apache License, Version 2.0 (the "License");
4
+ # you may not use this file except in compliance with the License.
5
+ # You may obtain a copy of the License at
6
+ #
7
+ # http://www.apache.org/licenses/LICENSE-2.0
8
+ #
9
+ # Unless required by applicable law or agreed to in writing, software
10
+ # distributed under the License is distributed on an "AS IS" BASIS,
11
+ # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12
+ # See the License for the specific language governing permissions and
13
+ # limitations under the License.
14
+ from __future__ import annotations
15
+
16
+ import json
17
+ import logging
18
+ from base64 import urlsafe_b64encode
19
+ from datetime import datetime, timezone
20
+ from typing import TYPE_CHECKING
21
+
22
+ import httpx
23
+ from pydantic import ValidationError
24
+
25
+ from qilisdk.analog.analog_backend import AnalogBackend
26
+ from qilisdk.digital.digital_backend import DigitalBackend
27
+
28
+ from .keyring import delete_credentials, load_credentials, store_credentials
29
+ from .models import Device, Token
30
+ from .qaas_settings import QaaSSettings
31
+
32
+ if TYPE_CHECKING:
33
+ from qilisdk.analog.analog_result import AnalogResult
34
+ from qilisdk.analog.hamiltonian import Hamiltonian, PauliOperator
35
+ from qilisdk.analog.quantum_objects import QuantumObject
36
+ from qilisdk.analog.schedule import Schedule
37
+ from qilisdk.common.algorithm import Algorithm
38
+ from qilisdk.digital.circuit import Circuit
39
+
40
+ from .qaas_digital_result import QaaSDigitalResult
41
+
42
+ logging.basicConfig(
43
+ format="%(levelname)s [%(asctime)s] %(name)s - %(message)s", datefmt="%Y-%m-%d %H:%M:%S", level=logging.DEBUG
44
+ )
45
+
46
+
47
+ class QaaSBackend(DigitalBackend, AnalogBackend):
48
+ """
49
+ Manages communication with a hypothetical QaaS service via synchronous HTTP calls.
50
+
51
+ Credentials to log in can come from:
52
+ a) method parameters,
53
+ b) environment (via Pydantic),
54
+ c) keyring (fallback).
55
+ """
56
+
57
+ _api_url: str = "https://qilimanjaroqaas.ddns.net:8080/api/v1"
58
+ _authorization_request_url: str = "https://qilimanjaroqaas.ddns.net:8080/api/v1/authorisation-tokens"
59
+ _authorization_refresh_url: str = "https://qilimanjaroqaas.ddns.net:8080/api/v1/authorisation-tokens/refresh"
60
+
61
+ def __init__(self) -> None:
62
+ """
63
+ Normally, you won't call __init__() directly.
64
+ Instead, use QaaSBackend.login(...) to create a logged-in instance.
65
+ """ # noqa: DOC501
66
+ credentials = load_credentials()
67
+ if credentials is None:
68
+ raise RuntimeError(
69
+ "No valid QaaS credentials found in keyring."
70
+ "Please call QaaSBackend.login(username, apikey) or ensure environment variables are set."
71
+ )
72
+ self._username, self._token = credentials
73
+
74
+ @classmethod
75
+ def login(
76
+ cls,
77
+ username: str | None = None,
78
+ apikey: str | None = None,
79
+ ) -> bool:
80
+ # Use provided parameters or fall back to environment variables via Settings()
81
+ if not username or not apikey:
82
+ try:
83
+ # Load environment variables into the settings object.
84
+ settings = QaaSSettings() # type: ignore[call-arg]
85
+ username = username or settings.username
86
+ apikey = apikey or settings.apikey
87
+ except ValidationError:
88
+ # Environment credentials could not be validated.
89
+ # Optionally, log error details here.
90
+ return False
91
+
92
+ if not username or not apikey:
93
+ # Insufficient credentials provided.
94
+ return False
95
+
96
+ # Send login request to QaaS
97
+ try:
98
+ assertion = {
99
+ "username": username,
100
+ "api_key": apikey,
101
+ "user_id": None,
102
+ "audience": QaaSBackend._api_url,
103
+ "iat": int(datetime.now(timezone.utc).timestamp()),
104
+ }
105
+ encoded_assertion = urlsafe_b64encode(json.dumps(assertion, indent=2).encode("utf-8")).decode("utf-8")
106
+ with httpx.Client(timeout=10.0) as client:
107
+ response = client.post(
108
+ QaaSBackend._authorization_request_url,
109
+ json={
110
+ "grantType": "urn:ietf:params:oauth:grant-type:jwt-bearer",
111
+ "assertion": encoded_assertion,
112
+ "scope": "user profile",
113
+ },
114
+ headers={"X-Client-Version": "0.23.2"},
115
+ )
116
+ response.raise_for_status()
117
+ # Suppose QaaS returns {"token": "..."} in JSON
118
+ token = Token(**response.json())
119
+ except httpx.RequestError:
120
+ # Log error message
121
+ return False
122
+
123
+ store_credentials(username=username, token=token)
124
+ return True
125
+
126
+ @classmethod
127
+ def logout(cls) -> None:
128
+ delete_credentials()
129
+
130
+ def list_devices(self) -> list[Device]:
131
+ with httpx.Client(timeout=20.0) as client:
132
+ response = client.get(
133
+ QaaSBackend._api_url + "/devices",
134
+ headers={"X-Client-Version": "0.23.2", "Authorization": f"Bearer {self._token.access_token}"},
135
+ )
136
+ response.raise_for_status()
137
+ response_json = response.json()
138
+ devices = [Device(**item) for item in response_json["items"]]
139
+ return devices
140
+
141
+ def execute(self, circuit: Circuit, nshots: int = 1000) -> QaaSDigitalResult:
142
+ raise NotImplementedError
143
+
144
+ def evolve(
145
+ self,
146
+ schedule: Schedule,
147
+ initial_state: QuantumObject,
148
+ observables: list[PauliOperator | Hamiltonian],
149
+ store_intermediate_results: bool = False,
150
+ ) -> AnalogResult:
151
+ raise NotImplementedError
152
+
153
+ def run(self, algorithm: Algorithm) -> None:
154
+ raise NotImplementedError
@@ -0,0 +1,20 @@
1
+ # Copyright 2025 Qilimanjaro Quantum Tech
2
+ #
3
+ # Licensed under the Apache License, Version 2.0 (the "License");
4
+ # you may not use this file except in compliance with the License.
5
+ # You may obtain a copy of the License at
6
+ #
7
+ # http://www.apache.org/licenses/LICENSE-2.0
8
+ #
9
+ # Unless required by applicable law or agreed to in writing, software
10
+ # distributed under the License is distributed on an "AS IS" BASIS,
11
+ # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12
+ # See the License for the specific language governing permissions and
13
+ # limitations under the License.
14
+
15
+ from qilisdk.digital.digital_result import DigitalResult
16
+ from qilisdk.yaml import yaml
17
+
18
+
19
+ @yaml.register_class
20
+ class QaaSDigitalResult(DigitalResult): ...
@@ -0,0 +1,23 @@
1
+ # Copyright 2025 Qilimanjaro Quantum Tech
2
+ #
3
+ # Licensed under the Apache License, Version 2.0 (the "License");
4
+ # you may not use this file except in compliance with the License.
5
+ # You may obtain a copy of the License at
6
+ #
7
+ # http://www.apache.org/licenses/LICENSE-2.0
8
+ #
9
+ # Unless required by applicable law or agreed to in writing, software
10
+ # distributed under the License is distributed on an "AS IS" BASIS,
11
+ # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12
+ # See the License for the specific language governing permissions and
13
+ # limitations under the License.
14
+
15
+ from pydantic import Field
16
+ from pydantic_settings import BaseSettings, SettingsConfigDict
17
+
18
+
19
+ class QaaSSettings(BaseSettings):
20
+ model_config = SettingsConfigDict(env_prefix="qaas_", env_file=".env", env_file_encoding="utf-8")
21
+
22
+ username: str = Field(..., description="QaaS Username")
23
+ apikey: str = Field(..., description="QaaS API Key")
qilisdk/py.typed ADDED
File without changes
@@ -0,0 +1,27 @@
1
+ # Copyright 2025 Qilimanjaro Quantum Tech
2
+ #
3
+ # Licensed under the Apache License, Version 2.0 (the "License");
4
+ # you may not use this file except in compliance with the License.
5
+ # You may obtain a copy of the License at
6
+ #
7
+ # http://www.apache.org/licenses/LICENSE-2.0
8
+ #
9
+ # Unless required by applicable law or agreed to in writing, software
10
+ # distributed under the License is distributed on an "AS IS" BASIS,
11
+ # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12
+ # See the License for the specific language governing permissions and
13
+ # limitations under the License.
14
+
15
+ from .openqasm2 import from_qasm2, from_qasm2_file, to_qasm2, to_qasm2_file
16
+ from .serialization import deserialize, deserialize_from, serialize, serialize_to
17
+
18
+ __all__ = [
19
+ "deserialize",
20
+ "deserialize_from",
21
+ "from_qasm2",
22
+ "from_qasm2_file",
23
+ "serialize",
24
+ "serialize_to",
25
+ "to_qasm2",
26
+ "to_qasm2_file",
27
+ ]
@@ -0,0 +1,215 @@
1
+ # Copyright 2025 Qilimanjaro Quantum Tech
2
+ #
3
+ # Licensed under the Apache License, Version 2.0 (the "License");
4
+ # you may not use this file except in compliance with the License.
5
+ # You may obtain a copy of the License at
6
+ #
7
+ # http://www.apache.org/licenses/LICENSE-2.0
8
+ #
9
+ # Unless required by applicable law or agreed to in writing, software
10
+ # distributed under the License is distributed on an "AS IS" BASIS,
11
+ # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12
+ # See the License for the specific language governing permissions and
13
+ # limitations under the License.
14
+ import re
15
+ from pathlib import Path
16
+
17
+ from qilisdk.digital.circuit import Circuit
18
+ from qilisdk.digital.exceptions import UnsupportedGateError
19
+ from qilisdk.digital.gates import CNOT, CZ, RX, RY, RZ, U1, U2, U3, Gate, H, M, S, T, X, Y, Z
20
+
21
+ OPENQASM2_MAP: dict[type[Gate], str] = {
22
+ X: "x",
23
+ Y: "y",
24
+ Z: "z",
25
+ H: "h",
26
+ S: "s",
27
+ T: "t",
28
+ RX: "rx",
29
+ RY: "ry",
30
+ RZ: "rz",
31
+ U1: "u1",
32
+ U2: "u2",
33
+ U3: "u3",
34
+ CNOT: "cx",
35
+ CZ: "cz",
36
+ }
37
+
38
+
39
+ def to_qasm2(circuit: Circuit) -> str:
40
+ """
41
+ Convert the circuit to an OpenQASM 2.0 formatted string.
42
+
43
+ Args:
44
+ circuit: The circuit to convert to OpenQASM 2.0.
45
+
46
+ Returns:
47
+ str: The OpenQASM 2.0 representation of the circuit.
48
+ """
49
+ qasm_lines: list[str] = []
50
+ # QASM header, standard library and quantum register.
51
+ qasm_lines.extend(("OPENQASM 2.0;", 'include "qelib1.inc";', f"qreg q[{circuit.nqubits}];"))
52
+
53
+ # If any measurement is present, declare a classical register.
54
+ if any(isinstance(gate, M) for gate in circuit.gates):
55
+ qasm_lines.append(f"creg c[{circuit.nqubits}];")
56
+
57
+ # Process each gate.
58
+ for gate in circuit.gates:
59
+ # Special conversion for measurement.
60
+ if isinstance(gate, M):
61
+ if len(gate.target_qubits) == circuit.nqubits:
62
+ qasm_lines.append("measure q -> c;")
63
+ else:
64
+ # Generate a measurement for each target qubit.
65
+ measurements = (f"measure q[{q}] -> c[{q}];" for q in gate.target_qubits)
66
+ qasm_lines.extend(measurements)
67
+ else:
68
+ # Map the internal gate name to its QASM equivalent.
69
+ qasm_name = OPENQASM2_MAP.get(type(gate), gate.name.lower())
70
+ # Format parameter string, if any.
71
+ param_str = ""
72
+ if gate.is_parameterized:
73
+ parameters = ", ".join(str(p) for p in gate.parameter_values)
74
+ param_str = f"({parameters})"
75
+ # Format qubit operands.
76
+ qubit_str = ", ".join(f"q[{q}]" for q in gate.qubits)
77
+ qasm_lines.append(f"{qasm_name}{param_str} {qubit_str};")
78
+
79
+ return "\n".join(qasm_lines)
80
+
81
+
82
+ def to_qasm2_file(circuit: Circuit, filename: str) -> None:
83
+ """
84
+ Save the QASM representation to a file.
85
+
86
+ Args:
87
+ circuit: The circuit to convert to OpenQASM 2.0.
88
+ filename (str): The path to the file where the QASM code will be saved.
89
+ """
90
+ qasm_code = to_qasm2(circuit)
91
+ Path(filename).write_text(qasm_code, encoding="utf-8")
92
+
93
+
94
+ # TODO(vyron): Add full support for OpenQASM 2.0 grammar.
95
+ def from_qasm2(qasm_str: str) -> Circuit:
96
+ """
97
+ Parse an OpenQASM 2.0 string and create a corresponding Circuit instance.
98
+
99
+ This parser supports the following instructions:
100
+ - Quantum register declaration (e.g., "qreg q[3];")
101
+ - Classical register declaration (ignored)
102
+ - Gate instructions (one-qubit and two-qubit gates)
103
+ - Measurement instructions (e.g., "measure q[0] -> c[0];")
104
+
105
+ Args:
106
+ qasm_str (str): The QASM string to parse.
107
+
108
+ Returns:
109
+ Circuit: The constructed Circuit object.
110
+ """ # noqa: DOC501
111
+ # Mapping from QASM gate names (lowercase) to internal gate names.
112
+ reverse_qasm2_map = {v: k for k, v in OPENQASM2_MAP.items()}
113
+
114
+ circuit = None
115
+ lines = qasm_str.splitlines()
116
+ for raw_line in lines:
117
+ line = raw_line.strip()
118
+ if not line or line.startswith("//"):
119
+ continue
120
+ # Skip header and include lines.
121
+ if line.startswith(("OPENQASM", "include")):
122
+ continue
123
+ # Parse quantum register declaration.
124
+ if line.startswith("qreg"):
125
+ # e.g., "qreg q[3];"
126
+ m = re.match(r"qreg\s+\w+\[(\d+)\];", line)
127
+ if m:
128
+ nqubits = int(m.group(1))
129
+ circuit = Circuit(nqubits)
130
+ continue
131
+ # Skip classical register declaration.
132
+ if line.startswith("creg"):
133
+ continue
134
+ # Process measurement instructions.
135
+ if line.startswith("measure"):
136
+ # e.g., "measure q[0] -> c[0];"
137
+ m = re.match(r"measure\s+q\[(\d+)\]\s*->\s*c\[\d+\];", line)
138
+ if m:
139
+ # TODO(vyron): Check consecutive lines of measurement and combine into single M.
140
+ q_index = int(m.group(1))
141
+ if circuit is None:
142
+ raise ValueError("Quantum register must be declared before measurement.")
143
+ circuit.add(M(q_index))
144
+ else:
145
+ # Special case: "measure q -> c;" means measure all qubits
146
+ m_all = re.match(r"measure\s+q\s*->\s*c\s*;", line)
147
+ if m_all:
148
+ if circuit is None:
149
+ raise ValueError("Quantum register must be declared before measurement.")
150
+ circuit.add(M(*list(range(circuit.nqubits))))
151
+ continue
152
+ # Process gate instructions.
153
+ # Pattern breakdown:
154
+ # Group 1: gate name (e.g., "h", "rx", "cx")
155
+ # Group 2: optional parameters (inside parentheses)
156
+ # Group 3: operand list (e.g., "q[0]" or "q[0], q[1]")
157
+ m = re.match(r"^(\w+)(?:\(([^)]*)\))?\s+(.+);$", line)
158
+ if m:
159
+ qasm_gate_name = m.group(1)
160
+ params_str = m.group(2)
161
+ operands_str = m.group(3)
162
+
163
+ # Convert QASM gate name to internal gate name.
164
+ gate_class = reverse_qasm2_map.get(qasm_gate_name.lower())
165
+ if gate_class is None:
166
+ raise UnsupportedGateError(f"Unknown gate: {qasm_gate_name}")
167
+
168
+ # Extract qubit indices.
169
+ qubit_matches = re.findall(r"q\[(\d+)\]", operands_str)
170
+ qubits = [int(q) for q in qubit_matches]
171
+
172
+ # Parse parameters, if any.
173
+ parameters = []
174
+ if params_str:
175
+ parameters = [float(p.strip()) for p in params_str.split(",") if p.strip()]
176
+
177
+ # Instantiate the gate based on the number of qubits.
178
+ # For one-qubit gates.
179
+ if len(qubits) == 1:
180
+ if gate_class.PARAMETER_NAMES:
181
+ # Build a dictionary of parameter names to values.
182
+ param_dict = {name: parameters[i] for i, name in enumerate(gate_class.PARAMETER_NAMES)}
183
+ gate_instance = gate_class(qubits[0], **param_dict) # type: ignore[call-arg]
184
+ else:
185
+ gate_instance = gate_class(qubits[0]) # type: ignore[call-arg]
186
+ # For two-qubit gates.
187
+ elif len(qubits) == 2: # noqa: PLR2004
188
+ if gate_class.PARAMETER_NAMES:
189
+ param_dict = {name: parameters[i] for i, name in enumerate(gate_class.PARAMETER_NAMES)}
190
+ gate_instance = gate_class(qubits[0], qubits[1], **param_dict) # type: ignore[call-arg]
191
+ else:
192
+ gate_instance = gate_class(qubits[0], qubits[1]) # type: ignore[call-arg]
193
+ else:
194
+ raise UnsupportedGateError("Only one- and two-qubit gates are supported.")
195
+
196
+ if circuit is None:
197
+ raise ValueError("Quantum register must be declared before adding gates.")
198
+ circuit.add(gate_instance)
199
+ if circuit is None:
200
+ raise ValueError("No quantum register declaration found in QASM.")
201
+ return circuit
202
+
203
+
204
+ def from_qasm2_file(filename: str) -> Circuit:
205
+ """
206
+ Read an OpenQASM 2.0 file and create a corresponding Circuit instance.
207
+
208
+ Args:
209
+ filename (str): The path to the QASM file.
210
+
211
+ Returns:
212
+ Circuit: The reconstructed Circuit object.
213
+ """
214
+ qasm_str = Path(filename).read_text(encoding="utf-8")
215
+ return from_qasm2(qasm_str)
@@ -0,0 +1,128 @@
1
+ # Copyright 2023 Qilimanjaro Quantum Tech
2
+ #
3
+ # Licensed under the Apache License, Version 2.0 (the "License");
4
+ # you may not use this file except in compliance with the License.
5
+ # You may obtain a copy of the License at
6
+ #
7
+ # http://www.apache.org/licenses/LICENSE-2.0
8
+ #
9
+ # Unless required by applicable law or agreed to in writing, software
10
+ # distributed under the License is distributed on an "AS IS" BASIS,
11
+ # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12
+ # See the License for the specific language governing permissions and
13
+ # limitations under the License.
14
+
15
+ # ruff: noqa: ANN401
16
+
17
+ from io import StringIO
18
+ from pathlib import Path
19
+ from typing import Any, TypeVar, overload
20
+
21
+ from qilisdk.yaml import yaml
22
+
23
+ T = TypeVar("T")
24
+
25
+
26
+ class SerializationError(Exception):
27
+ """Custom exception for serialization errors."""
28
+
29
+
30
+ class DeserializationError(Exception):
31
+ """Custom exception for deserialization errors."""
32
+
33
+
34
+ def serialize(obj: Any) -> str:
35
+ """Serialize an object to a YAML string.
36
+
37
+ Args:
38
+ obj (Any): The object to serialize.
39
+
40
+ Raises:
41
+ SerializationError: If serialization fails.
42
+
43
+ Returns:
44
+ str: The serialized YAML string.
45
+ """
46
+ try:
47
+ with StringIO() as stream:
48
+ yaml.dump(obj, stream)
49
+ return stream.getvalue()
50
+ except Exception as e:
51
+ raise SerializationError(f"Failed to serialize object {e}") from e
52
+
53
+
54
+ def serialize_to(obj: Any, file: str) -> None:
55
+ """Serialize an object to a YAML file.
56
+
57
+ Args:
58
+ obj (Any): The object to serialize.
59
+ file (str): The file path where the YAML data will be written.
60
+
61
+ Raises:
62
+ SerializationError: If serialization to file fails.
63
+ """
64
+ try:
65
+ yaml.dump(obj, Path(file))
66
+ except Exception as e:
67
+ raise SerializationError(f"Failed to serialize object {e} to file {file}") from e
68
+
69
+
70
+ @overload
71
+ def deserialize(string: str) -> Any: ...
72
+
73
+
74
+ @overload
75
+ def deserialize(string: str, cls: type[T]) -> T: ...
76
+
77
+
78
+ def deserialize(string: str, cls: type[T] | None = None) -> Any | T:
79
+ """Deserialize a YAML string to an object.
80
+
81
+ Args:
82
+ string (str): The YAML string to deserialize.
83
+ cls (type[T], optional): The class type to cast the deserialized object to. Defaults to None.
84
+
85
+ Raises:
86
+ DeserializationError: If deserialization fails or the resulting object is not of the specified type.
87
+
88
+ Returns:
89
+ Any | T: The deserialized object, optionally cast to the specified class type.
90
+ """
91
+ try:
92
+ with StringIO(string) as stream:
93
+ result = yaml.load(stream)
94
+ except Exception as e:
95
+ raise DeserializationError(f"Failed to deserialize YAML string: {e}") from e
96
+ if cls is not None and not isinstance(result, cls):
97
+ raise DeserializationError(f"Deserialized object is not of type {cls.__name__}")
98
+ return result
99
+
100
+
101
+ @overload
102
+ def deserialize_from(file: str) -> Any: ...
103
+
104
+
105
+ @overload
106
+ def deserialize_from(file: str, cls: type[T]) -> T: ...
107
+
108
+
109
+ def deserialize_from(file: str, cls: type[T] | None = None) -> Any | T:
110
+ """Deserialize a YAML file to an object.
111
+
112
+ Args:
113
+ file (str): The file path of the YAML file to deserialize.
114
+ cls (type[T], optional): The class type to cast the deserialized object to. Defaults to None.
115
+
116
+ Raises:
117
+ DeserializationError: If deserialization fails or the resulting object is not of the specified type.
118
+
119
+ Returns:
120
+ Any | T: The deserialized object, optionally cast to the specified class type.
121
+ """
122
+ try:
123
+ result = yaml.load(Path(file))
124
+ except Exception as e:
125
+ raise DeserializationError(f"Failed to deserialize YAML string {e} from file {file}") from e
126
+ if cls is not None and not isinstance(result, cls):
127
+ raise DeserializationError(f"Deserialized object is not of type {cls.__name__}")
128
+ return result