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

671 lines
16 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

# Fortran → Rust 重构问题与解决方案
记录重构过程中遇到的问题,避免重复踩坑。
---
## 1. Fortran 1-indexed 转 Rust 0-indexed
### 问题
Fortran 数组从 1 开始索引Rust 从 0 开始。
### 解决方案
```rust
// 数组访问
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
! Fortran: jl=0 需要调整
IF (J.EQ.0) J = J+1 ! 调整到 J=1
```
```rust
// Rust: jl=0 就是第一个有效索引,直接使用
// 删除 IF (J.EQ.0) 的调整逻辑
```
---
## 2. Fortran 表达式解析错误
### 问题
`XL=-LOG(X)` 被误解为 `(-x).ln()` 而不是 `-x.ln()`
### 示例 (erfcin.f)
```fortran
XL=-LOG(X) ! 意思是: XL = -ln(X)
```
```rust
// 错误:
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)` 编译错误,类型不明确
### 解决方案
```rust
// 错误:
(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
```rust
// 简单函数: epsilon = 1e-10
// 多项式近似: epsilon = 1e-7
assert_relative_eq!(e1, exp_e1, epsilon = 1e-7);
```
---
## 5. 数组索引越界
### 问题
`locate` 函数无限循环
### 原因
Fortran 中 ju=N+1 (越界值)Rust 直接用导致越界访问
### 解决方案
使用 i64 允许负值,或调整边界逻辑
```rust
// 使用 i64 支持 jl=-1
let mut jl: i64 = -1;
let mut ju: i64 = n as i64;
```
---
## 6. voigte 索引偏移
### 问题
voigte 返回负值
### 原因
Fortran m 值是 1-indexedRust 中直接用导致索引偏移
### 解决方案
```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` 递减到负数时,无符号整数溢出
### 解决方案
```rust
// 错误: 用 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 范围
### 解决方案
```rust
// 使用 i32 避免中间结果溢出
let idx = (((iu + 1) * (iu + 1) - 3 * (iu + 1) + 4) / 2 - 1) as usize;
```
---
## 11. 数组变量命名冲突
### 问题
不同 Fortran 文件中同名变量冲突
### 原因
Fortran 文件局部作用域Rust 需要唯一名称
### 解决方案
修改 `extract_fortran_data.py`:所有变量添加文件名前缀
```rust
// 旧: ADI, BDENS (冲突)
// 新: DIELRC_ADI, DIELRC_BDENS
pub const DIELRC_ADI: [f64; 18] = [...];
```
---
## 12. 2D 数组存储顺序
### 问题
Fortran 列优先Rust 行优先
### 解决方案
脚本自动转置,访问方式改变
```rust
// Fortran: ARR(j, i) 列优先
// Rust: ARR[i][j] 行优先 (已转置)
```
---
## 13. DATA 语句数值解析
### 问题
负数和科学计数法中有空格
### 示例
```fortran
DATA A / - 14.2, 1.48 D-2 /
```
### 解决方案
```python
# 修复负数空格
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两者写入同一列
### 解决方案
测试参数设置时避免列重叠:
```rust
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) * (...)`
### 解决方案
```rust
// 错误写法
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 等)。
### 解决方案
创建综合的输入结构体,使用生命周期参数引用数据:
```rust
/// 输入状态结构体 (使用生命周期引用数据)
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
```fortran
IF(ILMCOR.NE.3) GO TO 199
! ... ILMCOR==3 的代码
199 IF(ILASCT.NE.0) GO TO 299
! ... ILASCT==0 的代码
299 ! ... ILASCT!=0 的代码
```
```rust
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 *` 重新导出,导致名称冲突。
### 解决方案
添加新结构体前,先搜索是否已存在:
```bash
grep -r "pub struct Comptn" src/state/
```
如果已存在,扩展现有结构体而不是创建新的。
---
## 18. ALIFR6 - 循环中可变变量重新赋值
### 问题
在循环中重新赋值 `s0p = s0p_new` 报错 `cannot assign twice to immutable variable`
### 原因
变量在 if-else 块中首次定义后,无法在循环中重新赋值。
### 解决方案
```rust
// 错误:不可变变量
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/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.basnum`
- `ICHCOO`、`ICOMST`、`ICOMDE` → `BASICS.FOR``common/compti/``config.compti`
- `IJORIG``BASICS.FOR``common/comptn/``config.comptn.ijorig`
- `IWINBL``MODELQ.FOR``COMMON/WINDBL/``model.windbl.iwinbl`
- `IFZ0``BASICS.FOR``COMMON/CENTRL/``config.centrl.ifz0`
```bash
# 快速查找变量所在 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` 声明的变量在块外不可见。
### 解决方案
```rust
// 正确:循环前提前声明,循环内用 _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; // 此处可用
```
### 常见错误(变量被遮蔽)
```rust
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访问任意元素都越界。
### 解决方案
```rust
// ❌ 错误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不是深度**
### 解决方案
```rust
// 正确:遍历角度 (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` 数组
- 代码中有注释 "暂时跳过实际修改"
- 二分查找和插值系数计算完整
**待修复**:
```rust
// 当前代码 (错误):
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=0``dm[id+1] - dm[id]` 可能为零
2. `abso1` 数组可能包含零值导致除零
3. Hermitian 方法 (`isplin=2`) 被简化
**待修复**:
```rust
// 需要添加边界检查
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. 隐式的外部函数引用