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(上帝类已肢解完毕)
This commit is contained in:
DXC
2026-06-18 09:19:51 +08:00
parent 2d45610aa6
commit 2261b4b30e
28 changed files with 1446 additions and 2690 deletions

View File

@ -18,6 +18,13 @@ from src.core.handlers.step4_sampling import Step4SamplingHandler
from src.core.handlers.step5_process_csv import Step5ProcessCsvHandler
from src.core.handlers.step6_extract_spectra import Step6ExtractSpectraHandler
from src.core.handlers.step7_calc_indices import Step7CalcIndicesHandler
from src.core.handlers.step8_ml_train import Step8MlTrainHandler
from src.core.handlers.step9_ml_predict import Step9MlPredictHandler
from src.core.handlers.step10_qaa_inversion import Step10QaaInversionHandler
from src.core.handlers.step11_concentration import Step11ConcentrationHandler
from src.core.handlers.step12_kriging import Step12KrigingHandler
from src.core.handlers.step13_visualization import Step13VisualizationHandler
from src.core.handlers.step14_report import Step14ReportHandler
__all__ = [
'BaseStepHandler',
@ -29,4 +36,11 @@ __all__ = [
'Step5ProcessCsvHandler',
'Step6ExtractSpectraHandler',
'Step7CalcIndicesHandler',
'Step8MlTrainHandler',
'Step9MlPredictHandler',
'Step10QaaInversionHandler',
'Step11ConcentrationHandler',
'Step12KrigingHandler',
'Step13VisualizationHandler',
'Step14ReportHandler',
]

View File

@ -74,6 +74,11 @@ class PipelineContext:
self.training_csv_path: Optional[str] = None
self.indices_path: Optional[str] = None
self.custom_regression_path: Optional[str] = None
self.sampling_csv_path: Optional[str] = None
self.prediction_files: Dict[str, str] = {}
self.distribution_map_path: Optional[str] = None
self.qaa_output_path: Optional[str] = None
self.concentration_output_path: Optional[str] = None
# ── 计时 ──
self.step_timings: Dict[str, dict] = {}

View File

@ -18,6 +18,13 @@ from src.core.handlers.step4_sampling import Step4SamplingHandler
from src.core.handlers.step5_process_csv import Step5ProcessCsvHandler
from src.core.handlers.step6_extract_spectra import Step6ExtractSpectraHandler
from src.core.handlers.step7_calc_indices import Step7CalcIndicesHandler
from src.core.handlers.step8_ml_train import Step8MlTrainHandler
from src.core.handlers.step9_ml_predict import Step9MlPredictHandler
from src.core.handlers.step10_qaa_inversion import Step10QaaInversionHandler
from src.core.handlers.step11_concentration import Step11ConcentrationHandler
from src.core.handlers.step12_kriging import Step12KrigingHandler
from src.core.handlers.step13_visualization import Step13VisualizationHandler
from src.core.handlers.step14_report import Step14ReportHandler
if TYPE_CHECKING:
from src.core.handlers.pipeline_scheduler import PipelineScheduler
@ -41,3 +48,10 @@ def register_all_handlers(scheduler: PipelineScheduler):
scheduler.register_handler(Step5ProcessCsvHandler())
scheduler.register_handler(Step6ExtractSpectraHandler())
scheduler.register_handler(Step7CalcIndicesHandler())
scheduler.register_handler(Step8MlTrainHandler())
scheduler.register_handler(Step9MlPredictHandler())
scheduler.register_handler(Step10QaaInversionHandler())
scheduler.register_handler(Step11ConcentrationHandler())
scheduler.register_handler(Step12KrigingHandler())
scheduler.register_handler(Step13VisualizationHandler())
scheduler.register_handler(Step14ReportHandler())

View File

@ -0,0 +1,137 @@
#!/usr/bin/env python
# -*- coding: utf-8 -*-
"""
Step10 处理器QAA 准解析算法反演
将原 WaterQualityInversionPipeline.step8_qaa_inversion() 方法
剥离为独立的 Step10QaaInversionHandler。
"""
import os
import time
from typing import Any, Dict
import numpy as np
import pandas as pd
from src.core.handlers.base import BaseStepHandler, PipelineContext
class Step10QaaInversionHandler(BaseStepHandler):
"""步骤10QAA 准解析算法反演(非经验模型)。
对应 config key: 'step10_qaa'
直接使用 QAABaselineSolver 进行物理推导。
"""
step_key = 'step10_qaa'
def execute(self, context: PipelineContext, config: dict) -> Dict[str, Any]:
from src.core.algorithms.qaa.qaas_baseline import QAABaselineSolver
from src.utils.water_owt_config import get_lambda_0
step_start_time = time.time()
lake_name = config.get('lake_name', 'Unknown')
lambda_0 = config.get('lambda_0', get_lambda_0(lake_name))
output_dir = os.path.join(context.work_dir, "10_QAA_Inversion")
os.makedirs(output_dir, exist_ok=True)
output_path = config.get('output_path') or os.path.join(output_dir, "a_lambda_results.csv")
spectrum_csv = config.get('spectrum_csv_path')
if not spectrum_csv:
spectrum_csv = context.training_csv_path
if not spectrum_csv or not os.path.exists(spectrum_csv):
fallback_candidates = []
step6_dir = os.path.join(context.work_dir, "6_Spectral_Feature_Extraction")
if os.path.isdir(step6_dir):
for f in sorted(os.listdir(step6_dir)):
if f.lower().endswith('.csv'):
fallback_candidates.append(os.path.join(step6_dir, f))
if fallback_candidates:
spectrum_csv = fallback_candidates[0]
context.notify('step10_qaa', 'info',
f'spectrum_csv_path 为空,已自动回退到 step6 产物: {spectrum_csv}')
else:
msg = f'训练光谱 CSV 不存在或路径为空: {spectrum_csv}'
context.notify('step10_qaa', 'error', msg)
step_end_time = time.time()
context.record_step_time(
"步骤10: QAA 反演", step_start_time, step_end_time,
status="failed", error=msg
)
return {'error': msg}
try:
df = pd.read_csv(spectrum_csv, 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:
msg = "无法从 CSV 列名中识别波长信息"
context.notify('step10_qaa', 'error', msg)
step_end_time = time.time()
context.record_step_time(
"步骤10: QAA 反演", step_start_time, step_end_time,
status="failed", error=msg
)
return {'error': msg}
meta_df = df.iloc[:, :wavelength_col_idx].copy()
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)
if isinstance(raw_result, list):
sample_results = raw_result
else:
sample_results = [raw_result]
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']
meta_row = meta_df.iloc[i].to_dict() if i < len(meta_df) else {}
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],
**meta_row,
})
result_df = pd.DataFrame(rows_out)
result_df.to_csv(output_path, index=False, float_format='%.8f')
context.qaa_output_path = output_path
step_end_time = time.time()
context.record_step_time(
"步骤10: QAA 反演", step_start_time, step_end_time
)
context.notify('step10_qaa', 'completed',
f"QAA 反演完毕,水域={lake_name},λ₀={lambda_0}nm")
return {'qaa_output_path': output_path}
except Exception as e:
step_end_time = time.time()
context.record_step_time(
"步骤10: QAA 反演", step_start_time, step_end_time,
status="failed", error=str(e)
)
raise

View File

@ -0,0 +1,71 @@
#!/usr/bin/env python
# -*- coding: utf-8 -*-
"""
Step11 处理器:浓度反演
将原 WaterQualityInversionPipeline.step9_concentration_inversion() 方法
剥离为独立的 Step11ConcentrationHandler。
"""
import os
import time
from typing import Any, Dict
from src.core.handlers.base import BaseStepHandler, PipelineContext
class Step11ConcentrationHandler(BaseStepHandler):
"""步骤11浓度反演基于 QAA Step10 输出的 a_lambda/bb_lambda
对应 config key: 'step11_concentration'
直接使用 ConcentrationPipeline 进行浓度反演。
"""
step_key = 'step11_concentration'
def execute(self, context: PipelineContext, config: dict) -> Dict[str, Any]:
from src.core.algorithms.concentration_inversion import ConcentrationPipeline
step_start_time = time.time()
input_csv = config.get('input_csv') or context.qaa_output_path
output_csv = config.get('output_csv')
lake_case = config.get('lake_case', 'medium')
if not input_csv or not os.path.exists(input_csv):
msg = f"QAA 结果文件不存在或路径为空: {input_csv}"
context.notify('step11_concentration', 'error', msg)
step_end_time = time.time()
context.record_step_time(
"步骤11: 浓度反演", step_start_time, step_end_time,
status="failed", error=msg
)
return {'error': msg}
if not output_csv:
output_dir = os.path.join(context.work_dir, "11_Concentration")
os.makedirs(output_dir, exist_ok=True)
output_csv = os.path.join(output_dir, "final_concentrations.csv")
try:
pipeline = ConcentrationPipeline(lake_case=lake_case)
result_csv = pipeline.run_pipeline(input_csv, output_csv)
context.concentration_output_path = result_csv
step_end_time = time.time()
context.record_step_time(
"步骤11: 浓度反演", step_start_time, step_end_time
)
context.notify('step11_concentration', 'completed',
f"浓度反演完毕,结果保存于: {result_csv}")
return {'concentration_output_path': result_csv}
except Exception as e:
step_end_time = time.time()
context.record_step_time(
"步骤11: 浓度反演", step_start_time, step_end_time,
status="failed", error=str(e)
)
raise

