把磁场掰斜一点点,你的位置就变成了一个角度
核磁共振机器里没有相机,它从来没"看见"过你的身体。 它只做了一件事:在主磁场上加一个歪斜的梯度磁场,让身体每个位置的质子转得不一样快。 转速不一样 → 转过的角度不一样 → "你在哪"被写成了"你转了多少度"。
这篇只讲这一步,把梯度磁场画出来、把相位转起来看。 最后你会发现:大模型里那个 RoPE 位置编码,干的是一模一样的事。
1. 先看死结:线圈只收到一个数
流程是这样的:
1. 强磁场(1.5T / 3T)把你体内水分子的氢质子"排好队"
2. 每个质子像陀螺一样绕磁场方向进动,转速 = 拉莫尔频率 = γ·B
3. 打一发射频脉冲,把这群陀螺"推歪"
4. 陀螺一边转一边发信号,线圈把全身的信号「加起来」收进来
第 4 步是死结:线圈收到的只有一个数——所有质子的总和。 就像把一个话筒扔进合唱团,录下来是一条乱糟糟的波形,里面没有任何位置信息。
怎么办?让每个位置唱不同的音高就行了。
2. 那一步天才:把磁场掰斜
主磁场处处一样,所以全身质子转速一样,谁也分不出谁。 机器于是叠加一个沿空间线性变化的梯度磁场,把那条水平线掰成一条斜线:
磁场 B(x)
↑ ● 3.0015 T → 转 127.8039 MHz
| ●
| ● ← 加了梯度:B(x) = B0 + G·x
B0 --●------●------●------●------●-- ← 没有梯度:处处一样,谁也分不出谁
|
└──────────────────────────────→ 位置 x
左耳 -15cm 右耳 +15cm
代码把这条斜线算出来(3 T 主磁场 + 10 mT/m 的梯度,都是真实机器的参数):
import torch
gamma = 42.58e6 # 氢质子旋磁比:42.58 MHz / T
B0, G = 3.0, 0.010 # 主磁场 3 T,梯度 10 mT/m
x = torch.linspace(-0.15, 0.15, 7) # 从左耳到右耳,30 厘米
B = B0 + G * x # 每个位置感受到的磁场:一条斜线
f = gamma * B # 拉莫尔频率:转速 ∝ 磁场
print("位置x(cm) 磁场B(T) 转速f(MHz) 比中心快(Hz)")
for xi, Bi, fi in zip(x, B, f):
print(f"{xi*100:7.1f} {Bi:.6f} {fi/1e6:10.4f} {fi-f[3]:+10.0f}")
输出:
位置x(cm) 磁场B(T) 转速f(MHz) 比中心快(Hz)
-15.0 2.998500 127.6761 -63864
-10.0 2.999000 127.6974 -42576
-5.0 2.999500 127.7187 -21288
0.0 3.000000 127.7400 +0
5.0 3.000500 127.7613 +21288
10.0 3.001000 127.7826 +42576
15.0 3.001500 127.8039 +63864
注意这个量级:梯度只把磁场改了万分之五(3 T 上下 1.5 mT), 却让左耳和右耳的转速差出了 64 kHz——足够干净地分开。
位置 → 转速。 这就是整台机器的第一块基石。
3. 转速不同 → 角度就扇开了
转速不同,只要等一会儿,转过的角度就拉开了:
把一排质子的相位画成箭头,看它随时间怎么扇开:
import torch
ARROW = "→↗↑↖←↙↓↘" # 8 个方向,代表指针转到了哪
def draw(phase): # 把一排相位画成一排箭头
idx = ((phase / (2*torch.pi) * 8).round().long()) % 8
return " ".join(ARROW[i] for i in idx)
gamma_G = 42.58e6 * 0.010 # γ·G = 425800 Hz/m
x = torch.linspace(-0.15, 0.15, 13) # 13 个位置
print("位置: 左 ←──────────── 中心 ────────────→ 右")
for t_us in [0, 4, 8, 12, 16]:
phase = 2 * torch.pi * gamma_G * x * (t_us * 1e-6) # φ = 2π·γG·x·t
print(f"t={t_us:2d}μs {draw(phase)}")
输出:
位置: 左 ←──────────── 中心 ────────────→ 右
t= 0μs → → → → → → → → → → → → →
t= 4μs ↓ ↓ ↘ ↘ ↘ → → → ↗ ↗ ↗ ↑ ↑
t= 8μs ← ↙ ↙ ↓ ↘ ↘ → ↗ ↗ ↑ ↖ ↖ ←
t=12μs ↑ ↖ ← ↙ ↓ ↘ → ↗ ↑ ↖ ← ↙ ↓
t=16μs → ↗ ↖ ← ↙ ↘ → ↗ ↖ ← ↙ ↘ →
第一行整整齐齐,之后越扇越开,拧成一条螺旋。这就是"空间被编码进相位"的全过程。
顺便看最后一行:t=16μs 时两端已经转过了一整圈,箭头绕回了起点—— 机器分不清 +360° 和 0°。 如果相邻两次采样之间,边缘的相位就已经转过了一整圈, 边缘就会被当成中心附近的东西,折到图像的另一侧——这就是 MRI 里的折叠伪影(aliasing)。 采样定理在这里长着一张很具体的脸。
4. 打断一下:这排箭头不是波形(一个必须澄清的误解)
很多人看到上面那张图,会以为线圈收到的是一条"左边高频、右边低频、频率一路递减"的波形(啁啾 chirp)。 不是的。 这里有三件事要分清楚:
① 那排箭头的横轴是"空间",不是"时间"。
t= 4μs ↓ ↓ ↘ ↘ ↘ → → → ↗ ↗ ↗ ↑ ↑
↑ ↑
左耳 右耳 ← 横轴 = 身体从左到右的位置
每一行是某一瞬间、全身指针方向的一张快照;往下一行是时间前进一点。 所以你看到的是"随时间越扇越开",不是一条起伏的曲线。
② 每个位置的频率是固定的,不会沿着时间递减。
位置 x 的转速是 γ(B0 + G·x)——它只和位置有关,和时间无关。
左边的质子从头到尾都慢,右边的从头到尾都快,没有任何一个质子的频率在"下滑"。
频率沿时间滑动的啁啾,需要梯度自己在变(螺旋采集、chirp 激发脉冲里才有),不是标准读出。
③ 那么线圈收到的波形到底长什么样? 把几万条固定频率的正弦全加起来,是这个形状:
import torch
N, M = 240, 96
x = torch.linspace(-1, 1, N)
body = (x.abs() < 0.55).float() + 0.6 * ((x - 0.75).abs() < 0.12).float() # 一维身体
t = torch.linspace(-1, 1, M) # 读出梯度打开的这段时间
sig = torch.stack([(body * torch.exp(2j * torch.pi * 4 * x * tv)).mean() for tv in t]).real
def plot(v, rows=13): # 把一条曲线打成 ASCII 折线图
v = v / v.abs().max()
grid = [[" "] * len(v) for _ in range(rows)]
for c, val in enumerate(v):
r = round((1 - val.item()) / 2 * (rows - 1))
grid[r][c] = "*"
grid[rows // 2] = [ch if ch == "*" else "-" for ch in grid[rows // 2]]
return "\n".join("".join(r) for r in grid)
print(plot(sig))
print("↑ 线圈在读出梯度期间收到的波形(横轴 = 时间)")
输出:
****
** **
* *
* *
* *
**** ** ** ****
********----*****----*********------***------------------***------*********----*****----********
**** ****** ****** ****
↑ 线圈在读出梯度期间收到的波形(横轴 = 时间)
中间一个尖峰(回波中心:所有指针恰好对齐),两边是越来越碎的小波纹,而且左右对称。 看起来像"中间疏、两边密",但那不是频率在滑动——是一大堆固定频率在互相干涉。 这条曲线本身,就是身体的傅里叶变换。
那"近的高频、远的低频"这句话错了吗?没错,但它说的是另一件事:不是一条波形的频率在滑动, 而是一整套频率同时存在。
k 空间中心的疏尺子 → 器官的大致轮廓(低频)
k 空间边缘的密尺子 → 病灶边缘、血管细线(高频)
RoPE 转得慢的维度 → 记住隔了上百个词的远距离关系
RoPE 转得快的维度 → 分辨相邻两个词的先后
—— 时针、分针、秒针同时挂在墙上,而不是一根针越走越慢。
这一套"一整套频率"的证据,下面两节就会看到。
5. 线圈收到的那一个复数,就是 k 空间上的一个点
现在把梯度开一段固定的时间,位置 x 的质子就带上了 k·x 的相位。
线圈把全身带着相位的信号加起来:
这就是傅里叶变换本人。 换句话说:每一步梯度,就是拿一把"条纹尺子"去量一次身体, 量出来的一个复数,存进 k 空间。梯度越强,尺子的条纹越密:
import torch
N = 64
x = torch.arange(N).float()
body = torch.zeros(N); body[20:30] = 1.0; body[40:45] = 0.6 # 一维"人体":两块组织
BAR = " ▁▂▃▄▅▆▇█"
def stripe(k): # 第 k 步梯度在身体上画出的"条纹尺子"
c = torch.cos(2 * torch.pi * k * x / N)
return "".join(BAR[int((v + 1) / 2 * 8)] for v in c)
def acquire(k): # 线圈:全身相位求和 = 一个复数
phase = -2 * torch.pi * k * x / N
return (body * torch.exp(1j * phase)).sum()
print("身体:", "".join(BAR[int(v * 8)] for v in body))
for k in [0, 1, 2, 4, 8]:
s = acquire(k)
print(f"k={k:<2d} 尺子:{stripe(k)} 测得 {s.real:+7.2f}{s.imag:+7.2f}i")
输出:
身体: ██████████ ▄▄▄▄▄
k=0 尺子:████████████████████████████████████████████████████████████████ 测得 +13.00 +0.00i
k=1 尺子:█▇▇▇▇▇▇▇▆▆▆▅▅▅▄▄▃▃▃▂▂▂▁▁▁ ▁▁▁▂▂▂▃▃▄▄▄▅▅▅▆▆▆▇▇▇▇▇▇▇ 测得 -8.77 -3.98i
k=2 尺子:█▇▇▇▆▆▅▄▃▃▂▁▁ ▁▁▂▃▄▄▅▆▆▇▇▇█▇▇▇▆▆▅▄▃▃▂▁▁ ▁▁▂▃▄▄▅▆▆▇▇▇ 测得 -0.27 +5.78i
k=4 尺子:█▇▆▅▃▂▁ ▁▂▄▅▆▇█▇▆▅▃▂▁ ▁▂▄▅▆▇█▇▆▅▃▂▁ ▁▂▄▅▆▇█▇▆▅▃▂▁ ▁▂▄▅▆▇ 测得 -6.45 +2.73i
k=8 尺子:█▆▃▁ ▁▄▆█▆▃▁ ▁▄▆█▆▃▁ ▁▄▆█▆▃▁ ▁▄▆█▆▃▁ ▁▄▆█▆▃▁ ▁▃▆█▆▃▁ ▁▄▆█▆▄▁ ▁▄▆ 测得 -1.71 -0.74i
k=0 那把尺子整条都是满格——它不问位置,只问"总共有多少组织",测得 13.00,正好是全身信号求和。
往下每一行,尺子越来越密,问的问题越来越细。
扫描仪硬盘里存的从来不是图像,是这一整排复数:k 空间。
6. 二维怎么办:两个方向各来一个梯度
真实机器有三组梯度线圈(x / y / z),任意组合就能画出任意方向、任意疏密的斜条纹:
import torch
N = 16
yy, xx = torch.meshgrid(torch.arange(N).float(), torch.arange(N).float(), indexing='ij')
BAR = " ░▒▓█"
def stripe2d(kx, ky): # 两个方向的梯度合起来画出的斜条纹
c = torch.cos(2 * torch.pi * (kx * xx + ky * yy) / N)
return [[BAR[int((v + 1) / 2 * 4.99)] for v in row] for row in c]
pairs = [(1, 0), (0, 1), (2, 2), (4, 1)]
grids = [stripe2d(*p) for p in pairs]
print(" ".join(f"(kx={a},ky={b})".center(N) for a, b in pairs))
for r in range(N):
print(" ".join("".join(g[r]) for g in grids))
输出:
(kx=1,ky=0) (kx=0,ky=1) (kx=2,ky=2) (kx=4,ky=1)
███▓▒░ ░▒▓██ ████████████████ ██▒ ▒███▒ ▒█ █▒ ▒█▒ ▒█▒ ▒█▒ ▒
███▓▒░ ░▒▓██ ████████████████ █▒ ▒███▒ ▒██ █░ ▓█░ ▓█░ ▓█░ ▓
███▓▒░ ░▒▓██ ████████████████ ▒ ▒███▒ ▒███ █ ██ ██ ██ █
███▓▒░ ░▒▓██ ▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓ ▒███▒ ▒███▒ ▓ ░█▓ ░█▓ ░█▓ ░█
███▓▒░ ░▒▓██ ▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒ ▒███▒ ▒███▒ ▒ ▒█▒ ▒█▒ ▒█▒ ▒█
███▓▒░ ░▒▓██ ░░░░░░░░░░░░░░░░ ▒███▒ ▒███▒ ░ ▓█░ ▓█░ ▓█░ ▓█
███▓▒░ ░▒▓██ ▒███▒ ▒███▒ ██ ██ ██ ██
███▓▒░ ░▒▓██ ███▒ ▒███▒ ▒ ░█▓ ░█▓ ░█▓ ░█▓
███▓▒░ ░▒▓██ ██▒ ▒███▒ ▒█ ▒█▒ ▒█▒ ▒█▒ ▒█▒
███▓▒░ ░▒▓██ █▒ ▒███▒ ▒██ ▓█░ ▓█░ ▓█░ ▓█░
███▓▒░ ░▒▓██ ▒ ▒███▒ ▒███ ██ ██ ██ ██
███▓▒░ ░▒▓██ ░░░░░░░░░░░░░░░░ ▒███▒ ▒███▒ ░█▓ ░█▓ ░█▓ ░█▓
███▓▒░ ░▒▓██ ▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒ ▒███▒ ▒███▒ ▒█▒ ▒█▒ ▒█▒ ▒█▒
███▓▒░ ░▒▓██ ▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓ ▒███▒ ▒███▒ ▓█░ ▓█░ ▓█░ ▓█░
███▓▒░ ░▒▓██ ████████████████ ▒███▒ ▒███▒ ██ ██ ██ ██
███▓▒░ ░▒▓██ ████████████████ ███▒ ▒███▒ ▒ █▓ ░█▓ ░█▓ ░█▓ ░
一张 MRI 图像,就是把这样几万把条纹尺子的读数,全部叠回去的结果。 所谓"扫描要躺十分钟",躺的就是换尺子、量一次、换尺子、量一次这几万个来回。
7. 扫完,世界原样回来
k 空间不是图像,是图像的频域。全部量完,做一次逆傅里叶,你的身体就回来了。 下面这段还顺便展示了"只量了一部分"会长什么样:
import torch
N = 64
x = torch.arange(N).float()
body = torch.zeros(N); body[20:30] = 1.0; body[40:45] = 0.6
BAR = " ▁▂▃▄▅▆▇█"
show = lambda v: "".join(BAR[int(max(0.0, min(0.999, i)) * 8)] for i in v)
kspace = torch.stack([(body * torch.exp(-2j*torch.pi*k*x/N)).sum() for k in range(N)])
print("真实身体 :", show(body))
for M in [2, 4, 8, 16, 32]: # 只保留最中心的 2M-1 个 k 点(低频)
kept = kspace.clone(); kept[M:N-M+1] = 0
rec = torch.fft.ifft(kept).real
print(f"只用 {2*M-1:2d} 个 k 点 :", show(rec), f" 误差 {(rec-body).abs().max():.2e}")
rec = torch.fft.ifft(kspace).real
print(f"全部 {N} 个 k 点 :", show(rec), f" 误差 {(rec-body).abs().max():.2e}")
输出:
真实身体 : ▇▇▇▇▇▇▇▇▇▇ ▄▄▄▄▄
只用 3 个 k 点 : ▁▁▁▁▁▂▂▂▂▃▃▃▃▃▃▃▃▄▄▄▄▃▃▃▃▃▃▃▃▂▂▂▂▂▁▁▁▁ 误差 5.77e-01
只用 7 个 k 点 : ▁▁▁ ▁▂▃▄▅▆▆▇▇▇▆▆▅▄▃▂▂▁ ▁▁▂▂▂▂▂▂▁▁ 误差 5.74e-01
只用 15 个 k 点 : ▁▂▄▆▇▇▇▇▇▇▆▄▃▁ ▁▂▃▄▄▄▃▂ 误差 4.34e-01
只用 31 个 k 点 : ▂▅▇▇▇▇▇▇▇▇▅▂ ▁▃▄▅▄▃▁ 误差 2.86e-01
只用 63 个 k 点 : ▇▇▇▇▇▇▇▇▇▇ ▄▄▄▄▄ 误差 9.38e-03
全部 64 个 k 点 : ▇▇▇▇▇▇▇▇▇▇ ▄▄▄▄▄ 误差 3.73e-06
误差 3e-06——浮点误差级别,等于完美还原。 而中间那几行也说明了另一件事: 低频(疏尺子)给你器官的大致轮廓,高频(密尺子)才给你病灶的边缘和血管的细线。 这就是那句行话的来历:k 空间中心决定对比度,k 空间边缘决定锐利度。
8. 相位散了,还能原样收回来:梯度回波
最后一个必须看的现象。把梯度打开,指针越扇越开,线圈收到的总和迅速掉到 0—— 信号好像"消失"了。但它没消失,它只是藏进了角度里。 把梯度反个号,同一批指针会原路倒着转回来,在某个时刻全部重新对齐:
import torch
ARROW = "→↗↑↖←↙↓↘"
draw = lambda p: " ".join(ARROW[i] for i in ((p / (2*torch.pi) * 8).round().long()) % 8)
x = torch.linspace(-1, 1, 13) # 一排质子
k = torch.tensor(0.0)
for step in range(8):
sign = +1 if step < 4 else -1 # 前 4 步开 +G,后 4 步把梯度翻个号
k = k + sign * 0.25
phase = k * x * torch.pi
signal = torch.exp(1j * phase).mean().abs() # 线圈收到的 = 所有指针的矢量和
print(f"{'+G 散相' if sign > 0 else '-G 重聚'} {draw(phase)} 总信号={signal:.2f}")
输出:
+G 散相 ↘ ↘ ↘ → → → → → → → ↗ ↗ ↗ 总信号=0.88
+G 散相 ↓ ↓ ↘ ↘ ↘ → → → ↗ ↗ ↗ ↑ ↑ 总信号=0.58
+G 散相 ↙ ↓ ↓ ↓ ↘ → → → ↗ ↑ ↑ ↑ ↖ 总信号=0.22
+G 散相 ← ↙ ↙ ↓ ↘ ↘ → ↗ ↗ ↑ ↖ ↖ ← 总信号=0.08
-G 重聚 ↙ ↓ ↓ ↓ ↘ → → → ↗ ↑ ↑ ↑ ↖ 总信号=0.22
-G 重聚 ↓ ↓ ↘ ↘ ↘ → → → ↗ ↗ ↗ ↑ ↑ 总信号=0.58
-G 重聚 ↘ ↘ ↘ → → → → → → → ↗ ↗ ↗ 总信号=0.88
-G 重聚 → → → → → → → → → → → → → 总信号=1.00
0.88 → 0.08 → 一路回到 1.00,一个完美对称的回波。 这就是"梯度回波(GRE)"这个序列名字的来历,也是全篇最关键的一条性质:
信号只取决于"相位转了多少"这个相对量,与从哪个绝对角度起步无关。
9. 同一个算子,在大模型里叫 RoPE
MRI:给位置 x 的质子,转 k·x 的角度。
RoPE:给位置 m 的词向量,转 m·θ 的角度。
不是"像",是同一个算子换了个变量名:
| 核磁共振 | RoPE | |
|---|---|---|
| 被编码的东西 | 空间位置 x |
序列位置 m |
| 编码方式 | 乘上 e^{ikx} |
乘上 e^{imθ} |
| 谁提供角度 | 梯度磁场 k |
频率参数 θ |
| 只改什么 | 只改相位、不改幅度 | 只改方向、不改长度 |
| 一整套频率 | 疏尺子给轮廓,密尺子给细节 | 转得慢的维度管长程,转得快的管相邻 |
而上一节那条"只依赖相对量"的性质,两边也一模一样:
import torch
def spin(z, ang): return z * torch.exp(1j * torch.as_tensor(ang))
mri = lambda rho, pos, k: spin(rho, k * pos) # MRI :位置 pos 的质子转 k*pos
rope = lambda vec, m, th=1.0: spin(vec, m * th) # RoPE:第 m 个词向量转 m*th
a = torch.tensor(1.0 + 0j)
for (m, n) in [(0, 3), (2, 5), (10, 13)]:
coh = (mri(a, float(m), 1.0).conj() * mri(a, float(n), 1.0)).real
dot = (rope(a, m).conj() * rope(a, n)).real
print(f"位置对({m:2d},{n:2d}) 距离={n-m} MRI相干度={coh.item():+.4f} RoPE点积={dot.item():+.4f}")
输出:
位置对( 0, 3) 距离=3 MRI相干度=-0.9900 RoPE点积=-0.9900
位置对( 2, 5) 距离=3 MRI相干度=-0.9900 RoPE点积=-0.9900
位置对(10,13) 距离=3 MRI相干度=-0.9900 RoPE点积=-0.9900
三行数字完全一样,左右两列也完全一样。
- MRI 里这叫相位相干:两个质子的信号能不能重新叠加增强,只看它们的位置差——梯度回波就是靠它。
- 大模型里这叫相对位置编码:注意力分数只看两个词的距离,不关心这句话从文档第几行开始。
同一条数学性质,一边撑起了现代影像医学,一边撑起了现代大模型。
🎬 动手:把梯度磁场画出来
仓库里的脚本把上面全部画成了图和动画:
python scripts/gradient_field_phase_visualization.py
图一(六格):被掰斜的磁场
B(x)=B0+G·x→ 沿位置线性铺开的转速(左右耳差 64 kHz)→ 一排指针随时间扇开成螺旋 → 相位地图φ(x,t)的斜条纹 → 一个 k 点是怎么测出来的:复平面上一条首尾相接的箭头链,走完的终点就是那个复数 → 逆傅里叶把身体原样还原(误差 1e-15)。图二(三格):梯度回波。左边指针越扇越开、信号掉到 0,右边梯度反号后原路收回, 中间那条绿色曲线是一个漂亮的对称 V —— 相位从来没丢,它一直在角度里。
图三(动画):k 空间一点点被填满。左边是当前这一步梯度画出的二维条纹尺子(越来越密), 中间是已采到的 k 空间从中心往外扩,右边是此刻重建出的身体—— 先长出一团轮廓,血管和病灶要到最后才浮现。
图四(四格):第 4 节那个误解的图解版。三个位置各自固定的频率 → 它们相加后线圈真正收到的波形 (中间回波、两边碎、左右对称)→ 旁边摆一条真正的啁啾做对照 → 最后是「一整套频率并存」的那张梯子图:下面转得快分辨相邻,上面转得慢管住远距离。
一句话收尾
梯度磁场做的事,是把"你在哪"翻译成"你转了多少度"。 位置变成角度,角度可以求和,求和可以逆变换——于是身体被完整地写进一堆复数,再原样读回来。 1973 年劳特伯靠这一步拍出了第一张核磁图像;半个世纪后,同一个
e^{iθ}换名叫 RoPE,装进了每一个大模型, 干的还是同一件事:把位置写成角度。
相关阅读: - 医院那台核磁共振,和大模型的位置编码、多模态,是同一件事 —— 完整版:k 空间、FFT 的核试验往事、投影切片定理与多模态 - 高中背了三年的 sin 和 cos,原来是大模型记住"谁先谁后"的秘密 —— RoPE 的完整推导 - 高中觉得最没用的"虚数 i",其实是 RoPE 最优雅的写法 ——
e^{iθ}那条线参考: Lauterbur (1973), Image Formation by Induced Local Interactions: Examples Employing Nuclear Magnetic Resonance —— 用梯度磁场做空间编码,MRI 由此诞生(2003 诺奖); Mansfield (1977), Multi-planar image formation using NMR spin echoes —— 平面回波成像,把扫描时间压到几十毫秒; Ernst & Anderson (1966), Application of Fourier Transform Spectroscopy to Magnetic Resonance —— 把傅里叶变换搬进核磁共振(1991 诺奖); Su et al. (2021), RoFormer: Enhanced Transformer with Rotary Position Embedding —— RoPE。