refactor; algorithm
parent
e27c7659e0
commit
58361ac6f1
80
README.md
80
README.md
|
|
@ -2,21 +2,11 @@
|
||||||
|
|
||||||
## 环境配置
|
## 环境配置
|
||||||
|
|
||||||
如果你有 `conda`,推荐新建一个虚拟环境:
|
本项目使用 `uv` 进行包管理。
|
||||||
```bash
|
|
||||||
conda create -n EvaDutyArrangeTool python=3.8
|
|
||||||
conda activate EvaDutyArrangeTool
|
|
||||||
```
|
|
||||||
如果不新建虚拟环境也可以,本项目对 `python` 的版本不是很敏感。
|
|
||||||
|
|
||||||
安装项目中所需要的所有包的最新版本。其中 `pyside6` 是一个前端库,`pyinstaller` 用于打包项目,`ortools` 是一个高效的组合优化求解器,`pandas` 用于处理表格数据:
|
安装项目中所需要的所有包的最新版本。其中 `pyside6` 是一个前端库,`pyinstaller` 用于打包项目,`ortools` 是一个高效的组合优化求解器,`pandas` 用于处理表格数据:
|
||||||
```bash
|
```bash
|
||||||
pip install pyside6 pyinstaller ortools pandas
|
uv sync
|
||||||
```
|
|
||||||
|
|
||||||
如果发现以上包的最新版本与代码不兼容,可以尝试使用我开发时使用的版本:
|
|
||||||
```bash
|
|
||||||
pip install pyside6==6.6.3.1 pyinstaller==6.11.1 ortools==9.11.4210 pandas==2.0.3
|
|
||||||
```
|
```
|
||||||
|
|
||||||
## 项目结构
|
## 项目结构
|
||||||
|
|
@ -31,63 +21,81 @@ EVA_duty_arrange_tool/
|
||||||
├─ *.xlsx // 测试用例
|
├─ *.xlsx // 测试用例
|
||||||
```
|
```
|
||||||
|
|
||||||
## 项目运行&打包
|
## 项目运行 & 打包
|
||||||
项目运行方式:
|
项目运行方式:
|
||||||
```bash
|
```bash
|
||||||
python main.py
|
uv run main.py
|
||||||
```
|
```
|
||||||
|
|
||||||
本项目使用 `pyinstaller` 工具进行打包。如果要打包,请确保能够正常运行项目。打包命令如下:
|
本项目使用 `pyinstaller` 工具进行打包。如果要打包,请确保能够正常运行项目。打包命令如下:
|
||||||
```bash
|
```bash
|
||||||
pyinstaller --onefile --windowed --name=EVA_duty_arrange_tool main.py
|
uvx pyinstaller --onefile --windowed --name=EVA_duty_arrange_tool main.py
|
||||||
```
|
```
|
||||||
|
|
||||||
## 数学原理
|
## 数学原理
|
||||||
本项目将值班排班问题建模为了一个[组合优化](https://zh.wikipedia.org/wiki/%E7%BB%84%E5%90%88%E4%BC%98%E5%8C%96)问题。
|
本项目将值班排班问题建模为了一个[组合优化](https://zh.wikipedia.org/wiki/%E7%BB%84%E5%90%88%E4%BC%98%E5%8C%96)问题。
|
||||||
|
|
||||||
在本问题中,优化目标是:
|
在本问题中,优化目标是:
|
||||||
- 让每一班的同学数量尽可能平均
|
- 一个涉及多个方面的量化指标
|
||||||
|
- 每一班同学数量的平均程度
|
||||||
|
- 每一班包含技术部老人的个数
|
||||||
|
- 每一班包含技术部小朋友的个数
|
||||||
|
- 每一班包含人资部小朋友的个数
|
||||||
|
|
||||||
在本问题中,约束是:
|
在本问题中,约束是:
|
||||||
1. 让每位同学每周的班次数符合意愿
|
1. 让每位同学每周的班次数符合意愿
|
||||||
2. 让每位同学在自己想要的时间段值班
|
2. 让每位同学在自己想要的时间段值班
|
||||||
3. 每班次至少(多)包含若干位技术部老人
|
3. 每班次至少(多)包含若干位技术部成员
|
||||||
4. 每班次至少(多)包含若干位人资部小朋友
|
4. 等等...
|
||||||
5. 等等...
|
|
||||||
|
|
||||||
下面我将用数学语言建模以上定义的优化目标和约束。设一共有 $n$ 位同学,$m$ 个值班的班次,定义以下符号:
|
下面我们将用数学语言建模以上定义的优化目标和约束。设一共有 $n$ 位同学,$m$ 个值班的班次,协会共有 $t$ 个部门,定义以下符号:
|
||||||
- 令 $x_{ij} \in \{0,1\}, \quad i=1,2\dots n,\quad j=1,2\dots m$ 表示最终第 $i$ 位同学是否值第 $j$ 班
|
- 令 $x_{ij} \in \{0,1\}, \quad i=1,2\dots n,\quad j=1,2\dots m$ 表示最终第 $i$ 位同学是否值第 $j$ 班
|
||||||
- 令 $M_j = \sum_{i=1}^{n} x_{ij}, \quad j=1,2,\dots m$ 表示第 $j$ 班次实际安排的值班人数。
|
- 令 $M_j = \sum_{i=1}^{n} x_{ij}, \quad j=1,2,\dots m$ 表示第 $j$ 班次实际安排的值班人数。
|
||||||
- 令 $N_i, \quad i=1,2,\dots n$ 表示第 $i$ 个同学每周愿意值班次数
|
- 令 $N_i, \quad i=1,2,\dots n$ 表示第 $i$ 个同学每周愿意值班次数
|
||||||
- 令 $v_{ij} \in \{0,1\}, \quad i=1,2\dots n,\quad j=1,2\dots m$ 表示第 $i$ 位同学是否有空值第 $j$ 班
|
- 令 $v_{ij} \in \{0,1\}, \quad i=1,2\dots n,\quad j=1,2\dots m$ 表示第 $i$ 位同学是否有空值第 $j$ 班
|
||||||
- 令 $tech_{i} \in \{0,1\}, \quad i=1,2\dots n$ 表示第 $i$ 位同学是否是技术部成员
|
- 令 $\text{old}_{i} \in \{0,1\}, \quad i=1,2\dots n$ 表示第 $i$ 位同学是否是老人
|
||||||
- 令 $old_{i} \in \{0,1\}, \quad i=1,2\dots n$ 表示第 $i$ 位同学是否是老人
|
- 令 $d_{ik}\in\{0,1\} \quad i=1,2\dots n,\quad k=1,2\dots t$ 表示第 $i$ 位同学是否属于第 $k$ 个部门,目前顺序为电脑部、电器部、人资部、财外部、文宣部
|
||||||
- 令 $hr_{i} \in \{0,1\}, \quad i=1,2\dots n$ 表示第 $i$ 位同学是否是人资部成员
|
|
||||||
|
|
||||||
以上符号中,只有 $x_{ij}$ 是待求解的变量,其余均为已知量。
|
以上符号中,只有 $x_{ij}$ 是待求解的变量,其余均为已知量。
|
||||||
|
|
||||||
则优化目标为:
|
则优化目标为:
|
||||||
|
|
||||||
$$ \min_{x_{ij}}\left | M_j - \frac{\sum_{i=1}^{n}Ni }{m} \right | $$
|
1. 每一班的值班人数与平均每一班值班人数之差的绝对值尽可能小
|
||||||
|
|
||||||
也即,最小化每一班的值班人数与平均每一班值班人数之差的绝对值。
|
$$ X_1=\sum\left | M_j - \frac{\sum_{i=1}^{n}Ni }{m} \right | $$
|
||||||
> Tips: 在组合优化问题的定义中,只能定义线性的式子,是不允许出现“绝对值”运算的。所以需要引入辅助变量 $a_j \in [0,+\infty )$,并额外引入两组约束:$a_j \ge M_j-\frac{\sum_{i=1}^{n}Ni }{m}$ 和 $a_j \ge \frac{\sum_{i=1}^{n}Ni }{m} - M_j$。此时优化目标变为:$\min_{x_{ij},a_j}\sum_{j=1}^{m}a_j$。
|
|
||||||
|
> 在组合优化问题的定义中,只能定义线性的式子,是不允许出现“绝对值”运算的。所以需要引入辅助变量 $a_j \in [0,+\infty )$,并额外引入两组约束:$a_j \ge M_j-\frac{\sum_{i=1}^{n}Ni }{m}$ 和 $a_j \ge \frac{\sum_{i=1}^{n}Ni }{m} - M_j$。那么在优化的过程中,$a_j$ 就会逐渐趋向 $X_1$。
|
||||||
|
|
||||||
|
2. 每一班技术部老人数量要达到一定值
|
||||||
|
|
||||||
|
$$ X_2=\sum x_{ij}\cdot(d_{i1}+d_{i2})\cdot\text{old}_i $$
|
||||||
|
|
||||||
|
3. 每一班技术部小朋友数量要达到一定值
|
||||||
|
|
||||||
|
$$ X_3=\sum x_{ij}\cdot(d_{i1}+d_{i2})\cdot(1-\text{old}_i) $$
|
||||||
|
|
||||||
|
4. 每一班人资部小朋友数量要达到一定值
|
||||||
|
|
||||||
|
$$ X_4=\sum x_{ij}\cdot d_{i3}\cdot(1-\text{old}_i) $$
|
||||||
|
|
||||||
|
5. 每一班各个部门的人数尽可能平均,这里使用每一班中人数最多部门与人数最少部门的差
|
||||||
|
> 引入辅助变量 $ C_{jk}=\sum_{i=1}^{n}x_{ij}\cdot d_{ik} $
|
||||||
|
$$ X_5=\sum $$
|
||||||
|
|
||||||
接下来定义约束:
|
接下来定义约束:
|
||||||
|
|
||||||
1. 让每位同学每周的班次数符合意愿:
|
1. 让每位同学每周的班次数符合意愿
|
||||||
$$\sum_{j=1}^{m}x_{ij}=N_i, \quad i=1,2\dots n$$
|
$$\sum_{j=1}^{m}x_{ij}=N_i, \quad i=1,2\dots n$$
|
||||||
2. 让每位同学在自己想要的时间段值班
|
2. 让每位同学在自己想要的时间段值班
|
||||||
$$x_{ij} \le v_{ij}, \quad i=1,2\dots n,\quad j=1,2\dots m$$
|
$$x_{ij} \le v_{ij}, \quad i=1,2\dots n,\quad j=1,2\dots m$$
|
||||||
3. 每班次至少包含 $t_{min}$ 位技术部老人,至多包含 $t_{max}$ 位技术部老人
|
3. 每班次至少包含 $t_{min}$ 位技术部成员,至多包含 $t_{max}$ 位技术部成员
|
||||||
$$t_{min} \le \sum_{i=1}^{n}x_{ij}tech_{i}old_{i} \le t_{max}, \quad j=1,2\dots m$$
|
$$t_{min} \le \sum_{i=1}^{n}x_{ij}\cdot tech_{i} \le t_{max}, \quad j=1,2\dots m$$
|
||||||
4. 每班次至少包含 $h_{min}$ 位人资部小朋友,至多包含 $h_{max}$ 位人资部小朋友
|
4. 其他更多的限制也是类似的,这里就略过了。
|
||||||
$$h_{min} \le \sum_{i=1}^{n}x_{ij}hr_{i}(1-old_{i}) \le h_{max}, \quad j=1,2\dots m$$
|
|
||||||
5. 其他更多的限制也是类似的,这里就略过了。
|
|
||||||
|
|
||||||
以上完成了整个排班问题的建模。建模完成后,用任何组合优化求解器都能都求解问题。在本项目中,我使用了 `ortools` 这个谷歌开发的组合优化求解器。`ortools` 支持 `c++`,`python`,`c#`,`java` 等多种语言,也有跨平台支持,个人感觉比较好用。
|
以上完成了整个排班问题的建模。建模完成后,用任何组合优化求解器都能都求解问题。在本项目中,我们使用了 `ortools` 这个谷歌开发的组合优化求解器。`ortools` 支持 `C++`,`Python`,`C#`,`Java` 等多种语言,也有跨平台支持。
|
||||||
|
|
||||||
## 维护指南
|
## 维护指南
|
||||||
- 如果你想更改 excel 的读取、写入相关的功能,应该修改 `utils.py` 中的相关函数。
|
- 如果你想更改 Excel 的读取、写入相关的功能,应该修改 `utils.py` 中的相关函数。
|
||||||
- 如果你想更改软件的前端界面,应该修改 `main.py` 中 `MyWidget` 这个类相关的代码
|
- 如果你想更改软件的前端界面,应该修改 `main.py` 中 `MyWidget` 这个类相关的代码。
|
||||||
- 如果你想更换排班问题的建模方式、更换求解器、增减限制条件,应该修改 `solve.py` 中的相关代码
|
- 如果你想更换排班问题的建模方式、更换求解器、增减限制条件,应该修改 `solve.py` 中的相关代码。
|
||||||
113
main.py
113
main.py
|
|
@ -1,31 +1,37 @@
|
||||||
import sys
|
from sys import exit
|
||||||
from PySide6 import QtCore, QtWidgets, QtGui
|
from PySide6 import QtCore, QtWidgets, QtGui
|
||||||
from PySide6.QtGui import QFont
|
from PySide6.QtGui import QFont
|
||||||
from utils import read_excel, save_to_excel
|
from utils import read_excel, save_to_excel
|
||||||
from solve import solve_program
|
from solve import solve_program
|
||||||
from datetime import datetime
|
from datetime import datetime
|
||||||
import traceback
|
from traceback import format_exc
|
||||||
|
|
||||||
# 约束条件配置
|
# 约束条件配置
|
||||||
CONSTRAINTS_CONFIG = [
|
CONSTRAINTS_CONFIG = [
|
||||||
("每班次人数:", "4", "8"),
|
("每班次人数:", "5", "8"),
|
||||||
("每班次电脑或电器的老人数:", "", "2"),
|
("每班次电脑或电器的老人数:", "", "2"),
|
||||||
("每班次老人数:", "1", ""),
|
("每班次老人数:", "1", ""),
|
||||||
("每班次人资小朋友数:", "1", ""),
|
|
||||||
("每班次小朋友数:", "2", "")
|
("每班次小朋友数:", "2", "")
|
||||||
]
|
]
|
||||||
|
|
||||||
|
# 权重条件配置
|
||||||
|
WEIGHTS_CONFIG = [
|
||||||
|
("目标 1 权重:每班人数平均程度", "1.0"),
|
||||||
|
("目标 2 权重:每班技术部老人", "1.0"),
|
||||||
|
("目标 3 权重:每班技术部小朋友", "1.0"),
|
||||||
|
("目标 4 权重:每班人资部小朋友", "0.5"),
|
||||||
|
("目标 5 权重:每班部门平均程度", "0.5")
|
||||||
|
]
|
||||||
|
|
||||||
# 参数调优顺序和尝试值
|
# 参数调优顺序和尝试值
|
||||||
PARAM_CONFIGS = [
|
PARAM_CONFIGS = [
|
||||||
('num_tech_min', [1, 0]),
|
('num_min', [5, 4, 3, 2, 1]),
|
||||||
('num_tech_max', [1, 2, 3, 4, 5, 6, 7, 8]),
|
|
||||||
('num_old_min', [2, 1, 0]),
|
|
||||||
('num_old_max', [1, 2, 3, 4, 5, 6, 7, 8]),
|
|
||||||
('num_min', [4, 3, 2, 1]),
|
|
||||||
('num_max', [8, 9, 10, 11, 12, 13, 14, 15]),
|
('num_max', [8, 9, 10, 11, 12, 13, 14, 15]),
|
||||||
|
('num_old_min', [2, 1, 0]),
|
||||||
|
('num_old_max', [3, 4, 5, 6, 7, 8]),
|
||||||
('num_new_min', [1, 0]),
|
('num_new_min', [1, 0]),
|
||||||
('num_hr_min', [1, 0]),
|
('num_tech_old_min', [1, 0]),
|
||||||
('num_hr_max', [1, 2, 3, 4, 5, 6, 7, 8])
|
('num_tech_old_max', [1, 2, 3, 4, 5, 6, 7, 8]),
|
||||||
]
|
]
|
||||||
|
|
||||||
class MyWidget(QtWidgets.QWidget):
|
class MyWidget(QtWidgets.QWidget):
|
||||||
|
|
@ -57,13 +63,21 @@ class MyWidget(QtWidgets.QWidget):
|
||||||
self.group_box_1.setLayout(self.openfile_layout)
|
self.group_box_1.setLayout(self.openfile_layout)
|
||||||
self.main_layout.addWidget(self.group_box_1)
|
self.main_layout.addWidget(self.group_box_1)
|
||||||
|
|
||||||
|
|
||||||
# 限制条件部分
|
# 限制条件部分
|
||||||
self.group_box_2 = QtWidgets.QGroupBox("Step 2. 输入限制条件")
|
self.group_box_2 = QtWidgets.QGroupBox("Step 2. 输入限制条件,权重参数均已归一化")
|
||||||
self.group_box_2.setFont(bold_font)
|
self.group_box_2.setFont(bold_font)
|
||||||
self.cond_layout_overall = QtWidgets.QVBoxLayout()
|
self.cond_layout_overall = QtWidgets.QVBoxLayout()
|
||||||
self.group_box_2.setLayout(self.cond_layout_overall)
|
self.group_box_2.setLayout(self.cond_layout_overall)
|
||||||
|
|
||||||
|
# 创建权重输入框
|
||||||
|
self.weight_widgets = {}
|
||||||
|
|
||||||
|
for i, (label_text, value) in enumerate(WEIGHTS_CONFIG, 1):
|
||||||
|
layout, edit = self._create_weight_row(label_text, value, thin_font)
|
||||||
|
self.weight_widgets[f'layout_{i}'] = layout
|
||||||
|
self.weight_widgets[f'edit_{i}'] = edit
|
||||||
|
self.cond_layout_overall.addLayout(layout)
|
||||||
|
|
||||||
# 自动模式开关
|
# 自动模式开关
|
||||||
self._auto_mode = True
|
self._auto_mode = True
|
||||||
self.cond_layout_0 = QtWidgets.QVBoxLayout()
|
self.cond_layout_0 = QtWidgets.QVBoxLayout()
|
||||||
|
|
@ -141,6 +155,23 @@ class MyWidget(QtWidgets.QWidget):
|
||||||
|
|
||||||
return layout, min_edit, max_edit
|
return layout, min_edit, max_edit
|
||||||
|
|
||||||
|
def _create_weight_row(self, label_text, value, font):
|
||||||
|
"""创建一行权重输入框"""
|
||||||
|
layout = QtWidgets.QHBoxLayout()
|
||||||
|
|
||||||
|
label = QtWidgets.QLabel(label_text, self)
|
||||||
|
label.setFont(font)
|
||||||
|
|
||||||
|
edit = QtWidgets.QLineEdit(self)
|
||||||
|
edit.setFont(font)
|
||||||
|
edit.setValidator(QtGui.QDoubleValidator(0.0, 1.0, 2, self))
|
||||||
|
edit.setText(str(value))
|
||||||
|
|
||||||
|
layout.addWidget(label)
|
||||||
|
layout.addWidget(edit)
|
||||||
|
|
||||||
|
return layout, edit
|
||||||
|
|
||||||
def open_file(self):
|
def open_file(self):
|
||||||
file_path, _ = QtWidgets.QFileDialog.getOpenFileName(self, "打开文件", "", "所有文件 (*.*);;文本文件 (*.txt)")
|
file_path, _ = QtWidgets.QFileDialog.getOpenFileName(self, "打开文件", "", "所有文件 (*.*);;文本文件 (*.txt)")
|
||||||
if file_path:
|
if file_path:
|
||||||
|
|
@ -148,15 +179,13 @@ class MyWidget(QtWidgets.QWidget):
|
||||||
self._excel_dir = file_path
|
self._excel_dir = file_path
|
||||||
|
|
||||||
def on_switch_toggled(self, state):
|
def on_switch_toggled(self, state):
|
||||||
"""处理开关状态的变化事件"""
|
|
||||||
enabled = state == 0
|
enabled = state == 0
|
||||||
self._auto_mode = not enabled
|
self._auto_mode = not enabled
|
||||||
|
|
||||||
for i in range(1, 6):
|
for i in range(1, len(CONSTRAINTS_CONFIG) + 1):
|
||||||
self.constraint_widgets[f'min_{i}'].setEnabled(enabled)
|
self.constraint_widgets[f'min_{i}'].setEnabled(enabled)
|
||||||
self.constraint_widgets[f'max_{i}'].setEnabled(enabled)
|
self.constraint_widgets[f'max_{i}'].setEnabled(enabled)
|
||||||
|
|
||||||
|
|
||||||
@QtCore.Slot()
|
@QtCore.Slot()
|
||||||
def magic(self):
|
def magic(self):
|
||||||
try:
|
try:
|
||||||
|
|
@ -169,10 +198,11 @@ class MyWidget(QtWidgets.QWidget):
|
||||||
|
|
||||||
self.text_solve.append("开始排班...")
|
self.text_solve.append("开始排班...")
|
||||||
self.text_solve.append("读取文件中...")
|
self.text_solve.append("读取文件中...")
|
||||||
all_data, index_to_name_dict, preference_mat, want_num_array, is_new_array, is_tech_array, is_hr_array = read_excel(self._excel_dir)
|
all_data, 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))]
|
||||||
stu_num = len(index_to_name_dict)
|
stu_num = len(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))
|
||||||
|
|
@ -184,23 +214,27 @@ class MyWidget(QtWidgets.QWidget):
|
||||||
self.text_solve.append("信息统计:")
|
self.text_solve.append("信息统计:")
|
||||||
self.text_solve.append(f"\t学生总人数:{stu_num}")
|
self.text_solve.append(f"\t学生总人数:{stu_num}")
|
||||||
self.text_solve.append(f"\t班次总数:{class_num}")
|
self.text_solve.append(f"\t班次总数:{class_num}")
|
||||||
self.text_solve.append(f"\t电脑部或电气部的所有老人的意愿班次之和:{tech_sum}")
|
self.text_solve.append(f"\t电脑部或电器部的所有老人的意愿班次之和:{tech_sum}")
|
||||||
self.text_solve.append(f"\t所有人的意愿班次之和:{all_sum}")
|
self.text_solve.append(f"\t所有人的意愿班次之和:{all_sum}")
|
||||||
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 = []
|
vars = []
|
||||||
|
|
||||||
|
def get_weight_value(key):
|
||||||
|
text = self.weight_widgets[key].text()
|
||||||
|
return float(text) if text else 0.0
|
||||||
|
|
||||||
if self._auto_mode:
|
if self._auto_mode:
|
||||||
self.text_solve.append(f"自动调参开始...")
|
self.text_solve.append(f"自动调参开始...")
|
||||||
|
|
||||||
# 自动调参配置
|
# 自动调参配置
|
||||||
auto_params: dict[str, int | None] = {
|
auto_params = {
|
||||||
'num_min': None, 'num_max': None,
|
'num_min': None, 'num_max': None,
|
||||||
'num_tech_min': None, 'num_tech_max': None,
|
'num_tech_old_min': None, 'num_tech_old_max': None,
|
||||||
'num_old_min': None, 'num_old_max': None,
|
'num_old_min': None, 'num_old_max': None,
|
||||||
'num_hr_min': None, 'num_hr_max': None,
|
'num_new_min': None, 'num_new_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)]
|
||||||
}
|
}
|
||||||
|
|
||||||
# 逐步优化参数
|
# 逐步优化参数
|
||||||
|
|
@ -212,10 +246,9 @@ class MyWidget(QtWidgets.QWidget):
|
||||||
test_params[param_name] = try_value
|
test_params[param_name] = try_value
|
||||||
vars = solve_program(
|
vars = solve_program(
|
||||||
preference_mat=preference_mat,
|
preference_mat=preference_mat,
|
||||||
|
depart_mat=depart_mat,
|
||||||
want_num_array=want_num_array,
|
want_num_array=want_num_array,
|
||||||
is_new_array=is_new_array,
|
is_new_array=is_new_array,
|
||||||
is_tech_array=is_tech_array,
|
|
||||||
is_hr_array=is_hr_array,
|
|
||||||
**test_params
|
**test_params
|
||||||
)
|
)
|
||||||
if vars:
|
if vars:
|
||||||
|
|
@ -233,37 +266,35 @@ class MyWidget(QtWidgets.QWidget):
|
||||||
self.text_solve.append("自动计算成功!")
|
self.text_solve.append("自动计算成功!")
|
||||||
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_min']}, {auto_params['num_max']}]")
|
||||||
self.text_solve.append(f"\t每班电脑或电器的老人数:[{auto_params['num_tech_min']}, {auto_params['num_tech_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_old_min']}, {auto_params['num_old_max']}]")
|
||||||
self.text_solve.append(f"\t每班人资部小朋友数:[{auto_params['num_hr_min']}, {auto_params['num_hr_max']}]")
|
|
||||||
self.text_solve.append(f"\t每班小朋友数:[{auto_params['num_new_min']}, inf]")
|
self.text_solve.append(f"\t每班小朋友数:[{auto_params['num_new_min']}, inf]")
|
||||||
|
|
||||||
else:
|
else:
|
||||||
# 读取限制条件
|
# 读取限制条件
|
||||||
def get_value(key):
|
def get_constraint_value(key):
|
||||||
text = self.constraint_widgets[key].text()
|
text = self.constraint_widgets[key].text()
|
||||||
return int(text) if text else None
|
return int(text) if text else None
|
||||||
|
|
||||||
manual_params = {
|
manual_params = {
|
||||||
'num_min': get_value('min_1'),
|
'num_min': get_constraint_value('min_1'),
|
||||||
'num_max': get_value('max_1'),
|
'num_max': get_constraint_value('max_1'),
|
||||||
'num_tech_min': get_value('min_2'),
|
'num_tech_old_min': get_constraint_value('min_2'),
|
||||||
'num_tech_max': get_value('max_2'),
|
'num_tech_old_max': get_constraint_value('max_2'),
|
||||||
'num_old_min': get_value('min_3'),
|
'num_old_min': get_constraint_value('min_3'),
|
||||||
'num_old_max': get_value('max_3'),
|
'num_old_max': get_constraint_value('max_3'),
|
||||||
'num_hr_min': get_value('min_4'),
|
'num_new_min': get_constraint_value('min_4'),
|
||||||
'num_hr_max': get_value('max_4'),
|
'num_new_max': get_constraint_value('max_4'),
|
||||||
'num_new_min': get_value('min_5'),
|
# Ensure weights is either None or a valid list if required by solve_program
|
||||||
'num_new_max': get_value('max_5')
|
'weights': [get_weight_value(f'edit_{i}') for i in range(1, len(WEIGHTS_CONFIG) + 1)]
|
||||||
}
|
}
|
||||||
|
|
||||||
self.text_solve.append("计算最优解中...")
|
self.text_solve.append("计算最优解中...")
|
||||||
vars = solve_program(
|
vars = solve_program(
|
||||||
preference_mat=preference_mat,
|
preference_mat=preference_mat,
|
||||||
want_num_array=want_num_array,
|
want_num_array=want_num_array,
|
||||||
|
depart_mat=depart_mat,
|
||||||
is_new_array=is_new_array,
|
is_new_array=is_new_array,
|
||||||
is_tech_array=is_tech_array,
|
|
||||||
is_hr_array=is_hr_array,
|
|
||||||
**manual_params
|
**manual_params
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
@ -280,7 +311,7 @@ class MyWidget(QtWidgets.QWidget):
|
||||||
self.text_solve.append(f"保存结果成功!保存路径:{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{traceback.format_exc()}")
|
self.text_solve.append(f"Error Details:\n{format_exc()}")
|
||||||
|
|
||||||
if __name__ == "__main__":
|
if __name__ == "__main__":
|
||||||
app = QtWidgets.QApplication([])
|
app = QtWidgets.QApplication([])
|
||||||
|
|
@ -290,4 +321,4 @@ if __name__ == "__main__":
|
||||||
widget.resize(600, 600)
|
widget.resize(600, 600)
|
||||||
widget.show()
|
widget.show()
|
||||||
|
|
||||||
sys.exit(app.exec())
|
exit(app.exec())
|
||||||
|
|
@ -1,12 +1,13 @@
|
||||||
[project]
|
[project]
|
||||||
name = "eva-duty-arrange-tool"
|
name = "eva-duty-arrange-tool"
|
||||||
version = "0.1.0"
|
version = "1.2.0"
|
||||||
description = "Add your description here"
|
description = "EVA 值班排班工具"
|
||||||
readme = "README.md"
|
readme = "README.md"
|
||||||
requires-python = ">=3.12"
|
requires-python = ">=3.12"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"openpyxl>=3.1.5",
|
"openpyxl>=3.1.5",
|
||||||
"ortools>=9.14.6206",
|
"ortools>=9.14.6206",
|
||||||
"pandas>=2.3.3",
|
"pandas>=2.3.3",
|
||||||
|
"pyinstaller>=6.16.0",
|
||||||
"pyside6>=6.10.0",
|
"pyside6>=6.10.0",
|
||||||
]
|
]
|
||||||
|
|
|
||||||
166
solve.py
166
solve.py
|
|
@ -1,23 +1,16 @@
|
||||||
from ortools.linear_solver import pywraplp
|
from ortools.linear_solver import pywraplp
|
||||||
|
|
||||||
def solve_program(preference_mat:list,
|
def solve_program(preference_mat:list, depart_mat:list, want_num_array:list, is_new_array:list,
|
||||||
want_num_array:list,
|
num_min=None, num_max=None,
|
||||||
is_new_array:list,
|
num_tech_old_min=None, num_tech_old_max=None,
|
||||||
is_tech_array:list,
|
num_old_min=None, num_old_max=None,
|
||||||
is_hr_array:list,
|
num_new_min=None, num_new_max=None,
|
||||||
num_min=None,
|
weights=[1.0, 1.0, 1.0, 0.5, 0.5]
|
||||||
num_max=None,
|
|
||||||
num_tech_min=None,
|
|
||||||
num_tech_max=None,
|
|
||||||
num_old_min=None,
|
|
||||||
num_old_max=None,
|
|
||||||
num_hr_min=None,
|
|
||||||
num_hr_max=None,
|
|
||||||
num_new_min=None,
|
|
||||||
num_new_max=None
|
|
||||||
):
|
):
|
||||||
|
|
||||||
is_old_array = [not is_new for is_new in is_new_array]
|
is_old_array = [not is_new for is_new in is_new_array]
|
||||||
|
is_tech_array = [depart_mat[i][0] == 1 or depart_mat[i][1] == 1 for i in range(len(depart_mat))]
|
||||||
|
is_hr_array = [depart_mat[i][2] == 1 for i in range(len(depart_mat))]
|
||||||
|
|
||||||
# 使用 SCIP 求解器求解组合优化问题
|
# 使用 SCIP 求解器求解组合优化问题
|
||||||
solver = pywraplp.Solver.CreateSolver("SCIP")
|
solver = pywraplp.Solver.CreateSolver("SCIP")
|
||||||
|
|
@ -29,13 +22,37 @@ def solve_program(preference_mat:list,
|
||||||
avg_num = sum(want_num_array) / M
|
avg_num = sum(want_num_array) / M
|
||||||
print(f"平均人数:{avg_num}")
|
print(f"平均人数:{avg_num}")
|
||||||
|
|
||||||
# 创建决策变量:variables[i][j] 表示第i个学生是否选择第j个班次
|
# 各个目标的期望值/归一化系数
|
||||||
|
m = [2.5, 3, 4, 1, 8] # [班次人数偏差, 技术部老人, 技术部小朋友, 人资部小朋友, 部门均衡]
|
||||||
|
|
||||||
|
# 创建决策变量:variables[i][j] 表示第 i 个学生是否选择第 j 个班次
|
||||||
variables = [[solver.IntVar(0.0, 1.0, f"choice_{i}_{j}")
|
variables = [[solver.IntVar(0.0, 1.0, f"choice_{i}_{j}")
|
||||||
for j in range(M)] for i in range(N)]
|
for j in range(M)] for i in range(N)]
|
||||||
|
|
||||||
# 创建辅助变量用于优化目标(均衡班次人数)
|
# 创建辅助变量用于优化目标1(均衡班次人数)
|
||||||
aux_vars = [solver.NumVar(0.0, solver.infinity(), f"aux_{j}")
|
aux_vars_x1 = [solver.NumVar(0.0, solver.infinity(), f"aux_x1_{j}")
|
||||||
for j in range(M)]
|
for j in range(M)]
|
||||||
|
|
||||||
|
# 创建辅助变量用于优化目标2(每班次技术部老人数量缺口)
|
||||||
|
aux_vars_x2 = [solver.NumVar(0.0, solver.infinity(), f"aux_x2_{j}")
|
||||||
|
for j in range(M)]
|
||||||
|
|
||||||
|
# 创建辅助变量用于优化目标3(每班次技术部小朋友数量缺口)
|
||||||
|
aux_vars_x3 = [solver.NumVar(0.0, solver.infinity(), f"aux_x3_{j}")
|
||||||
|
for j in range(M)]
|
||||||
|
|
||||||
|
# 创建辅助变量用于优化目标4(每班次人资部小朋友数量缺口)
|
||||||
|
aux_vars_x4 = [solver.NumVar(0.0, solver.infinity(), f"aux_x4_{j}")
|
||||||
|
for j in range(M)]
|
||||||
|
|
||||||
|
# 创建辅助变量用于优化目标5(均衡各部门人数)
|
||||||
|
# 如果提供了部门信息,则创建相应的辅助变量
|
||||||
|
aux_vars_x5 = []
|
||||||
|
num_departments = 5 # 电脑部、电器部、人资部、财外部、文宣部
|
||||||
|
# 为每个班次的每个部门创建辅助变量
|
||||||
|
aux_vars_x5 = [[solver.NumVar(0.0, solver.infinity(), f"aux_x5_{j}_{k}")
|
||||||
|
for k in range(num_departments)] for j in range(M)]
|
||||||
|
|
||||||
print("Number of variables =", solver.NumVariables())
|
print("Number of variables =", solver.NumVariables())
|
||||||
|
|
||||||
# 约束:每个同学意愿一定要满足
|
# 约束:每个同学意愿一定要满足
|
||||||
|
|
@ -43,11 +60,51 @@ def solve_program(preference_mat:list,
|
||||||
for j in range(M):
|
for j in range(M):
|
||||||
solver.Add(variables[i][j] <= preference_mat[i][j])
|
solver.Add(variables[i][j] <= preference_mat[i][j])
|
||||||
|
|
||||||
# 约束:辅助变量用于计算与平均人数的偏差(绝对值)
|
# 约束:辅助变量 X1 用于计算与平均人数的偏差(绝对值)
|
||||||
for j in range(M):
|
for j in range(M):
|
||||||
actual_num = sum(variables[i][j] for i in range(N))
|
actual_num = sum(variables[i][j] for i in range(N))
|
||||||
solver.Add(aux_vars[j] >= actual_num - avg_num)
|
solver.Add(aux_vars_x1[j] >= actual_num - avg_num)
|
||||||
solver.Add(aux_vars[j] >= avg_num - actual_num)
|
solver.Add(aux_vars_x1[j] >= avg_num - actual_num)
|
||||||
|
|
||||||
|
# 约束:辅助变量 X2 用于计算每班次技术部老人数量超出期望值的部分
|
||||||
|
# 辅助变量表示超出期望值(期望每班有m2个技术部老人,超过即计入)
|
||||||
|
# 目标是让尽可能多的班次达到但不超过这个期望值
|
||||||
|
tech_old_target = m[1] # 使用归一化系数作为期望值
|
||||||
|
for j in range(M):
|
||||||
|
tech_old_count = sum(variables[i][j] * is_old_array[i] * is_tech_array[i] for i in range(N))
|
||||||
|
# 辅助变量捕获超出部分和不足部分
|
||||||
|
solver.Add(aux_vars_x2[j] >= tech_old_count - tech_old_target)
|
||||||
|
solver.Add(aux_vars_x2[j] >= tech_old_target - tech_old_count)
|
||||||
|
|
||||||
|
# 约束:辅助变量 X3 用于计算每班次技术部小朋友数量超出期望值的部分
|
||||||
|
tech_new_target = m[2] # 使用归一化系数作为期望值
|
||||||
|
for j in range(M):
|
||||||
|
tech_new_count = sum(variables[i][j] * is_new_array[i] * is_tech_array[i] for i in range(N))
|
||||||
|
solver.Add(aux_vars_x3[j] >= tech_new_count - tech_new_target)
|
||||||
|
solver.Add(aux_vars_x3[j] >= tech_new_target - tech_new_count)
|
||||||
|
|
||||||
|
# 约束:辅助变量 X4 用于计算每班次人资部小朋友数量超出期望值的部分
|
||||||
|
hr_new_target = m[3] # 使用归一化系数作为期望值
|
||||||
|
for j in range(M):
|
||||||
|
hr_new_count = sum(variables[i][j] * is_new_array[i] * is_hr_array[i] for i in range(N))
|
||||||
|
solver.Add(aux_vars_x4[j] >= hr_new_count - hr_new_target)
|
||||||
|
solver.Add(aux_vars_x4[j] >= hr_new_target - hr_new_count)
|
||||||
|
|
||||||
|
# 约束:辅助变量 X5 用于计算各部门人数与平均值的偏差(绝对值)
|
||||||
|
if aux_vars_x5 is not None:
|
||||||
|
for j in range(M):
|
||||||
|
# 计算第 j 班次的总人数
|
||||||
|
shift_total = sum(variables[i][j] for i in range(N))
|
||||||
|
# 计算平均每个部门应有的人数
|
||||||
|
avg_dept = shift_total / num_departments
|
||||||
|
|
||||||
|
# 对每个部门 k
|
||||||
|
for k in range(num_departments):
|
||||||
|
# 计算第 j 班次中第 k 个部门的实际人数
|
||||||
|
dept_count = sum(variables[i][j] * depart_mat[i][k] for i in range(N))
|
||||||
|
# 添加绝对值约束
|
||||||
|
solver.Add(aux_vars_x5[j][k] >= dept_count - avg_dept)
|
||||||
|
solver.Add(aux_vars_x5[j][k] >= avg_dept - dept_count)
|
||||||
|
|
||||||
# 约束:满足每位同学的意愿班次数
|
# 约束:满足每位同学的意愿班次数
|
||||||
for i in range(N):
|
for i in range(N):
|
||||||
|
|
@ -58,7 +115,7 @@ def solve_program(preference_mat:list,
|
||||||
else:
|
else:
|
||||||
solver.Add(total_shifts == want_num_array[i])
|
solver.Add(total_shifts == want_num_array[i])
|
||||||
|
|
||||||
# 添加班次人数约束的辅助函数
|
# 添加约束的辅助函数
|
||||||
def add_shift_constraint(array, min_val, max_val):
|
def add_shift_constraint(array, min_val, max_val):
|
||||||
for j in range(M):
|
for j in range(M):
|
||||||
shift_count = sum(variables[i][j] * array[i] for i in range(N))
|
shift_count = sum(variables[i][j] * array[i] for i in range(N))
|
||||||
|
|
@ -72,19 +129,46 @@ def solve_program(preference_mat:list,
|
||||||
|
|
||||||
# 约束:每班次电脑或电器的老人数量
|
# 约束:每班次电脑或电器的老人数量
|
||||||
tech_old_array = [is_old_array[i] * is_tech_array[i] for i in range(N)]
|
tech_old_array = [is_old_array[i] * is_tech_array[i] for i in range(N)]
|
||||||
add_shift_constraint(tech_old_array, num_tech_min, num_tech_max)
|
add_shift_constraint(tech_old_array, num_tech_old_min, num_tech_old_max)
|
||||||
|
|
||||||
# 约束:每班次老人数量
|
# 约束:每班次老人数量
|
||||||
add_shift_constraint(is_old_array, num_old_min, num_old_max)
|
add_shift_constraint(is_old_array, num_old_min, num_old_max)
|
||||||
|
|
||||||
# 约束:每班次人资部小朋友数量
|
|
||||||
add_shift_constraint(is_hr_array, num_hr_min, num_hr_max)
|
|
||||||
|
|
||||||
# 约束:每班次小朋友数量
|
# 约束:每班次小朋友数量
|
||||||
add_shift_constraint(is_new_array, num_new_min, num_new_max)
|
add_shift_constraint(is_new_array, num_new_min, num_new_max)
|
||||||
|
|
||||||
# 优化目标:最小化各班次人数与平均人数的偏差总和
|
# 构建多目标优化函数
|
||||||
solver.Minimize(sum(aux_vars))
|
m1, m2, m3, m4, m5 = m[0], m[1], m[2], m[3], m[4]
|
||||||
|
|
||||||
|
# X1: 每班次人数与平均人数的偏差
|
||||||
|
x1 = sum(aux_vars_x1)
|
||||||
|
obj_x1 = x1 / (m1 * M) # 归一化处理,防止数值过大
|
||||||
|
|
||||||
|
# X2: 每班次技术部老人数量的缺口总和(优化目标是让每班都达到目标值)
|
||||||
|
x2 = sum(aux_vars_x2)
|
||||||
|
obj_x2 = x2 / (m2 * M) # 归一化处理,缺口越小越好
|
||||||
|
|
||||||
|
# X3: 每班次技术部小朋友数量的缺口总和
|
||||||
|
x3 = sum(aux_vars_x3)
|
||||||
|
obj_x3 = x3 / (m3 * M) # 归一化处理,缺口越小越好
|
||||||
|
|
||||||
|
# X4: 每班次人资部小朋友数量的缺口总和
|
||||||
|
x4 = sum(aux_vars_x4)
|
||||||
|
obj_x4 = x4 / (m4 * M) # 归一化处理,缺口越小越好
|
||||||
|
|
||||||
|
# X5: 每班次各部门人数的平均程度
|
||||||
|
obj_x5 = 0
|
||||||
|
if aux_vars_x5:
|
||||||
|
x5 = sum(sum(aux_vars_x5[j]) for j in range(M))
|
||||||
|
obj_x5 = x5 / (m5 * M) # 归一化处理
|
||||||
|
|
||||||
|
# 线性加权组合所有目标
|
||||||
|
total_objective = (
|
||||||
|
weights[0] * obj_x1 + weights[1] * obj_x2 + weights[2] * obj_x3 + weights[3] * obj_x4 + weights[4] * obj_x5
|
||||||
|
)
|
||||||
|
|
||||||
|
print(f"优化目标权重: X1={weights[0]}, X2={weights[1]}, X3={weights[2]}, X4={weights[3]}, X5={weights[4]}")
|
||||||
|
solver.Minimize(total_objective)
|
||||||
|
|
||||||
# 求解
|
# 求解
|
||||||
status = solver.Solve()
|
status = solver.Solve()
|
||||||
|
|
@ -93,10 +177,32 @@ def solve_program(preference_mat:list,
|
||||||
print("Optimal solution found:")
|
print("Optimal solution found:")
|
||||||
variables_return = [[variables[i][j].solution_value() for j in range(M)]
|
variables_return = [[variables[i][j].solution_value() for j in range(M)]
|
||||||
for i in range(N)]
|
for i in range(N)]
|
||||||
aux_vars_return = [aux_vars[j].solution_value() for j in range(M)]
|
aux_vars_x1_return = sum([aux_vars_x1[j].solution_value() for j in range(M)]) / M
|
||||||
|
|
||||||
print(f"Optimized objective value: {solver.Objective().Value()}")
|
print(f"Optimized objective value: {solver.Objective().Value()}")
|
||||||
print(aux_vars_return)
|
print(f"X1 (每班次人数平均偏差): {aux_vars_x1_return}")
|
||||||
|
|
||||||
|
# 输出各个优化目标的详细信息
|
||||||
|
tech_old_array = [is_old_array[i] * is_tech_array[i] for i in range(N)]
|
||||||
|
tech_new_array = [is_new_array[i] * is_tech_array[i] for i in range(N)]
|
||||||
|
hr_new_array = [is_new_array[i] * is_hr_array[i] for i in range(N)]
|
||||||
|
|
||||||
|
x2_value = sum(sum(variables[i][j].solution_value() * tech_old_array[i] for i in range(N)) for j in range(M)) / M
|
||||||
|
x3_value = sum(sum(variables[i][j].solution_value() * tech_new_array[i] for i in range(N)) for j in range(M)) / M
|
||||||
|
x4_value = sum(sum(variables[i][j].solution_value() * hr_new_array[i] for i in range(N)) for j in range(M)) / M
|
||||||
|
|
||||||
|
aux_vars_x2_return = sum([aux_vars_x2[j].solution_value() for j in range(M)]) / M
|
||||||
|
aux_vars_x3_return = sum([aux_vars_x3[j].solution_value() for j in range(M)]) / M
|
||||||
|
aux_vars_x4_return = sum([aux_vars_x4[j].solution_value() for j in range(M)]) / M
|
||||||
|
|
||||||
|
print(f"X2 (每班平均技术部老人): {x2_value}, 缺口平均: {aux_vars_x2_return}")
|
||||||
|
print(f"X3 (每班平均技术部小朋友): {x3_value}, 缺口平均: {aux_vars_x3_return}")
|
||||||
|
print(f"X4 (每班平均人资部小朋友): {x4_value}, 缺口平均: {aux_vars_x4_return}")
|
||||||
|
|
||||||
|
if aux_vars_x5:
|
||||||
|
x5_value = sum(sum(aux_vars_x5[j][k].solution_value() for k in range(num_departments)) for j in range(M))
|
||||||
|
print(f"X5 (部门人数偏差): {x5_value}")
|
||||||
|
|
||||||
return variables_return
|
return variables_return
|
||||||
else:
|
else:
|
||||||
print("No solution found.")
|
print("No solution found.")
|
||||||
|
|
|
||||||
46
utils.py
46
utils.py
|
|
@ -1,21 +1,15 @@
|
||||||
import pandas as pd
|
import pandas as pd
|
||||||
from datetime import datetime
|
from datetime import datetime
|
||||||
import random
|
from random import choice
|
||||||
|
|
||||||
# 对于表格每一列:
|
# 对于表格每一列:
|
||||||
# i = 1 -> 时间戳
|
col_timestamp = 1 # i = 1 -> 时间戳
|
||||||
# i = 6 -> 姓名
|
col_name = 6 # i = 6 -> 姓名
|
||||||
# i = 7 -> 部门编号
|
col_department = 7 # i = 7 -> 部门编号
|
||||||
# i = 8 -> 老人 / 小朋友
|
col_member = 8 # i = 8 -> 老人 / 小朋友
|
||||||
# i = 9~28 -> 共 20 个班次,每个班次的意愿度
|
col_dutystart = 9 # i = 9 -> 值班开始时间
|
||||||
# i = 29 -> 想要值班的次数
|
col_dutyend = 28 # i = 28 -> 值班结束时间
|
||||||
col_timestamp = 1
|
col_dutyfreq = 29 # i = 29 -> 想要值班的次数
|
||||||
col_name = 6
|
|
||||||
col_department = 7
|
|
||||||
col_member = 8
|
|
||||||
col_dutystart = 9
|
|
||||||
col_dutyend = 28
|
|
||||||
col_dutyfreq = 29
|
|
||||||
|
|
||||||
index_to_departments = { # 部门编号和部门名称的对应关系
|
index_to_departments = { # 部门编号和部门名称的对应关系
|
||||||
1: "电脑部",
|
1: "电脑部",
|
||||||
|
|
@ -36,10 +30,9 @@ def read_excel(file_path):
|
||||||
# 待返回的所有信息。N 为学生人数,M 为班次数
|
# 待返回的所有信息。N 为学生人数,M 为班次数
|
||||||
index_to_name_dict = {} # 长度为 N 的字典,包含数组下标和学生姓名的对应关系
|
index_to_name_dict = {} # 长度为 N 的字典,包含数组下标和学生姓名的对应关系
|
||||||
preference_mat = [] # N x M 的二维数组,表示每位学生对每个班次的满意度
|
preference_mat = [] # N x M 的二维数组,表示每位学生对每个班次的满意度
|
||||||
|
depart_mat = [] # N x D 的二维数组,表示每位学生所属的部门(D 为部门数量)
|
||||||
want_num_array = [] # 长度为 N 的数组,表示每位学生想要值班的次数
|
want_num_array = [] # 长度为 N 的数组,表示每位学生想要值班的次数
|
||||||
is_new_array = [] # 长度为 N 的数组,表示每位学生是否是小朋友
|
is_new_array = [] # 长度为 N 的数组,表示每位学生是否是小朋友
|
||||||
is_tech_array = [] # 长度为 N 的数组,表示每位学生是否是电脑部或电器部成员
|
|
||||||
is_hr_array = [] # 长度为 N 的数组,表示每位学生是否是人资部成员
|
|
||||||
|
|
||||||
'''
|
'''
|
||||||
考虑到原始填表信息中可能有某位同学多次提交的记录,先过滤一下冗余信息
|
考虑到原始填表信息中可能有某位同学多次提交的记录,先过滤一下冗余信息
|
||||||
|
|
@ -52,23 +45,14 @@ def read_excel(file_path):
|
||||||
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()
|
||||||
|
|
||||||
# 构建返回的数据结构
|
|
||||||
index_to_name_dict = {}
|
|
||||||
preference_mat = []
|
|
||||||
want_num_array = []
|
|
||||||
is_new_array = []
|
|
||||||
is_tech_array = []
|
|
||||||
is_hr_array = []
|
|
||||||
|
|
||||||
for index, (name, info_list) in enumerate(all_data.items()):
|
for index, (name, info_list) in enumerate(all_data.items()):
|
||||||
index_to_name_dict[index] = name
|
index_to_name_dict[index] = name
|
||||||
preference_mat.append(info_list[col_dutystart:col_dutyend + 1])
|
preference_mat.append(info_list[col_dutystart:col_dutyend + 1])
|
||||||
|
depart_mat.append([1 if info_list[col_department] == i else 0 for i in range(1, 6)])
|
||||||
want_num_array.append(info_list[col_dutyfreq])
|
want_num_array.append(info_list[col_dutyfreq])
|
||||||
is_new_array.append(index_to_type[info_list[col_member]] == "小朋友")
|
is_new_array.append(index_to_type[info_list[col_member]] == "小朋友")
|
||||||
is_tech_array.append(index_to_departments[info_list[col_department]] in ["电脑部", "电器部"])
|
|
||||||
is_hr_array.append(index_to_departments[info_list[col_department]] == "人资部")
|
|
||||||
|
|
||||||
return all_data, index_to_name_dict, preference_mat, want_num_array, is_new_array, is_tech_array, is_hr_array
|
return all_data, index_to_name_dict, preference_mat, depart_mat, want_num_array, is_new_array
|
||||||
|
|
||||||
def save_to_excel(variables, all_data, index_to_name_dict, file_path):
|
def save_to_excel(variables, all_data, index_to_name_dict, file_path):
|
||||||
|
|
||||||
|
|
@ -108,13 +92,13 @@ def save_to_excel(variables, all_data, index_to_name_dict, file_path):
|
||||||
|
|
||||||
# 选择值班组长(优先级:人资部小朋友 > 非技术部小朋友 > 小朋友 > 所有人)
|
# 选择值班组长(优先级:人资部小朋友 > 非技术部小朋友 > 小朋友 > 所有人)
|
||||||
if hr_new_index:
|
if hr_new_index:
|
||||||
duty_monitor_index = random.choice(hr_new_index)
|
duty_monitor_index = choice(hr_new_index)
|
||||||
elif none_tech_new_index:
|
elif none_tech_new_index:
|
||||||
duty_monitor_index = random.choice(none_tech_new_index)
|
duty_monitor_index = choice(none_tech_new_index)
|
||||||
elif new_index:
|
elif new_index:
|
||||||
duty_monitor_index = random.choice(new_index)
|
duty_monitor_index = choice(new_index)
|
||||||
else:
|
else:
|
||||||
duty_monitor_index = random.choice(all_index)
|
duty_monitor_index = choice(all_index)
|
||||||
on_duty_list[duty_monitor_index]["duty_monitor"] = True
|
on_duty_list[duty_monitor_index]["duty_monitor"] = True
|
||||||
|
|
||||||
on_duty_list.sort(key=lambda x: x["department"])
|
on_duty_list.sort(key=lambda x: x["department"])
|
||||||
|
|
|
||||||
102
uv.lock
102
uv.lock
|
|
@ -11,6 +11,15 @@ wheels = [
|
||||||
{ url = "https://files.pythonhosted.org/packages/8f/aa/ba0014cc4659328dc818a28827be78e6d97312ab0cb98105a770924dc11e/absl_py-2.3.1-py3-none-any.whl", hash = "sha256:eeecf07f0c2a93ace0772c92e596ace6d3d3996c042b2128459aaae2a76de11d", size = 135811, upload-time = "2025-07-03T09:31:42.253Z" },
|
{ url = "https://files.pythonhosted.org/packages/8f/aa/ba0014cc4659328dc818a28827be78e6d97312ab0cb98105a770924dc11e/absl_py-2.3.1-py3-none-any.whl", hash = "sha256:eeecf07f0c2a93ace0772c92e596ace6d3d3996c042b2128459aaae2a76de11d", size = 135811, upload-time = "2025-07-03T09:31:42.253Z" },
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "altgraph"
|
||||||
|
version = "0.17.4"
|
||||||
|
source = { registry = "https://pypi.org/simple" }
|
||||||
|
sdist = { url = "https://files.pythonhosted.org/packages/de/a8/7145824cf0b9e3c28046520480f207df47e927df83aa9555fb47f8505922/altgraph-0.17.4.tar.gz", hash = "sha256:1b5afbb98f6c4dcadb2e2ae6ab9fa994bbb8c1d75f4fa96d340f9437ae454406", size = 48418, upload-time = "2023-09-25T09:04:52.164Z" }
|
||||||
|
wheels = [
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/4d/3f/3bc3f1d83f6e4a7fcb834d3720544ca597590425be5ba9db032b2bf322a2/altgraph-0.17.4-py2.py3-none-any.whl", hash = "sha256:642743b4750de17e655e6711601b077bc6598dbfa3ba5fa2b2a35ce12b508dff", size = 21212, upload-time = "2023-09-25T09:04:50.691Z" },
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "et-xmlfile"
|
name = "et-xmlfile"
|
||||||
version = "2.0.0"
|
version = "2.0.0"
|
||||||
|
|
@ -22,12 +31,13 @@ wheels = [
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "eva-duty-arrange-tool"
|
name = "eva-duty-arrange-tool"
|
||||||
version = "0.1.0"
|
version = "1.2.0"
|
||||||
source = { virtual = "." }
|
source = { virtual = "." }
|
||||||
dependencies = [
|
dependencies = [
|
||||||
{ name = "openpyxl" },
|
{ name = "openpyxl" },
|
||||||
{ name = "ortools" },
|
{ name = "ortools" },
|
||||||
{ name = "pandas" },
|
{ name = "pandas" },
|
||||||
|
{ name = "pyinstaller" },
|
||||||
{ name = "pyside6" },
|
{ name = "pyside6" },
|
||||||
]
|
]
|
||||||
|
|
||||||
|
|
@ -36,6 +46,7 @@ requires-dist = [
|
||||||
{ name = "openpyxl", specifier = ">=3.1.5" },
|
{ name = "openpyxl", specifier = ">=3.1.5" },
|
||||||
{ name = "ortools", specifier = ">=9.14.6206" },
|
{ name = "ortools", specifier = ">=9.14.6206" },
|
||||||
{ name = "pandas", specifier = ">=2.3.3" },
|
{ name = "pandas", specifier = ">=2.3.3" },
|
||||||
|
{ name = "pyinstaller", specifier = ">=6.16.0" },
|
||||||
{ name = "pyside6", specifier = ">=6.10.0" },
|
{ name = "pyside6", specifier = ">=6.10.0" },
|
||||||
]
|
]
|
||||||
|
|
||||||
|
|
@ -48,6 +59,18 @@ wheels = [
|
||||||
{ url = "https://files.pythonhosted.org/packages/63/7b/04ab6afa1ff7eb9ccb09049918c0407b205f5009092c0416147d163e4e2b/immutabledict-4.2.2-py3-none-any.whl", hash = "sha256:97c31d098a2c850e93a958badeef765e4736ed7942ec73e439facd764a3a7217", size = 4736, upload-time = "2025-10-12T13:32:58.326Z" },
|
{ url = "https://files.pythonhosted.org/packages/63/7b/04ab6afa1ff7eb9ccb09049918c0407b205f5009092c0416147d163e4e2b/immutabledict-4.2.2-py3-none-any.whl", hash = "sha256:97c31d098a2c850e93a958badeef765e4736ed7942ec73e439facd764a3a7217", size = 4736, upload-time = "2025-10-12T13:32:58.326Z" },
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "macholib"
|
||||||
|
version = "1.16.3"
|
||||||
|
source = { registry = "https://pypi.org/simple" }
|
||||||
|
dependencies = [
|
||||||
|
{ name = "altgraph" },
|
||||||
|
]
|
||||||
|
sdist = { url = "https://files.pythonhosted.org/packages/95/ee/af1a3842bdd5902ce133bd246eb7ffd4375c38642aeb5dc0ae3a0329dfa2/macholib-1.16.3.tar.gz", hash = "sha256:07ae9e15e8e4cd9a788013d81f5908b3609aa76f9b1421bae9c4d7606ec86a30", size = 59309, upload-time = "2023-09-25T09:10:16.155Z" }
|
||||||
|
wheels = [
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/d1/5d/c059c180c84f7962db0aeae7c3b9303ed1d73d76f2bfbc32bc231c8be314/macholib-1.16.3-py2.py3-none-any.whl", hash = "sha256:0e315d7583d38b8c77e815b1ecbdbf504a8258d8b3e17b61165c6feb60d18f2c", size = 38094, upload-time = "2023-09-25T09:10:14.188Z" },
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "numpy"
|
name = "numpy"
|
||||||
version = "2.3.3"
|
version = "2.3.3"
|
||||||
|
|
@ -156,6 +179,15 @@ wheels = [
|
||||||
{ url = "https://files.pythonhosted.org/packages/91/a5/97a86cdd52c961273981e9ab905a301b7f086593b597d8974c411decf61a/ortools-9.14.6206-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:f044bb277db3ab6a1b958728fe1cf14ca87c3800d67d7b321d876b48269340f6", size = 29519050, upload-time = "2025-06-19T15:54:04.147Z" },
|
{ url = "https://files.pythonhosted.org/packages/91/a5/97a86cdd52c961273981e9ab905a301b7f086593b597d8974c411decf61a/ortools-9.14.6206-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:f044bb277db3ab6a1b958728fe1cf14ca87c3800d67d7b321d876b48269340f6", size = 29519050, upload-time = "2025-06-19T15:54:04.147Z" },
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "packaging"
|
||||||
|
version = "25.0"
|
||||||
|
source = { registry = "https://pypi.org/simple" }
|
||||||
|
sdist = { url = "https://files.pythonhosted.org/packages/a1/d4/1fc4078c65507b51b96ca8f8c3ba19e6a61c8253c72794544580a7b6c24d/packaging-25.0.tar.gz", hash = "sha256:d443872c98d677bf60f6a1f2f8c1cb748e8fe762d2bf9d3148b5599295b0fc4f", size = 165727, upload-time = "2025-04-19T11:48:59.673Z" }
|
||||||
|
wheels = [
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/20/12/38679034af332785aac8774540895e234f4d07f7545804097de4b666afd8/packaging-25.0-py3-none-any.whl", hash = "sha256:29572ef2b1f17581046b3a2227d5c611fb25ec70ca1ba8554b24b0e69331a484", size = 66469, upload-time = "2025-04-19T11:48:57.875Z" },
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "pandas"
|
name = "pandas"
|
||||||
version = "2.3.3"
|
version = "2.3.3"
|
||||||
|
|
@ -203,6 +235,15 @@ wheels = [
|
||||||
{ url = "https://files.pythonhosted.org/packages/70/44/5191d2e4026f86a2a109053e194d3ba7a31a2d10a9c2348368c63ed4e85a/pandas-2.3.3-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:3869faf4bd07b3b66a9f462417d0ca3a9df29a9f6abd5d0d0dbab15dac7abe87", size = 13202175, upload-time = "2025-09-29T23:31:59.173Z" },
|
{ url = "https://files.pythonhosted.org/packages/70/44/5191d2e4026f86a2a109053e194d3ba7a31a2d10a9c2348368c63ed4e85a/pandas-2.3.3-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:3869faf4bd07b3b66a9f462417d0ca3a9df29a9f6abd5d0d0dbab15dac7abe87", size = 13202175, upload-time = "2025-09-29T23:31:59.173Z" },
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "pefile"
|
||||||
|
version = "2023.2.7"
|
||||||
|
source = { registry = "https://pypi.org/simple" }
|
||||||
|
sdist = { url = "https://files.pythonhosted.org/packages/78/c5/3b3c62223f72e2360737fd2a57c30e5b2adecd85e70276879609a7403334/pefile-2023.2.7.tar.gz", hash = "sha256:82e6114004b3d6911c77c3953e3838654b04511b8b66e8583db70c65998017dc", size = 74854, upload-time = "2023-02-07T12:23:55.958Z" }
|
||||||
|
wheels = [
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/55/26/d0ad8b448476d0a1e8d3ea5622dc77b916db84c6aa3cb1e1c0965af948fc/pefile-2023.2.7-py3-none-any.whl", hash = "sha256:da185cd2af68c08a6cd4481f7325ed600a88f6a813bad9dea07ab3ef73d8d8d6", size = 71791, upload-time = "2023-02-07T12:28:36.678Z" },
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "protobuf"
|
name = "protobuf"
|
||||||
version = "6.31.1"
|
version = "6.31.1"
|
||||||
|
|
@ -217,6 +258,47 @@ wheels = [
|
||||||
{ url = "https://files.pythonhosted.org/packages/f7/af/ab3c51ab7507a7325e98ffe691d9495ee3d3aa5f589afad65ec920d39821/protobuf-6.31.1-py3-none-any.whl", hash = "sha256:720a6c7e6b77288b85063569baae8536671b39f15cc22037ec7045658d80489e", size = 168724, upload-time = "2025-05-28T19:25:53.926Z" },
|
{ url = "https://files.pythonhosted.org/packages/f7/af/ab3c51ab7507a7325e98ffe691d9495ee3d3aa5f589afad65ec920d39821/protobuf-6.31.1-py3-none-any.whl", hash = "sha256:720a6c7e6b77288b85063569baae8536671b39f15cc22037ec7045658d80489e", size = 168724, upload-time = "2025-05-28T19:25:53.926Z" },
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "pyinstaller"
|
||||||
|
version = "6.16.0"
|
||||||
|
source = { registry = "https://pypi.org/simple" }
|
||||||
|
dependencies = [
|
||||||
|
{ name = "altgraph" },
|
||||||
|
{ name = "macholib", marker = "sys_platform == 'darwin'" },
|
||||||
|
{ name = "packaging" },
|
||||||
|
{ name = "pefile", marker = "sys_platform == 'win32'" },
|
||||||
|
{ name = "pyinstaller-hooks-contrib" },
|
||||||
|
{ name = "pywin32-ctypes", marker = "sys_platform == 'win32'" },
|
||||||
|
{ name = "setuptools" },
|
||||||
|
]
|
||||||
|
sdist = { url = "https://files.pythonhosted.org/packages/94/94/1f62e95e4a28b64cfbb5b922ef3046f968b47170d37a1e1a029f56ac9cb4/pyinstaller-6.16.0.tar.gz", hash = "sha256:53559fe1e041a234f2b4dcc3288ea8bdd57f7cad8a6644e422c27bb407f3edef", size = 4008473, upload-time = "2025-09-13T20:07:01.733Z" }
|
||||||
|
wheels = [
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/7b/0a/c42ce6e5d3de287f2e9432a074fb209f1fb72a86a72f3903849fdb5e4829/pyinstaller-6.16.0-py3-none-macosx_10_13_universal2.whl", hash = "sha256:7fd1c785219a87ca747c21fa92f561b0d2926a7edc06d0a0fe37f3736e00bd7a", size = 1027899, upload-time = "2025-09-13T20:05:59.2Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/4e/d0/f18fedde32835d5a758f464c75924e2154065625f09d5456c3c303527654/pyinstaller-6.16.0-py3-none-manylinux2014_aarch64.whl", hash = "sha256:b756ddb9007b8141c5476b553351f9d97559b8af5d07f9460869bfae02be26b0", size = 727990, upload-time = "2025-09-13T20:06:03.583Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/7a/db/c8bb47514ce857b24bf9294cf1ff74844b6a489fa0ab4ef6f923288c4e38/pyinstaller-6.16.0-py3-none-manylinux2014_i686.whl", hash = "sha256:0a48f55b85ff60f83169e10050f2759019cf1d06773ad1c4da3a411cd8751058", size = 739238, upload-time = "2025-09-13T20:06:07.69Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/c6/3e/451dc784a8fcca0fe9f9b6b802d58555364a95b60f253613a2c83fc6b023/pyinstaller-6.16.0-py3-none-manylinux2014_ppc64le.whl", hash = "sha256:73ba72e04fcece92e32518bbb1e1fb5ac2892677943dfdff38e01a06e8742851", size = 737142, upload-time = "2025-09-13T20:06:11.732Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/71/37/2f457479ef8fa2821cdb448acee2421dfb19fbe908bf5499d1930c164084/pyinstaller-6.16.0-py3-none-manylinux2014_s390x.whl", hash = "sha256:b1752488248f7899281b17ca3238eefb5410521291371a686a4f5830f29f52b3", size = 734133, upload-time = "2025-09-13T20:06:15.477Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/63/c4/0f7daac4d062a4d1ac2571d8a8b9b5d6812094fcd914d139af591ca5e1ba/pyinstaller-6.16.0-py3-none-manylinux2014_x86_64.whl", hash = "sha256:ba618a61627ee674d6d68e5de084ba17c707b59a4f2a856084b3999bdffbd3f0", size = 733817, upload-time = "2025-09-13T20:06:19.683Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/11/e4/b6127265b42bef883e8873d850becadf748bc5652e5a7029b059328f3c31/pyinstaller-6.16.0-py3-none-musllinux_1_1_aarch64.whl", hash = "sha256:c8b7ef536711617e12fef4673806198872033fa06fa92326ad7fd1d84a9fa454", size = 732912, upload-time = "2025-09-13T20:06:23.46Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/2b/00/c6663107bdf814b2916e71563beabd09f693c47712213bc228994cb2cc65/pyinstaller-6.16.0-py3-none-musllinux_1_1_x86_64.whl", hash = "sha256:d1ebf84d02c51fed19b82a8abb4df536923abd55bb684d694e1356e4ae2a0ce5", size = 732773, upload-time = "2025-09-13T20:06:27.352Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/a3/14/cabe9bc5f60b95d2e70e7d045ab94b0015ff8f6c8b16e2142d3597e30749/pyinstaller-6.16.0-py3-none-win32.whl", hash = "sha256:6d5f8617f3650ff9ef893e2ab4ddbf3c0d23d0c602ef74b5df8fbef4607840c8", size = 1313878, upload-time = "2025-09-13T20:06:33.234Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/aa/99/2005efbc297e7813c1d6f18484aa94a1a81ce87b6a5b497c563681f4c4ea/pyinstaller-6.16.0-py3-none-win_amd64.whl", hash = "sha256:bc10eb1a787f99fea613509f55b902fbd2d8b73ff5f51ff245ea29a481d97d41", size = 1374706, upload-time = "2025-09-13T20:06:39.95Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/ca/f4/4dfcf69b86d60fcaae05a42bbff1616d48a91e71726e5ed795d773dae9b3/pyinstaller-6.16.0-py3-none-win_arm64.whl", hash = "sha256:d0af8a401de792c233c32c44b16d065ca9ab8262ee0c906835c12bdebc992a64", size = 1315923, upload-time = "2025-09-13T20:06:45.846Z" },
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "pyinstaller-hooks-contrib"
|
||||||
|
version = "2025.9"
|
||||||
|
source = { registry = "https://pypi.org/simple" }
|
||||||
|
dependencies = [
|
||||||
|
{ name = "packaging" },
|
||||||
|
{ name = "setuptools" },
|
||||||
|
]
|
||||||
|
sdist = { url = "https://files.pythonhosted.org/packages/7d/83/be0f57c0b77b66c33c2283ebd4ea341022b5a743e97c5fb3bebab82b38b9/pyinstaller_hooks_contrib-2025.9.tar.gz", hash = "sha256:56e972bdaad4e9af767ed47d132362d162112260cbe488c9da7fee01f228a5a6", size = 165189, upload-time = "2025-09-24T11:21:35.113Z" }
|
||||||
|
wheels = [
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/a2/26/23b4cfc77d7f808c69f59070e1e8293a579ec281a547c61562357160b346/pyinstaller_hooks_contrib-2025.9-py3-none-any.whl", hash = "sha256:ccbfaa49399ef6b18486a165810155e5a8d4c59b41f20dc5da81af7482aaf038", size = 444283, upload-time = "2025-09-24T11:21:33.67Z" },
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "pyside6"
|
name = "pyside6"
|
||||||
version = "6.10.0"
|
version = "6.10.0"
|
||||||
|
|
@ -286,6 +368,24 @@ wheels = [
|
||||||
{ url = "https://files.pythonhosted.org/packages/81/c4/34e93fe5f5429d7570ec1fa436f1986fb1f00c3e0f43a589fe2bbcd22c3f/pytz-2025.2-py2.py3-none-any.whl", hash = "sha256:5ddf76296dd8c44c26eb8f4b6f35488f3ccbf6fbbd7adee0b7262d43f0ec2f00", size = 509225, upload-time = "2025-03-25T02:24:58.468Z" },
|
{ url = "https://files.pythonhosted.org/packages/81/c4/34e93fe5f5429d7570ec1fa436f1986fb1f00c3e0f43a589fe2bbcd22c3f/pytz-2025.2-py2.py3-none-any.whl", hash = "sha256:5ddf76296dd8c44c26eb8f4b6f35488f3ccbf6fbbd7adee0b7262d43f0ec2f00", size = 509225, upload-time = "2025-03-25T02:24:58.468Z" },
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "pywin32-ctypes"
|
||||||
|
version = "0.2.3"
|
||||||
|
source = { registry = "https://pypi.org/simple" }
|
||||||
|
sdist = { url = "https://files.pythonhosted.org/packages/85/9f/01a1a99704853cb63f253eea009390c88e7131c67e66a0a02099a8c917cb/pywin32-ctypes-0.2.3.tar.gz", hash = "sha256:d162dc04946d704503b2edc4d55f3dba5c1d539ead017afa00142c38b9885755", size = 29471, upload-time = "2024-08-14T10:15:34.626Z" }
|
||||||
|
wheels = [
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/de/3d/8161f7711c017e01ac9f008dfddd9410dff3674334c233bde66e7ba65bbf/pywin32_ctypes-0.2.3-py3-none-any.whl", hash = "sha256:8a1513379d709975552d202d942d9837758905c8d01eb82b8bcc30918929e7b8", size = 30756, upload-time = "2024-08-14T10:15:33.187Z" },
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "setuptools"
|
||||||
|
version = "80.9.0"
|
||||||
|
source = { registry = "https://pypi.org/simple" }
|
||||||
|
sdist = { url = "https://files.pythonhosted.org/packages/18/5d/3bf57dcd21979b887f014ea83c24ae194cfcd12b9e0fda66b957c69d1fca/setuptools-80.9.0.tar.gz", hash = "sha256:f36b47402ecde768dbfafc46e8e4207b4360c654f1f3bb84475f0a28628fb19c", size = 1319958, upload-time = "2025-05-27T00:56:51.443Z" }
|
||||||
|
wheels = [
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/a3/dc/17031897dae0efacfea57dfd3a82fdd2a2aeb58e0ff71b77b87e44edc772/setuptools-80.9.0-py3-none-any.whl", hash = "sha256:062d34222ad13e0cc312a4c02d73f059e86a4acbfbdea8f8f76b28c99f306922", size = 1201486, upload-time = "2025-05-27T00:56:49.664Z" },
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "shiboken6"
|
name = "shiboken6"
|
||||||
version = "6.10.0"
|
version = "6.10.0"
|
||||||
|
|
|
||||||
18
使用手册.md
18
使用手册.md
|
|
@ -1,11 +1,11 @@
|
||||||
# EVA 值班排班工具使用手册 - v1.1.0
|
# EVA 值班排班工具使用手册 - v1.2.0
|
||||||
|
|
||||||
## 前言
|
## 前言
|
||||||
这是浙江大学学生 E 志者协会“排班工具软件”的使用手册,将简要地介绍该软件的使用方法和注意事项。请注意,这是面向使用者而非开发者的手册,如果想了解该工具的开发流程和所使用的技术,请移步至[此仓库](https://git.zjueva.net/happywind/EVA_duty_arrange_tool)的说明手册。
|
这是浙江大学学生E志者协会“排班工具软件”的使用手册,将简要地介绍该软件的使用方法和注意事项。请注意,这是面向使用者而非开发者的手册,如果想了解该工具的开发流程和所使用的技术,请移步至[此仓库](https://git.zjueva.net/happywind/EVA_duty_arrange_tool)的说明手册。
|
||||||
|
|
||||||
## 软件运行环境&基本情况
|
## 软件运行环境 & 基本情况
|
||||||
- 本软件只能在 windows 系统上运行。
|
- 本软件只能在 Windows 系统上运行。
|
||||||
- 本软件在部分版本的 win10 和 win11 系统上均测试过,能够正常运行。但是不排除可能出现缺少相关 dll 文件而无法运行的情况,如果出现请联系开发者解决。
|
- 本软件在部分版本的 Win10 和 Win11 系统上均测试过,能够正常运行。但是不排除可能出现缺少相关 dll 文件而无法运行的情况,如果出现请联系开发者解决。
|
||||||
- 本软件将排班问题建模为组合优化问题,以“尽可能让各个班次的人数平均”作为优化目标。在指定的限制条件下,若有解,则得到的一定是最优解。
|
- 本软件将排班问题建模为组合优化问题,以“尽可能让各个班次的人数平均”作为优化目标。在指定的限制条件下,若有解,则得到的一定是最优解。
|
||||||
|
|
||||||
## 软件使用方法
|
## 软件使用方法
|
||||||
|
|
@ -18,7 +18,7 @@
|
||||||
|
|
||||||
进行以下三个步骤:
|
进行以下三个步骤:
|
||||||
|
|
||||||
1. 选择待输入的问卷星结果 excel 表格。对于选择的 excel 表格,有以下**格式要求**:
|
1. 选择待输入的问卷星结果 Excel 表格。对于选择的 Excel 表格,有以下**格式要求**:
|
||||||
- 第 1 行,即表头的名称不会影响软件运行,但需要保证每一列的数据的格式和含义正确
|
- 第 1 行,即表头的名称不会影响软件运行,但需要保证每一列的数据的格式和含义正确
|
||||||
- 第 1 列为序号,不会用到,但需要有
|
- 第 1 列为序号,不会用到,但需要有
|
||||||
- 第 2 列为提交答卷时间,格式为 "year/month/day hour:min:sec"
|
- 第 2 列为提交答卷时间,格式为 "year/month/day hour:min:sec"
|
||||||
|
|
@ -47,7 +47,7 @@
|
||||||
- 第 10 ~ 29 列,表示对于每个班次的意愿(1 代表有时间,0 代表没时间)。因为从周一第一班到周日第二班一共有 20 班,所以总共有 20 列。
|
- 第 10 ~ 29 列,表示对于每个班次的意愿(1 代表有时间,0 代表没时间)。因为从周一第一班到周日第二班一共有 20 班,所以总共有 20 列。
|
||||||
- 第 30 列,表示愿意排几班,用一个数字表示。
|
- 第 30 列,表示愿意排几班,用一个数字表示。
|
||||||
|
|
||||||
**我提供了一个名为“问卷星结果_样例输入.xlsx”的文件,以作为正确输入格式的参考。**
|
**我们提供了一个名为“问卷星结果_样例输入.xlsx”的文件,以作为正确输入格式的参考,以及一个名为“问卷星结果_较极端案例.xlsx”的文件,以作为程序运行的测试。**
|
||||||
|
|
||||||
**本软件可以自动处理同一名同学多次填写问卷的情况**,只要保证多次填写中“姓名”保持一致即可,软件将会自动选择最后一次填写的结果作为最终意愿。由于“姓名”是每位同学的唯一标识符,如果出现了某位同学“姓名”填错的情况,需要手动删除该姓名的记录。
|
**本软件可以自动处理同一名同学多次填写问卷的情况**,只要保证多次填写中“姓名”保持一致即可,软件将会自动选择最后一次填写的结果作为最终意愿。由于“姓名”是每位同学的唯一标识符,如果出现了某位同学“姓名”填错的情况,需要手动删除该姓名的记录。
|
||||||
|
|
||||||
|
|
@ -55,12 +55,12 @@
|
||||||
|
|
||||||
2. 输入想要的限制条件。(**若勾选“自动模式”则跳过这一步**)
|
2. 输入想要的限制条件。(**若勾选“自动模式”则跳过这一步**)
|
||||||
|
|
||||||
本软件是在“限制条件必须满足”的条件下,以“每班次人数尽可能平均”作为最终目的来求解的。设置的限制条件太紧的话,最后可能导致无解(比如要求每班有4人,但实际上有一班选的人数就只有3人),这时候就需要手动宽松下限制参数。
|
本软件是在“限制条件必须满足”的条件下,以“每班次人数尽可能平均”作为最终目的来求解的。设置的限制条件太紧的话,最后可能导致无解(比如要求每班有 4 人,但实际上有一班选的人数就只有 3 人),这时候就需要手动宽松下限制参数。
|
||||||
|
|
||||||
本软件提供了自动模式来帮你尝试不同的参数并选择一个局部最优解。在自动模式下,用户无法自主设置参数。
|
本软件提供了自动模式来帮你尝试不同的参数并选择一个局部最优解。在自动模式下,用户无法自主设置参数。
|
||||||
|
|
||||||
3. 开始排班
|
3. 开始排班
|
||||||
|
|
||||||
点击“开始排班!”按钮以开始排班,输出结果会以 excel 表格的形式输出,输出的文件名称是“result_<当前时间戳>.xlsx”
|
点击“开始排班!”按钮以开始排班,输出结果会以 Excel 表格的形式输出,输出的文件名称是`result_<当前时间戳>.xlsx`
|
||||||
|
|
||||||
**在输出结果中,组长会被标黄。**
|
**在输出结果中,组长会被标黄。**
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue