feat: Multithread support
parent
58361ac6f1
commit
353ef9e510
252
main.py
252
main.py
|
|
@ -34,9 +34,89 @@ PARAM_CONFIGS = [
|
||||||
('num_tech_old_max', [1, 2, 3, 4, 5, 6, 7, 8]),
|
('num_tech_old_max', [1, 2, 3, 4, 5, 6, 7, 8]),
|
||||||
]
|
]
|
||||||
|
|
||||||
|
# 工作线程类,用于在后台执行求解任务
|
||||||
|
class SolverThread(QtCore.QThread):
|
||||||
|
# 定义信号用于线程与主界面通信
|
||||||
|
log_signal = QtCore.Signal(str) # 发送日志消息
|
||||||
|
finished_signal = QtCore.Signal(object, dict) # 发送求解结果 (vars, params)
|
||||||
|
error_signal = QtCore.Signal(str) # 发送错误消息
|
||||||
|
|
||||||
|
def __init__(self, preference_mat, depart_mat, want_num_array, is_new_array,
|
||||||
|
auto_mode, manual_params=None, parent=None):
|
||||||
|
super().__init__(parent)
|
||||||
|
self.preference_mat = preference_mat
|
||||||
|
self.depart_mat = depart_mat
|
||||||
|
self.want_num_array = want_num_array
|
||||||
|
self.is_new_array = is_new_array
|
||||||
|
self.auto_mode = auto_mode
|
||||||
|
self.manual_params = manual_params or {}
|
||||||
|
|
||||||
|
def run(self):
|
||||||
|
try:
|
||||||
|
vars = None
|
||||||
|
final_params = {}
|
||||||
|
|
||||||
|
if self.auto_mode:
|
||||||
|
self.log_signal.emit("自动调参开始...")
|
||||||
|
|
||||||
|
auto_params = {
|
||||||
|
'num_min': None, 'num_max': None,
|
||||||
|
'num_tech_old_min': None, 'num_tech_old_max': None,
|
||||||
|
'num_old_min': None, 'num_old_max': None,
|
||||||
|
'num_new_min': None, 'num_new_max': None,
|
||||||
|
'weights': self.manual_params.get('weights', [1.0, 1.0, 1.0, 0.5, 0.5])
|
||||||
|
}
|
||||||
|
|
||||||
|
# 逐步优化参数
|
||||||
|
for param_name, try_values in PARAM_CONFIGS:
|
||||||
|
value = None
|
||||||
|
vars = None
|
||||||
|
for try_value in try_values:
|
||||||
|
self.log_signal.emit(f"尝试 {param_name} = {try_value}...")
|
||||||
|
test_params = auto_params.copy()
|
||||||
|
test_params[param_name] = try_value
|
||||||
|
vars = solve_program(
|
||||||
|
preference_mat=self.preference_mat,
|
||||||
|
depart_mat=self.depart_mat,
|
||||||
|
want_num_array=self.want_num_array,
|
||||||
|
is_new_array=self.is_new_array,
|
||||||
|
**test_params
|
||||||
|
)
|
||||||
|
if vars:
|
||||||
|
value = try_value
|
||||||
|
self.log_signal.emit(f"✓ {param_name} = {try_value} 成功")
|
||||||
|
break
|
||||||
|
else:
|
||||||
|
self.log_signal.emit(f"✗ {param_name} = {try_value} 失败")
|
||||||
|
if value:
|
||||||
|
auto_params[param_name] = value
|
||||||
|
else:
|
||||||
|
break
|
||||||
|
|
||||||
|
final_params = auto_params
|
||||||
|
|
||||||
|
else:
|
||||||
|
self.log_signal.emit("计算最优解中...")
|
||||||
|
vars = solve_program(
|
||||||
|
preference_mat=self.preference_mat,
|
||||||
|
want_num_array=self.want_num_array,
|
||||||
|
depart_mat=self.depart_mat,
|
||||||
|
is_new_array=self.is_new_array,
|
||||||
|
**self.manual_params
|
||||||
|
)
|
||||||
|
final_params = self.manual_params
|
||||||
|
|
||||||
|
# 发送完成信号
|
||||||
|
self.finished_signal.emit(vars, final_params)
|
||||||
|
|
||||||
|
except Exception as e:
|
||||||
|
self.error_signal.emit(format_exc())
|
||||||
|
|
||||||
class MyWidget(QtWidgets.QWidget):
|
class MyWidget(QtWidgets.QWidget):
|
||||||
def __init__(self):
|
def __init__(self):
|
||||||
super().__init__()
|
super().__init__()
|
||||||
|
|
||||||
|
self.solver_thread = None # 用于存储求解线程
|
||||||
|
|
||||||
self.main_layout = QtWidgets.QVBoxLayout(self)
|
self.main_layout = QtWidgets.QVBoxLayout(self)
|
||||||
|
|
||||||
|
|
@ -196,14 +276,18 @@ class MyWidget(QtWidgets.QWidget):
|
||||||
self.text_solve.append("请先选择文件!")
|
self.text_solve.append("请先选择文件!")
|
||||||
return
|
return
|
||||||
|
|
||||||
|
# 禁用按钮,防止重复点击
|
||||||
|
self.button_solve.setEnabled(False)
|
||||||
|
self.button_solve.setText("计算中...")
|
||||||
|
|
||||||
self.text_solve.append("开始排班...")
|
self.text_solve.append("开始排班...")
|
||||||
self.text_solve.append("读取文件中...")
|
self.text_solve.append("读取文件中...")
|
||||||
all_data, index_to_name_dict, preference_mat, depart_mat, want_num_array, is_new_array = read_excel(self._excel_dir)
|
self.all_data, self.index_to_name_dict, preference_mat, depart_mat, want_num_array, is_new_array = read_excel(self._excel_dir)
|
||||||
self.text_solve.append("读取文件成功!")
|
self.text_solve.append("读取文件成功!")
|
||||||
|
|
||||||
# 计算并打印统计信息
|
# 计算并打印统计信息
|
||||||
is_tech_array = [depart_mat[i][0] == 1 or depart_mat[i][1] == 1 for i in range(len(depart_mat))]
|
is_tech_array = [depart_mat[i][0] == 1 or depart_mat[i][1] == 1 for i in range(len(depart_mat))]
|
||||||
stu_num = len(index_to_name_dict)
|
stu_num = len(self.index_to_name_dict)
|
||||||
class_num = len(preference_mat[0])
|
class_num = len(preference_mat[0])
|
||||||
tech_sum = sum(want_num_array[i] for i in range(len(preference_mat))
|
tech_sum = sum(want_num_array[i] for i in range(len(preference_mat))
|
||||||
if not is_new_array[i] and is_tech_array[i])
|
if not is_new_array[i] and is_tech_array[i])
|
||||||
|
|
@ -219,99 +303,93 @@ class MyWidget(QtWidgets.QWidget):
|
||||||
self.text_solve.append(f"\t平均每班人数:{all_sum/20}")
|
self.text_solve.append(f"\t平均每班人数:{all_sum/20}")
|
||||||
self.text_solve.append(f"\t所有班次中最少拥有意愿数:{min_prefer}")
|
self.text_solve.append(f"\t所有班次中最少拥有意愿数:{min_prefer}")
|
||||||
|
|
||||||
vars = []
|
|
||||||
|
|
||||||
def get_weight_value(key):
|
def get_weight_value(key):
|
||||||
text = self.weight_widgets[key].text()
|
text = self.weight_widgets[key].text()
|
||||||
return float(text) if text else 0.0
|
return float(text) if text else 0.0
|
||||||
|
|
||||||
if self._auto_mode:
|
def get_constraint_value(key):
|
||||||
self.text_solve.append(f"自动调参开始...")
|
text = self.constraint_widgets[key].text()
|
||||||
|
return int(text) if text else None
|
||||||
|
|
||||||
|
# 准备参数
|
||||||
|
params = {
|
||||||
|
'num_min': get_constraint_value('min_1'),
|
||||||
|
'num_max': get_constraint_value('max_1'),
|
||||||
|
'num_tech_old_min': get_constraint_value('min_2'),
|
||||||
|
'num_tech_old_max': get_constraint_value('max_2'),
|
||||||
|
'num_old_min': get_constraint_value('min_3'),
|
||||||
|
'num_old_max': get_constraint_value('max_3'),
|
||||||
|
'num_new_min': get_constraint_value('min_4'),
|
||||||
|
'num_new_max': get_constraint_value('max_4'),
|
||||||
|
'weights': [get_weight_value(f'edit_{i}') for i in range(1, len(WEIGHTS_CONFIG) + 1)]
|
||||||
|
}
|
||||||
|
|
||||||
# 自动调参配置
|
# 创建并启动求解线程
|
||||||
auto_params = {
|
self.solver_thread = SolverThread(
|
||||||
'num_min': None, 'num_max': None,
|
preference_mat, depart_mat, want_num_array, is_new_array,
|
||||||
'num_tech_old_min': None, 'num_tech_old_max': None,
|
self._auto_mode, params, self
|
||||||
'num_old_min': None, 'num_old_max': None,
|
)
|
||||||
'num_new_min': None, 'num_new_max': None,
|
|
||||||
'weights': [get_weight_value(f'edit_{i}') for i in range(1, len(WEIGHTS_CONFIG) + 1)]
|
# 连接信号
|
||||||
}
|
self.solver_thread.log_signal.connect(self.on_log_message)
|
||||||
|
self.solver_thread.finished_signal.connect(self.on_solve_finished)
|
||||||
# 逐步优化参数
|
self.solver_thread.error_signal.connect(self.on_solve_error)
|
||||||
for param_name, try_values in PARAM_CONFIGS:
|
|
||||||
value = None
|
# 启动线程
|
||||||
vars = None
|
self.solver_thread.start()
|
||||||
for try_value in try_values:
|
|
||||||
test_params = auto_params.copy()
|
|
||||||
test_params[param_name] = try_value
|
|
||||||
vars = solve_program(
|
|
||||||
preference_mat=preference_mat,
|
|
||||||
depart_mat=depart_mat,
|
|
||||||
want_num_array=want_num_array,
|
|
||||||
is_new_array=is_new_array,
|
|
||||||
**test_params
|
|
||||||
)
|
|
||||||
if vars:
|
|
||||||
value = try_value
|
|
||||||
break
|
|
||||||
if value:
|
|
||||||
auto_params[param_name] = value
|
|
||||||
else:
|
|
||||||
break
|
|
||||||
|
|
||||||
if vars is None:
|
|
||||||
self.text_solve.append("自动求解失败!请尝试手动调参!")
|
|
||||||
return
|
|
||||||
else:
|
|
||||||
self.text_solve.append("自动计算成功!")
|
|
||||||
self.text_solve.append("参数:")
|
|
||||||
self.text_solve.append(f"\t每班人数:[{auto_params['num_min']}, {auto_params['num_max']}]")
|
|
||||||
self.text_solve.append(f"\t每班电脑或电器的老人数:[{auto_params['num_tech_old_min']}, {auto_params['num_tech_old_max']}]")
|
|
||||||
self.text_solve.append(f"\t每班老人数:[{auto_params['num_old_min']}, {auto_params['num_old_max']}]")
|
|
||||||
self.text_solve.append(f"\t每班小朋友数:[{auto_params['num_new_min']}, inf]")
|
|
||||||
|
|
||||||
else:
|
|
||||||
# 读取限制条件
|
|
||||||
def get_constraint_value(key):
|
|
||||||
text = self.constraint_widgets[key].text()
|
|
||||||
return int(text) if text else None
|
|
||||||
|
|
||||||
manual_params = {
|
|
||||||
'num_min': get_constraint_value('min_1'),
|
|
||||||
'num_max': get_constraint_value('max_1'),
|
|
||||||
'num_tech_old_min': get_constraint_value('min_2'),
|
|
||||||
'num_tech_old_max': get_constraint_value('max_2'),
|
|
||||||
'num_old_min': get_constraint_value('min_3'),
|
|
||||||
'num_old_max': get_constraint_value('max_3'),
|
|
||||||
'num_new_min': get_constraint_value('min_4'),
|
|
||||||
'num_new_max': get_constraint_value('max_4'),
|
|
||||||
# Ensure weights is either None or a valid list if required by solve_program
|
|
||||||
'weights': [get_weight_value(f'edit_{i}') for i in range(1, len(WEIGHTS_CONFIG) + 1)]
|
|
||||||
}
|
|
||||||
|
|
||||||
self.text_solve.append("计算最优解中...")
|
|
||||||
vars = solve_program(
|
|
||||||
preference_mat=preference_mat,
|
|
||||||
want_num_array=want_num_array,
|
|
||||||
depart_mat=depart_mat,
|
|
||||||
is_new_array=is_new_array,
|
|
||||||
**manual_params
|
|
||||||
)
|
|
||||||
|
|
||||||
if vars is None:
|
|
||||||
self.text_solve.append("在目前限制条件下无解!请尝试更改限制条件!")
|
|
||||||
return
|
|
||||||
else:
|
|
||||||
self.text_solve.append("计算最优解成功!")
|
|
||||||
|
|
||||||
# 保存结果到 excel
|
|
||||||
self.text_solve.append("保存结果中...")
|
|
||||||
save_dir = f"result_{datetime.now().strftime('%Y%m%d_%H%M%S')}.xlsx"
|
|
||||||
save_to_excel(vars, all_data, index_to_name_dict, save_dir)
|
|
||||||
self.text_solve.append(f"保存结果成功!保存路径:{save_dir}")
|
|
||||||
except Exception:
|
except Exception:
|
||||||
self.text_solve.append("程序出现严重错误,请联系开发者解决问题!!!")
|
self.text_solve.append("程序出现严重错误,请联系开发者解决问题!!!")
|
||||||
self.text_solve.append(f"Error Details:\n{format_exc()}")
|
self.text_solve.append(f"Error Details:\n{format_exc()}")
|
||||||
|
self.button_solve.setEnabled(True)
|
||||||
|
self.button_solve.setText("开始排班!")
|
||||||
|
|
||||||
|
@QtCore.Slot(str)
|
||||||
|
def on_log_message(self, message):
|
||||||
|
"""接收线程发送的日志消息并实时显示"""
|
||||||
|
self.text_solve.append(message)
|
||||||
|
|
||||||
|
@QtCore.Slot(object, dict)
|
||||||
|
def on_solve_finished(self, vars, params):
|
||||||
|
"""求解完成后的回调"""
|
||||||
|
try:
|
||||||
|
if vars is None:
|
||||||
|
if self._auto_mode:
|
||||||
|
self.text_solve.append("自动求解失败!请尝试手动调参!")
|
||||||
|
else:
|
||||||
|
self.text_solve.append("在目前限制条件下无解!请尝试更改限制条件!")
|
||||||
|
else:
|
||||||
|
if self._auto_mode:
|
||||||
|
self.text_solve.append("自动计算成功!")
|
||||||
|
self.text_solve.append("参数:")
|
||||||
|
self.text_solve.append(f"\t每班人数:[{params['num_min']}, {params['num_max']}]")
|
||||||
|
self.text_solve.append(f"\t每班电脑或电器的老人数:[{params['num_tech_old_min']}, {params['num_tech_old_max']}]")
|
||||||
|
self.text_solve.append(f"\t每班老人数:[{params['num_old_min']}, {params['num_old_max']}]")
|
||||||
|
self.text_solve.append(f"\t每班小朋友数:[{params['num_new_min']}, inf]")
|
||||||
|
else:
|
||||||
|
self.text_solve.append("计算最优解成功!")
|
||||||
|
|
||||||
|
# 保存结果到 excel
|
||||||
|
self.text_solve.append("保存结果中...")
|
||||||
|
save_dir = f"result_{datetime.now().strftime('%Y%m%d_%H%M%S')}.xlsx"
|
||||||
|
save_to_excel(vars, self.all_data, self.index_to_name_dict, save_dir)
|
||||||
|
self.text_solve.append(f"保存结果成功!保存路径:{save_dir}")
|
||||||
|
|
||||||
|
except Exception:
|
||||||
|
self.text_solve.append("保存结果时出现错误!")
|
||||||
|
self.text_solve.append(f"Error Details:\n{format_exc()}")
|
||||||
|
finally:
|
||||||
|
# 恢复按钮状态
|
||||||
|
self.button_solve.setEnabled(True)
|
||||||
|
self.button_solve.setText("开始排班!")
|
||||||
|
|
||||||
|
@QtCore.Slot(str)
|
||||||
|
def on_solve_error(self, error_msg):
|
||||||
|
"""处理求解过程中的错误"""
|
||||||
|
self.text_solve.append("程序出现严重错误,请联系开发者解决问题!!!")
|
||||||
|
self.text_solve.append(f"Error Details:\n{error_msg}")
|
||||||
|
self.button_solve.setEnabled(True)
|
||||||
|
self.button_solve.setText("开始排班!")
|
||||||
|
|
||||||
if __name__ == "__main__":
|
if __name__ == "__main__":
|
||||||
app = QtWidgets.QApplication([])
|
app = QtWidgets.QApplication([])
|
||||||
|
|
|
||||||
4
utils.py
4
utils.py
|
|
@ -40,8 +40,8 @@ def read_excel(file_path):
|
||||||
all_data = {} # 储存过滤后的数据
|
all_data = {} # 储存过滤后的数据
|
||||||
time_format = "%Y/%m/%d %H:%M:%S"
|
time_format = "%Y/%m/%d %H:%M:%S"
|
||||||
for index, row in df.iterrows():
|
for index, row in df.iterrows():
|
||||||
name = row[col_name]
|
name = row.iloc[col_name]
|
||||||
time_this = datetime.strptime(row[col_timestamp], time_format)
|
time_this = datetime.strptime(row.iloc[col_timestamp], time_format)
|
||||||
if name not in all_data or time_this > datetime.strptime(all_data[name][col_timestamp], time_format):
|
if name not in all_data or time_this > datetime.strptime(all_data[name][col_timestamp], time_format):
|
||||||
all_data[name] = row.tolist()
|
all_data[name] = row.tolist()
|
||||||
|
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue