线路构建:从门操作到格式互转¶
第 1 章我们用三行代码搭出了 Bell 态线路。本章把 Circuit API 讲完整:全部原生门、命名寄存器、符号参数、控制结构与可复用子程序,以及 Circuit、OriginIR、OpenQASM 2.0 三种表示之间的互转。读完本章,你能用任何顺手的方式把线路"写出来",并把它交给后面章节的模拟器或云平台。
门的矩阵定义与量子线路的理论模型见姊妹站量子计算算法教程·量子计算基础。本页示例基于 unified-quantum 0.1.0,所有输出均为实际运行结果。
本课知识点
门操作与测量——能用
CircuitAPI 写出单比特门、双比特门与三比特门线路,并解释寄存器自动扩展规则。命名寄存器——能用
qregs与get_qreg以命名寄存器组织多比特线路,解释导出时寄存器名被"扫平"的行为。参数化线路与绑定——能构造符号参数线路并计算
free_parameters,用assign_parameters完成整体或部分绑定。控制结构与可复用子程序——能构造 CONTROL 块、DAGGER 块与
circuit_def子程序,比较 Python 块语法与导出的内联形式。OriginIR 与 OpenQASM 导入导出——能在 Circuit、OriginIR、OpenQASM 2.0 之间互转并验证 round-trip,解释扩展门到官方 OriginIR 的自动降级。
线路统计、重映射与酉矩阵——会读取
depth/qubit_num等统计属性、执行量子比特重映射,并用get_matrix验证线路的 little-endian 酉矩阵约定。
1. 门操作与测量¶
Circuit() 从空线路开始,不需要预先声明比特数:首次用到某个比特时寄存器自动扩展。门方法与门同名,按作用的比特数与参数个数分类。我们用三类门各一个,搭一个真正有用的线路——半加器(输入 \(a,b\),输出和 \(s=a\oplus b\) 与进位 \(c=a\wedge b\)):
from uniqc import Circuit
c = Circuit() # 空线路,用到哪个比特,寄存器就自动扩到多大
c.x(0) # 输入 a = 1
c.x(1) # 输入 b = 1
c.cnot(0, 2) # q2 ^= a
c.cnot(1, 2) # q2 ^= b → sum = a ⊕ b
c.toffoli(0, 1, 3) # q3 = a ∧ b → carry
c.measure(0, 1, 2, 3) # 测量,结果依次写入 c[0..3]
print(c.originir)
QINIT 4
CREG 4
X q[0]
X q[1]
CNOT q[0], q[2]
CNOT q[1], q[2]
TOFFOLI q[0], q[1], q[3]
MEASURE q[0], c[0]
MEASURE q[1], c[1]
MEASURE q[2], c[2]
MEASURE q[3], c[3]
measure(0, 1, 2, 3) 的参数是被测的量子比特编号(不要求连续)。常用门一览(参数均为弧度制数值;符号参数见第 3 节):
类别 |
门 |
|---|---|
单比特·无参数 |
|
单比特·带参数 |
|
双比特 |
|
三比特 |
|
快速验证一下半加器确实算对了(模拟细节在第 3 章展开):
from uniqc.simulator import Simulator
print(Simulator().simulate_shots(c.originir, shots=8))
{11: 8}
11 = 0b1011,按"c[0] 在最右"的约定从右往左读:a=1、b=1、sum=0、carry=1——正是 \(1+1=10_2\) 的半加结果,且 8 次采样全部一致(该线路输出是确定性的)。
2. 命名寄存器¶
比特一多,裸整数索引很快变得难读。Circuit(qregs=...) 支持命名寄存器:get_qreg 取回寄存器引用后,用 data[0] 这样的名字访问比特,代码与算法描述一一对应:
from uniqc import Circuit
c = Circuit(qregs={"data": 3, "anc": 1})
data = c.get_qreg("data")
anc = c.get_qreg("anc")
c.h(data[0])
c.cnot(data[0], data[1])
c.cnot(data[1], data[2]) # 在 data 上制备 3 比特 GHZ 态
c.x(anc[0]) # ancilla 恒置 1
c.measure(data[0], data[1], data[2], anc[0])
print(c.originir)
QINIT 4
CREG 4
H q[0]
CNOT q[0], q[1]
CNOT q[1], q[2]
X q[3]
MEASURE q[0], c[0]
MEASURE q[1], c[1]
MEASURE q[2], c[2]
MEASURE q[3], c[3]
注意导出结果:data 占据 q[0..2]、anc 占据 q[3](按声明顺序接续编号),且寄存器名消失了——命名只存在于构建期,originir 导出时一律"扫平"为物理索引。这是刻意的:所有后端只需要理解扁平的 q[i]。
from uniqc.simulator import Simulator
print(Simulator().simulate_shots(c.originir, shots=16))
{8: 7, 15: 9}
两个键:8 = 0b1000(data 全 0、anc=1)与 15 = 0b1111(data 全 1、anc=1)——GHZ 的关联性加上恒为 1 的 ancilla(计数比例每次运行略有不同)。
3. 参数化线路与绑定¶
变分算法(VQE、QAOA)需要"一个线路模板、反复绑定不同角度"。Parameter 是符号标量,Parameters 是符号数组,角度槽里还可以写符号表达式(参数化门的理论背景见姊妹站参数化门附录):
from uniqc import Circuit, Parameter, Parameters
theta = Parameter("theta")
phi = Parameter("phi")
w = Parameters("w", size=2) # 参数数组:w[0]、w[1]
c = Circuit(2)
c.rx(0, theta)
c.ry(1, theta * 2 + phi / 3) # 角度槽支持符号表达式
c.rz(0, w[0])
c.measure(0, 1)
print(c.originir)
print("free_parameters =", c.free_parameters)
QINIT 2
CREG 2
PARAM phi
PARAM theta
PARAM w[2]
RX q[0], (theta)
RY q[1], (phi/3 + 2*theta)
RZ q[0], (w[0])
MEASURE q[0], c[0]
MEASURE q[1], c[1]
free_parameters = ['phi', 'theta', 'w_0']
头部多了 PARAM 声明,门的角度槽内联参数名或表达式(表达式经符号运算规范化,theta * 2 + phi / 3 输出为 phi/3 + 2*theta)。两个细节:free_parameters 只列出实际被引用的符号(w[1] 没用到,所以不在列表里);PARAM 是 OriginIR-ext 的本地扩展,提交云端或导出 QASM 之前必须先绑定成数值。
assign_parameters 把符号换成数值,默认返回新线路、不动原线路,绑定字典的键可以是名字符串、Parameter 对象或整个 Parameters 数组:
bound = c.assign_parameters({"theta": 0.5, "phi": 1.0, w: [0.3, 0.7]})
print(bound.is_parametric, bound.free_parameters)
print(bound.originir)
False []
QINIT 2
CREG 2
RX q[0], (0.5)
RY q[1], (1.3333333333333333)
RZ q[0], (0.3)
MEASURE q[0], c[0]
MEASURE q[1], c[1]
还支持部分绑定——没给的参数保持符号,可以分批赋值。但带着未绑定符号的线路不能模拟,错误信息会把缺失的参数名列出来:
partial = c.assign_parameters({"theta": 0.5})
print("partial free:", partial.free_parameters)
from uniqc.simulator import Simulator
try:
Simulator().simulate_shots(partial.originir, shots=8)
except ValueError as e:
print("ValueError:", e)
partial free: ['phi', 'w_0']
ValueError: Cannot simulate a circuit with unbound symbolic parameters: phi, w_0. Bind them to concrete values first, e.g. circuit.assign_parameters({'<name>': <value>, ...}).
4. 控制结构与可复用子程序¶
CONTROL 块把一段线路整体变成受控操作:块内所有门共享同一组控制比特。两个控制位控制一个 X,就得到了 TOFFOLI:
from uniqc import Circuit
from uniqc.simulator import Simulator
c = Circuit(3)
c.x(0)
c.x(1) # 先把两个控制位置 1
with c.control(0, 1): # 块内门受 q0、q1 控制
c.x(2) # 等价于 TOFFOLI(0, 1, 2)
c.measure(0, 1, 2)
print(c.originir)
print(Simulator().simulate_shots(c.originir, shots=8))
QINIT 3
CREG 3
X q[0]
X q[1]
X q[2] controlled_by (q[0], q[1])
MEASURE q[0], c[0]
MEASURE q[1], c[1]
MEASURE q[2], c[2]
{7: 8}
导出时 CONTROL 块被序列化为内联的 controlled_by 后缀(OriginIR-ext 扩展语法);导出官方 OriginIR 时会自动转回 CONTROL ... ENDCONTROL 块语法。{7: 8} 即 0b111:控制位全 1,受控 X 触发。
DAGGER 块把块内门取共轭转置。单个旋转门取逆等于角度变号,前后抵消:
c2 = Circuit(1)
c2.rx(0, 1.23)
with c2.dagger(): # 块内门取共轭转置
c2.rx(0, 1.23) # RX(θ)†,与上面的 RX(θ) 抵消
c2.measure(0)
print(c2.originir)
print(Simulator().simulate_shots(c2.originir, shots=8))
QINIT 1
CREG 1
RX q[0], (1.23)
RX q[0], (1.23) dagger
MEASURE q[0], c[0]
{0: 8}
注意:多门 DAGGER 块的顺序语义
在 0.1.0 中,Python 的 with c.dagger(): 对块内每个门逐个取逆,并按书写顺序导出为 G ... dagger 行序列;而 OriginIR 文本的 DAGGER ... ENDDAGGER 块语义是整段逆序取逆(\((AB)^\dagger = B^\dagger A^\dagger\))。对不交换的多门序列,两者结果不同(例如"H 后接 CNOT"再取逆)。需要整段取逆时,请在块内按逆序手写各门,并先在小线路上验证;单门或相互交换的门则两种写法完全一致。
可复用子程序用 @circuit_def 定义:声明形式量子寄存器(还可以声明标量形参),调用时通过 qreg_mapping 映射到父线路的实际比特。定义一次,到处复用:
from uniqc import Circuit, circuit_def
@circuit_def(name="bell_pair", qregs={"q": 2})
def bell_pair(circ, q):
circ.h(q[0])
circ.cnot(q[0], q[1])
return circ
print(bell_pair.to_originir_def())
c3 = Circuit(qregs={"data": 4})
data = c3.get_qreg("data")
bell_pair(c3, qreg_mapping={"q": [data[0], data[1]]}) # 第一对 Bell
bell_pair(c3, qreg_mapping={"q": [data[2], data[3]]}) # 第二对 Bell
c3.measure(data[0], data[1], data[2], data[3])
print(c3.originir)
print(Simulator().simulate_shots(c3.originir, shots=8))
DEF bell_pair(q[2])
H q[0]
CNOT q[0], q[1]
ENDDEF
QINIT 4
CREG 4
H q[0]
CNOT q[0], q[1]
H q[2]
CNOT q[2], q[3]
MEASURE q[0], c[0]
MEASURE q[1], c[1]
MEASURE q[2], c[2]
MEASURE q[3], c[3]
{0: 2, 15: 1, 3: 2, 12: 3}
to_originir_def() 单独导出子程序定义为 DEF 块(OriginIR-ext 支持 DEF/ENDDEF);放进父线路后,导出的 originir 会把调用内联展开——round-trip 是语义等价而非文本等价。计数只出现 0(00|00)、3(00|11)、12(11|00)、15(11|11):两对比特各自纠缠成 Bell 对(比例每次运行略有不同)。
5. OriginIR 与 OpenQASM 导入导出¶
UnifiedQuantum 里线路有三种表示:内存中的 Circuit(opcode 序列)、OriginIR-ext 文本(.originir,默认本地语言,官方 OriginIR 的超集)、OpenQASM 2.0 文本(.qasm,IBM 等平台的标准)。三者可以自由互转:
from uniqc import Circuit
c = Circuit()
c.h(0)
c.cnot(0, 1)
c.measure(0, 1)
print(c.qasm)
OPENQASM 2.0;
include "qelib1.inc";
qreg q[2];
creg c[2];
h q[0];
cx q[0], q[1];
measure q[0] -> c[0];
measure q[1] -> c[1];
from_qasm / from_originir 类方法把文本导回 Circuit,可以继续编辑或提交。互转是可逆的——round-trip 后文本完全一致:
c2 = Circuit.from_qasm(c.qasm) # QASM → Circuit
c3 = Circuit.from_originir(c.originir) # OriginIR → Circuit
print("QASM round-trip:", c2.originir == c.originir)
print("OriginIR round-trip:", c3.qasm == c.qasm)
QASM round-trip: True
OriginIR round-trip: True
那 OriginIR-ext 比官方 OriginIR"超"在哪里?一部分是扩展门(ISWAP、ECR、XX、YY、ZZ 等)与 PARAM、DEF、内联 dagger/controlled_by 这些语法。.originir 原样保留它们;.originir_official 则导出官方子集,扩展门被自动分解成官方门:
c4 = Circuit(2)
c4.iswap(0, 1) # ISWAP 是 ext 扩展门
c4.measure(0, 1)
print(c4.originir_official)
QINIT 2
CREG 2
S q[0]
S q[1]
H q[0]
CNOT q[0], q[1]
CNOT q[1], q[0]
H q[1]
MEASURE q[0], c[0]
MEASURE q[1], c[1]
一条 ISWAP 被分解为 S–S–H–CNOT–CNOT–H 六个官方门,等价但不冗余——这正是提交真实云平台时后台发生的事(第 5 章)。日常写代码时用哪种格式不用纠结:Circuit 对象、OriginIR 文本、QASM 文本都能直接交给模拟与提交入口。
6. 线路统计、重映射与酉矩阵¶
写完线路常要问三个问题:多大规模?多深?怎么搬到别的比特上?前两个看属性(注意 depth 等是属性不是方法,不要加括号):
from uniqc import Circuit
c = Circuit()
c.h(0)
c.cnot(0, 1)
c.cz(1, 2)
c.measure(0, 1, 2)
print("qubit_num =", c.qubit_num, "| cbit_num =", c.cbit_num,
"| depth =", c.depth, "| 门操作数 =", len(c.opcode_list))
remapped = c.remapping({0: 10, 1: 11, 2: 12}) # 逻辑比特 → 物理比特
print(remapped.originir)
qubit_num = 3 | cbit_num = 3 | depth = 3 | 门操作数 = 3
QINIT 13
CREG 3
H q[10]
CNOT q[10], q[11]
CZ q[11], q[12]
MEASURE q[10], c[0]
MEASURE q[11], c[1]
MEASURE q[12], c[2]
depth 是关键路径上的门数(不计 I 与 BARRIER),直接对应真机上的运行时长量级。remapping 返回一条新线路,把逻辑比特映射到指定物理索引——对接真机拓扑时(哪些物理比特相连、哪些更干净)就靠它。
最后一个工具把线路当成数学对象:get_matrix() 返回线路(不含测量)的完整酉矩阵(幺正演化与酉矩阵的定义见姊妹站量子力学基础):
import numpy as np
from uniqc import Circuit
c = Circuit()
c.h(0)
c.cnot(0, 1) # 制备 Bell 态的线路
U = c.get_matrix()
print(np.round(U, 4))
[[ 0.7071+0.j 0.7071+0.j 0. +0.j 0. +0.j]
[ 0. +0.j 0. +0.j 0.7071+0.j -0.7071+0.j]
[ 0. +0.j 0. +0.j 0.7071+0.j 0.7071+0.j]
[ 0.7071+0.j -0.7071+0.j 0. +0.j 0. +0.j]]
读法:矩阵第 \(j\) 列是 \(U|j\rangle\),即输入基态 \(|j\rangle\) 演化出的态矢。约定是 little-endian——q[0] 是态矢量索引的最低位。于是第 0 列 \((0.7071, 0, 0, 0.7071)^{\mathsf T} = \frac{1}{\sqrt 2}(|00\rangle + |11\rangle)\),正是 Bell 态 \(|\Phi^+\rangle\);第 1 列(输入 \(|01\rangle\),即 q0=1)给出 \(\frac{1}{\sqrt 2}(|00\rangle - |11\rangle) = |\Phi^-\rangle\)——这个矩阵把计算基映成 Bell 基。
get_matrix 显式构造 \(2^n\times 2^n\) 矩阵,只适合小线路(约 12 比特以内);含测量的线路没有酉矩阵表示,会抛出明确的错误:
m = Circuit(1)
m.h(0)
m.measure(0)
try:
m.get_matrix()
except Exception as e:
print(type(e).__name__, ":", e)
NotMatrixableError : Measured circuits have no unitary matrix
下一步¶
线路构建只是第一步,接下来把它跑起来:第 3 章:本地与含噪模拟
想画线路图、结果统计图与时间线图,去第 4 章:可视化
参数化线路是变分算法的地基,理论推导见姊妹站 VQE 教程
练习题¶
练习 1【门操作与测量】(→ 第 1 节)
把半加器改成输入 a=1、b=0(删去
c.x(1)那行),先笔算 4 个经典比特的取值与十进制计数键,再运行验证。把
c.x(0)换成c.rx(0, math.pi),预测测量计数是否改变、为什么,然后运行验证你的预测。
提示:\(RX(\pi) = -iX\),与 X 只差全局相位,测量统计完全相同。
练习 2【命名寄存器】(→ 第 2 节)
构造
qregs={"a": 2, "b": 2}:对a制备 Bell 态、对b两个比特各加一个 X 门。先手写出你预期的originir(注意谁占哪些物理索引、寄存器名去哪了),再打印对照。把第 2 节的 data 从 3 比特扩成 4 比特 GHZ(再补一个 CNOT),预测模拟只会出现哪两个计数键,再运行验证。
提示:4 比特 GHZ 只测得全 0 与全 1,加上恒为 1 的 ancilla,两个键分别是 16 与 31。
练习 3【参数化线路与绑定】(→ 第 3 节)
构造
rx(0, theta); measure(0)的单比特参数化线路,分别绑定 theta = 0、π/2、π 各模拟 100 shots,先预测三组计数的形态(全 0?五五开?全 1?)再验证。故意只绑定部分参数就去
simulate_shots,先预测会发生什么,再运行并确认报错里列出的参数名与free_parameters一致。
提示:\(RX(\pi)|0\rangle = -i|1\rangle\)(必得 1),\(RX(0)\) 是恒等(必得 0),\(\pi/2\) 介于两者之间。
练习 4【控制结构与可复用子程序】(→ 第 4 节)
把 CONTROL 块的控制位从
(0, 1)改成(0,)(并把 q0 初始化为 1),先预测导出的 OriginIR 与测量结果,再运行验证。定义三比特 GHZ 子程序
ghz(q[3]),在 6 比特寄存器上调用两次,预测计数只可能出现哪些键并模拟验证。
提示:两个独立 GHZ 各自全 0 或全 1,拼接后共 4 个等概率键。
练习 5【OriginIR 与 OpenQASM 导入导出】(→ 第 5 节)
手写一段两比特 Bell 线路的 OpenQASM 2.0 文本(含测量),用
Circuit.from_qasm导入后打印.originir,逐行对比两种文本里同一个门的写法差异(如cx与CNOT、测量的写法)。构造同时含官方门(H、CNOT)与扩展门(ISWAP 或 ECR)的线路,导出
.originir_official,数一数扩展门被分解成几个官方门、官方门是否原样保留。
提示:只有 ext 专属门才触发分解,官方门逐行照抄。
练习 6【线路统计、重映射与酉矩阵】(→ 第 6 节)
在 H–CNOT–CZ 线路中插入
c.barrier(0, 1, 2)和c.identity(0),预测depth与门操作数各怎么变,再运行验证。打印 Bell 线路酉矩阵的第 0 列与第 1 列,验证它们分别是 \(|\Phi^+\rangle\) 与 \(|\Phi^-\rangle\),并解释态矢量索引 1 为什么对应"q0=1、q1=0"。
提示:little-endian——q0 是索引的最低位,索引 1 = 二进制
01的最低位是 1。