| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778779780781782783784785786787788789790 |
- # -*- 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('<span style="color:%s;font-weight:%s;">%s</span>' % (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(
- '<span style="color:%s;font-weight:600;">%s</span>' % (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(
- '<span style="color:#16a34a;font-weight:600;">✓ 扫描完成</span> '
- '<span style="color:#7c3aed;">磁负刚度 (N/mm): %s</span>' % 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('<span style="color:#dc2626;font-weight:600;">✗ 运行出错, 详见日志</span>')
- 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()
|