update server

This commit is contained in:
2026-07-30 11:12:31 +08:00
commit 4312cb878c
99 changed files with 24034 additions and 0 deletions
+167
View File
@@ -0,0 +1,167 @@
#!/usr/bin/env python3
# -*- coding: utf-8 -*-
"""
自动控制 GUI 界面脚本 - 通过模拟用户操作设置目标压力
升级功能:
1. 加入坐标校准功能,摆脱写死的硬编码坐标
2. 自动寻找并置顶 GUI 窗口
3. 加入 PyAutoGUI 故障保护 (防失控)
使用方法:
1. 首次使用建议进行校准: python auto_test.py --calibrate --targets 50 80 100
2. 后续固定窗口位置后直接运行: python auto_test.py --targets 50 80 100
python tool/auto_test.py --calibrate --targets 50 80 100 180 170 130 200 210 270 290 280 250 175 165 100 45
"""
import argparse
import time
import platform
import pyautogui
try:
import pygetwindow as gw
except ImportError:
gw = None
# 配置 PyAutoGUI
pyautogui.FAILSAFE = True # 将鼠标移动到屏幕四个角落可紧急停止脚本
pyautogui.PAUSE = 0.3 # 每个动作后默认停顿 0.3 秒,让 UI 有时间反应
# 平台相关的全选快捷键:macOS 用 commandWindows/Linux 用 ctrl
_MODIFIER_KEY = 'command' if platform.system() == 'Darwin' else 'ctrl'
class GUIController:
def __init__(self):
# 默认坐标 (如果不使用 calibrate 模式,将使用这些备用坐标)
# 注意:这些默认值是错误的,请务必使用 --calibrate 参数校准
self.input_x, self.input_y = 200, 150
self.btn_x, self.btn_y = 320, 150
def activate_window(self, title_keyword="ReinLoop"):
"""尝试寻找并激活目标窗口(支持部分标题匹配)"""
if gw is None:
print("⚠️ 未安装 pygetwindow,请手动确保 GUI 窗口在前台。")
print(" 安装命令: pip install pygetwindow")
return
print(f"正在寻找包含 '{title_keyword}' 的窗口...")
try:
windows = gw.getWindowsWithTitle(title_keyword)
if windows:
win = windows[0]
if win.isMinimized:
win.restore()
win.activate()
print(f"✅ 成功激活窗口: {win.title}")
time.sleep(1) # 等待窗口彻底弹出
else:
print(f"⚠️ 未找到包含 '{title_keyword}' 的窗口。")
print(f" 当前所有窗口列表:")
all_wins = gw.getAllWindows()
for w in all_wins:
if w.title.strip():
print(f" - {w.title}")
print(" 请确保 ReinLoop GUI 已打开,或使用 --calibrate 后手动置顶窗口。")
except Exception as e:
print(f"⚠️ 窗口激活失败: {e},请手动将窗口切换到前台。")
def calibrate(self):
"""交互式坐标校准,动态获取按钮位置"""
print("\n" + "=" * 40)
print("🔧 进入坐标校准模式 (请不要切走窗口)")
print("=" * 40)
print("\n👉 请在 5 秒内将鼠标光标移动到【目标压力输入框】中心...")
for i in range(5, 0, -1):
print(f"\r倒计时: {i}", end='')
time.sleep(1)
self.input_x, self.input_y = pyautogui.position()
print(f"\n✅ 输入框坐标已记录: ({self.input_x}, {self.input_y})")
print("\n👉 请在 5 秒内将鼠标光标移动到【设置目标】按钮中心...")
for i in range(5, 0, -1):
print(f"\r倒计时: {i}", end='')
time.sleep(1)
self.btn_x, self.btn_y = pyautogui.position()
print(f"\n✅ 按钮坐标已记录: ({self.btn_x}, {self.btn_y})")
print("=" * 40 + "\n")
def set_target_pressure(self, target):
"""模拟用户操作设置目标压力"""
print(f"▶ 正在设置目标压力: {target} kPa")
try:
# 点击输入框
pyautogui.click(x=self.input_x, y=self.input_y)
# 全选并删除现有内容(macOS: command+a, Windows/Linux: ctrl+a
pyautogui.hotkey(_MODIFIER_KEY, 'a')
pyautogui.press('backspace')
# 输入新的目标压力值
pyautogui.typewrite(str(target))
# 点击"设置目标"按钮
pyautogui.click(x=self.btn_x, y=self.btn_y)
print(f"✅ 成功设置目标压力: {target} kPa")
return True
except Exception as e:
print(f"❌ 设置目标压力失败: {e}")
return False
def auto_control(targets, interval, do_calibrate):
print("=" * 60)
print("🤖 GUI 自动控制脚本启动")
print("提示: 运行过程中将鼠标移动到屏幕四个角落即可紧急停止")
print("=" * 60)
controller = GUIController()
controller.activate_window()
if do_calibrate:
controller.calibrate()
else:
print(
f"️ 使用默认坐标 (输入框: {controller.input_x},{controller.input_y} | "
f"按钮: {controller.btn_x},{controller.btn_y})")
print("⚠️ 如果点击位置不准确,请使用 --calibrate 参数运行脚本。")
print("\n3秒后开始自动控制序列...")
time.sleep(3)
for i, target in enumerate(targets):
print(f"\n--- 步骤 {i + 1}/{len(targets)} ---")
if not controller.set_target_pressure(target):
print(f"❌ 步骤 {i + 1} 出现异常,提前终止自动控制")
break
if i < len(targets) - 1:
print(f"等待 {interval} 秒...")
for j in range(interval, 0, -1):
print(f"\r剩余时间: {j}", end='')
time.sleep(1)
print()
print("\n🎉 自动控制序列全部完成!")
def main():
parser = argparse.ArgumentParser(description='GUI 自动控制脚本')
parser.add_argument('--targets', type=float, nargs='+', default=[50, 80, 100, 120],
help='目标压力值列表,用空格隔开,单位 kPa')
parser.add_argument('--interval', type=int, default=10,
help='每个目标压力持续时间,单位秒')
parser.add_argument('--calibrate', action='store_true',
help='启动坐标校准模式,动态获取输入框和按钮的屏幕坐标')
args = parser.parse_args()
auto_control(args.targets, args.interval, args.calibrate)
if __name__ == "__main__":
main()
+285
View File
@@ -0,0 +1,285 @@
import os
import pickle
import glob
def load_and_merge_pickle_chunks(folder_path, file_pattern="*.pkl"):
"""
从指定文件夹中读取所有匹配的分片文件,解包并合并成一个总的数据列表。
Args:
folder_path: 存放 .pkl 分片文件的文件夹路径
file_pattern: 文件匹配模式,默认匹配所有 .pkl 文件
"""
all_episodes = []
# 获取所有匹配的 pkl 文件路径,并按名称排序(确保 part1, part2 顺序或逻辑清晰)
search_path = os.path.join(folder_path, file_pattern)
file_list = sorted(glob.glob(search_path))
if not file_list:
print(f"❌ 未在路径 【{folder_path}】 下找到任何匹配 【{file_pattern}】 的文件!")
return []
print(f"📂 找到 {len(file_list)} 个数据分片文件,开始加载...")
for file_path in file_list:
try:
with open(file_path, 'rb') as f:
# 每个分片解包出来都是一个 list [ep1, ep2, ...]
chunk_data = pickle.load(f)
if isinstance(chunk_data, list):
all_episodes.extend(chunk_data)
print(f" ✅ 成功加载: {os.path.basename(file_path)} (包含 {len(chunk_data)} 个 Episode)")
else:
print(f" ⚠️ 警告: {os.path.basename(file_path)} 解析出的数据格式不是列表,跳过。")
except Exception as e:
print(f" ❌ 读取文件 {os.path.basename(file_path)} 失败: {e}")
print(f"整个序列加载完成,共合并了 {len(all_episodes)} 个 Episode。")
return all_episodes
def analyze_episodes_data(episode_data_raw):
"""
分析 Episode 数据,统计超调情况。
"""
total_episodes = len(episode_data_raw)
if total_episodes == 0:
print("没有数据可供分析。")
return
invalid_count = 0 # 最后一步误差绝对值 > 2 kPa 的无效 episode
invalid_high_flow = 0 # 无效 episode 中流量 > 200
invalid_low_flow = 0 # 无效 episode 中流量 < 100
all_steady_abs_errors = [] # 所有有效 episode 的稳态误差(绝对值)
no_overshoot_count = 0
no_overshoot_abs_errors = [] # 绝对值稳态误差
no_overshoot_raw_errors = [] # 带符号稳态误差(+ = 高于目标, - = 低于目标)
overshoot_lt_1_count = 0
overshoot_1_to_2_count = 0
overshoot_2_to_3_count = 0
overshoot_3_to_4_count = 0
overshoot_4_to_5_count = 0
overshoot_5_to_10_count = 0
overshoot_gt_10_count = 0
overshoots_5_to_10 = []
overshoots_gt_10 = []
for idx, ep in enumerate(episode_data_raw):
pressures = ep.get('pressures', [])
target_p = ep.get('target_pressure', 0.0)
if not pressures:
continue
# 最后一步误差绝对值 > 2 kPa → 无效 episode,跳过
errors = ep.get('errors', [])
if errors and abs(errors[-1]) > 2:
invalid_count += 1
q = ep.get('Q_in', 0)
if q > 200:
invalid_high_flow += 1
elif q < 100:
invalid_low_flow += 1
continue
initial_p = pressures[0]
# 所有有效 episode 的稳态误差(最后 30 步绝对值均值)
if errors:
last_n = errors[-30:] if len(errors) >= 30 else errors
all_steady_abs_errors.append(sum(abs(e) for e in last_n) / len(last_n))
is_step_up = target_p >= initial_p # 升压为 True,降压为 False
overshoot = 0.0
if is_step_up:
# 升压:最大值大于目标压力为超调
max_p = max(pressures)
if max_p > target_p:
overshoot = max_p - target_p
else:
# 降压:最小值小于目标压力为超调
min_p = min(pressures)
if min_p < target_p:
overshoot = target_p - min_p
# 统计区间
if overshoot == 0:
no_overshoot_count += 1
elif overshoot < 1.0:
overshoot_lt_1_count += 1
# 最后 30 步的平均误差作为稳态误差(分别记录绝对值和带符号值)
if len(errors) >= 30:
last_30 = errors[-30:]
elif errors:
last_30 = errors
else:
last_30 = []
if last_30:
no_overshoot_abs_errors.append(sum(abs(e) for e in last_30) / len(last_30))
no_overshoot_raw_errors.append(sum(last_30) / len(last_30))
elif 1.0 <= overshoot < 2.0:
overshoot_1_to_2_count += 1
elif 2.0 <= overshoot < 3.0:
overshoot_2_to_3_count += 1
elif 3.0 <= overshoot < 4.0:
overshoot_3_to_4_count += 1
elif 4.0 <= overshoot <= 5.0:
overshoot_4_to_5_count += 1
else:
item = {
"index": idx,
"direction": "升压" if is_step_up else "降压",
"initial_p": initial_p,
"target_p": target_p,
"overshoot_value": round(overshoot, 3),
"Q_in": ep.get("Q_in", 0),
}
if overshoot <= 10.0:
overshoot_5_to_10_count += 1
overshoots_5_to_10.append(item)
else:
overshoot_gt_10_count += 1
overshoots_gt_10.append(item)
# 打印报告
def _pct(n): return f"{n / total_episodes * 100:.1f}%"
print("\n" + "="*25 + " 离线数据分析 " + "="*25)
valid_episodes = total_episodes - invalid_count
print(f"合并后的总 Episode 数 : {total_episodes}")
print(f" - 无效 Episode(末步误差>2: {invalid_count} ({_pct(invalid_count)})")
if invalid_count > 0:
print(f" ├ 流量 > 200 L/min : {invalid_high_flow}")
print(f" └ 流量 < 100 L/min : {invalid_low_flow}")
print(f" - 有效 Episode 数 : {valid_episodes}")
print(f" - 未超调的 Episode 数 : {no_overshoot_count} ({_pct(no_overshoot_count)})")
print(f" - 超调 < 1 kPa : {overshoot_lt_1_count} ({_pct(overshoot_lt_1_count)})")
print(f" - 超调在 1 ~ 2 kPa 之间 : {overshoot_1_to_2_count} ({_pct(overshoot_1_to_2_count)})")
print(f" - 超调在 2 ~ 3 kPa 之间 : {overshoot_2_to_3_count} ({_pct(overshoot_2_to_3_count)})")
print(f" - 超调在 3 ~ 4 kPa 之间 : {overshoot_3_to_4_count} ({_pct(overshoot_3_to_4_count)})")
print(f" - 超调在 4 ~ 5 kPa 之间 : {overshoot_4_to_5_count} ({_pct(overshoot_4_to_5_count)})")
print(f" - 超调在 5 ~ 10 kPa 之间 : {overshoot_5_to_10_count} ({_pct(overshoot_5_to_10_count)})")
print(f" - 超调 > 10 kPa : {overshoot_gt_10_count} ({_pct(overshoot_gt_10_count)})")
print("=" * 68)
def _print_detail(title, items):
if items:
print(f"\n[⚠️ {title}]:")
for item in items:
print(f" * Episode [{item['index']}] ({item['direction']}): "
f"初始 {item['initial_p']:.2f} -> 目标 {item['target_p']:.2f} | "
f"超调量: {item['overshoot_value']:.2f} kPa | "
f"流量: {item['Q_in']:.1f} L/min")
_print_detail("超调在 5 ~ 10 kPa", overshoots_5_to_10)
_print_detail("超调大于 10 kPa", overshoots_gt_10)
if not overshoots_5_to_10 and not overshoots_gt_10:
print("\n🎉 极好!没有发现超调大于 5 kPa 的数据。")
# ---- 流量分布统计 ----
flow_bins = [
(0, 10), (10, 50), (50, 100), (100, 150),
(150, 200), (200, 250), (250, 300),
]
flow_counts = {f"{lo}~{hi}": 0 for lo, hi in flow_bins}
flow_counts["300+"] = 0
for ep in episode_data_raw:
q = ep.get('Q_in', 0)
placed = False
for lo, hi in flow_bins:
if lo <= q < hi:
flow_counts[f"{lo}~{hi}"] += 1
placed = True
break
if not placed:
flow_counts["300+"] += 1
print(f"\n📊 流量分布统计 (共 {total_episodes} 个 Episode):")
for lo, hi in flow_bins:
label = f"{lo}~{hi}"
print(f" {label:>10} L/min : {flow_counts[label]:>5} ({flow_counts[label]/total_episodes*100:5.1f}%)")
print(f" {'300+':>10} L/min : {flow_counts['300+']:>5} ({flow_counts['300+']/total_episodes*100:5.1f}%)")
if all_steady_abs_errors:
avg_all = sum(all_steady_abs_errors) / len(all_steady_abs_errors)
print(f"\n📊 所有有效 Episode 平均稳态误差(最后 30 步绝对值均值): {avg_all:.3f} kPa"
f" ({len(all_steady_abs_errors)} 个 Episode)")
if no_overshoot_abs_errors:
avg_abs = sum(no_overshoot_abs_errors) / len(no_overshoot_abs_errors)
avg_raw = sum(no_overshoot_raw_errors) / len(no_overshoot_raw_errors)
print(f"\n📊 超调01kpa Episode 平均稳态误差(最后 30 步):")
print(f" 绝对值均值 : {avg_abs:.3f} kPa")
print(f" 带符号均值 : {avg_raw:.3f} kPa ({'偏高于目标' if avg_raw > 0 else '偏低' if avg_raw < 0 else '无偏'})"
f" ({no_overshoot_count} 个 Episode)")
def print_episode_detail(episode_data_raw, index):
"""打印指定 episode 的完整数据"""
if index < 0 or index >= len(episode_data_raw):
print(f"❌ Episode 索引 {index} 超出范围 (0~{len(episode_data_raw)-1})")
return
ep = episode_data_raw[index]
print(f"\n{'='*60}")
print(f" Episode [{index}] 完整数据")
print(f"{'='*60}")
for key in ['Q_in', 'volume', 'target_pressure', 'mode']:
if key in ep:
print(f" {key}: {ep[key]}")
pressures = ep.get('pressures', [])
errors = ep.get('errors', [])
valve_openings = ep.get('valves', [])
print(f"\n 步数: {len(pressures)}")
if pressures:
print(f" 初始压力: {pressures[0]:.2f} kPa")
print(f" 最终压力: {pressures[-1]:.2f} kPa")
print(f" 目标压力: {ep.get('target_pressure', 'N/A')} kPa")
if errors:
print(f" 最终误差: {errors[-1]:.3f} kPa")
print(f"\n {'步':>4s} {'压力(kPa)':>10s} {'误差(kPa)':>10s} {'开度(%)':>8s}")
print(f" {'-'*36}")
n = len(pressures)
for i in range(n):
p = pressures[i]
e = errors[i] if i < len(errors) else float('nan')
vo = valve_openings[i] if i < len(valve_openings) else float('nan')
print(f" {i:4d} {p:10.2f} {e:10.3f} {vo:8.2f}")
print(f"{'='*60}\n")
# --- 执行离线分析 ---
if __name__ == "__main__":
# 💡 数据存放文件夹路径
DATA_FOLDER = "/Users/menglingrui/Documents/DominatedConvergence/cloud_down_file/永久/data_8L"
# 1. 读取并合并分片
merged_data = load_and_merge_pickle_chunks(DATA_FOLDER, file_pattern="*part*.pkl")
# 2. 执行分析
if merged_data:
analyze_episodes_data(merged_data)
# 3. 找出无效 episode(末步误差绝对值 > 2 kPa),打印前 3 个的完整数据
# invalid_indices = []
# for idx, ep in enumerate(merged_data):
# errors = ep.get('errors', [])
# if errors and abs(errors[-1]) > 2:
# invalid_indices.append(idx)
# if len(invalid_indices) >= 3:
# break
# if invalid_indices:
# print(f"\n找到 {len(invalid_indices)} 个无效 Episode,索引: {invalid_indices}")
# for idx in invalid_indices:
# print_episode_detail(merged_data, idx)
# else:
# print("\n未找到无效 Episode")
print_episode_detail(merged_data, 2500)
+2024
View File
File diff suppressed because it is too large Load Diff
@@ -0,0 +1,10 @@
parameter,value
q_in_val,50.0
dt,0.1
n_order,6
t_c,2.5
levels,"10,20,30,40,50,60,70,80"
dead_area,240.0
xa_full,1000.0
V_val,5.0
repeat,2
1 parameter value
2 q_in_val 50.0
3 dt 0.1
4 n_order 6
5 t_c 2.5
6 levels 10,20,30,40,50,60,70,80
7 dead_area 240.0
8 xa_full 1000.0
9 V_val 5.0
10 repeat 2
+61
View File
@@ -0,0 +1,61 @@
import matplotlib
import shutil
import os
# 获取 Matplotlib 缓存目录
cache_dir = matplotlib.get_cachedir()
print(f"正在清理缓存目录: {cache_dir}")
# 删除缓存
if os.path.exists(cache_dir):
shutil.rmtree(cache_dir)
print("字体缓存已清除!请重新运行你的主程序。")
else:
print("未找到缓存目录。")
import os
import matplotlib
matplotlib.use('TkAgg')
import matplotlib.pyplot as plt
import matplotlib.font_manager as fm
# ----------------- 强制解决中文乱码 (Mac版) -----------------
def force_chinese_font_mac():
"""强制加载 macOS 系统自带的苹方或黑体"""
# macOS 常见中文字体路径
font_paths = [
"/System/Library/Fonts/PingFang.ttc", # 苹方 (现代 macOS 默认中文字体)
"/System/Library/Fonts/STHeiti Light.ttc", # 华文黑体
"/System/Library/Fonts/STHeiti Medium.ttc", # 华文黑体 (中等粗细)
"/System/Library/Fonts/Supplemental/Songti.ttc", # 宋体 (部分较新 macOS 系统的路径)
"/Library/Fonts/Arial Unicode.ttf" # 包含中文的通用字体
]
font_loaded = False
for path in font_paths:
if os.path.exists(path):
try:
# 强制将字体加入 Matplotlib 的内存库
fm.fontManager.addfont(path)
# 获取该字体在 matplotlib 内部的真实名称
prop = fm.FontProperties(fname=path)
plt.rcParams['font.family'] = prop.get_name()
font_loaded = True
print(f"已成功加载 Mac 系统字体: {path}")
break # 加载成功一个就跳出
except Exception as e:
print(f"尝试加载字体 {path} 失败: {e}")
continue
if not font_loaded:
print("警告: 未在 macOS 默认路径找到中文字体文件。")
# 解决负号 '-' 显示为方块的问题
plt.rcParams['axes.unicode_minus'] = False
# 立即执行字体加载
force_chinese_font_mac()
# ----------------------------------------------------
@@ -0,0 +1,26 @@
"""已迁移至 ControlPanel 的辨识反馈管理能力。"""
import argparse
def submit_feedback(customer: str, result: int, run_id=None, timeout=20):
raise RuntimeError("辨识反馈已迁移至 ControlPanel,客户端不提供管理接口")
def main():
parser = argparse.ArgumentParser(description="提交辨识结果 0/1")
parser.add_argument("customer", help="许可证中的客户名称")
parser.add_argument("result", type=int, choices=(0, 1), help="1=通过,0=未通过")
parser.add_argument("--run-id", help="可选:限定当前辨识 CSV 文件名")
args = parser.parse_args()
try:
data = submit_feedback(args.customer, args.result, args.run_id)
except Exception as exc:
print(f"提交失败: {exc}")
return 1
state = "已通过" if data["result"] == 1 else "未通过"
print(f"提交成功:{state}runId={data.get('runId', '')}")
return 0
if __name__ == "__main__":
raise SystemExit(main())
@@ -0,0 +1,10 @@
{
"q_in_val": 50.0,
"dt": 0.05,
"p_max": 200.0,
"fit_low": 50.0,
"fit_high": 150.0,
"T_delta": 30.0,
"xa_full": 1000.0,
"num_runs": 3
}