The four legs of wai-quantum-windows.yml had never passed. Run 32449 (8617d9d02) failed in
each; what each leg's failure was, and what this changes:
- engine · windows. Two engine::energy tests failed with os error 267: their fixture makes
the powercap interface's own zone directories, `intel-rapl:0`, and NTFS reads the colon as
the start of a stream's name, so no Windows file system can hold that name (checked on the
runner: 267 for `intel-rapl:0`, 123 for `intel-rapl:0:0`). The reader is Linux's interface
and every caller opens it on Linux alone, so the fixture, `tree()` and the two tests are
cfg(unix), with a note saying why; a cfg(windows) test pins what Windows has instead: the
name cannot be made a directory, `access_in` answers NoInterface, and `Packages::open`
fails with that reason. No line outside the tests changes.
- command line · windows. Its test binary did not compile (E0433, E0599): the ceiling test
used std::os::unix's PermissionsExt and Permissions::from_mode with no gate. Its runs are
shell scripts held to Linux's and macOS's counters, so it is cfg(unix), and a cfg(windows)
test asserts what Windows does: a run under a ceiling is refused, for want of a package
counter, before any process starts. Two more tests of that module, which now compile on
Windows, would have failed there: the package-counter test (the same 267 fixture, now
cfg(unix)), and the timings-path test, whose `/x` is not absolute on Windows (it now names
`C:\x` there; path_from is unchanged). die_with_parent says what Windows does: it starts
no run in a process of its own, as it reads no counter there.
- job service · windows. The binary and its tests did not compile: open_files() raised
RLIMIT_NOFILE through libc, a cfg(unix) dependency, with no gate (six E0433). It is
cfg(unix); on Windows, which sets a process no open-file limit, a twin logs that and
returns the 2^24 handles a process may hold, so connections are held under
WAI_QSVC_CONNECTIONS alone. Built, the service would then have failed every job and held
none to its budget, so each Unix-only step it takes has its Windows form:
- workers and the writer were started through `nice`, which Windows has not; they are
started directly, in BELOW_NORMAL_PRIORITY_CLASS;
- a process's memory, read through `ps` off Linux and macOS, and the host's, through
sysctl and memory_pressure, read nothing there, which turned off the per-job cap, the
reader's cap and the queue's wait for free memory; src/win32.rs asks the system
(K32GetProcessMemoryInfo, the commit charge, which counts pages paged out as Linux's
VmSwap does; GlobalMemoryStatusEx);
- a worker, a reader or the writer whose service has died ran on, as watch_parent is
Unix's; on Windows it waits on the service's process (OpenProcess, WaitForSingleObject)
and exits when it ends, the reader now told the service's pid as workers are;
- write_private wrote over an existing key on Windows; it now makes a new file only, as
on Unix, and says the file is as private as its folder's inherited access list.
The tests that would have failed there: the binary-replacement test moves the running
binary aside and puts the new one at its name (Windows refuses a copy over it, 32, and a
rename onto it, 5); mem_kib reads the working set through PowerShell, as there is no `ps`;
the writer tests' script reaches the writer late through a named pipe the test serves
(Late), as through a FIFO on Unix, where mkfifo is unchanged. The depth.rs connection
helpers are cfg(unix) with the connection tests, which hold the service to `ulimit -n`.
- python · windows. test_a_set_s_text_is_judged_as_a_set found cli-set-sweep.json invalid
(items_sealed and root false): Path.read_text() with no encoding decodes in the ANSI code
page (1252 on the runner), which turned the receipt's `π` and `≤` into other characters,
so the items' sealed texts no longer hashed to their seals; cli-set-programs.json, all
ASCII, passed. The package's own path readers (_source, _job_of, _job_text, _document,
landscape) did the same, so every Windows user's receipts and programs given as a path were
read wrong. Every read_text in the package and its tests now names UTF-8, as the command
line and the service read; the verifier is unchanged. In the tests, the command line's and
node's output are decoded as UTF-8, and test_cli's fixtures are written as UTF-8 bytes,
without the CR LF text mode writes on Windows. Key.save writes its bytes in binary mode, so
a key file is the command line's `<64 hex>\n` on every system, and the test compares the
bytes everywhere and exempts only the mode bits on Windows; user_key_path follows the
command line's order, XDG_CONFIG_HOME, then HOME's .config, then APPDATA, which it
lacked, so the two found different keys on Windows. Tests for each, one of them run against
the command line with only APPDATA set.
Unix behaves as before: every change there is a cfg attribute, a branch that is false on
Unix (cfg!(windows)), test code, or Python that decodes as the UTF-8 locales on Linux and
macOS already did; the FIFO tests run as they did.
Verified, toolchain 1.99.1. Build host (macOS): `cargo check --target x86_64-pc-windows-msvc
--all-targets` exits 0 for wai-quantum (full), the command line and the service, and clippy for
that target warns of nothing in these files (ring's C, which needs MSVC headers, was compiled
from an empty file for the check; nothing is linked); a type error put in five of the new
Windows-only pieces (win32.rs, rss_bytes, watch_parent, the pipe's give, the command line's
Windows test) fails that check at each. On macOS and on Linux (x86_64): clippy -D warnings in
wai-quantum (default, full, wasm32-wasip2 full), the command line (default, none) and the
service; engine::energy 15 and 14 passed; the command line's tests and the service's tests
all passed; on macOS the Python package's ci/check.sh (103 passed, nothing skipped, the
notebooks, Python 3.9) and the suite again under LC_ALL=en_US.ISO8859-1 (103 passed). Text
checks: comparative prose, public names (maintainer products), vanity metrics, quantum names,
changelog entries, tests in CI. On the Windows runner, a probe compiled from src/win32.rs and
the test's Late as they are: a process's commit charge and the host's memory read, a watch on
a process that ends exits 3 when it ends, the below-normal start runs, a killed first reader
leaves the script to the second, which reads all 100,004 bytes, and a running binary is moved
aside and replaced. Not run on Windows: the crates' own tests, which the nightly Windows run
will show.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
13 KiB
Python 版 wai-quantum
本文为机器辅助翻译,尚待熟练的读者审校。英文原文:README.md。
在 Python 和笔记本中以进程内方式使用 WAI Quantum。这个包内含 WAI Quantum 作业引擎(Rust crate wai-quantum 的 engine,通过一个 pyo3 扩展模块提供),因此在这里规划、运行并封存的作业,与 wai-quantum 命令行、作业服务、MCP 服务器、Studio 和桌面应用给出相同的计划、相同的结果和相同的回执。
- 引擎:
run、plan、lint、draw(文本和 SVG)、layout、critique、improve、shortcuts、equivalent、verify、seal、canonical、digest、methods、accuracy_classes、bloch、history、estimate和verify_estimate。 - 回执:每个结果都用你传入的密钥(
wai_quantum.Key)封存;任何回执(无论是在这里、由命令行、由作业服务还是在浏览器标签页中生成的)都可以在这里离线检查。 - 作业服务:
wai_quantum.Service,一个只用标准库实现的客户端,用于https://wai.transaction.science/quantum-jobs/v1。 - 笔记本小组件:
Circuit、Result、Receipt和Bloch,附有可在任何笔记本中渲染的静态 HTML 和 SVG 后备显示。
安装
从 Python 包索引安装。每个平台一个 wheel,适用于 3.9 及以后的所有 Python 版本(稳定 ABI,abi3):macOS arm64 和 x86_64、Linux x86_64 和 arm64(manylinux 2.17)、Windows x86_64。
pip install wai-quantum # 引擎和作业服务客户端:没有其他依赖
pip install "wai-quantum[widgets]" # 另加笔记本小组件(anywidget)
同样的 wheel 也附在代码托管平台(forge)上包含它们的 wai-quantum-cli-v<version> 发布版中,每个都附有 SHA-256:
pip install wai_quantum-0.1.4-cp39-abi3-<platform>.whl
要自己构建 wheel,需要 Rust 1.98 和 maturin 1.15.0:
cd wai/wai-quantum-py
maturin build --release --locked -o dist
pip install dist/wai_quantum-*.whl
使用
import wai_quantum as wq
bell = "OPENQASM 3.0; qubit[2] q; bit[2] c; h q[0]; cx q[0], q[1]; c = measure q;"
key = wq.Key.generate() # 或 Key.from_file(...)、Key.demo();默认使用这个用户的密钥
job = wq.run(bell, shots=1000, key=key)
job.method, job.accuracy # ('stabilizer', 'exact')
job.counts # {'00': 522, '11': 478}
job.probabilities() # 每个计数除以测量次数:采样得到的频率
wq.verify(job).valid # True:回执封存了这个请求和这个结果
作业就是作业服务所用的形式,表示为一个普通的 dict(status、method、why、estimate、request,以及带有 receipt 的 result),因此 json.dumps(job) 写出的就是作业服务会发送的内容。它的辅助方法读取最常用的部分:counts、shots、probabilities()、expectations、amplitudes、method、accuracy、receipt、verify()。
run 接受 OpenQASM 3(或 2)文本、此类文件的路径或一个作业 dict,并以关键字参数接受作业的各字段:shots、seed、method(默认为 auto)、observables("Z0 Z1" 或 {"pauli": "X3 Y4", "coeff": 0.5})、amplitudes(比特串,量子比特 n − 1 在前)、noise(depolarizing_1q、depolarizing_2q、amplitude_damping、dephasing、readout)以及 options(因方法而异;wq.methods() 会列出它们)。memory_gb(16,即命令行的默认值)限定作业在这里可以占用的内存,因此作业在两者上会规划到同一种方法。
src = "OPENQASM 3.0; qubit[2] q; h q[0]; t q[0]; t q[0]; cx q[0], q[1];"
a, b = "qubit[1] q; h q[0]; x q[0]; h q[0];", "qubit[1] q; z q[0];"
job = wq.run(bell, shots=100, intent={"distribution": {"00": 0.5, "11": 0.5}}, key=key)
job.verdict["level"], job.verdict["holds"] # ('proof', True):引擎检查到了哪一级,已封存
job.verdict["layer"], job.verdict["limits"] # 由什么判定('exact_simulation'),以及它不能说明什么
wq.verify(job).answer["again"] # 'reproduced':已封存的裁定在此处重新得出
wq.verdict(job, {"distribution": {"00": 1.0}}) # 另一个意图,在这里得出:注明未经封存
wq.signoff(job, {"outcomes": ["00", "11"]}, key=key).verdict["holds"] # 为已封存的作业签核:其摘要、意图和裁定封存于同一回执之下
fair = {"distribution": {"00": 0.5, "11": 0.5}}
wq.check(bell, fair)["next"] # 签核循环:每个用例由最强的层判定,并说明下一步;不封存任何内容
g = wq.grade(bell, {"title": "A Bell pair", "intent": fair}, key=key) # 评分作为作业封存,任何人都可以重新运行
wq.verify(g).answer["again"] # 'reproduced':评分在这里重新得出
wq.attest(job, {"outcomes": ["00", "11"]}, key=key)["attested"]["held_to"] # 作业先像 verify 那样核验,然后签核
wq.repair("OPENQASM 3.0;\nqubit[1] q\nh q[0];\n") # 对被标出的行的修正,以及需要人来处理的部分
wq.plan(bell) # 方法及其原因,不运行
wq.lint("qubit[1] q;\nfoo q[0];") # {'ok': False, 'error': 'line 2: unknown gate foo', 'line': 2}
print(wq.draw(bell)) # 文本线路图,与 `wai-quantum draw` 打印的相同
svg = wq.draw(bell, format="svg") # Studio 中的图,作为一个 SVG 文档
wq.critique(src); wq.improve(src) # 度量与问题模式;经证明等价的变体
wq.equivalent(a, b) # {'verdict': 'equivalent', 'how': 'tableau', 'why': ...}
wq.bloch("OPENQASM 3.0; qubit[1] q; ry(0.3) q[0];", key=key) # 每个量子比特的约化态,来自引擎
e = wq.estimate(src, qubit="ns-e4", budget=1e-3, key=key) # 容错量子计算机需要什么,每个估算都已封存
wq.verify_estimate(e) # 封存签名、每一份回执,以及在这里重新运行的请求
wq.estimate(counts={"numQubits": 100, "cczCount": 5000}) # 或者一份逻辑计数(或 routine=)文档
estimate 给出程序的逻辑计数(逻辑量子比特,包括多控制门所需的辅助量子比特;测量;T 门;Toffoli 门;旋转门及其合成所得门序列中的 T 门;T 深度和深度)、错误预算中分给合成的份额及每个旋转门所占的部分、根据引擎自己的解码器对码模型所做的校准、在该物理量子比特上的估算,以及物理量子比特数与运行时间之间的前沿,每个估算都在资源估算回执族中封存。export="counts" 或 "routine" 会把计数另外作为一份文档给出。每个数字的含义和假设,见估算页面。
c = wq.qec_code("color:7", key=key) # 它的校验、k、可用的解码器,以及经过认证的距离
wq.qec_code("color:7", certificate=c) # 重新运行证书的见证和搜索
wq.qec_decode("surface:5", error=[3, 12]) # 解码其校验子,并判定残差
wq.qec_decode(memory=(5, 5, 0.003), shots=20000, decoder="correlated") # 线路层面的失败率
wq.qec_threshold(distances=[3, 5, 7], p=[0.002, 0.004], shots=20000) # 失败率及其误差
job = wq.run(src, observables=["Z0 Z1"], noise={"depolarizing_2q": 0.02}, mitigate="zne", key=key)
job["result"]["mitigation"] # 每个值的原始值和缓解后的值及其误差棒,以及该方法的假设
qec_code、qec_decode 和 qec_threshold 是引擎的纠错功能,与命令行的 wai-quantum qec 以及两个 MCP 服务器的 qec_* 工具给出的回答相同,每个回答都以作业的形式封存(wq.verify 检查它)。距离证书在每个扇区中给出找到的逻辑算符的最小权重、该权重的一个见证,以及经穷举搜索排除的每个更轻的支撑;被 bound 截止的搜索对其之外的情况不作结论。每个解码器保证什么,以及每种缓解方法的偏差和方差,见纠错页面。
密钥与回执
Key.demo() 是公开的演示密钥(即命令行的 --demo-key):用它签名的回执在任何地方都逐字节重现,只能证明结果未被篡改。Key.generate() 生成一个密钥;key.save(path) 以命令行所用的格式(64 位十六进制数字)把它写出,且仅其所有者可读,因此 wai-quantum run --key path 会用同一个密钥签名。未给出密钥时,run 用这个用户的密钥签名,也就是命令行自己用的那个($WAI_QUANTUM_KEY,否则为 $XDG_CONFIG_HOME/wai/quantum-key 或 ~/.config/wai/quantum-key,在 Unix 类 shell 之外的 Windows 上为 %APPDATA%\wai\quantum-key,首次使用时生成)。
回执是对请求和结果的 RFC 8785 规范化 JSON 所求 BLAKE3 摘要的 Ed25519 签名。wq.canonical(value) 给出这些字节,wq.digest(request, result) 给出摘要;wq.seal(request, result, key=...) 可封存任何结果。拒绝会引发 wq.EngineError,附带引擎自身的消息、消息指明了行号时的 line,以及其 code:读取器的(Q103)或作业层的(J011),与消息末尾所附的相同。对于改动了没有任何诊断标出的行的建议,wq.repair(program, proposed=...) 会引发一个带 J021 的此类错误。判定页面列出了各个级别和规则。
作业服务
svc = wq.Service() # $WAI_QUANTUM_SERVICE,或 https://wai.transaction.science/quantum-jobs/v1
job = svc.run(bell, shots=1000) # 提交并等待
rec = svc.submit(bell, shots=1000) # 或者:先提交,再 status / result / cancel
job = svc.result(rec["id"])
svc.verify(job) # 在这里离线检查;把它的密钥与 svc.describe()["receipt_key"] 对比
客户端使用 urllib,并在每个请求的 User-Agent 中自称 wai-quantum-python/<version>:作业服务的边缘节点会拒绝带有通用库默认用户代理的请求。拒绝、失败或被取消的作业,或无法连通的作业服务,都会引发 wq.ServiceError,附带 HTTP status 和作业服务的原话。
笔记本
h = wq.history("start", {"source": src, "by": {"kind": "person"}, "at": 1}, key=key)["history"]
program = "OPENQASM 3.0; qubit[2] q; ry(0.3) q[0]; h q[1];"
wq.Circuit(bell) # 引擎的线路图;点击一个门:.selected 就是它的操作
wq.Circuit(src, history=h) # 每个门都标出出自谁之手(wq.history)
wq.Result(job) # 以条形(或频率)显示的计数、方法、精度类别、回执的检查结果
wq.Receipt(job) # 签名者、摘要,以及引擎在这里计算出的 verified / INVALID 徽章
wq.Bloch(program, key=key) # 每个量子比特的 Bloch 矢量;拖动可旋转视角
每个小组件的前端都是一个小巧的普通 ES 模块(python/wai_quantum/static/)。若未安装 widgets 附加依赖,或前端没有小组件扩展,每个小组件都渲染为静态 HTML(Circuit 渲染为 SVG);Job 显示为其结果的卡片。每个小组件只显示引擎给出的内容,自己不计算任何东西:它的 data 就是引擎的回答。
notebooks/ 中的文档笔记本在 CI 中运行,并须与其存储的输出一致:getting-started、bell-receipt(一个 Bell 对及其回执)、equivalence、noisy-run,以及 service-job(在为这次运行而启动的作业服务上执行的作业;没有这样的服务时,它的单元格会跳过)。
测试
ci/check.sh 就是完整的检查,与 CI 的运行方式相同(.github/workflows/wai-quantum-py.yml,在 linux 和 macos 上):它从这份检出构建命令行和作业服务,用 maturin 构建 wheel,在一个全新的 venv 中用固定版本并附哈希的测试依赖(tests/requirements.lock)安装该 wheel,运行 pytest 并要求每个测试都执行,针对本地作业服务运行各笔记本,并在 Python 3.9 上只用该 wheel 运行 tests/smoke.py。这些测试要求这个包与其他入口保持一致:在这里运行的作业与命令行和作业服务的作业相同(各主机的时间及其关于能耗的说明除外),在这里封存的回执能通过 wai-quantum verify 验证,命令行的回执也能在这里验证,两者用同一个用户密钥签名,而小组件的前端会在 node 下以这个包构建的数据运行。
能耗:在这里的运行不计量;其结果会注明这一点(energy.unmetered),与命令行和浏览器标签页中的运行一样。作业服务会为每个作业的 worker 计量。
许可证:Apache-2.0。