SpectraRust/docs/IO_COMPATIBILITY_LAYER.md
2026-03-23 15:45:52 +08:00

12 KiB
Raw Blame History

Fortran I/O 兼容层设计

目标

  1. 读取与 Fortran 相同格式的输入文件
  2. 生成与 Fortran 相同格式的输出文件
  3. 便于 A/B 测试对比

TLUSTY I/O 模式分析

单元号映射

Fortran 单元 文件 用途 格式
5 stdin 输入命令 格式化
6 stdout 进度/结果输出 格式化
7 fort.7 模型输出 格式化
8 fort.8 模型输入 格式化
9 fort.9 频率网格 格式化
91-93 临时 内部暂存 UNFORMATTED

输入格式特点

! 自由格式读取list-directed
READ(IBUFF,*) TEFF, GRAV      ! 35000. 4.0

! 字符串用单引号
READ(IBUFF,*) TYPION          ! ' H 1'

! 行内注释用 !
35000. 4.0        ! TEFF, GRAV

输出格式特点

! FORMAT 语句控制
WRITE(6,604) ION,TYPION(ION),N0I,N1I,NKI,IZ(ION)
604 FORMAT(1H ,I3,2X,A4,6I6,1PD15.3)

! 科学计数法
3.289007E+04   3.010526E+08   5.839922E-16

Rust 架构设计

目录结构

src/
├── io/
│   ├── mod.rs           # I/O 模块入口
│   ├── reader.rs        # Fortran 格式输入解析
│   ├── writer.rs        # Fortran 格式输出
│   ├── units.rs         # 单元号管理
│   └── format.rs        # FORMAT 语句模拟
├── math/                # 纯计算(已有)
└── main.rs              # 入口点

核心设计

1. 输入解析器 (reader.rs)

/// Fortran 风格的自由格式读取器
pub struct FortranReader<R: BufRead> {
    inner: R,
    current_line: String,
    line_number: usize,
}

impl<R: BufRead> FortranReader<R> {
    /// 读取一行,处理注释
    pub fn read_line(&mut self) -> io::Result<&str> {
        loop {
            self.current_line.clear();
            self.inner.read_line(&mut self.current_line)?;
            self.line_number += 1;

            // 去除行内注释
            if let Some(pos) = self.current_line.find('!') {
                self.current_line.truncate(pos);
            }

            let trimmed = self.current_line.trim();
            if !trimmed.is_empty() {
                return Ok(trimmed);
            }
        }
    }

    /// 读取值(模拟 READ(*,*)
    pub fn read_values<T: FromFortran>(&mut self) -> io::Result<T> {
        let line = self.read_line()?;
        T::from_fortran_str(line)
    }
}

/// 从 Fortran 字符串解析的 trait
pub trait FromFortran: Sized {
    fn from_fortran_str(s: &str) -> io::Result<Self>;
}

impl FromFortran for f64 {
    fn from_fortran_str(s: &str) -> io::Result<Self> {
        // 处理 Fortran 的 'D' 和 'E' 指数符号
        let s = s.to_uppercase().replace('D', "E");
        s.trim().parse().map_err(|e| io::Error::new(io::ErrorKind::InvalidData, e))
    }
}

impl FromFortran for bool {
    fn from_fortran_str(s: &str) -> io::Result<Self> {
        match s.trim().to_uppercase().as_str() {
            "T" | ".TRUE." => Ok(true),
            "F" | ".FALSE." => Ok(false),
            _ => Err(io::Error::new(io::ErrorKind::InvalidData, "invalid boolean")),
        }
    }
}

2. 输出格式化器 (writer.rs)

/// Fortran 格式输出写入器
pub struct FortranWriter<W: Write> {
    inner: W,
}

impl<W: Write> FortranWriter<W> {
    /// 按 FORMAT 规范写入
    pub fn write_formatted(&mut self, format: &FormatSpec, values: &[Value]) -> io::Result<()> {
        let output = format.apply(values);
        write!(self.inner, "{}", output)
    }

    /// 写入科学计数法(模拟 1PD15.3
    pub fn write_exp(&mut self, val: f64, width: usize, decimals: usize) -> io::Result<()> {
        let s = format_exp_fortran(val, width, decimals);
        write!(self.inner, "{}", s)
    }
}