View File

@ -0,0 +1,81 @@
#!/usr/bin/env python
# -*- coding: utf-8 -*-
"""
Step12 处理器:克里金空间插值与分布图生成
将原 WaterQualityInversionPipeline.step10_map() 方法
剥离为独立的 Step12KrigingHandler。
"""
import time
from pathlib import Path
from typing import Any, Dict
from src.core.handlers.base import BaseStepHandler, PipelineContext
from src.core.steps.mapping_step import MappingStep
class Step12KrigingHandler(BaseStepHandler):
"""步骤12克里金空间插值与分布图生成。
对应 config key: 'step12_kriging'
委托类: MappingStep.generate_distribution_map()
"""
step_key = 'step12_kriging'
def execute(self, context: PipelineContext, config: dict) -> Dict[str, Any]:
step_start_time = time.time()
prediction_csv_path = config.get('prediction_csv_path')
boundary_shp_path = config.get('boundary_shp_path')
# 强制输出到 visualization_dir
csv_name = Path(prediction_csv_path).stem if prediction_csv_path else "distribution"
forced_image_path = str(context.visualization_dir / f"{csv_name}_distribution.png")
viz_dir_resolved = str(context.visualization_dir)
output_image_path = config.get('output_image_path')
if output_image_path and output_image_path != forced_image_path:
norm_user = output_image_path.replace('\\', '/').rstrip('/')
norm_viz = viz_dir_resolved.replace('\\', '/').rstrip('/')
if not norm_user.startswith(norm_viz + '/') and norm_user != norm_viz:
output_image_path = forced_image_path
else:
output_image_path = forced_image_path
try:
result = MappingStep.generate_distribution_map(
prediction_csv_path=prediction_csv_path,
boundary_shp_path=boundary_shp_path,
output_image_path=output_image_path,
resolution=config.get('resolution', 30),
input_crs=config.get('input_crs', 'EPSG:32651'),
output_crs=config.get('output_crs', 'EPSG:4326'),
show_sample_points=config.get('show_sample_points', False),
base_map_tif=config.get('base_map_tif'),
use_distance_diffusion=config.get('use_distance_diffusion', True),
max_diffusion_distance=config.get('max_diffusion_distance'),
diffusion_power=config.get('diffusion_power', 2),
diffusion_n_neighbors=config.get('diffusion_n_neighbors', 15),
cmap=config.get('cmap'),
expand_ratio=config.get('expand_ratio', 0.05),
output_dir=str(context.visualization_dir),
)
context.distribution_map_path = result
step_end_time = time.time()
context.record_step_time(
"步骤12: 克里金插值与分布图", step_start_time, step_end_time
)
return {'distribution_map_path': result}
except Exception as e:
step_end_time = time.time()
context.record_step_time(
"步骤12: 克里金插值与分布图", step_start_time, step_end_time,
status="failed", error=str(e)
)
raise

View File

@ -0,0 +1,349 @@
#!/usr/bin/env python
# -*- coding: utf-8 -*-
"""
Step13 处理器:可视化成图
将原 WaterQualityInversionPipeline 中的可视化方法
(散点图、箱型图、光谱曲线、统计图表、耀斑预览)
剥离为独立的 Step13VisualizationHandler。
"""
import time
from pathlib import Path
from typing import Any, Dict, List, Optional
import numpy as np
import pandas as pd
import matplotlib.pyplot as plt
import seaborn as sns
from src.core.handlers.base import BaseStepHandler, PipelineContext
class Step13VisualizationHandler(BaseStepHandler):
"""步骤13可视化成图。
对应 config key: 'step13_visualization'
包含:散点图、箱型图、光谱曲线、统计图表、耀斑预览。
"""
step_key = 'step13_visualization'
def execute(self, context: PipelineContext, config: dict) -> Dict[str, Any]:
step_start_time = time.time()
output_files: Dict[str, Any] = {}
try:
# ── 散点图 ──
if config.get('generate_scatter', True):
if context.training_csv_path and context.models_dir.exists():
try:
scatter_config = config.get('scatter_config', {})
scatter_paths = self._generate_scatter_plots(context, scatter_config)
output_files['scatter_plots'] = scatter_paths
except Exception as e:
context.notify('step13_visualization', 'warning',
f"生成散点图时出错: {e}")
# ── 箱型图 ──
if config.get('generate_boxplots', True):
if context.processed_csv_path:
try:
boxplot_config = config.get('boxplot_config', {})
boxplot_paths = self._generate_boxplots(context, boxplot_config)
output_files['boxplots'] = boxplot_paths
except Exception as e:
context.notify('step13_visualization', 'warning',
f"生成箱型图时出错: {e}")
# ── 光谱曲线 ──
if config.get('generate_spectrum', True):
if context.training_csv_path:
try:
spectrum_paths = self._generate_spectrum_plots(context, config)
output_files['spectrum_plots'] = spectrum_paths
except Exception as e:
context.notify('step13_visualization', 'warning',
f"生成光谱曲线图时出错: {e}")
# ── 统计图表 ──
if config.get('generate_statistics', True):
if context.processed_csv_path:
try:
stat_charts = self._generate_statistics(context)
output_files['statistical_charts'] = stat_charts
except Exception as e:
context.notify('step13_visualization', 'warning',
f"生成统计图表时出错: {e}")
# ── 耀斑预览 ──
if config.get('generate_glint_previews', True):
try:
glint_config = config.get('glint_preview_config', {})
preview_paths = context.visualizer.generate_glint_deglint_previews(
work_dir=glint_config.get('work_dir') or str(context.work_dir),
output_subdir=glint_config.get('output_subdir', 'glint_deglint_previews'),
generate_glint=glint_config.get('generate_glint', True),
generate_deglint=glint_config.get('generate_deglint', True),
)
output_files['glint_deglint_previews'] = preview_paths
except Exception as e:
context.notify('step13_visualization', 'warning',
f"生成耀斑预览图时出错: {e}")
step_end_time = time.time()
context.record_step_time(
"步骤13: 可视化成图", step_start_time, step_end_time
)
return {'visualization_outputs': output_files}
except Exception as e:
step_end_time = time.time()
context.record_step_time(
"步骤13: 可视化成图", step_start_time, step_end_time,
status="failed", error=str(e)
)
raise
# ── 散点图 ──
def _generate_scatter_plots(self, context: PipelineContext,
scatter_config: dict) -> Dict[str, str]:
training_csv_path = context.training_csv_path
models_dir = str(context.models_dir)
metric = scatter_config.get('metric', 'test_r2')
use_enhanced = scatter_config.get('use_enhanced', True)
feature_start_column = scatter_config.get('feature_start_column', 13)
test_size = scatter_config.get('test_size', 0.2)
random_state = scatter_config.get('random_state', 42)
scatter_paths = {}
if use_enhanced:
try:
results = context.scatter_batch.batch_plot_scatter(
models_root_dir=models_dir,
csv_path=training_csv_path,
output_dir=str(context.visualization_dir / "scatter_plots"),
metric=metric,
target_column=None,
feature_start_column=feature_start_column,
test_size=test_size,
random_state=random_state,
)
for target_name, result in results.items():
if result.get('status') == 'success':
scatter_paths[target_name] = result.get('save_path', '')
except Exception:
use_enhanced = False
if not use_enhanced or not scatter_paths:
from src.core.prediction.inference_batch import WaterQualityInference
models_path = Path(models_dir)
for target_folder in models_path.iterdir():
if not target_folder.is_dir():
continue
target_name = target_folder.name
try:
inferencer = WaterQualityInference(str(target_folder))
eval_result = inferencer.evaluate_with_split(
data_csv_path=training_csv_path,
split_method="spxy",
test_size=test_size,
random_state=random_state,
metric=metric,
)
predictions = eval_result.get('predictions', {})
if predictions:
y_train_true = predictions.get('y_train_true')
y_train_pred = predictions.get('y_train_pred')
y_test_true = predictions.get('y_test_true')
y_test_pred = predictions.get('y_test_pred')
metrics = eval_result.get('test_metrics', {})
if y_train_true is not None and y_test_true is not None:
y_all_true = np.concatenate([y_train_true, y_test_true])
y_all_pred = np.concatenate([y_train_pred, y_test_pred])
train_indices = np.arange(len(y_train_true))
test_indices = np.arange(len(y_train_true), len(y_all_true))
scatter_path = context.visualizer.plot_scatter_true_vs_pred(
y_true=y_all_true,
y_pred=y_all_pred,
target_name=target_name,
train_indices=train_indices,
test_indices=test_indices,
metrics={
'train_r2': eval_result.get('train_metrics', {}).get('r2', 0),
'test_r2': metrics.get('r2', 0),
'train_rmse': eval_result.get('train_metrics', {}).get('rmse', 0),
'test_rmse': metrics.get('rmse', 0),
}
)
scatter_paths[target_name] = scatter_path
except Exception:
continue
return scatter_paths
# ── 箱型图 ──
def _generate_boxplots(self, context: PipelineContext,
boxplot_config: dict) -> Dict[str, str]:
csv_path = context.processed_csv_path
parameter_columns = boxplot_config.get('parameter_columns')
data_start_column = boxplot_config.get('data_start_column', 4)
save_individual = boxplot_config.get('save_individual', True)
use_seaborn = boxplot_config.get('use_seaborn', True)
df = pd.read_csv(csv_path)
if parameter_columns is None:
data_columns = df.iloc[:, data_start_column:]
parameter_columns = list(data_columns.columns)
else:
parameter_columns = [col for col in parameter_columns if col in df.columns]
if not parameter_columns:
return {}
boxplot_dir = context.visualization_dir / "boxplots"
boxplot_dir.mkdir(parents=True, exist_ok=True)
boxplot_paths = {}
if save_individual:
for column in parameter_columns:
if column not in df.columns:
continue
clean_data = df[column].dropna()
if len(clean_data) == 0:
continue
try:
plt.figure(figsize=(8, 6))
if use_seaborn:
plot_data = pd.DataFrame({'参数': [column] * len(clean_data), '数值': clean_data})
sns.boxplot(data=plot_data, x='参数', y='数值', palette='Set2')
sns.stripplot(data=plot_data, x='参数', y='数值',
color='red', alpha=0.6, size=5, jitter=True)
else:
box_plot = plt.boxplot([clean_data], labels=[column],
patch_artist=True, showfliers=False)
box_plot['boxes'][0].set_facecolor('lightblue')
box_plot['boxes'][0].set_alpha(0.7)
x_pos = np.random.normal(1, 0.04, size=len(clean_data))
plt.scatter(x_pos, clean_data, alpha=0.6, s=30, color='red',
edgecolors='black', linewidth=0.5, zorder=3)
plt.title(f'{column} - 箱型图', fontsize=14, fontweight='bold')
plt.xlabel('参数', fontsize=12)
plt.ylabel('数值', fontsize=12)
stats_text = (f'数据点数: {len(clean_data)}\n'
f'均值: {clean_data.mean():.2f}\n'
f'中位数: {clean_data.median():.2f}\n'
f'标准差: {clean_data.std():.2f}')
plt.text(0.02, 0.98, stats_text, transform=plt.gca().transAxes,
verticalalignment='top',
bbox=dict(boxstyle='round',
facecolor='wheat' if not use_seaborn else 'lightgreen',
alpha=0.8))
plt.grid(True, alpha=0.3, linestyle='--')
plt.tight_layout()
safe_name = column.replace('/', '_').replace('\\', '_').replace(':', '_')
save_path = boxplot_dir / f'{safe_name}_boxplot.png'
plt.savefig(save_path, dpi=300, bbox_inches='tight')
plt.close()
boxplot_paths[column] = str(save_path)
except Exception:
continue
# 综合箱型图
try:
plt.figure(figsize=(max(12, len(parameter_columns) * 0.8), 8))
box_data = []
labels = []
for column in parameter_columns:
if column in df.columns:
clean_data = df[column].dropna()
if len(clean_data) > 0:
box_data.append(clean_data)
labels.append(column)
if box_data:
if use_seaborn:
melted_data = pd.melt(df[labels], var_name='参数', value_name='数值')
melted_data = melted_data.dropna()
sns.boxplot(data=melted_data, x='参数', y='数值', palette='Set3')
sns.stripplot(data=melted_data, x='参数', y='数值',
color='red', alpha=0.6, size=4, jitter=True)
else:
box_plot = plt.boxplot(box_data, labels=labels, patch_artist=True, showfliers=False)
colors = plt.cm.Set3(np.linspace(0, 1, len(box_data)))
for patch, color in zip(box_plot['boxes'], colors):
patch.set_facecolor(color)
patch.set_alpha(0.7)
for i, data in enumerate(box_data):
x_pos = np.random.normal(i + 1, 0.04, size=len(data))
plt.scatter(x_pos, data, alpha=0.6, s=20, color='red',
edgecolors='black', linewidth=0.5, zorder=3)
plt.title('水质参数箱型图(综合)', fontsize=16, fontweight='bold')
plt.xlabel('参数', fontsize=12)
plt.ylabel('数值', fontsize=12)
plt.xticks(rotation=45, ha='right')
plt.grid(True, alpha=0.3, linestyle='--')
plt.tight_layout()
combined_path = boxplot_dir / 'all_parameters_boxplot.png'
plt.savefig(combined_path, dpi=300, bbox_inches='tight')
plt.close()
boxplot_paths['all_parameters'] = str(combined_path)
except Exception:
pass
return boxplot_paths
# ── 光谱曲线 ──
def _generate_spectrum_plots(self, context: PipelineContext,
config: dict) -> Dict[str, str]:
csv_path = context.training_csv_path
wavelength_start_column = config.get('feature_start_column', 'UTM_Y')
df = pd.read_csv(csv_path)
if isinstance(wavelength_start_column, str):
try:
wavelength_start_idx = df.columns.get_loc(wavelength_start_column)
except KeyError:
wavelength_start_idx = 13
else:
wavelength_start_idx = wavelength_start_column
parameter_columns = list(df.columns[:wavelength_start_idx])
if len(parameter_columns) > 2:
parameter_columns = parameter_columns[2:]
spectrum_paths = {}
for param_col in parameter_columns:
if param_col not in df.columns:
continue
try:
spectrum_path = context.visualizer.plot_spectrum_by_parameter(
csv_path=csv_path,
parameter_column=param_col,
wavelength_start_column=wavelength_start_column,
n_groups=5,
)
spectrum_paths[param_col] = spectrum_path
except Exception:
continue
return spectrum_paths
# ── 统计图表 ──
def _generate_statistics(self, context: PipelineContext) -> Dict[str, str]:
csv_path = context.processed_csv_path
df = pd.read_csv(csv_path)
parameter_columns = list(df.columns[2:])
parameter_columns = [col for col in parameter_columns
if df[col].dtype in [np.float64, np.int64]]
return context.visualizer.plot_statistical_charts(
csv_path=csv_path,
parameter_columns=parameter_columns,
)

View File

@ -0,0 +1,142 @@
#!/usr/bin/env python
# -*- coding: utf-8 -*-
"""
Step14 处理器:报告生成
将原 WaterQualityInversionPipeline.generate_pipeline_report() 方法
剥离为独立的 Step14ReportHandler。
"""
import time
from datetime import datetime
from pathlib import Path
from typing import Any, Dict
import numpy as np
import pandas as pd
from src.core.handlers.base import BaseStepHandler, PipelineContext
class Step14ReportHandler(BaseStepHandler):
"""步骤14流程执行报告生成。
对应 config key: 'step14_report'
生成 CSV 和 TXT 格式的流程执行报告。
"""
step_key = 'step14_report'
def execute(self, context: PipelineContext, config: dict) -> Dict[str, Any]:
step_start_time = time.time()
try:
output_path = config.get('output_path')
if output_path is None:
timestamp = datetime.now().strftime('%Y%m%d_%H%M%S')
output_path = str(context.reports_dir / f"pipeline_report_{timestamp}.csv")
report_data = []
total_time = 0.0
step_order = [
"步骤1: 水域掩膜生成",
"步骤2: 耀斑区域检测",
"步骤3: 耀斑去除",
"步骤4: 数据预处理",
"步骤5: 光谱提取",
"步骤6: 水质光谱指数计算",
"步骤7: 机器学习建模与训练",
"步骤8: 非经验模型训练",
"步骤9: 自定义回归",
"步骤10: 采样点生成",
"步骤11: 参数预测",
"步骤12: 分布图生成",
]
for step_name in step_order:
if step_name in context.step_timings:
timing_info = context.step_timings[step_name]
report_data.append({
'步骤': step_name,
'开始时间': timing_info['start_time'],
'结束时间': timing_info['end_time'],
'耗时(秒)': f"{timing_info['elapsed_seconds']:.2f}",
'耗时(格式化)': timing_info['elapsed_formatted'],
'状态': timing_info['status'],
'错误信息': timing_info.get('error', '')
})
if timing_info['status'] == 'completed':
total_time += timing_info['elapsed_seconds']
if context.pipeline_start_time and context.pipeline_end_time:
pipeline_total = context.pipeline_end_time - context.pipeline_start_time
report_data.append({
'步骤': '总计',
'开始时间': datetime.fromtimestamp(context.pipeline_start_time).strftime('%Y-%m-%d %H:%M:%S'),
'结束时间': datetime.fromtimestamp(context.pipeline_end_time).strftime('%Y-%m-%d %H:%M:%S'),
'耗时(秒)': f"{pipeline_total:.2f}",
'耗时(格式化)': context._format_time(pipeline_total),
'状态': 'completed',
'错误信息': ''
})
df_report = pd.DataFrame(report_data)
df_report.to_csv(output_path, index=False, encoding='utf-8-sig')
txt_output_path = str(Path(output_path).with_suffix('.txt'))
with open(txt_output_path, 'w', encoding='utf-8') as f:
f.write("=" * 80 + "\n")
f.write("水质参数反演流程执行报告\n")
f.write("=" * 80 + "\n\n")
if context.pipeline_start_time and context.pipeline_end_time:
f.write(f"流程开始时间: {datetime.fromtimestamp(context.pipeline_start_time).strftime('%Y-%m-%d %H:%M:%S')}\n")
f.write(f"流程结束时间: {datetime.fromtimestamp(context.pipeline_end_time).strftime('%Y-%m-%d %H:%M:%S')}\n")
f.write(f"总耗时: {context._format_time(context.pipeline_end_time - context.pipeline_start_time)}\n\n")
f.write("-" * 80 + "\n")
f.write("各步骤执行详情:\n")
f.write("-" * 80 + "\n\n")
for step_name in step_order:
if step_name in context.step_timings:
timing_info = context.step_timings[step_name]
f.write(f"{step_name}\n")
f.write(f" 开始时间: {timing_info['start_time']}\n")
f.write(f" 结束时间: {timing_info['end_time']}\n")
f.write(f" 耗时: {timing_info['elapsed_formatted']} ({timing_info['elapsed_seconds']:.2f}秒)\n")
f.write(f" 状态: {timing_info['status']}\n")
if timing_info.get('error'):
f.write(f" 错误: {timing_info['error']}\n")
f.write("\n")
f.write("-" * 80 + "\n")
f.write("统计摘要:\n")
f.write("-" * 80 + "\n")
completed_steps = [s for s in context.step_timings.values() if s['status'] == 'completed']
failed_steps = [s for s in context.step_timings.values() if s['status'] == 'failed']
skipped_steps = [s for s in context.step_timings.values() if s['status'] == 'skipped']
f.write(f"成功完成的步骤: {len(completed_steps)}\n")
f.write(f"失败的步骤: {len(failed_steps)}\n")
f.write(f"跳过的步骤: {len(skipped_steps)}\n")
if completed_steps:
completed_times = [s['elapsed_seconds'] for s in completed_steps]
f.write(f"平均耗时: {context._format_time(np.mean(completed_times))}\n")
f.write(f"最长耗时: {context._format_time(np.max(completed_times))}\n")
f.write(f"最短耗时: {context._format_time(np.min(completed_times))}\n")
step_end_time = time.time()
context.record_step_time(
"步骤14: 报告生成", step_start_time, step_end_time
)
return {'report_csv': output_path, 'report_txt': txt_output_path}
except Exception as e:
step_end_time = time.time()
context.record_step_time(
"步骤14: 报告生成", step_start_time, step_end_time,
status="failed", error=str(e)
)
raise

