feat: 重构EGM计算核心并添加实时日志推送功能

将EGM计算逻辑从webview_app.py移到main.py中的run_egm函数
添加实时日志推送和计算结果回调机制
支持后台线程计算不阻塞前端
This commit is contained in:
dmy
2026-03-02 22:49:38 +08:00
parent 47d3b7b6b4
commit 6f0f8d02a8
3 changed files with 168 additions and 248 deletions

49
main.py
View File

@@ -60,18 +60,12 @@ def read_parameter(toml_file_path):
para.max_i = toml_optional["max_i"] para.max_i = toml_optional["max_i"]
def egm(): def run_egm() -> dict:
if len(sys.argv) < 2: """
toml_file_path = r"D:/code/EGM/历史/平乾750kV.toml" 执行 EGM 计算的核心函数,可被外部调用。
else: 假设参数已通过 para 全局对象设置好。
toml_file_path = sys.argv[1] 返回计算结果字典。
if not os.path.exists(toml_file_path): """
logger.info(f"无法找到数据文件{toml_file_path},程序退出。")
sys.exit(0)
logger.info(f"读取文件{toml_file_path}")
read_parameter(toml_file_path)
#########################################################
# 以上是需要设置的参数
parameter_display(para) parameter_display(para)
h_whole = para.h_arm[0] # 挂点高 h_whole = para.h_arm[0] # 挂点高
string_g_len = para.string_g_len string_g_len = para.string_g_len
@@ -356,6 +350,37 @@ def egm():
f"不同相跳闸率是{np.array2string(np.mean(n_sf_phases,axis=1),precision=16)}次/(100km·a)" f"不同相跳闸率是{np.array2string(np.mean(n_sf_phases,axis=1),precision=16)}次/(100km·a)"
) )
return {
"success": True,
"message": "计算完成",
"data": {
"tripping_rate": f"{avr_n_sf:.16f} 次/(100km·a)",
"avr_n_sf": avr_n_sf,
"n_sf_phases": np.mean(n_sf_phases, axis=1).tolist(),
"parameters": {
"rated_voltage": para.rated_voltage,
"td": para.td,
"altitude": para.altitude,
"ground_angels": [a / math.pi * 180 for a in para.ground_angels],
"max_i": para.max_i
}
}
}
def egm():
"""命令行入口函数"""
if len(sys.argv) < 2:
toml_file_path = r"D:/code/EGM/历史/平乾750kV.toml"
else:
toml_file_path = sys.argv[1]
if not os.path.exists(toml_file_path):
logger.info(f"无法找到数据文件{toml_file_path},程序退出。")
sys.exit(0)
logger.info(f"读取文件{toml_file_path}")
read_parameter(toml_file_path)
run_egm()
def speed(): def speed():
a = 0 a = 0

View File

@@ -317,7 +317,7 @@
</template> </template>
<script setup lang="ts"> <script setup lang="ts">
import { ref, reactive, computed } from 'vue' import { ref, reactive, computed, onMounted, onUnmounted } from 'vue'
import type { AllParameters } from '@/types' import type { AllParameters } from '@/types'
import LogComponent from './Log.vue' import LogComponent from './Log.vue'
@@ -411,17 +411,10 @@ const calculate = async () => {
try { try {
// 调用 pywebview 的 Python 函数 // 调用 pywebview 的 Python 函数
if (window.pywebview) { if (window.pywebview) {
const response = await window.pywebview.api.calculate(params) // 后台线程启动计算,实时日志通过 addLogFromBackend 推送
// 结果通过 receiveResult 回调接收
// 显示后端日志 await window.pywebview.api.calculate(params)
const logs = response.logs // 不在这里设置 calculating = false等待 receiveResult 回调
if (Array.isArray(logs) && logs.length > 0) {
for (const log of logs) {
logRef.value?.addLog(log.level as any, log.message)
}
}
result.value = JSON.stringify(response, null, 2)
} else { } else {
// 开发模式下的模拟 // 开发模式下的模拟
await new Promise(resolve => setTimeout(resolve, 1000)) await new Promise(resolve => setTimeout(resolve, 1000))
@@ -436,11 +429,11 @@ const calculate = async () => {
parameters: params parameters: params
} }
}, null, 2) }, null, 2)
calculating.value = false
} }
} catch (e: any) { } catch (e: any) {
error.value = e.message || '计算失败' error.value = e.message || '计算失败'
logRef.value?.addLog('error', e.message || '计算失败') logRef.value?.addLog('error', e.message || '计算失败')
} finally {
calculating.value = false calculating.value = false
} }
} }
@@ -514,8 +507,33 @@ declare global {
export_config: (params: AllParameters) => Promise<any> export_config: (params: AllParameters) => Promise<any>
} }
} }
addLogFromBackend?: (log: { level: string; time: string; message: string }) => void
receiveResult?: (result: { success: boolean; message: string; data?: any; error?: string }) => void
} }
} }
// 注册全局日志接收函数,供后端实时调用
onMounted(() => {
// 实时日志推送
window.addLogFromBackend = (log: { level: string; time: string; message: string }) => {
logRef.value?.addLog(log.level as any, log.message)
}
// 接收计算结果
window.receiveResult = (res: { success: boolean; message: string; data?: any; error?: string }) => {
calculating.value = false
if (res.success) {
result.value = JSON.stringify(res, null, 2)
} else {
error.value = res.error || res.message
}
}
})
onUnmounted(() => {
window.addLogFromBackend = undefined
window.receiveResult = undefined
})
</script> </script>
<style scoped> <style scoped>

