feat(all): 源精度命名体系、工作流可观测台、节点停用管理与白名单归档
核心变更:
1. GridAxisValue 源精度命名
- 新增 GridAxisValue 类型,携带 f64 数值 + YAML 源书写文本(Deref 透明兼容算术)
- config.rs 绕过 serde_yaml 归一化,逐 token 捕获轴值原文(logg: 5.0 → g5.0)
- runner/executor/scheduler 全链路改用 DB TEXT 列权威 point_name,
修复 REAL 列回读丢精度导致的 model_name 错配
2. 工作流执行可观测台
- 新增 stats/progress/points 三组 API(进度时间序列、经验速率 ETA、
停滞预警、逐点明细分页、收敛性热力图数据)
- 新增 workflow_progress_snapshots 表 + tasks/grid_points 耗时列
- runner 携带 last_iter/worst_depth/n_depths 进 conv.json
- 前端新增 hash 路由、工作流详情页(概览/网格点/收敛分析三 Tab)、YAML 编辑器
3. 节点停用/启用管理
- 新增 disabled 状态 + disable/enable API;停用节点保持心跳但停止分发,
worker 空闲待命而非退出;移除 revoke API,token 失效统一走重发覆盖;
移除 host_name 字段
4. 白名单结果归档
- 新增 result_filter 模块,只归档有语义产物,丢弃 Tlusty 中间单元(~2MB/模型)
- executor 原子写入归档 + 200 点 LRU 上限
5. 历史数据导入
- sync_seeds 重写为 import_results:经 /admin/import_seed 标记 converged +
按新版命名迁移产物树
6. 部署与目录重规划
- data/results→seeds、data/archive→result + migrate_data_dirs.sh
- deploy.sh 增强(SSH 复用、Profile、远程 env);Dockerfile 瘦身
7. 文档同步更新 api/database/architecture/deployment
This commit is contained in:
@@ -21,6 +21,6 @@ reqwest.workspace = true
|
||||
tokio.workspace = true
|
||||
|
||||
[features]
|
||||
default = ["embed-binaries"]
|
||||
default = []
|
||||
embed-binaries = []
|
||||
|
||||
|
||||
+166
-43
@@ -1,15 +1,143 @@
|
||||
use crate::models::GridAxisValue;
|
||||
use anyhow::{Context, Result};
|
||||
use regex::Regex;
|
||||
use serde::{Deserialize, Serialize};
|
||||
use std::path::Path;
|
||||
use std::sync::OnceLock;
|
||||
|
||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||
pub struct GridAxesConfig {
|
||||
pub teff: Vec<f64>,
|
||||
pub logg: Vec<f64>,
|
||||
pub loghe: Vec<f64>,
|
||||
pub logc: Vec<f64>,
|
||||
pub logn: Vec<f64>,
|
||||
pub logo: Vec<f64>,
|
||||
pub teff: Vec<GridAxisValue>,
|
||||
pub logg: Vec<GridAxisValue>,
|
||||
pub loghe: Vec<GridAxisValue>,
|
||||
pub logc: Vec<GridAxisValue>,
|
||||
pub logn: Vec<GridAxisValue>,
|
||||
pub logo: Vec<GridAxisValue>,
|
||||
}
|
||||
|
||||
/// 解析单个标量 token 为 `GridAxisValue`:保留 YAML 源书写原文(`5.0`→`"5.0"`,
|
||||
/// `20000`→`"20000"`,`-2`→`"-2"`),同时取其 f64 数值。
|
||||
fn axis_value_from_token(tok: &str) -> Option<GridAxisValue> {
|
||||
let tok = tok.trim().trim_matches(',');
|
||||
// 允许的标量形式:可选符号 + 数字(含小数、科学计数)。剔除引号/非法字符。
|
||||
if tok.is_empty() {
|
||||
return None;
|
||||
}
|
||||
let cleaned = tok.trim_matches(|c| c == '"' || c == '\'');
|
||||
if cleaned.parse::<f64>().is_ok() {
|
||||
Some(GridAxisValue::from_text(cleaned))
|
||||
} else {
|
||||
None
|
||||
}
|
||||
}
|
||||
|
||||
/// 从 YAML 文本中按轴名提取**源精度原文 token**,构造 `GridAxesConfig`。
|
||||
///
|
||||
/// # 为什么需要它
|
||||
/// `serde_yaml` 的公开 API 在解析时会把纯标量 `5.0` 解析为 `visit_f64(5.0)`、`20000`
|
||||
/// 解析为 `visit_i64`,**丢失原始书写文本**——而 `model_name()` 必须严格忠于源精度
|
||||
/// (`logg: 5.0` → `g5.0`,不是 `g5`),否则与旧版 Python `gen_input5.model_name`
|
||||
/// 对不上、迁移失败。本函数直接扫 YAML 文本的 `grid:` 块,逐 token 捕获原文,绕过
|
||||
/// serde_yaml 的类型归一化。
|
||||
///
|
||||
/// 支持两种块格式(与 run_grid.py / DCTS 配置一致):
|
||||
/// - 流式:`logg: [5.0, 6.0]`
|
||||
/// - 块式:`logg:\n - 5.0\n - 6.0`
|
||||
fn parse_grid_axes_raw(yaml: &str) -> Option<GridAxesConfig> {
|
||||
let axes_re = {
|
||||
static RE: OnceLock<Regex> = OnceLock::new();
|
||||
// 捕获轴名与该行 `key:` 之后的内容(流式 `[...]` 或空),供后续按行解析块式。
|
||||
RE.get_or_init(|| Regex::new(r"^\s*(teff|logg|loghe|logc|logn|logo)\s*:\s*(.*)$").unwrap())
|
||||
};
|
||||
|
||||
let mut axes: std::collections::HashMap<&str, Vec<GridAxisValue>> =
|
||||
std::collections::HashMap::new();
|
||||
let mut current_axis: Option<&str> = None; // 块式 `- value` 续行归属
|
||||
let lines: Vec<&str> = yaml.lines().collect();
|
||||
|
||||
// 是否已进入 `grid:` 块(grid 块之外的同名键不误捕,尽管实际不会重名)。
|
||||
let mut in_grid = false;
|
||||
|
||||
for (i, raw) in lines.iter().enumerate() {
|
||||
let line = raw.split('#').next().unwrap_or("").trim_end();
|
||||
let stripped = line.trim_start();
|
||||
if stripped.is_empty() {
|
||||
continue;
|
||||
}
|
||||
// 顶层键:grid / 其它。仅顶层(无缩进)键切换 in_grid。
|
||||
if !raw.starts_with(' ') && !raw.starts_with('\t') {
|
||||
in_grid = stripped.starts_with("grid:");
|
||||
current_axis = None;
|
||||
continue;
|
||||
}
|
||||
if !in_grid {
|
||||
continue;
|
||||
}
|
||||
|
||||
// 块式列表续行:` - 5.0`
|
||||
if let Some(rest) = stripped.strip_prefix("- ") {
|
||||
if let Some(axis) = current_axis {
|
||||
if let Some(v) = axis_value_from_token(rest) {
|
||||
axes.entry(axis).or_default().push(v);
|
||||
}
|
||||
}
|
||||
continue;
|
||||
}
|
||||
|
||||
// 轴定义行:` logg: [5.0, 6.0]` 或 ` logg:`(块式,值在下几行)
|
||||
if let Some(caps) = axes_re.captures(line) {
|
||||
let axis = caps.get(1).unwrap().as_str();
|
||||
let tail = caps.get(2).unwrap().as_str().trim();
|
||||
current_axis = Some(axis);
|
||||
if tail.starts_with('[') {
|
||||
// 流式:`[5.0, 6.0]` —— 可能在单行内闭合,也可能跨行(本配置不会跨行)。
|
||||
let inner = tail.trim_start_matches('[').split(']').next().unwrap_or("");
|
||||
for tok in inner.split(',') {
|
||||
if let Some(v) = axis_value_from_token(tok) {
|
||||
axes.entry(axis).or_default().push(v);
|
||||
}
|
||||
}
|
||||
current_axis = None; // 流式在本行闭合,不再续行
|
||||
}
|
||||
// tail 为空 → 块式,等后续 `- value` 行(current_axis 已设)
|
||||
}
|
||||
// 跨行未闭合的流式列表(grid 块极少如此)不单独处理;YAML 规范允许但本配置不使用。
|
||||
let _ = i;
|
||||
}
|
||||
|
||||
// 六轴齐全才算解析成功;任一缺失回退 None(调用方走纯 serde 数值路径)。
|
||||
let get = |k: &str| axes.get(k).cloned().filter(|v| !v.is_empty());
|
||||
Some(GridAxesConfig {
|
||||
teff: get("teff")?,
|
||||
logg: get("logg")?,
|
||||
loghe: get("loghe")?,
|
||||
logc: get("logc")?,
|
||||
logn: get("logn")?,
|
||||
logo: get("logo")?,
|
||||
})
|
||||
}
|
||||
|
||||
impl GridConfig {
|
||||
pub fn load_from_file(path: &Path) -> Result<Self> {
|
||||
let content = std::fs::read_to_string(path)
|
||||
.with_context(|| format!("Failed to read config file: {}", path.display()))?;
|
||||
GridConfig::from_yaml_str(&content)
|
||||
.with_context(|| format!("Failed to parse YAML config: {}", path.display()))
|
||||
}
|
||||
|
||||
/// 解析 YAML 配置,**优先用源精度原文重建 grid 轴**。
|
||||
///
|
||||
/// 两步:
|
||||
/// 1. `serde_yaml` 解析整个 `GridConfig`(chain/synspec 等正常字段,grid 轴为数值)。
|
||||
/// 2. 若能从原文捕到六轴 token,用源精度 `GridAxisValue` 覆盖 grid 字段;
|
||||
/// 捕不到(格式异常)则保留 serde 数值结果(命名精度回退,不阻断解析)。
|
||||
pub fn from_yaml_str(yaml: &str) -> Result<Self> {
|
||||
let mut cfg: GridConfig = serde_yaml::from_str(yaml)?;
|
||||
if let Some(raw_axes) = parse_grid_axes_raw(yaml) {
|
||||
cfg.grid = raw_axes;
|
||||
}
|
||||
Ok(cfg)
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||
@@ -99,6 +227,11 @@ pub struct GridConfig {
|
||||
pub resume: bool,
|
||||
#[serde(default = "default_true")]
|
||||
pub seed_step_fallback: bool,
|
||||
/// **已弃用的死字段**:旧版 Python 工具链遗留,无任何代码读取(实际目录以
|
||||
/// `ServerConfig.seeds_dir` / `DCTS_SEEDS_DIR` 为准)。仅因 `deny_unknown_fields`
|
||||
/// 必须能解析而保留。workflow YAML 里仍可写(如 `results: data/seeds`)但被忽略。
|
||||
#[deprecated(note = "死字段,实际目录以 DCTS_SEEDS_DIR 为准")]
|
||||
#[serde(default)]
|
||||
pub results: Option<String>,
|
||||
#[serde(default)]
|
||||
pub itek_fallback: Vec<StageConfig>,
|
||||
@@ -125,22 +258,16 @@ fn default_true() -> bool {
|
||||
true
|
||||
}
|
||||
|
||||
impl GridConfig {
|
||||
pub fn load_from_file(path: &Path) -> Result<Self> {
|
||||
let content = std::fs::read_to_string(path)
|
||||
.with_context(|| format!("Failed to read config file: {}", path.display()))?;
|
||||
let cfg: GridConfig = serde_yaml::from_str(&content)
|
||||
.with_context(|| format!("Failed to parse YAML config: {}", path.display()))?;
|
||||
Ok(cfg)
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Clone, Serialize, Deserialize)]
|
||||
pub struct ServerConfig {
|
||||
pub bind_addr: String,
|
||||
pub db_path: String,
|
||||
pub queue_db_path: String,
|
||||
pub results_dir: String,
|
||||
/// server 端种子库目录:node 上报/历史导入收敛后,落地 `<name>/conv.json` +
|
||||
/// `<name>/<name>.7` 于此,供 `download_seed` 端点给远程 node 热启动下载。
|
||||
/// **永不清理**(种子是 SeedStep 热启动的必需资源,删除会导致已收敛点重算)。
|
||||
/// 默认 "data/seeds",可经 DCTS_SEEDS_DIR 覆盖(回退读旧 DCTS_RESULTS_DIR)。
|
||||
pub seeds_dir: String,
|
||||
/// 数据库备份目录(每日自动备份落盘位置)。默认 "data/backups",可经 DCTS_BACKUP_DIR 覆盖。
|
||||
pub backup_dir: String,
|
||||
pub grid_config: String,
|
||||
@@ -152,9 +279,6 @@ pub struct ServerConfig {
|
||||
/// Admin 凭据(Dashboard 登录用)。优先 DCTS_ADMIN_TOKEN,回退旧变量 DCTS_AUTH_TOKEN。
|
||||
#[serde(default)]
|
||||
pub admin_token: Option<String>,
|
||||
/// 兼容字段:保留以判断「是否启用鉴权」与旧中间件逻辑。取 admin_token 的值。
|
||||
#[serde(default)]
|
||||
pub auth_token: Option<String>,
|
||||
/// 应急开关:DCTS_AUTH_DISABLE=1 时跳过全部鉴权(仅本地调试,默认关闭)。
|
||||
#[serde(default)]
|
||||
pub auth_disabled: bool,
|
||||
@@ -167,7 +291,7 @@ impl std::fmt::Debug for ServerConfig {
|
||||
.field("bind_addr", &self.bind_addr)
|
||||
.field("db_path", &self.db_path)
|
||||
.field("queue_db_path", &self.queue_db_path)
|
||||
.field("results_dir", &self.results_dir)
|
||||
.field("seeds_dir", &self.seeds_dir)
|
||||
.field("backup_dir", &self.backup_dir)
|
||||
.field("grid_config", &self.grid_config)
|
||||
.field("stale_sec", &self.stale_sec)
|
||||
@@ -178,10 +302,6 @@ impl std::fmt::Debug for ServerConfig {
|
||||
"admin_token",
|
||||
&self.admin_token.as_ref().map(|_| "***REDACTED***"),
|
||||
)
|
||||
.field(
|
||||
"auth_token",
|
||||
&self.auth_token.as_ref().map(|_| "***REDACTED***"),
|
||||
)
|
||||
.field("auth_disabled", &self.auth_disabled)
|
||||
.finish()
|
||||
}
|
||||
@@ -200,8 +320,10 @@ impl Default for ServerConfig {
|
||||
let db_path = std::env::var("DCTS_DB_PATH").unwrap_or_else(|_| "data/dcts.db".to_string());
|
||||
let queue_db_path = std::env::var("DCTS_QUEUE_DB_PATH")
|
||||
.unwrap_or_else(|_| "data/dcts_queue.db".to_string());
|
||||
let results_dir =
|
||||
std::env::var("DCTS_RESULTS_DIR").unwrap_or_else(|_| "data/results".to_string());
|
||||
// 种子库目录:优先 DCTS_SEEDS_DIR,回退旧 DCTS_RESULTS_DIR(已弃用,保留兼容)。
|
||||
let seeds_dir = std::env::var("DCTS_SEEDS_DIR")
|
||||
.or_else(|_| std::env::var("DCTS_RESULTS_DIR"))
|
||||
.unwrap_or_else(|_| "data/seeds".to_string());
|
||||
let backup_dir =
|
||||
std::env::var("DCTS_BACKUP_DIR").unwrap_or_else(|_| "data/backups".to_string());
|
||||
let grid_config = std::env::var("DCTS_GRID_CONFIG")
|
||||
@@ -226,13 +348,10 @@ impl Default for ServerConfig {
|
||||
.ok()
|
||||
.filter(|s| !s.is_empty())
|
||||
.or_else(|| legacy_token.clone());
|
||||
if legacy_token.is_some()
|
||||
&& (std::env::var("DCTS_ADMIN_TOKEN").is_err()
|
||||
|| std::env::var("DCTS_ENROLLMENT_TOKEN").is_err())
|
||||
{
|
||||
if legacy_token.is_some() && std::env::var("DCTS_ADMIN_TOKEN").is_err() {
|
||||
tracing::warn!(
|
||||
"检测到旧的 DCTS_AUTH_TOKEN,已自动用作 admin/enrollment 凭据。\
|
||||
建议迁移到 DCTS_ADMIN_TOKEN(管理)与 DCTS_ENROLLMENT_TOKEN(节点注册)"
|
||||
"检测到旧的 DCTS_AUTH_TOKEN,已自动回退用作 admin 凭据。\
|
||||
建议迁移到 DCTS_ADMIN_TOKEN"
|
||||
);
|
||||
}
|
||||
|
||||
@@ -245,19 +364,11 @@ impl Default for ServerConfig {
|
||||
);
|
||||
}
|
||||
|
||||
// auth_token 兼容字段:用于 main.rs 判断「是否启用鉴权中间件」。
|
||||
// 启用条件 = 显式配置了 admin 或 enrollment 凭据,且未应急关闭。
|
||||
let auth_token = if auth_disabled {
|
||||
None
|
||||
} else {
|
||||
admin_token.clone()
|
||||
};
|
||||
|
||||
Self {
|
||||
bind_addr: format!("0.0.0.0:{}", port),
|
||||
db_path,
|
||||
queue_db_path,
|
||||
results_dir,
|
||||
seeds_dir,
|
||||
backup_dir,
|
||||
grid_config,
|
||||
stale_sec,
|
||||
@@ -265,7 +376,6 @@ impl Default for ServerConfig {
|
||||
mq_type,
|
||||
rabbitmq_url,
|
||||
admin_token,
|
||||
auth_token,
|
||||
auth_disabled,
|
||||
}
|
||||
}
|
||||
@@ -278,6 +388,13 @@ pub struct NodeConfig {
|
||||
pub max_slots: usize,
|
||||
pub runtime_dir: String,
|
||||
pub work_dir: String,
|
||||
/// node 端完整计算结果归档目录:任务上报成功后、沙盒清理前,把完整科学产物
|
||||
/// (.spec/.cont/.iden/.7/各阶段快照/.6/.err/.log/conv.json 等)拷贝至此,
|
||||
/// 避免随沙盒删除而丢失。与 server 的 `seeds` 目录区分:此处存的是**完整产物**
|
||||
/// (光谱/连续谱/各阶段大气快照等),seeds 只存最小种子集(.7+conv.json)。
|
||||
/// 默认 "data/result",可经 DCTS_RESULT_DIR 覆盖(回退读旧 DCTS_ARCHIVE_DIR)。
|
||||
/// 超过 MAX_RESULT_MODELS 个网格点子目录时按 LRU 删除最旧的。
|
||||
pub result_dir: String,
|
||||
pub heartbeat_sec: u64,
|
||||
}
|
||||
|
||||
@@ -289,6 +406,7 @@ impl std::fmt::Debug for NodeConfig {
|
||||
.field("max_slots", &self.max_slots)
|
||||
.field("runtime_dir", &self.runtime_dir)
|
||||
.field("work_dir", &self.work_dir)
|
||||
.field("result_dir", &self.result_dir)
|
||||
.field("heartbeat_sec", &self.heartbeat_sec)
|
||||
.finish()
|
||||
}
|
||||
@@ -318,6 +436,10 @@ impl Default for NodeConfig {
|
||||
let runtime_dir =
|
||||
std::env::var("DCTS_RUNTIME_DIR").unwrap_or_else(|_| "data/runtime".to_string());
|
||||
let work_dir = std::env::var("DCTS_WORK_DIR").unwrap_or_else(|_| "data/work".to_string());
|
||||
// 结果归档目录:优先 DCTS_RESULT_DIR,回退旧 DCTS_ARCHIVE_DIR(已弃用,保留兼容)。
|
||||
let result_dir = std::env::var("DCTS_RESULT_DIR")
|
||||
.or_else(|_| std::env::var("DCTS_ARCHIVE_DIR"))
|
||||
.unwrap_or_else(|_| "data/result".to_string());
|
||||
let heartbeat_sec = std::env::var("DCTS_HEARTBEAT_SEC")
|
||||
.ok()
|
||||
.and_then(|v| v.parse::<u64>().ok())
|
||||
@@ -329,6 +451,7 @@ impl Default for NodeConfig {
|
||||
max_slots,
|
||||
runtime_dir,
|
||||
work_dir,
|
||||
result_dir,
|
||||
heartbeat_sec,
|
||||
}
|
||||
}
|
||||
|
||||
@@ -196,21 +196,21 @@ pub fn make_input5(
|
||||
|
||||
// Atoms block
|
||||
let mut atom_rows: Vec<(i32, String)> = vec![
|
||||
(2, "0.".to_string()), // 1 H
|
||||
(2, fmt_abn(params.loghe)), // 2 He
|
||||
(0, "0.".to_string()), // 3 Li
|
||||
(0, "0.".to_string()), // 4 Be
|
||||
(0, "0.".to_string()), // 5 B
|
||||
(2, "0.".to_string()), // 1 H
|
||||
(2, fmt_abn(*params.loghe)), // 2 He
|
||||
(0, "0.".to_string()), // 3 Li
|
||||
(0, "0.".to_string()), // 4 Be
|
||||
(0, "0.".to_string()), // 5 B
|
||||
];
|
||||
|
||||
if has_c {
|
||||
atom_rows.push((2, fmt_abn(params.logc))); // 6 C
|
||||
atom_rows.push((2, fmt_abn(*params.logc))); // 6 C
|
||||
}
|
||||
if has_n {
|
||||
atom_rows.push((2, fmt_abn(params.logn))); // 7 N
|
||||
atom_rows.push((2, fmt_abn(*params.logn))); // 7 N
|
||||
}
|
||||
if has_o {
|
||||
atom_rows.push((2, fmt_abn(params.logo))); // 8 O
|
||||
atom_rows.push((2, fmt_abn(*params.logo))); // 8 O
|
||||
}
|
||||
|
||||
let natoms =
|
||||
@@ -271,12 +271,12 @@ mod tests {
|
||||
#[test]
|
||||
fn test_make_input5() {
|
||||
let params = GridPointParams {
|
||||
teff: 35000.0,
|
||||
logg: 5.5,
|
||||
loghe: -1.0,
|
||||
logc: -2.0,
|
||||
logn: -2.0,
|
||||
logo: -2.0,
|
||||
teff: 35000.0.into(),
|
||||
logg: 5.5.into(),
|
||||
loghe: (-1.0).into(),
|
||||
logc: (-2.0).into(),
|
||||
logn: (-2.0).into(),
|
||||
logo: (-2.0).into(),
|
||||
};
|
||||
let input5 = make_input5(¶ms, "F", "F", "cno", 100);
|
||||
assert!(input5.contains("35000.0 5.5"));
|
||||
|
||||
@@ -6,5 +6,6 @@ pub mod gen_input5;
|
||||
pub mod logging;
|
||||
pub mod models;
|
||||
pub mod nst_writer;
|
||||
pub mod result_filter;
|
||||
pub mod runner;
|
||||
pub mod seed_finder;
|
||||
|
||||
+401
-32
@@ -1,19 +1,122 @@
|
||||
use chrono::{DateTime, Utc};
|
||||
use serde::{Deserialize, Serialize};
|
||||
use std::ops::Deref;
|
||||
use uuid::Uuid;
|
||||
|
||||
/// 6D grid point parameter specification
|
||||
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
|
||||
pub struct GridPointParams {
|
||||
pub teff: f64,
|
||||
pub logg: f64,
|
||||
pub loghe: f64,
|
||||
pub logc: f64,
|
||||
pub logn: f64,
|
||||
pub logo: f64,
|
||||
/// 网格轴值:同时携带**数值**(供算术/排序/DB REAL 列)与**源书写文本**(供命名)。
|
||||
///
|
||||
/// # 为什么需要它
|
||||
/// `f64` 无法区分 YAML 里 `5.0`(带小数)与 `5`(省略小数)——两者解析后都是 `5.0_f64`。
|
||||
/// 而 `model_name()` 必须严格忠于源精度("配置里多少位小数就多少位"),否则 Rust 名
|
||||
/// (`t20000_g5_...`) 与 Python `gen_input5.model_name` 名 (`t20000_g5.0_...`) 对不上,
|
||||
/// 旧版单机网格数据无法迁移到本系统。
|
||||
///
|
||||
/// 本类型在反序列化时通过 `deserialize_any` 的 `visit_str` 捕获 YAML/JSON 标量原文,
|
||||
/// 把它一路携带到 `model_name()` 直接拼接;算术则通过 `Deref<Target = f64>` 透明转发到数值,
|
||||
/// 绝大多数 `params.teff` 算术调用点无需改动。
|
||||
///
|
||||
/// # serde 行为
|
||||
/// - **反序列化**:优先抓原文(`5.0`→text=`"5.0"`,`20000`→text=`"20000"`);纯数值来源
|
||||
/// (DB REAL 列回读、JSON 数值 payload)无原文时用数值兜底文本(仅占位,该路径应使用
|
||||
/// 存储的 `name` 列而非 `model_name()` 重推)。
|
||||
/// - **序列化**:输出为纯 `f64` 数值,保证 JSON payload / 旧消费者无感。
|
||||
#[derive(Debug, Clone, PartialEq)]
|
||||
pub struct GridAxisValue {
|
||||
value: f64,
|
||||
text: Box<str>,
|
||||
}
|
||||
|
||||
fn fmt_num(val: f64) -> String {
|
||||
impl GridAxisValue {
|
||||
/// 构造:用给定数值,文本由数值生成(兜底路径:DB 回读 / 程序内构造)。
|
||||
pub fn from_value(value: f64) -> Self {
|
||||
let text = format_float_minimal(value).into();
|
||||
Self { value, text }
|
||||
}
|
||||
|
||||
/// 构造:用给定文本,数值由文本解析(反序列化主路径,信任源精度)。
|
||||
pub(crate) fn from_text(text: &str) -> Self {
|
||||
let value = text.parse::<f64>().unwrap_or(f64::NAN);
|
||||
Self {
|
||||
value,
|
||||
text: text.into(),
|
||||
}
|
||||
}
|
||||
|
||||
/// 数值(算术用)。
|
||||
pub fn value(&self) -> f64 {
|
||||
self.value
|
||||
}
|
||||
|
||||
/// 源书写文本(命名用)。
|
||||
pub fn text(&self) -> &str {
|
||||
&self.text
|
||||
}
|
||||
}
|
||||
|
||||
/// `Deref` 到 `f64`:所有 `params.teff` 形式的算术调用点(排序、量化、求和等)
|
||||
/// 无需显式 `.value()` 即可继续工作,把跨 crate 改动量压到最低。
|
||||
impl Deref for GridAxisValue {
|
||||
type Target = f64;
|
||||
fn deref(&self) -> &f64 {
|
||||
&self.value
|
||||
}
|
||||
}
|
||||
|
||||
/// 排序委托数值,保证调度排序语义不变。
|
||||
impl PartialOrd for GridAxisValue {
|
||||
fn partial_cmp(&self, other: &Self) -> Option<std::cmp::Ordering> {
|
||||
self.value.partial_cmp(&other.value)
|
||||
}
|
||||
}
|
||||
|
||||
/// 算术运算全部委托内部 f64,使本类型在数值计算上与 f64 完全等价。
|
||||
/// 结果一律退化为纯 f64——轴值只在其作为网格轴字段时才需保留源文本,
|
||||
/// 参与运算后的中间结果无需再保留精度(也不会用于命名)。
|
||||
/// 借用场景(如 `&GridPointParams` 字段相减)请用 `.value()` 显式取值。
|
||||
macro_rules! impl_arith {
|
||||
($trait:ident, $method:ident) => {
|
||||
impl std::ops::$trait for GridAxisValue {
|
||||
type Output = f64;
|
||||
fn $method(self, rhs: Self) -> f64 {
|
||||
self.value.$method(rhs.value)
|
||||
}
|
||||
}
|
||||
impl std::ops::$trait<f64> for GridAxisValue {
|
||||
type Output = f64;
|
||||
fn $method(self, rhs: f64) -> f64 {
|
||||
self.value.$method(rhs)
|
||||
}
|
||||
}
|
||||
};
|
||||
}
|
||||
impl_arith!(Add, add);
|
||||
impl_arith!(Sub, sub);
|
||||
impl_arith!(Mul, mul);
|
||||
impl_arith!(Div, div);
|
||||
|
||||
/// `Display` 委托数值(用于 `format!("{}", params.x)` 这类数值展示场景,
|
||||
/// 如 gen_input5 的 `{:.1f}` teff/logg 格式化)。命名场景请用 `.text()`。
|
||||
impl std::fmt::Display for GridAxisValue {
|
||||
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
|
||||
std::fmt::Display::fmt(&self.value, f)
|
||||
}
|
||||
}
|
||||
|
||||
impl From<f64> for GridAxisValue {
|
||||
fn from(value: f64) -> Self {
|
||||
Self::from_value(value)
|
||||
}
|
||||
}
|
||||
|
||||
impl From<i32> for GridAxisValue {
|
||||
fn from(value: i32) -> Self {
|
||||
Self::from_value(value as f64)
|
||||
}
|
||||
}
|
||||
|
||||
/// 把 f64 格式化为最简可解析文本(兜底路径用;源精度路径不走这里)。
|
||||
/// 整数取整无小数,非整数去尾零。
|
||||
fn format_float_minimal(val: f64) -> String {
|
||||
let rounded = (val * 1e6).round() / 1e6;
|
||||
if (rounded - rounded.round()).abs() < 1e-6 {
|
||||
format!("{:.0}", rounded.round())
|
||||
@@ -25,17 +128,85 @@ fn fmt_num(val: f64) -> String {
|
||||
}
|
||||
}
|
||||
|
||||
impl Serialize for GridAxisValue {
|
||||
fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
|
||||
where
|
||||
S: serde::Serializer,
|
||||
{
|
||||
serializer.serialize_f64(self.value)
|
||||
}
|
||||
}
|
||||
|
||||
impl<'de> Deserialize<'de> for GridAxisValue {
|
||||
fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
|
||||
where
|
||||
D: serde::Deserializer<'de>,
|
||||
{
|
||||
struct AxisVisitor;
|
||||
|
||||
impl<'de> serde::de::Visitor<'de> for AxisVisitor {
|
||||
type Value = GridAxisValue;
|
||||
|
||||
fn expecting(&self, f: &mut std::fmt::Formatter) -> std::fmt::Result {
|
||||
f.write_str("a numeric grid axis value")
|
||||
}
|
||||
|
||||
// 主路径:YAML/JSON 标量原文(`5.0`、`20000`、`-2`、`"-4"`)。
|
||||
fn visit_str<E: serde::de::Error>(self, v: &str) -> Result<Self::Value, E> {
|
||||
Ok(GridAxisValue::from_text(v))
|
||||
}
|
||||
|
||||
fn visit_string<E: serde::de::Error>(self, v: String) -> Result<Self::Value, E> {
|
||||
Ok(GridAxisValue::from_text(&v))
|
||||
}
|
||||
|
||||
// 兜底路径:纯数值来源(无原文)。先尝试解析原 deserializer 文本不可得,
|
||||
// 这里只能从数值反推文本(占位;该路径应用存储的 name 列)。
|
||||
fn visit_f64<E: serde::de::Error>(self, v: f64) -> Result<Self::Value, E> {
|
||||
Ok(GridAxisValue::from_value(v))
|
||||
}
|
||||
|
||||
fn visit_i64<E: serde::de::Error>(self, v: i64) -> Result<Self::Value, E> {
|
||||
// 整数来源:文本取整无损(如 JSON 里 `20000`),保留无小数形式。
|
||||
Ok(GridAxisValue::from_text(&v.to_string()))
|
||||
}
|
||||
|
||||
fn visit_u64<E: serde::de::Error>(self, v: u64) -> Result<Self::Value, E> {
|
||||
Ok(GridAxisValue::from_text(&v.to_string()))
|
||||
}
|
||||
}
|
||||
|
||||
// deserialize_any:serde_yaml 的标量会先尝试以 str 形式投递给 visit_str,
|
||||
// 从而捕获原文;纯数值 deserializer(如 serde_json 的 f64 字段)会走数值分支。
|
||||
deserializer.deserialize_any(AxisVisitor)
|
||||
}
|
||||
}
|
||||
|
||||
/// 6D grid point parameter specification
|
||||
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
|
||||
pub struct GridPointParams {
|
||||
pub teff: GridAxisValue,
|
||||
pub logg: GridAxisValue,
|
||||
pub loghe: GridAxisValue,
|
||||
pub logc: GridAxisValue,
|
||||
pub logn: GridAxisValue,
|
||||
pub logo: GridAxisValue,
|
||||
}
|
||||
|
||||
impl GridPointParams {
|
||||
/// Generates canonical model name string e.g. "t35000_g5.5_he-1_c-2_n-2_o-2"
|
||||
/// 生成规范模型名,**严格忠于各轴的源书写精度**:直接拼接 YAML 原文。
|
||||
///
|
||||
/// 例:YAML 中 `teff: [20000]`、`logg: [5.0]`、丰度 `[-2]` → `t20000_g5.0_he-2_c-2_n-2_o-2`。
|
||||
/// 这与旧版 Python `gen_input5.model_name` 逐字符一致,保证旧数据可迁移。
|
||||
pub fn model_name(&self) -> String {
|
||||
format!(
|
||||
"t{}_g{}_he{}_c{}_n{}_o{}",
|
||||
fmt_num(self.teff),
|
||||
fmt_num(self.logg),
|
||||
fmt_num(self.loghe),
|
||||
fmt_num(self.logc),
|
||||
fmt_num(self.logn),
|
||||
fmt_num(self.logo)
|
||||
self.teff.text(),
|
||||
self.logg.text(),
|
||||
self.loghe.text(),
|
||||
self.logc.text(),
|
||||
self.logn.text(),
|
||||
self.logo.text(),
|
||||
)
|
||||
}
|
||||
|
||||
@@ -44,7 +215,7 @@ impl GridPointParams {
|
||||
/// 注:此数值专门用于网格调度中的 Wave 难度分级与保序分组(对数和越小代表重元素丰度越低,
|
||||
/// 通常在大气模型计算中更容易收敛,作为冷启动基准)。
|
||||
pub fn cno_sum(&self) -> f64 {
|
||||
self.logc + self.logn + self.logo
|
||||
*self.logc + *self.logn + *self.logo
|
||||
}
|
||||
}
|
||||
|
||||
@@ -109,6 +280,10 @@ pub struct TaskSpec {
|
||||
/// 旧数据反序列化时缺省为 None。
|
||||
#[serde(default)]
|
||||
pub workflow_name: Option<String>,
|
||||
/// 网格点所属 wave(难度分批),用于队列内按难度优先出队,恢复"先易后难积累种子"语义。
|
||||
/// 旧 payload 反序列化时缺省为 0。
|
||||
#[serde(default)]
|
||||
pub wave: i32,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
|
||||
@@ -149,7 +324,6 @@ pub struct TaskReport {
|
||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||
pub struct NodeRegisterRequest {
|
||||
pub node_id: String,
|
||||
pub host_name: String,
|
||||
pub max_slots: i32,
|
||||
}
|
||||
|
||||
@@ -166,7 +340,6 @@ pub struct NodeHeartbeatRequest {
|
||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||
pub struct NodeInfo {
|
||||
pub node_id: String,
|
||||
pub host_name: String,
|
||||
pub max_slots: i32,
|
||||
pub active_slots: i32,
|
||||
pub status: String,
|
||||
@@ -197,6 +370,16 @@ pub struct StageSummary {
|
||||
pub best_max_relc: Option<f64>,
|
||||
pub elapsed_sec: f64,
|
||||
pub note: Option<String>,
|
||||
/// 末次迭代序号(fort.9 解析所得);NITER=0 的 grey 阶段为 null。
|
||||
/// 收敛难度直接指标:17 次迭代收敛 vs 顶着 NITER 上限勉强收敛稳定性迥异。
|
||||
#[serde(default)]
|
||||
pub last_iter: Option<i32>,
|
||||
/// 收敛最差的深度点编号(|maximum| 最大处),诊断定位用。
|
||||
#[serde(default)]
|
||||
pub worst_depth: Option<i32>,
|
||||
/// 深度点总数,诊断用。
|
||||
#[serde(default)]
|
||||
pub n_depths: Option<usize>,
|
||||
}
|
||||
|
||||
/// Full execution summary for a grid point
|
||||
@@ -213,6 +396,9 @@ pub struct ModelSummary {
|
||||
pub synspec_rc: Option<i32>,
|
||||
pub synspec_error: Option<String>,
|
||||
pub synspec_sec: Option<f64>,
|
||||
/// 单点总墙钟耗时(秒)。极旧版 conv.json 可能缺此字段,default 0.0 兜底
|
||||
/// (展示层把 ≤0 视为"无数据");现版 run_one.py 总是写入。
|
||||
#[serde(default)]
|
||||
pub elapsed_sec: f64,
|
||||
pub note: Option<String>,
|
||||
}
|
||||
@@ -221,30 +407,213 @@ pub struct ModelSummary {
|
||||
mod tests {
|
||||
use super::*;
|
||||
|
||||
/// StageSummary 新增迭代诊断字段的向后兼容:
|
||||
/// 旧 conv.json(无 last_iter/worst_depth/n_depths)必须能正常反序列化为 null,
|
||||
/// 新写入的 conv.json 往返保真。
|
||||
#[test]
|
||||
fn test_stage_summary_iter_fields_backward_compat() {
|
||||
let legacy = r#"{
|
||||
"label": "nl", "chmax": 0.001, "lte": "F", "converged": true,
|
||||
"best_max_relc": 0.0005, "elapsed_sec": 64.0, "note": null
|
||||
}"#;
|
||||
let st: StageSummary = serde_json::from_str(legacy).unwrap();
|
||||
assert_eq!(st.last_iter, None);
|
||||
assert_eq!(st.worst_depth, None);
|
||||
assert_eq!(st.n_depths, None);
|
||||
|
||||
let full = StageSummary {
|
||||
label: "nl".to_string(),
|
||||
chmax: Some(0.001),
|
||||
lte: "F".to_string(),
|
||||
converged: true,
|
||||
best_max_relc: Some(0.0005),
|
||||
elapsed_sec: 64.0,
|
||||
note: None,
|
||||
last_iter: Some(17),
|
||||
worst_depth: Some(1),
|
||||
n_depths: Some(50),
|
||||
};
|
||||
let round: StageSummary =
|
||||
serde_json::from_str(&serde_json::to_string(&full).unwrap()).unwrap();
|
||||
assert_eq!(round.last_iter, Some(17));
|
||||
assert_eq!(round.worst_depth, Some(1));
|
||||
assert_eq!(round.n_depths, Some(50));
|
||||
}
|
||||
|
||||
/// 旧版 Python run_one.py 写出的 conv.json 必须能完整解析为 ModelSummary
|
||||
/// (历史结果迁移链路 import_results → /api/admin/import_seed 的兼容性命门)。
|
||||
///
|
||||
/// 载荷严格复刻 run_one.py 的真实输出形态:stage 含 `itek_attempts`/`final` 嵌套
|
||||
/// dict、`note`、可选 `best_max_relc`,顶层含 `final_chmax`/`synspec_*`/`seed` 等。
|
||||
/// Rust StageSummary 未声明的字段(itek_attempts/final)应被 serde 静默忽略。
|
||||
#[test]
|
||||
fn test_model_summary_parses_python_legacy_conv_json() {
|
||||
let legacy = r#"{
|
||||
"name": "t20000_g5.0_he-2_c-4_n-4_o-4",
|
||||
"params": {"teff": 20000.0, "logg": 5.0, "loghe": -2.0, "logc": -4.0, "logn": -4.0, "logo": -4.0},
|
||||
"stages": [
|
||||
{"label": "lte", "chmax": null, "lte": "T",
|
||||
"itek_attempts": [{"itek": null, "rc": 0, "converged": true, "max_relc": 0.0,
|
||||
"note": "NITER=0 grey start (no iterations)"}],
|
||||
"converged": true,
|
||||
"final": {"itek": null, "rc": 0, "converged": true, "max_relc": 0.0,
|
||||
"note": "NITER=0 grey start (no iterations)"},
|
||||
"best_max_relc": 0.0, "elapsed_sec": 2.1},
|
||||
{"label": "nc", "chmax": null, "lte": "F",
|
||||
"itek_attempts": [{"itek": null, "rc": 0, "converged": false, "max_relc": 0.957,
|
||||
"worst_depth": 1, "last_iter": 10, "n_depths": 50}],
|
||||
"converged": false,
|
||||
"final": {"itek": null, "rc": 0, "converged": false, "max_relc": 0.957,
|
||||
"worst_depth": 1, "last_iter": 10, "n_depths": 50},
|
||||
"note": "accepted as seed (convergence not required)",
|
||||
"best_max_relc": 0.957, "elapsed_sec": 62.4},
|
||||
{"label": "nl", "chmax": null, "lte": "F",
|
||||
"itek_attempts": [{"itek": null, "rc": 0, "converged": true, "max_relc": 0.0069,
|
||||
"worst_depth": 1, "last_iter": 17, "n_depths": 50}],
|
||||
"converged": true,
|
||||
"final": {"itek": null, "rc": 0, "converged": true, "max_relc": 0.0069,
|
||||
"worst_depth": 1, "last_iter": 17, "n_depths": 50},
|
||||
"best_max_relc": 0.0069, "elapsed_sec": 650.2}
|
||||
],
|
||||
"converged": true,
|
||||
"final_max_relc": 0.0069,
|
||||
"final_chmax": null,
|
||||
"seed": null,
|
||||
"atmosphere_has_nan": false,
|
||||
"synspec_rc": 0,
|
||||
"synspec_sec": 3.1,
|
||||
"elapsed_sec": 715.0
|
||||
}"#;
|
||||
let s: ModelSummary = serde_json::from_str(legacy).expect("旧版 conv.json 应可解析");
|
||||
assert_eq!(s.name, "t20000_g5.0_he-2_c-4_n-4_o-4");
|
||||
assert_eq!(*s.params.teff, 20000.0);
|
||||
assert!(s.converged);
|
||||
assert!(!s.atmosphere_has_nan);
|
||||
assert_eq!(s.final_max_relc, Some(0.0069));
|
||||
assert_eq!(s.elapsed_sec, 715.0);
|
||||
assert_eq!(s.synspec_rc, Some(0));
|
||||
assert_eq!(s.stages.len(), 3);
|
||||
assert_eq!(s.stages[0].label, "lte");
|
||||
assert_eq!(s.stages[0].lte, "T");
|
||||
assert!(s.stages[0].converged);
|
||||
assert_eq!(s.stages[2].label, "nl");
|
||||
assert_eq!(s.stages[2].best_max_relc, Some(0.0069));
|
||||
assert_eq!(s.stages[2].elapsed_sec, 650.2);
|
||||
// 旧版把迭代诊断嵌在 final/itek_attempts 里(非扁平字段)→ 扁平字段为 None,
|
||||
// 但原始诊断仍随 conv.json 原文落盘(import_seed 存原始 summary_json),无数据丢失。
|
||||
assert_eq!(s.stages[2].last_iter, None);
|
||||
assert_eq!(s.stages[2].worst_depth, None);
|
||||
}
|
||||
|
||||
/// 极旧 conv.json 变体(无 elapsed_sec 字段)也应可解析(default 0.0 兜底),
|
||||
/// 使历史迁移不因缺耗时字段而整点跳过。
|
||||
#[test]
|
||||
fn test_model_summary_tolerates_missing_elapsed() {
|
||||
let minimal = r#"{
|
||||
"name": "t20000_g5.0_he-2_c-4_n-4_o-4",
|
||||
"params": {"teff": 20000.0, "logg": 5.0, "loghe": -2.0, "logc": -4.0, "logn": -4.0, "logo": -4.0},
|
||||
"stages": [],
|
||||
"converged": true,
|
||||
"final_max_relc": 0.001,
|
||||
"seed": null,
|
||||
"atmosphere_has_nan": false
|
||||
}"#;
|
||||
let s: ModelSummary = serde_json::from_str(minimal).expect("缺 elapsed_sec 应可解析");
|
||||
assert_eq!(s.elapsed_sec, 0.0);
|
||||
assert_eq!(s.synspec_rc, None);
|
||||
assert_eq!(s.final_chmax, None);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_model_name_formatting_and_decimal_precision() {
|
||||
let p1 = GridPointParams {
|
||||
teff: 35000.0,
|
||||
logg: 5.5,
|
||||
loghe: -1.0,
|
||||
logc: -2.0,
|
||||
logn: -2.0,
|
||||
logo: -2.0,
|
||||
teff: 35000.0.into(),
|
||||
logg: 5.5.into(),
|
||||
loghe: (-1.0).into(),
|
||||
logc: (-2.0).into(),
|
||||
logn: (-2.0).into(),
|
||||
logo: (-2.0).into(),
|
||||
};
|
||||
assert_eq!(p1.model_name(), "t35000_g5.5_he-1_c-2_n-2_o-2");
|
||||
|
||||
let p2 = GridPointParams {
|
||||
teff: 35000.0,
|
||||
logg: 5.25,
|
||||
loghe: -1.5,
|
||||
logc: -2.75,
|
||||
logn: -2.0,
|
||||
logo: -1.25,
|
||||
teff: 35000.0.into(),
|
||||
logg: 5.25.into(),
|
||||
loghe: (-1.5).into(),
|
||||
logc: (-2.75).into(),
|
||||
logn: (-2.0).into(),
|
||||
logo: (-1.25).into(),
|
||||
};
|
||||
assert_eq!(p2.model_name(), "t35000_g5.25_he-1.5_c-2.75_n-2_o-1.25");
|
||||
assert_ne!(p1.model_name(), p2.model_name());
|
||||
}
|
||||
|
||||
/// 回归:YAML 源书写的整数小数位必须原样保留(`logg: 5.0` → `g5.0`,不是 `g5`)。
|
||||
/// 这是旧版单机网格数据迁移的命门——Python `gen_input5.model_name` 用 `g{:.1f}`,
|
||||
/// 故 `t20000_g5.0_he-2_c-4_n-4_o-4` 才是正确名。`f64` 无法区分 `5.0` 与 `5`,
|
||||
/// 必须由 `GridAxisValue` 的源文本携带。
|
||||
#[test]
|
||||
fn test_model_name_preserves_source_decimal_precision() {
|
||||
// 模拟 serde_yaml 解析 grid: { teff: [20000], logg: [5.0], loghe: [-2], ... }
|
||||
// 每个轴值通过 from_text(visit_str 路径)构造,保留原文。
|
||||
let p = GridPointParams {
|
||||
teff: GridAxisValue::from_text("20000"),
|
||||
logg: GridAxisValue::from_text("5.0"),
|
||||
loghe: GridAxisValue::from_text("-2"),
|
||||
logc: GridAxisValue::from_text("-4"),
|
||||
logn: GridAxisValue::from_text("-4"),
|
||||
logo: GridAxisValue::from_text("-4"),
|
||||
};
|
||||
assert_eq!(p.model_name(), "t20000_g5.0_he-2_c-4_n-4_o-4");
|
||||
// 数值正确解析(算术/排序不受影响)
|
||||
assert!((p.teff.value() - 20000.0).abs() < 1e-9);
|
||||
assert!((p.logg.value() - 5.0).abs() < 1e-9);
|
||||
assert!((p.cno_sum() - (-12.0)).abs() < 1e-9);
|
||||
}
|
||||
|
||||
/// 端到端:`GridConfig::from_yaml_str` 解析真实 config(含 `grid:` 块),确认
|
||||
/// `logg: 5.0` 的源精度原文被捕获、命名逐字符等于 Python `gen_input5.model_name`。
|
||||
///
|
||||
/// 这是数据迁移的命门:旧版单机网格目录名是 `t20000_g5.0_...`,DCTS 必须产出同名。
|
||||
/// 注:纯 `serde_yaml::from_str` 会把 `5.0` 归一化为 `visit_f64` 丢失原文,故走
|
||||
/// `from_yaml_str` 的源文本捕获路径(见 config.rs `parse_grid_axes_raw`)。
|
||||
#[test]
|
||||
fn test_grid_axis_value_preserves_yaml_source_via_serde() {
|
||||
use crate::config::GridConfig;
|
||||
// 两种块格式都覆盖:流式 + 块式。
|
||||
let yaml_flow = "grid:\n teff: [20000]\n logg: [5.0]\n loghe: [-2]\n logc: [-4]\n logn: [-4]\n logo: [-4]\n";
|
||||
let yaml_block = "grid:\n teff:\n - 20000\n logg:\n - 5.0\n loghe:\n - -2\n logc:\n - -4\n logn:\n - -4\n logo:\n - -4\n";
|
||||
|
||||
for (label, yaml) in [("flow", yaml_flow), ("block", yaml_block)] {
|
||||
let cfg =
|
||||
GridConfig::from_yaml_str(yaml).unwrap_or_else(|_| panic!("{} 解析失败", label));
|
||||
let pt = GridPointParams {
|
||||
teff: cfg.grid.teff[0].clone(),
|
||||
logg: cfg.grid.logg[0].clone(),
|
||||
loghe: cfg.grid.loghe[0].clone(),
|
||||
logc: cfg.grid.logc[0].clone(),
|
||||
logn: cfg.grid.logn[0].clone(),
|
||||
logo: cfg.grid.logo[0].clone(),
|
||||
};
|
||||
assert_eq!(
|
||||
pt.model_name(),
|
||||
"t20000_g5.0_he-2_c-4_n-4_o-4",
|
||||
"{} 格式应保留源精度",
|
||||
label
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
/// `Deref<Target=f64>` 让算术调用点无感:可直接比较/运算。
|
||||
#[test]
|
||||
fn test_grid_axis_value_deref_to_f64() {
|
||||
let v: GridAxisValue = 5.0.into();
|
||||
assert!((*v - 5.0).abs() < 1e-9);
|
||||
assert!(*v > 4.0);
|
||||
let sum = *v + 1.0_f64;
|
||||
assert!((sum - 6.0).abs() < 1e-9);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_grid_point_status_display_and_conversion() {
|
||||
assert_eq!(GridPointStatus::Pending.to_string(), "pending");
|
||||
|
||||
@@ -0,0 +1,199 @@
|
||||
//! 归档白名单:决定沙盒里的哪些文件值得进入持久归档目录(`data/result/<name>/`)。
|
||||
//!
|
||||
//! node 端实时结果归档(`executor::save_result_artifacts`)与离线迁移工具(`import_results`)
|
||||
//! 共用本模块的 [`is_result_worthy`],确保两条路径的结果归档口径一致、不会随时间漂移。
|
||||
//!
|
||||
//! ## 设计原则
|
||||
//!
|
||||
//! 归档目标只保留**有语义价值的产物**,丢弃 Tlusty/Synspec 运行时产生的中间工作单元
|
||||
//! (`fort.1/2/3/13/14/18/22/42/44/50/57/69/82/95` 等)。旧版用「拷贝所有普通文件」的
|
||||
//! catch-all 策略,把这些无语义单元也搬进了归档,浪费磁盘(每个模型约 2MB / 4.8MB)。
|
||||
//!
|
||||
//! ## 保留的文件名形态(`model_name` 为网格点权威名,如 `t20000_g5.0_he-2_c-4_n-4_o-4`)
|
||||
//!
|
||||
//! 1. **裸名保留**(有独立语义,不以 model_name 为前缀):
|
||||
//! `conv.json`、`fort.8`(synspec 输入大气)、`fort.55`(synspec 控制卡)
|
||||
//! 2. **科学核心**:`<name>.7`、`<name>.spec`、`<name>.cont`、`<name>.iden`、`<name>.log`
|
||||
//! 3. **阶段快照**:`<name>.<label>.5/.6/.err/.nst/.7`(label ∈ lte/nc/nl/seed_nc)
|
||||
//! 4. **收敛诊断**:`<name>.<label>_chmax*.9`(**唯一保留的 .9**;裸 `<name>.<label>.9`
|
||||
//! 已在 runner 源头停止写出,因其与 `_chmax*.9` 内容完全重复)
|
||||
//!
|
||||
//! 符号链接、子目录、`.tmp`、`fort.84` 及所有 Tlusty 中间单元均不在白名单内,自然被跳过。
|
||||
|
||||
/// 科学核心产物的文件名后缀(挂在 `<name>.` 之后,无阶段标签)。
|
||||
const SCIENCE_SUFFIXES: &[&str] = &["7", "spec", "cont", "iden", "log"];
|
||||
|
||||
/// 阶段快照的文件名后缀(挂在 `<name>.<label>.` 之后)。
|
||||
const STAGE_SNAPSHOT_SUFFIXES: &[&str] = &["5", "6", "err", "nst", "7"];
|
||||
|
||||
/// 合法阶段标签(来自 `default_cold_chain` / `default_seed_chain` 的 label)。
|
||||
/// 阶段标签由 workflow 配置保证唯一,不会与科学后缀或 synspec 产物冲突。
|
||||
///
|
||||
/// **约束**:此处硬编码了默认链的 4 个标签。runner 的 `run_model_with_timeout`
|
||||
/// 虽接受 `custom_chain`(可含任意 label),但当前唯一生产调用方(executor)传 `None`
|
||||
/// 走默认链,故白名单覆盖安全。若将来启用自定义 chain 且引入新标签,需同步加入此处,
|
||||
/// 否则带新标签的阶段快照(`.5/.6/.err/.nst/.7`)和 `_chmax*.9` 会被白名单静默丢弃。
|
||||
const STAGE_LABELS: &[&str] = &["lte", "nc", "nl", "seed_nc"];
|
||||
|
||||
/// 有独立语义、保留的裸文件名(不以 model_name 为前缀)。
|
||||
const BARE_KEEPS: &[&str] = &["conv.json", "fort.8", "fort.55"];
|
||||
|
||||
/// 判断沙盒里的文件 `fname` 是否值得归档。
|
||||
///
|
||||
/// `model_name` 是网格点的权威名(如 `t20000_g5.0_he-2_c-4_n-4_o-4`),用于识别
|
||||
/// `<name>.<后缀>` 形式的产物。裸名保留(conv.json/fort.8/fort.55)不依赖 model_name。
|
||||
///
|
||||
/// 返回 `true` 表示应归档,`false` 表示应跳过(中间单元、未知后缀、`.tmp`、fort.84 等)。
|
||||
///
|
||||
/// # 示例
|
||||
/// ```
|
||||
/// use common::result_filter::is_result_worthy;
|
||||
/// let name = "t20000_g5.0_he-2_c-4_n-4_o-4";
|
||||
/// assert!(is_result_worthy("conv.json", name));
|
||||
/// assert!(is_result_worthy(&format!("{name}.7"), name));
|
||||
/// assert!(is_result_worthy(&format!("{name}.nl.nst"), name));
|
||||
/// assert!(is_result_worthy(&format!("{name}.nl_chmax0.001.9"), name));
|
||||
/// assert!(!is_result_worthy("fort.18", name));
|
||||
/// assert!(!is_result_worthy("fort.84", name));
|
||||
/// assert!(!is_result_worthy(&format!("{name}.nl.9"), name)); // 无 _chmax 的 .9 已不再写
|
||||
/// ```
|
||||
pub fn is_result_worthy(fname: &str, model_name: &str) -> bool {
|
||||
// 原子写入残留、NATOMS 崩溃缓存:显式排除(虽不在白名单,提前 short-circuit 更清晰)。
|
||||
if fname.ends_with(".tmp") || fname == "fort.84" {
|
||||
return false;
|
||||
}
|
||||
|
||||
// 1. 裸名保留(有独立语义)。
|
||||
if BARE_KEEPS.contains(&fname) {
|
||||
return true;
|
||||
}
|
||||
|
||||
// 其余保留项必须以 `<model_name>.` 开头。不以该前缀开头的文件(含所有其它 fort.N
|
||||
// 中间单元)一律跳过。
|
||||
let prefix = format!("{}.", model_name);
|
||||
let Some(rest) = fname.strip_prefix(&prefix) else {
|
||||
return false;
|
||||
};
|
||||
// 此时 rest 形如:`7` / `spec` / `nl.7` / `nl.nst` / `nl_chmax0.001.9` 等。
|
||||
|
||||
// 2. 科学核心:rest 本身就是后缀(`<name>.7`、`<name>.spec` 等)。
|
||||
if SCIENCE_SUFFIXES.contains(&rest) {
|
||||
return true;
|
||||
}
|
||||
|
||||
// 3. 阶段快照:`<label>.<suffix>`(如 `nl.7`、`nc.nst`)。
|
||||
// 用 split_once('.', label/suffix) 切一刀;label 必须在 STAGE_LABELS 内,
|
||||
// suffix 必须在 STAGE_SNAPSHOT_SUFFIXES 内。这样能精确排除 `<name>.nl.9`
|
||||
// (suffix=9 不在快照后缀集)等。
|
||||
if let Some((label, suffix)) = rest.split_once('.') {
|
||||
if STAGE_LABELS.contains(&label) && STAGE_SNAPSHOT_SUFFIXES.contains(&suffix) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
// 4. 收敛诊断:`<label>_chmax*.9`(如 `nl_chmax0.001.9`)。
|
||||
// rest 以 `<label>_chmax` 开头,以 `.9` 结尾。这是唯一保留的 .9 形态。
|
||||
if rest.ends_with(".9") {
|
||||
for label in STAGE_LABELS {
|
||||
let tag = format!("{}_chmax", label);
|
||||
if rest.starts_with(&tag) && rest.ends_with(".9") {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
false
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
|
||||
const NAME: &str = "t20000_g5.0_he-2_c-4_n-4_o-4";
|
||||
|
||||
#[test]
|
||||
fn test_bare_keeps() {
|
||||
assert!(is_result_worthy("conv.json", NAME));
|
||||
assert!(is_result_worthy("fort.8", NAME));
|
||||
assert!(is_result_worthy("fort.55", NAME));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_science_core() {
|
||||
for s in ["7", "spec", "cont", "iden", "log"] {
|
||||
let f = format!("{}.{}", NAME, s);
|
||||
assert!(is_result_worthy(&f, NAME), "{} 应归档", f);
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_stage_snapshots() {
|
||||
// 各阶段标签 × 快照后缀 都应保留
|
||||
for label in STAGE_LABELS {
|
||||
for s in STAGE_SNAPSHOT_SUFFIXES {
|
||||
let f = format!("{}.{}.{}", NAME, label, s);
|
||||
assert!(is_result_worthy(&f, NAME), "{} 应归档", f);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_convergence_diagnostic_chmax9() {
|
||||
// 唯一保留的 .9:带 _chmax 标签
|
||||
for label in STAGE_LABELS {
|
||||
let f = format!("{}.{}_chmax0.001.9", NAME, label);
|
||||
assert!(is_result_worthy(&f, NAME), "{} 应归档", f);
|
||||
}
|
||||
// 科学计数法形态的 chmax(1e-05)也应保留
|
||||
assert!(is_result_worthy(&format!("{}.nc_chmax1e-05.9", NAME), NAME));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_redundant_dot9_without_chmax_skipped() {
|
||||
// 无 _chmax 的阶段 .9 快照:与 _chmax.9 内容重复,跳过
|
||||
for label in STAGE_LABELS {
|
||||
let f = format!("{}.{}.9", NAME, label);
|
||||
assert!(
|
||||
!is_result_worthy(&f, NAME),
|
||||
"{} 应跳过(与 _chmax.9 重复)",
|
||||
f
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_tlusty_intermediate_units_skipped() {
|
||||
// 所有 Tlusty 中间工作单元一律跳过(含 0 字节空单元和大文件)
|
||||
for f in [
|
||||
"fort.1", "fort.2", "fort.3", "fort.10", "fort.11", "fort.13", "fort.14", "fort.18",
|
||||
"fort.22", "fort.42", "fort.44", "fort.50", "fort.57", "fort.69", "fort.82", "fort.95",
|
||||
"fort.84",
|
||||
] {
|
||||
assert!(!is_result_worthy(f, NAME), "{} 应跳过", f);
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_tmp_and_unknown_skipped() {
|
||||
assert!(!is_result_worthy("residue.tmp", NAME));
|
||||
assert!(!is_result_worthy("random_file.txt", NAME));
|
||||
assert!(!is_result_worthy("nst", NAME)); // 裸 nst(应已被 runner 改名)
|
||||
assert!(!is_result_worthy("fort.9", NAME)); // 裸 fort.9(应已被 runner 删除)
|
||||
assert!(!is_result_worthy("fort.12", NAME)); // 裸 fort.12(已 copy 为 .iden)
|
||||
assert!(!is_result_worthy("fort.17", NAME)); // 裸 fort.17(已 copy 为 .cont)
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_unknown_suffix_with_name_prefix_skipped() {
|
||||
// 以 model_name 开头但后缀不在白名单的也应跳过
|
||||
assert!(!is_result_worthy(&format!("{}.unknown", NAME), NAME));
|
||||
assert!(!is_result_worthy(&format!("{}.nl.foo", NAME), NAME));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_unknown_stage_label_skipped() {
|
||||
// 未知的阶段标签不归档(防御性:未来若引入新标签需显式加入 STAGE_LABELS)
|
||||
assert!(!is_result_worthy(&format!("{}.xxx.7", NAME), NAME));
|
||||
assert!(!is_result_worthy(&format!("{}.xxx.nst", NAME), NAME));
|
||||
}
|
||||
}
|
||||
+109
-3
@@ -125,6 +125,7 @@ impl<'a> ExecutionRunner<'a> {
|
||||
pub async fn run_model(
|
||||
&self,
|
||||
params: &GridPointParams,
|
||||
name: &str,
|
||||
task_type: TaskType,
|
||||
custom_chain: Option<Vec<StageConfig>>,
|
||||
seed_atmos: Option<&Path>,
|
||||
@@ -132,6 +133,7 @@ impl<'a> ExecutionRunner<'a> {
|
||||
) -> Result<ModelSummary> {
|
||||
self.run_model_with_timeout(
|
||||
params,
|
||||
name,
|
||||
task_type,
|
||||
custom_chain,
|
||||
seed_atmos,
|
||||
@@ -141,17 +143,32 @@ impl<'a> ExecutionRunner<'a> {
|
||||
.await
|
||||
}
|
||||
|
||||
#[allow(clippy::too_many_arguments)]
|
||||
pub async fn run_model_with_timeout(
|
||||
&self,
|
||||
params: &GridPointParams,
|
||||
name: &str,
|
||||
task_type: TaskType,
|
||||
custom_chain: Option<Vec<StageConfig>>,
|
||||
seed_atmos: Option<&Path>,
|
||||
synspec_cfg: Option<&SynspecConfig>,
|
||||
timeout_sec: u64,
|
||||
) -> Result<ModelSummary> {
|
||||
let name = params.model_name();
|
||||
let model_dir = self.work_dir.join(&name);
|
||||
// `name` 取自权威的 TaskSpec.point_name(DB 的 grid_points.name 列,源精度正确),
|
||||
// 而非 params.model_name()。原因:服务端把 GridPointParams 存成 6 个 REAL 数值列,
|
||||
// 回读时用 from_value() 反推文本会丢精度("5.0"→"5"),导致 params.model_name()
|
||||
// 产出错误名(g5 而非 g5.0)。point_name 走独立 TEXT 列,精度全程保留。
|
||||
// 下游(沙盒子目录、各阶段快照、conv.json.name、归档目录)全部用此 name,
|
||||
// 故只需在此处用权威 name 即可让整条链精度正确。
|
||||
let derived = params.model_name();
|
||||
if derived != name {
|
||||
warn!(
|
||||
"网格点权威名 {} 与 params 重推名 {} 不一致(DB REAL 列回读丢精度所致),\
|
||||
采用权威 point_name",
|
||||
name, derived
|
||||
);
|
||||
}
|
||||
let model_dir = self.work_dir.join(name);
|
||||
tokio::fs::create_dir_all(&model_dir).await?;
|
||||
|
||||
info!("开始物理计算网格模型 {} (类型: {:?})", name, task_type);
|
||||
@@ -279,6 +296,9 @@ impl<'a> ExecutionRunner<'a> {
|
||||
best_max_relc: None,
|
||||
elapsed_sec: stage_t0.elapsed().as_secs_f64(),
|
||||
note: None,
|
||||
last_iter: None,
|
||||
worst_depth: None,
|
||||
n_depths: None,
|
||||
};
|
||||
|
||||
if rc == 0 && fort7.is_file() {
|
||||
@@ -287,6 +307,11 @@ impl<'a> ExecutionRunner<'a> {
|
||||
let res = check_fort9(&fort9, eff_chmax);
|
||||
stage_summary.converged = res.converged;
|
||||
stage_summary.best_max_relc = Some(res.max_relc);
|
||||
// 携带迭代诊断量进 conv.json(旧版在此处丢弃):
|
||||
// 迭代数/最差深度点供详情页阶段链展示收敛难度。
|
||||
stage_summary.last_iter = res.last_iter;
|
||||
stage_summary.worst_depth = Some(res.worst_depth);
|
||||
stage_summary.n_depths = Some(res.n_depths);
|
||||
|
||||
// Save fort.9 snapshot
|
||||
let snap_name = format!("{}.{}_chmax{}.9", name, stage_def.label, eff_chmax);
|
||||
@@ -306,6 +331,30 @@ impl<'a> ExecutionRunner<'a> {
|
||||
stage_summary.note = Some(format!("tlusty rc={} or missing fort.7", rc));
|
||||
}
|
||||
|
||||
// 快照本阶段的同名输入/输出文件,带阶段标签保留。
|
||||
// 背景:.5/.6/.err/nst 在每阶段用同名文件覆盖,若不快照则只有最后阶段(nl)
|
||||
// 的版本能存活到归档,nc 等中间阶段的日志/输入会丢失。失败阶段的日志对排错
|
||||
// 尤其重要,因此此处无条件(不论 rc 是否为 0)快照。
|
||||
// 命名风格与上方 .7/.9 快照一致:<name>.<label>.<后缀>(单 name,不重复)。
|
||||
// 注意:stage_def.label 由配置保证唯一(lte/nc/nl/seed_nc),不会与 synspec 产物冲突。
|
||||
//
|
||||
// 不快照 fort.9:上方 L292 已把 fort.9 收敛诊断存为 `<name>.<label>_chmax*.9`
|
||||
// (带 chmax 阈值语义),再快照成 `<name>.<label>.9` 会与它内容完全重复。
|
||||
// 故 .9 收敛诊断只保留 `_chmax*.9` 一份,不留重复快照。
|
||||
// (suffix, full_src_name) —— suffix 用于快照名后缀,full_src_name 用于定位源文件
|
||||
for (suffix, full_name) in [
|
||||
("5", format!("{}.5", name)),
|
||||
("6", format!("{}.6", name)),
|
||||
("err", format!("{}.err", name)),
|
||||
("nst", "nst".to_string()),
|
||||
] {
|
||||
let src = model_dir.join(&full_name);
|
||||
if src.is_file() {
|
||||
let snap = model_dir.join(format!("{}.{}.{}", name, stage_def.label, suffix));
|
||||
let _ = tokio::fs::copy(&src, &snap).await;
|
||||
}
|
||||
}
|
||||
|
||||
final_chmax = stage_def.chmax;
|
||||
final_converged = stage_summary.converged;
|
||||
if let Some(r) = stage_summary.best_max_relc {
|
||||
@@ -429,9 +478,28 @@ impl<'a> ExecutionRunner<'a> {
|
||||
synspec_err = Some("No atmosphere .7 produced".to_string());
|
||||
}
|
||||
|
||||
// 清理冗余的裸文件:这些文件的内容已被带阶段标签的快照或重命名的科学产物覆盖,
|
||||
// 保留它们只会与归档里的 <name>.<label>.* / <name>.iden / <name>.cont 等重复(尤其
|
||||
// .spec/.cont 是大文件,双份存储浪费磁盘)。删除后归档目录干净无冗余。
|
||||
// 注意:fort.8(synspec 输入大气)和 fort.55(synspec 控制卡)有独立语义,予以保留。
|
||||
for redundant in [
|
||||
format!("{}.5", name), // 同 <name>.<最后阶段label>.5
|
||||
format!("{}.6", name), // 同 <name>.<最后阶段label>.6
|
||||
format!("{}.err", name), // 同 <name>.<最后阶段label>.err
|
||||
"nst".to_string(), // 同 <name>.<最后阶段label>.nst
|
||||
"fort.9".to_string(), // 内容已被 <name>.<label>_chmax*.9 收敛诊断覆盖
|
||||
"fort.12".to_string(), // 同 <name>.iden(synspec 谱线证认)
|
||||
"fort.17".to_string(), // 同 <name>.cont(synspec 连续谱)
|
||||
] {
|
||||
let p = model_dir.join(&redundant);
|
||||
if p.is_file() {
|
||||
let _ = tokio::fs::remove_file(&p).await;
|
||||
}
|
||||
}
|
||||
|
||||
let elapsed_sec = t0.elapsed().as_secs_f64();
|
||||
let summary = ModelSummary {
|
||||
name,
|
||||
name: name.to_string(),
|
||||
params: params.clone(),
|
||||
stages: stage_summaries,
|
||||
converged: final_converged,
|
||||
@@ -460,6 +528,8 @@ impl<'a> ExecutionRunner<'a> {
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use crate::models::{GridAxisValue, GridPointParams};
|
||||
|
||||
#[test]
|
||||
fn test_synspec_timeout_calculation() {
|
||||
let long_tlusty_timeout: u64 = 7200;
|
||||
@@ -470,4 +540,40 @@ mod tests {
|
||||
let synspec_timeout_short = 600_u64.min(short_tlusty_timeout);
|
||||
assert_eq!(synspec_timeout_short, 300);
|
||||
}
|
||||
|
||||
/// 回归测试:复现命名精度丢失场景,并锁定「runner 用 point_name 作权威名」的契约。
|
||||
///
|
||||
/// 背景:服务端 grid_points 表把 GridPointParams 存成 6 个 REAL 列,回读时用
|
||||
/// `GridAxisValue::from_value()` 反推文本(`format_float_minimal`),整数-valued
|
||||
/// 浮点数会丢小数(5.0 → "5")。于是 `params.model_name()` 产出 `g5` 而非 `g5.0`。
|
||||
/// 而 `TaskSpec.point_name`(DB 的 name TEXT 列,源精度)始终是 `g5.0`。
|
||||
///
|
||||
/// runner 的 `run_model_with_timeout` 现接收外部 `name: &str`(由 executor 传入
|
||||
/// `task.point_name`),不再用降级的 `params.model_name()`。本测试构造降级后的
|
||||
/// params,证明二者确实不同,从而确认「必须用 point_name」的修复是必要的。
|
||||
#[test]
|
||||
fn test_point_name_bypasses_degraded_params_model_name() {
|
||||
// 模拟 DB REAL 列回读后的 params:logg 经 from_value(5.0) 丢精度
|
||||
let degraded = GridPointParams {
|
||||
teff: GridAxisValue::from_value(20000.0),
|
||||
logg: GridAxisValue::from_value(5.0), // text 退化为 "5"
|
||||
loghe: GridAxisValue::from_value(-2.0),
|
||||
logc: GridAxisValue::from_value(-4.0),
|
||||
logn: GridAxisValue::from_value(-4.0),
|
||||
logo: GridAxisValue::from_value(-4.0),
|
||||
};
|
||||
// 权威 point_name(DB name 列,保留源精度)
|
||||
let point_name = "t20000_g5.0_he-2_c-4_n-4_o-4";
|
||||
|
||||
// 降级的 params 重推出的名字丢了 ".0"
|
||||
assert_ne!(
|
||||
degraded.model_name(),
|
||||
point_name,
|
||||
"降级 params.model_name() 应与权威 point_name 不同(这是 bug 的可观测证据)"
|
||||
);
|
||||
assert_eq!(degraded.model_name(), "t20000_g5_he-2_c-4_n-4_o-4");
|
||||
// runner 现在直接采用 point_name(不再调 params.model_name()),故归档/产物名正确
|
||||
let authoritative_name = point_name; // 即 executor 传入的 task.point_name
|
||||
assert_eq!(authoritative_name, "t20000_g5.0_he-2_c-4_n-4_o-4");
|
||||
}
|
||||
}
|
||||
|
||||
@@ -11,12 +11,12 @@ pub struct SeedMatch {
|
||||
pub const MAX_GLOBAL_SEED_DISTANCE: f64 = 3.0;
|
||||
|
||||
pub fn calculate_seed_distance(cand: &GridPointParams, target: &GridPointParams) -> (bool, f64) {
|
||||
let d_teff = (cand.teff - target.teff).abs();
|
||||
let d_logg = (cand.logg - target.logg).abs();
|
||||
let d_loghe = (cand.loghe - target.loghe).abs();
|
||||
let d_cno = (cand.logc - target.logc).abs()
|
||||
+ (cand.logn - target.logn).abs()
|
||||
+ (cand.logo - target.logo).abs();
|
||||
let d_teff = (cand.teff.value() - target.teff.value()).abs();
|
||||
let d_logg = (cand.logg.value() - target.logg.value()).abs();
|
||||
let d_loghe = (cand.loghe.value() - target.loghe.value()).abs();
|
||||
let d_cno = (cand.logc.value() - target.logc.value()).abs()
|
||||
+ (cand.logn.value() - target.logn.value()).abs()
|
||||
+ (cand.logo.value() - target.logo.value()).abs();
|
||||
|
||||
// exact family 判定:Teff/logg/logHe 视为“同物理族”,仅 CNO 丰度不同。
|
||||
// Teff 容忍度取半步 5000K:实际网格 Teff 档位通常为整数千(20000/30000/.../60000),
|
||||
|
||||
Reference in New Issue
Block a user