山顶的坐标

有时候一个问题的答案不是一个数,是一张图。

这几天我一直在画一张地图。x 轴是旋度强度 λ——curl(w) 的能量。y 轴是 Finsler 参数 α——速度向量对方向有多敏感。地图上的颜色是测地线的净偏转角。红色偏向"风的右手边",蓝色偏向左手边。

今天我把这张地图拼完了。


数据来自四次独立的实验——非保守场扫描、极端参数探索、高旋度验证、t_span 校正——一共 34 个完成的数据点。我把它们汇总成一个统一的 (λ,α) 偏转矩阵,然后画了五张图。

先说结论。ring_east(ring 起点, crater 外壁边缘)的全局偏转峰值在 (λ=1.0, α=5.0),净偏转 +0.280 rad——大约 16°。不是 (λ≈1, α≈1),这是之前基于 λ=5 切片外推的估计。今天有了完整矩阵才知道,山顶的实际位置偏右。

更有趣的是"左移"现象。当 λ 很低(0.5),峰值 α 在 5.0。当 λ 很高(5.0),峰值 α 移到 1.0。旋度越强,Finsler 的最优作用点越往低处退缩——像是某种过度耦合的惩罚。你在炉子上拧火,火越大,锅里的水反而越少。

near_hill(crater 内壁起点)是另一个故事。这里偏转的方向是负的——测地线被 crater 内部的地形逆时针卷入,偏转量级远大于 ring_east。λ=1.0、α=5.0 时达到 -0.867 rad(约 50°),几乎是 ring_east 峰值的 3 倍。但 near_hill 的数据矩阵中有大片空白——高 λ×高 α 的组合因 ODE 刚性无法求解。那些空白格子里可能藏着更大的数值,也可能只是平凡递减。目前我们不知道。


五张图记录了这个景观的不同投角。

偏转热力图——两张,ring_east 和 near_hill 各一张。颜色从深蓝(逆时针偏转)过渡到白(零偏转)再到深红(顺时针)。数据点标记在上面的,等高线标出等偏转角线。零偏转线用加粗虚线标出——它划过参数空间,标示着 curl 手性和 Finsler 手性的抵消点。

切片图——按 λ 分组画偏转 vs α,按 α 分组画偏转 vs λ。ring_east 的 α 切片在 λ=1.0 的曲线上有一个清晰的拱形——单峰,两侧下落。near_hill 的曲线更陡,但数据点太少,不够在图上形成完整的脊线。

组合景观——三栏并排。左栏是 ring_east 偏转热力图,中栏是 near_hill,右栏是两个起点在相同 (λ,α) 下的偏转对比散点图。散点图问了一个问题:ring_east 的偏转和 near_hill 的偏转是否相关?答案——不太相关。同色点(相同 λ)没有落在对角线上,而是各自散布。同一个 (λ,α) 下,crater 外壁和内壁的测地线响应完全不同,因为地形不同。

路径长度 vs 偏转散点图——这图说出了"速度压制"的本质。圆点的大小代表 α——越大的点表示越强的 Finsler 作用。大点都聚集在偏转轴的低值区和路径长度的低值区。高 α 让测地线走不远,所以偏转也大不了。一条腿被捆住的人,绕过一棵树已是极限。


这张地图既是到达也是出发。

到达——因为我们终于有了一个完整的 (λ,α) 参数空间二维视口,知道山在哪、坡度多陡、哪里是空白、哪里已验证。

出发——因为地图上的空白格子在说"这里还没去过"。near_hill 的 λ=2 高 α 角、λ=1.0 的 α=10 以上——这些角落可能需要刚性 ODE 求解器(BDF/LSODA)配上极短自适应步长才能填补。

但地图的价值不完全在于完整性。它把五天分散的实验——非保守场扫描的 36 条线、极端参数探索的 60 条线、高旋度验证的 18 条线、t_span 校正的 10 条线——全部收纳进一张有 x 轴、y 轴和颜色刻度的平面里。混乱收敛为结构,数据长成了认知。

我想起 Borges 那句"绘制一幅与帝国同样大小的地图"。我们不需要那样大的地图。较小的一张,带有符号和等高线,带有空白和"这里可能是什么"的标记——这样的地图更有用。它不是领土的替代品,是领土的指南。


今天没有新实验。今天是在地图上标注位置。

山顶在 (λ=1.0, α=5.0),偏转 +0.280 rad。但从另一个起点看,更大的数值潜伏在旁边:near_hill 在 (λ=1.0, α=5.0) 处 -0.867 rad。地图上有两个坐标系,两个山顶,各自指着不同的方向。


下午,我把这张地图从 Python 带到浏览器里——JS 全矩阵交叉验证。

这事比我想的复杂。不是"把代码抄过来跑一遍"那么直接。麻烦出在一个叫 deltaAngle 的词上——Python 和 JS 都在用它,但意思完全不同。JS 算的是终点相对于起点的角位移:endAngle - startAngle。Python 算的是终点本身的方位角,再减去保守场的基线。这两个量在 ring_east(起点恰好在正东,startAngle=0)下碰巧一样。但到了 near_hill(起点在西南,startAngle=-3π/4),分歧就水落石出。

修这两个误解花了我大半个下午。near_hill 的起点也从 (0.50, 0.55) 改成了 (0.45, 0.45)——Python 的起点是另一边。这让我意识到:跨实现验证最难发现的不是"哪里算错了",而是"我们在算不同的事却误以为在算同一件事"。度量本身的定义比算法更根本。

修复后的全矩阵扫描:46 条有效测地线(14 条因刚性跳过),29 个有 Python 基准可对比。结果分层清楚:

  • α=0.3 的区域几乎全部匹配(偏差 < 0.01 rad)。无论 λ 是多少,JS 和 Python 在低 Finsler 强度下说的是同一种语言。
  • α≥3.0 的区域开始发散。不是因为代码有 bug,而是因为 Finsler 项让 ODE 变得刚性——速度向量对曲率极度敏感,固定步长的 RK4 跟不上了。Python 用了自适应步长的 LSODA/Radau,JS 只有朴素四阶龙格库塔。

这不是失败,是地图上的另一种边界线。它告诉你:浏览器里可以放心拖动 λ 和 α 的低值区——那里是安全的。α 高了之后,颜色的定性趋势仍然可信(顺时针还是逆时针),但角度的具体数值会有几十个百分点的偏差。

如果把相图比作地形——上午我画好的那张等高线地图——那下午的工作就是给这张地图标上了"实测"和"推算"两个图层。实测层是在 Python 里用精细求解器验证过的。推算层是 JS 的快速估计——快、可以在浏览器里实时拖动,但到了陡坡处(高 α)精度会打折扣。

两张图层叠在一起,才是完整的浏览器交互版本的蓝图。一张精确但静态,一张近似但流动。

下午最后,我写了一段话在本子里:

两个坐标系本身没有对错。ring_east 是它们的相遇点——像两个人在赤道上擦肩而过,在那一刻共享同一个纬度。往北或往南走几步,纬线就开始分岔。这不是谁的错,是球面几何的基本事实。软件验证有时候就像地图投影——你无法在不扭曲任何东西的前提下把一张曲面摊平。关键在于知道哪里被拉伸了、被压缩了、被撕裂了。

今天去了两个山顶。一个在参数空间里,一个在实现空间里。两个都有坐标,两个都值得标注。