View File

@@ -7,6 +7,7 @@ import os
import sys import sys
import json import json
import math import math
import threading
from pathlib import Path from pathlib import Path
from typing import Dict, Any, List from typing import Dict, Any, List
from datetime import datetime from datetime import datetime
@@ -17,14 +18,8 @@ from loguru import logger
project_root = Path(__file__).parent project_root = Path(__file__).parent
sys.path.insert(0, str(project_root)) sys.path.insert(0, str(project_root))
from core import ( from core import para
Parameter, para, from main import parameter_display, run_egm
func_ng, min_i, rs_fun, rc_fun, rg_fun,
bd_area, thunder_density, arc_possibility,
rg_line_function_factory, solve_circle_intersection,
solve_circle_line_intersection, Draw
)
import numpy as np
class WebHandler: class WebHandler:
@@ -45,6 +40,35 @@ class WebHandler:
self.callback(log_entry) self.callback(log_entry)
class LoguruWebHandler:
"""Loguru 日志处理器,将 loguru 日志转发到 Web 界面"""
def __init__(self, app: 'EGMWebApp'):
self.app = app
def write(self, message):
"""loguru handler 的写入方法"""
record = message.record
level = record['level'].name.lower()
# 映射 loguru 级别到前端级别
level_map = {
'trace': 'debug',
'debug': 'debug',
'info': 'info',
'success': 'info',
'warning': 'warning',
'error': 'error',
'critical': 'error'
}
frontend_level = level_map.get(level, 'info')
# 提取消息文本
msg = record['message']
if msg.strip():
self.app.add_log(frontend_level, msg)
class EGMWebApp: class EGMWebApp:
"""EGM 计算程序的 Web 界面后端""" """EGM 计算程序的 Web 界面后端"""
@@ -52,9 +76,10 @@ class EGMWebApp:
self.window = None self.window = None
self.web_handler = None self.web_handler = None
self.logs: List[Dict[str, str]] = [] self.logs: List[Dict[str, str]] = []
self._loguru_handler_id = None
def add_log(self, level: str, message: str): def add_log(self, level: str, message: str):
"""添加日志""" """添加日志并实时推送到前端"""
log_entry = { log_entry = {
"level": level, "level": level,
"time": datetime.now().strftime("%H:%M:%S"), "time": datetime.now().strftime("%H:%M:%S"),
@@ -62,23 +87,59 @@ class EGMWebApp:
} }
self.logs.append(log_entry) self.logs.append(log_entry)
# 实时推送到前端
if self.window:
try:
import json
js_code = f'if(window.addLogFromBackend){{window.addLogFromBackend({json.dumps(log_entry)})}}'
self.window.evaluate_js(js_code)
except Exception as e:
logger.error(f"推送日志到前端失败: {e}")
def get_logs(self) -> List[Dict[str, str]]: def get_logs(self) -> List[Dict[str, str]]:
"""获取日志列表""" """获取日志列表"""
logs = self.logs.copy() logs = self.logs.copy()
self.logs = [] self.logs = []
return logs return logs
def _setup_loguru_handler(self):
"""设置 loguru 处理器,捕获所有 logger 调用"""
self._loguru_handler_id = logger.add(
LoguruWebHandler(self),
format="{message}",
level="DEBUG"
)
def _remove_loguru_handler(self):
"""移除 loguru 处理器"""
if self._loguru_handler_id is not None:
logger.remove(self._loguru_handler_id)
self._loguru_handler_id = None
def calculate(self, params: Dict[str, Any]) -> Dict[str, Any]: def calculate(self, params: Dict[str, Any]) -> Dict[str, Any]:
""" """
执行 EGM 计算 执行 EGM 计算(启动后台线程,立即返回)
Args: Args:
params: 包含 parameter, advance, optional 的字典 params: 包含 parameter, advance, optional 的字典
Returns: Returns:
计算结果字典 计算状态字典
""" """
self.logs = [] # 清空日志 self.logs = [] # 清空日志
# 启动后台线程执行计算
thread = threading.Thread(target=self._calculate_thread, args=(params,))
thread.daemon = True
thread.start()
return {"status": "started", "message": "计算已启动"}
def _calculate_thread(self, params: Dict[str, Any]):
"""后台线程中执行计算"""
# 设置 loguru 处理器,捕获所有 logger.info/debug 等调用
self._setup_loguru_handler()
self.add_log("info", "开始 EGM 计算...") self.add_log("info", "开始 EGM 计算...")
try: try:
@@ -112,233 +173,49 @@ class EGMWebApp:
para.voltage_n = int(optional_data.get('voltage_n', 3)) para.voltage_n = int(optional_data.get('voltage_n', 3))
para.max_i = float(optional_data.get('max_i', 200)) para.max_i = float(optional_data.get('max_i', 200))
self.add_log("info", f"参数: 额定电压={para.rated_voltage}kV, 雷暴日={para.td}d, 海拔={para.altitude}m") # 设置 ac_or_dc 参数
ac_or_dc_value = str(parameter_data.get('ac_or_dc', 'AC'))
para.ac_or_dc = 'DC' if 'DC' in ac_or_dc_value.upper() else 'AC'
logger.info("开始执行 _do_calculate...") # 调用 main.py 的参数显示函数,日志会被 loguru handler 捕获
parameter_display(para)
# 执行实际计算 logger.info("开始执行 EGM 计算...")
self.add_log("info", "EGM 计算中...")
result = self._do_calculate()
# 调试输出 # 调用 main.py 的核心计算函数
logger.info(f"_do_calculate 返回 keys: {list(result.keys())}") result = run_egm()
logger.info(f"日志列表: {self.logs}")
# 将日志添加到结果中(在返回之前添加最后一条日志)
self.add_log("info", "EGM 计算完成") self.add_log("info", "EGM 计算完成")
# 创建一个新的返回值,确保 logs 字段被包含 # 推送结果到前端
final_result = { self._send_result_to_frontend(result)
"success": result.get("success", True),
"message": result.get("message", "计算完成"),
"data": result.get("data", {}),
"logs": self.logs,
"DEBUG_VERSION": "v2" # 标记版本
}
logger.info(f"最终返回结果 keys: {list(final_result.keys())}") # 移除 loguru 处理器
logger.info(f"日志数量: {len(self.logs)}") self._remove_loguru_handler()
logger.info(f"DEBUG: self.logs = {self.logs}")
return final_result
except Exception as e: except Exception as e:
self.add_log("error", f"计算失败: {str(e)}") self.add_log("error", f"计算失败: {str(e)}")
import traceback import traceback
traceback.print_exc() traceback.print_exc()
return {
# 移除 loguru 处理器
self._remove_loguru_handler()
# 推送错误到前端
self._send_result_to_frontend({
"success": False, "success": False,
"message": f"计算失败: {str(e)}", "message": f"计算失败: {str(e)}",
"error": str(e), "error": str(e)
"logs": self.logs })
}
def _do_calculate(self) -> Dict[str, Any]: def _send_result_to_frontend(self, result: Dict[str, Any]):
"""执行实际的EGM计算""" """将计算结果推送到前端"""
try: if self.window:
h_whole = para.h_arm[0] try:
except Exception as e: js_code = f'if(window.receiveResult){{window.receiveResult({json.dumps(result)})}}'
self.add_log("error", f"获取参数失败: {str(e)}") self.window.evaluate_js(js_code)
raise except Exception as e:
string_g_len = para.string_g_len logger.error(f"推送结果到前端失败: {e}")
string_c_len = para.string_c_len
h_g_sag = para.h_g_sag
h_c_sag = para.h_c_sag
gc_x = para.gc_x.copy()
h_arm = para.h_arm
gc_y = [
h_whole - string_g_len - h_g_sag * 2 / 3,
]
if len(h_arm) > 1:
for hoo in h_arm[1:]:
gc_y.append(hoo - string_c_len - h_c_sag * 2 / 3)
if len(gc_y) > 2:
phase_n = 3
else:
phase_n = 1
td = para.td
ng = func_ng(td)
avr_n_sf = 0
ground_angels = para.ground_angels
voltage_n = para.voltage_n
n_sf_phases = np.zeros((phase_n, voltage_n))
results = []
for ground_angel in ground_angels:
self.add_log("info", f"地面倾角 {ground_angel / math.pi * 180:.3f}°")
rg_type = None
rg_x = None
rg_y = None
for phase_conductor_foo in range(phase_n):
rs_x = gc_x[phase_conductor_foo]
rs_y = gc_y[phase_conductor_foo]
rc_x = gc_x[phase_conductor_foo + 1]
rc_y = gc_y[phase_conductor_foo + 1]
if phase_n == 1:
rg_type = "g"
if phase_n > 1:
if phase_conductor_foo < 2:
rg_type = "c"
rg_x = gc_x[phase_conductor_foo + 2]
rg_y = gc_y[phase_conductor_foo + 2]
else:
rg_type = "g"
rated_voltage = para.rated_voltage
for u_bar in range(voltage_n):
u_ph = rated_voltage / 1.732
insulator_c_len = para.insulator_c_len
i_min = min_i(insulator_c_len, u_ph)
_min_i = i_min
_max_i = para.max_i
i_max = _min_i
for i_bar in np.linspace(_min_i, _max_i, int((_max_i - _min_i) / 1)):
rs = rs_fun(i_bar)
rc = rc_fun(i_bar, u_ph)
rg = rg_fun(i_bar, rc_y, u_ph, typ=rg_type)
rg_line_func = None
if rg_type == "g":
rg_line_func = rg_line_function_factory(rg, ground_angel)
rs_rc_circle_intersection = solve_circle_intersection(
rs, rc, rs_x, rs_y, rc_x, rc_y
)
i_max = i_bar
if not rs_rc_circle_intersection:
continue
circle_rc_or_rg_line_intersection = None
if rg_type == "g":
circle_rc_or_rg_line_intersection = solve_circle_line_intersection(
rc, rc_x, rc_y, rg_line_func
)
elif rg_type == "c":
circle_rc_or_rg_line_intersection = solve_circle_intersection(
rg, rc, rg_x, rg_y, rc_x, rc_y
)
if not circle_rc_or_rg_line_intersection:
if rg_type == "g":
if rg_line_func(rc_x) > rc_y:
i_min = i_bar
continue
else:
continue
min_distance_intersection = (
np.sum(
(
np.array(rs_rc_circle_intersection)
- np.array(circle_rc_or_rg_line_intersection)
)
** 2
)
** 0.5
)
if min_distance_intersection < 0.1:
break
self.add_log("info", f"最大电流为 {i_max:.2f}, 最小电流为 {i_min:.2f}")
curt_fineness = 0.1
if i_min > i_max or abs(i_min - i_max) < curt_fineness:
self.add_log("info", "最大电流小于等于最小电流,没有暴露弧。")
continue
curt_segment_n = int((i_max - i_min) / curt_fineness)
i_curt_samples, d_curt = np.linspace(
i_min, i_max, curt_segment_n + 1, retstep=True
)
bd_area_vec = np.vectorize(bd_area)
ip_a = para.Ip_a
ip_b = para.Ip_b
bd_area_vec_result = bd_area_vec(
i_curt_samples,
u_ph,
rc_x,
rc_y,
rs_x,
rs_y,
rg_x,
rg_y,
ground_angel,
rg_type,
)
thunder_density_result = thunder_density(
i_curt_samples, td, ip_a, ip_b
)
cal_bd_np = bd_area_vec_result * thunder_density_result
calculus = np.sum(cal_bd_np[:-1] + cal_bd_np[1:]) / 2 * d_curt
n_sf = (
2
* ng
/ 10
* calculus
* arc_possibility(rated_voltage, insulator_c_len)
)
avr_n_sf += n_sf / voltage_n
n_sf_phases[phase_conductor_foo][u_bar] = n_sf
self.add_log("info", f"{phase_conductor_foo + 1}, 跳闸率: {n_sf:.16f} 次/(100km·a)")
result = {
"ground_angle": f"{ground_angel / math.pi * 180:.3f}°",
"tripping_rate": avr_n_sf,
"phases": np.mean(n_sf_phases, axis=1).tolist()
}
results.append(result)
return {
"success": True,
"message": "计算完成",
"data": {
"tripping_rate": f"{avr_n_sf:.16f} 次/(100km·a)",
"results": results,
"parameters": {
"rated_voltage": para.rated_voltage,
"td": para.td,
"altitude": para.altitude,
"ground_angels": [a / math.pi * 180 for a in para.ground_angels],
"max_i": para.max_i
}
}
}
def dict_to_toml(self, obj: Dict[str, Any], indent: str = '') -> str: def dict_to_toml(self, obj: Dict[str, Any], indent: str = '') -> str:
""" """