From 353ef9e510bac659314b777aacca0ef0376fc96b Mon Sep 17 00:00:00 2001 From: Dawn1Ocean <1785590531@qq.com> Date: Thu, 16 Oct 2025 13:36:00 +0800 Subject: [PATCH] feat: Multithread support --- main.py | 252 ++++++++++++++++++++++++++++++++++++------------------- utils.py | 4 +- 2 files changed, 167 insertions(+), 89 deletions(-) diff --git a/main.py b/main.py index 5b144bd..dcebe28 100644 --- a/main.py +++ b/main.py @@ -34,9 +34,89 @@ PARAM_CONFIGS = [ ('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): def __init__(self): super().__init__() + + self.solver_thread = None # 用于存储求解线程 self.main_layout = QtWidgets.QVBoxLayout(self) @@ -196,14 +276,18 @@ class MyWidget(QtWidgets.QWidget): self.text_solve.append("请先选择文件!") return + # 禁用按钮,防止重复点击 + self.button_solve.setEnabled(False) + self.button_solve.setText("计算中...") + 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("读取文件成功!") # 计算并打印统计信息 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]) 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]) @@ -219,99 +303,93 @@ class MyWidget(QtWidgets.QWidget): self.text_solve.append(f"\t平均每班人数:{all_sum/20}") self.text_solve.append(f"\t所有班次中最少拥有意愿数:{min_prefer}") - vars = [] - def get_weight_value(key): text = self.weight_widgets[key].text() return float(text) if text else 0.0 - if self._auto_mode: - self.text_solve.append(f"自动调参开始...") + def get_constraint_value(key): + 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 = { - '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': [get_weight_value(f'edit_{i}') for i in range(1, len(WEIGHTS_CONFIG) + 1)] - } - - # 逐步优化参数 - for param_name, try_values in PARAM_CONFIGS: - value = None - vars = None - 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}") + # 创建并启动求解线程 + self.solver_thread = SolverThread( + preference_mat, depart_mat, want_num_array, is_new_array, + self._auto_mode, params, self + ) + + # 连接信号 + 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) + + # 启动线程 + self.solver_thread.start() + except Exception: self.text_solve.append("程序出现严重错误,请联系开发者解决问题!!!") 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__": app = QtWidgets.QApplication([]) diff --git a/utils.py b/utils.py index 391a054..fc4f0d0 100644 --- a/utils.py +++ b/utils.py @@ -40,8 +40,8 @@ def read_excel(file_path): all_data = {} # 储存过滤后的数据 time_format = "%Y/%m/%d %H:%M:%S" for index, row in df.iterrows(): - name = row[col_name] - time_this = datetime.strptime(row[col_timestamp], time_format) + name = row.iloc[col_name] + 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): all_data[name] = row.tolist()