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:
@ -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.step5_process_csv import Step5ProcessCsvHandler
|
||||||
from src.core.handlers.step6_extract_spectra import Step6ExtractSpectraHandler
|
from src.core.handlers.step6_extract_spectra import Step6ExtractSpectraHandler
|
||||||
from src.core.handlers.step7_calc_indices import Step7CalcIndicesHandler
|
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__ = [
|
__all__ = [
|
||||||
'BaseStepHandler',
|
'BaseStepHandler',
|
||||||
@ -29,4 +36,11 @@ __all__ = [
|
|||||||
'Step5ProcessCsvHandler',
|
'Step5ProcessCsvHandler',
|
||||||
'Step6ExtractSpectraHandler',
|
'Step6ExtractSpectraHandler',
|
||||||
'Step7CalcIndicesHandler',
|
'Step7CalcIndicesHandler',
|
||||||
|
'Step8MlTrainHandler',
|
||||||
|
'Step9MlPredictHandler',
|
||||||
|
'Step10QaaInversionHandler',
|
||||||
|
'Step11ConcentrationHandler',
|
||||||
|
'Step12KrigingHandler',
|
||||||
|
'Step13VisualizationHandler',
|
||||||
|
'Step14ReportHandler',
|
||||||
]
|
]
|
||||||
|
|||||||
@ -74,6 +74,11 @@ class PipelineContext:
|
|||||||
self.training_csv_path: Optional[str] = None
|
self.training_csv_path: Optional[str] = None
|
||||||
self.indices_path: Optional[str] = None
|
self.indices_path: Optional[str] = None
|
||||||
self.custom_regression_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] = {}
|
self.step_timings: Dict[str, dict] = {}
|
||||||
|
|||||||
@ -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.step5_process_csv import Step5ProcessCsvHandler
|
||||||
from src.core.handlers.step6_extract_spectra import Step6ExtractSpectraHandler
|
from src.core.handlers.step6_extract_spectra import Step6ExtractSpectraHandler
|
||||||
from src.core.handlers.step7_calc_indices import Step7CalcIndicesHandler
|
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:
|
if TYPE_CHECKING:
|
||||||
from src.core.handlers.pipeline_scheduler import PipelineScheduler
|
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(Step5ProcessCsvHandler())
|
||||||
scheduler.register_handler(Step6ExtractSpectraHandler())
|
scheduler.register_handler(Step6ExtractSpectraHandler())
|
||||||
scheduler.register_handler(Step7CalcIndicesHandler())
|
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())
|
||||||
|
|||||||
137
src/core/handlers/step10_qaa_inversion.py
Normal file
137
src/core/handlers/step10_qaa_inversion.py
Normal 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):
|
||||||
|
"""步骤10:QAA 准解析算法反演(非经验模型)。
|
||||||
|
|
||||||
|
对应 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
|
||||||
71
src/core/handlers/step11_concentration.py
Normal file
71
src/core/handlers/step11_concentration.py
Normal 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
|
||||||
81
src/core/handlers/step12_kriging.py
Normal file
81
src/core/handlers/step12_kriging.py
Normal 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
|
||||||
349
src/core/handlers/step13_visualization.py
Normal file
349
src/core/handlers/step13_visualization.py
Normal 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,
|
||||||
|
)
|
||||||
142
src/core/handlers/step14_report.py
Normal file
142
src/core/handlers/step14_report.py
Normal 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
|
||||||
58
src/core/handlers/step8_ml_train.py
Normal file
58
src/core/handlers/step8_ml_train.py
Normal 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
|
||||||
64
src/core/handlers/step9_ml_predict.py
Normal file
64
src/core/handlers/step9_ml_predict.py
Normal 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
@ -26,6 +26,7 @@ Pipeline 执行器
|
|||||||
|
|
||||||
import os
|
import os
|
||||||
import copy
|
import copy
|
||||||
|
import traceback
|
||||||
from pathlib import Path
|
from pathlib import Path
|
||||||
from typing import Dict, List, Optional
|
from typing import Dict, List, Optional
|
||||||
|
|
||||||
@ -74,6 +75,9 @@ class PipelineExecutor(QObject):
|
|||||||
self._workspace_initializer = workspace_initializer
|
self._workspace_initializer = workspace_initializer
|
||||||
self._worker: Optional[WorkerThread] = None
|
self._worker: Optional[WorkerThread] = None
|
||||||
|
|
||||||
|
# 订阅面板发出的单步执行请求(解耦面板与执行器)
|
||||||
|
global_event_bus.subscribe('RequestRunSingleStep', self._on_request_run_single_step)
|
||||||
|
|
||||||
# ═══════════════════════════════════════════════════════════
|
# ═══════════════════════════════════════════════════════════
|
||||||
# 公开 API
|
# 公开 API
|
||||||
# ═══════════════════════════════════════════════════════════
|
# ═══════════════════════════════════════════════════════════
|
||||||
@ -98,26 +102,60 @@ class PipelineExecutor(QObject):
|
|||||||
6. 获取配置 + 模式裁剪
|
6. 获取配置 + 模式裁剪
|
||||||
7. 一次性全预检 + 用户交互
|
7. 一次性全预检 + 用户交互
|
||||||
8. 确认执行 → 创建 WorkerThread → 启动
|
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:
|
if not PIPELINE_AVAILABLE:
|
||||||
global_event_bus.publish('LogMessage', {
|
global_event_bus.publish('LogMessage', {
|
||||||
'message': '无法导入 Pipeline 模块,请检查项目文件结构!',
|
'message': '无法导入 Pipeline 模块,请检查项目文件结构!',
|
||||||
'level': 'error',
|
'level': 'error',
|
||||||
})
|
})
|
||||||
# 阻断性错误仍需弹窗(用户必须知道)
|
|
||||||
QMessageBox.critical(
|
QMessageBox.critical(
|
||||||
self.parent(), "错误",
|
self.parent(), "错误",
|
||||||
"无法导入pipeline模块,请确保water_quality_inversion_pipeline_GUI.py文件存在!"
|
"无法导入 Pipeline 模块,请检查 src/core/handlers/ 目录是否完整!"
|
||||||
)
|
)
|
||||||
return
|
return
|
||||||
|
|
||||||
# ── 1) 获取 work_dir ──
|
# ── 1) 获取 work_dir ──
|
||||||
work_dir = self._workspace_initializer.work_dir
|
work_dir = self._workspace_initializer.work_dir
|
||||||
if not work_dir:
|
if not work_dir:
|
||||||
|
global_event_bus.publish('LogMessage', {
|
||||||
|
'message': '⚠ 未选择工作目录,流程中止。请先通过「工具 → 设置工作目录」选择工作目录。',
|
||||||
|
'level': 'warning',
|
||||||
|
})
|
||||||
QMessageBox.warning(self.parent(), "警告", "未选择工作目录,请先设置工作目录。")
|
QMessageBox.warning(self.parent(), "警告", "未选择工作目录,请先设置工作目录。")
|
||||||
return
|
return
|
||||||
|
|
||||||
work_path = Path(work_dir)
|
work_path = Path(work_dir)
|
||||||
|
global_event_bus.publish('LogMessage', {
|
||||||
|
'message': f'[运行] 工作目录: {work_dir}',
|
||||||
|
'level': 'info',
|
||||||
|
})
|
||||||
|
|
||||||
# ── 2) 运行前扫描 + 自动回填 ──
|
# ── 2) 运行前扫描 + 自动回填 ──
|
||||||
global_event_bus.publish('LogMessage', {
|
global_event_bus.publish('LogMessage', {
|
||||||
@ -132,11 +170,19 @@ class PipelineExecutor(QObject):
|
|||||||
|
|
||||||
# ── 3) step3 波段越界预检 ──
|
# ── 3) step3 波段越界预检 ──
|
||||||
if not self._precheck_step3_bands():
|
if not self._precheck_step3_bands():
|
||||||
|
global_event_bus.publish('LogMessage', {
|
||||||
|
'message': '⚠ 流程中止:step3 波段越界预检未通过(用户取消或波段配置无效)',
|
||||||
|
'level': 'warning',
|
||||||
|
})
|
||||||
return
|
return
|
||||||
|
|
||||||
# ── 4) 全流程模式选择弹窗 ──
|
# ── 4) 全流程模式选择弹窗 ──
|
||||||
mode_dlg = PipelineModeDialog(main_window=self.parent(), parent=self.parent())
|
mode_dlg = PipelineModeDialog(main_window=self.parent(), parent=self.parent())
|
||||||
if mode_dlg.exec() != QDialog.Accepted:
|
if mode_dlg.exec() != QDialog.Accepted:
|
||||||
|
global_event_bus.publish('LogMessage', {
|
||||||
|
'message': '⚠ 流程中止:用户取消了模式选择对话框',
|
||||||
|
'level': 'warning',
|
||||||
|
})
|
||||||
return
|
return
|
||||||
selected_mode = mode_dlg.selected_mode
|
selected_mode = mode_dlg.selected_mode
|
||||||
global_event_bus.publish('LogMessage', {
|
global_event_bus.publish('LogMessage', {
|
||||||
@ -147,8 +193,17 @@ class PipelineExecutor(QObject):
|
|||||||
'level': 'info',
|
'level': 'info',
|
||||||
})
|
})
|
||||||
|
|
||||||
# ── 5) 获取配置 ──
|
# ── 5) 获取配置(★ 先预加载所有面板,确保配置完整) ──
|
||||||
|
global_event_bus.publish('LogMessage', {
|
||||||
|
'message': '[运行] 正在收集所有步骤面板的配置...',
|
||||||
|
'level': 'info',
|
||||||
|
})
|
||||||
|
self._panel_factory.preload_all()
|
||||||
config = self._get_current_config()
|
config = self._get_current_config()
|
||||||
|
global_event_bus.publish('LogMessage', {
|
||||||
|
'message': f'[运行] 已收集 {len(config)} 个步骤的配置: {list(config.keys())}',
|
||||||
|
'level': 'info',
|
||||||
|
})
|
||||||
|
|
||||||
# ── 6) 模式裁剪 ──
|
# ── 6) 模式裁剪 ──
|
||||||
if selected_mode == "prediction_only":
|
if selected_mode == "prediction_only":
|
||||||
@ -164,9 +219,17 @@ class PipelineExecutor(QObject):
|
|||||||
skip_list: List[str] = []
|
skip_list: List[str] = []
|
||||||
|
|
||||||
if missing_items:
|
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]
|
critical_items = [it for it in missing_items if it.is_critical]
|
||||||
if critical_items:
|
if critical_items:
|
||||||
lines = "\n".join(f" - [{it.step_name}] {it.reason}" for it in 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(
|
QMessageBox.critical(
|
||||||
self.parent(), "预检失败(阻断性错误)",
|
self.parent(), "预检失败(阻断性错误)",
|
||||||
f"以下为阻断性缺失,流程无法启动:\n\n{lines}\n\n请填写后重新运行。"
|
f"以下为阻断性缺失,流程无法启动:\n\n{lines}\n\n请填写后重新运行。"
|
||||||
@ -175,21 +238,28 @@ class PipelineExecutor(QObject):
|
|||||||
|
|
||||||
dialog = PreflightDialog(missing_items, parent=self.parent())
|
dialog = PreflightDialog(missing_items, parent=self.parent())
|
||||||
if dialog.exec() != QDialog.Accepted:
|
if dialog.exec() != QDialog.Accepted:
|
||||||
|
global_event_bus.publish('LogMessage', {
|
||||||
|
'message': '⚠ 流程中止:用户取消了预检对话框',
|
||||||
|
'level': 'warning',
|
||||||
|
})
|
||||||
return
|
return
|
||||||
result = dialog.get_result()
|
result = dialog.get_result()
|
||||||
if result is None:
|
if result is None:
|
||||||
|
global_event_bus.publish('LogMessage', {
|
||||||
|
'message': '⚠ 流程中止:预检对话框返回空结果',
|
||||||
|
'level': 'warning',
|
||||||
|
})
|
||||||
return
|
return
|
||||||
|
|
||||||
action, *payload = result
|
action, *payload = result
|
||||||
if action == "fill":
|
if action == "fill":
|
||||||
_, step_id, tab_index = result
|
_, step_id, tab_index = result
|
||||||
# 发布事件:请求切换到指定 tab
|
|
||||||
global_event_bus.publish('NavigateToTab', {
|
global_event_bus.publish('NavigateToTab', {
|
||||||
'tab_index': tab_index,
|
'tab_index': tab_index,
|
||||||
'step_id': step_id,
|
'step_id': step_id,
|
||||||
})
|
})
|
||||||
global_event_bus.publish('LogMessage', {
|
global_event_bus.publish('LogMessage', {
|
||||||
'message': f'[预检] 用户选择填写 {step_id},已切换到对应面板。',
|
'message': f'[预检] 用户选择填写 {step_id},已切换到对应面板。流程暂停,填写完成后请重新运行。',
|
||||||
'level': 'info',
|
'level': 'info',
|
||||||
})
|
})
|
||||||
return
|
return
|
||||||
@ -197,8 +267,13 @@ class PipelineExecutor(QObject):
|
|||||||
if skip_list:
|
if skip_list:
|
||||||
global_event_bus.publish('LogMessage', {
|
global_event_bus.publish('LogMessage', {
|
||||||
'message': f'[预检] 用户强制跳过 {len(skip_list)} 个步骤: {skip_list}',
|
'message': f'[预检] 用户强制跳过 {len(skip_list)} 个步骤: {skip_list}',
|
||||||
'level': 'info',
|
'level': 'warning',
|
||||||
})
|
})
|
||||||
|
else:
|
||||||
|
global_event_bus.publish('LogMessage', {
|
||||||
|
'message': '[预检] ✓ 所有必需项均已就绪,无需弹窗',
|
||||||
|
'level': 'info',
|
||||||
|
})
|
||||||
|
|
||||||
# ── 8) 确认执行 ──
|
# ── 8) 确认执行 ──
|
||||||
reply = QMessageBox.question(
|
reply = QMessageBox.question(
|
||||||
@ -207,6 +282,10 @@ class PipelineExecutor(QObject):
|
|||||||
QMessageBox.Yes | QMessageBox.No
|
QMessageBox.Yes | QMessageBox.No
|
||||||
)
|
)
|
||||||
if reply != QMessageBox.Yes:
|
if reply != QMessageBox.Yes:
|
||||||
|
global_event_bus.publish('LogMessage', {
|
||||||
|
'message': '⚠ 流程中止:用户取消了执行确认',
|
||||||
|
'level': 'warning',
|
||||||
|
})
|
||||||
return
|
return
|
||||||
|
|
||||||
# ── 9) 准备 worker_config ──
|
# ── 9) 准备 worker_config ──
|
||||||
@ -222,6 +301,11 @@ class PipelineExecutor(QObject):
|
|||||||
if not enabled:
|
if not enabled:
|
||||||
worker_config.pop('step6_feature', None)
|
worker_config.pop('step6_feature', None)
|
||||||
|
|
||||||
|
global_event_bus.publish('LogMessage', {
|
||||||
|
'message': f'[运行] 最终执行配置包含 {len(worker_config)} 个步骤: {list(worker_config.keys())}',
|
||||||
|
'level': 'info',
|
||||||
|
})
|
||||||
|
|
||||||
# ── 10) 创建 WorkerThread 并连线 ──
|
# ── 10) 创建 WorkerThread 并连线 ──
|
||||||
self._worker = WorkerThread(work_dir, worker_config, mode='full', skip_list=skip_list)
|
self._worker = WorkerThread(work_dir, worker_config, mode='full', skip_list=skip_list)
|
||||||
self._worker.log_message.connect(self._on_log_message, Qt.QueuedConnection)
|
self._worker.log_message.connect(self._on_log_message, Qt.QueuedConnection)
|
||||||
@ -245,17 +329,48 @@ class PipelineExecutor(QObject):
|
|||||||
step_name: 步骤名称(如 'step1', 'step5_clean')
|
step_name: 步骤名称(如 'step1', 'step5_clean')
|
||||||
config: 步骤配置字典(可选,默认从面板获取)
|
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:
|
if not PIPELINE_AVAILABLE:
|
||||||
|
global_event_bus.publish('LogMessage', {
|
||||||
|
'message': '无法导入 Pipeline 模块,请检查 src/core/handlers/ 目录是否完整!',
|
||||||
|
'level': 'error',
|
||||||
|
})
|
||||||
QMessageBox.critical(
|
QMessageBox.critical(
|
||||||
self.parent(), "错误",
|
self.parent(), "错误",
|
||||||
"无法导入pipeline模块,请确保water_quality_inversion_pipeline_GUI.py文件存在!"
|
"无法导入 Pipeline 模块,请检查 src/core/handlers/ 目录是否完整!"
|
||||||
)
|
)
|
||||||
return
|
return
|
||||||
|
|
||||||
work_dir = self._workspace_initializer.work_dir or './work_dir'
|
work_dir = self._workspace_initializer.work_dir or './work_dir'
|
||||||
|
|
||||||
if config is None:
|
if config is None:
|
||||||
|
global_event_bus.publish('LogMessage', {
|
||||||
|
'message': '[运行] 正在收集所有步骤面板的配置...',
|
||||||
|
'level': 'info',
|
||||||
|
})
|
||||||
|
self._panel_factory.preload_all()
|
||||||
config = self._get_current_config()
|
config = self._get_current_config()
|
||||||
|
global_event_bus.publish('LogMessage', {
|
||||||
|
'message': f'[运行] 已收集 {len(config)} 个步骤的配置',
|
||||||
|
'level': 'info',
|
||||||
|
})
|
||||||
|
|
||||||
global_event_bus.publish('LogMessage', {
|
global_event_bus.publish('LogMessage', {
|
||||||
'message': f'初始化 Pipeline,工作目录: {work_dir}',
|
'message': f'初始化 Pipeline,工作目录: {work_dir}',
|
||||||
@ -295,6 +410,47 @@ class PipelineExecutor(QObject):
|
|||||||
})
|
})
|
||||||
global_event_bus.publish('PipelineStopped', {})
|
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 操作)
|
# WorkerThread 信号 → EventBus 事件(纯转发,零 UI 操作)
|
||||||
# ═══════════════════════════════════════════════════════════
|
# ═══════════════════════════════════════════════════════════
|
||||||
|
|||||||
@ -178,7 +178,7 @@ class VisualizationWorkerThread(QThread):
|
|||||||
{"task": "statistics", "output_paths": output_paths}
|
{"task": "statistics", "output_paths": output_paths}
|
||||||
)
|
)
|
||||||
elif self.task == "scatter":
|
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()
|
training_csv_path = (self.extra.get("training_csv_path") or "").strip()
|
||||||
models_dir = (self.extra.get("models_dir") 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():
|
if not models_dir or not Path(models_dir).is_dir():
|
||||||
self.failed.emit("模型目录无效或不存在,请确认步骤6已生成 7_Supervised_Model_Training 下的参数子文件夹。")
|
self.failed.emit("模型目录无效或不存在,请确认步骤6已生成 7_Supervised_Model_Training 下的参数子文件夹。")
|
||||||
return
|
return
|
||||||
pipeline = WaterQualityInversionPipeline(work_dir=str(wp))
|
scatter_paths = generate_model_scatter_plots(
|
||||||
scatter_paths = pipeline.generate_model_scatter_plots(
|
|
||||||
training_csv_path=training_csv_path,
|
|
||||||
models_dir=models_dir,
|
models_dir=models_dir,
|
||||||
|
training_csv_path=training_csv_path,
|
||||||
)
|
)
|
||||||
self.finished_ok.emit({"task": "scatter", "scatter_paths": scatter_paths or {}})
|
self.finished_ok.emit({"task": "scatter", "scatter_paths": scatter_paths or {}})
|
||||||
elif self.task == "generate_all_selected":
|
elif self.task == "generate_all_selected":
|
||||||
@ -205,11 +204,10 @@ class VisualizationWorkerThread(QThread):
|
|||||||
if training_csv.is_file():
|
if training_csv.is_file():
|
||||||
models_dir = wp / "7_Supervised_Model_Training"
|
models_dir = wp / "7_Supervised_Model_Training"
|
||||||
if models_dir.is_dir() and any(d.is_dir() for d in models_dir.iterdir()):
|
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
|
from src.core.visualization.scatter_plot import generate_model_scatter_plots
|
||||||
pipeline = WaterQualityInversionPipeline(work_dir=str(wp))
|
scatter_paths = generate_model_scatter_plots(
|
||||||
scatter_paths = pipeline.generate_model_scatter_plots(
|
|
||||||
training_csv_path=str(training_csv),
|
|
||||||
models_dir=str(models_dir),
|
models_dir=str(models_dir),
|
||||||
|
training_csv_path=str(training_csv),
|
||||||
)
|
)
|
||||||
count = len(scatter_paths) if scatter_paths else 0
|
count = len(scatter_paths) if scatter_paths else 0
|
||||||
parts.append(f"散点图: {count} 个")
|
parts.append(f"散点图: {count} 个")
|
||||||
|
|||||||
@ -54,16 +54,16 @@ def diagnose_pipeline_import_error():
|
|||||||
"[INFO] PyInstaller 环境:Pipeline 从程序内置包加载,跳过对仓库路径 src/core/*.py 的磁盘检查"
|
"[INFO] PyInstaller 环境:Pipeline 从程序内置包加载,跳过对仓库路径 src/core/*.py 的磁盘检查"
|
||||||
)
|
)
|
||||||
else:
|
else:
|
||||||
pipeline_file = os.path.normpath(
|
handlers_dir = os.path.normpath(
|
||||||
os.path.join(os.path.dirname(__file__), "..", "..", "core", "water_quality_inversion_pipeline_GUI.py")
|
os.path.join(os.path.dirname(__file__), "..", "..", "core", "handlers")
|
||||||
)
|
)
|
||||||
if not os.path.exists(pipeline_file):
|
if not os.path.isdir(handlers_dir):
|
||||||
error_info.append(f"[ERROR] Pipeline文件不存在: {pipeline_file}")
|
error_info.append(f"[ERROR] Handlers 目录不存在: {handlers_dir}")
|
||||||
error_info.append(
|
error_info.append(
|
||||||
" 解决方案: 请确保项目结构完整,检查 src/core/ 下是否有 water_quality_inversion_pipeline_GUI.py"
|
" 解决方案: 请确保项目结构完整,检查 src/core/handlers/ 目录是否存在"
|
||||||
)
|
)
|
||||||
else:
|
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__)))
|
current_dir = os.path.dirname(os.path.dirname(os.path.dirname(__file__)))
|
||||||
if current_dir not in sys.path:
|
if current_dir not in sys.path:
|
||||||
@ -240,24 +240,34 @@ class WorkerThread(QThread):
|
|||||||
self.log_message.emit(f" [WARNING] {message}", "warning")
|
self.log_message.emit(f" [WARNING] {message}", "warning")
|
||||||
|
|
||||||
def run(self):
|
def run(self):
|
||||||
"""运行 pipeline:子线程内切换 Matplotlib 为 Agg,避免 Qt5Agg 在后台线程绘图导致界面卡死。"""
|
"""运行 pipeline:子线程内切换 Matplotlib 为 Agg,避免 Qt5Agg 在后台线程绘图导致界面卡死。
|
||||||
|
|
||||||
|
终极防崩溃设计:
|
||||||
|
- 整个 run() 方法体包裹在单一 try/except 中
|
||||||
|
- 任何未预期的异常都会被捕获并通过 finished 信号回报主线程
|
||||||
|
- 确保前端永远不会面对"静默死亡"的后台线程
|
||||||
|
"""
|
||||||
import os
|
import os
|
||||||
# GDAL 环境变量保护(放在最前面,防止路径/编码问题)
|
|
||||||
os.environ['GDAL_FILENAME_IS_UTF8'] = 'YES'
|
os.environ['GDAL_FILENAME_IS_UTF8'] = 'YES'
|
||||||
os.environ['SHAPE_ENCODING'] = 'UTF-8'
|
os.environ['SHAPE_ENCODING'] = 'UTF-8'
|
||||||
|
|
||||||
mpl_prev = None
|
mpl_prev = None
|
||||||
try:
|
try:
|
||||||
import matplotlib
|
# ★ 终端即时反馈
|
||||||
mpl_prev = matplotlib.get_backend()
|
print(f"\n[WorkerThread] 后台线程启动 (mode={self.mode}, work_dir={self.work_dir})")
|
||||||
except Exception:
|
|
||||||
pass
|
# ── Matplotlib 后端切换(Agg 线程安全) ──
|
||||||
try:
|
try:
|
||||||
import matplotlib.pyplot as plt
|
import matplotlib
|
||||||
plt.switch_backend("Agg")
|
mpl_prev = matplotlib.get_backend()
|
||||||
except Exception:
|
except Exception:
|
||||||
mpl_prev = None
|
pass
|
||||||
try:
|
try:
|
||||||
|
import matplotlib.pyplot as plt
|
||||||
|
plt.switch_backend("Agg")
|
||||||
|
except Exception:
|
||||||
|
mpl_prev = None
|
||||||
|
|
||||||
# ── 新架构:PipelineScheduler + Handler 注册表 ──
|
# ── 新架构:PipelineScheduler + Handler 注册表 ──
|
||||||
scheduler = PipelineScheduler(work_dir=self.work_dir)
|
scheduler = PipelineScheduler(work_dir=self.work_dir)
|
||||||
scheduler.set_callback(self.pipeline_callback)
|
scheduler.set_callback(self.pipeline_callback)
|
||||||
@ -267,14 +277,17 @@ class WorkerThread(QThread):
|
|||||||
if self.mode == 'full':
|
if self.mode == 'full':
|
||||||
self.log_message.emit("开始运行完整流程 (Handler 调度模式)...", "info")
|
self.log_message.emit("开始运行完整流程 (Handler 调度模式)...", "info")
|
||||||
|
|
||||||
# ── ★ 预检已由 GUI 层 perform_preflight() 完成,此处不再重复预检 ──
|
|
||||||
|
|
||||||
# 过滤 skip_list 中的步骤
|
# 过滤 skip_list 中的步骤
|
||||||
active_config = {
|
active_config = {
|
||||||
k: v for k, v in self.config.items()
|
k: v for k, v in self.config.items()
|
||||||
if k not in self.skip_list
|
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)
|
result = scheduler.run_full_pipeline(active_config)
|
||||||
|
|
||||||
errors = result.get('errors', {})
|
errors = result.get('errors', {})
|
||||||
@ -295,16 +308,28 @@ class WorkerThread(QThread):
|
|||||||
|
|
||||||
self.progress_update.emit(100, f"步骤 {self.step_name} 执行完成")
|
self.progress_update.emit(100, f"步骤 {self.step_name} 执行完成")
|
||||||
self.finished.emit(True, f"步骤 {self.step_name} 独立运行成功!")
|
self.finished.emit(True, f"步骤 {self.step_name} 独立运行成功!")
|
||||||
|
|
||||||
except PipelineHalt as exc:
|
except PipelineHalt as exc:
|
||||||
# 预检失败 / 硬终止:透传清晰错误信息,不打印完整 traceback
|
# 预检失败 / 硬终止:透传清晰错误信息,不打印完整 traceback
|
||||||
error_msg = str(exc)
|
error_msg = str(exc)
|
||||||
self.log_message.emit(f"[预检失败] {error_msg}", "error")
|
self.log_message.emit(f"[预检失败] {error_msg}", "error")
|
||||||
self.finished.emit(False, error_msg)
|
self.finished.emit(False, error_msg)
|
||||||
|
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
error_msg = f"执行失败: {str(e)}\n{traceback.format_exc()}"
|
# ★ 终极捕获:任何未预期的异常都会被完整回报
|
||||||
self.log_message.emit(error_msg, "error")
|
full_tb = traceback.format_exc()
|
||||||
self.finished.emit(False, error_msg)
|
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:
|
finally:
|
||||||
|
# ── 恢复 Matplotlib 后端 ──
|
||||||
if mpl_prev:
|
if mpl_prev:
|
||||||
try:
|
try:
|
||||||
import matplotlib.pyplot as plt
|
import matplotlib.pyplot as plt
|
||||||
|
|||||||
@ -243,7 +243,7 @@ class Step10WatercolorPanel(QWidget):
|
|||||||
|
|
||||||
self.run_btn = QPushButton("▶ 执行水色指数反演")
|
self.run_btn = QPushButton("▶ 执行水色指数反演")
|
||||||
self.run_btn.setStyleSheet(ModernStylesheet.get_button_stylesheet('success'))
|
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.addWidget(self.run_btn)
|
||||||
|
|
||||||
layout.addStretch()
|
layout.addStretch()
|
||||||
@ -484,7 +484,54 @@ class Step10WatercolorPanel(QWidget):
|
|||||||
if not self.output_dir.get_path():
|
if not self.output_dir.get_path():
|
||||||
self.output_dir.set_path(out_dir)
|
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):
|
def run_step(self):
|
||||||
|
"""独立运行步骤10(旧版 parent 链上溯方式,保留兼容)。"""
|
||||||
bsq_path = self.bsq_file.get_path().strip()
|
bsq_path = self.bsq_file.get_path().strip()
|
||||||
hdr_path = self.hdr_file.get_path().strip()
|
hdr_path = self.hdr_file.get_path().strip()
|
||||||
output_dir = self.output_dir.get_path().strip()
|
output_dir = self.output_dir.get_path().strip()
|
||||||
|
|||||||
@ -27,12 +27,7 @@ from PyQt5.QtWidgets import (
|
|||||||
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
|
||||||
|
|
||||||
# Pipeline 可用性(与 core/worker_thread.py 保持一致)
|
PIPELINE_AVAILABLE = True
|
||||||
try:
|
|
||||||
from src.core.water_quality_inversion_pipeline_GUI import WaterQualityInversionPipeline
|
|
||||||
PIPELINE_AVAILABLE = True
|
|
||||||
except ImportError:
|
|
||||||
PIPELINE_AVAILABLE = False
|
|
||||||
|
|
||||||
|
|
||||||
class Step11MapBatchThread(QThread):
|
class Step11MapBatchThread(QThread):
|
||||||
@ -63,19 +58,19 @@ class Step11MapBatchThread(QThread):
|
|||||||
except Exception:
|
except Exception:
|
||||||
mpl_prev = None
|
mpl_prev = None
|
||||||
try:
|
try:
|
||||||
from src.core.water_quality_inversion_pipeline_GUI import WaterQualityInversionPipeline
|
from src.core.steps.mapping_step import MappingStep
|
||||||
pipeline = WaterQualityInversionPipeline(work_dir=self.work_dir)
|
|
||||||
n = len(self.csv_paths)
|
n = len(self.csv_paths)
|
||||||
for i, csv_p in enumerate(self.csv_paths):
|
for i, csv_p in enumerate(self.csv_paths):
|
||||||
self.progress.emit(i + 1, n)
|
self.progress.emit(i + 1, n)
|
||||||
self.log_message.emit(f"专题图 [{i + 1}/{n}] {csv_p}", "info")
|
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:
|
if self.output_dir_optional:
|
||||||
stem = Path(csv_p).stem
|
stem = Path(csv_p).stem
|
||||||
kw["output_image_path"] = str(Path(self.output_dir_optional) / f"{stem}_distribution.png")
|
kw["output_image_path"] = str(Path(self.output_dir_optional) / f"{stem}_distribution.png")
|
||||||
else:
|
else:
|
||||||
kw["output_image_path"] = None
|
kw["output_image_path"] = None
|
||||||
pipeline.step10_map(**kw)
|
MappingStep.generate_distribution_map(**kw)
|
||||||
self.finished_ok.emit(n)
|
self.finished_ok.emit(n)
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
self.failed.emit(f"{e}\n{traceback.format_exc()}")
|
self.failed.emit(f"{e}\n{traceback.format_exc()}")
|
||||||
|
|||||||
@ -32,12 +32,7 @@ from matplotlib.backends.backend_qt5agg import FigureCanvasQTAgg as FigureCanvas
|
|||||||
from matplotlib.backends.backend_qt5agg import NavigationToolbar2QT as NavigationToolbar
|
from matplotlib.backends.backend_qt5agg import NavigationToolbar2QT as NavigationToolbar
|
||||||
from matplotlib.figure import Figure
|
from matplotlib.figure import Figure
|
||||||
|
|
||||||
# Pipeline 可用性(与 core/worker_thread.py 保持一致)
|
PIPELINE_AVAILABLE = True
|
||||||
try:
|
|
||||||
from src.core.water_quality_inversion_pipeline_GUI import WaterQualityInversionPipeline
|
|
||||||
PIPELINE_AVAILABLE = True
|
|
||||||
except ImportError:
|
|
||||||
PIPELINE_AVAILABLE = False
|
|
||||||
|
|
||||||
|
|
||||||
def _viz_training_spectra_csv_path(work_path: Path) -> Path:
|
def _viz_training_spectra_csv_path(work_path: Path) -> Path:
|
||||||
@ -208,7 +203,7 @@ class VisualizationWorkerThread(QThread):
|
|||||||
{"task": "statistics", "output_paths": output_paths}
|
{"task": "statistics", "output_paths": output_paths}
|
||||||
)
|
)
|
||||||
elif self.task == "scatter":
|
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()
|
training_csv_path = (self.extra.get("training_csv_path") or "").strip()
|
||||||
models_dir = (self.extra.get("models_dir") 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():
|
if not models_dir or not Path(models_dir).is_dir():
|
||||||
self.failed.emit("模型目录无效或不存在,请确认步骤6已生成 7_Supervised_Model_Training 下的参数子文件夹。")
|
self.failed.emit("模型目录无效或不存在,请确认步骤6已生成 7_Supervised_Model_Training 下的参数子文件夹。")
|
||||||
return
|
return
|
||||||
pipeline = WaterQualityInversionPipeline(work_dir=str(wp))
|
scatter_paths = generate_model_scatter_plots(
|
||||||
scatter_paths = pipeline.generate_model_scatter_plots(
|
|
||||||
training_csv_path=training_csv_path,
|
|
||||||
models_dir=models_dir,
|
models_dir=models_dir,
|
||||||
|
training_csv_path=training_csv_path,
|
||||||
)
|
)
|
||||||
self.finished_ok.emit({"task": "scatter", "scatter_paths": scatter_paths or {}})
|
self.finished_ok.emit({"task": "scatter", "scatter_paths": scatter_paths or {}})
|
||||||
elif self.task == "generate_all_selected":
|
elif self.task == "generate_all_selected":
|
||||||
@ -235,11 +229,10 @@ class VisualizationWorkerThread(QThread):
|
|||||||
if training_csv.is_file():
|
if training_csv.is_file():
|
||||||
models_dir = wp / "7_Supervised_Model_Training"
|
models_dir = wp / "7_Supervised_Model_Training"
|
||||||
if models_dir.is_dir() and any(d.is_dir() for d in models_dir.iterdir()):
|
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
|
from src.core.visualization.scatter_plot import generate_model_scatter_plots
|
||||||
pipeline = WaterQualityInversionPipeline(work_dir=str(wp))
|
scatter_paths = generate_model_scatter_plots(
|
||||||
scatter_paths = pipeline.generate_model_scatter_plots(
|
|
||||||
training_csv_path=str(training_csv),
|
|
||||||
models_dir=str(models_dir),
|
models_dir=str(models_dir),
|
||||||
|
training_csv_path=str(training_csv),
|
||||||
)
|
)
|
||||||
count = len(scatter_paths) if scatter_paths else 0
|
count = len(scatter_paths) if scatter_paths else 0
|
||||||
parts.append(f"散点图: {count} 个")
|
parts.append(f"散点图: {count} 个")
|
||||||
|
|||||||
@ -27,12 +27,7 @@ from PyQt5.QtWidgets import (
|
|||||||
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
|
||||||
|
|
||||||
# Pipeline 可用性(与 core/worker_thread.py 保持一致)
|
PIPELINE_AVAILABLE = True
|
||||||
try:
|
|
||||||
from src.core.water_quality_inversion_pipeline_GUI import WaterQualityInversionPipeline
|
|
||||||
PIPELINE_AVAILABLE = True
|
|
||||||
except ImportError:
|
|
||||||
PIPELINE_AVAILABLE = False
|
|
||||||
|
|
||||||
|
|
||||||
class Step14BatchThread(QThread):
|
class Step14BatchThread(QThread):
|
||||||
@ -63,19 +58,19 @@ class Step14BatchThread(QThread):
|
|||||||
except Exception:
|
except Exception:
|
||||||
mpl_prev = None
|
mpl_prev = None
|
||||||
try:
|
try:
|
||||||
from src.core.water_quality_inversion_pipeline_GUI import WaterQualityInversionPipeline
|
from src.core.steps.mapping_step import MappingStep
|
||||||
pipeline = WaterQualityInversionPipeline(work_dir=self.work_dir)
|
|
||||||
n = len(self.csv_paths)
|
n = len(self.csv_paths)
|
||||||
for i, csv_p in enumerate(self.csv_paths):
|
for i, csv_p in enumerate(self.csv_paths):
|
||||||
self.progress.emit(i + 1, n)
|
self.progress.emit(i + 1, n)
|
||||||
self.log_message.emit(f"专题图 [{i + 1}/{n}] {csv_p}", "info")
|
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:
|
if self.output_dir_optional:
|
||||||
stem = Path(csv_p).stem
|
stem = Path(csv_p).stem
|
||||||
kw["output_image_path"] = str(Path(self.output_dir_optional) / f"{stem}_distribution.png")
|
kw["output_image_path"] = str(Path(self.output_dir_optional) / f"{stem}_distribution.png")
|
||||||
else:
|
else:
|
||||||
kw["output_image_path"] = None
|
kw["output_image_path"] = None
|
||||||
pipeline.step10_map(**kw)
|
MappingStep.generate_distribution_map(**kw)
|
||||||
self.finished_ok.emit(n)
|
self.finished_ok.emit(n)
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
self.failed.emit(f"{e}\n{traceback.format_exc()}")
|
self.failed.emit(f"{e}\n{traceback.format_exc()}")
|
||||||
|
|||||||
@ -144,7 +144,7 @@ class Step1Panel(QWidget):
|
|||||||
# 独立运行按钮
|
# 独立运行按钮
|
||||||
self.run_btn = QPushButton("独立运行此步骤")
|
self.run_btn = QPushButton("独立运行此步骤")
|
||||||
self.run_btn.setStyleSheet(ModernStylesheet.get_button_stylesheet('success'))
|
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.addWidget(self.run_btn)
|
||||||
|
|
||||||
# 连接信号
|
# 连接信号
|
||||||
@ -257,8 +257,40 @@ class Step1Panel(QWidget):
|
|||||||
|
|
||||||
self.update_ui_state()
|
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):
|
def run_step(self):
|
||||||
"""独立运行步骤1"""
|
"""独立运行步骤1(旧版 parent 链上溯方式,保留兼容)。"""
|
||||||
# 验证输入
|
# 验证输入
|
||||||
if self.use_ndwi_radio.isChecked():
|
if self.use_ndwi_radio.isChecked():
|
||||||
# NDWI模式:需要影像文件
|
# NDWI模式:需要影像文件
|
||||||
|
|||||||
@ -108,7 +108,7 @@ class Step2Panel(QWidget):
|
|||||||
# 独立运行按钮
|
# 独立运行按钮
|
||||||
self.run_btn = QPushButton("独立运行此步骤")
|
self.run_btn = QPushButton("独立运行此步骤")
|
||||||
self.run_btn.setStyleSheet(ModernStylesheet.get_button_stylesheet('success'))
|
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.addWidget(self.run_btn)
|
||||||
|
|
||||||
layout.addStretch()
|
layout.addStretch()
|
||||||
@ -203,8 +203,23 @@ class Step2Panel(QWidget):
|
|||||||
# 没有工作目录时,清空输出路径
|
# 没有工作目录时,清空输出路径
|
||||||
self.output_file.set_path("")
|
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):
|
def run_step(self):
|
||||||
"""独立运行步骤2"""
|
"""独立运行步骤2(旧版 parent 链上溯方式,保留兼容)。"""
|
||||||
# 验证输入
|
# 验证输入
|
||||||
img_path = self.img_file.get_path()
|
img_path = self.img_file.get_path()
|
||||||
if not img_path:
|
if not img_path:
|
||||||
|
|||||||
@ -228,7 +228,7 @@ class Step3Panel(QWidget):
|
|||||||
# 独立运行按钮
|
# 独立运行按钮
|
||||||
self.run_btn = QPushButton("独立运行此步骤")
|
self.run_btn = QPushButton("独立运行此步骤")
|
||||||
self.run_btn.setStyleSheet(ModernStylesheet.get_button_stylesheet('success'))
|
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.addWidget(self.run_btn)
|
||||||
|
|
||||||
layout.addStretch()
|
layout.addStretch()
|
||||||
@ -433,8 +433,34 @@ class Step3Panel(QWidget):
|
|||||||
if 'sugar_bounds' in config:
|
if 'sugar_bounds' in config:
|
||||||
self.sugar_bounds.setText(str(config['sugar_bounds']))
|
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):
|
def run_step(self):
|
||||||
"""独立运行步骤3"""
|
"""独立运行步骤3(旧版 parent 链上溯方式,保留兼容)。"""
|
||||||
# 验证输入
|
# 验证输入
|
||||||
img_path = self.img_file.get_path()
|
img_path = self.img_file.get_path()
|
||||||
if not img_path:
|
if not img_path:
|
||||||
|
|||||||
@ -91,7 +91,7 @@ class Step4SamplingPanel(QWidget):
|
|||||||
# 独立运行按钮
|
# 独立运行按钮
|
||||||
self.run_btn = QPushButton("独立运行此步骤")
|
self.run_btn = QPushButton("独立运行此步骤")
|
||||||
self.run_btn.setStyleSheet(ModernStylesheet.get_button_stylesheet('success'))
|
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.addWidget(self.run_btn)
|
||||||
|
|
||||||
# 交互式预览按钮
|
# 交互式预览按钮
|
||||||
@ -228,8 +228,23 @@ class Step4SamplingPanel(QWidget):
|
|||||||
# 4. 同步更新预览按钮状态(路径可能已自动填充)
|
# 4. 同步更新预览按钮状态(路径可能已自动填充)
|
||||||
self._check_csv_exists()
|
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):
|
def run_step(self):
|
||||||
"""独立运行步骤4"""
|
"""独立运行步骤4(旧版 parent 链上溯方式,保留兼容)。"""
|
||||||
deglint_img_path = self.deglint_img_file.get_path()
|
deglint_img_path = self.deglint_img_file.get_path()
|
||||||
if not deglint_img_path:
|
if not deglint_img_path:
|
||||||
QMessageBox.warning(self, "输入错误", "请选择去耀斑影像文件!")
|
QMessageBox.warning(self, "输入错误", "请选择去耀斑影像文件!")
|
||||||
|
|||||||
@ -95,7 +95,7 @@ class Step5CleanPanel(QWidget):
|
|||||||
# 独立运行按钮
|
# 独立运行按钮
|
||||||
self.run_btn = QPushButton("独立运行此步骤")
|
self.run_btn = QPushButton("独立运行此步骤")
|
||||||
self.run_btn.setStyleSheet(ModernStylesheet.get_button_stylesheet('success'))
|
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.addWidget(self.run_btn)
|
||||||
|
|
||||||
layout.addStretch()
|
layout.addStretch()
|
||||||
@ -142,8 +142,23 @@ class Step5CleanPanel(QWidget):
|
|||||||
else:
|
else:
|
||||||
self.output_file.set_path("")
|
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):
|
def run_step(self):
|
||||||
"""独立运行步骤5"""
|
"""独立运行步骤5(旧版 parent 链上溯方式,保留兼容)。"""
|
||||||
csv_path = self.csv_file.get_path()
|
csv_path = self.csv_file.get_path()
|
||||||
if not csv_path:
|
if not csv_path:
|
||||||
QMessageBox.warning(self, "输入错误", "请选择水质参数文件!")
|
QMessageBox.warning(self, "输入错误", "请选择水质参数文件!")
|
||||||
|
|||||||
@ -106,7 +106,7 @@ class Step6FeaturePanel(QWidget):
|
|||||||
# 独立运行按钮
|
# 独立运行按钮
|
||||||
self.run_btn = QPushButton("独立运行此步骤")
|
self.run_btn = QPushButton("独立运行此步骤")
|
||||||
self.run_btn.setStyleSheet(ModernStylesheet.get_button_stylesheet('success'))
|
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.addWidget(self.run_btn)
|
||||||
|
|
||||||
layout.addStretch()
|
layout.addStretch()
|
||||||
@ -258,8 +258,35 @@ class Step6FeaturePanel(QWidget):
|
|||||||
if not existing_csv or not existing_csv.strip():
|
if not existing_csv or not existing_csv.strip():
|
||||||
self.csv_file.set_path(step5_clean_output_path)
|
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):
|
def run_step(self):
|
||||||
"""独立运行步骤6"""
|
"""独立运行步骤6(旧版 parent 链上溯方式,保留兼容)。"""
|
||||||
# 验证输入
|
# 验证输入
|
||||||
deglint_img_path = self.deglint_img_file.get_path()
|
deglint_img_path = self.deglint_img_file.get_path()
|
||||||
csv_path = self.csv_file.get_path()
|
csv_path = self.csv_file.get_path()
|
||||||
|
|||||||
@ -119,7 +119,7 @@ class Step8MlTrainPanel(QWidget):
|
|||||||
# 独立运行按钮
|
# 独立运行按钮
|
||||||
self.run_btn = QPushButton("独立运行此步骤")
|
self.run_btn = QPushButton("独立运行此步骤")
|
||||||
self.run_btn.setStyleSheet(ModernStylesheet.get_button_stylesheet('success'))
|
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.addWidget(self.run_btn)
|
||||||
|
|
||||||
layout.addStretch()
|
layout.addStretch()
|
||||||
@ -398,8 +398,23 @@ class Step8MlTrainPanel(QWidget):
|
|||||||
else:
|
else:
|
||||||
self.output_path.set_path("")
|
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):
|
def run_step(self):
|
||||||
"""独立运行步骤8"""
|
"""独立运行步骤8(旧版 parent 链上溯方式,保留兼容)。"""
|
||||||
training_csv_path = self.training_csv_file.get_path()
|
training_csv_path = self.training_csv_file.get_path()
|
||||||
if not training_csv_path:
|
if not training_csv_path:
|
||||||
QMessageBox.warning(self, "输入错误", "请选择训练数据CSV文件!")
|
QMessageBox.warning(self, "输入错误", "请选择训练数据CSV文件!")
|
||||||
|
|||||||
@ -109,7 +109,7 @@ class Step8QAAPanel(QWidget):
|
|||||||
# 独立运行按钮
|
# 独立运行按钮
|
||||||
self.run_btn = QPushButton("执行 QAA 反演")
|
self.run_btn = QPushButton("执行 QAA 反演")
|
||||||
self.run_btn.setStyleSheet(ModernStylesheet.get_button_stylesheet('success'))
|
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.addWidget(self.run_btn)
|
||||||
|
|
||||||
layout.addStretch()
|
layout.addStretch()
|
||||||
@ -212,8 +212,23 @@ class Step8QAAPanel(QWidget):
|
|||||||
else:
|
else:
|
||||||
self.output_path.set_path("")
|
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):
|
def run_step(self):
|
||||||
"""独立运行 QAA 反演"""
|
"""独立运行 QAA 反演(旧版 parent 链上溯方式,保留兼容)。"""
|
||||||
spectrum_path = self.spectrum_csv_file.get_path()
|
spectrum_path = self.spectrum_csv_file.get_path()
|
||||||
if not spectrum_path:
|
if not spectrum_path:
|
||||||
QMessageBox.warning(self, "输入错误", "请选择光谱 CSV 文件!")
|
QMessageBox.warning(self, "输入错误", "请选择光谱 CSV 文件!")
|
||||||
|
|||||||
@ -175,7 +175,7 @@ class Step9MlPredictPanel(QWidget):
|
|||||||
# 独立运行按钮
|
# 独立运行按钮
|
||||||
self.run_btn = QPushButton("独立运行此步骤")
|
self.run_btn = QPushButton("独立运行此步骤")
|
||||||
self.run_btn.setStyleSheet(ModernStylesheet.get_button_stylesheet('success'))
|
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.addWidget(self.run_btn)
|
||||||
|
|
||||||
layout.addStretch()
|
layout.addStretch()
|
||||||
@ -414,8 +414,57 @@ class Step9MlPredictPanel(QWidget):
|
|||||||
if 'output_path' in config:
|
if 'output_path' in config:
|
||||||
self.output_file.set_path(config['output_path'])
|
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):
|
def run_step(self):
|
||||||
"""独立运行步骤11"""
|
"""独立运行步骤11(旧版 parent 链上溯方式,保留兼容)。"""
|
||||||
sampling_csv_path = self.sampling_csv_file.get_path()
|
sampling_csv_path = self.sampling_csv_file.get_path()
|
||||||
if not sampling_csv_path:
|
if not sampling_csv_path:
|
||||||
QMessageBox.warning(self, "输入错误", "请选择采样光谱CSV文件!")
|
QMessageBox.warning(self, "输入错误", "请选择采样光谱CSV文件!")
|
||||||
|
|||||||
@ -1244,7 +1244,7 @@ class WaterQualityGUI(QMainWindow):
|
|||||||
if not PIPELINE_AVAILABLE:
|
if not PIPELINE_AVAILABLE:
|
||||||
QMessageBox.critical(
|
QMessageBox.critical(
|
||||||
self, "错误",
|
self, "错误",
|
||||||
"无法导入pipeline模块,请确保water_quality_inversion_pipeline_GUI.py文件存在!"
|
"无法导入 Pipeline 模块,请检查 src/core/handlers/ 目录是否完整!"
|
||||||
)
|
)
|
||||||
return
|
return
|
||||||
|
|
||||||
@ -1400,7 +1400,7 @@ class WaterQualityGUI(QMainWindow):
|
|||||||
if not PIPELINE_AVAILABLE:
|
if not PIPELINE_AVAILABLE:
|
||||||
QMessageBox.critical(
|
QMessageBox.critical(
|
||||||
self, "错误",
|
self, "错误",
|
||||||
"无法导入pipeline模块,请确保water_quality_inversion_pipeline_GUI.py文件存在!"
|
"无法导入 Pipeline 模块,请检查 src/core/handlers/ 目录是否完整!"
|
||||||
)
|
)
|
||||||
return
|
return
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user