View File

@ -0,0 +1,58 @@
#!/usr/bin/env python
# -*- coding: utf-8 -*-
"""
Step8 处理器:机器学习建模与训练
将原 WaterQualityInversionPipeline.step8_train_ml() 方法
剥离为独立的 Step8MlTrainHandler。
"""
import time
from typing import Any, Dict
from src.core.handlers.base import BaseStepHandler, PipelineContext
from src.core.steps.modeling_step import ModelingStep
class Step8MlTrainHandler(BaseStepHandler):
"""步骤8机器学习建模与训练。
对应 config key: 'step8_ml_train'
委托类: ModelingStep.train_models()
"""
step_key = 'step8_ml_train'
def execute(self, context: PipelineContext, config: dict) -> Dict[str, Any]:
step_start_time = time.time()
training_csv_path = self._resolve_path(
config.get('training_csv_path'), context.training_csv_path, 'training_csv'
)
try:
result = ModelingStep.train_models(
feature_start_column=config.get('feature_start_column', '374.285004'),
preprocessing_methods=config.get('preprocessing_methods'),
model_names=config.get('model_names'),
split_methods=config.get('split_methods'),
cv_folds=config.get('cv_folds', 5),
training_csv_path=training_csv_path,
output_dir=str(context.models_dir),
_report_generator=context.report_generator,
)
step_end_time = time.time()
context.record_step_time(
"步骤8: 机器学习建模与训练", step_start_time, step_end_time
)
return {'models_dir': result}
except Exception as e:
step_end_time = time.time()
context.record_step_time(
"步骤8: 机器学习建模与训练", step_start_time, step_end_time,
status="failed", error=str(e)
)
raise

View File

@ -0,0 +1,64 @@
#!/usr/bin/env python
# -*- coding: utf-8 -*-
"""
Step9 处理器:机器学习推理预测
将原 WaterQualityInversionPipeline.step9_predict_ml() 方法
剥离为独立的 Step9MlPredictHandler。
"""
import time
from typing import Any, Dict
from src.core.handlers.base import BaseStepHandler, PipelineContext
from src.core.steps.prediction_step import PredictionStep
class Step9MlPredictHandler(BaseStepHandler):
"""步骤9机器学习推理预测。
对应 config key: 'step9_ml_predict'
委托类: PredictionStep.predict_water_quality()
"""
step_key = 'step9_ml_predict'
def execute(self, context: PipelineContext, config: dict) -> Dict[str, Any]:
step_start_time = time.time()
sampling_csv_path = self._resolve_path(
config.get('sampling_csv_path'), context.sampling_csv_path, 'sampling_csv'
)
models_dir = config.get('models_dir') or str(context.models_dir)
try:
result = PredictionStep.predict_water_quality(
sampling_csv_path=sampling_csv_path,
models_dir=models_dir,
metric=config.get('metric', 'test_r2'),
prediction_column=config.get('prediction_column', 'prediction'),
output_dir=str(context.prediction_dir / "9_ML_Prediction"),
_report_generator=context.report_generator,
_external_model=config.get('_external_model'),
_external_model_path=config.get('_external_model_path'),
_external_models_dict=config.get('_external_models_dict'),
_external_model_dir=config.get('_external_model_dir'),
)
context.prediction_files.update(result)
step_end_time = time.time()
context.record_step_time(
"步骤9: 机器学习推理预测", step_start_time, step_end_time
)
return {'prediction_files': result}
except Exception as e:
step_end_time = time.time()
context.record_step_time(
"步骤9: 机器学习推理预测", step_start_time, step_end_time,
status="failed", error=str(e)
)
raise

File diff suppressed because it is too large Load Diff

View File

