qubecli 1.0.2__tar.gz → 1.0.4__tar.gz
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- {qubecli-1.0.2 → qubecli-1.0.4}/PKG-INFO +66 -13
- qubecli-1.0.4/README.md +154 -0
- {qubecli-1.0.2 → qubecli-1.0.4}/pyproject.toml +2 -2
- qubecli-1.0.4/src/qubecli/main.py +268 -0
- qubecli-1.0.2/README.md +0 -101
- qubecli-1.0.2/src/qubecli/main.py +0 -188
- {qubecli-1.0.2 → qubecli-1.0.4}/.gitignore +0 -0
- {qubecli-1.0.2 → qubecli-1.0.4}/LICENSE +0 -0
- {qubecli-1.0.2 → qubecli-1.0.4}/src/qubecli/__init__.py +0 -0
- {qubecli-1.0.2 → qubecli-1.0.4}/uv.lock +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: qubecli
|
|
3
|
-
Version: 1.0.
|
|
3
|
+
Version: 1.0.4
|
|
4
4
|
Summary: QubeCore CLI — Command Line Interface for QubeCore
|
|
5
5
|
License: MIT License
|
|
6
6
|
|
|
@@ -25,7 +25,7 @@ License: MIT License
|
|
|
25
25
|
SOFTWARE.
|
|
26
26
|
License-File: LICENSE
|
|
27
27
|
Requires-Python: >=3.11
|
|
28
|
-
Requires-Dist: qubecore-client>=1.0.
|
|
28
|
+
Requires-Dist: qubecore-client>=1.0.12
|
|
29
29
|
Requires-Dist: typer>=0.12.0
|
|
30
30
|
Provides-Extra: dev
|
|
31
31
|
Requires-Dist: mypy>=1.8.0; extra == 'dev'
|
|
@@ -58,8 +58,25 @@ pip install qubecli
|
|
|
58
58
|
# Set server address
|
|
59
59
|
qubecore config set host 192.168.1.100:50051
|
|
60
60
|
|
|
61
|
-
# Submit a job
|
|
62
|
-
qubecore submit --backend backend1
|
|
61
|
+
# Submit a gate job
|
|
62
|
+
qubecore submit --backend backend1 \
|
|
63
|
+
--circuit 'OPENQASM 2.0;\ninclude "qelib1.inc";\nqreg q[2];\ncreg c[2];\nh q[0];\ncx q[0],q[1];\nmeasure q -> c;' \
|
|
64
|
+
--shots 100
|
|
65
|
+
|
|
66
|
+
# Submit a pulse job
|
|
67
|
+
qubecore submit-pulse --backend backend1 \
|
|
68
|
+
--pulse '{"channel": "ch0", "waveform": [...]}' \
|
|
69
|
+
--shots 100
|
|
70
|
+
|
|
71
|
+
# Submit a reset job
|
|
72
|
+
qubecore submit-reset --backend backend1 \
|
|
73
|
+
--qubits qubit_0,qubit_1 \
|
|
74
|
+
--shots 100
|
|
75
|
+
|
|
76
|
+
# Submit a calibration job
|
|
77
|
+
qubecore submit-calibration --backend backend1 \
|
|
78
|
+
--calibration-type widescan \
|
|
79
|
+
--params '{"qubit": "qubit_0", "span": 100000000, "n_points": 200, "num_shots": 100}'
|
|
63
80
|
|
|
64
81
|
# Check status
|
|
65
82
|
qubecore status --job-id <job_id>
|
|
@@ -67,7 +84,7 @@ qubecore status --job-id <job_id>
|
|
|
67
84
|
# Fetch result
|
|
68
85
|
qubecore result --job-id <job_id>
|
|
69
86
|
|
|
70
|
-
# Cancel job
|
|
87
|
+
# Cancel job (only possible while status is PENDING)
|
|
71
88
|
qubecore cancel --job-id <job_id>
|
|
72
89
|
```
|
|
73
90
|
|
|
@@ -92,17 +109,26 @@ qubecore config unset host
|
|
|
92
109
|
### Job Commands
|
|
93
110
|
|
|
94
111
|
```bash
|
|
95
|
-
#
|
|
112
|
+
# Gate job
|
|
96
113
|
qubecore submit --backend <backend> --circuit <circuit_string> --shots <n>
|
|
97
114
|
qubecore submit --backend backend1 --circuit '...' --shots 100 --priority 2 --optimization-level 1
|
|
98
115
|
|
|
116
|
+
# Pulse job
|
|
117
|
+
qubecore submit-pulse --backend <backend> --pulse <pulse_json> --shots <n>
|
|
118
|
+
|
|
119
|
+
# Reset job
|
|
120
|
+
qubecore submit-reset --backend <backend> --qubits qubit_0,qubit_1 --shots <n>
|
|
121
|
+
|
|
122
|
+
# Calibration job
|
|
123
|
+
qubecore submit-calibration --backend <backend> --calibration-type widescan --params '{"qubit": "qubit_0", "span": 100000000, "n_points": 200, "num_shots": 100}'
|
|
124
|
+
|
|
99
125
|
# Status
|
|
100
126
|
qubecore status --job-id <job_id>
|
|
101
127
|
|
|
102
128
|
# Result
|
|
103
129
|
qubecore result --job-id <job_id>
|
|
104
130
|
|
|
105
|
-
# Cancel
|
|
131
|
+
# Cancel
|
|
106
132
|
qubecore cancel --job-id <job_id>
|
|
107
133
|
```
|
|
108
134
|
|
|
@@ -117,12 +143,39 @@ qubecore --host 10.0.0.1:50051 submit --backend backend1 --circuit "..." --shots
|
|
|
117
143
|
### `submit`
|
|
118
144
|
|
|
119
145
|
| Option | Required | Description |
|
|
120
|
-
|
|
121
|
-
| `--backend` | Yes |
|
|
122
|
-
| `--circuit` | Yes |
|
|
123
|
-
| `--shots` | Yes |
|
|
124
|
-
| `--priority` | No |
|
|
125
|
-
| `--optimization-level` | No |
|
|
146
|
+
|--------|:--------:|-------------|
|
|
147
|
+
| `--backend` | Yes | Backend name (`backend1`, `kreo.sc-20`, etc.) |
|
|
148
|
+
| `--circuit` | Yes | Circuit string (QASM2/QASM3/JSON) |
|
|
149
|
+
| `--shots` | Yes | Number of shots |
|
|
150
|
+
| `--priority` | No | Priority 1–4 (1=CRITICAL, 2=HIGH, 3=NORMAL, 4=LOW) |
|
|
151
|
+
| `--optimization-level` | No | Transpile optimization level 0–3 |
|
|
152
|
+
|
|
153
|
+
### `submit-pulse`
|
|
154
|
+
|
|
155
|
+
| Option | Required | Description |
|
|
156
|
+
|--------|:--------:|-------------|
|
|
157
|
+
| `--backend` | Yes | Backend name |
|
|
158
|
+
| `--pulse` | Yes | Pulse program string (backend-specific JSON) |
|
|
159
|
+
| `--shots` | Yes | Number of shots |
|
|
160
|
+
| `--priority` | No | Priority 1–4 (default: 3=NORMAL) |
|
|
161
|
+
|
|
162
|
+
### `submit-reset`
|
|
163
|
+
|
|
164
|
+
| Option | Required | Description |
|
|
165
|
+
|--------|:--------:|-------------|
|
|
166
|
+
| `--backend` | Yes | Backend name |
|
|
167
|
+
| `--qubits` | Yes | Comma-separated qubit names (e.g. `qubit_0,qubit_1`) |
|
|
168
|
+
| `--shots` | Yes | Number of verification shots after reset |
|
|
169
|
+
| `--priority` | No | Priority 1–4 (default: 3=NORMAL) |
|
|
170
|
+
|
|
171
|
+
### `submit-calibration`
|
|
172
|
+
|
|
173
|
+
| Option | Required | Description |
|
|
174
|
+
|--------|:--------:|-------------|
|
|
175
|
+
| `--backend` | Yes | Backend name |
|
|
176
|
+
| `--calibration-type` | Yes | Calibration type: `widescan` or `punchout` |
|
|
177
|
+
| `--params` | Yes | Calibration parameters as JSON string |
|
|
178
|
+
| `--priority` | No | Priority 1–4 (default: 3=NORMAL) |
|
|
126
179
|
|
|
127
180
|
## Compatibility
|
|
128
181
|
|
qubecli-1.0.4/README.md
ADDED
|
@@ -0,0 +1,154 @@
|
|
|
1
|
+
# QubeCli
|
|
2
|
+
|
|
3
|
+
Command Line Interface for QubeCore — Quantum Computing Operating System.
|
|
4
|
+
|
|
5
|
+
## Overview
|
|
6
|
+
|
|
7
|
+
QubeCli provides a terminal-based interface for submitting and managing
|
|
8
|
+
quantum jobs on QubeCore. Built on [qubecore-client](https://github.com/sdt-quantum/qubecore-client).
|
|
9
|
+
|
|
10
|
+
## Requirements
|
|
11
|
+
|
|
12
|
+
- Python 3.11+
|
|
13
|
+
- QubeCore Server running and accessible
|
|
14
|
+
|
|
15
|
+
## Installation
|
|
16
|
+
|
|
17
|
+
```bash
|
|
18
|
+
pip install qubecli
|
|
19
|
+
```
|
|
20
|
+
|
|
21
|
+
## Quick Start
|
|
22
|
+
|
|
23
|
+
```bash
|
|
24
|
+
# Set server address
|
|
25
|
+
qubecore config set host 192.168.1.100:50051
|
|
26
|
+
|
|
27
|
+
# Submit a gate job
|
|
28
|
+
qubecore submit --backend backend1 \
|
|
29
|
+
--circuit 'OPENQASM 2.0;\ninclude "qelib1.inc";\nqreg q[2];\ncreg c[2];\nh q[0];\ncx q[0],q[1];\nmeasure q -> c;' \
|
|
30
|
+
--shots 100
|
|
31
|
+
|
|
32
|
+
# Submit a pulse job
|
|
33
|
+
qubecore submit-pulse --backend backend1 \
|
|
34
|
+
--pulse '{"channel": "ch0", "waveform": [...]}' \
|
|
35
|
+
--shots 100
|
|
36
|
+
|
|
37
|
+
# Submit a reset job
|
|
38
|
+
qubecore submit-reset --backend backend1 \
|
|
39
|
+
--qubits qubit_0,qubit_1 \
|
|
40
|
+
--shots 100
|
|
41
|
+
|
|
42
|
+
# Submit a calibration job
|
|
43
|
+
qubecore submit-calibration --backend backend1 \
|
|
44
|
+
--calibration-type widescan \
|
|
45
|
+
--params '{"qubit": "qubit_0", "span": 100000000, "n_points": 200, "num_shots": 100}'
|
|
46
|
+
|
|
47
|
+
# Check status
|
|
48
|
+
qubecore status --job-id <job_id>
|
|
49
|
+
|
|
50
|
+
# Fetch result
|
|
51
|
+
qubecore result --job-id <job_id>
|
|
52
|
+
|
|
53
|
+
# Cancel job (only possible while status is PENDING)
|
|
54
|
+
qubecore cancel --job-id <job_id>
|
|
55
|
+
```
|
|
56
|
+
|
|
57
|
+
## Commands
|
|
58
|
+
|
|
59
|
+
### Version
|
|
60
|
+
|
|
61
|
+
```bash
|
|
62
|
+
qubecore -v
|
|
63
|
+
qubecore --version
|
|
64
|
+
```
|
|
65
|
+
|
|
66
|
+
### Server Configuration
|
|
67
|
+
|
|
68
|
+
```bash
|
|
69
|
+
qubecore config set host 192.168.1.100:50051
|
|
70
|
+
qubecore config get
|
|
71
|
+
qubecore config get host
|
|
72
|
+
qubecore config unset host
|
|
73
|
+
```
|
|
74
|
+
|
|
75
|
+
### Job Commands
|
|
76
|
+
|
|
77
|
+
```bash
|
|
78
|
+
# Gate job
|
|
79
|
+
qubecore submit --backend <backend> --circuit <circuit_string> --shots <n>
|
|
80
|
+
qubecore submit --backend backend1 --circuit '...' --shots 100 --priority 2 --optimization-level 1
|
|
81
|
+
|
|
82
|
+
# Pulse job
|
|
83
|
+
qubecore submit-pulse --backend <backend> --pulse <pulse_json> --shots <n>
|
|
84
|
+
|
|
85
|
+
# Reset job
|
|
86
|
+
qubecore submit-reset --backend <backend> --qubits qubit_0,qubit_1 --shots <n>
|
|
87
|
+
|
|
88
|
+
# Calibration job
|
|
89
|
+
qubecore submit-calibration --backend <backend> --calibration-type widescan --params '{"qubit": "qubit_0", "span": 100000000, "n_points": 200, "num_shots": 100}'
|
|
90
|
+
|
|
91
|
+
# Status
|
|
92
|
+
qubecore status --job-id <job_id>
|
|
93
|
+
|
|
94
|
+
# Result
|
|
95
|
+
qubecore result --job-id <job_id>
|
|
96
|
+
|
|
97
|
+
# Cancel
|
|
98
|
+
qubecore cancel --job-id <job_id>
|
|
99
|
+
```
|
|
100
|
+
|
|
101
|
+
One-time server override:
|
|
102
|
+
|
|
103
|
+
```bash
|
|
104
|
+
qubecore --host 10.0.0.1:50051 submit --backend backend1 --circuit "..." --shots 100
|
|
105
|
+
```
|
|
106
|
+
|
|
107
|
+
## Options
|
|
108
|
+
|
|
109
|
+
### `submit`
|
|
110
|
+
|
|
111
|
+
| Option | Required | Description |
|
|
112
|
+
|--------|:--------:|-------------|
|
|
113
|
+
| `--backend` | Yes | Backend name (`backend1`, `kreo.sc-20`, etc.) |
|
|
114
|
+
| `--circuit` | Yes | Circuit string (QASM2/QASM3/JSON) |
|
|
115
|
+
| `--shots` | Yes | Number of shots |
|
|
116
|
+
| `--priority` | No | Priority 1–4 (1=CRITICAL, 2=HIGH, 3=NORMAL, 4=LOW) |
|
|
117
|
+
| `--optimization-level` | No | Transpile optimization level 0–3 |
|
|
118
|
+
|
|
119
|
+
### `submit-pulse`
|
|
120
|
+
|
|
121
|
+
| Option | Required | Description |
|
|
122
|
+
|--------|:--------:|-------------|
|
|
123
|
+
| `--backend` | Yes | Backend name |
|
|
124
|
+
| `--pulse` | Yes | Pulse program string (backend-specific JSON) |
|
|
125
|
+
| `--shots` | Yes | Number of shots |
|
|
126
|
+
| `--priority` | No | Priority 1–4 (default: 3=NORMAL) |
|
|
127
|
+
|
|
128
|
+
### `submit-reset`
|
|
129
|
+
|
|
130
|
+
| Option | Required | Description |
|
|
131
|
+
|--------|:--------:|-------------|
|
|
132
|
+
| `--backend` | Yes | Backend name |
|
|
133
|
+
| `--qubits` | Yes | Comma-separated qubit names (e.g. `qubit_0,qubit_1`) |
|
|
134
|
+
| `--shots` | Yes | Number of verification shots after reset |
|
|
135
|
+
| `--priority` | No | Priority 1–4 (default: 3=NORMAL) |
|
|
136
|
+
|
|
137
|
+
### `submit-calibration`
|
|
138
|
+
|
|
139
|
+
| Option | Required | Description |
|
|
140
|
+
|--------|:--------:|-------------|
|
|
141
|
+
| `--backend` | Yes | Backend name |
|
|
142
|
+
| `--calibration-type` | Yes | Calibration type: `widescan` or `punchout` |
|
|
143
|
+
| `--params` | Yes | Calibration parameters as JSON string |
|
|
144
|
+
| `--priority` | No | Priority 1–4 (default: 3=NORMAL) |
|
|
145
|
+
|
|
146
|
+
## Compatibility
|
|
147
|
+
|
|
148
|
+
| qubecli | qubecore-client | qubecore |
|
|
149
|
+
|---------|----------------|----------|
|
|
150
|
+
| 1.x | 1.x | 1.x |
|
|
151
|
+
|
|
152
|
+
## License
|
|
153
|
+
|
|
154
|
+
MIT
|
|
@@ -4,14 +4,14 @@ build-backend = "hatchling.build"
|
|
|
4
4
|
|
|
5
5
|
[project]
|
|
6
6
|
name = "qubecli"
|
|
7
|
-
version = "1.0.
|
|
7
|
+
version = "1.0.4"
|
|
8
8
|
description = "QubeCore CLI — Command Line Interface for QubeCore"
|
|
9
9
|
readme = "README.md"
|
|
10
10
|
license = { file = "LICENSE" }
|
|
11
11
|
requires-python = ">=3.11"
|
|
12
12
|
dependencies = [
|
|
13
13
|
"typer>=0.12.0",
|
|
14
|
-
"qubecore-client>=1.0.
|
|
14
|
+
"qubecore-client>=1.0.12",
|
|
15
15
|
]
|
|
16
16
|
|
|
17
17
|
[project.optional-dependencies]
|
|
@@ -0,0 +1,268 @@
|
|
|
1
|
+
import json
|
|
2
|
+
from contextlib import contextmanager
|
|
3
|
+
from importlib.metadata import version
|
|
4
|
+
from pathlib import Path
|
|
5
|
+
from typing import Annotated, Generator, Optional
|
|
6
|
+
|
|
7
|
+
import grpc
|
|
8
|
+
import typer
|
|
9
|
+
from qubecore_client import QubeClient
|
|
10
|
+
|
|
11
|
+
_CONFIG_PATH = Path.home() / ".qubecore" / "config.json"
|
|
12
|
+
|
|
13
|
+
app = typer.Typer(help="QubeCore CLI — Quantum Computing Operating System", invoke_without_command=True)
|
|
14
|
+
config_app = typer.Typer(help="Manage server configuration")
|
|
15
|
+
app.add_typer(config_app, name="config")
|
|
16
|
+
|
|
17
|
+
|
|
18
|
+
_address: str = "localhost:50051"
|
|
19
|
+
|
|
20
|
+
|
|
21
|
+
def _load_config() -> dict:
|
|
22
|
+
if _CONFIG_PATH.exists():
|
|
23
|
+
return json.loads(_CONFIG_PATH.read_text())
|
|
24
|
+
return {}
|
|
25
|
+
|
|
26
|
+
|
|
27
|
+
def _save_config(data: dict) -> None:
|
|
28
|
+
_CONFIG_PATH.parent.mkdir(parents=True, exist_ok=True)
|
|
29
|
+
_CONFIG_PATH.write_text(json.dumps(data, indent=2))
|
|
30
|
+
|
|
31
|
+
|
|
32
|
+
def _resolve_address(host: Optional[str]) -> str:
|
|
33
|
+
if host is not None:
|
|
34
|
+
return host
|
|
35
|
+
return _load_config().get("host", "localhost:50051")
|
|
36
|
+
|
|
37
|
+
|
|
38
|
+
@contextmanager
|
|
39
|
+
def _get_client(address: str) -> Generator[QubeClient, None, None]:
|
|
40
|
+
client = QubeClient(address=address)
|
|
41
|
+
try:
|
|
42
|
+
yield client
|
|
43
|
+
except grpc.RpcError as e:
|
|
44
|
+
if e.code() == grpc.StatusCode.UNAVAILABLE:
|
|
45
|
+
typer.echo(f"Cannot connect to server: {address}", err=True)
|
|
46
|
+
typer.echo(" Run 'qubecore config set host <host:port>' to update the server address.", err=True)
|
|
47
|
+
else:
|
|
48
|
+
typer.echo(f"gRPC error: {e.details()}", err=True)
|
|
49
|
+
raise typer.Exit(1)
|
|
50
|
+
finally:
|
|
51
|
+
client.close()
|
|
52
|
+
|
|
53
|
+
|
|
54
|
+
def _priority_label(priority: int) -> str:
|
|
55
|
+
return {1: "CRITICAL", 2: "HIGH", 3: "NORMAL", 4: "LOW"}.get(priority, str(priority))
|
|
56
|
+
|
|
57
|
+
|
|
58
|
+
@app.callback()
|
|
59
|
+
def _callback(
|
|
60
|
+
host: Annotated[Optional[str], typer.Option(help="QubeCore server address (host:port)")] = None,
|
|
61
|
+
ver: Annotated[bool, typer.Option("-v", "--version", help="Print version", is_eager=True)] = False,
|
|
62
|
+
) -> None:
|
|
63
|
+
global _address
|
|
64
|
+
if ver:
|
|
65
|
+
typer.echo(f"v{version('qubecli')}")
|
|
66
|
+
raise typer.Exit()
|
|
67
|
+
_address = _resolve_address(host)
|
|
68
|
+
|
|
69
|
+
|
|
70
|
+
@config_app.command("set")
|
|
71
|
+
def config_set(
|
|
72
|
+
key: Annotated[str, typer.Argument(help="Config key")],
|
|
73
|
+
value: Annotated[str, typer.Argument(help="Config value")],
|
|
74
|
+
) -> None:
|
|
75
|
+
"""Save a config value (e.g. qubecore config set host 192.168.1.100:50051)"""
|
|
76
|
+
if key != "host":
|
|
77
|
+
typer.echo(f"Unknown key: {key}. Available: host", err=True)
|
|
78
|
+
raise typer.Exit(1)
|
|
79
|
+
cfg = _load_config()
|
|
80
|
+
cfg[key] = value
|
|
81
|
+
_save_config(cfg)
|
|
82
|
+
typer.echo(f"✓ {key} = {value} saved ({_CONFIG_PATH})")
|
|
83
|
+
|
|
84
|
+
|
|
85
|
+
@config_app.command("get")
|
|
86
|
+
def config_get(
|
|
87
|
+
key: Annotated[Optional[str], typer.Argument(help="Config key (omit to show all)")] = None,
|
|
88
|
+
) -> None:
|
|
89
|
+
"""Show config value(s)"""
|
|
90
|
+
cfg = _load_config()
|
|
91
|
+
if not cfg:
|
|
92
|
+
typer.echo("No config saved")
|
|
93
|
+
return
|
|
94
|
+
if key:
|
|
95
|
+
typer.echo(f" {key} : {cfg.get(key, '(not set)')}")
|
|
96
|
+
else:
|
|
97
|
+
for k, v in cfg.items():
|
|
98
|
+
typer.echo(f" {k} : {v}")
|
|
99
|
+
|
|
100
|
+
|
|
101
|
+
@config_app.command("unset")
|
|
102
|
+
def config_unset(
|
|
103
|
+
key: Annotated[str, typer.Argument(help="Config key to remove")],
|
|
104
|
+
) -> None:
|
|
105
|
+
"""Remove a config value"""
|
|
106
|
+
cfg = _load_config()
|
|
107
|
+
if key not in cfg:
|
|
108
|
+
typer.echo(f" {key} : (not set)")
|
|
109
|
+
return
|
|
110
|
+
del cfg[key]
|
|
111
|
+
_save_config(cfg)
|
|
112
|
+
typer.echo(f"✓ {key} removed")
|
|
113
|
+
|
|
114
|
+
|
|
115
|
+
@app.command()
|
|
116
|
+
def submit(
|
|
117
|
+
backend: Annotated[str, typer.Option(help="Backend name (e.g. backend1, kreo.sc-20)")],
|
|
118
|
+
circuit: Annotated[str, typer.Option(help="Circuit string (QASM2/QASM3/JSON)")],
|
|
119
|
+
shots: Annotated[int, typer.Option(help="Number of shots")],
|
|
120
|
+
priority: Annotated[Optional[int], typer.Option(help="Priority 1–4 (default: 3=NORMAL)")] = None,
|
|
121
|
+
optimization_level: Annotated[Optional[int], typer.Option(help="Transpile optimization level 0–3")] = None,
|
|
122
|
+
) -> None:
|
|
123
|
+
"""Submit a gate-circuit execution job"""
|
|
124
|
+
if priority is not None and priority not in (1, 2, 3, 4):
|
|
125
|
+
typer.echo("priority must be between 1 and 4.", err=True)
|
|
126
|
+
raise typer.Exit(1)
|
|
127
|
+
if optimization_level is not None and optimization_level not in (0, 1, 2, 3):
|
|
128
|
+
typer.echo("optimization-level must be between 0 and 3.", err=True)
|
|
129
|
+
raise typer.Exit(1)
|
|
130
|
+
with _get_client(_address) as client:
|
|
131
|
+
job_id = client.submit_gate_job(
|
|
132
|
+
backend_name=backend,
|
|
133
|
+
gate_circuits=[circuit.replace("\\n", "\n").replace("\\t", "\t")],
|
|
134
|
+
shots=shots,
|
|
135
|
+
priority=priority,
|
|
136
|
+
optimization_level=optimization_level,
|
|
137
|
+
)
|
|
138
|
+
typer.echo("✓ Job submitted")
|
|
139
|
+
typer.echo(f" job_id : {job_id}")
|
|
140
|
+
typer.echo(f" priority : {_priority_label(priority if priority is not None else 3)}")
|
|
141
|
+
|
|
142
|
+
|
|
143
|
+
@app.command("submit-pulse")
|
|
144
|
+
def submit_pulse(
|
|
145
|
+
backend: Annotated[str, typer.Option(help="Backend name")],
|
|
146
|
+
pulse: Annotated[str, typer.Option(help="Pulse program string (backend-specific JSON)")],
|
|
147
|
+
shots: Annotated[int, typer.Option(help="Number of shots")],
|
|
148
|
+
priority: Annotated[Optional[int], typer.Option(help="Priority 1–4 (default: 3=NORMAL)")] = None,
|
|
149
|
+
) -> None:
|
|
150
|
+
"""Submit a pulse-program execution job"""
|
|
151
|
+
if priority is not None and priority not in (1, 2, 3, 4):
|
|
152
|
+
typer.echo("priority must be between 1 and 4.", err=True)
|
|
153
|
+
raise typer.Exit(1)
|
|
154
|
+
with _get_client(_address) as client:
|
|
155
|
+
job_id = client.submit_pulse_job(
|
|
156
|
+
backend_name=backend,
|
|
157
|
+
pulse_circuits=[pulse],
|
|
158
|
+
shots=shots,
|
|
159
|
+
priority=priority,
|
|
160
|
+
)
|
|
161
|
+
typer.echo("✓ Job submitted")
|
|
162
|
+
typer.echo(f" job_id : {job_id}")
|
|
163
|
+
typer.echo(f" priority : {_priority_label(priority if priority is not None else 3)}")
|
|
164
|
+
|
|
165
|
+
|
|
166
|
+
@app.command("submit-reset")
|
|
167
|
+
def submit_reset(
|
|
168
|
+
backend: Annotated[str, typer.Option(help="Backend name")],
|
|
169
|
+
qubits: Annotated[str, typer.Option(help="Comma-separated qubit names (e.g. qubit_0,qubit_1)")],
|
|
170
|
+
shots: Annotated[int, typer.Option(help="Number of verification shots after reset")],
|
|
171
|
+
priority: Annotated[Optional[int], typer.Option(help="Priority 1–4 (default: 3=NORMAL)")] = None,
|
|
172
|
+
) -> None:
|
|
173
|
+
"""Submit a qubit active-reset job"""
|
|
174
|
+
if priority is not None and priority not in (1, 2, 3, 4):
|
|
175
|
+
typer.echo("priority must be between 1 and 4.", err=True)
|
|
176
|
+
raise typer.Exit(1)
|
|
177
|
+
qubit_list = [q.strip() for q in qubits.split(",")]
|
|
178
|
+
with _get_client(_address) as client:
|
|
179
|
+
job_id = client.submit_reset_job(
|
|
180
|
+
backend_name=backend,
|
|
181
|
+
qubits=qubit_list,
|
|
182
|
+
shots=shots,
|
|
183
|
+
priority=priority,
|
|
184
|
+
)
|
|
185
|
+
typer.echo("✓ Job submitted")
|
|
186
|
+
typer.echo(f" job_id : {job_id}")
|
|
187
|
+
typer.echo(f" qubits : {qubit_list}")
|
|
188
|
+
typer.echo(f" priority : {_priority_label(priority if priority is not None else 3)}")
|
|
189
|
+
|
|
190
|
+
|
|
191
|
+
@app.command("submit-calibration")
|
|
192
|
+
def submit_calibration(
|
|
193
|
+
backend: Annotated[str, typer.Option(help="Backend name")],
|
|
194
|
+
calibration_type: Annotated[str, typer.Option(help="Calibration type: widescan or punchout")],
|
|
195
|
+
params: Annotated[str, typer.Option(help="Calibration parameters as JSON string")],
|
|
196
|
+
priority: Annotated[Optional[int], typer.Option(help="Priority 1–4 (default: 3=NORMAL)")] = None,
|
|
197
|
+
) -> None:
|
|
198
|
+
"""Submit a calibration job"""
|
|
199
|
+
if priority is not None and priority not in (1, 2, 3, 4):
|
|
200
|
+
typer.echo("priority must be between 1 and 4.", err=True)
|
|
201
|
+
raise typer.Exit(1)
|
|
202
|
+
try:
|
|
203
|
+
params_dict = json.loads(params)
|
|
204
|
+
except json.JSONDecodeError as e:
|
|
205
|
+
typer.echo(f"Failed to parse params JSON: {e}", err=True)
|
|
206
|
+
raise typer.Exit(1)
|
|
207
|
+
with _get_client(_address) as client:
|
|
208
|
+
job_id = client.submit_calibration_job(
|
|
209
|
+
backend_name=backend,
|
|
210
|
+
calibration_type=calibration_type,
|
|
211
|
+
params=params_dict,
|
|
212
|
+
priority=priority,
|
|
213
|
+
)
|
|
214
|
+
typer.echo("✓ Job submitted")
|
|
215
|
+
typer.echo(f" job_id : {job_id}")
|
|
216
|
+
typer.echo(f" calibration_type : {calibration_type}")
|
|
217
|
+
typer.echo(f" priority : {_priority_label(priority if priority is not None else 3)}")
|
|
218
|
+
|
|
219
|
+
|
|
220
|
+
@app.command()
|
|
221
|
+
def status(
|
|
222
|
+
job_id: Annotated[str, typer.Option(help="Job ID")],
|
|
223
|
+
) -> None:
|
|
224
|
+
"""Show job status"""
|
|
225
|
+
with _get_client(_address) as client:
|
|
226
|
+
job = client.get_job_status(job_id)
|
|
227
|
+
typer.echo(f" job_id : {job['job_id']}")
|
|
228
|
+
typer.echo(f" status : {job['status']}")
|
|
229
|
+
typer.echo(f" priority : {_priority_label(job['priority'])}")
|
|
230
|
+
typer.echo(f" submitted : {job['submitted_at']}")
|
|
231
|
+
typer.echo(f" started : {job['started_at'] or '(not started)'}")
|
|
232
|
+
typer.echo(f" finished : {job['finished_at'] or '(not finished)'}")
|
|
233
|
+
if job["error"]:
|
|
234
|
+
typer.echo(f" error : {job['error']}")
|
|
235
|
+
|
|
236
|
+
|
|
237
|
+
@app.command()
|
|
238
|
+
def result(
|
|
239
|
+
job_id: Annotated[str, typer.Option(help="Job ID")],
|
|
240
|
+
) -> None:
|
|
241
|
+
"""Show job result"""
|
|
242
|
+
with _get_client(_address) as client:
|
|
243
|
+
job = client.get_job_result(job_id)
|
|
244
|
+
typer.echo(f" job_id : {job['job_id']}")
|
|
245
|
+
typer.echo(f" status : {job['status']}")
|
|
246
|
+
if "calibration_result" in job:
|
|
247
|
+
typer.echo(f" calibration_result : {json.dumps(job['calibration_result'], indent=2)}")
|
|
248
|
+
else:
|
|
249
|
+
for i, r in enumerate(job.get("results", [])):
|
|
250
|
+
typer.echo(f" [circuit {i}] counts : {r['counts']}")
|
|
251
|
+
|
|
252
|
+
|
|
253
|
+
@app.command()
|
|
254
|
+
def cancel(
|
|
255
|
+
job_id: Annotated[str, typer.Option(help="Job ID")],
|
|
256
|
+
) -> None:
|
|
257
|
+
"""Cancel a job (only possible while status is PENDING)"""
|
|
258
|
+
with _get_client(_address) as client:
|
|
259
|
+
message = client.cancel_job(job_id)
|
|
260
|
+
typer.echo(f"✓ {message}")
|
|
261
|
+
|
|
262
|
+
|
|
263
|
+
def main() -> None:
|
|
264
|
+
app()
|
|
265
|
+
|
|
266
|
+
|
|
267
|
+
if __name__ == "__main__":
|
|
268
|
+
main()
|
qubecli-1.0.2/README.md
DELETED
|
@@ -1,101 +0,0 @@
|
|
|
1
|
-
# QubeCli
|
|
2
|
-
|
|
3
|
-
Command Line Interface for QubeCore — Quantum Computing Operating System.
|
|
4
|
-
|
|
5
|
-
## Overview
|
|
6
|
-
|
|
7
|
-
QubeCli provides a terminal-based interface for submitting and managing
|
|
8
|
-
quantum jobs on QubeCore. Built on [qubecore-client](https://github.com/sdt-quantum/qubecore-client).
|
|
9
|
-
|
|
10
|
-
## Requirements
|
|
11
|
-
|
|
12
|
-
- Python 3.11+
|
|
13
|
-
- QubeCore Server running and accessible
|
|
14
|
-
|
|
15
|
-
## Installation
|
|
16
|
-
|
|
17
|
-
```bash
|
|
18
|
-
pip install qubecli
|
|
19
|
-
```
|
|
20
|
-
|
|
21
|
-
## Quick Start
|
|
22
|
-
|
|
23
|
-
```bash
|
|
24
|
-
# Set server address
|
|
25
|
-
qubecore config set host 192.168.1.100:50051
|
|
26
|
-
|
|
27
|
-
# Submit a job (use single quotes to avoid shell interpretation of \n)
|
|
28
|
-
qubecore submit --backend backend1 --circuit 'OPENQASM 2.0;\ninclude "qelib1.inc";\nqreg q[2];\ncreg c[2];\nh q[0];\ncx q[0],q[1];\nmeasure q -> c;' --shots 100
|
|
29
|
-
|
|
30
|
-
# Check status
|
|
31
|
-
qubecore status --job-id <job_id>
|
|
32
|
-
|
|
33
|
-
# Fetch result
|
|
34
|
-
qubecore result --job-id <job_id>
|
|
35
|
-
|
|
36
|
-
# Cancel job
|
|
37
|
-
qubecore cancel --job-id <job_id>
|
|
38
|
-
```
|
|
39
|
-
|
|
40
|
-
## Commands
|
|
41
|
-
|
|
42
|
-
### Version
|
|
43
|
-
|
|
44
|
-
```bash
|
|
45
|
-
qubecore -v
|
|
46
|
-
qubecore --version
|
|
47
|
-
```
|
|
48
|
-
|
|
49
|
-
### Server Configuration
|
|
50
|
-
|
|
51
|
-
```bash
|
|
52
|
-
qubecore config set host 192.168.1.100:50051
|
|
53
|
-
qubecore config get
|
|
54
|
-
qubecore config get host
|
|
55
|
-
qubecore config unset host
|
|
56
|
-
```
|
|
57
|
-
|
|
58
|
-
### Job Commands
|
|
59
|
-
|
|
60
|
-
```bash
|
|
61
|
-
# Submit
|
|
62
|
-
qubecore submit --backend <backend> --circuit <circuit_string> --shots <n>
|
|
63
|
-
qubecore submit --backend backend1 --circuit '...' --shots 100 --priority 2 --optimization-level 1
|
|
64
|
-
|
|
65
|
-
# Status
|
|
66
|
-
qubecore status --job-id <job_id>
|
|
67
|
-
|
|
68
|
-
# Result
|
|
69
|
-
qubecore result --job-id <job_id>
|
|
70
|
-
|
|
71
|
-
# Cancel (PENDING 상태일 때만 가능)
|
|
72
|
-
qubecore cancel --job-id <job_id>
|
|
73
|
-
```
|
|
74
|
-
|
|
75
|
-
One-time server override:
|
|
76
|
-
|
|
77
|
-
```bash
|
|
78
|
-
qubecore --host 10.0.0.1:50051 submit --backend backend1 --circuit "..." --shots 100
|
|
79
|
-
```
|
|
80
|
-
|
|
81
|
-
## Options
|
|
82
|
-
|
|
83
|
-
### `submit`
|
|
84
|
-
|
|
85
|
-
| Option | Required | Description |
|
|
86
|
-
|--------|----------|-------------|
|
|
87
|
-
| `--backend` | Yes | 백엔드 이름 (`backend1`, `kreo.sc-20` 등) |
|
|
88
|
-
| `--circuit` | Yes | 회로 문자열 (QASM2/QASM3/JSON) |
|
|
89
|
-
| `--shots` | Yes | 실행 횟수 |
|
|
90
|
-
| `--priority` | No | 우선순위 1~4 (1=CRITICAL, 2=HIGH, 3=NORMAL, 4=LOW) |
|
|
91
|
-
| `--optimization-level` | No | 최적화 레벨 0~3 |
|
|
92
|
-
|
|
93
|
-
## Compatibility
|
|
94
|
-
|
|
95
|
-
| qubecli | qubecore-client | qubecore |
|
|
96
|
-
|---------|----------------|----------|
|
|
97
|
-
| 1.x | 1.x | 1.x |
|
|
98
|
-
|
|
99
|
-
## License
|
|
100
|
-
|
|
101
|
-
MIT
|
|
@@ -1,188 +0,0 @@
|
|
|
1
|
-
import json
|
|
2
|
-
from contextlib import contextmanager
|
|
3
|
-
from importlib.metadata import version
|
|
4
|
-
from pathlib import Path
|
|
5
|
-
from typing import Annotated, Generator, Optional
|
|
6
|
-
|
|
7
|
-
import grpc
|
|
8
|
-
import typer
|
|
9
|
-
from qubecore_client import QubeClient
|
|
10
|
-
|
|
11
|
-
_CONFIG_PATH = Path.home() / ".qubecore" / "config.json"
|
|
12
|
-
|
|
13
|
-
app = typer.Typer(help="QubeCore CLI — Quantum Computing Operating System", invoke_without_command=True)
|
|
14
|
-
config_app = typer.Typer(help="서버 설정 관리")
|
|
15
|
-
app.add_typer(config_app, name="config")
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
_address: str = "localhost:50051"
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
def _load_config() -> dict:
|
|
22
|
-
if _CONFIG_PATH.exists():
|
|
23
|
-
return json.loads(_CONFIG_PATH.read_text())
|
|
24
|
-
return {}
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
def _save_config(data: dict) -> None:
|
|
28
|
-
_CONFIG_PATH.parent.mkdir(parents=True, exist_ok=True)
|
|
29
|
-
_CONFIG_PATH.write_text(json.dumps(data, indent=2))
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
def _resolve_address(host: Optional[str]) -> str:
|
|
33
|
-
if host is not None:
|
|
34
|
-
return host
|
|
35
|
-
return _load_config().get("host", "localhost:50051")
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
@contextmanager
|
|
39
|
-
def _get_client(address: str) -> Generator[QubeClient, None, None]:
|
|
40
|
-
client = QubeClient(address=address)
|
|
41
|
-
try:
|
|
42
|
-
yield client
|
|
43
|
-
except grpc.RpcError as e:
|
|
44
|
-
if e.code() == grpc.StatusCode.UNAVAILABLE:
|
|
45
|
-
typer.echo(f"서버에 연결할 수 없습니다: {address}", err=True)
|
|
46
|
-
typer.echo(" 'qubecore config set host <host:port>' 로 서버 주소를 확인하세요.", err=True)
|
|
47
|
-
else:
|
|
48
|
-
typer.echo(f"gRPC 오류: {e.details()}", err=True)
|
|
49
|
-
raise typer.Exit(1)
|
|
50
|
-
finally:
|
|
51
|
-
client.close()
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
def _priority_label(priority: int) -> str:
|
|
55
|
-
return {1: "CRITICAL", 2: "HIGH", 3: "NORMAL", 4: "LOW"}.get(priority, str(priority))
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
@app.callback()
|
|
59
|
-
def _callback(
|
|
60
|
-
host: Annotated[Optional[str], typer.Option(help="QubeCore 서버 주소 (host:port)")] = None,
|
|
61
|
-
ver: Annotated[bool, typer.Option("-v", "--version", help="버전 출력", is_eager=True)] = False,
|
|
62
|
-
) -> None:
|
|
63
|
-
global _address
|
|
64
|
-
if ver:
|
|
65
|
-
typer.echo(f"v{version('qubecli')}")
|
|
66
|
-
raise typer.Exit()
|
|
67
|
-
_address = _resolve_address(host)
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
@config_app.command("set")
|
|
71
|
-
def config_set(
|
|
72
|
-
key: Annotated[str, typer.Argument(help="설정 키")],
|
|
73
|
-
value: Annotated[str, typer.Argument(help="설정 값")],
|
|
74
|
-
) -> None:
|
|
75
|
-
"""설정값 저장 (예: qubecore config set host 192.168.1.100:50051)"""
|
|
76
|
-
if key != "host":
|
|
77
|
-
typer.echo(f"알 수 없는 키: {key}. 사용 가능: host", err=True)
|
|
78
|
-
raise typer.Exit(1)
|
|
79
|
-
cfg = _load_config()
|
|
80
|
-
cfg[key] = value
|
|
81
|
-
_save_config(cfg)
|
|
82
|
-
typer.echo(f"✓ {key} = {value} 저장 완료 ({_CONFIG_PATH})")
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
@config_app.command("get")
|
|
86
|
-
def config_get(
|
|
87
|
-
key: Annotated[Optional[str], typer.Argument(help="설정 키 (생략 시 전체 출력)")] = None,
|
|
88
|
-
) -> None:
|
|
89
|
-
"""설정값 조회"""
|
|
90
|
-
cfg = _load_config()
|
|
91
|
-
if not cfg:
|
|
92
|
-
typer.echo("저장된 설정 없음")
|
|
93
|
-
return
|
|
94
|
-
if key:
|
|
95
|
-
typer.echo(f" {key} : {cfg.get(key, '(없음)')}")
|
|
96
|
-
else:
|
|
97
|
-
for k, v in cfg.items():
|
|
98
|
-
typer.echo(f" {k} : {v}")
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
@config_app.command("unset")
|
|
102
|
-
def config_unset(
|
|
103
|
-
key: Annotated[str, typer.Argument(help="삭제할 설정 키")],
|
|
104
|
-
) -> None:
|
|
105
|
-
"""설정값 삭제"""
|
|
106
|
-
cfg = _load_config()
|
|
107
|
-
if key not in cfg:
|
|
108
|
-
typer.echo(f" {key} : 설정된 값 없음")
|
|
109
|
-
return
|
|
110
|
-
del cfg[key]
|
|
111
|
-
_save_config(cfg)
|
|
112
|
-
typer.echo(f"✓ {key} 삭제 완료")
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
@app.command()
|
|
116
|
-
def submit(
|
|
117
|
-
backend: Annotated[str, typer.Option(help="백엔드 이름 (backend1, kreo.sc-20 등)")],
|
|
118
|
-
circuit: Annotated[str, typer.Option(help="회로 문자열 (QASM2/QASM3/JSON)")],
|
|
119
|
-
shots: Annotated[int, typer.Option(help="실행 횟수")],
|
|
120
|
-
priority: Annotated[Optional[int], typer.Option(help="우선순위 1~4 (기본값 3=NORMAL)")] = None,
|
|
121
|
-
optimization_level: Annotated[Optional[int], typer.Option(help="최적화 레벨 0~3")] = None,
|
|
122
|
-
) -> None:
|
|
123
|
-
"""게이트 회로 실행 작업 제출"""
|
|
124
|
-
if priority is not None and priority not in (1, 2, 3, 4):
|
|
125
|
-
typer.echo("priority는 1~4 사이여야 합니다.", err=True)
|
|
126
|
-
raise typer.Exit(1)
|
|
127
|
-
if optimization_level is not None and optimization_level not in (0, 1, 2, 3):
|
|
128
|
-
typer.echo("optimization-level은 0~3 사이여야 합니다.", err=True)
|
|
129
|
-
raise typer.Exit(1)
|
|
130
|
-
with _get_client(_address) as client:
|
|
131
|
-
job_id = client.submit_gate_job(
|
|
132
|
-
backend_name=backend,
|
|
133
|
-
gate_circuits=[circuit.replace("\\n", "\n").replace("\\t", "\t")],
|
|
134
|
-
shots=shots,
|
|
135
|
-
priority=priority,
|
|
136
|
-
optimization_level=optimization_level,
|
|
137
|
-
)
|
|
138
|
-
typer.echo("✓ 작업 제출 완료")
|
|
139
|
-
typer.echo(f" job_id : {job_id}")
|
|
140
|
-
typer.echo(f" priority : {_priority_label(priority if priority is not None else 3)}")
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
@app.command()
|
|
144
|
-
def status(
|
|
145
|
-
job_id: Annotated[str, typer.Option(help="작업 ID")],
|
|
146
|
-
) -> None:
|
|
147
|
-
"""작업 상태 조회"""
|
|
148
|
-
with _get_client(_address) as client:
|
|
149
|
-
job = client.get_job_status(job_id)
|
|
150
|
-
typer.echo(f" job_id : {job['job_id']}")
|
|
151
|
-
typer.echo(f" status : {job['status']}")
|
|
152
|
-
typer.echo(f" priority : {_priority_label(job['priority'])}")
|
|
153
|
-
typer.echo(f" submitted : {job['submitted_at']}")
|
|
154
|
-
typer.echo(f" started : {job['started_at'] or '(미시작)'}")
|
|
155
|
-
typer.echo(f" finished : {job['finished_at'] or '(미완료)'}")
|
|
156
|
-
if job["error"]:
|
|
157
|
-
typer.echo(f" error : {job['error']}")
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
@app.command()
|
|
161
|
-
def result(
|
|
162
|
-
job_id: Annotated[str, typer.Option(help="작업 ID")],
|
|
163
|
-
) -> None:
|
|
164
|
-
"""작업 결과 조회"""
|
|
165
|
-
with _get_client(_address) as client:
|
|
166
|
-
job = client.get_job_result(job_id)
|
|
167
|
-
typer.echo(f" job_id : {job['job_id']}")
|
|
168
|
-
typer.echo(f" status : {job['status']}")
|
|
169
|
-
for i, r in enumerate(job["results"]):
|
|
170
|
-
typer.echo(f" [회로 {i}] counts : {r['counts_json']}")
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
@app.command()
|
|
174
|
-
def cancel(
|
|
175
|
-
job_id: Annotated[str, typer.Option(help="작업 ID")],
|
|
176
|
-
) -> None:
|
|
177
|
-
"""작업 취소 (PENDING 상태일 때만 가능)"""
|
|
178
|
-
with _get_client(_address) as client:
|
|
179
|
-
message = client.cancel_job(job_id)
|
|
180
|
-
typer.echo(f"✓ {message}")
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
def main() -> None:
|
|
184
|
-
app()
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
if __name__ == "__main__":
|
|
188
|
-
main()
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|