16 KiB
Fortran → Rust 重构问题与解决方案
记录重构过程中遇到的问题,避免重复踩坑。
1. Fortran 1-indexed 转 Rust 0-indexed
问题
Fortran 数组从 1 开始索引,Rust 从 0 开始。
解决方案
// 数组访问
arr(i) → arr[i-1]
// 循环范围
DO I=1,N → for i in 0..n
// 边界条件 (locate, ylintp 等)
// Fortran: jl=0 表示"在第一个有效索引之前"
// Rust: jl=0 就是第一个有效索引,无需调整
示例 (ylintp.f)
! Fortran: jl=0 需要调整
IF (J.EQ.0) J = J+1 ! 调整到 J=1
// Rust: jl=0 就是第一个有效索引,直接使用
// 删除 IF (J.EQ.0) 的调整逻辑
2. Fortran 表达式解析错误
问题
XL=-LOG(X) 被误解为 (-x).ln() 而不是 -x.ln()
示例 (erfcin.f)
XL=-LOG(X) ! 意思是: XL = -ln(X)
// 错误:
let xl = (-x).ln(); // ln(-x) = NaN for x>0
// 正确:
let xl = -x.ln(); // -ln(x)
教训
Fortran 中 -LOG(X) 是 -(LOG(X)),不是 LOG(-X)
3. powi 类型歧义
问题
(z1 - z2).powi(2) 编译错误,类型不明确
解决方案
// 错误:
(z1 - z2).powi(2) // 编译器无法推断类型
// 方案1: 显式乘法 (推荐)
(z1 - z2) * (z1 - z2)
// 方案2: 显式类型标注
((z1 - z2): f64).powi(2)
4. 多项式近似精度
问题
eint 函数 Rust 与 Fortran 结果差异 ~1.3e-8
原因
Abramowitz-Stegun 多项式近似本身精度有限
解决方案
放宽 epsilon 到 1e-7
// 简单函数: epsilon = 1e-10
// 多项式近似: epsilon = 1e-7
assert_relative_eq!(e1, exp_e1, epsilon = 1e-7);
5. 数组索引越界
问题
locate 函数无限循环
原因
Fortran 中 ju=N+1 (越界值),Rust 直接用导致越界访问
解决方案
使用 i64 允许负值,或调整边界逻辑
// 使用 i64 支持 jl=-1
let mut jl: i64 = -1;
let mut ju: i64 = n as i64;
6. voigte 索引偏移
问题
voigte 返回负值
原因
Fortran m 值是 1-indexed,Rust 中直接用导致索引偏移
解决方案
// Fortran: m=6 表示第6个元素
// Rust: 需要用 m-1
let (m, quo) = if v < 2.4 {
(5, 1.0) // Fortran m=6 → 0-indexed m=5
} else {
(10, 1.0) // Fortran m=11 → 0-indexed m=10
};
7. 条件分支遗漏
问题
voigte 某些参数组合返回错误值
原因
漏掉了 Fortran 中的嵌套条件判断
教训
仔细对比 Fortran 的所有分支,特别是嵌套 IF
8. COMMON 依赖误判
问题
某些标记为"纯函数"的文件实际有 COMMON 依赖
已确认有依赖的文件
gamsp.f - 使用 VOIPAR COMMON
sgmer1.f - 使用 COMMON
sgmerd.f - 使用 COMMON
cross.f - 使用 COMMON
gfree1.f - 使用 COMMON
gfree0.f - 使用 BASICS, MODELQ COMMON
gfreed.f - 使用 BASICS, MODELQ COMMON
wn.f - 使用 BASICS COMMON
crossd.f - 使用 BASICS, ATOMIC, MODELQ COMMON
dopgam.f - 使用 BASICS, ATOMIC, MODELQ COMMON
verner.f - 使用 BASICS, ATOMIC COMMON
sbfhe1.f - 使用 BASICS, ATOMIC COMMON
rayini.f - 有文件 I/O + COMMON
解决方案
重构前检查所有 INCLUDE 语句,不仅是 IMPLIC.FOR
9. Clenshaw 求和整数溢出
问题
collhe 中递减循环溢出
原因
ir 和 jj 递减到负数时,无符号整数溢出
解决方案
// 错误: 用 usize
let mut ir: usize = ...;
ir -= 1; // 当 ir=0 时溢出!
// 正确: 用有符号整数
let mut ir: isize = ...;
ir -= 1; // 正常变为 -1
10. 索引计算中间结果溢出
问题
collhe 的索引计算 ((iu+1)^2 - 3*(iu+1) + 4)/2 溢出
原因
中间结果超出 usize 范围
解决方案
// 使用 i32 避免中间结果溢出
let idx = (((iu + 1) * (iu + 1) - 3 * (iu + 1) + 4) / 2 - 1) as usize;
11. 数组变量命名冲突
问题
不同 Fortran 文件中同名变量冲突
原因
Fortran 文件局部作用域,Rust 需要唯一名称
解决方案
修改 extract_fortran_data.py:所有变量添加文件名前缀
// 旧: ADI, BDENS (冲突)
// 新: DIELRC_ADI, DIELRC_BDENS
pub const DIELRC_ADI: [f64; 18] = [...];
12. 2D 数组存储顺序
问题
Fortran 列优先,Rust 行优先
解决方案
脚本自动转置,访问方式改变
// Fortran: ARR(j, i) 列优先
// Rust: ARR[i][j] 行优先 (已转置)
13. DATA 语句数值解析
问题
负数和科学计数法中有空格
示例
DATA A / - 14.2, 1.48 D-2 /
解决方案
# 修复负数空格
val = re.sub(r'-\s+(\d)', r'-\1', val)
# 修复科学计数法空格
val = re.sub(r'(\d)\s+([eEdD])', r'\1\2', val)
14. BPOPF 测试失败 - 温度列与主循环列重叠
问题
BPOPF 函数测试中,B[20][21] 的值是 -0.12 而不是预期的 -0.02。
原因
测试参数设置导致温度列 (NRE) 与主循环的某一列 (NSE+II) 重叠:
- 主循环更新列 NSE+1 到 NSE+NLVEXP (Fortran 1-indexed: 21-23)
- 温度列是 NRE (Fortran 1-indexed: 22)
- 当 II=2 时,列 NSE+II = 22 = NRE,两者写入同一列
解决方案
测试参数设置时避免列重叠:
let params = BpopfParams {
nfreqe: 10,
inse: 11, // NSE = 10 + 11 - 1 = 20
inre: 0, // 不更新温度列,避免与主循环列重叠
inpc: 0, // 不更新电子密度列
// ...
};
Fortran 索引转换注意事项
NSE = NFREQE + INSE - 1(Fortran 起始列索引)NRE = NFREQE + INRE(温度列,1-indexed)- Rust 主循环:
col = nse + ii(nse 已经是 0-indexed 起始点) - Rust 温度列:
col = (nre - 1) as usize(需要减 1 转换为 0-indexed)
调试技巧
- 添加
#[cfg(test)]条件编译的调试输出 - 打印 ESEMAT 矩阵验证是否为单位矩阵
- 打印每次 B 矩阵更新的行、列、值
- 检查温度列和电子密度列是否与主循环列重叠
15. RAYLEIGH 氦散射截面公式错误
问题
Rayleigh 氦散射截面计算结果不正确。
原因
公式理解错误:
- 错误:
5.484e-14 / x2 * (...)其中x2 = 1/x² - 正确:
5.484e-14 / (x * x) * (...)
解决方案
// 错误写法
let x2 = 1.0 / (x * x);
raysct.rche[ik] = 5.484e-14 / x2 * (...); // 这等于 5.484e-14 * x²
// 正确写法
raysct.rche[ik] = 5.484e-14 / (x * x) * (...); // 这等于 5.484e-14 / x²
16. ALIFR3 - 大型 COMMON 块函数重构策略
问题
ALIFR3 函数依赖大量 COMMON 块变量(来自 FIXALP, MODELQ, ALIPAR 等)。
解决方案
创建综合的输入结构体,使用生命周期参数引用数据:
/// 输入状态结构体 (使用生命周期引用数据)
pub struct Alifr3ModelState<'a> {
// 深度相关 (MDEPTH)
pub elec: &'a [f64],
pub densi: &'a [f64],
// ... 其他字段
// 输出累积变量 (可变引用)
pub heit: &'a mut [f64],
pub hein: &'a mut [f64],
// ... 其他字段
}
多分支条件处理
Fortran 使用 GOTO 分支,Rust 使用 if-else:
IF(ILMCOR.NE.3) GO TO 199
! ... ILMCOR==3 的代码
199 IF(ILASCT.NE.0) GO TO 299
! ... ILASCT==0 的代码
299 ! ... ILASCT!=0 的代码
if params.ilmcor == 3 {
// ... ILMCOR==3 的代码
return;
}
if params.ilasct == 0 {
// ... ILASCT==0 的代码
return;
}
// ... ILASCT!=0 的代码
注意事项
- ABST 的计算方式在不同分支中不同:
- ILMCOR==3:
ABST = UN/ABSO1(ID) - ILASCT==0:
ABST = UN/(ABSO1(ID)-ELSCAT(ID)) - ILASCT!=0:
ABST = UN/ABSO1(ID)
- ILMCOR==3:
- DSFN1 的计算在不同分支也有差异(是否包含 SIGEC 项)
17. ALIFR6 - COMMON 块结构体命名冲突
问题
添加新的 COMMON 块结构体时,编译器报错 ambiguous glob re-exports: the name 'Comptn'。
原因
Comptn 结构体同时存在于:
src/state/config.rs- Compton 散射角度参数(已有)src/state/model.rs- 新添加的重复定义
两个模块都通过 pub use * 重新导出,导致名称冲突。
解决方案
添加新结构体前,先搜索是否已存在:
grep -r "pub struct Comptn" src/state/
如果已存在,扩展现有结构体而不是创建新的。
18. ALIFR6 - 循环中可变变量重新赋值
问题
在循环中重新赋值 s0p = s0p_new 报错 cannot assign twice to immutable variable。
原因
变量在 if-else 块中首次定义后,无法在循环中重新赋值。
解决方案
// 错误:不可变变量
let (s0p, dsft1p, dsfn1p) = if ... { ... };
// 正确:声明为可变
let (mut s0p, mut dsft1p, mut dsfn1p) = if ... { ... };
19. ALIFR6/ALIFR3 - 大型函数三段式处理模式
模式
ALIFR6 和 ALIFR3 类函数有三个独立处理部分:
-
第一个深度点 (ID=1)
- 特殊边界条件
- DSFT1M/DSFN1M 初始化为 0
-
深度循环 (ID=2 到 ND-1)
- 保存前一步值 (DSFTMM, DSFNMM)
- 更新当前值
- 计算下一步值 (DSFT1P, DSFN1P)
-
最深点 (ID=ND)
- IBC 下边界条件处理
- 可能的额外导数 (DSFT1D, DSFN1D)
IBC 下边界条件
| IBC | 说明 |
|---|---|
| 0 | 无特殊处理 |
| 1 | 简单 Planck 函数修正 |
| 2 | 改进边界条件 |
| 3 | 完整边界条件 + DSFT1D/DSFN1D |
20. ALIFR6 - 三对角 Lambda* 算子
特点
ALIFR6 与 ALIFR3 的主要区别是额外计算三对角算子:
- 下对角线: AREIT, AREIN, AREIP (使用 ALIM1)
- 上对角线: CREIT, CREIN, CREIP (使用 ALIP1)
- 对角线: REIT, REIN, REIP (使用 ALI1)
IFALI >= 7 额外计算
- HEITP, HEINP, HEIPP (He 相关)
- REDTP, REDNP, REDPP (Red 相关)
- EHET, EHEN, EHEP (Ehe 相关)
- ERET, EREN, EREP (Ere 相关)
快速检查清单
重构新函数前检查:
- 是否有 INCLUDE 语句 (除 IMPLIC.FOR 外)
- 是否使用 COMMON 块
- COMMON 块结构体是否已存在于其他模块 (grep 检查)
- 是否有文件 I/O (OPEN, READ, WRITE)
- 数组索引是否需要 -1 调整
- 循环变量是否可能变负 (用 isize/i32)
- 多项式近似精度是否需要放宽
- 是否有嵌套 IF 分支遗漏
- 矩阵列索引是否可能与温度/密度列重叠
- 公式中
1/x和x的顺序是否正确 - 大型 COMMON 块函数是否需要创建综合输入结构体
- 多分支 GOTO 是否正确转换为 if-else
- 循环中重新赋值的变量是否声明为 mut
- 变量字段在哪个 COMMON 块 → 对应哪个 Rust 结构体 (查
.FOR原文) - 测试初始化用
ModelState::new()而非::default() - 二维数组内层维度是否是深度/角度/频率 (不要默认 MDEPTH)
- loop 内部积分变量是否在 loop 外声明以便 break 后使用
21. COMMON 块变量所属结构体查找
问题
Fortran COMMON 块变量(如 IDISK、IBC、ICHCOO、IJORIG)应放在哪个 Rust 结构体中?
解决方案
通过查阅 .FOR 中的 COMMON 定义来确认:
IDISK、IBC→BASICS.FOR的COMMON/BASNUM/→config.basnumICHCOO、ICOMST、ICOMDE→BASICS.FOR的common/compti/→config.comptiIJORIG→BASICS.FOR的common/comptn/→config.comptn.ijorigIWINBL→MODELQ.FOR的COMMON/WINDBL/→model.windbl.iwinblIFZ0→BASICS.FOR的COMMON/CENTRL/→config.centrl.ifz0
# 快速查找变量所在 COMMON 块
grep -i "变量名" tlusty/extracted/*.FOR
教训
编译报错 no field 'xxx' on type 'Accel' 时,不要盲目添加字段——先查原始 .FOR 确认 COMMON 归属。
22. 循环外变量对 break 后可见
问题
ah、qq0、u0 在 ALI 循环内定义,循环 break 后用于写回结果时编译报 cannot find value。
原因
Rust 变量作用域严格,loop {} 块内 let 声明的变量在块外不可见。
解决方案
// 正确:循环前提前声明,循环内用 _inner 临时变量
let mut ah = 0.0f64;
let mut qq0 = 0.0f64;
loop {
let mut ah_inner = 0.0f64;
let mut qq0_inner = 0.0f64;
// 积分计算...
if converged {
ah = ah_inner; // break 前赋值回外部变量
qq0 = qq0_inner;
break;
}
}
model.surfac.flux[ij] = ah; // 此处可用
常见错误(变量被遮蔽)
let mut ah = 0.0f64;
loop {
let mut ah = 0.0; // ← 遮蔽外层 ah!外层永远是 0
if converged { break; }
}
// ah 还是 0
23. 测试初始化:ModelState::new() vs ::default()
问题
测试报错 index out of bounds: the len is 0 but the index is 0,即便访问 model.totrad.rad[0][0]。
原因
ModelState 有 #[derive(Default)],但 CurRad 等子结构体只有 new() 方法,没有手动 Default 实现。
derive(Default) 对 Vec 产生空 Vec,访问任意元素都越界。
解决方案
// ❌ 错误:CurRad::default() 产生空 Vec
let mut model = ModelState::default();
// ✅ 正确:ModelState::new() 调用 CurRad::new(),正确分配数组
let mut model = ModelState::new();
24. extint 索引:内层是角度维度
问题
model.totrad.extint[ij][i] 在 for i in 0..MDEPTH 循环中越界(len=6, index=6)。
原因
extint 对应 Fortran EXTINT(MFREQ, MMU):
- 外层:频率索引(MFREQ)
- 内层:角度索引(MMU = 6),不是深度!
解决方案
// 正确:遍历角度 (MMU=6)
use crate::state::constants::MMU;
for i in 0..MMU {
model.totrad.extint[0][i] = 0.0;
}
25. 常用二维数组维度汇总
| 变量 | 外层维度 | 内层维度 |
|---|---|---|
totrad.rad[ij][id] |
频率 MFREQ | 深度 MDEPTH |
totrad.extint[ij][i] |
频率 MFREQ | 角度 MMU |
totrad.fak[ij][id] |
频率 MFREQ | 深度 MDEPTH |
comptf.delj[iji][id] |
频率 MFREQ | 深度 MDEPTH |
expraf.radex[ije][id] |
显式频率 MFREX | 深度 MDEPTH |
angles.amu[i] / wtmu[i] |
角度 MMU | — |
教训
看到二维数组前,确认两个维度各是什么,不要默认都是 MDEPTH。
26. 2026-03-21 新增模块完整性检查
SETDRT (密度对温度的导数)
- 状态: ✅ 完整
- 原版 Fortran 调用 RHOEOS 函数
- Rust 版使用泛型函数参数
rhoeos_fn,设计更灵活 - 有限差分计算完全一致
TAUFR1 (光学深度计算)
- 状态: ✅ 完整
ss0数组在原版中也计算但未使用,与 Fortran 一致XCON常量在原版定义但未使用,已忽略- 核心逻辑:光学深度计算、参考深度插值、Planck 函数计算
TABINT (频率表插值)
- 状态: ⚠️ 部分实现
- 问题:
interpolate_opacity函数中未实际修改absopac数组 - 代码中有注释 "暂时跳过实际修改"
- 二分查找和插值系数计算完整
待修复:
// 当前代码 (错误):
let opac = rc * (params.freq[ij] / frtab[j - 1]).log10() + absort[j - 1];
let _ = opac; // 计算了但没有写回
// 应该:
// 需要设计一个可变引用来修改 absopac
RYBMAT (Rybicki矩阵)
- 状态: ⚠️ 简化实现
- 原版 ~390 行 → Rust 375 行
- 问题: 测试失败
result.rb[0].is_finite()返回 NaN
可能原因:
- 边界条件
id=0时dm[id+1] - dm[id]可能为零 abso1数组可能包含零值导致除零- Hermitian 方法 (
isplin=2) 被简化
待修复:
// 需要添加边界检查
let ddm = (params.dm[id + 1] - params.dm[id]) * HALF;
if ddm.abs() < 1e-30 {
continue; // 跳过无效深度点
}
let dtm = UN / ((params.abso1[id] + params.abso1[id + 1]) * ddm);
27. 优先级列表注意事项
优先级列表 (python3 scripts/analyze_fortran.py --priority) 显示"传递未实现=0"的函数可能有隐藏依赖:
- SETDRT 调用 RHOEOS(标记为 pending),但通过函数参数传入解决了
- TABINT 无外部调用,真正独立
- TAUFR1 无外部调用,真正独立
- RYBMAT 无外部调用,但依赖大量 COMMON 块变量
教训: 优先级列表只检查显式的 SUBROUTINE/FUNCTION 调用,不检查:
- COMMON 块依赖
- 通过参数传入的函数指针
- 隐式的外部函数引用