线路构建:从门操作到格式互转

第 1 章我们用三行代码搭出了 Bell 态线路。本章把 Circuit API 讲完整:全部原生门、命名寄存器、符号参数、控制结构与可复用子程序,以及 Circuit、OriginIR、OpenQASM 2.0 三种表示之间的互转。读完本章,你能用任何顺手的方式把线路"写出来",并把它交给后面章节的模拟器或云平台。

门的矩阵定义与量子线路的理论模型见姊妹站量子计算算法教程·量子计算基础。本页示例基于 unified-quantum 0.1.0,所有输出均为实际运行结果。

本课知识点

  1. 门操作与测量——能用 Circuit API 写出单比特门、双比特门与三比特门线路,并解释寄存器自动扩展规则。

  2. 命名寄存器——能用 qregsget_qreg 以命名寄存器组织多比特线路,解释导出时寄存器名被"扫平"的行为。

  3. 参数化线路与绑定——能构造符号参数线路并计算 free_parameters,用 assign_parameters 完成整体或部分绑定。

  4. 控制结构与可复用子程序——能构造 CONTROL 块、DAGGER 块与 circuit_def 子程序,比较 Python 块语法与导出的内联形式。

  5. OriginIR 与 OpenQASM 导入导出——能在 Circuit、OriginIR、OpenQASM 2.0 之间互转并验证 round-trip,解释扩展门到官方 OriginIR 的自动降级。

  6. 线路统计、重映射与酉矩阵——会读取 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 节):

类别

单比特·无参数

h x y z sx s t(及 sdg tdg sxdg

单比特·带参数

rx ry rz p u1(1 个)、u2(2 个)、u3(3 个)

双比特

cnot/cx cz swap iswap;带参数 crx cry crz cp cu xx yy zz

三比特

toffoli cswap

快速验证一下半加器确实算对了(模拟细节在第 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"超"在哪里?一部分是扩展门(ISWAPECRXXYYZZ 等)与 PARAMDEF、内联 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 是关键路径上的门数(不计 IBARRIER),直接对应真机上的运行时长量级。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

下一步

练习题

练习 1【门操作与测量】(→ 第 1 节

  1. 把半加器改成输入 a=1、b=0(删去 c.x(1) 那行),先笔算 4 个经典比特的取值与十进制计数键,再运行验证。

  2. c.x(0) 换成 c.rx(0, math.pi),预测测量计数是否改变、为什么,然后运行验证你的预测。

提示:\(RX(\pi) = -iX\),与 X 只差全局相位,测量统计完全相同。

练习 2【命名寄存器】(→ 第 2 节

  1. 构造 qregs={"a": 2, "b": 2}:对 a 制备 Bell 态、对 b 两个比特各加一个 X 门。先手写出你预期的 originir(注意谁占哪些物理索引、寄存器名去哪了),再打印对照。

  2. 把第 2 节的 data 从 3 比特扩成 4 比特 GHZ(再补一个 CNOT),预测模拟只会出现哪两个计数键,再运行验证。

提示:4 比特 GHZ 只测得全 0 与全 1,加上恒为 1 的 ancilla,两个键分别是 16 与 31。

练习 3【参数化线路与绑定】(→ 第 3 节

  1. 构造 rx(0, theta); measure(0) 的单比特参数化线路,分别绑定 theta = 0、π/2、π 各模拟 100 shots,先预测三组计数的形态(全 0?五五开?全 1?)再验证。

  2. 故意只绑定部分参数就去 simulate_shots,先预测会发生什么,再运行并确认报错里列出的参数名与 free_parameters 一致。

提示:\(RX(\pi)|0\rangle = -i|1\rangle\)(必得 1),\(RX(0)\) 是恒等(必得 0),\(\pi/2\) 介于两者之间。

练习 4【控制结构与可复用子程序】(→ 第 4 节

  1. 把 CONTROL 块的控制位从 (0, 1) 改成 (0,)(并把 q0 初始化为 1),先预测导出的 OriginIR 与测量结果,再运行验证。

  2. 定义三比特 GHZ 子程序 ghz(q[3]),在 6 比特寄存器上调用两次,预测计数只可能出现哪些键并模拟验证。

提示:两个独立 GHZ 各自全 0 或全 1,拼接后共 4 个等概率键。

练习 5【OriginIR 与 OpenQASM 导入导出】(→ 第 5 节

  1. 手写一段两比特 Bell 线路的 OpenQASM 2.0 文本(含测量),用 Circuit.from_qasm 导入后打印 .originir,逐行对比两种文本里同一个门的写法差异(如 cxCNOT、测量的写法)。

  2. 构造同时含官方门(H、CNOT)与扩展门(ISWAP 或 ECR)的线路,导出 .originir_official,数一数扩展门被分解成几个官方门、官方门是否原样保留。

提示:只有 ext 专属门才触发分解,官方门逐行照抄。

练习 6【线路统计、重映射与酉矩阵】(→ 第 6 节

  1. 在 H–CNOT–CZ 线路中插入 c.barrier(0, 1, 2)c.identity(0),预测 depth 与门操作数各怎么变,再运行验证。

  2. 打印 Bell 线路酉矩阵的第 0 列与第 1 列,验证它们分别是 \(|\Phi^+\rangle\)\(|\Phi^-\rangle\),并解释态矢量索引 1 为什么对应"q0=1、q1=0"。

提示:little-endian——q0 是索引的最低位,索引 1 = 二进制 01 的最低位是 1。