/// Fortran 风格的科学计数法
fn format_exp_fortran(val: f64, width: usize, decimals: usize) -> String {
    if val == 0.0 {
        return format!("{:>width$}", "0.0", width = width);
    }

    let abs_val = val.abs();
    let exp = abs_val.log10().floor() as i32;

    // Fortran 格式: ±0.XXXXE±YY 或 ±0.XXXXD±YY
    let mantissa = val / 10f64.powi(exp);
    let sign = if val >= 0.0 { " " } else { "" };

    format!("{sign}{mantissa:width$.decimals$}E{exp:+03}")
}

3. 单元号管理 (units.rs)

use std::collections::HashMap;
use std::fs::File;
use std::io::{BufReader, BufWriter, Read, Write};

/// 文件单元管理器(模拟 Fortran 单元号)
pub struct FortranUnits {
    units: HashMap<u8, Box<dyn UnitFile>>,
}

trait UnitFile {
    fn as_reader(&mut self) -> Option<&mut dyn BufRead>;
    fn as_writer(&mut self) -> Option<&mut dyn Write>;
}

impl FortranUnits {
    pub fn new() -> Self {
        let mut units = HashMap::new();

        // 默认单元
        units.insert(5, Box::new(StdinUnit::new()) as Box<dyn UnitFile>);
        units.insert(6, Box::new(StdoutUnit::new()) as Box<dyn UnitFile>);

        Self { units }
    }

    /// 打开文件到指定单元
    pub fn open(&mut self, unit: u8, path: &str, status: OpenStatus) -> io::Result<()> {
        let file = File::open(path)?;
        let reader = BufReader::new(file);
        self.units.insert(unit, Box::new(FileReader { inner: reader }));
        Ok(())
    }

    /// 读取
    pub fn read_line(&mut self, unit: u8) -> io::Result<String> {
        if let Some(u) = self.units.get_mut(&unit) {
            if let Some(reader) = u.as_reader() {
                let mut line = String::new();
                reader.read_line(&mut line)?;
                return Ok(line);
            }
        }
        Err(io::Error::new(io::ErrorKind::NotFound, "unit not found"))
    }

    /// 写入
    pub fn write(&mut self, unit: u8, data: &str) -> io::Result<()> {
        if let Some(u) = self.units.get_mut(&unit) {
            if let Some(writer) = u.as_writer() {
                return writer.write_all(data.as_bytes());
            }
        }
        Err(io::Error::new(io::ErrorKind::NotFound, "unit not found"))
    }
}

4. FORMAT 解析器 (format.rs)

/// Fortran FORMAT 规范
#[derive(Debug, Clone)]
pub struct FormatSpec {
    items: Vec<FormatItem>,
}

#[derive(Debug, Clone)]
pub enum FormatItem {
    Integer { width: usize },
    Float { width: usize, decimals: usize, exponential: bool },
    String { width: usize },
    Char(char),
    Newline,
    Skip,
}

impl FormatSpec {
    /// 解析 FORMAT 字符串
    /// 例如: "(I3,2X,A4,6I6,1PD15.3)"
    pub fn parse(s: &str) -> Result<Self, FormatError> {
        // 移除外层括号
        let s = s.trim_start_matches('(').trim_end_matches(')');

        let mut items = Vec::new();
        let mut chars = s.chars().peekable();

        while let Some(&c) = chars.peek() {
            match c {
                'I' | 'i' => items.push(Self::parse_int(&mut chars)?),
                'F' | 'f' => items.push(Self::parse_float(&mut chars)?),
                'E' | 'e' | 'D' | 'd' => items.push(Self::parse_exp(&mut chars)?),
                'A' | 'a' => items.push(Self::parse_string(&mut chars)?),
                'X' | 'x' => { chars.next(); items.push(FormatItem::Skip); }
                '/' => { chars.next(); items.push(FormatItem::Newline); }
                '1'..='9' => {
                    let repeat = Self::parse_number(&mut chars)?;
                    // 处理重复计数
                }
                _ => { chars.next(); }
            }
        }

        Ok(FormatSpec { items })
    }

