SpectraRust/MEMORY/refactoring_notes.md
2026-03-21 16:23:35 +08:00

16 KiB
Raw Blame History

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-indexedRust 中直接用导致索引偏移

解决方案

// 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 中递减循环溢出

原因

irjj 递减到负数时,无符号整数溢出

解决方案

// 错误: 用 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)

调试技巧

  1. 添加 #[cfg(test)] 条件编译的调试输出
  2. 打印 ESEMAT 矩阵验证是否为单位矩阵
  3. 打印每次 B 矩阵更新的行、列、值
  4. 检查温度列和电子密度列是否与主循环列重叠

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)
  • 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 类函数有三个独立处理部分:

  1. 第一个深度点 (ID=1)

    • 特殊边界条件
    • DSFT1M/DSFN1M 初始化为 0
  2. 深度循环 (ID=2 到 ND-1)

    • 保存前一步值 (DSFTMM, DSFNMM)
    • 更新当前值
    • 计算下一步值 (DSFT1P, DSFN1P)
  3. 最深点 (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/xx 的顺序是否正确
  • 大型 COMMON 块函数是否需要创建综合输入结构体
  • 多分支 GOTO 是否正确转换为 if-else
  • 循环中重新赋值的变量是否声明为 mut
  • 变量字段在哪个 COMMON 块 → 对应哪个 Rust 结构体 (查 .FOR 原文)
  • 测试初始化用 ModelState::new() 而非 ::default()
  • 二维数组内层维度是否是深度/角度/频率 (不要默认 MDEPTH)
  • loop 内部积分变量是否在 loop 外声明以便 break 后使用

21. COMMON 块变量所属结构体查找

问题

Fortran COMMON 块变量(如 IDISKIBCICHCOOIJORIG)应放在哪个 Rust 结构体中?

解决方案

通过查阅 .FOR 中的 COMMON 定义来确认:

  • IDISKIBCBASICS.FORCOMMON/BASNUM/config.basnum
  • ICHCOOICOMSTICOMDEBASICS.FORcommon/compti/config.compti
  • IJORIGBASICS.FORcommon/comptn/config.comptn.ijorig
  • IWINBLMODELQ.FORCOMMON/WINDBL/model.windbl.iwinbl
  • IFZ0BASICS.FORCOMMON/CENTRL/config.centrl.ifz0
# 快速查找变量所在 COMMON 块
grep -i "变量名" tlusty/extracted/*.FOR

教训

编译报错 no field 'xxx' on type 'Accel' 时,不要盲目添加字段——先查原始 .FOR 确认 COMMON 归属。


22. 循环外变量对 break 后可见

问题

ahqq0u0 在 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

可能原因:

  1. 边界条件 id=0dm[id+1] - dm[id] 可能为零
  2. abso1 数组可能包含零值导致除零
  3. 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 调用,不检查:

  1. COMMON 块依赖
  2. 通过参数传入的函数指针
  3. 隐式的外部函数引用