Files
WQ_GUI/src/gui/panels/step8_qaa_panel.py
DXC 2261b4b30e feat: Step1~Step14 面板单步按钮 EventBus 解耦 + Handler 补全(Step8~Step14)+ 旧上帝类删除
- 9 个面板(step1~step6/step8_ml_train/step8_qaa/step9_ml_predict/step10)单步执行按钮从 parent 链上溯改为 global_event_bus.publish('RequestRunSingleStep')

- PipelineExecutor 新增 _on_request_run_single_step 订阅

- 新增 Handler: step8_ml_train / step9_ml_predict / step10_qaa_inversion / step11_concentration / step12_kriging / step13_visualization / step14_report

- 删除旧 water_quality_inversion_pipeline_GUI.py(上帝类已肢解完毕)
2026-06-18 09:19:51 +08:00

337 lines
12 KiB
Python
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

#!/usr/bin/env python
# -*- coding: utf-8 -*-
"""
Step8 面板 - QAA 物理反演(非经验模型)
"""
import os
import sys
# 路径归一化 helper与 pipeline.get_step_output_dir 互为表里)
_HERE = os.path.dirname(os.path.abspath(__file__))
if _HERE not in sys.path:
sys.path.insert(0, _HERE)
from _step_path_resolver import resolve_subdir
from PyQt5.QtWidgets import (
QWidget, QVBoxLayout, QGroupBox, QFormLayout, QGridLayout,
QHBoxLayout, QLabel, QLineEdit, QComboBox, QCheckBox,
QPushButton, QFileDialog, QMessageBox,
)
from PyQt5.QtGui import QFont
from PyQt5.QtCore import Qt
from src.gui.components.custom_widgets import FileSelectWidget
from src.gui.styles import ModernStylesheet
from src.utils.water_owt_config import (
get_all_lake_names,
get_lake_config,
get_lambda_0,
get_default_lake,
)
from src.core.algorithms.qaa import QAABaselineSolver
class Step8QAAPanel(QWidget):
"""步骤8QAA 物理反演(非经验模型)"""
def __init__(self, parent=None):
super().__init__(parent)
self.init_ui()
def init_ui(self):
layout = QVBoxLayout()
title = QLabel("步骤8QAA 物理反演(非经验模型)")
title.setFont(QFont("Arial", 12, QFont.Bold))
layout.addWidget(title)
# 光谱 CSV 文件输入
self.spectrum_csv_file = FileSelectWidget(
"光谱 CSV 文件:",
"CSV Files (*.csv);;All Files (*.*)"
)
self.spectrum_csv_file.line_edit.setPlaceholderText(
"选择实测光谱或采样点光谱 CSV含波长列"
)
layout.addWidget(self.spectrum_csv_file)
# 水域类型选择
lake_group = QGroupBox("水域类型配置")
lake_layout = QFormLayout()
self.lake_combo = QComboBox()
lake_names = get_all_lake_names()
self.lake_combo.addItems(lake_names)
default_lake = get_default_lake()
if default_lake in lake_names:
self.lake_combo.setCurrentText(default_lake)
self.lake_combo.currentTextChanged.connect(self._on_lake_changed)
lake_layout.addRow("水域选择:", self.lake_combo)
# 参考波长显示
self.lambda_0_label = QLabel()
self.lambda_0_label.setStyleSheet(
f"color: {ModernStylesheet.COLORS['accent']}; "
f"font-weight: bold;"
)
lake_layout.addRow("参考波长 λ₀:", self.lambda_0_label)
# 算法提示
self.hint_label = QLabel()
self.hint_label.setWordWrap(True)
self.hint_label.setStyleSheet(
f"color: {ModernStylesheet.COLORS['text_secondary']}; "
"font-size: 12px;"
)
lake_layout.addRow("算法提示:", self.hint_label)
lake_group.setLayout(lake_layout)
layout.addWidget(lake_group)
# 输出路径
self.output_path = FileSelectWidget(
"输出文件:",
"CSV Files (*.csv);;All Files (*.*)",
mode="save"
)
self.output_path.line_edit.setPlaceholderText(
"自动生成到 8_QAA_Inversion或手动指定..."
)
self.output_path.browse_btn.clicked.disconnect()
self.output_path.browse_btn.clicked.connect(self.browse_output_path)
layout.addWidget(self.output_path)
# 启用步骤
self.enable_checkbox = QCheckBox("启用此步骤")
self.enable_checkbox.setChecked(False)
layout.addWidget(self.enable_checkbox)
# 独立运行按钮
self.run_btn = QPushButton("执行 QAA 反演")
self.run_btn.setStyleSheet(ModernStylesheet.get_button_stylesheet('success'))
self.run_btn.clicked.connect(self._on_run_single_clicked)
layout.addWidget(self.run_btn)
layout.addStretch()
self.setLayout(layout)
self._on_lake_changed(self.lake_combo.currentText())
def _on_lake_changed(self, lake_name: str):
"""当用户切换水域时更新显示"""
cfg = get_lake_config(lake_name)
if cfg:
self.lambda_0_label.setText(
f"{cfg['lambda_0']} nm{cfg['qaa_version']}"
)
self.hint_label.setText(cfg.get('notes', ''))
else:
self.lambda_0_label.setText("")
self.hint_label.setText("")
def _get_default_work_dir(self) -> str:
"""获取 work_dir优先用 panel 自身缓存的,否则尝试从主窗口取"""
if hasattr(self, 'work_dir') and self.work_dir:
return str(self.work_dir)
mw = self.window()
if mw and hasattr(mw, 'work_dir') and mw.work_dir:
return str(mw.work_dir)
return ""
def browse_output_path(self):
"""浏览输出文件路径"""
current = self.output_path.get_path().strip()
if current:
initial_dir = os.path.dirname(current)
initial_file = os.path.basename(current)
else:
initial_dir = ""
initial_file = ""
if not initial_dir or not os.path.isdir(initial_dir):
work_dir = self._get_default_work_dir()
initial_dir = resolve_subdir(work_dir, 'qaa_inversion') if work_dir else ""
if initial_dir and not os.path.isdir(initial_dir):
os.makedirs(initial_dir, exist_ok=True)
file_path, _ = QFileDialog.getSaveFileName(
self, "保存输出文件",
os.path.join(initial_dir, initial_file) if initial_file else initial_dir,
"CSV Files (*.csv);;All Files (*.*)"
)
if file_path:
self.output_path.set_path(file_path)
def get_config(self) -> dict:
"""获取面板配置"""
config = {
'lake_name': self.lake_combo.currentText(),
'lambda_0': get_lambda_0(self.lake_combo.currentText()),
'spectrum_csv_path': self.spectrum_csv_file.get_path(),
}
output_path = self.output_path.get_path()
if output_path:
config['output_path'] = output_path
return config
def set_config(self, config: dict):
"""设置面板配置"""
if 'lake_name' in config:
lake_name = config['lake_name']
idx = self.lake_combo.findText(lake_name)
if idx >= 0:
self.lake_combo.setCurrentIndex(idx)
if 'spectrum_csv_path' in config:
self.spectrum_csv_file.set_path(config['spectrum_csv_path'])
if 'output_path' in config:
self.output_path.set_path(config['output_path'])
def update_from_config(self, work_dir=None, pipeline=None):
"""从全局配置自动填充训练数据和输出路径"""
if work_dir:
self.work_dir = work_dir
elif hasattr(self, 'work_dir') and self.work_dir:
pass
else:
self.work_dir = None
main_window = self.window()
if main_window and hasattr(main_window, 'step5_panel'):
step5_output = main_window.step5_panel.output_file.get_path()
if step5_output:
if not os.path.isabs(step5_output):
step5_output = os.path.join(self.work_dir or '', step5_output).replace('\\', '/')
self.spectrum_csv_file.set_path(step5_output)
if self.work_dir:
qaa_dir = resolve_subdir(self.work_dir, 'qaa_inversion')
os.makedirs(qaa_dir, exist_ok=True)
output_path = os.path.join(qaa_dir, "a_lambda_results.csv").replace('\\', '/')
self.output_path.set_path(output_path)
else:
self.output_path.set_path("")
def _on_run_single_clicked(self):
"""通过 EventBus 发布单步执行请求(解耦面板与 PipelineExecutor"""
from src.gui.core.event_bus import global_event_bus
spectrum_path = self.spectrum_csv_file.get_path()
if not spectrum_path:
QMessageBox.warning(self, "输入错误", "请选择光谱 CSV 文件!")
return
config = {'step8_qaa': self.get_config()}
global_event_bus.publish('RequestRunSingleStep', {
'step_name': 'step8_qaa',
'config': config,
})
def run_step(self):
"""独立运行 QAA 反演(旧版 parent 链上溯方式,保留兼容)。"""
spectrum_path = self.spectrum_csv_file.get_path()
if not spectrum_path:
QMessageBox.warning(self, "输入错误", "请选择光谱 CSV 文件!")
return
main_window = self.window()
if hasattr(main_window, 'run_single_step'):
config = {'step8_qaa': self.get_config()}
main_window.run_single_step('step8_qaa', config)
else:
self._run_qaa_direct()
def _run_qaa_direct(self):
"""直接执行 QAA 反演(不依赖主窗口流水线)"""
spectrum_path = self.spectrum_csv_file.get_path()
output_path = self.output_path.get_path()
lake_name = self.lake_combo.currentText()
lambda_0 = get_lambda_0(lake_name)
if not output_path:
work_dir = self._get_default_work_dir()
qaa_dir = resolve_subdir(work_dir, 'qaa_inversion') if work_dir else ""
if qaa_dir and not os.path.isdir(qaa_dir):
os.makedirs(qaa_dir, exist_ok=True)
output_path = os.path.join(qaa_dir, "a_lambda_results.csv").replace('\\', '/')
try:
import numpy as np
import pandas as pd
df = pd.read_csv(spectrum_path, encoding="utf-8-sig")
col_names = df.columns.tolist()
wavelength_col_idx = None
for i, col in enumerate(col_names):
try:
float(col)
wavelength_col_idx = i
break
except (ValueError, TypeError):
pass
if wavelength_col_idx is None:
QMessageBox.warning(
self, "解析错误",
"无法从 CSV 列名中识别波长信息请确保列名包含数值型波长nm"
)
return
wavelengths = np.array(
[float(c) for c in col_names[wavelength_col_idx:]], dtype=np.float64
)
data_matrix = df.iloc[:, wavelength_col_idx:].values.astype(np.float64)
if data_matrix.ndim == 1:
data_matrix = data_matrix[np.newaxis, :]
solver = QAABaselineSolver()
raw_result = solver.run_inversion(wavelengths, data_matrix, lambda_0)
# run_inversion 返回:单样本 → dict多样本 → list[dict]
if isinstance(raw_result, list):
sample_results = raw_result
aw_0 = raw_result[0].get('aw_0', 0)
bbw_0 = raw_result[0].get('bbw_0', 0)
else:
sample_results = [raw_result]
aw_0 = raw_result.get('aw_0', 0)
bbw_0 = raw_result.get('bbw_0', 0)
rows_out = []
for i, sample_result in enumerate(sample_results):
wl_arr = wavelengths
a_arr = sample_result['a_lambda']
bb_arr = sample_result['bb_lambda']
for j, wl in enumerate(wl_arr):
rows_out.append({
'sample_id': f"sample_{i}",
'Wavelength': wl,
'a_lambda': a_arr[j],
'bb_lambda': bb_arr[j],
})
result_df = pd.DataFrame(rows_out)
os.makedirs(os.path.dirname(output_path) or '.', exist_ok=True)
result_df.to_csv(output_path, index=False, float_format='%.8f')
QMessageBox.information(
self, "执行成功",
f"QAA 反演完成!\n"
f"水域: {lake_name}\n"
f"参考波长 λ₀: {lambda_0} nm\n"
f"λ₀ 处 aw: {aw_0:.6f} m⁻¹\n"
f"λ₀ 处 bbw: {bbw_0:.6f} m⁻¹\n"
f"结果已保存到:\n{output_path}"
)
except Exception as e:
QMessageBox.critical(self, "执行错误", f"QAA 反演失败:\n{str(e)}")
def get_training_params(self) -> dict:
"""获取反演参数"""
return {
'pipeline_type': 'qaa_non_empirical',
'lake_name': self.lake_combo.currentText(),
'lambda_0': get_lambda_0(self.lake_combo.currentText()),
}