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.
- qilisdk/__init__.py +47 -0
- qilisdk/__init__.pyi +30 -0
- qilisdk/_optionals.py +105 -0
- qilisdk/analog/__init__.py +17 -0
- qilisdk/analog/algorithms.py +111 -0
- qilisdk/analog/analog_backend.py +43 -0
- qilisdk/analog/analog_result.py +114 -0
- qilisdk/analog/exceptions.py +19 -0
- qilisdk/analog/hamiltonian.py +706 -0
- qilisdk/analog/quantum_objects.py +486 -0
- qilisdk/analog/schedule.py +311 -0
- qilisdk/common/__init__.py +20 -0
- qilisdk/common/algorithm.py +17 -0
- qilisdk/common/backend.py +16 -0
- qilisdk/common/model.py +16 -0
- qilisdk/common/optimizer.py +136 -0
- qilisdk/common/optimizer_result.py +110 -0
- qilisdk/common/result.py +17 -0
- qilisdk/digital/__init__.py +66 -0
- qilisdk/digital/ansatz.py +143 -0
- qilisdk/digital/circuit.py +106 -0
- qilisdk/digital/digital_algorithm.py +20 -0
- qilisdk/digital/digital_backend.py +90 -0
- qilisdk/digital/digital_result.py +145 -0
- qilisdk/digital/exceptions.py +31 -0
- qilisdk/digital/gates.py +989 -0
- qilisdk/digital/vqe.py +165 -0
- qilisdk/extras/__init__.py +13 -0
- qilisdk/extras/cuda/__init__.py +18 -0
- qilisdk/extras/cuda/cuda_analog_result.py +19 -0
- qilisdk/extras/cuda/cuda_backend.py +398 -0
- qilisdk/extras/cuda/cuda_digital_result.py +19 -0
- qilisdk/extras/qaas/__init__.py +13 -0
- qilisdk/extras/qaas/keyring.py +54 -0
- qilisdk/extras/qaas/models.py +57 -0
- qilisdk/extras/qaas/qaas_backend.py +154 -0
- qilisdk/extras/qaas/qaas_digital_result.py +20 -0
- qilisdk/extras/qaas/qaas_settings.py +23 -0
- qilisdk/py.typed +0 -0
- qilisdk/utils/__init__.py +27 -0
- qilisdk/utils/openqasm2.py +215 -0
- qilisdk/utils/serialization.py +128 -0
- qilisdk/yaml.py +71 -0
- qilisdk-0.1.0.dist-info/METADATA +237 -0
- qilisdk-0.1.0.dist-info/RECORD +47 -0
- qilisdk-0.1.0.dist-info/WHEEL +4 -0
- 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
|