441 lines
16 KiB
Python
441 lines
16 KiB
Python
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_())
|