# -*- coding: utf-8 -*- """ MARS 轴向磁拉力仿真工具 — PyQt5 GUI (现代工业风 v2) ==================================================== 双击运行 (或 python main.py), 调整参数后点击运行, 无需 AI token。 """ import os import sys import json import time import traceback from PyQt5.QtCore import Qt, QThread, pyqtSignal, QObject, QSize from PyQt5.QtWidgets import ( QApplication, QMainWindow, QWidget, QVBoxLayout, QHBoxLayout, QGridLayout, QLabel, QLineEdit, QPushButton, QFileDialog, QCheckBox, QTabWidget, QTextEdit, QGroupBox, QDoubleSpinBox, QSpinBox, QTableWidget, QTableWidgetItem, QHeaderView, QMessageBox, QSplitter, QFrame, QSizePolicy, QStatusBar, QScrollArea ) from PyQt5.QtGui import QFont, QColor, QPalette BASE = os.path.dirname(os.path.abspath(__file__)) DEFAULT_MOT = os.path.normpath( os.path.join(BASE, "..", "MARS-12S10P_SSSR_D76-C150_V5.0-0819.mot")) DEFAULT_OUT = os.path.normpath(os.path.join(BASE, "..", "output_motorcad")) DEFAULT_MC_EXE = r"D:\Program Files\ANSYS Inc\v261\motorcad\MotorCAD.exe" # ============================================================ # 现代工业风 QSS — 浅灰蓝底 + 白色卡片 + 工业蓝强调 # ============================================================ STYLE = """ * { font-family: "Microsoft YaHei", "Segoe UI", sans-serif; font-size: 10pt; } QMainWindow { background: #eef1f5; } /* --- 卡片式分组 --- */ QGroupBox { font-weight: 600; font-size: 11pt; color: #1e293b; border: 1px solid #e2e8f0; border-radius: 8px; margin-top: 16px; padding-top: 14px; padding-left: 10px; padding-right: 10px; background: #ffffff; } QGroupBox::title { subcontrol-origin: margin; left: 14px; padding: 0 8px; background: #ffffff; color: #1e293b; } /* --- 标签 --- */ QLabel { color: #334155; font-size: 10pt; } QLabel#cardValue { font-size: 22pt; font-weight: 700; color: #1e293b; } QLabel#cardUnit { font-size: 11pt; color: #64748b; font-weight: 400; } QLabel#cardLabel { font-size: 9pt; color: #94a3b8; font-weight: 500; letter-spacing: 1px; } QLabel#sectionTitle { font-size: 11pt; font-weight: 600; color: #1e293b; padding-bottom: 4px; } /* --- 输入框 --- */ QLineEdit, QDoubleSpinBox, QSpinBox { padding: 6px 10px; border: 1px solid #cbd5e1; border-radius: 6px; background: #f8fafc; color: #1e293b; font-size: 10pt; min-height: 22px; selection-background-color: #2563eb; selection-color: white; } QLineEdit:focus, QDoubleSpinBox:focus, QSpinBox:focus { border: 1px solid #2563eb; background: #ffffff; } QDoubleSpinBox::up-button, QSpinBox::up-button, QDoubleSpinBox::down-button, QSpinBox::down-button { width: 18px; } /* --- 按钮 --- */ QPushButton { padding: 8px 20px; border-radius: 6px; font-size: 10pt; font-weight: 600; border: 1px solid #2563eb; background: #2563eb; color: white; } QPushButton:hover { background: #1d4ed8; border-color: #1d4ed8; } QPushButton:pressed { background: #1e40af; } QPushButton:disabled { background: #94a3b8; border-color: #94a3b8; color: #e2e8f0; } QPushButton#secondary { background: #ffffff; color: #2563eb; border: 1px solid #cbd5e1; } QPushButton#secondary:hover { background: #f1f5f9; border-color: #2563eb; } QPushButton#secondary:disabled { color: #94a3b8; background: #f1f5f9; border-color: #e2e8f0; } /* --- 日志区 (浅色终端风) --- */ QTextEdit#logView { background: #f8fafc; color: #334155; font-family: "Consolas", "Cascadia Code", "Courier New", monospace; font-size: 9.5pt; border: 1px solid #e2e8f0; border-radius: 8px; padding: 10px; line-height: 1.5; } /* --- Tab --- */ QTabWidget::pane { border: 1px solid #e2e8f0; border-radius: 8px; top: -1px; background: #ffffff; } QTabBar::tab { padding: 8px 24px; font-size: 10pt; font-weight: 600; color: #64748b; background: #f1f5f9; border: 1px solid #e2e8f0; border-bottom: none; border-top-left-radius: 6px; border-top-right-radius: 6px; margin-right: 4px; } QTabBar::tab:selected { background: #ffffff; color: #2563eb; border-bottom: 2px solid #2563eb; } QTabBar::tab:hover:!selected { background: #e2e8f0; color: #334155; } /* --- 表格 --- */ QTableWidget { gridline-color: #f1f5f9; font-size: 10pt; color: #334155; border: 1px solid #e2e8f0; border-radius: 6px; background: #ffffff; alternate-background-color: #f8fafc; } QTableWidget::item { padding: 6px; } QHeaderView::section { background: #1e293b; color: #f1f5f9; padding: 8px; font-weight: 600; font-size: 9.5pt; border: none; } /* --- 状态栏 --- */ QStatusBar { background: #1e293b; color: #e2e8f0; font-size: 9.5pt; } QStatusBar QLabel { color: #e2e8f0; font-size: 9.5pt; padding: 2px 8px; } /* --- 滚动条 --- */ QScrollBar:vertical { width: 10px; background: #f1f5f9; border-radius: 5px; } QScrollBar::handle:vertical { background: #cbd5e1; border-radius: 5px; min-height: 30px; } QScrollBar::handle:vertical:hover { background: #94a3b8; } QScrollBar::add-line:vertical, QScrollBar::sub-line:vertical { height: 0; } QScrollBar:horizontal { height: 10px; background: #f1f5f9; border-radius: 5px; } QScrollBar::handle:horizontal { background: #cbd5e1; border-radius: 5px; min-width: 30px; } QScrollBar::add-line:horizontal, QScrollBar::sub-line:horizontal { width: 0; } /* --- 复选框 --- */ QCheckBox { spacing: 8px; color: #334155; font-size: 10pt; } QCheckBox::indicator { width: 18px; height: 18px; border: 2px solid #cbd5e1; border-radius: 4px; background: #ffffff; } QCheckBox::indicator:checked { background: #2563eb; border-color: #2563eb; } """ class SolverWorker(QObject): """后台仿真线程, 通过信号与 GUI 通信。""" log = pyqtSignal(str) result_single = pyqtSignal(dict) result_sweep = pyqtSignal(dict) finished = pyqtSignal() error = pyqtSignal(str) def __init__(self, mode, params): super().__init__() self.mode = mode self.params = params def run(self): try: from solver import Solver p = self.params solver = Solver( mot_path=p["mot_path"], out_dir=p["out_dir"], rms_current=p["rms_current"], speed_rpm=p["speed_rpm"], airgap_mm=p["airgap_mm"], magnet_temp_c=p["magnet_temp_c"], sec_radii_mm=p.get("sec_radii_mm", [28.25, 34.75]), motorcad_exe=p.get("motorcad_exe") or None, keep_open=p.get("keep_open", True), log_cb=lambda msg: self.log.emit(msg), ) if self.mode == "single": results = solver.run_single() self.result_single.emit(results) else: results = solver.run_sweep(p["sweep_gaps"]) self.result_sweep.emit(results) except Exception as e: tb = traceback.format_exc() self.error.emit("%s\n%s" % (e, tb)) finally: self.finished.emit() class ResultCard(QFrame): """结果数值卡片: 大数字 + 标签 + 单位""" def __init__(self, label, unit="N"): super().__init__() self.setFrameShape(QFrame.StyledPanel) self.setStyleSheet(""" ResultCard { background: #ffffff; border: 1px solid #e2e8f0; border-radius: 10px; } """) lay = QVBoxLayout(self) lay.setContentsMargins(16, 14, 16, 14) lay.setSpacing(2) self.lbl_label = QLabel(label) self.lbl_label.setObjectName("cardLabel") self.lbl_value = QLabel("—") self.lbl_value.setObjectName("cardValue") self.lbl_value.setAlignment(Qt.AlignLeft | Qt.AlignVCenter) self.lbl_unit = QLabel(unit) self.lbl_unit.setObjectName("cardUnit") val_row = QHBoxLayout() val_row.setSpacing(4) val_row.addWidget(self.lbl_value) val_row.addWidget(self.lbl_unit) val_row.addStretch() lay.addWidget(self.lbl_label) lay.addLayout(val_row) def set_value(self, val, fmt="%.1f"): self.lbl_value.setText(fmt % val if val is not None else "—") def set_color(self, color): self.lbl_value.setStyleSheet("color: %s;" % color) class MainWindow(QMainWindow): def __init__(self): super().__init__() self.setWindowTitle("MARS 轴向磁拉力仿真工具") self.resize(1200, 780) self.setMinimumSize(1024, 680) self.worker = None self.thread = None self.last_results = None self.last_mode = None self._build_ui() self._set_status("就绪", "#94a3b8") def _build_ui(self): central = QWidget() self.setCentralWidget(central) root = QHBoxLayout(central) root.setContentsMargins(12, 12, 12, 8) root.setSpacing(12) # ============ 左侧参数面板 ============ left = QVBoxLayout() left.setSpacing(10) # 模型与路径 gb_file = QGroupBox("模型与路径") gl = QGridLayout(gb_file) gl.setSpacing(8) gl.setVerticalSpacing(10) self.ed_mot = QLineEdit(DEFAULT_MOT) btn_mot = QPushButton("浏览") btn_mot.setObjectName("secondary") btn_mot.setFixedWidth(64) btn_mot.clicked.connect(self._browse_mot) gl.addWidget(QLabel("模型文件"), 0, 0) gl.addWidget(self.ed_mot, 0, 1) gl.addWidget(btn_mot, 0, 2) self.ed_out = QLineEdit(DEFAULT_OUT) btn_out = QPushButton("浏览") btn_out.setObjectName("secondary") btn_out.setFixedWidth(64) btn_out.clicked.connect(self._browse_out) gl.addWidget(QLabel("输出目录"), 1, 0) gl.addWidget(self.ed_out, 1, 1) gl.addWidget(btn_out, 1, 2) self.ed_mc = QLineEdit(DEFAULT_MC_EXE) gl.addWidget(QLabel("MC exe 路径"), 2, 0) gl.addWidget(self.ed_mc, 2, 1, 1, 2) left.addWidget(gb_file) # 工况参数 gb_param = QGroupBox("工况参数") gp = QGridLayout(gb_param) gp.setSpacing(8) gp.setVerticalSpacing(10) self.sp_rms = QDoubleSpinBox(); self.sp_rms.setRange(0, 200); self.sp_rms.setValue(21.0); self.sp_rms.setSuffix(" A") self.sp_speed = QSpinBox(); self.sp_speed.setRange(100, 100000); self.sp_speed.setValue(5000); self.sp_speed.setSuffix(" rpm") self.sp_airgap = QDoubleSpinBox(); self.sp_airgap.setRange(0.1, 10); self.sp_airgap.setSingleStep(0.1); self.sp_airgap.setValue(1.0); self.sp_airgap.setSuffix(" mm") self.sp_temp = QDoubleSpinBox(); self.sp_temp.setRange(-40, 200); self.sp_temp.setValue(100.0); self.sp_temp.setSuffix(" °C") gp.addWidget(QLabel("RMS 电流"), 0, 0); gp.addWidget(self.sp_rms, 0, 1) gp.addWidget(QLabel("转速"), 0, 2); gp.addWidget(self.sp_speed, 0, 3) gp.addWidget(QLabel("气隙"), 1, 0); gp.addWidget(self.sp_airgap, 1, 1) gp.addWidget(QLabel("磁钢温度"), 1, 2); gp.addWidget(self.sp_temp, 1, 3) self.cb_keep = QCheckBox("运行后保持 Motor-CAD 打开") self.cb_keep.setChecked(True) gp.addWidget(self.cb_keep, 2, 0, 1, 4) left.addWidget(gb_param) # 运行模式 Tab self.tabs = QTabWidget() # 单点 tab_single = QWidget() ts = QVBoxLayout(tab_single) ts.setContentsMargins(8, 12, 8, 8) self.btn_run_single = QPushButton("▶ 运行单点仿真") self.btn_run_single.setMinimumHeight(42) self.btn_run_single.setStyleSheet("font-size: 11pt;") self.btn_run_single.clicked.connect(lambda: self._start_run("single")) ts.addWidget(self.btn_run_single) ts.addStretch() self.tabs.addTab(tab_single, " 单点仿真 ") # 气隙扫描 tab_sweep = QWidget() sw = QGridLayout(tab_sweep) sw.setContentsMargins(8, 12, 8, 8) sw.setSpacing(8) self.sp_gap_start = QDoubleSpinBox(); self.sp_gap_start.setRange(0.1, 10); self.sp_gap_start.setSingleStep(0.1); self.sp_gap_start.setValue(0.6); self.sp_gap_start.setSuffix(" mm") self.sp_gap_end = QDoubleSpinBox(); self.sp_gap_end.setRange(0.1, 10); self.sp_gap_end.setSingleStep(0.1); self.sp_gap_end.setValue(1.5); self.sp_gap_end.setSuffix(" mm") self.sp_gap_step = QDoubleSpinBox(); self.sp_gap_step.setRange(0.05, 5); self.sp_gap_step.setSingleStep(0.05); self.sp_gap_step.setValue(0.2); self.sp_gap_step.setSuffix(" mm") sw.addWidget(QLabel("起始气隙"), 0, 0); sw.addWidget(self.sp_gap_start, 0, 1) sw.addWidget(QLabel("结束气隙"), 0, 2); sw.addWidget(self.sp_gap_end, 0, 3) sw.addWidget(QLabel("步长"), 1, 0); sw.addWidget(self.sp_gap_step, 1, 1) self.lbl_sweep_preview = QLabel("") self.lbl_sweep_preview.setStyleSheet("color: #64748b; font-size: 9pt; padding: 4px 0;") self.lbl_sweep_preview.setWordWrap(True) sw.addWidget(self.lbl_sweep_preview, 1, 2, 1, 2) self.btn_run_sweep = QPushButton("▶ 运行气隙扫描") self.btn_run_sweep.setMinimumHeight(42) self.btn_run_sweep.setStyleSheet("font-size: 11pt;") self.btn_run_sweep.clicked.connect(lambda: self._start_run("sweep")) sw.addWidget(self.btn_run_sweep, 2, 0, 1, 4) sw.setRowStretch(3, 1) for sp in (self.sp_gap_start, self.sp_gap_end, self.sp_gap_step): sp.valueChanged.connect(self._update_sweep_preview) self._update_sweep_preview() self.tabs.addTab(tab_sweep, " 气隙扫描 ") left.addWidget(self.tabs) left.addStretch() left_w = QWidget() left_w.setLayout(left) left_w.setFixedWidth(400) # ============ 右侧: 结果 + 日志 ============ right = QVBoxLayout() right.setSpacing(10) # --- 结果面板 (卡片式) --- gb_result = QGroupBox("运行结果") res_lay = QVBoxLayout(gb_result) res_lay.setSpacing(10) # 卡片行 card_row = QHBoxLayout() card_row.setSpacing(10) self.card_oc = ResultCard("空载净轴向力", "N") self.card_ol = ResultCard("负载净轴向力", "N") self.card_ripple = ResultCard("纹波峰峰", "N") self.card_kneg = ResultCard("磁负刚度", "N/mm") self.card_kneg.setVisible(False) for c in (self.card_oc, self.card_ol, self.card_ripple, self.card_kneg): c.setMinimumHeight(88) card_row.addWidget(c) res_lay.addLayout(card_row) # 判据 + 摘要 self.lbl_checks = QLabel("尚未运行仿真。") self.lbl_checks.setWordWrap(True) self.lbl_checks.setStyleSheet(""" QLabel { background: #f8fafc; border: 1px solid #e2e8f0; border-radius: 6px; padding: 10px 14px; color: #64748b; font-size: 9.5pt; } """) res_lay.addWidget(self.lbl_checks) # 结果表格 self.table_result = QTableWidget(0, 4) self.table_result.setHorizontalHeaderLabels(["工况", "净轴向力 (N)", "纹波峰峰 (N)", "备注"]) self.table_result.horizontalHeader().setSectionResizeMode(QHeaderView.Stretch) self.table_result.verticalHeader().setVisible(False) self.table_result.setAlternatingRowColors(True) self.table_result.setVisible(False) self.table_result.setMaximumHeight(160) res_lay.addWidget(self.table_result) # 导出按钮 btn_row = QHBoxLayout() self.btn_export_json = QPushButton("导出 JSON") self.btn_export_json.setObjectName("secondary") self.btn_export_json.setEnabled(False) self.btn_export_json.clicked.connect(self._export_json) self.btn_export_csv = QPushButton("导出 CSV") self.btn_export_csv.setObjectName("secondary") self.btn_export_csv.setEnabled(False) self.btn_export_csv.clicked.connect(self._export_csv) btn_row.addWidget(self.btn_export_json) btn_row.addWidget(self.btn_export_csv) btn_row.addStretch() res_lay.addLayout(btn_row) right.addWidget(gb_result, 4) # --- 日志区 --- gb_log = QGroupBox("运行日志") log_lay = QVBoxLayout(gb_log) log_lay.setContentsMargins(10, 14, 10, 10) self.txt_log = QTextEdit() self.txt_log.setObjectName("logView") self.txt_log.setReadOnly(True) log_lay.addWidget(self.txt_log) right.addWidget(gb_log, 5) right_w = QWidget() right_w.setLayout(right) splitter = QSplitter(Qt.Horizontal) splitter.addWidget(left_w) splitter.addWidget(right_w) splitter.setStretchFactor(0, 0) splitter.setStretchFactor(1, 1) splitter.setHandleWidth(2) root.addWidget(splitter) # 状态栏 self.statusBar = QStatusBar() self.setStatusBar(self.statusBar) self.lbl_status = QLabel("就绪") self.lbl_runtime = QLabel("") self.statusBar.addWidget(self.lbl_status, 1) self.statusBar.addPermanentWidget(self.lbl_runtime) # ================================================================ # 交互 # ================================================================ def _set_status(self, text, color="#e2e8f0"): self.lbl_status.setText(text) self.lbl_status.setStyleSheet("color: %s;" % color) def _browse_mot(self): path, _ = QFileDialog.getOpenFileName(self, "选择 Motor-CAD 模型", self.ed_mot.text(), "Motor-CAD (*.mot)") if path: self.ed_mot.setText(path) def _browse_out(self): path = QFileDialog.getExistingDirectory(self, "选择输出目录", self.ed_out.text()) if path: self.ed_out.setText(path) def _update_sweep_preview(self): gaps = self._calc_sweep_gaps() if gaps: est = len(gaps) * 2.5 self.lbl_sweep_preview.setText("将求解 %d 个气隙点 (%.1f~%.1fmm), 预计约 %.0f 分钟" % (len(gaps), gaps[0], gaps[-1], est)) else: self.lbl_sweep_preview.setText("参数无效 (起始应 < 结束, 步长 > 0)") def _calc_sweep_gaps(self): s, e, step = self.sp_gap_start.value(), self.sp_gap_end.value(), self.sp_gap_step.value() if s >= e or step <= 0: return [] gaps = [] g = s while g <= e + 1e-9: gaps.append(round(g, 4)) g += step return gaps def _collect_params(self): return { "mot_path": self.ed_mot.text().strip(), "out_dir": self.ed_out.text().strip(), "rms_current": self.sp_rms.value(), "speed_rpm": self.sp_speed.value(), "airgap_mm": self.sp_airgap.value(), "magnet_temp_c": self.sp_temp.value(), "motorcad_exe": self.ed_mc.text().strip(), "keep_open": self.cb_keep.isChecked(), "sec_radii_mm": [28.25, 34.75], } def _start_run(self, mode): if self.worker and self.thread and self.thread.isRunning(): QMessageBox.warning(self, "正在运行", "仿真正在进行中, 请等待完成。") return p = self._collect_params() if not os.path.isfile(p["mot_path"]): QMessageBox.critical(self, "错误", "模型文件不存在:\n%s" % p["mot_path"]) return if mode == "sweep": gaps = self._calc_sweep_gaps() if not gaps: QMessageBox.critical(self, "错误", "气隙扫描参数无效。") return p["sweep_gaps"] = gaps if len(gaps) > 10: reply = QMessageBox.question(self, "确认", "将求解 %d 个气隙点, 预计约 %d 分钟, 确认?" % (len(gaps), len(gaps) * 3)) if reply != QMessageBox.Yes: return # 重置界面 self.last_results = None self.txt_log.clear() self.table_result.setVisible(False) self.card_oc.set_value(None); self.card_oc.set_color("#1e293b") self.card_ol.set_value(None); self.card_ol.set_color("#1e293b") self.card_ripple.set_value(None); self.card_ripple.set_color("#1e293b") self.card_kneg.setVisible(False) self.lbl_checks.setText("仿真运行中...") self.lbl_checks.setStyleSheet(""" QLabel { background: #fffbeb; border: 1px solid #fbbf24; border-radius: 6px; padding: 10px 14px; color: #92400e; font-size: 9.5pt; } """) self.btn_export_json.setEnabled(False) self.btn_export_csv.setEnabled(False) self._set_running(True) self._set_status("● 运行中... Motor-CAD 求解中", "#fbbf24") self.lbl_runtime.setText("") # 启动线程 self.thread = QThread() self.worker = SolverWorker(mode, p) self.worker.moveToThread(self.thread) self.thread.started.connect(self.worker.run) self.worker.log.connect(self._append_log) self.worker.result_single.connect(self._on_single_result) self.worker.result_sweep.connect(self._on_sweep_result) self.worker.error.connect(self._on_error) self.worker.finished.connect(self._on_finished) self.worker.finished.connect(self.thread.quit) self.worker.finished.connect(self.worker.deleteLater) self.thread.finished.connect(self.thread.deleteLater) self.last_mode = mode self._run_start_time = __import__("time").time() self.thread.start() def _set_running(self, running): for btn in (self.btn_run_single, self.btn_run_sweep): btn.setEnabled(not running) if running: self.btn_run_single.setText("⏳ 运行中...") self.btn_run_sweep.setText("⏳ 运行中...") else: self.btn_run_single.setText("▶ 运行单点仿真") self.btn_run_sweep.setText("▶ 运行气隙扫描") def _append_log(self, msg): """彩色日志: 根据内容前缀着色""" msg = msg.rstrip() if not msg: return # 判断颜色 if msg.startswith("[结论]") or "✓" in msg: color = "#16a34a" # 绿 weight = "600" elif msg.startswith("[错误]") or msg.startswith("[ERROR]") or "Traceback" in msg: color = "#dc2626" # 红 weight = "600" elif msg.startswith("[警告]"): color = "#d97706" # 橙 weight = "600" elif msg.startswith("[提示]"): color = "#2563eb" # 蓝 weight = "500" elif msg.startswith("== ") or msg.startswith("启动") or "求解" in msg[:6]: color = "#475569" # 深灰 (阶段标题) weight = "600" elif "RESULTS:" in msg or "结果文件:" in msg or "扫描结果:" in msg: color = "#7c3aed" # 紫 (文件路径) weight = "500" else: color = "#334155" # 默认 weight = "400" import html escaped = html.escape(msg) self.txt_log.append('%s' % (color, weight, escaped)) sb = self.txt_log.verticalScrollBar() sb.setValue(sb.maximum()) def _on_single_result(self, results): try: self.last_results = results forces = results.get("axial_forces", {}) or {} checks = results.get("checks", {}) or {} # 安全访问: forces[graph] 可能为 None oc_data = forces.get("Fr_Rotor_OC_Lumped") or {} ol_data = forces.get("Fr_Rotor_OL_Lumped") or {} oc = oc_data.get("stats", {}) or {} ol = ol_data.get("stats", {}) or {} self.card_oc.set_value(oc.get("mean")) self.card_ol.set_value(ol.get("mean")) self.card_ripple.set_value(oc.get("pk2pk")) self.card_oc.set_color("#16a34a") self.card_ol.set_color("#2563eb") self.card_ripple.set_color("#64748b") # 判据文本 parts = [] ar_ol = (checks.get("action_reaction_OL") or {}).get("imbalance_pct") ar_oc = (checks.get("action_reaction_OC") or {}).get("imbalance_pct") if ar_ol is not None: ok = ar_ol < 5 parts.append(("%s 作用反作用 OL=%.1f%%" % ("✓" if ok else "✗", ar_ol), "#16a34a" if ok else "#dc2626")) if ar_oc is not None: ok = ar_oc < 5 parts.append(("%s OC=%.1f%%" % ("✓" if ok else "✗", ar_oc), "#16a34a" if ok else "#dc2626")) tq = checks.get("torque_crosscheck") or {} if tq.get("sum_Ft_x_r_Nm_t0") and tq.get("torque_graph_mean_Nm"): diff = abs(tq["sum_Ft_x_r_Nm_t0"] - tq["torque_graph_mean_Nm"]) \ / max(abs(tq["torque_graph_mean_Nm"]), 1e-9) * 100 ok = diff < 10 parts.append(("%s 转矩交叉=%.1f%% (%.3f vs %.3f Nm)" % ("✓" if ok else "✗", diff, tq["sum_Ft_x_r_Nm_t0"], tq["torque_graph_mean_Nm"]), "#16a34a" if ok else "#dc2626")) an = checks.get("analytic") or {} if an.get("F_est_N"): parts.append(("解析量级=%.0fN (同量级)" % an["F_est_N"], "#7c3aed")) html_parts = "    ".join( '%s' % (c, t) for t, c in parts ) self.lbl_checks.setText(html_parts or "无判据数据") self.lbl_checks.setStyleSheet(""" QLabel { background: #f0fdf4; border: 1px solid #86efac; border-radius: 6px; padding: 10px 14px; font-size: 9.5pt; } """) # 表格 rows = [] rms = (results.get("params") or {}).get("rms_current", 0) for case, label in (("OC", "空载 (开路)"), ("OL", "负载 (RMS %.0fA)" % rms)): v = forces.get("Fr_Rotor_%s_Lumped" % case) if v and v.get("stats"): s = v["stats"] rows.append((label, "%.1f" % s["mean"], "%.2f" % s["pk2pk"], "指向定子")) self._fill_table(rows) self.btn_export_json.setEnabled(True) self.btn_export_csv.setEnabled(True) except Exception as e: self._append_log("[错误] 结果显示异常: %s" % e) traceback.print_exc() def _on_sweep_result(self, results): try: self.last_results = results cases = results.get("cases", []) or [] kneg = results.get("kneg_N_per_mm", {}) or {} # 卡片: 用第一个和最后一个气隙的力 if cases: first = cases[0] or {} last = cases[-1] or {} foc_first = (first.get("F_OC") or {}).get("mean") foc_last = (last.get("F_OC") or {}).get("mean") self.card_oc.set_value(foc_first) self.card_oc.lbl_label.setText( "起始气隙 %.2fmm 空载力" % (first.get("airgap_mm", 0) or 0)) self.card_ol.set_value(foc_last) self.card_ol.lbl_label.setText( "结束气隙 %.2fmm 空载力" % (last.get("airgap_mm", 0) or 0)) # 平均纹波 ripples = [(c.get("F_OC") or {}).get("pk2pk", 0) for c in cases if (c.get("F_OC") or {}).get("pk2pk") is not None] if ripples: self.card_ripple.set_value(sum(ripples) / len(ripples)) self.card_oc.set_color("#16a34a") self.card_ol.set_color("#2563eb") self.card_ripple.set_color("#64748b") # 磁负刚度卡片 if kneg: central = kneg.get("central") if central is not None: self.card_kneg.set_value(central) self.card_kneg.set_color("#7c3aed") self.card_kneg.setVisible(True) kneg_txt = " | ".join("%s=%.0f" % (k, v) for k, v in kneg.items()) if kneg else "—" self.lbl_checks.setText( '✓ 扫描完成    ' '磁负刚度 (N/mm): %s' % kneg_txt) self.lbl_checks.setStyleSheet(""" QLabel { background: #f0fdf4; border: 1px solid #86efac; border-radius: 6px; padding: 10px 14px; font-size: 9.5pt; } """) rows = [] for c in cases: if not c: continue g = c.get("airgap_mm", 0) foc = (c.get("F_OC") or {}).get("mean", 0) fol = (c.get("F_OL") or {}).get("mean", 0) rip = (c.get("F_OC") or {}).get("pk2pk", 0) rows.append(("气隙 %.2fmm" % g, "OC=%.1f / OL=%.1f" % (foc, fol), "%.2f" % rip, "耗时%.0fs" % (c.get("solve_seconds", 0) or 0))) self._fill_table(rows) self.btn_export_json.setEnabled(True) self.btn_export_csv.setEnabled(False) except Exception as e: self._append_log("[错误] 扫描结果显示异常: %s" % e) traceback.print_exc() def _fill_table(self, rows): self.table_result.setVisible(True) self.table_result.setRowCount(len(rows)) for i, (a, b, c, d) in enumerate(rows): for j, val in enumerate((a, b, c, d)): item = QTableWidgetItem(str(val)) item.setTextAlignment(Qt.AlignCenter) self.table_result.setItem(i, j, item) def _on_error(self, msg): try: self.lbl_checks.setText('✗ 运行出错, 详见日志') self.lbl_checks.setStyleSheet(""" QLabel { background: #fef2f2; border: 1px solid #fca5a5; border-radius: 6px; padding: 10px 14px; font-size: 9.5pt; } """) self._append_log("[错误] %s" % msg) except Exception: traceback.print_exc() def _on_finished(self): try: elapsed = time.time() - getattr(self, "_run_start_time", time.time()) self._set_running(False) # 检查是否有结果 (成功) 还是出错 if self.last_results is not None: self._set_status("✓ 运行完成 — 结果已显示在上方", "#86efac") self.lbl_runtime.setText("总耗时 %.0fs" % elapsed) else: self._set_status("✗ 运行出错 — 详见日志", "#fca5a5") self.lbl_runtime.setText("") except Exception as e: traceback.print_exc() finally: # 延迟清理线程引用, 避免与 deleteLater 竞态 from PyQt5.QtCore import QTimer QTimer.singleShot(100, self._cleanup_thread) def _cleanup_thread(self): self.worker = None self.thread = None def _export_json(self): if not self.last_results: return path, _ = QFileDialog.getSaveFileName(self, "导出 JSON", "axialforce_result.json", "JSON (*.json)") if path: with open(path, "w", encoding="utf-8") as f: json.dump(self.last_results, f, ensure_ascii=False, indent=2) self._append_log("[提示] JSON 已导出 -> %s" % path) def _export_csv(self): if not self.last_results or self.last_mode != "single": return path, _ = QFileDialog.getSaveFileName(self, "导出 CSV", "axial_force.csv", "CSV (*.csv)") if path: forces = self.last_results.get("axial_forces", {}) graphs = ["Fr_Rotor_OL_Lumped", "Fr_Stator_OL_Lumped", "Fr_Rotor_OC_Lumped", "Fr_Stator_OC_Lumped"] nmax = max(len(v["series_N"]) if v else 0 for v in forces.values()) with open(path, "w", encoding="utf-8") as f: f.write("tstep," + ",".join(graphs) + "\n") for i in range(nmax): row = [str(i)] for g in graphs: v = forces.get(g) row.append("%.4f" % v["series_N"][i] if v and i < len(v["series_N"]) else "") f.write(",".join(row) + "\n") self._append_log("[提示] CSV 已导出 -> %s" % path) def main(): app = QApplication(sys.argv) app.setStyle("Fusion") app.setStyleSheet(STYLE) # 关键: 防止子窗口/对话框关闭时触发整个应用退出 app.setQuitOnLastWindowClosed(False) # 全局默认字体 font = QFont("Microsoft YaHei", 10) app.setFont(font) # 全局异常钩子: 捕获未处理异常, 防止 --windowed 模式下静默闪退 def _excepthook(exc_type, exc_value, exc_tb): import traceback msg = "".join(traceback.format_exception(exc_type, exc_value, exc_tb)) try: with open(os.path.join(os.path.expanduser("~"), "axial_force_gui_error.log"), "a", encoding="utf-8") as f: f.write("\n=== %s ===\n%s" % (time.strftime("%Y-%m-%d %H:%M:%S"), msg)) except Exception: pass # 尝试在 GUI 中显示错误 try: QMessageBox.critical(None, "程序异常", "发生未捕获异常:\n%s\n\n详情已写入 ~/axial_force_gui_error.log" % str(exc_value)) except Exception: pass sys.excepthook = _excepthook w = MainWindow() w.show() sys.exit(app.exec_()) if __name__ == "__main__": main()