From de7040738e1a480ebb5e83daf29724a979529e5d Mon Sep 17 00:00:00 2001 From: Reyppp <1310590802zss@gmail.com> Date: Sun, 30 Aug 2026 16:20:22 +0800 Subject: [PATCH] Support incomplete and partial log alignment --- PRODUCT.md | 3 +- README.md | 10 +-- RELEASE_NOTES.md | 24 ++++-- analyzer.py | 172 ++++++++++++++++++++++++++++++++------ index.html | 6 +- installer/LogAnalysis.iss | 2 +- site/demo.js | 4 +- site/index.html | 26 +++--- test_analyzer.py | 132 ++++++++++++++++++++++++++++- version.py | 2 +- 10 files changed, 321 insertions(+), 60 deletions(-) diff --git a/PRODUCT.md b/PRODUCT.md index d4f2687..9538346 100644 --- a/PRODUCT.md +++ b/PRODUCT.md @@ -32,9 +32,10 @@ Log Analysis 将连续工控log与逐层监控log只读汇总为可筛选、可 - 工控log设备趋势采用自适应采样,优先保留真实边界与极值;不强制加载全部采样点。 - 层范围、材料、监控方式和区段筛选统一作用于可计算的图表、表格和汇总;来源完整性、完成状态、数据质量及关联诊断保持整炉口径。 - 综合模式下 `Motor Speed` 以工控log为准,并保留监控log参考值用于审计。 +- 双源关联以实际完成层为准;未完成尾部理论层继续保留。工控log只覆盖局部镀膜时间时,可关联到唯一的连续监控层区间。 - 中文界面为主,核心入口保持 `analyze_folder(path) -> AnalysisResult`。 - 第一版只支持 Windows x64,不提供在线分析、数据库、账号或自动更新。 -- 当前二进制版本是未签名测试版 `v0.1.0-beta.7`。 +- 当前二进制版本是未签名测试版 `v0.1.0-beta.8`。 ## Brand Commitments diff --git a/README.md b/README.md index 38b9afb..90bef2a 100644 --- a/README.md +++ b/README.md @@ -16,11 +16,11 @@ ## 核心功能 -- **综合分析**:自动关联工控log与监控log,校正时间偏移,并按层数或时间对照双源设备数据。 +- **综合分析**:自动关联工控log与监控log,校正时间偏移,并按层数或时间对照双源设备数据。未完成炉次按实际完成层关联;局部工控时间段可映射到唯一的连续监控层区间。 - **监控log分析**:提供总览、理论趋势、光学、设备和异常复核五个模块,支持单层曲线、理论曲线、原始截图及前后切层。 - **工控log分析**:连续查看功率、电流、电压、真空、温度、转速和气体数据,并按镀膜区段及材料筛选工作时间。 - **统一筛选**:层范围、材料、监控方式和区段同步作用于可计算的汇总、图表和表格。 -- **设备趋势**:综合时间图在整张图内最多保留 8,000 个真实边界点和极值点;缩放与平移不重新请求数据。 +- **设备趋势**:综合时间图展示日志中的实际采样值,并优先保留真实边界与极值;缩放与平移不重新请求数据。 - **数据来源**:综合模式下转速以工控log为准,同时保留监控log参考值用于核验。 ## 安装 @@ -33,11 +33,11 @@ ### 下载与校验 1. 升级前先关闭所有正在运行的 Log Analysis 窗口。 -2. 下载 [Log Analysis v0.1.0-beta.7 安装程序](https://github.com/Reyppp/log-analysis/releases/download/v0.1.0-beta.7/Log-Analysis-Setup-v0.1.0-beta.7-x64.exe) 和同一版本的 [SHA256SUMS.txt](https://github.com/Reyppp/log-analysis/releases/download/v0.1.0-beta.7/SHA256SUMS.txt)。也可以先查看[完整版本说明](https://github.com/Reyppp/log-analysis/releases/tag/v0.1.0-beta.7)。 +2. 下载 [Log Analysis v0.1.0-beta.8 安装程序](https://github.com/Reyppp/log-analysis/releases/download/v0.1.0-beta.8/Log-Analysis-Setup-v0.1.0-beta.8-x64.exe) 和同一版本的 [SHA256SUMS.txt](https://github.com/Reyppp/log-analysis/releases/download/v0.1.0-beta.8/SHA256SUMS.txt)。也可以先查看[完整版本说明](https://github.com/Reyppp/log-analysis/releases/tag/v0.1.0-beta.8)。 3. 在下载目录打开 PowerShell,运行: ```powershell -Get-FileHash -Algorithm SHA256 .\Log-Analysis-Setup-v0.1.0-beta.7-x64.exe +Get-FileHash -Algorithm SHA256 .\Log-Analysis-Setup-v0.1.0-beta.8-x64.exe Get-Content .\SHA256SUMS.txt ``` @@ -45,7 +45,7 @@ Get-Content .\SHA256SUMS.txt ### 安装与首次使用 -1. 双击 `Log-Analysis-Setup-v0.1.0-beta.7-x64.exe`。 +1. 双击 `Log-Analysis-Setup-v0.1.0-beta.8-x64.exe`。 2. 在安装向导中选择安装目录;程序默认安装到当前用户目录,不需要管理员权限,桌面快捷方式可选。 3. 桌面位于其他磁盘或使用目录链接时,安装程序会把已验证的开始菜单快捷方式复制到 Windows 登记的真实桌面路径;创建失败也不会中断应用安装。 4. 当前版本是未签名测试版。若 SmartScreen 显示“Windows 已保护你的电脑”,请先确认 SHA-256 已匹配,再选择“更多信息”,核对文件名后选择“仍要运行”。如果组织安全策略禁止运行,请联系管理员,不要关闭系统安全功能。 diff --git a/RELEASE_NOTES.md b/RELEASE_NOTES.md index 7dccf88..b29f141 100644 --- a/RELEASE_NOTES.md +++ b/RELEASE_NOTES.md @@ -1,8 +1,16 @@ -# Log Analysis v0.1.0-beta.7 +# Log Analysis v0.1.0-beta.8 -安装与打包可靠性修复,保留 beta.6 的双日志分析与设备工作台功能。 +双日志关联扩展到未完成炉次和局部工控时间段,保留 beta.7 的安装与设备工作台修复。 -## 主要更新 +## 关联更新 + +- 关联范围改为实际完成层。未完成尾部理论层继续保留,不参与材料顺序、持续时间和时钟偏移计算。 +- 工控log只覆盖部分镀膜时间时,可自动匹配监控log中唯一的连续层区间;局部关联至少需要 30 层。 +- 支持忽略日志边缘最多一个首段和一个末段,以兼容工控log从某层中途开始或结束。 +- 唯一高可信候选优先于中可信候选;同级最高候选不唯一时停止自动关联并提示复核。 +- 综合总览新增理论层数、已完成层数、已关联层数和关联层范围。未关联层继续使用监控log转速参考值。 + +## 既有能力 - 安装向导始终显示安装目录页面,可在安装前选择目标位置。 - 桌面快捷方式改为复制已验证的开始菜单快捷方式,避免 Windows 在重定向桌面路径上直接保存 `.lnk` 失败。 @@ -11,7 +19,7 @@ - 自动识别日期命名的工控log和逐层监控log,并根据文件夹内容提供综合、监控log、工控log三种分析范围。 - 综合模式自动匹配镀膜层顺序并校正两类日志的时间偏移;关联失败时保留两类日志的独立分析,不强制合并。 -- 综合设备支持层数和时间两种横坐标。时间图以监控log时间为基准,工控log与监控log共享每张图最多 8,000 个真实采样点。 +- 综合设备支持层数和时间两种横坐标。时间图以监控log时间为基准,展示两类日志中的实际采样值。 - 时间图顶部增加同步镀膜层副坐标。层号保持水平,并根据图表宽度自动控制密度。 - 工控log提供独立总览和连续设备趋势,覆盖电气、真空、温度、转速及 O₂/Ar 气体通道。 - 层范围、材料、监控方式和区段筛选同步作用于可计算的汇总、图表与表格。 @@ -22,16 +30,16 @@ - 综合模式精简为总览和设备,工控log精简为总览和设备,监控log保留五个分析模块。 - 设备页统一图表尺寸、指标选择栏、H/L 配色、图例顺序和数据详情格式。 - 缩放、平移和坐标轴拖动只改变本地视口,不重新请求设备数据,也不重置 Pan 状态。 -- 工控log趋势使用圆点显示;综合时间图使用高密度圆点,避免跨层或断轴连接出不存在的趋势。 +- 工控log与综合时间趋势使用圆点显示,避免跨层或断轴连接出不存在的趋势。 - 单选 H 或 L 时只显示该材料参与工作的采样,未工作时间不占横轴。 - 监控log“异常关联”更名为“异常复核”,移除 Spearman 相关表格;综合和工控log不再展示独立异常页面。 ## 数据与性能 - 工控log连续数据在服务端会话中缓存,指标与筛选切换不重复读取原始文件。 -- 高密度采样保留真实边界点和桶内极值,不计算均值、不插值。 +- 时间趋势优先保留真实边界点和桶内极值,不计算均值、不插值。 - 关联算法允许跳过夹在完整镀膜序列之间的非镀膜工作段,避免额外工控序列导致整炉关联失败。 -- 37 项自动测试覆盖单源、双源、时间对齐、8,000 点预算、筛选、桌面接口、安装器和公开网站。 +- 自动测试覆盖单源、双源、未完成炉次、局部区间、候选冲突、时间对齐、采样预算、筛选、桌面接口、安装器和公开网站。 ## 系统要求 @@ -40,7 +48,7 @@ ## 安装与校验 -同时下载 `Log-Analysis-Setup-v0.1.0-beta.7-x64.exe` 和 `SHA256SUMS.txt`。使用 PowerShell 的 `Get-FileHash` 核对 SHA-256 后运行安装程序。 +同时下载 `Log-Analysis-Setup-v0.1.0-beta.8-x64.exe` 和 `SHA256SUMS.txt`。使用 PowerShell 的 `Get-FileHash` 核对 SHA-256 后运行安装程序。 ## 已知限制 diff --git a/analyzer.py b/analyzer.py index 7677af3..db874d9 100644 --- a/analyzer.py +++ b/analyzer.py @@ -935,17 +935,34 @@ def _align_sources(layer_summary: pd.DataFrame, segments: pd.DataFrame) -> tuple summary["target_time_delta"] = math.nan summary["machine_segment_id"] = math.nan reviews: list[dict[str, Any]] = [] + planned_layers = len(summary) + required = [column for column in ("layer_start", "layer_end", "actual_time") if column in summary] + complete_mask = summary[required].notna().all(axis=1) if len(required) == 3 else pd.Series(False, index=summary.index) + completed_layers = int(complete_mask.sum()) failed = { "status": "failed", "confidence": "关联失败", "matched_segments": 0, "candidate_count": 0, "duration_correlation": None, "time_offset_seconds": None, "offset_std_seconds": None, + "coverage": None, "planned_layers": planned_layers, "completed_layers": completed_layers, + "matched_layer_start": None, "matched_layer_end": None, + "ignored_machine_edge_segments": [], "candidate_ranges": [], "reason": "未找到与监控log材料顺序一致的连续工控工作时段。", } - if summary.empty or segments.empty or summary["layer_start"].isna().any(): + if summary.empty or segments.empty or completed_layers == 0: failed["reason"] = "镀膜层时间或工控工作时段不足,无法自动关联。" reviews.append({"source": "跨源关联", "layer": None, "material": None, "metric": "alignment", "value": None, "level": "重点复核", "reason": failed["reason"]}) return failed, summary, pd.DataFrame(reviews, columns=CROSS_SOURCE_COLUMNS) + last_completed = complete_mask[complete_mask].index[-1] + if not complete_mask.loc[:last_completed].all(): + failed["reason"] = "已完成镀膜层之间存在时间数据缺口,未自动关联。" + reviews.append({"source": "跨源关联", "layer": None, "material": None, "metric": "alignment", "value": None, "level": "重点复核", "reason": failed["reason"]}) + return failed, summary, pd.DataFrame(reviews, columns=CROSS_SOURCE_COLUMNS) + if completed_layers < planned_layers and completed_layers < 30: + failed["reason"] = "未完成炉次少于 30 个连续已完成层,未自动关联。" + reviews.append({"source": "跨源关联", "layer": None, "material": None, "metric": "alignment", "value": completed_layers, "level": "重点复核", "reason": failed["reason"]}) + return failed, summary, pd.DataFrame(reviews, columns=CROSS_SOURCE_COLUMNS) + completed = summary.loc[complete_mask].reset_index(drop=True) eligible = segments[segments["material"].isin(["H", "L"])].sort_values("start").reset_index(drop=True) - target = summary["material"].astype(str).str.strip().tolist() + target = completed["material"].astype(str).str.strip().tolist() def candidate_metrics(monitor: pd.DataFrame, candidate: pd.DataFrame) -> tuple[float, float, float, float]: monitor_duration = pd.to_numeric(monitor["actual_time"], errors="coerce").reset_index(drop=True) @@ -958,18 +975,58 @@ def candidate_metrics(monitor: pd.DataFrame, candidate: pd.DataFrame) -> tuple[f score = (-1 if not math.isfinite(correlation) else correlation) - offset_std / 1000 return score, correlation, offset_std, float(offsets.median()) - candidates = [] + def confidence_for(correlation: float, offset_std: float) -> str | None: + if math.isfinite(correlation) and correlation >= 0.98 and offset_std <= 5: + return "高可信" + if math.isfinite(correlation) and correlation >= 0.90 and offset_std <= 15: + return "中可信" + return None + + def make_candidate( + monitor: pd.DataFrame, machine: pd.DataFrame, coverage: str, ignored: list[int] | None = None, + ) -> dict[str, Any]: + score, correlation, offset_std, offset = candidate_metrics(monitor, machine) + layer_start = int(monitor["layer"].iloc[0]) + layer_end = int(monitor["layer"].iloc[-1]) + return { + "score": score, "correlation": correlation, "offset_std": offset_std, "offset": offset, + "monitor": monitor.reset_index(drop=True), "machine": machine.reset_index(drop=True), + "coverage": coverage, "ignored": ignored or [], "layer_start": layer_start, "layer_end": layer_end, + "confidence": confidence_for(correlation, offset_std), + } + + def candidate_range(candidate: dict[str, Any]) -> dict[str, Any]: + machine = candidate["machine"] + return { + "layer_start": candidate["layer_start"], "layer_end": candidate["layer_end"], + "confidence": candidate["confidence"], + "duration_correlation": candidate["correlation"], + "time_offset_seconds": candidate["offset"], "offset_std_seconds": candidate["offset_std"], + "machine_segment_start": int(machine["segment_id"].iloc[0]), + "machine_segment_end": int(machine["segment_id"].iloc[-1]), + } + + def select_candidate(candidates: list[dict[str, Any]]) -> tuple[dict[str, Any] | None, list[dict[str, Any]]]: + for confidence in ("高可信", "中可信"): + tier = [candidate for candidate in candidates if candidate["confidence"] == confidence] + if len(tier) == 1: + return tier[0], tier + if len(tier) > 1: + return None, tier + return None, [] + + candidates: list[dict[str, Any]] = [] for start in range(len(eligible) - len(target) + 1): candidate = eligible.iloc[start:start + len(target)] if candidate["material"].tolist() != target: continue - candidates.append((*candidate_metrics(summary, candidate), candidate)) + candidates.append(make_candidate(completed, candidate, "all_completed")) - if not candidates and "layer_end" in summary: - gap_seconds = pd.to_datetime(summary["layer_start"], errors="coerce").sub( - pd.to_datetime(summary["layer_end"], errors="coerce").shift() + if not candidates: + gap_seconds = pd.to_datetime(completed["layer_start"], errors="coerce").sub( + pd.to_datetime(completed["layer_end"], errors="coerce").shift() ).dt.total_seconds() - monitor_runs = [group.reset_index(drop=True) for _, group in summary.groupby(gap_seconds.ge(300).cumsum())] + monitor_runs = [group.reset_index(drop=True) for _, group in completed.groupby(gap_seconds.ge(300).cumsum())] if len(monitor_runs) > 1: run_options: list[list[tuple[int, int, pd.DataFrame]]] = [] for monitor_run in monitor_runs: @@ -1008,33 +1065,93 @@ def candidate_metrics(monitor: pd.DataFrame, candidate: pd.DataFrame) -> tuple[f if not paths: break candidates.extend( - (*metrics, pd.concat([item[2] for item in path], ignore_index=True)) - for path, _, metrics in paths + make_candidate(completed, pd.concat([item[2] for item in path], ignore_index=True), "all_completed") + for path, _, _ in paths ) + + chosen, conflicts = select_candidate(candidates) + if chosen is None and conflicts: + failed["candidate_count"] = len(candidates) + failed["candidate_ranges"] = [candidate_range(candidate) for candidate in conflicts] + failed["reason"] = "存在多个同级最高可信候选区间,未自动关联。" + reviews.append({"source": "跨源关联", "layer": None, "material": None, "metric": "alignment", "value": None, "level": "重点复核", "reason": failed["reason"]}) + return failed, summary, pd.DataFrame(reviews, columns=CROSS_SOURCE_COLUMNS) + + partial_candidates: list[dict[str, Any]] = [] + if chosen is None and len(completed) >= 30: + session_groups = [group.sort_values("start").reset_index(drop=True) for _, group in eligible.groupby("session_id", sort=False)] if "session_id" in eligible else [eligible] + + def search_partial(machine: pd.DataFrame, ignored: list[int]) -> list[dict[str, Any]]: + if not 30 <= len(machine) < len(completed): + return [] + machine_target = machine["material"].astype(str).str.strip().tolist() + found = [] + for start in range(len(completed) - len(machine) + 1): + monitor = completed.iloc[start:start + len(machine)] + if monitor["material"].astype(str).str.strip().tolist() == machine_target: + found.append(make_candidate(monitor, machine, "partial_completed", ignored)) + return found + + for group in session_groups: + partial_candidates.extend(search_partial(group, [])) + partial_chosen, partial_conflicts = select_candidate(partial_candidates) + + if partial_chosen is None: + exact_conflicts = partial_conflicts + partial_conflicts = [] + for variants in (((1, 0), (0, 1)), ((1, 1),)): + edge_candidates = [] + for group in session_groups: + for drop_first, drop_last in variants: + stop = len(group) - drop_last if drop_last else len(group) + machine = group.iloc[drop_first:stop].reset_index(drop=True) + ignored_rows = pd.concat([group.iloc[:drop_first], group.iloc[stop:]], ignore_index=True) + ignored = [int(value) for value in ignored_rows.get("segment_id", pd.Series(dtype=int)).tolist()] + edge_candidates.extend(search_partial(machine, ignored)) + partial_candidates.extend(edge_candidates) + partial_chosen, partial_conflicts = select_candidate(edge_candidates) + if partial_chosen is not None or partial_conflicts: + break + if partial_chosen is None and not partial_conflicts: + partial_conflicts = exact_conflicts + + candidates.extend(partial_candidates) + if partial_chosen is not None: + chosen = partial_chosen + elif partial_conflicts: + failed["candidate_count"] = len(candidates) + failed["candidate_ranges"] = [candidate_range(candidate) for candidate in partial_conflicts] + failed["reason"] = "存在多个同级最高可信候选区间,未自动关联。" + reviews.append({"source": "跨源关联", "layer": None, "material": None, "metric": "alignment", "value": None, "level": "重点复核", "reason": failed["reason"]}) + return failed, summary, pd.DataFrame(reviews, columns=CROSS_SOURCE_COLUMNS) + failed["candidate_count"] = len(candidates) - if not candidates: + failed["candidate_ranges"] = [candidate_range(candidate) for candidate in candidates if candidate["confidence"]] + if chosen is None and not candidates: reviews.append({"source": "跨源关联", "layer": None, "material": None, "metric": "alignment", "value": None, "level": "重点复核", "reason": failed["reason"]}) return failed, summary, pd.DataFrame(reviews, columns=CROSS_SOURCE_COLUMNS) - _, correlation, offset_std, offset, matched = max(candidates, key=lambda item: item[0]) - if math.isfinite(correlation) and correlation >= 0.98 and offset_std <= 5: - confidence = "高可信" - elif math.isfinite(correlation) and correlation >= 0.90 and offset_std <= 15: - confidence = "中可信" - else: + if chosen is None: + best = max(candidates, key=lambda item: item["score"]) failed.update( - candidate_count=len(candidates), duration_correlation=correlation, - time_offset_seconds=offset, offset_std_seconds=offset_std, + candidate_count=len(candidates), duration_correlation=best["correlation"], + time_offset_seconds=best["offset"], offset_std_seconds=best["offset_std"], reason="材料顺序匹配,但持续时间相关性或时钟偏移稳定性不足。", ) - reviews.append({"source": "跨源关联", "layer": None, "material": None, "metric": "alignment", "value": correlation, "level": "重点复核", "reason": failed["reason"]}) + reviews.append({"source": "跨源关联", "layer": None, "material": None, "metric": "alignment", "value": best["correlation"], "level": "重点复核", "reason": failed["reason"]}) return failed, summary, pd.DataFrame(reviews, columns=CROSS_SOURCE_COLUMNS) - matched = matched.copy().reset_index(drop=True) - matched["layer"] = summary["layer"].astype(int).tolist() + + correlation, offset_std, offset = chosen["correlation"], chosen["offset_std"], chosen["offset"] + confidence = chosen["confidence"] + matched_monitor = chosen["monitor"] + matched = chosen["machine"].copy().reset_index(drop=True) + matched["layer"] = matched_monitor["layer"].astype(int).tolist() matched_by_layer = matched.set_index("layer") layer_index = summary["layer"] for suffix in ("mean", "std", "min", "max", "range"): - summary[f"motor_{suffix}"] = layer_index.map(matched_by_layer[f"motor_{suffix}"]) - summary["motor_source"] = "工控log" + mapped = layer_index.map(matched_by_layer[f"motor_{suffix}"]) + summary.loc[mapped.notna(), f"motor_{suffix}"] = mapped[mapped.notna()] + matched_rows = layer_index.isin(matched_by_layer.index) + summary.loc[matched_rows, "motor_source"] = "工控log" summary["target_power_seconds"] = layer_index.map(matched_by_layer["target_seconds"]) summary["target_time_delta"] = summary["target_power_seconds"] - summary["actual_time"] summary["machine_segment_id"] = layer_index.map(matched_by_layer["segment_id"]) @@ -1068,8 +1185,13 @@ def candidate_metrics(monitor: pd.DataFrame, candidate: pd.DataFrame) -> tuple[f "status": "matched", "confidence": confidence, "matched_segments": len(matched), "candidate_count": len(candidates), "duration_correlation": correlation, "time_offset_seconds": offset, "offset_std_seconds": offset_std, + "coverage": chosen["coverage"], "planned_layers": planned_layers, + "completed_layers": completed_layers, "matched_layer_start": chosen["layer_start"], + "matched_layer_end": chosen["layer_end"], + "ignored_machine_edge_segments": chosen["ignored"], + "candidate_ranges": [candidate_range(candidate) for candidate in candidates if candidate["confidence"]], "machine_start": matched["start"].min(), "machine_end": matched["end"].max(), - "reason": "材料顺序、持续时间和时钟偏移满足自动关联条件。", + "reason": "材料顺序、持续时间和时钟偏移满足自动关联条件。" if chosen["coverage"] == "all_completed" else "工控log与监控log的连续层区间满足自动关联条件。", } return alignment, summary, pd.DataFrame(reviews, columns=CROSS_SOURCE_COLUMNS) diff --git a/index.html b/index.html index 86321e8..cc2bf35 100644 --- a/index.html +++ b/index.html @@ -21,7 +21,7 @@ const SESSION_TOKEN=new URLSearchParams(location.search).get('token')||'';const withToken=url=>{if(!SESSION_TOKEN)return url;const join=url.includes('?')?'&':'?';return `${url}${join}token=${encodeURIComponent(SESSION_TOKEN)}`}; const FIELD_META={layer:{label:'层号',digits:0},material:{label:'材料'},method:{label:'监控方式'},phy_thick:{label:'物理厚度',unit:'nm',digits:2},recipe_rate:{label:'理论速率',unit:'nm/s',digits:3},planned_time:{label:'理论时间',unit:'s',digits:1},start_t:{label:'理论 Start T',unit:'%',digits:2,percent:true},end_t:{label:'理论 End T',unit:'%',digits:2,percent:true},recipe_extreme:{label:'理论极值状态'},final_meas:{label:'终点 Meas',unit:'%',digits:2,percent:true},final_calc:{label:'终点动态 Calc',unit:'%',digits:2,percent:true},fit_end_residual:{label:'终点 Meas−动态 Calc',unit:'%',digits:2,percent:true},recipe_end_offset:{label:'Meas−理论 End T',unit:'%',digits:2,percent:true},fit_mae:{label:'全层 MAE',unit:'%',digits:2,percent:true},fit_rmse:{label:'全层 RMSE',unit:'%',digits:2,percent:true},fit_p95_abs:{label:'P95 绝对残差',unit:'%',digits:2,percent:true},fit_max_abs:{label:'最大绝对残差',unit:'%',digits:2,percent:true},actual_time:{label:'实际镀膜时间',unit:'s',digits:1},time_delta:{label:'实际−理论时间',unit:'s',digits:1},actual_rate:{label:'实际速率',unit:'nm/s',digits:3},rate_delta:{label:'实际−理论速率',unit:'nm/s',digits:3},actual_extreme:{label:'实际极值状态'},signal_median:{label:'信号中位数',unit:'%',digits:2,percent:true},signal_std:{label:'信号标准差',unit:'%',digits:2,percent:true},signal_cv:{label:'信号变异系数',unit:'%',digits:2,percent:true},optical_rows:{label:'光学采样数',digits:0},substrate_shutter_mismatch:{label:'基片挡板不一致次数',digits:0},material_shutter_mismatch:{label:'材料挡板不一致次数',digits:0},shutter_mismatch:{label:'挡板不一致次数',digits:0},layer_start:{label:'层开始时间'},layer_end:{label:'层结束时间'},machine_span:{label:'设备记录跨度',unit:'s',digits:1},machine_rows:{label:'设备采样数',digits:0},severity:{label:'级别'},scope:{label:'范围'},issue:{label:'问题'},detail:{label:'详情'},metric:{label:'指标'},value:{label:'数值',digits:4},change:{label:'相邻层变化',digits:4},group:{label:'分组基准'},baseline:{label:'分组中位数',digits:4},mad:{label:'MAD',digits:4},robust_z:{label:'稳健 Z 分数',digits:2},level:{label:'复核等级'},reason:{label:'异常依据'},source:{label:'数据源'},field:{label:'字段'},count:{label:'样本数',digits:0},mean:{label:'均值',digits:4},min:{label:'最小值',digits:4},max:{label:'最大值',digits:4},SamTime:{label:'采样时间',unit:'s'},thickness:{label:'理论厚度',unit:'nm'},calculated_t:{label:'理论透过率',unit:'%',percent:true}}; const DEVICE_BASE={power:{label:'有效功率',unit:'kW',digits:3},current:{label:'有效电流',unit:'A',digits:3},voltage:{label:'有效电压',unit:'V',digits:2},vacuum:{label:'腔体真空',unit:'日志原始值',digits:4},chamber_temp:{label:'腔体温度',unit:'°C',digits:2},water_in:{label:'进水温度',unit:'°C',digits:2},water_out:{label:'出水温度',unit:'°C',digits:2},motor:{label:'转速',unit:'rpm',digits:1},o2:{label:'O₂',unit:'日志原始值',digits:3},ar:{label:'Ar',unit:'日志原始值',digits:3}},SUFFIX={mean:'均值',std:'标准差',min:'最小值',max:'最大值',range:'范围'};for(const [base,b] of Object.entries(DEVICE_BASE))for(const [suffix,s] of Object.entries(SUFFIX))FIELD_META[`${base}_${suffix}`]={label:`${b.label}${s}`,unit:b.unit,digits:b.digits}; -Object.assign(FIELD_META,{session_id:{label:'区段编号',digits:0},session_name:{label:'区段'},start:{label:'开始时间'},end:{label:'结束时间'},elapsed_seconds:{label:'记录跨度',unit:'s',digits:0},target_seconds:{label:'靶材工作时间',unit:'s',digits:0},is_default:{label:'默认区段'},rows:{label:'采样数',digits:0},category:{label:'类别'},time:{label:'开始时间'},end_time:{label:'结束时间'},motor_source:{label:'转速来源'},target_power_seconds:{label:'靶材工作时间',unit:'s',digits:0},target_time_delta:{label:'靶材工作时间 - 实际镀膜时间',unit:'s',digits:0},machine_segment_id:{label:'工控工作时段',digits:0},alignment:{label:'关联状态'},confidence:{label:'关联状态'},matched_segments:{label:'镀膜层数',digits:0},candidate_count:{label:'候选序列数',digits:0},duration_correlation:{label:'持续时间相关系数',digits:5},time_offset_seconds:{label:'时钟偏移',unit:'s',digits:1},offset_std_seconds:{label:'偏移标准差',unit:'s',digits:2},sample_gap:{label:'采样时间缺口',unit:'s',digits:0},simultaneous_power:{label:'H/L 同时工作',unit:'s',digits:0},'H Power':{label:'H 材料功率',unit:'kW',digits:3},'L Power':{label:'L 材料功率',unit:'kW',digits:3},'H Current':{label:'H 材料电流',unit:'A',digits:3},'L Current':{label:'L 材料电流',unit:'A',digits:3},'H Voltage':{label:'H 材料电压',unit:'V',digits:2},'L Voltage':{label:'L 材料电压',unit:'V',digits:2},'Motor Speed':{label:'转速',unit:'rpm',digits:1},'Chammber Vacuum':{label:'腔体真空',unit:'日志原始值',digits:6},'Pipe Vacuum':{label:'管道真空',unit:'日志原始值',digits:6},'Chammber Temperture':{label:'腔体温度',unit:'°C',digits:1},'Water-In':{label:'进水温度',unit:'°C',digits:2},'Water-Out':{label:'出水温度',unit:'°C',digits:2},'Gas-O2(1)':{label:'O₂ 通道 1',unit:'日志原始值',digits:3},'Gas-O2(4)':{label:'O₂ 通道 4',unit:'日志原始值',digits:3},'Gas-Ar(2)':{label:'Ar 通道 2',unit:'日志原始值',digits:3},'Gas-Ar(3)':{label:'Ar 通道 3',unit:'日志原始值',digits:3}});for(const suffix of Object.keys(SUFFIX))FIELD_META[`monitor_motor_${suffix}`]={label:`监控log转速参考值${SUFFIX[suffix]}`,unit:'rpm',digits:1}; +Object.assign(FIELD_META,{session_id:{label:'区段编号',digits:0},session_name:{label:'区段'},start:{label:'开始时间'},end:{label:'结束时间'},elapsed_seconds:{label:'记录跨度',unit:'s',digits:0},target_seconds:{label:'靶材工作时间',unit:'s',digits:0},is_default:{label:'默认区段'},rows:{label:'采样数',digits:0},category:{label:'类别'},time:{label:'开始时间'},end_time:{label:'结束时间'},motor_source:{label:'转速来源'},target_power_seconds:{label:'靶材工作时间',unit:'s',digits:0},target_time_delta:{label:'靶材工作时间 - 实际镀膜时间',unit:'s',digits:0},machine_segment_id:{label:'工控工作时段',digits:0},alignment:{label:'关联状态'},confidence:{label:'关联状态'},coverage:{label:'关联覆盖'},planned_layers:{label:'理论层数',digits:0},completed_layers:{label:'已完成层数',digits:0},matched_segments:{label:'已关联层数',digits:0},matched_layer_start:{label:'关联起始层',digits:0},matched_layer_end:{label:'关联结束层',digits:0},matched_layer_range:{label:'关联层范围'},ignored_machine_edge_segments:{label:'忽略的首尾工控时段'},candidate_ranges:{label:'候选层范围'},candidate_count:{label:'候选序列数',digits:0},duration_correlation:{label:'持续时间相关系数',digits:5},time_offset_seconds:{label:'时钟偏移',unit:'s',digits:1},offset_std_seconds:{label:'偏移标准差',unit:'s',digits:2},sample_gap:{label:'采样时间缺口',unit:'s',digits:0},simultaneous_power:{label:'H/L 同时工作',unit:'s',digits:0},'H Power':{label:'H 材料功率',unit:'kW',digits:3},'L Power':{label:'L 材料功率',unit:'kW',digits:3},'H Current':{label:'H 材料电流',unit:'A',digits:3},'L Current':{label:'L 材料电流',unit:'A',digits:3},'H Voltage':{label:'H 材料电压',unit:'V',digits:2},'L Voltage':{label:'L 材料电压',unit:'V',digits:2},'Motor Speed':{label:'转速',unit:'rpm',digits:1},'Chammber Vacuum':{label:'腔体真空',unit:'日志原始值',digits:6},'Pipe Vacuum':{label:'管道真空',unit:'日志原始值',digits:6},'Chammber Temperture':{label:'腔体温度',unit:'°C',digits:1},'Water-In':{label:'进水温度',unit:'°C',digits:2},'Water-Out':{label:'出水温度',unit:'°C',digits:2},'Gas-O2(1)':{label:'O₂ 通道 1',unit:'日志原始值',digits:3},'Gas-O2(4)':{label:'O₂ 通道 4',unit:'日志原始值',digits:3},'Gas-Ar(2)':{label:'Ar 通道 2',unit:'日志原始值',digits:3},'Gas-Ar(3)':{label:'Ar 通道 3',unit:'日志原始值',digits:3}});for(const suffix of Object.keys(SUFFIX))FIELD_META[`monitor_motor_${suffix}`]={label:`监控log转速参考值${SUFFIX[suffix]}`,unit:'rpm',digits:1}; const DEVICE_GROUPS={电气:['power_mean','current_mean','voltage_mean'],环境:['vacuum_mean'],温度:['chamber_temp_mean','water_in_mean','water_out_mean'],转速:['motor_mean'],气体:['o2_mean','ar_mean']},COLORS=['#075e91','#c5691a','#6d68a8','#267451']; const NAV={combined:[['combined-overview','总览'],['combined-device','设备']],machine:[['machine-overview','总览'],['machine-trend','设备']],monitor:[['overview','总览'],['theory','理论趋势'],['optical','光学'],['device','设备'],['anomaly','异常复核']]}; const MACHINE_METRICS={power:{label:'功率',unit:'kW',group:'电气',api:'power_mean',columns:['H Power','L Power']},current:{label:'电流',unit:'A',group:'电气',api:'current_mean',columns:['H Current','L Current']},voltage:{label:'电压',unit:'V',group:'电气',api:'voltage_mean',columns:['H Voltage','L Voltage']},'Chammber Vacuum':{label:'腔体真空',unit:'日志原始值',group:'真空',api:'vacuum_mean',columns:['Chammber Vacuum']},'Pipe Vacuum':{label:'管道真空',unit:'日志原始值',group:'真空',api:'pipe_vacuum',columns:['Pipe Vacuum']},'Chammber Temperture':{label:'腔体温度',unit:'°C',group:'温度',api:'chamber_temp_mean',columns:['Chammber Temperture']},'Water-In':{label:'进水温度',unit:'°C',group:'温度',api:'water_in_mean',columns:['Water-In']},'Water-Out':{label:'出水温度',unit:'°C',group:'温度',api:'water_out_mean',columns:['Water-Out']},'Motor Speed':{label:'转速',unit:'rpm',group:'转速',api:'motor_mean',columns:['Motor Speed']},o2:{label:'O₂',unit:'日志原始值',group:'气体',api:'o2_mean',columns:['Gas-O2(1)','Gas-O2(4)']},ar:{label:'Ar',unit:'日志原始值',group:'气体',api:'ar_mean',columns:['Gas-Ar(2)','Gas-Ar(3)']}}; @@ -33,7 +33,7 @@ class ApiError extends Error{constructor(message,code='request_failed',recovery='请稍后重试。'){super(message);this.code=code;this.recovery=recovery}}function setService(state,text){$('service').dataset.state=state;$('status').textContent=text}function setBusy(kind,on,text){if(kind==='choose')$('choose').disabled=on;else{$('choose').disabled=$('analyze').disabled=on;$('threshold').disabled=on}$('skeleton').classList.toggle('hidden',!(on&&kind==='analyze'));setService(on?'busy':'online',text||'本地服务已连接')}async function api(url,options={}){let r;const headers={...(options.headers||{})};if(SESSION_TOKEN)headers['X-Log-Analysis-Token']=SESSION_TOKEN;try{r=await fetch(url,{...options,headers})}catch(error){if(error?.name==='AbortError')throw error;throw new ApiError('无法连接本地分析服务。','service_unavailable','请重新启动 Log Analysis,然后点击“重新连接”。')}let d;try{d=await r.json()}catch{throw new ApiError('本地服务返回了无法识别的响应。','invalid_response','请重新启动分析工具后重试。')}if(!r.ok)throw new ApiError(d.error||'请求未完成。',d.code||'request_failed',d.recovery||'请检查炉次文件夹后重试。');return d} function resetAnalysisView(){if(viewState.focused)toggleChartFocus(viewState.focused.querySelector('.focus-chart'));payload=null;Object.assign(viewState,{scope:'combined',activeByScope:{combined:'combined-overview',machine:'machine-overview',monitor:'overview'},active:'combined-overview',selectedLayer:1,opticalResidual:'fit_end_residual',opticalSignal:'signal_median',deviceMetric:'power_mean',anomalyMetric:'',combinedDeviceAxis:'layer',machineMetric:'power',machineSession:''});$('controls').classList.add('hidden');$('source-switch').classList.add('hidden');$('sidebar').classList.add('hidden');$('modules').replaceChildren();$('scope-options').replaceChildren();$('layer-min').value=$('layer-max').value='';$('threshold').value='3.5';$('material').replaceChildren();$('method').replaceChildren();$('page').replaceChildren();$('empty').classList.remove('hidden');$('empty').querySelector('h2').textContent='日志文件夹已选择,请开始分析';$('empty').querySelector('p').textContent='已清空上一份分析结果。原始日志保持只读。'} function availableScopes(){const s=payload?.sources||{};return s.combined?['combined','monitor','machine']:s.machine?['machine']:['monitor']} -function alignmentLabel(alignment=payload?.alignment||{}){if(alignment.status!=='matched')return '关联失败';return alignment.confidence==='中可信'?'已关联(待复核)':'已关联'} +function alignmentLabel(alignment=payload?.alignment||{}){if(alignment.status!=='matched')return '关联失败';const pending=alignment.confidence==='中可信',partial=alignment.coverage==='partial_completed';if(partial)return pending?'已关联(部分区间,待复核)':'已关联(部分区间)';return pending?'已关联(待复核)':'已关联'} function renderSourceSwitch(){const names={combined:'综合',machine:'工控log',monitor:'监控log'},available=availableScopes();$('scope-options').innerHTML=available.map(scope=>``).join('');const s=payload.sources;$('source-note').textContent=s.combined?`${alignmentLabel()} ${payload.alignment.matched_segments||0} 个镀膜层`:s.machine?'当前仅包含工控log':'当前仅包含监控log';$('source-switch').classList.remove('hidden')} function renderNavigation(){const items=NAV[viewState.scope]||[];if(!items.some(([key])=>key===viewState.active))viewState.active=viewState.activeByScope[viewState.scope]||items[0]?.[0];$('modules').innerHTML=items.map(([key,name])=>``).join('')} function setScope(scope){if(!availableScopes().includes(scope))return;viewState.activeByScope[viewState.scope]=viewState.active;viewState.scope=scope;viewState.active=viewState.activeByScope[scope]||NAV[scope][0][0];renderSourceSwitch();renderNavigation();updateToolbar();renderActivePage()} @@ -140,7 +140,7 @@ } function renderMachineTrend(){cancelDeviceSeries();const available=availableMachineMetrics();if(!available.includes(viewState.machineMetric))viewState.machineMetric=available[0]||'power';const spec=MACHINE_METRICS[viewState.machineMetric];$('page').innerHTML=pageHeader('设备','按区段与材料查看工控采样;关联成功时显示镀膜层')+`
自动关联工控log与监控log,汇总逐层理论、光学与设备数据,定位需复核层。全程本地运行,原始日志只读。
+自动关联工控log与监控log,汇总逐层理论、光学与设备数据。支持未完成炉次和局部工控时间段,全程本地只读运行。
程序自动识别工控log与监控log。切换分析范围后,模块、筛选条件和结果在同一界面内更新。演示使用合成数据。
+程序自动识别工控log与监控log。完整炉次按全部已完成层关联;局部工控时间段匹配唯一的连续监控层区间。演示使用合成数据。
双源完整性、关联状态与当前筛选统计
v0.1.0-beta.7 修复重定向桌面快捷方式与本地打包环境污染问题。当前版本未进行代码签名。已知问题:部分环境下单层详情图表或原始截图可能不显示。
v0.1.0-beta.8 支持未完成炉次与局部工控时间段关联,并保留快捷方式和打包可靠性修复。当前版本未进行代码签名。已知问题:部分环境下单层详情图表或原始截图可能不显示。
升级前先退出所有正在运行的 Log Analysis 窗口。
同时下载 Log-Analysis-Setup-v0.1.0-beta.7-x64.exe 和 SHA256SUMS.txt。
在下载目录运行 Get-FileHash -Algorithm SHA256 .\Log-Analysis-Setup-v0.1.0-beta.7-x64.exe,与校验文件比较。
同时下载 Log-Analysis-Setup-v0.1.0-beta.8-x64.exe 和 SHA256SUMS.txt。
在下载目录运行 Get-FileHash -Algorithm SHA256 .\Log-Analysis-Setup-v0.1.0-beta.8-x64.exe,与校验文件比较。
双击安装程序并选择位置;桌面快捷方式可选,失败时不影响应用安装。
哈希匹配后,如 SmartScreen 警告可选择“更多信息”并核对文件名;组织策略阻止时请联系管理员。
从开始菜单打开应用,点击“选择文件夹”,再点击“开始分析”。