refactor(step5_5): 公式内置化,界面精简

This commit is contained in:
DXC
2026-05-10 18:38:45 +08:00
parent 2a4a7ec7be
commit 2c52ca19c5
2 changed files with 90 additions and 322 deletions

View File

@ -140,4 +140,9 @@ class FileSelectWidget(QWidget):
def set_path(self, path): def set_path(self, path):
"""设置路径""" """设置路径"""
self.line_edit.setText(str(path)) self.line_edit.setText(str(path))
def set_read_only(self, read_only=True):
"""设置文件选择框为只读,并禁用浏览按钮。"""
self.line_edit.setReadOnly(read_only)
self.browse_btn.setEnabled(not read_only)

View File

@ -1,408 +1,171 @@
#!/usr/bin/env python
# -*- coding: utf-8 -*-
"""
Step5_5 面板 - 水质指数计算
"""
import os import os
import sys import sys
import pandas as pd
from pathlib import Path from pathlib import Path
from typing import Dict, List, Union from typing import Dict, List, Union
def get_resource_path(relative_path: str) -> str:
"""获取资源的绝对路径,适配 PyInstaller 打包环境。"""
if hasattr(sys, '_MEIPASS'):
return os.path.join(sys._MEIPASS, relative_path)
return os.path.abspath(
os.path.join(os.path.dirname(os.path.dirname(os.path.dirname(__file__))), relative_path)
)
import pandas as pd
from PyQt5.QtWidgets import ( from PyQt5.QtWidgets import (
QWidget, QVBoxLayout, QGroupBox, QFormLayout, QGridLayout, QWidget, QVBoxLayout, QGroupBox, QGridLayout,
QHBoxLayout, QLabel, QLineEdit, QComboBox, QCheckBox, QHBoxLayout, QLabel, QCheckBox, QPushButton, QMessageBox, QScrollArea
QPushButton, QMessageBox,
) )
from PyQt5.QtCore import Qt from PyQt5.QtCore import Qt
from src.gui.components.custom_widgets import FileSelectWidget from src.gui.components.custom_widgets import FileSelectWidget
from src.gui.styles import ModernStylesheet from src.gui.styles import ModernStylesheet
def get_resource_path(relative_path: str) -> str:
if hasattr(sys, '_MEIPASS'):
return os.path.join(sys._MEIPASS, relative_path)
return os.path.abspath(os.path.join(os.path.dirname(os.path.dirname(os.path.dirname(__file__))), relative_path))
class Step5_5Panel(QWidget): class Step5_5Panel(QWidget):
"""步骤5.5:水质指数计算"""
def __init__(self, parent=None): def __init__(self, parent=None):
super().__init__(parent) super().__init__(parent)
self.index_checkboxes: Dict[str, QCheckBox] = {} self.index_checkboxes: Dict[str, QCheckBox] = {}
self.csv_columns = [] # 存储CSV文件列名 self.builtin_formula_path = get_resource_path("data/sub/waterindex.csv")
self.init_ui() self.init_ui()
self._auto_load_formulas()
def init_ui(self): def init_ui(self):
main_layout = QVBoxLayout() main_layout = QVBoxLayout()
main_layout.setContentsMargins(20, 20, 20, 20)
# 标题 # 1. 数据文件(隐藏公式路径,自动填入训练数据)
data_group = QGroupBox("输入数据")
# 数据文件选择
data_group = QGroupBox("数据文件")
data_layout = QVBoxLayout() data_layout = QVBoxLayout()
self.training_data_widget = FileSelectWidget("训练数据CSV:", "CSV Files (*.csv)")
# 训练数据CSV文件选择
self.training_data_widget = FileSelectWidget("训练数据CSV文件:", "CSV Files (*.csv)")
data_layout.addWidget(self.training_data_widget) data_layout.addWidget(self.training_data_widget)
# 公式CSV文件选择 self.formula_csv_widget = FileSelectWidget("公式配置:", "CSV Files (*.csv)")
self.formula_csv_widget = FileSelectWidget("公式CSV文件:", "CSV Files (*.csv)") self.formula_csv_widget.hide() # 界面隐身
data_layout.addWidget(self.formula_csv_widget) data_layout.addWidget(self.formula_csv_widget)
# 刷新公式按钮
refresh_layout = QHBoxLayout()
self.refresh_button = QPushButton("刷新公式列表")
self.refresh_button.clicked.connect(self.refresh_formulas)
refresh_layout.addWidget(self.refresh_button)
refresh_layout.addStretch()
data_layout.addLayout(refresh_layout)
data_group.setLayout(data_layout) data_group.setLayout(data_layout)
main_layout.addWidget(data_group) main_layout.addWidget(data_group)
# 公式选择区 # 2. 公式选择区
self.formula_group = QGroupBox("选择要计算的公式") self.formula_group = QGroupBox("水质指数公式勾选")
formula_outer_layout = QVBoxLayout() formula_outer_layout = QVBoxLayout()
# 按钮控制区域 btn_layout = QHBoxLayout()
button_layout = QHBoxLayout()
self.select_all_btn = QPushButton("全选") self.select_all_btn = QPushButton("全选")
self.select_all_btn.clicked.connect(self.select_all_formulas)
self.deselect_all_btn = QPushButton("清空") self.deselect_all_btn = QPushButton("清空")
self.select_all_btn.clicked.connect(self.select_all_formulas)
self.deselect_all_btn.clicked.connect(self.deselect_all_formulas) self.deselect_all_btn.clicked.connect(self.deselect_all_formulas)
button_layout.addWidget(self.select_all_btn) btn_layout.addWidget(self.select_all_btn)
button_layout.addWidget(self.deselect_all_btn) btn_layout.addWidget(self.deselect_all_btn)
button_layout.addStretch() btn_layout.addStretch()
formula_outer_layout.addLayout(btn_layout)
formula_outer_layout.addLayout(button_layout) # 滚动显示区域
scroll = QScrollArea()
# 公式勾选框网格布局 scroll.setWidgetResizable(True)
self.formula_layout = QGridLayout() scroll_content = QWidget()
formula_outer_layout.addLayout(self.formula_layout) self.formula_layout = QGridLayout(scroll_content)
scroll.setWidget(scroll_content)
formula_outer_layout.addWidget(scroll)
self.formula_group.setLayout(formula_outer_layout) self.formula_group.setLayout(formula_outer_layout)
main_layout.addWidget(self.formula_group) main_layout.addWidget(self.formula_group)
# 输出文件设置 # 3. 输出设置
output_group = QGroupBox("输出设置") output_group = QGroupBox("输出设置")
output_layout = QVBoxLayout() output_layout = QVBoxLayout()
self.output_file_widget = FileSelectWidget("输出CSV路径:", "CSV Files (*.csv)", mode="save")
self.output_file_widget = FileSelectWidget(
"输出文件:", "CSV Files (*.csv)", mode="save"
)
output_layout.addWidget(self.output_file_widget) output_layout.addWidget(self.output_file_widget)
output_group.setLayout(output_layout) output_group.setLayout(output_layout)
main_layout.addWidget(output_group) main_layout.addWidget(output_group)
# 启用选项 # 4. 操作区
self.enable_checkbox = QCheckBox("启用此步骤") self.enable_checkbox = QCheckBox("启用此步骤")
self.enable_checkbox.setChecked(True) self.enable_checkbox.setChecked(True)
main_layout.addWidget(self.enable_checkbox) main_layout.addWidget(self.enable_checkbox)
# 独立运行按钮
self.run_button = QPushButton("独立运行此步骤") self.run_button = QPushButton("独立运行此步骤")
self.run_button.setStyleSheet(ModernStylesheet.get_button_stylesheet('success')) self.run_button.setStyleSheet(ModernStylesheet.get_button_stylesheet('success'))
self.run_button.clicked.connect(self.run_step) self.run_button.clicked.connect(self.run_step)
main_layout.addWidget(self.run_button) main_layout.addWidget(self.run_button)
# 公式编辑区域
formula_edit_group = QGroupBox("添加自定义公式")
formula_edit_layout = QFormLayout()
self.formula_name_edit = QLineEdit()
# 公式类别下拉选择框
self.formula_category_combo = QComboBox()
self.formula_category_combo.addItems([
"chlorophyll_a",
"Phycocyanin (BGA_PC)",
"Total Nitrogen (TN)",
"Total Phosphorus (TP)",
"Orthophosphate",
"COD",
"BOD",
"TOC",
"Dissolved Oxygen (DO)",
"E. coli",
"Total Coliforms",
"Turbidity",
"Total Suspended Solids (TSS)",
"Color",
"pH",
"Temperature",
"Conductivity",
"Total Dissolved Solids (TDS)"
])
self.formula_category_combo.setEditable(True) # 允许用户输入自定义类别
self.formula_expression_edit = QLineEdit()
self.formula_reference_edit = QLineEdit()
formula_edit_layout.addRow("公式名称:", self.formula_name_edit)
formula_edit_layout.addRow("公式类别:", self.formula_category_combo)
formula_edit_layout.addRow("公式表达式:", self.formula_expression_edit)
formula_edit_layout.addRow("参考文献:", self.formula_reference_edit)
add_button = QPushButton("添加公式")
add_button.clicked.connect(self.add_custom_formula)
formula_edit_layout.addRow(add_button)
formula_edit_group.setLayout(formula_edit_layout)
main_layout.addWidget(formula_edit_group)
main_layout.addStretch() main_layout.addStretch()
self.setLayout(main_layout) self.setLayout(main_layout)
# 自动加载内置公式文件 def _auto_load_formulas(self):
formula_csv_path = get_resource_path("data/sub/waterindex.csv") if os.path.isfile(self.builtin_formula_path):
if os.path.isfile(formula_csv_path): self.formula_csv_widget.set_path(self.builtin_formula_path)
self.formula_csv_widget.set_path(str(formula_csv_path)) self.refresh_formulas(silent=True)
self.refresh_formulas()
def refresh_formulas(self): def refresh_formulas(self, silent=False):
"""刷新公式列表""" path = self.formula_csv_widget.get_path()
formula_csv_path = self.formula_csv_widget.get_path() if not path or not os.path.exists(path):
if not formula_csv_path or not os.path.exists(formula_csv_path): if not silent: QMessageBox.warning(self, "警告", "内置公式文件丢失")
QMessageBox.warning(self, "警告", "请先选择有效的公式CSV文件")
return return
try: try:
# 清除现有的勾选框 # 清理旧项
for checkbox in self.index_checkboxes.values(): for i in reversed(range(self.formula_layout.count())):
self.formula_layout.removeWidget(checkbox) self.formula_layout.itemAt(i).widget().setParent(None)
checkbox.deleteLater()
self.index_checkboxes.clear() self.index_checkboxes.clear()
# 读取公式CSV文件 df = pd.read_csv(path)
df = pd.read_csv(formula_csv_path) # 修正:不使用 [1:] 切片,直接读取所有有效行
if df.empty or 'Formula_Name' not in df.columns: formula_names = df['Formula_Name'].dropna().unique().tolist()
QMessageBox.warning(self, "警告", "公式CSV文件格式不正确")
return
# 获取所有公式名称(跳过第一行)
formula_names = df['Formula_Name'].tolist()[1:]
# 创建3列布局的勾选框
row, col = 0, 0 row, col = 0, 0
for formula_name in formula_names: for name in formula_names:
if pd.isna(formula_name) or not formula_name.strip(): name = name.strip()
continue cb = QCheckBox(name)
cb.setChecked(True)
checkbox = QCheckBox(formula_name.strip()) self.index_checkboxes[name] = cb
checkbox.setChecked(True) self.formula_layout.addWidget(cb, row, col)
self.index_checkboxes[formula_name.strip()] = checkbox
self.formula_layout.addWidget(checkbox, row, col)
col += 1 col += 1
if col >= 3: # 每行3列 if col >= 3:
col = 0 col = 0
row += 1 row += 1
except Exception as e: except Exception as e:
QMessageBox.critical(self, "错误", f"读取公式文件失败: {str(e)}") if not silent: QMessageBox.critical(self, "错误", f"解析公式失败: {e}")
def add_custom_formula(self): def select_all_formulas(self):
"""添加自定义公式到公式CSV文件""" for cb in self.index_checkboxes.values(): cb.setChecked(True)
formula_csv_path = self.formula_csv_widget.get_path()
if not formula_csv_path:
QMessageBox.warning(self, "警告", "请先选择公式CSV文件")
return
formula_name = self.formula_name_edit.text().strip() def deselect_all_formulas(self):
formula_category = self.formula_category_combo.currentText().strip() for cb in self.index_checkboxes.values(): cb.setChecked(False)
formula_expression = self.formula_expression_edit.text().strip()
formula_reference = self.formula_reference_edit.text().strip()
if not all([formula_name, formula_category, formula_expression]): def get_config(self):
QMessageBox.warning(self, "警告", "请填写公式名称、类别和表达式") selected = [n for n, cb in self.index_checkboxes.items() if cb.isChecked()]
return
try:
# 读取现有公式文件或创建新文件
if os.path.exists(formula_csv_path):
df = pd.read_csv(formula_csv_path)
else:
df = pd.DataFrame(columns=['Formula_Name', 'Category', 'Formula', 'Reference'])
# 添加新公式
new_row = pd.DataFrame({
'Formula_Name': [formula_name],
'Category': [formula_category],
'Formula': [formula_expression],
'Reference': [formula_reference]
})
df = pd.concat([df, new_row], ignore_index=True)
# 保存文件
df.to_csv(formula_csv_path, index=False, encoding='utf-8')
# 清空输入框
self.formula_name_edit.clear()
self.formula_category_combo.setCurrentIndex(0) # 重置到第一个选项
self.formula_expression_edit.clear()
self.formula_reference_edit.clear()
# 刷新公式列表
self.refresh_formulas()
QMessageBox.information(self, "成功", "公式添加成功")
except Exception as e:
QMessageBox.critical(self, "错误", f"添加公式失败: {str(e)}")
def get_config(self) -> Dict[str, Union[List[str], str, bool]]:
"""获取配置"""
selected = [
name for name, checkbox in self.index_checkboxes.items()
if checkbox.isChecked()
]
output_path = self.output_file_widget.get_path()
return { return {
'training_spectra_path': self.training_data_widget.get_path() or None, 'training_spectra_path': self.training_data_widget.get_path(),
'formula_csv_file': self.formula_csv_widget.get_path() or None, 'formula_csv_file': self.formula_csv_widget.get_path(),
'formula_names': selected, 'formula_names': selected,
'output_file': output_path or None, 'output_file': self.output_file_widget.get_path(),
'enabled': self.enable_checkbox.isChecked() 'enabled': self.enable_checkbox.isChecked()
} }
def set_config(self, config): def set_config(self, config):
"""设置配置""" if 'training_spectra_path' in config: self.training_data_widget.set_path(config['training_spectra_path'])
if 'training_spectra_path' in config:
self.training_data_widget.set_path(config['training_spectra_path'])
if 'formula_csv_file' in config: if 'formula_csv_file' in config:
self.formula_csv_widget.set_path(config['formula_csv_file']) self.formula_csv_widget.set_path(config['formula_csv_file'])
self.refresh_formulas() self.refresh_formulas(silent=True)
if 'formula_names' in config: if 'formula_names' in config:
selected_formulas = set(config['formula_names']) sel = set(config['formula_names'])
for name, checkbox in self.index_checkboxes.items(): for n, cb in self.index_checkboxes.items(): cb.setChecked(n in sel)
checkbox.setChecked(name in selected_formulas) if 'output_file' in config: self.output_file_widget.set_path(config['output_file'])
if 'enabled' in config: self.enable_checkbox.setChecked(config['enabled'])
if 'output_file' in config and config['output_file']:
self.output_file_widget.set_path(config['output_file'])
elif 'output_filename' in config and config['output_filename']:
self.output_file_widget.set_path(config['output_filename'])
if 'enabled' in config:
self.enable_checkbox.setChecked(config['enabled'])
def update_from_config(self, work_dir=None, pipeline=None): def update_from_config(self, work_dir=None, pipeline=None):
"""从全局配置自动填充训练数据和输出路径 if work_dir: self.work_dir = work_dir
main = self.window()
if hasattr(main, 'step5_panel'):
p5 = main.step5_panel.output_file.get_path()
if p5:
if not os.path.isabs(p5): p5 = os.path.join(self.work_dir or '', p5).replace('\\', '/')
self.training_data_widget.set_path(p5)
Args:
work_dir: 工作目录路径
pipeline: Pipeline 实例(未使用,保留接口兼容性)
"""
if work_dir:
self.work_dir = work_dir
elif hasattr(self, 'work_dir') and self.work_dir:
pass
else:
self.work_dir = None
# 1. 自动填入训练数据路径(从 Step5 的输出中获取)
# 优先级:直接 widget > pipeline.step_outputs 回退
main_window = self.window()
if hasattr(main_window, 'step5_panel'):
# 优先直接从 Step5 的输出 widget 读取(已运行的最新输出)
step5_output = main_window.step5_panel.output_file.get_path()
if step5_output:
# 若为相对路径,使用 work_dir 合成为绝对路径
if not os.path.isabs(step5_output):
step5_output = os.path.join(self.work_dir or '', step5_output).replace('\\', '/')
self.training_data_widget.set_path(step5_output)
else:
# 退而求其次,使用 Step5 的输入 CSV
step5_csv = main_window.step5_panel.csv_file.get_path()
if step5_csv:
# 若为相对路径,使用 work_dir 合成为绝对路径
if not os.path.isabs(step5_csv):
step5_csv = os.path.join(self.work_dir or '', step5_csv).replace('\\', '/')
self.training_data_widget.set_path(step5_csv)
# 如果上述都没找到,尝试从 pipeline.step_outputs 回退
if not self.training_data_widget.get_path() and pipeline and hasattr(pipeline, 'step_outputs'):
step5_outputs = getattr(pipeline, 'step_outputs', {}).get('step5', {})
training_path = step5_outputs.get('training_spectra')
if training_path:
self.training_data_widget.set_path(training_path)
# 2. 自动填入输出文件的绝对路径
if self.work_dir: if self.work_dir:
output_abs = os.path.join(self.work_dir, "6_water_quality_indices", out = os.path.join(self.work_dir, "6_water_quality_indices", "training_spectra_indices.csv").replace('\\', '/')
"training_spectra_indices.csv").replace('\\', '/') self.output_file_widget.set_path(out)
self.output_file_widget.set_path(output_abs)
def is_enabled(self) -> bool:
return self.enable_checkbox.isChecked()
def select_all_formulas(self):
"""全选所有公式"""
for checkbox in self.index_checkboxes.values():
checkbox.setChecked(True)
def deselect_all_formulas(self):
"""清空所有公式"""
for checkbox in self.index_checkboxes.values():
checkbox.setChecked(False)
def run_step(self): def run_step(self):
"""独立运行步骤5.5:计算水质指数。
动态根据输入 CSV 文件名生成输出文件名,自动填入 output_file_widget。
例如training_spectra.csv → training_spectra_indices.csv
sampling_spectra.csv → sampling_spectra_indices.csv
"""
# 验证输入
training_csv_path = self.training_data_widget.get_path()
formula_csv_path = self.formula_csv_widget.get_path()
if not training_csv_path:
QMessageBox.warning(self, "输入验证失败", "请选择训练数据CSV文件")
return
if not formula_csv_path:
QMessageBox.warning(self, "输入验证失败", "请选择公式CSV文件")
return
if not os.path.exists(training_csv_path):
QMessageBox.warning(self, "输入验证失败", "训练数据CSV文件不存在")
return
if not os.path.exists(formula_csv_path):
QMessageBox.warning(self, "输入验证失败", "公式CSV文件不存在")
return
# 动态生成输出文件:自动拼接 _indices 后缀
input_name = Path(training_csv_path).stem
dynamic_output = f"{input_name}_indices.csv"
# 合成完整绝对路径(优先使用 work_dir其次从 training_csv_path 推导)
work_dir = getattr(self, 'work_dir', None)
if work_dir:
dynamic_output = os.path.join(
work_dir, "6_water_quality_indices", dynamic_output
).replace('\\', '/')
self.output_file_widget.set_path(dynamic_output)
# 获取配置
config = self.get_config() config = self.get_config()
if not config['training_spectra_path']:
# 调用GUI的run_single_step方法 QMessageBox.warning(self, "提示", "请选择输入数据")
return
parent = self.parent() parent = self.parent()
while parent and not hasattr(parent, 'run_single_step'): while parent and not hasattr(parent, 'run_single_step'): parent = parent.parent()
parent = parent.parent() if parent: parent.run_single_step('step5_5', {'step5_5': config})
if parent and hasattr(parent, 'run_single_step'):
parent.run_single_step('step5_5', {'step5_5': config})
else:
QMessageBox.critical(self, "错误", "无法找到父级GUI对象")