quafu-sdk 0.2.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.
quafu/__init__.py ADDED
@@ -0,0 +1,60 @@
1
+ # -*- coding: utf-8 -*-
2
+ """QUAFU 量子计算平台的 Python SDK(发行名 quafu-sdk)。
3
+
4
+ import quafu
5
+ job = quafu.submit(qasm, target='sim') # 第一次用时打印登录链接,在浏览器里登录即可
6
+ print(job.result().counts)
7
+
8
+ quafu.login() / quafu.logout() / quafu.whoami()
9
+ quafu.devices() # 能交到哪些目标
10
+ quafu.job('任务组号') # 接回之前交的任务组
11
+ quafu.sweep(template, parameters, points, target) # 扫参:平台按参数点代入模板
12
+ quafu.open_job(target, ...) # 迭代式任务组:append / wait / table / close
13
+ quafu.device('Dongling').calibration() # 设备详情与校准数据;quafu.calibration(id)
14
+ quafu.stats() # 本人用量与今日额度
15
+ quafu.Client(api_key='qf_…') # 显式身份
16
+ from quafu.qiskit import QuafuProvider, sweep # Qiskit 后端适配与参数化线路扫参(需要 qiskit)
17
+
18
+ 运行时只用标准库;Qiskit 线路作为输入时需要 pip install "quafu-sdk[qiskit]"。"""
19
+ import os as _os
20
+ import sys as _sys
21
+
22
+ from ._conflict import ConflictError, check as _check_conflict
23
+
24
+ try:
25
+ _check_conflict(_os.path.dirname(_os.path.abspath(__file__)))
26
+ except ConflictError as _e:
27
+ # 命令行 quafu 也要先 import 本包:那里报一句话、退出码 1,不甩 Python 栈
28
+ if _os.path.basename(_sys.argv[0] if _sys.argv else '').lower() in ('quafu', 'quafu.exe'):
29
+ from ._i18n import safe_streams as _safe_streams, t as _t
30
+ _safe_streams()
31
+ _sys.stderr.write(_t('cli.error', _e) + '\n')
32
+ raise SystemExit(1)
33
+ raise
34
+
35
+ __version__ = '0.2.0'
36
+
37
+ from ._i18n import get_language, set_language # noqa: E402
38
+
39
+ from .config import DEFAULT_BASE_URL # noqa: E402
40
+ from .errors import (QuafuError, AuthError, NotLoggedIn, LoginFailed, LoginExpired, LoginRevoked, # noqa: E402
41
+ CredentialsError, BadRequest, PrecheckError, QuotaError, DeviceUnavailable, JobFailed,
42
+ JobCancelled, JobNotFound, JobClosed, TaskNotFound, ServerUnavailable, SubmitUncertain,
43
+ ProtocolError, ClientOutdated, Timeout)
44
+ from .jobs import Job, Result # noqa: E402
45
+ from .tables import ResultTable # noqa: E402
46
+ from .devices import Device, Calibration # noqa: E402
47
+ from .bits import reorder_counts, counts_by_qubit, probabilities, expectation_z # noqa: E402
48
+ from .client import (Client, login, logout, whoami, submit, sweep, open_job, job, devices, device, # noqa: E402
49
+ calibration, stats)
50
+
51
+ __all__ = [
52
+ '__version__', 'DEFAULT_BASE_URL', 'set_language', 'get_language',
53
+ 'login', 'logout', 'whoami', 'submit', 'sweep', 'open_job', 'job', 'devices', 'device', 'calibration', 'stats',
54
+ 'Client', 'Job', 'Result', 'ResultTable', 'Device', 'Calibration',
55
+ 'reorder_counts', 'counts_by_qubit', 'probabilities', 'expectation_z',
56
+ 'QuafuError', 'AuthError', 'NotLoggedIn', 'LoginFailed', 'LoginExpired', 'LoginRevoked', 'CredentialsError',
57
+ 'BadRequest', 'PrecheckError', 'QuotaError', 'DeviceUnavailable', 'JobFailed', 'JobCancelled', 'JobNotFound', 'JobClosed',
58
+ 'TaskNotFound', 'ServerUnavailable', 'SubmitUncertain', 'ProtocolError', 'ClientOutdated', 'Timeout',
59
+ 'ConflictError',
60
+ ]
quafu/__main__.py ADDED
@@ -0,0 +1,7 @@
1
+ # -*- coding: utf-8 -*-
2
+ """python -m quafu 等同于命令行 quafu。"""
3
+ import sys
4
+
5
+ from .cli import main
6
+
7
+ sys.exit(main())
quafu/_conflict.py ADDED
@@ -0,0 +1,121 @@
1
+ # -*- coding: utf-8 -*-
2
+ """导入名冲突检查:import quafu 与命令行 quafu 的第一件事。
3
+
4
+ PyPI 上另有发行包也用导入名 quafu。pip 不检查两个发行包是否写同一个目录,两者装进同一个 Python 环境时,
5
+ 文件会装进同一个 site-packages/quafu/、互相覆盖,后装的赢;卸掉其中一个还会删掉另一个的同名文件。
6
+ 混合体的行为不可预测,所以一检测到就报错、说明怎么办,而不是带着警告跑下去。
7
+
8
+ 两道检查,都只用标准库、只读本地元数据,不联网:
9
+ 1. 当前环境里有没有别的发行包也提供顶层名 quafu(读各发行包的 RECORD / top_level.txt;
10
+ 冲突包的名字从元数据动态读出,源码里不写死任何旧组件的名字)
11
+ 2. 本包目录里有没有不属于本包的条目(别的发行包把文件装进了 quafu/,而它的元数据已经不在了)"""
12
+ import os
13
+ import re
14
+
15
+ from ._i18n import t
16
+
17
+ PACKAGE = 'quafu'
18
+ OWN_DISTRIBUTION = 'quafu-sdk'
19
+ # 本包目录下的全部顶层条目(模块去掉 .py)。新增模块时同步这里,tests/unit/test_conflict.py 会核对
20
+ OWN_ENTRIES = frozenset(('__init__', '__pycache__', '__main__', '_conflict', '_i18n', '_inputs', '_messages', '_qasm',
21
+ 'bits', 'cli', 'client',
22
+ 'config', 'credentials', 'devices', 'errors', 'jobs', 'oauth', 'qiskit', 'tables', 'transport',
23
+ 'py.typed'))
24
+
25
+
26
+ class ConflictError(ImportError):
27
+ """当前 Python 环境里还装了另一个占用 quafu 名字的包。"""
28
+
29
+
30
+ def normalize(name):
31
+ return re.sub(r'[-_.]+', '-', name or '').lower()
32
+
33
+
34
+ def _provides(dist, package):
35
+ """这个发行包是否往顶层名 package 下装文件。"""
36
+ try:
37
+ record = dist.read_text('RECORD')
38
+ except Exception:
39
+ record = None
40
+ if record:
41
+ return record.startswith(package + '/') or ('\n%s/' % package) in record
42
+ try:
43
+ top = dist.read_text('top_level.txt')
44
+ except Exception:
45
+ top = None
46
+ return bool(top) and package in top.split()
47
+
48
+
49
+ def other_owners(package=PACKAGE):
50
+ """除本包外、也提供顶层名 package 的发行包:[(名字, 版本)],按名字排序。"""
51
+ try:
52
+ from importlib import metadata
53
+ except ImportError: # pragma: no cover Python < 3.8
54
+ return []
55
+ found = {}
56
+ for dist in metadata.distributions():
57
+ try:
58
+ name = dist.metadata['Name'] or ''
59
+ except Exception:
60
+ continue
61
+ if normalize(name) == OWN_DISTRIBUTION or not name:
62
+ continue
63
+ if _provides(dist, package):
64
+ found[normalize(name)] = (name, dist.version or '?')
65
+ return [found[k] for k in sorted(found)]
66
+
67
+
68
+ def foreign_entries(package_dir):
69
+ """本包目录里不属于本包的顶层条目(排好序)。"""
70
+ try:
71
+ names = os.listdir(package_dir)
72
+ except OSError:
73
+ return []
74
+ out = []
75
+ for n in names:
76
+ stem = n[:-3] if n.endswith('.py') else n
77
+ if n.endswith('.pyc') or stem in OWN_ENTRIES:
78
+ continue
79
+ out.append(n)
80
+ return sorted(out)
81
+
82
+
83
+ # 换干净环境的命令:与默认装法 uv 一致,每种终端一块、能整块照抄(#810:没进虚拟环境时 macOS / Ubuntu 24.04 没有
84
+ # python 命令,只用 uv 装 Python 的 Windows 上 python 返回 9009;一块里混写两种终端也照抄不了)。只给当前系统那几块。
85
+ # PowerShell 默认执行策略会拦 Activate.ps1,所以先放开本进程的策略(只对这个窗口有效)。
86
+ _POSIX_COMMANDS = (' uv venv quafu-env\n'
87
+ ' source quafu-env/bin/activate\n'
88
+ ' uv pip install quafu-sdk')
89
+ _WINDOWS_COMMANDS = (' PowerShell:\n'
90
+ ' uv venv quafu-env\n'
91
+ ' Set-ExecutionPolicy Bypass -Scope Process -Force\n'
92
+ ' quafu-env\\Scripts\\Activate.ps1\n'
93
+ ' uv pip install quafu-sdk\n'
94
+ ' cmd:\n'
95
+ ' uv venv quafu-env\n'
96
+ ' quafu-env\\Scripts\\activate.bat\n'
97
+ ' uv pip install quafu-sdk')
98
+
99
+
100
+ def recovery_commands(windows=None):
101
+ """当前系统上新建干净环境、装本包的命令块(缩进好,可直接接在说明后面)。"""
102
+ if windows is None:
103
+ windows = os.name == 'nt'
104
+ return _WINDOWS_COMMANDS if windows else _POSIX_COMMANDS
105
+
106
+
107
+ def message(owners, entries, windows=None):
108
+ sep = t('conflict.list_sep')
109
+ if owners:
110
+ what = sep.join('%s %s' % kv for kv in owners)
111
+ else:
112
+ what = t('conflict.foreign_files', sep.join(entries[:5]))
113
+ return t('conflict.message', what, recovery_commands(windows))
114
+
115
+
116
+ def check(package_dir):
117
+ """有冲突抛 ConflictError。"""
118
+ owners = other_owners()
119
+ entries = foreign_entries(package_dir)
120
+ if owners or entries:
121
+ raise ConflictError(message(owners, entries))
quafu/_i18n.py ADDED
@@ -0,0 +1,154 @@
1
+ # -*- coding: utf-8 -*-
2
+ """Display language: English (default) or Chinese, for CLI output, login prompts and exception messages.
3
+
4
+ Which language, first match wins:
5
+ 1. quafu.set_language('zh') this process only, not saved (an explicit call in code, like an argument)
6
+ 2. environment variable QUAFU_LANG en / zh; also en_US, zh_CN, zh-CN, zh_CN.UTF-8 …; anything else is ignored
7
+ 3. saved setting ~/.quafu-sdk/config.json (QUAFU_CONFIG_DIR moves the directory), written by
8
+ `quafu config set language zh` or by pressing Z / E during a browser login;
9
+ kept apart from credentials.json and not split by BASE_URL
10
+ 4. default en. The system locale is never consulted: Neo 2026-10-08 wants English by default.
11
+
12
+ Standard library only, and no imports from the rest of the package: _conflict uses it before anything else loads.
13
+ Message text lives in _messages.py."""
14
+ import json
15
+ import os
16
+
17
+ from ._messages import MESSAGES
18
+
19
+ LANGUAGES = ('en', 'zh')
20
+ DEFAULT = 'en'
21
+ ENV_LANG = 'QUAFU_LANG'
22
+ ENV_CONFIG_DIR = 'QUAFU_CONFIG_DIR'
23
+ DIR_NAME = '.quafu-sdk'
24
+ SETTINGS_FILE = 'config.json'
25
+ SETTINGS_VERSION = 1
26
+ _INDEX = {'en': 0, 'zh': 1}
27
+
28
+ _override = None # set_language() in this process
29
+ _cache = [None, None, None] # [path, mtime_ns, language]: one stat per lookup instead of reading the file
30
+
31
+
32
+ def normalize(value):
33
+ """'zh' / 'zh_CN' / 'zh-CN' / 'zh_CN.UTF-8' / 'ZH' → 'zh'; same for en. Anything else → None."""
34
+ if not isinstance(value, str):
35
+ return None
36
+ v = value.strip().lower().split('.')[0].split('@')[0].replace('_', '-')
37
+ base = v.split('-')[0]
38
+ return base if base in LANGUAGES else None
39
+
40
+
41
+ def config_root():
42
+ """The SDK's directory on this machine: QUAFU_CONFIG_DIR, else ~/.quafu-sdk. Credentials and settings both live here."""
43
+ d = os.environ.get(ENV_CONFIG_DIR)
44
+ if d:
45
+ return os.path.abspath(os.path.expanduser(d))
46
+ return os.path.join(os.path.abspath(os.path.expanduser('~')), DIR_NAME)
47
+
48
+
49
+ def settings_path():
50
+ return os.path.join(config_root(), SETTINGS_FILE)
51
+
52
+
53
+ def load_settings(path=None):
54
+ """The settings file as a dict; missing, unreadable or malformed → {} (a broken settings file must not stop the SDK)."""
55
+ path = path or settings_path()
56
+ try:
57
+ with open(path, encoding='utf-8') as f:
58
+ data = json.load(f)
59
+ except (OSError, ValueError):
60
+ return {}
61
+ return data if isinstance(data, dict) else {}
62
+
63
+
64
+ def saved_language():
65
+ """The language saved in the settings file, or None."""
66
+ path = settings_path()
67
+ try:
68
+ mtime = os.stat(path).st_mtime_ns
69
+ except OSError:
70
+ return None
71
+ if _cache[0] == path and _cache[1] == mtime:
72
+ return _cache[2]
73
+ lang = normalize(load_settings(path).get('language'))
74
+ _cache[:] = [path, mtime, lang]
75
+ return lang
76
+
77
+
78
+ def save_language(lang):
79
+ """Save the language in the settings file (other keys kept) and return the path. Raises ValueError for an
80
+ unsupported language and OSError if the file can't be written. Written atomically, like credentials."""
81
+ code = normalize(lang)
82
+ if code is None:
83
+ raise ValueError(t('lang.invalid', lang))
84
+ path = settings_path()
85
+ data = load_settings(path)
86
+ data.update(version=SETTINGS_VERSION, language=code)
87
+ os.makedirs(os.path.dirname(path), mode=0o700, exist_ok=True)
88
+ tmp = '%s.%d.tmp' % (path, os.getpid())
89
+ try:
90
+ with open(tmp, 'w', encoding='utf-8') as f:
91
+ json.dump(data, f, ensure_ascii=False, indent=2)
92
+ f.write('\n')
93
+ os.replace(tmp, path)
94
+ finally:
95
+ if os.path.exists(tmp):
96
+ os.remove(tmp)
97
+ _cache[:] = [None, None, None]
98
+ return path
99
+
100
+
101
+ def resolve():
102
+ """(language, source); source is 'process', 'env', 'saved' or 'default'."""
103
+ if _override is not None:
104
+ return _override, 'process'
105
+ env = normalize(os.environ.get(ENV_LANG))
106
+ if env is not None:
107
+ return env, 'env'
108
+ saved = saved_language()
109
+ if saved is not None:
110
+ return saved, 'saved'
111
+ return DEFAULT, 'default'
112
+
113
+
114
+ def get_language():
115
+ """The display language in effect: 'en' or 'zh'."""
116
+ return resolve()[0]
117
+
118
+
119
+ def set_language(lang):
120
+ """Set the display language for this process only ('en' or 'zh'; zh_CN-style forms are accepted). It takes
121
+ precedence over QUAFU_LANG and the saved setting and is not saved; set_language(None) removes it.
122
+ To save a choice for every run, use `quafu config set language zh` on the command line."""
123
+ global _override
124
+ if lang is None:
125
+ _override = None
126
+ return None
127
+ code = normalize(lang)
128
+ if code is None:
129
+ raise ValueError(t('lang.invalid', lang))
130
+ _override = code
131
+ return code
132
+
133
+
134
+ def t(key, *args):
135
+ """The message `key` in the current language, %-formatted with args."""
136
+ text = MESSAGES[key][_INDEX[get_language()]]
137
+ return text % args if args else text
138
+
139
+
140
+ def safe_streams():
141
+ """Command line only: make stdout / stderr replace characters they can't encode instead of raising.
142
+
143
+ Redirected to a file or a pipe, Python encodes output with the locale's code page; on English Windows that is
144
+ cp1252, which can't encode Chinese, so `quafu --help > f.txt` with the Chinese UI crashed with
145
+ UnicodeEncodeError and a traceback (#810). The terminal itself is unaffected (Windows writes the console in
146
+ UTF-16). Only the error handler changes, never the encoding: a file or pipe gets what its reader expects, with
147
+ '?' for what can't be written. Streams without reconfigure() (replaced by tests or an embedding host) are left
148
+ alone; this must never raise."""
149
+ import sys
150
+ for stream in (sys.stdout, sys.stderr):
151
+ try:
152
+ stream.reconfigure(errors='replace')
153
+ except Exception:
154
+ pass
quafu/_inputs.py ADDED
@@ -0,0 +1,92 @@
1
+ # -*- coding: utf-8 -*-
2
+ """交给平台之前的输入规整:参数点表、参数声明、线路项。只做形状与类型检查,取值合不合规由平台判。"""
3
+ import math
4
+ import operator
5
+
6
+ from ._i18n import t
7
+ from ._qasm import is_qiskit_circuit, to_qasm
8
+
9
+ CIRCUIT_KEYS = ('qasm', 'shots', 'name', 'parameter_values', 'iteration', 'evaluation_index', 'metadata')
10
+ PARAMETER_KEYS = ('name', 'unit', 'symbol')
11
+
12
+
13
+ def number(v, where):
14
+ try:
15
+ x = float(v)
16
+ except (TypeError, ValueError):
17
+ raise TypeError(t('inputs.not_number', where, v)) from None
18
+ except OverflowError:
19
+ raise ValueError(t('inputs.overflow', where)) from None
20
+ if not math.isfinite(x):
21
+ raise ValueError(t('inputs.not_finite', where))
22
+ return x
23
+
24
+
25
+ INDEX_MAX = 2 ** 31 - 1 # iteration / evaluation_index 的取值范围 0..2^31−1(契约 D132)
26
+
27
+
28
+ def index(v, where):
29
+ x = operator.index(v)
30
+ if not 0 <= x <= INDEX_MAX:
31
+ raise ValueError(t('inputs.index_range', where, INDEX_MAX, x))
32
+ return x
33
+
34
+
35
+ def normalize_points(points):
36
+ """参数点表 → [[float, ...], ...]。收列表或 numpy 数组;某行是单个数时当作一个参数的点。"""
37
+ if hasattr(points, 'tolist'):
38
+ points = points.tolist()
39
+ rows = []
40
+ for i, row in enumerate(points):
41
+ if hasattr(row, 'tolist'):
42
+ row = row.tolist()
43
+ if not isinstance(row, (list, tuple)):
44
+ row = [row]
45
+ rows.append([number(v, 'points[%d]' % i) for v in row])
46
+ return rows
47
+
48
+
49
+ def normalize_parameters(parameters):
50
+ """参数声明 → [{name, unit[, symbol]}]。每项写 dict 或 (name, unit[, symbol]);unit 必填,无量纲写 "1"。"""
51
+ out = []
52
+ for p in parameters:
53
+ if isinstance(p, dict):
54
+ extra = set(p) - set(PARAMETER_KEYS)
55
+ if extra:
56
+ raise TypeError(t('inputs.param_unknown_fields', ', '.join(sorted(extra))))
57
+ item = {k: p[k] for k in PARAMETER_KEYS if p.get(k) is not None}
58
+ elif isinstance(p, (tuple, list)) and 2 <= len(p) <= 3:
59
+ item = dict(zip(PARAMETER_KEYS, p))
60
+ else:
61
+ raise TypeError(t('inputs.param_shape'))
62
+ if not item.get('name') or not item.get('unit'):
63
+ raise TypeError(t('inputs.param_required', p))
64
+ out.append(item)
65
+ return out
66
+
67
+
68
+ def normalize_circuit(item, iteration=None, evaluation_index=None):
69
+ """一条线路 → 契约的 circuits 项。收 OpenQASM 2 文本或 dict;iteration / evaluation_index 只补给没写的项。"""
70
+ if isinstance(item, str) or is_qiskit_circuit(item):
71
+ item = {'qasm': to_qasm(item)}
72
+ elif not isinstance(item, dict):
73
+ raise TypeError(t('inputs.circuit_type'))
74
+ extra = set(item) - set(CIRCUIT_KEYS)
75
+ if extra:
76
+ raise TypeError(t('inputs.circuit_unknown_fields', ', '.join(sorted(extra))))
77
+ if is_qiskit_circuit(item.get('qasm')):
78
+ item = dict(item, qasm=to_qasm(item['qasm']))
79
+ if not isinstance(item.get('qasm'), str):
80
+ raise TypeError(t('inputs.qasm_type'))
81
+ out = {k: v for k, v in item.items() if v is not None}
82
+ if 'shots' in out:
83
+ out['shots'] = operator.index(out['shots'])
84
+ if 'parameter_values' in out:
85
+ out['parameter_values'] = {str(k): number(v, 'parameter_values[%r]' % k)
86
+ for k, v in out['parameter_values'].items()}
87
+ for key, value in (('iteration', iteration), ('evaluation_index', evaluation_index)):
88
+ if value is not None:
89
+ out.setdefault(key, value)
90
+ if key in out:
91
+ out[key] = index(out[key], key)
92
+ return out