- server: 实现按 workflow_name 的多工作流数据隔离与旧数据库平滑迁移机制 - server: 新增 API Key 认证(auth)、限流中间件(rate_limit)与运维备份接口(admin) - server: 统一 AppError 错误处理体系,重构调度器 scheduler 支持工作流级重置与抢占 - node: 节点 ID 缺失时自动生成随机 UUID,原生支持 `docker compose --scale node=N` 动态扩容 - dashboard: 前端模块化重构(state/api/components),升级 CSS 变量设计系统与 Toast 通知 - docker/docs: 更新 /healthz 健康检查、部署脚本 IP 配置及数据库设计文档
176 lines
5.2 KiB
Rust
176 lines
5.2 KiB
Rust
use crate::models::ConvCheckResult;
|
|
use regex::Regex;
|
|
use std::fs::File;
|
|
use std::io::{BufRead, BufReader};
|
|
use std::path::Path;
|
|
use std::sync::OnceLock;
|
|
|
|
static FORT9_RE: OnceLock<Regex> = OnceLock::new();
|
|
static NAN_RE: OnceLock<Regex> = OnceLock::new();
|
|
|
|
#[derive(Debug, Clone)]
|
|
struct Fort9Row {
|
|
depth: i32,
|
|
maximum: f64,
|
|
}
|
|
|
|
/// Parses `fort.9` and evaluates convergence against `chmax`
|
|
pub fn check_fort9(path: &Path, chmax: f64) -> ConvCheckResult {
|
|
let file = match File::open(path) {
|
|
Ok(f) => f,
|
|
Err(e) => {
|
|
return ConvCheckResult {
|
|
converged: false,
|
|
max_relc: f64::INFINITY,
|
|
worst_depth: -1,
|
|
last_iter: None,
|
|
n_depths: 0,
|
|
chmax,
|
|
error: Some(format!("Failed to open fort.9: {}", e)),
|
|
}
|
|
}
|
|
};
|
|
|
|
let reader = BufReader::new(file);
|
|
let re = FORT9_RE.get_or_init(|| {
|
|
Regex::new(
|
|
r"^\s*(\d+)\s+(\d+)\s+([-+\dE.]+)\s+([-+\dE.]+)\s+([-+\dE.]+)\s+([-+\dE.]+)\s+([-+\dE.]+)\s+(\d+)\s+(\d+)\s*$"
|
|
).unwrap()
|
|
});
|
|
|
|
let mut last_iter: Option<i32> = None;
|
|
let mut cur_iter: Option<i32> = None;
|
|
let mut cur_rows: Vec<Fort9Row> = Vec::new();
|
|
|
|
for line in reader.lines().map_while(Result::ok) {
|
|
if let Some(caps) = re.captures(&line) {
|
|
let iter: i32 = match caps[1].parse() {
|
|
Ok(v) => v,
|
|
Err(_) => continue,
|
|
};
|
|
let depth: i32 = match caps[2].parse() {
|
|
Ok(v) => v,
|
|
Err(_) => continue,
|
|
};
|
|
let maximum: f64 = match caps[7].parse() {
|
|
Ok(v) => v,
|
|
Err(_) => continue,
|
|
};
|
|
|
|
if cur_iter != Some(iter) {
|
|
cur_iter = Some(iter);
|
|
cur_rows.clear();
|
|
}
|
|
|
|
cur_rows.push(Fort9Row { depth, maximum });
|
|
last_iter = Some(iter);
|
|
}
|
|
}
|
|
|
|
if cur_rows.is_empty() || last_iter.is_none() {
|
|
return ConvCheckResult {
|
|
converged: false,
|
|
max_relc: f64::INFINITY,
|
|
worst_depth: -1,
|
|
last_iter: None,
|
|
n_depths: 0,
|
|
chmax,
|
|
error: Some("No valid iteration data found in fort.9".to_string()),
|
|
};
|
|
}
|
|
|
|
// Safely find depth with maximum absolute change without unwrap panic on NaN
|
|
let worst = match cur_rows.iter().max_by(|a, b| {
|
|
a.maximum
|
|
.abs()
|
|
.partial_cmp(&b.maximum.abs())
|
|
.unwrap_or(std::cmp::Ordering::Equal)
|
|
}) {
|
|
Some(row) => row,
|
|
None => {
|
|
return ConvCheckResult {
|
|
converged: false,
|
|
max_relc: f64::INFINITY,
|
|
worst_depth: -1,
|
|
last_iter,
|
|
n_depths: 0,
|
|
chmax,
|
|
error: Some(
|
|
"No valid iteration rows found when calculating maximum change".to_string(),
|
|
),
|
|
};
|
|
}
|
|
};
|
|
|
|
let max_relc = worst.maximum.abs();
|
|
let is_valid_num = max_relc.is_finite();
|
|
|
|
ConvCheckResult {
|
|
converged: is_valid_num && max_relc < chmax,
|
|
max_relc,
|
|
worst_depth: worst.depth,
|
|
last_iter,
|
|
n_depths: cur_rows.len(),
|
|
chmax,
|
|
error: if is_valid_num {
|
|
None
|
|
} else {
|
|
Some("Convergence value is NaN or Inf".to_string())
|
|
},
|
|
}
|
|
}
|
|
|
|
/// Checks if an atmosphere file (.7) contains NaN lines (>10% NaN lines = invalid) using exact word boundary
|
|
///
|
|
/// 文件缺失时返回 `false`(语义:不存在 NaN 内容)。这与“含 NaN 导致无效”是不同语义;
|
|
/// 调用方需先自行确认文件存在性,不应将“缺失”与“含 NaN”混为一谈。
|
|
pub fn atmosphere_has_nan(path: &Path) -> bool {
|
|
let file = match File::open(path) {
|
|
Ok(f) => f,
|
|
Err(_) => return false,
|
|
};
|
|
let reader = BufReader::new(file);
|
|
let mut total_lines = 0;
|
|
let mut nan_lines = 0;
|
|
let nan_re = NAN_RE.get_or_init(|| Regex::new(r"(?i)\bnan\b").unwrap());
|
|
|
|
for line in reader.lines().map_while(Result::ok) {
|
|
total_lines += 1;
|
|
if nan_re.is_match(&line) {
|
|
nan_lines += 1;
|
|
}
|
|
}
|
|
|
|
if total_lines == 0 {
|
|
return true;
|
|
}
|
|
|
|
(nan_lines as f64) > (total_lines as f64 * 0.1)
|
|
}
|
|
|
|
#[cfg(test)]
|
|
mod tests {
|
|
use super::*;
|
|
|
|
#[test]
|
|
fn test_nan_check() {
|
|
let dir = tempfile::tempdir().unwrap();
|
|
let file_path = dir.path().join("test.7");
|
|
std::fs::write(&file_path, "1 2 3\n4 5 6\n7 8 9\n").unwrap();
|
|
assert!(!atmosphere_has_nan(&file_path));
|
|
|
|
let nan_file_path = dir.path().join("nan.7");
|
|
std::fs::write(&nan_file_path, "NaN 2 3\nNaN 5 6\n7 8 9\n").unwrap();
|
|
assert!(atmosphere_has_nan(&nan_file_path));
|
|
|
|
// Substring false positive test
|
|
let banana_file_path = dir.path().join("banana.7");
|
|
std::fs::write(&banana_file_path, "banana 2 3\nbanana 5 6\n7 8 9\n").unwrap();
|
|
assert!(!atmosphere_has_nan(&banana_file_path));
|
|
|
|
// Missing file returns false (absence != contains NaN)
|
|
let missing_path = dir.path().join("missing.7");
|
|
assert!(!atmosphere_has_nan(&missing_path));
|
|
}
|
|
}
|