    /// 应用格式到值
    pub fn apply(&self, values: &[Value]) -> String {
        let mut output = String::new();
        let mut val_idx = 0;

        for item in &self.items {
            match item {
                FormatItem::Integer { width } => {
                    if let Some(Value::Int(v)) = values.get(val_idx) {
                        output.push_str(&format!("{:>width$}", v));
                        val_idx += 1;
                    }
                }
                FormatItem::Float { width, decimals, exponential } => {
                    if let Some(Value::Float(v)) = values.get(val_idx) {
                        if *exponential {
                            output.push_str(&format_exp_fortran(*v, *width, *decimals));
                        } else {
                            output.push_str(&format!("{:>width$.decimals$}", v));
                        }
                        val_idx += 1;
                    }
                }
                FormatItem::String { width } => {
                    if let Some(Value::Str(s)) = values.get(val_idx) {
                        output.push_str(&format!("{:<width$}", s));
                        val_idx += 1;
                    }
                }
                FormatItem::Char(c) => output.push(*c),
                FormatItem::Newline => output.push('\n'),
                FormatItem::Skip => output.push(' '),
            }
        }

        output
    }
}

使用示例

use tlusty::io::{FortranReader, FortranWriter, FortranUnits};
use tlusty::math::some_calculation;

fn main() -> io::Result<()> {
    // 初始化单元
    let mut units = FortranUnits::new();
    units.open(8, "model_input.dat", OpenStatus::Old)?;
    units.open(7, "model_output.dat", OpenStatus::Replace)?;

    // 读取输入
    let reader = FortranReader::new(stdin().lock());
    let teff: f64 = reader.read_values()?;
    let grav: f64 = reader.read_values()?;

    // 调用纯计算函数
    let result = some_calculation(teff, grav);

    // 写入输出Fortran 格式)
    let mut writer = FortranWriter::new(units.get_writer(7)?);
    writer.write_exp(result.temperature, 15, 3)?;
    writer.write_exp(result.pressure, 15, 3)?;

    Ok(())
}

测试策略

1. 单元测试

#[cfg(test)]
mod tests {
    use super::*;

    #[test]
    fn test_format_exp() {
        assert_eq!(
            format_exp_fortran(3.289e4, 15, 3),
            " 3.289E+04"
        );
    }

    #[test]
    fn test_read_values() {
        let input = "35000. 4.0  ! TEFF, GRAV\n";
        let mut reader = FortranReader::new(input.as_bytes());

        let teff: f64 = reader.read_values().unwrap();
        let grav: f64 = reader.read_values().unwrap();

        assert_relative_eq!(teff, 35000.0);
        assert_relative_eq!(grav, 4.0);
    }
}

2. 集成测试

#[test]
fn test_hhe_model() {
    // 运行 Rust 版本
    let rust_output = run_tlusty_rust("tests/hhe/hhe35lt.5");

    // 读取 Fortran 参考输出
    let fortran_output = fs::read_to_string("tests/hhe/hhe35lt.7.bak").unwrap();

    // 对比
    assert_outputs_match(&rust_output, &fortran_output, tolerance=1e-6);
}

重构策略

对于有 I/O 的模块:

  1. 分离 I/O 和计算

    // ❌ 混合
    pub fn read_and_calculate() -> Result {
        let input = read_from_file()?;  // I/O
        let result = calculate(input);   // 计算
        write_to_file(result)?;          // I/O
        Ok(result)
    }
    
    // ✅ 分离
    pub fn calculate(input: Input) -> Output { ... }  // 纯函数,可测试
    
    pub fn run() -> Result {
        let input = read_from_file()?;
        let result = calculate(input);
        write_to_file(result)?;
        Ok(result)
    }
    
  2. 保持格式兼容

    • 输入解析器必须接受 Fortran 格式
    • 输出必须与 Fortran 输出字节级兼容(用于 diff
  3. 渐进式重构

    • 先实现纯计算部分
    • 后实现 I/O 包装层
    • 最后集成测试

实现优先级

  1. Phase 1: 实现 FortranReader(输入解析)
  2. Phase 2: 实现 FortranWriter(输出格式化)
  3. Phase 3: 实现 FortranUnits(文件管理)
  4. Phase 4: 集成测试框架