import sys import os import numpy as np import pandas as pd import matplotlib.pyplot as plt from matplotlib.backends.backend_qt5agg import FigureCanvasQTAgg as FigureCanvas from matplotlib.backends.backend_qt5agg import NavigationToolbar2QT as NavigationToolbar from PyQt5.QtWidgets import (QApplication, QMainWindow, QWidget, QVBoxLayout, QHBoxLayout, QGroupBox, QLabel, QDoubleSpinBox, QRadioButton, QPushButton, QButtonGroup, QSplitter, QFrame, QFileDialog, QMessageBox, QCheckBox, QProgressBar, QStatusBar) from PyQt5.QtCore import Qt, QThread, pyqtSignal from PyQt5.QtGui import QFont, QIcon # 添加当前目录到Python路径,以便导入本地模块 current_dir = os.path.dirname(os.path.abspath(__file__)) if current_dir not in sys.path: sys.path.insert(0, current_dir) from modules.load_coefficients import load_coefficients from modules.prospect_d import prospect_d from modules.sail import sail class SimulationWorker(QThread): """Worker thread for running PROSAIL simulation""" finished = pyqtSignal(object) # Signal emitted when simulation is complete error = pyqtSignal(str) # Signal emitted when error occurs def __init__(self, params): super().__init__() self.params = params def run(self): try: # Extract parameters N, Cab, Car, Ant, Cbrown, Cw, Cm = self.params['leaf_params'] LIDFa, LIDFb, TypeLidf, LAI, q = self.params['canopy_params'] tts, tto, psi = self.params['angle_params'] rsoil = self.params['soil_param'] # Load coefficients data_path = os.path.join(current_dir, "data", "dataSpec_PDB.txt") wl, nr, Kab, Kcar, Kant, Kbrown, Kw, Km, Rsoil1, Rsoil2 = load_coefficients(data_path) # Run PROSPECT-D LRT = prospect_d(N, Cab, Car, Ant, Cbrown, Cw, Cm, wl, nr, Kab, Kcar, Kant, Kbrown, Kw, Km) rho = LRT[:, 1] tau = LRT[:, 2] # Run SAIL refl = sail(rho, tau, LIDFa, LIDFb, TypeLidf, LAI, q, tts, tto, psi, rsoil, wl) rdot = refl[:, 1] rsot = refl[:, 2] # Calculate RESV data_all = np.loadtxt(data_path, comments='%', delimiter=None) Es = data_all[:, 8] Ed = data_all[:, 9] rd = np.pi / 180 skyl = 0.847 - 1.61 * np.sin((90 - tts) * rd) + 1.04 * np.sin((90 - tts) * rd) ** 2 PARdiro = (1 - skyl) * Es PARdifo = skyl * Ed denominator = PARdiro + PARdifo denominator[denominator == 0] = 1e-6 resv = (rdot * PARdifo + rsot * PARdiro) / denominator # Create DataFrame df = pd.DataFrame({"wavelength": wl, "resv": resv}) self.finished.emit(df) except Exception as e: self.error.emit(str(e)) class PROSAILSimulator(QMainWindow): def __init__(self): super().__init__() self.results = [] # Store simulation results as (params, df) tuples self.worker = None self.initUI() def initUI(self): self.setWindowTitle("PROSAIL Simulator") self.setGeometry(100, 100, 1200, 800) # Create central widget and main layout central_widget = QWidget() self.setCentralWidget(central_widget) main_layout = QHBoxLayout(central_widget) # Create splitter for resizable panels splitter = QSplitter(Qt.Horizontal) main_layout.addWidget(splitter) # Left panel - Parameters self.create_parameter_panel() splitter.addWidget(self.parameter_panel) # Right panel - Plot self.create_plot_panel() splitter.addWidget(self.plot_panel) # Set splitter proportions splitter.setSizes([400, 800]) # Status bar self.status_bar = self.statusBar() self.status_bar.showMessage("就绪") # Progress bar for simulation self.progress_bar = QProgressBar() self.progress_bar.setVisible(False) self.status_bar.addPermanentWidget(self.progress_bar) def create_parameter_panel(self): self.parameter_panel = QWidget() layout = QVBoxLayout(self.parameter_panel) # Control buttons control_group = QGroupBox("控制选项") control_layout = QHBoxLayout(control_group) self.keep_checkbox = QCheckBox(" 保留") self.simulate_btn = QPushButton(" 模拟") self.simulate_btn.clicked.connect(self.run_simulation) self.clear_btn = QPushButton(" 清空") self.clear_btn.clicked.connect(self.clear_results) control_layout.addWidget(self.keep_checkbox) control_layout.addWidget(self.simulate_btn) control_layout.addWidget(self.clear_btn) layout.addWidget(control_group) # Parameters section params_scroll = QWidget() params_layout = QVBoxLayout(params_scroll) # Canopy parameters self.create_canopy_params(params_layout) # Angle parameters self.create_angle_params(params_layout) # Leaf parameters self.create_leaf_params(params_layout) # Soil parameters self.create_soil_params(params_layout) layout.addWidget(params_scroll) # Save button self.save_btn = QPushButton(" 保存为 CSV") self.save_btn.clicked.connect(self.save_results) self.save_btn.setEnabled(False) layout.addWidget(self.save_btn) def create_canopy_params(self, parent_layout): canopy_group = QGroupBox("1. 冠层参数") layout = QVBoxLayout(canopy_group) # LIDF Type lidf_layout = QHBoxLayout() lidf_layout.addWidget(QLabel("LIDF 类型:")) self.lidf_group = QButtonGroup() self.lidf_1 = QRadioButton("双参数分布") self.lidf_2 = QRadioButton("Campbell 分布") self.lidf_1.setChecked(True) self.lidf_group.addButton(self.lidf_1, 1) self.lidf_group.addButton(self.lidf_2, 2) lidf_layout.addWidget(self.lidf_1) lidf_layout.addWidget(self.lidf_2) layout.addLayout(lidf_layout) # Parameters params_layout = QHBoxLayout() left_col = QVBoxLayout() self.lai_input = self.create_spinbox("LAI (叶面积指数)", 3.0, 0.0, 8.0, 0.1) left_col.addWidget(self.lai_input) self.lidfb_input = self.create_spinbox("LIDFb", 0.0, -1.0, 1.0, 0.05) left_col.addWidget(self.lidfb_input) right_col = QVBoxLayout() self.lidfa_input = self.create_spinbox("LIDFa", 0.0, -1.0, 1.0, 0.05) right_col.addWidget(self.lidfa_input) self.q_input = self.create_spinbox("hotspot q", 0.05, 0.0, 1.0, 0.01) right_col.addWidget(self.q_input) params_layout.addLayout(left_col) params_layout.addLayout(right_col) layout.addLayout(params_layout) # Connect LIDF type change self.lidf_group.buttonClicked.connect(self.on_lidf_changed) parent_layout.addWidget(canopy_group) def create_angle_params(self, parent_layout): angle_group = QGroupBox("2. 观测与光照角度") layout = QHBoxLayout(angle_group) left_col = QVBoxLayout() self.tts_input = self.create_spinbox("tts (太阳天顶角)", 30.0, 0.0, 90.0, 1.0) left_col.addWidget(self.tts_input) self.psi_input = self.create_spinbox("psi (方位角差)", 0.0, 0.0, 180.0, 5.0) left_col.addWidget(self.psi_input) right_col = QVBoxLayout() self.tto_input = self.create_spinbox("tto (观测天顶角)", 20.0, 0.0, 90.0, 1.0) right_col.addWidget(self.tto_input) layout.addLayout(left_col) layout.addLayout(right_col) parent_layout.addWidget(angle_group) def create_leaf_params(self, parent_layout): leaf_group = QGroupBox("3. 叶片参数") layout = QHBoxLayout(leaf_group) left_col = QVBoxLayout() self.n_input = self.create_spinbox("N (叶结构参数)", 1.5, 1.0, 3.0, 0.1) left_col.addWidget(self.n_input) self.car_input = self.create_spinbox("Car (类胡萝卜素)", 8.0, 0.0, 30.0, 1.0) left_col.addWidget(self.car_input) self.cm_input = self.create_spinbox("Cm (干物质含量)", 0.009, 0.0, 0.05, 0.001) left_col.addWidget(self.cm_input) self.ant_input = self.create_spinbox("Ant (花青素)", 0.0, 0.0, 5.0, 0.1) left_col.addWidget(self.ant_input) right_col = QVBoxLayout() self.cab_input = self.create_spinbox("Cab (叶绿素含量)", 40.0, 0.0, 100.0, 1.0) right_col.addWidget(self.cab_input) self.cw_input = self.create_spinbox("Cw (叶片含水量)", 0.015, 0.0, 0.05, 0.001) right_col.addWidget(self.cw_input) self.cbrown_input = self.create_spinbox("Cbrown (棕色素)", 0.0, 0.0, 1.0, 0.05) right_col.addWidget(self.cbrown_input) layout.addLayout(left_col) layout.addLayout(right_col) parent_layout.addWidget(leaf_group) def create_soil_params(self, parent_layout): soil_group = QGroupBox("4. 土壤湿度") layout = QVBoxLayout(soil_group) self.rsoil_input = self.create_spinbox("rsoil (土壤反射率)", 0.2, 0.0, 1.0, 0.01) layout.addWidget(self.rsoil_input) parent_layout.addWidget(soil_group) def create_spinbox(self, label, default, min_val, max_val, step): widget = QWidget() layout = QVBoxLayout(widget) layout.addWidget(QLabel(label)) spinbox = QDoubleSpinBox() spinbox.setRange(min_val, max_val) spinbox.setValue(default) spinbox.setSingleStep(step) spinbox.setDecimals(3) layout.addWidget(spinbox) return widget def create_plot_panel(self): self.plot_panel = QWidget() layout = QVBoxLayout(self.plot_panel) # Create matplotlib figure self.figure = plt.Figure(figsize=(10, 6)) self.canvas = FigureCanvas(self.figure) self.toolbar = NavigationToolbar(self.canvas, self) layout.addWidget(self.toolbar) layout.addWidget(self.canvas) # Initial empty plot self.update_plot() def on_lidf_changed(self): lidf_type = self.lidf_group.checkedId() if lidf_type == 1: # 双参数分布 self.lidfa_input.findChild(QDoubleSpinBox).setRange(-1.0, 1.0) self.lidfa_input.findChild(QDoubleSpinBox).setValue(0.0) else: # Campbell 分布 self.lidfa_input.findChild(QDoubleSpinBox).setRange(0.0, 90.0) self.lidfa_input.findChild(QDoubleSpinBox).setValue(30.0) def get_params(self): leaf_params = ( self.n_input.findChild(QDoubleSpinBox).value(), self.cab_input.findChild(QDoubleSpinBox).value(), self.car_input.findChild(QDoubleSpinBox).value(), self.ant_input.findChild(QDoubleSpinBox).value(), self.cbrown_input.findChild(QDoubleSpinBox).value(), self.cw_input.findChild(QDoubleSpinBox).value(), self.cm_input.findChild(QDoubleSpinBox).value() ) canopy_params = ( self.lidfa_input.findChild(QDoubleSpinBox).value(), self.lidfb_input.findChild(QDoubleSpinBox).value(), self.lidf_group.checkedId(), self.lai_input.findChild(QDoubleSpinBox).value(), self.q_input.findChild(QDoubleSpinBox).value() ) angle_params = ( self.tts_input.findChild(QDoubleSpinBox).value(), self.tto_input.findChild(QDoubleSpinBox).value(), self.psi_input.findChild(QDoubleSpinBox).value() ) soil_param = self.rsoil_input.findChild(QDoubleSpinBox).value() return { 'leaf_params': leaf_params, 'canopy_params': canopy_params, 'angle_params': angle_params, 'soil_param': soil_param } def run_simulation(self): if self.worker and self.worker.isRunning(): return self.simulate_btn.setEnabled(False) self.progress_bar.setVisible(True) self.progress_bar.setRange(0, 0) # Indeterminate progress self.status_bar.showMessage("正在运行 PROSAIL 模拟...") params = self.get_params() self.worker = SimulationWorker(params) self.worker.finished.connect(self.on_simulation_finished) self.worker.error.connect(self.on_simulation_error) self.worker.start() def on_simulation_finished(self, df): self.progress_bar.setVisible(False) self.simulate_btn.setEnabled(True) self.status_bar.showMessage("模拟完成") # Get current parameters at simulation time current_params = self.get_params() if self.keep_checkbox.isChecked(): self.results.append((current_params, df)) else: self.results = [(current_params, df)] self.save_btn.setEnabled(True) self.update_plot() def on_simulation_error(self, error_msg): self.progress_bar.setVisible(False) self.simulate_btn.setEnabled(True) self.status_bar.showMessage("模拟失败") QMessageBox.critical(self, "错误", f"模拟过程中出现错误:\n{error_msg}") def clear_results(self): self.results = [] self.save_btn.setEnabled(False) self.update_plot() self.status_bar.showMessage("结果已清空") def update_plot(self): self.figure.clear() ax = self.figure.add_subplot(111) if self.results: for i, (params, df_plot) in enumerate(self.results): ax.plot(df_plot["wavelength"], df_plot["resv"], label=f"refl #{i+1}") ax.set_ylabel("Reflectance") ax.legend() else: ax.text(0.5, 0.5, "无模拟结果", fontsize=16, ha='center', va='center', alpha=0.5, transform=ax.transAxes) ax.set_xlabel("Wavelength (nm)") ax.grid(True) self.canvas.draw() def save_results(self): if not self.results: return filename, _ = QFileDialog.getSaveFileName( self, "保存结果", "prosail_resv.csv", "CSV 文件 (*.csv);;所有文件 (*)" ) if filename: try: # Prepare data for CSV - each row represents one sample all_data = [] for i, (params, df) in enumerate(self.results): # Create one row per sample with parameters + reflectance spectrum data_row = { 'sample_id': i + 1, 'N': params['leaf_params'][0], 'Cab': params['leaf_params'][1], 'Car': params['leaf_params'][2], 'Ant': params['leaf_params'][3], 'Cbrown': params['leaf_params'][4], 'Cw': params['leaf_params'][5], 'Cm': params['leaf_params'][6], 'LIDFa': params['canopy_params'][0], 'LIDFb': params['canopy_params'][1], 'TypeLidf': params['canopy_params'][2], 'LAI': params['canopy_params'][3], 'q': params['canopy_params'][4], 'tts': params['angle_params'][0], 'tto': params['angle_params'][1], 'psi': params['angle_params'][2], 'rsoil': params['soil_param'] } # Add reflectance values for each wavelength for _, row in df.iterrows(): wavelength = int(row['wavelength']) data_row[f'refl_{wavelength}'] = row['resv'] all_data.append(data_row) # Create DataFrame and save result_df = pd.DataFrame(all_data) result_df.to_csv(filename, index=False) self.status_bar.showMessage(f"结果已保存到 {filename} (共 {len(all_data)} 个样本)") except Exception as e: QMessageBox.critical(self, "错误", f"保存文件时出现错误:\n{str(e)}") if __name__ == "__main__": app = QApplication(sys.argv) app.setStyle('Fusion') # Modern style window = PROSAILSimulator() window.show() sys.exit(app.exec_())