@ -26,6 +26,7 @@ Pipeline 执行器
import os
import copy
import traceback
from pathlib import Path
from typing import Dict, List, Optional
@ -74,6 +75,9 @@ class PipelineExecutor(QObject):
self._workspace_initializer = workspace_initializer
self._worker: Optional[WorkerThread] = None
# 订阅面板发出的单步执行请求(解耦面板与执行器)
global_event_bus.subscribe('RequestRunSingleStep', self._on_request_run_single_step)
# ═══════════════════════════════════════════════════════════
# 公开 API
# ═══════════════════════════════════════════════════════════
@ -98,26 +102,60 @@ class PipelineExecutor(QObject):
6. 获取配置 + 模式裁剪
7. 一次性全预检 + 用户交互
8. 确认执行 → 创建 WorkerThread → 启动
关键防静默失败设计:
- 每一个 return 前必须通过 EventBus 发布 LogMessage
- 整个方法体包裹在 try/except 中,防止 PyQt5 槽函数静默吞异常
"""
print("==== [探针] run_full_pipeline 方法体已进入 ====", flush=True)
try:
self._run_full_pipeline_impl()
except Exception as e:
err_detail = traceback.format_exc()
global_event_bus.publish('LogMessage', {
'message': f'[致命错误] run_full_pipeline 异常: {e}',
'level': 'error',
})
global_event_bus.publish('LogMessage', {
'message': f'详细追踪:\n{err_detail}',
'level': 'error',
})
QMessageBox.critical(
self.parent(), "运行失败",
f"启动流程时发生未预期的错误:\n\n{e}\n\n详细信息已输出到日志区。"
)
def _run_full_pipeline_impl(self):
"""run_full_pipeline 的实现体,由外层 try/except 保护。"""
# ★ 终端即时反馈:确保即使 EventBus/日志区未就绪也能看到
print("\n[PipelineExecutor] 收到「运行完整流程」指令,开始执行...")
if not PIPELINE_AVAILABLE:
global_event_bus.publish('LogMessage', {
'message': '无法导入 Pipeline 模块,请检查项目文件结构!',
'level': 'error',
})
# 阻断性错误仍需弹窗(用户必须知道)
QMessageBox.critical(
self.parent(), "错误",
"无法导入pipeline模块确保water_quality_inversion_pipeline_GUI.py文件存在"
"无法导入 Pipeline 模块,请检查 src/core/handlers/ 目录是否完整"
)
return
# ── 1) 获取 work_dir ──
work_dir = self._workspace_initializer.work_dir
if not work_dir:
global_event_bus.publish('LogMessage', {
'message': '⚠ 未选择工作目录,流程中止。请先通过「工具 → 设置工作目录」选择工作目录。',
'level': 'warning',
})
QMessageBox.warning(self.parent(), "警告", "未选择工作目录,请先设置工作目录。")
return
work_path = Path(work_dir)
global_event_bus.publish('LogMessage', {
'message': f'[运行] 工作目录: {work_dir}',
'level': 'info',
})
# ── 2) 运行前扫描 + 自动回填 ──
global_event_bus.publish('LogMessage', {
@ -132,11 +170,19 @@ class PipelineExecutor(QObject):
# ── 3) step3 波段越界预检 ──
if not self._precheck_step3_bands():
global_event_bus.publish('LogMessage', {
'message': '⚠ 流程中止step3 波段越界预检未通过(用户取消或波段配置无效)',
'level': 'warning',
})
return
# ── 4) 全流程模式选择弹窗 ──
mode_dlg = PipelineModeDialog(main_window=self.parent(), parent=self.parent())
if mode_dlg.exec() != QDialog.Accepted:
global_event_bus.publish('LogMessage', {
'message': '⚠ 流程中止:用户取消了模式选择对话框',
'level': 'warning',
})
return
selected_mode = mode_dlg.selected_mode
global_event_bus.publish('LogMessage', {
@ -147,8 +193,17 @@ class PipelineExecutor(QObject):
'level': 'info',
})
# ── 5) 获取配置 ──
# ── 5) 获取配置(★ 先预加载所有面板,确保配置完整) ──
global_event_bus.publish('LogMessage', {
'message': '[运行] 正在收集所有步骤面板的配置...',
'level': 'info',
})
self._panel_factory.preload_all()
config = self._get_current_config()
global_event_bus.publish('LogMessage', {
'message': f'[运行] 已收集 {len(config)} 个步骤的配置: {list(config.keys())}',
'level': 'info',
})
# ── 6) 模式裁剪 ──
if selected_mode == "prediction_only":
@ -164,9 +219,17 @@ class PipelineExecutor(QObject):
skip_list: List[str] = []
if missing_items:
global_event_bus.publish('LogMessage', {
'message': f'[预检] 发现 {len(missing_items)} 个缺失项,弹出预检对话框...',
'level': 'warning',
})
critical_items = [it for it in missing_items if it.is_critical]
if critical_items:
lines = "\n".join(f" - [{it.step_name}] {it.reason}" for it in critical_items)
global_event_bus.publish('LogMessage', {
'message': f'[预检] 阻断性错误 ({len(critical_items)} 项):\n{lines}',
'level': 'error',
})
QMessageBox.critical(
self.parent(), "预检失败(阻断性错误)",
f"以下为阻断性缺失,流程无法启动:\n\n{lines}\n\n请填写后重新运行。"
@ -175,21 +238,28 @@ class PipelineExecutor(QObject):
dialog = PreflightDialog(missing_items, parent=self.parent())
if dialog.exec() != QDialog.Accepted:
global_event_bus.publish('LogMessage', {
'message': '⚠ 流程中止:用户取消了预检对话框',
'level': 'warning',
})
return
result = dialog.get_result()
if result is None:
global_event_bus.publish('LogMessage', {
'message': '⚠ 流程中止:预检对话框返回空结果',
'level': 'warning',
})
return
action, *payload = result
if action == "fill":
_, step_id, tab_index = result
# 发布事件:请求切换到指定 tab
global_event_bus.publish('NavigateToTab', {
'tab_index': tab_index,
'step_id': step_id,
})
global_event_bus.publish('LogMessage', {
'message': f'[预检] 用户选择填写 {step_id},已切换到对应面板。',
'message': f'[预检] 用户选择填写 {step_id},已切换到对应面板。流程暂停,填写完成后请重新运行。',
'level': 'info',
})
return
@ -197,8 +267,13 @@ class PipelineExecutor(QObject):
if skip_list:
global_event_bus.publish('LogMessage', {
'message': f'[预检] 用户强制跳过 {len(skip_list)} 个步骤: {skip_list}',
'level': 'info',
'level': 'warning',
})
else:
global_event_bus.publish('LogMessage', {
'message': '[预检] ✓ 所有必需项均已就绪,无需弹窗',
'level': 'info',
})
# ── 8) 确认执行 ──
reply = QMessageBox.question(
@ -207,6 +282,10 @@ class PipelineExecutor(QObject):
QMessageBox.Yes | QMessageBox.No
)
if reply != QMessageBox.Yes:
global_event_bus.publish('LogMessage', {
'message': '⚠ 流程中止:用户取消了执行确认',
'level': 'warning',
})
return
# ── 9) 准备 worker_config ──
@ -222,6 +301,11 @@ class PipelineExecutor(QObject):
if not enabled:
worker_config.pop('step6_feature', None)
global_event_bus.publish('LogMessage', {
'message': f'[运行] 最终执行配置包含 {len(worker_config)} 个步骤: {list(worker_config.keys())}',
'level': 'info',
})
# ── 10) 创建 WorkerThread 并连线 ──
self._worker = WorkerThread(work_dir, worker_config, mode='full', skip_list=skip_list)
self._worker.log_message.connect(self._on_log_message, Qt.QueuedConnection)
@ -245,17 +329,48 @@ class PipelineExecutor(QObject):
step_name: 步骤名称(如 'step1', 'step5_clean'
config: 步骤配置字典(可选,默认从面板获取)
"""
try:
self._run_single_step_impl(step_name, config)
except Exception as e:
err_detail = traceback.format_exc()
global_event_bus.publish('LogMessage', {
'message': f'[致命错误] run_single_step 异常: {e}',
'level': 'error',
})
global_event_bus.publish('LogMessage', {
'message': f'详细追踪:\n{err_detail}',
'level': 'error',
})
QMessageBox.critical(
self.parent(), "运行失败",
f"启动单步执行时发生未预期的错误:\n\n{e}\n\n详细信息已输出到日志区。"
)
def _run_single_step_impl(self, step_name: str, config: dict = None):
if not PIPELINE_AVAILABLE:
global_event_bus.publish('LogMessage', {
'message': '无法导入 Pipeline 模块,请检查 src/core/handlers/ 目录是否完整!',
'level': 'error',
})
QMessageBox.critical(
self.parent(), "错误",
"无法导入pipeline模块确保water_quality_inversion_pipeline_GUI.py文件存在"
"无法导入 Pipeline 模块,请检查 src/core/handlers/ 目录是否完整"
)
return
work_dir = self._workspace_initializer.work_dir or './work_dir'
if config is None:
global_event_bus.publish('LogMessage', {
'message': '[运行] 正在收集所有步骤面板的配置...',
'level': 'info',
})
self._panel_factory.preload_all()
config = self._get_current_config()
global_event_bus.publish('LogMessage', {
'message': f'[运行] 已收集 {len(config)} 个步骤的配置',
'level': 'info',
})
global_event_bus.publish('LogMessage', {
'message': f'初始化 Pipeline工作目录: {work_dir}',
@ -295,6 +410,47 @@ class PipelineExecutor(QObject):
})
global_event_bus.publish('PipelineStopped', {})
# ═══════════════════════════════════════════════════════════
# EventBus 订阅回调
# ═══════════════════════════════════════════════════════════
def _on_request_run_single_step(self, data: dict):
"""处理面板通过 EventBus 发出的单步执行请求。
data 格式: {'step_name': 'step1', 'config': {'step1': {...}}}
前置条件检查(预检/工作目录)由 run_single_step → _run_single_step_impl
内部统一处理,此处仅做解析 + 转发 + 异常兜底。
"""
try:
step_name = data.get('step_name')
config = data.get('config')
if not step_name:
global_event_bus.publish('LogMessage', {
'message': '[单步执行] 请求缺少 step_name忽略',
'level': 'warning',
})
return
global_event_bus.publish('LogMessage', {
'message': f'[单步执行] 收到 {step_name} 的执行请求',
'level': 'info',
})
self.run_single_step(step_name, config)
except Exception as e:
err_detail = traceback.format_exc()
global_event_bus.publish('LogMessage', {
'message': f'[致命错误] _on_request_run_single_step({step_name}) 异常: {e}',
'level': 'error',
})
global_event_bus.publish('LogMessage', {
'message': f'详细追踪:\n{err_detail}',
'level': 'error',
})
# ═══════════════════════════════════════════════════════════
# WorkerThread 信号 → EventBus 事件(纯转发,零 UI 操作)
# ═══════════════════════════════════════════════════════════

View File

@ -178,7 +178,7 @@ class VisualizationWorkerThread(QThread):
{"task": "statistics", "output_paths": output_paths}
)
elif self.task == "scatter":
from src.core.water_quality_inversion_pipeline_GUI import WaterQualityInversionPipeline
from src.core.visualization.scatter_plot import generate_model_scatter_plots
training_csv_path = (self.extra.get("training_csv_path") or "").strip()
models_dir = (self.extra.get("models_dir") or "").strip()
@ -188,10 +188,9 @@ class VisualizationWorkerThread(QThread):
if not models_dir or not Path(models_dir).is_dir():
self.failed.emit("模型目录无效或不存在请确认步骤6已生成 7_Supervised_Model_Training 下的参数子文件夹。")
return
pipeline = WaterQualityInversionPipeline(work_dir=str(wp))
scatter_paths = pipeline.generate_model_scatter_plots(
training_csv_path=training_csv_path,
scatter_paths = generate_model_scatter_plots(
models_dir=models_dir,
training_csv_path=training_csv_path,
)
self.finished_ok.emit({"task": "scatter", "scatter_paths": scatter_paths or {}})
elif self.task == "generate_all_selected":
@ -205,11 +204,10 @@ class VisualizationWorkerThread(QThread):
if training_csv.is_file():
models_dir = wp / "7_Supervised_Model_Training"
if models_dir.is_dir() and any(d.is_dir() for d in models_dir.iterdir()):
from src.core.water_quality_inversion_pipeline_GUI import WaterQualityInversionPipeline
pipeline = WaterQualityInversionPipeline(work_dir=str(wp))
scatter_paths = pipeline.generate_model_scatter_plots(
training_csv_path=str(training_csv),
from src.core.visualization.scatter_plot import generate_model_scatter_plots
scatter_paths = generate_model_scatter_plots(
models_dir=str(models_dir),
training_csv_path=str(training_csv),
)
count = len(scatter_paths) if scatter_paths else 0
parts.append(f"散点图: {count}")

View File

@ -54,16 +54,16 @@ def diagnose_pipeline_import_error():
"[INFO] PyInstaller 环境Pipeline 从程序内置包加载,跳过对仓库路径 src/core/*.py 的磁盘检查"
)
else:
pipeline_file = os.path.normpath(
os.path.join(os.path.dirname(__file__), "..", "..", "core", "water_quality_inversion_pipeline_GUI.py")
handlers_dir = os.path.normpath(
os.path.join(os.path.dirname(__file__), "..", "..", "core", "handlers")
)
if not os.path.exists(pipeline_file):
error_info.append(f"[ERROR] Pipeline文件不存在: {pipeline_file}")
if not os.path.isdir(handlers_dir):
error_info.append(f"[ERROR] Handlers 目录不存在: {handlers_dir}")
error_info.append(
" 解决方案: 请确保项目结构完整,检查 src/core/ 下是否有 water_quality_inversion_pipeline_GUI.py"
" 解决方案: 请确保项目结构完整,检查 src/core/handlers/ 目录是否存在"
)
else:
error_info.append(f"[OK] Pipeline文件存在: {pipeline_file}")
error_info.append(f"[OK] Handlers 目录存在: {handlers_dir}")
current_dir = os.path.dirname(os.path.dirname(os.path.dirname(__file__)))
if current_dir not in sys.path:
@ -240,24 +240,34 @@ class WorkerThread(QThread):
self.log_message.emit(f" [WARNING] {message}", "warning")
def run(self):
"""运行 pipeline子线程内切换 Matplotlib 为 Agg避免 Qt5Agg 在后台线程绘图导致界面卡死。"""
"""运行 pipeline子线程内切换 Matplotlib 为 Agg避免 Qt5Agg 在后台线程绘图导致界面卡死。
终极防崩溃设计:
- 整个 run() 方法体包裹在单一 try/except 中
- 任何未预期的异常都会被捕获并通过 finished 信号回报主线程
- 确保前端永远不会面对"静默死亡"的后台线程
"""
import os
# GDAL 环境变量保护(放在最前面,防止路径/编码问题)
os.environ['GDAL_FILENAME_IS_UTF8'] = 'YES'
os.environ['SHAPE_ENCODING'] = 'UTF-8'
mpl_prev = None
try:
import matplotlib
mpl_prev = matplotlib.get_backend()
except Exception:
pass
try:
import matplotlib.pyplot as plt
plt.switch_backend("Agg")
except Exception:
mpl_prev = None
try:
# ★ 终端即时反馈
print(f"\n[WorkerThread] 后台线程启动 (mode={self.mode}, work_dir={self.work_dir})")
# ── Matplotlib 后端切换Agg 线程安全) ──
try:
import matplotlib
mpl_prev = matplotlib.get_backend()
except Exception:
pass
try:
import matplotlib.pyplot as plt
plt.switch_backend("Agg")
except Exception:
mpl_prev = None
# ── 新架构PipelineScheduler + Handler 注册表 ──
scheduler = PipelineScheduler(work_dir=self.work_dir)
scheduler.set_callback(self.pipeline_callback)
@ -267,14 +277,17 @@ class WorkerThread(QThread):
if self.mode == 'full':
self.log_message.emit("开始运行完整流程 (Handler 调度模式)...", "info")
# ── ★ 预检已由 GUI 层 perform_preflight() 完成,此处不再重复预检 ──
# 过滤 skip_list 中的步骤
active_config = {
k: v for k, v in self.config.items()
if k not in self.skip_list
}
self.log_message.emit(
f"[调度] 待执行步骤 ({len(active_config)} 个): {list(active_config.keys())}",
"info"
)
result = scheduler.run_full_pipeline(active_config)
errors = result.get('errors', {})
@ -295,16 +308,28 @@ class WorkerThread(QThread):
self.progress_update.emit(100, f"步骤 {self.step_name} 执行完成")
self.finished.emit(True, f"步骤 {self.step_name} 独立运行成功!")
except PipelineHalt as exc:
# 预检失败 / 硬终止:透传清晰错误信息,不打印完整 traceback
error_msg = str(exc)
self.log_message.emit(f"[预检失败] {error_msg}", "error")
self.finished.emit(False, error_msg)
except Exception as e:
error_msg = f"执行失败: {str(e)}\n{traceback.format_exc()}"
self.log_message.emit(error_msg, "error")
self.finished.emit(False, error_msg)
# ★ 终极捕获:任何未预期的异常都会被完整回报
full_tb = traceback.format_exc()
self.log_message.emit(f"[致命错误] 后台线程崩溃: {e}", "error")
self.log_message.emit(f"详细追踪:\n{full_tb}", "error")
# 同时 print 到终端(确保即使信号失效也能看到)
print(f"\n{'='*60}")
print(f"[WorkerThread 崩溃] {e}")
print(f"{'='*60}")
print(full_tb)
print(f"{'='*60}\n")
self.finished.emit(False, f"后台线程崩溃: {e}\n\n{full_tb}")
finally:
# ── 恢复 Matplotlib 后端 ──
if mpl_prev:
try:
import matplotlib.pyplot as plt

View File

@ -243,7 +243,7 @@ class Step10WatercolorPanel(QWidget):
self.run_btn = QPushButton("▶ 执行水色指数反演")
self.run_btn.setStyleSheet(ModernStylesheet.get_button_stylesheet('success'))
self.run_btn.clicked.connect(self.run_step)
self.run_btn.clicked.connect(self._on_run_single_clicked)
layout.addWidget(self.run_btn)
layout.addStretch()
@ -484,7 +484,54 @@ class Step10WatercolorPanel(QWidget):
if not self.output_dir.get_path():
self.output_dir.set_path(out_dir)
def _on_run_single_clicked(self):
"""通过 EventBus 发布单步执行请求(解耦面板与 PipelineExecutor"""
from src.gui.core.event_bus import global_event_bus
bsq_path = self.bsq_file.get_path().strip()
hdr_path = self.hdr_file.get_path().strip()
output_dir = self.output_dir.get_path().strip()
if not bsq_path:
QMessageBox.warning(self, "输入错误", "请选择去耀斑 BSQ 影像!")
return
if not Path(bsq_path).exists():
QMessageBox.warning(self, "输入错误", f"BSQ 影像不存在:\n{bsq_path}")
return
if not hdr_path:
auto_hdr = Path(bsq_path).with_suffix('.hdr')
if auto_hdr.exists():
hdr_path = str(auto_hdr)
self.hdr_file.set_path(hdr_path)
else:
QMessageBox.warning(self, "输入错误", "请选择 ENVI 头文件!")
return
if not Path(hdr_path).exists():
QMessageBox.warning(self, "输入错误", f"HDR 文件不存在:\n{hdr_path}")
return
if not output_dir:
work_dir = self._get_default_work_dir()
output_dir = resolve_subdir(work_dir, 'watercolor')
os.makedirs(output_dir, exist_ok=True)
self.output_dir.set_path(output_dir)
selected = self._get_selected_formula_names()
if not selected:
QMessageBox.warning(self, "输入错误", "请至少选择一个公式!")
return
if self._waterindex_csv and not Path(self._waterindex_csv).exists():
QMessageBox.warning(self, "配置错误", f"waterindex.csv 不存在:\n{self._waterindex_csv}")
return
config = {'step10': self.get_config()}
global_event_bus.publish('RequestRunSingleStep', {
'step_name': 'step10',
'config': config,
})
def run_step(self):
"""独立运行步骤10旧版 parent 链上溯方式,保留兼容)。"""
bsq_path = self.bsq_file.get_path().strip()
hdr_path = self.hdr_file.get_path().strip()
output_dir = self.output_dir.get_path().strip()

View File

@ -27,12 +27,7 @@ from PyQt5.QtWidgets import (
from src.gui.components.custom_widgets import FileSelectWidget
from src.gui.styles import ModernStylesheet
# Pipeline 可用性(与 core/worker_thread.py 保持一致)
try:
from src.core.water_quality_inversion_pipeline_GUI import WaterQualityInversionPipeline
PIPELINE_AVAILABLE = True
except ImportError:
PIPELINE_AVAILABLE = False
PIPELINE_AVAILABLE = True
class Step11MapBatchThread(QThread):
@ -63,19 +58,19 @@ class Step11MapBatchThread(QThread):
except Exception:
mpl_prev = None
try:
from src.core.water_quality_inversion_pipeline_GUI import WaterQualityInversionPipeline
pipeline = WaterQualityInversionPipeline(work_dir=self.work_dir)
from src.core.steps.mapping_step import MappingStep
n = len(self.csv_paths)
for i, csv_p in enumerate(self.csv_paths):
self.progress.emit(i + 1, n)
self.log_message.emit(f"专题图 [{i + 1}/{n}] {csv_p}", "info")
kw = {**self.step10_kwargs, "prediction_csv_path": csv_p, "skip_dependency_check": True}
kw = {**self.step10_kwargs, "prediction_csv_path": csv_p}
kw.pop("skip_dependency_check", None)
if self.output_dir_optional:
stem = Path(csv_p).stem
kw["output_image_path"] = str(Path(self.output_dir_optional) / f"{stem}_distribution.png")
else:
kw["output_image_path"] = None
pipeline.step10_map(**kw)
MappingStep.generate_distribution_map(**kw)
self.finished_ok.emit(n)
except Exception as e:
self.failed.emit(f"{e}\n{traceback.format_exc()}")

View File

@ -32,12 +32,7 @@ from matplotlib.backends.backend_qt5agg import FigureCanvasQTAgg as FigureCanvas
from matplotlib.backends.backend_qt5agg import NavigationToolbar2QT as NavigationToolbar
from matplotlib.figure import Figure
# Pipeline 可用性(与 core/worker_thread.py 保持一致)
try:
from src.core.water_quality_inversion_pipeline_GUI import WaterQualityInversionPipeline
PIPELINE_AVAILABLE = True
except ImportError:
PIPELINE_AVAILABLE = False
PIPELINE_AVAILABLE = True
def _viz_training_spectra_csv_path(work_path: Path) -> Path:
@ -208,7 +203,7 @@ class VisualizationWorkerThread(QThread):
{"task": "statistics", "output_paths": output_paths}
)
elif self.task == "scatter":
from src.core.water_quality_inversion_pipeline_GUI import WaterQualityInversionPipeline
from src.core.visualization.scatter_plot import generate_model_scatter_plots
training_csv_path = (self.extra.get("training_csv_path") or "").strip()
models_dir = (self.extra.get("models_dir") or "").strip()
@ -218,10 +213,9 @@ class VisualizationWorkerThread(QThread):
if not models_dir or not Path(models_dir).is_dir():
self.failed.emit("模型目录无效或不存在请确认步骤6已生成 7_Supervised_Model_Training 下的参数子文件夹。")
return
pipeline = WaterQualityInversionPipeline(work_dir=str(wp))
scatter_paths = pipeline.generate_model_scatter_plots(
training_csv_path=training_csv_path,
scatter_paths = generate_model_scatter_plots(
models_dir=models_dir,
training_csv_path=training_csv_path,
)
self.finished_ok.emit({"task": "scatter", "scatter_paths": scatter_paths or {}})
elif self.task == "generate_all_selected":
@ -235,11 +229,10 @@ class VisualizationWorkerThread(QThread):
if training_csv.is_file():
models_dir = wp / "7_Supervised_Model_Training"
if models_dir.is_dir() and any(d.is_dir() for d in models_dir.iterdir()):
from src.core.water_quality_inversion_pipeline_GUI import WaterQualityInversionPipeline
pipeline = WaterQualityInversionPipeline(work_dir=str(wp))
scatter_paths = pipeline.generate_model_scatter_plots(
training_csv_path=str(training_csv),
from src.core.visualization.scatter_plot import generate_model_scatter_plots
scatter_paths = generate_model_scatter_plots(
models_dir=str(models_dir),
training_csv_path=str(training_csv),
)
count = len(scatter_paths) if scatter_paths else 0
parts.append(f"散点图: {count}")

View File

@ -27,12 +27,7 @@ from PyQt5.QtWidgets import (
from src.gui.components.custom_widgets import FileSelectWidget
from src.gui.styles import ModernStylesheet
# Pipeline 可用性(与 core/worker_thread.py 保持一致)
try:
from src.core.water_quality_inversion_pipeline_GUI import WaterQualityInversionPipeline
PIPELINE_AVAILABLE = True
except ImportError:
PIPELINE_AVAILABLE = False
PIPELINE_AVAILABLE = True
class Step14BatchThread(QThread):
@ -63,19 +58,19 @@ class Step14BatchThread(QThread):
except Exception:
mpl_prev = None
try:
from src.core.water_quality_inversion_pipeline_GUI import WaterQualityInversionPipeline
pipeline = WaterQualityInversionPipeline(work_dir=self.work_dir)
from src.core.steps.mapping_step import MappingStep
n = len(self.csv_paths)
for i, csv_p in enumerate(self.csv_paths):
self.progress.emit(i + 1, n)
self.log_message.emit(f"专题图 [{i + 1}/{n}] {csv_p}", "info")
kw = {**self.step14_kwargs, "prediction_csv_path": csv_p, "skip_dependency_check": True}
kw = {**self.step14_kwargs, "prediction_csv_path": csv_p}
kw.pop("skip_dependency_check", None)
if self.output_dir_optional:
stem = Path(csv_p).stem
kw["output_image_path"] = str(Path(self.output_dir_optional) / f"{stem}_distribution.png")
else:
kw["output_image_path"] = None
pipeline.step10_map(**kw)
MappingStep.generate_distribution_map(**kw)
self.finished_ok.emit(n)
except Exception as e:
self.failed.emit(f"{e}\n{traceback.format_exc()}")

View File

@ -144,7 +144,7 @@ class Step1Panel(QWidget):
# 独立运行按钮
self.run_btn = QPushButton("独立运行此步骤")
self.run_btn.setStyleSheet(ModernStylesheet.get_button_stylesheet('success'))
self.run_btn.clicked.connect(self.run_step)
self.run_btn.clicked.connect(self._on_run_single_clicked)
layout.addWidget(self.run_btn)
# 连接信号
@ -257,8 +257,40 @@ class Step1Panel(QWidget):
self.update_ui_state()
def _on_run_single_clicked(self):
"""通过 EventBus 发布单步执行请求(解耦面板与 PipelineExecutor
替代旧有的 parent 链上溯查找 run_single_step 的紧耦合方式。
PipelineExecutor 在 __init__ 中订阅 RequestRunSingleStep 事件,
收到后调用 run_single_step(step_name, config) 统一处理预检/工作目录/执行。
"""
from src.gui.core.event_bus import global_event_bus
# 验证输入(与旧 run_step 逻辑一致)
if self.use_ndwi_radio.isChecked():
img_path = self.img_file.get_path()
if not img_path:
QMessageBox.warning(self, "输入错误", "请选择参考影像文件!")
return
else:
mask_path = self.mask_file.get_path()
if not mask_path:
QMessageBox.warning(self, "输入错误", "请选择掩膜文件!")
return
if mask_path.lower().endswith('.shp'):
img_path = self.img_file.get_path()
if not img_path:
QMessageBox.warning(self, "输入错误", "当使用shp文件时需要提供参考影像用于栅格化")
return
config = {'step1': self.get_config()}
global_event_bus.publish('RequestRunSingleStep', {
'step_name': 'step1',
'config': config,
})
def run_step(self):
"""独立运行步骤1"""
"""独立运行步骤1(旧版 parent 链上溯方式,保留兼容)。"""
# 验证输入
if self.use_ndwi_radio.isChecked():
# NDWI模式需要影像文件

View File

@ -108,7 +108,7 @@ class Step2Panel(QWidget):
# 独立运行按钮
self.run_btn = QPushButton("独立运行此步骤")
self.run_btn.setStyleSheet(ModernStylesheet.get_button_stylesheet('success'))
self.run_btn.clicked.connect(self.run_step)
self.run_btn.clicked.connect(self._on_run_single_clicked)
layout.addWidget(self.run_btn)
layout.addStretch()
@ -203,8 +203,23 @@ class Step2Panel(QWidget):
# 没有工作目录时,清空输出路径
self.output_file.set_path("")
def _on_run_single_clicked(self):
"""通过 EventBus 发布单步执行请求(解耦面板与 PipelineExecutor"""
from src.gui.core.event_bus import global_event_bus
img_path = self.img_file.get_path()
if not img_path:
QMessageBox.warning(self, "输入错误", "请选择影像文件!")
return
config = {'step2': self.get_config()}
global_event_bus.publish('RequestRunSingleStep', {
'step_name': 'step2',
'config': config,
})
def run_step(self):
"""独立运行步骤2"""
"""独立运行步骤2(旧版 parent 链上溯方式,保留兼容)。"""
# 验证输入
img_path = self.img_file.get_path()
if not img_path:

View File

@ -228,7 +228,7 @@ class Step3Panel(QWidget):
# 独立运行按钮
self.run_btn = QPushButton("独立运行此步骤")
self.run_btn.setStyleSheet(ModernStylesheet.get_button_stylesheet('success'))
self.run_btn.clicked.connect(self.run_step)
self.run_btn.clicked.connect(self._on_run_single_clicked)
layout.addWidget(self.run_btn)
layout.addStretch()
@ -433,8 +433,34 @@ class Step3Panel(QWidget):
if 'sugar_bounds' in config:
self.sugar_bounds.setText(str(config['sugar_bounds']))
def _on_run_single_clicked(self):
"""通过 EventBus 发布单步执行请求(解耦面板与 PipelineExecutor"""
from src.gui.core.event_bus import global_event_bus
img_path = self.img_file.get_path()
if not img_path:
QMessageBox.warning(self, "输入错误", "请选择影像文件!")
return
if self.enable_checkbox.isChecked():
water_mask_path = self.water_mask_file.get_path()
if not water_mask_path:
QMessageBox.warning(
self,
"输入错误",
"独立运行耀斑去除时,必须选择水域掩膜或边界文件。\n\n"
"请提供与当前影像空间一致的水域栅格掩膜(.dat/.tif或水域矢量边界.shp\n"
"若刚跑过完整流程可使用步骤1生成的水域掩膜文件。",
)
return
config = {'step3': self.get_config()}
global_event_bus.publish('RequestRunSingleStep', {
'step_name': 'step3',
'config': config,
})
def run_step(self):
"""独立运行步骤3"""
"""独立运行步骤3(旧版 parent 链上溯方式,保留兼容)。"""
# 验证输入
img_path = self.img_file.get_path()
if not img_path:

View File

@ -91,7 +91,7 @@ class Step4SamplingPanel(QWidget):
# 独立运行按钮
self.run_btn = QPushButton("独立运行此步骤")
self.run_btn.setStyleSheet(ModernStylesheet.get_button_stylesheet('success'))
self.run_btn.clicked.connect(self.run_step)
self.run_btn.clicked.connect(self._on_run_single_clicked)
layout.addWidget(self.run_btn)
# 交互式预览按钮
@ -228,8 +228,23 @@ class Step4SamplingPanel(QWidget):
# 4. 同步更新预览按钮状态(路径可能已自动填充)
self._check_csv_exists()
def _on_run_single_clicked(self):
"""通过 EventBus 发布单步执行请求(解耦面板与 PipelineExecutor"""
from src.gui.core.event_bus import global_event_bus
deglint_img_path = self.deglint_img_file.get_path()
if not deglint_img_path:
QMessageBox.warning(self, "输入错误", "请选择去耀斑影像文件!")
return
config = {'step4_sampling': self.get_config()}
global_event_bus.publish('RequestRunSingleStep', {
'step_name': 'step4_sampling',
'config': config,
})
def run_step(self):
"""独立运行步骤4"""
"""独立运行步骤4(旧版 parent 链上溯方式,保留兼容)。"""
deglint_img_path = self.deglint_img_file.get_path()
if not deglint_img_path:
QMessageBox.warning(self, "输入错误", "请选择去耀斑影像文件!")

View File

@ -95,7 +95,7 @@ class Step5CleanPanel(QWidget):
# 独立运行按钮
self.run_btn = QPushButton("独立运行此步骤")
self.run_btn.setStyleSheet(ModernStylesheet.get_button_stylesheet('success'))
self.run_btn.clicked.connect(self.run_step)
self.run_btn.clicked.connect(self._on_run_single_clicked)
layout.addWidget(self.run_btn)
layout.addStretch()
@ -142,8 +142,23 @@ class Step5CleanPanel(QWidget):
else:
self.output_file.set_path("")
def _on_run_single_clicked(self):
"""通过 EventBus 发布单步执行请求(解耦面板与 PipelineExecutor"""
from src.gui.core.event_bus import global_event_bus
csv_path = self.csv_file.get_path()
if not csv_path:
QMessageBox.warning(self, "输入错误", "请选择水质参数文件!")
return
config = {'step5_clean': self.get_config()}
global_event_bus.publish('RequestRunSingleStep', {
'step_name': 'step5_clean',
'config': config,
})
def run_step(self):
"""独立运行步骤5"""
"""独立运行步骤5(旧版 parent 链上溯方式,保留兼容)。"""
csv_path = self.csv_file.get_path()
if not csv_path:
QMessageBox.warning(self, "输入错误", "请选择水质参数文件!")

View File

@ -106,7 +106,7 @@ class Step6FeaturePanel(QWidget):
# 独立运行按钮
self.run_btn = QPushButton("独立运行此步骤")
self.run_btn.setStyleSheet(ModernStylesheet.get_button_stylesheet('success'))
self.run_btn.clicked.connect(self.run_step)
self.run_btn.clicked.connect(self._on_run_single_clicked)
layout.addWidget(self.run_btn)
layout.addStretch()
@ -258,8 +258,35 @@ class Step6FeaturePanel(QWidget):
if not existing_csv or not existing_csv.strip():
self.csv_file.set_path(step5_clean_output_path)
def _on_run_single_clicked(self):
"""通过 EventBus 发布单步执行请求(解耦面板与 PipelineExecutor"""
from src.gui.core.event_bus import global_event_bus
deglint_img_path = self.deglint_img_file.get_path()
csv_path = self.csv_file.get_path()
if not deglint_img_path:
QMessageBox.warning(self, "输入错误", "请选择去耀斑影像文件!")
return
if not csv_path:
QMessageBox.warning(self, "输入错误", "请选择处理后的CSV文件")
return
if not self.glint_mask_file.get_path():
QMessageBox.warning(
self,
"输入错误",
"独立运行光谱特征提取时,必须选择耀斑掩膜文件。\n\n"
"请提供与去耀斑影像对应的耀斑二值掩膜一般为步骤2输出的 severe_glint_area.dat",
)
return
config = {'step6_feature': self.get_config()}
global_event_bus.publish('RequestRunSingleStep', {
'step_name': 'step6_feature',
'config': config,
})
def run_step(self):
"""独立运行步骤6"""
"""独立运行步骤6(旧版 parent 链上溯方式,保留兼容)。"""
# 验证输入
deglint_img_path = self.deglint_img_file.get_path()
csv_path = self.csv_file.get_path()

View File

@ -119,7 +119,7 @@ class Step8MlTrainPanel(QWidget):
# 独立运行按钮
self.run_btn = QPushButton("独立运行此步骤")
self.run_btn.setStyleSheet(ModernStylesheet.get_button_stylesheet('success'))
self.run_btn.clicked.connect(self.run_step)
self.run_btn.clicked.connect(self._on_run_single_clicked)
layout.addWidget(self.run_btn)
layout.addStretch()
@ -398,8 +398,23 @@ class Step8MlTrainPanel(QWidget):
else:
self.output_path.set_path("")
def _on_run_single_clicked(self):
"""通过 EventBus 发布单步执行请求(解耦面板与 PipelineExecutor"""
from src.gui.core.event_bus import global_event_bus
training_csv_path = self.training_csv_file.get_path()
if not training_csv_path:
QMessageBox.warning(self, "输入错误", "请选择训练数据CSV文件")
return
config = {'step8_ml_train': self.get_config()}
global_event_bus.publish('RequestRunSingleStep', {
'step_name': 'step8_ml_train',
'config': config,
})
def run_step(self):
"""独立运行步骤8"""
"""独立运行步骤8(旧版 parent 链上溯方式,保留兼容)。"""
training_csv_path = self.training_csv_file.get_path()
if not training_csv_path:
QMessageBox.warning(self, "输入错误", "请选择训练数据CSV文件")

View File

@ -109,7 +109,7 @@ class Step8QAAPanel(QWidget):
# 独立运行按钮
self.run_btn = QPushButton("执行 QAA 反演")
self.run_btn.setStyleSheet(ModernStylesheet.get_button_stylesheet('success'))
self.run_btn.clicked.connect(self.run_step)
self.run_btn.clicked.connect(self._on_run_single_clicked)
layout.addWidget(self.run_btn)
layout.addStretch()
@ -212,8 +212,23 @@ class Step8QAAPanel(QWidget):
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 反演"""
"""独立运行 QAA 反演(旧版 parent 链上溯方式,保留兼容)。"""
spectrum_path = self.spectrum_csv_file.get_path()
if not spectrum_path:
QMessageBox.warning(self, "输入错误", "请选择光谱 CSV 文件!")

View File

@ -175,7 +175,7 @@ class Step9MlPredictPanel(QWidget):
# 独立运行按钮
self.run_btn = QPushButton("独立运行此步骤")
self.run_btn.setStyleSheet(ModernStylesheet.get_button_stylesheet('success'))
self.run_btn.clicked.connect(self.run_step)
self.run_btn.clicked.connect(self._on_run_single_clicked)
layout.addWidget(self.run_btn)
layout.addStretch()
@ -414,8 +414,57 @@ class Step9MlPredictPanel(QWidget):
if 'output_path' in config:
self.output_file.set_path(config['output_path'])
def _on_run_single_clicked(self):
"""通过 EventBus 发布单步执行请求(解耦面板与 PipelineExecutor"""
from src.gui.core.event_bus import global_event_bus
sampling_csv_path = self.sampling_csv_file.get_path()
if not sampling_csv_path:
QMessageBox.warning(self, "输入错误", "请选择采样光谱CSV文件")
return
# 外部模型优先:用户选择了"导入本地预训练模型"
if self.use_external_model.isChecked():
if not self.external_models_dict:
QMessageBox.warning(
self,
"模型未加载",
"请先点击「浏览...」按钮选择模型母文件夹!",
)
return
checked_dict = self._get_checked_models_dict()
if not checked_dict:
QMessageBox.warning(
self,
"未选择模型",
"请至少勾选一个模型参与预测!",
)
return
config = {
'step9_ml_predict': self.get_config(),
'_external_models_dict': checked_dict,
'_external_model_dir': self.external_model_dir,
}
global_event_bus.publish('RequestRunSingleStep', {
'step_name': 'step9_ml_predict',
'config': config,
})
return
# 默认流程:使用模型目录
models_dir = self.models_dir_file.get_path()
if not models_dir:
QMessageBox.warning(self, "输入错误", "请选择模型目录!")
return
config = {'step9_ml_predict': self.get_config()}
global_event_bus.publish('RequestRunSingleStep', {
'step_name': 'step9_ml_predict',
'config': config,
})
def run_step(self):
"""独立运行步骤11"""
"""独立运行步骤11(旧版 parent 链上溯方式,保留兼容)。"""
sampling_csv_path = self.sampling_csv_file.get_path()
if not sampling_csv_path:
QMessageBox.warning(self, "输入错误", "请选择采样光谱CSV文件")

View File

@ -1244,7 +1244,7 @@ class WaterQualityGUI(QMainWindow):
if not PIPELINE_AVAILABLE:
QMessageBox.critical(
self, "错误",
"无法导入pipeline模块确保water_quality_inversion_pipeline_GUI.py文件存在"
"无法导入 Pipeline 模块,请检查 src/core/handlers/ 目录是否完整"
)
return
@ -1400,7 +1400,7 @@ class WaterQualityGUI(QMainWindow):
if not PIPELINE_AVAILABLE:
QMessageBox.critical(
self, "错误",
"无法导入pipeline模块确保water_quality_inversion_pipeline_GUI.py文件存在"
"无法导入 Pipeline 模块,请检查 src/core/handlers/ 目录是否完整"
)
return