├── .gitignore ├── README.md ├── elevator.png ├── elevator.py ├── elevator_demo.mp4 └── requirements.txt /.gitignore: -------------------------------------------------------------------------------- 1 | .idea/ 2 | elevator_old.py 3 | elevator_simulation_report.docx -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # 电梯调度模拟 2 | ## 简介 3 | 实现了多部电梯的调度算法和可视化图形界面。单部电梯调度采用LOOK算法,多部电梯之间的调度根据启发式的开销函数来选择合适的电梯。另外,可实时计算乘客的平均周转时间,用以指导优化调度算法。 4 | ## 问题分析 5 | ### 单部电梯运行策略 6 | 在仅考虑一部电梯的运行的情况下,电梯应尽可能地无差别地对待每个乘客并尽可能地减少所有乘客地平均等待时间。因此当电梯在朝一个方向运行时,应满足该方向后续楼层的所有请求使用电梯的乘客需求,并满足当前电梯乘客在该方向的所有到达需求。当电梯运行到满足这些需求后的最高/低楼层后,并且在准备更改电梯运行方向前,电梯里所有乘客需求应全部完成,并且外部乘客在该方向后续楼层再无请求。这样保证电梯在一个方向上的一次运行,能尽可能满足更多该方向上乘客的需求,来防止出现某些乘客等待时间过长(饥饿效应)的情况。此外,电梯应在仅和外部乘客请求方向(上/下)相同的楼层停靠,比如电梯在向上运行时,不会在请求使用电梯向下的乘客楼层停靠来接该乘客。 7 | ### 多部电梯调度策略 8 | 当考虑多部电梯的调度时,应选择调度能满足乘客需求“时间开销”最小的电梯。因此,需要实现一个启发式的开销估计:由于在乘客需求实时产生的系统中,真实的时间开销无法精确计算,故用一个可行解来估计,即若楼层在电梯运行方向后续楼层上且请求方向和电梯运行方向相同,则开销为两者楼层之差;若楼层不在电梯运行方向后续楼层上或者请求方向和电梯运行方向相反,则开销为电梯运行到该方向上的最后一个楼层(顶层或底层)后,再折返到该楼层所经过的楼层数。当一个新的乘客请求发生时,同时计算该乘客需求和三个电梯之间的时间开销,并把该乘客分配给预计开销最小的电梯,加入到其请求列表中。 9 | ### 可视化图形界面 10 | wxpython是python里一个可以实现GUI的库,利用该库可以实现本项目的可视化需求。由于电梯调度模拟是一个实时的过程,需要在后台长时间运行,若把电梯调度和GUI更 新放在一起,会导致GUI的不稳定甚至崩溃,因此需要引入多线程的机制,即电梯调度模拟在工作线程中运行,仅在需要更新GUI的时候发送信号量给主线程(GUI线程),来更新图形界面。 11 | ## 程序运行 12 | 所需环境包含在[requirements.txt](requirements.txt)文件中,在anaconda的环境下,命令行输入以下命令安装环境: 13 | `conda install -r requirements.txt` 14 | 命令行输入下面命令即可执行程序: 15 | `python elevator.py` 16 | ## 程序演示 17 | 查看[elevator_demo.mp4](elevator_demo.mp4)获取演示视频。 18 | 19 | -------------------------------------------------------------------------------- /elevator.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Bink98/Elevator_simulation/5c961c11722fe2ff5523574e880b7318f5d425fe/elevator.png -------------------------------------------------------------------------------- /elevator.py: -------------------------------------------------------------------------------- 1 | import numpy as np 2 | import wx 3 | import threading 4 | from pubsub import pub 5 | import time 6 | import matplotlib.pyplot as plt 7 | 8 | max_floor = 20 9 | sum_time = 0 10 | finished_p = 0 11 | time_count = 0 12 | 13 | 14 | class Passenger: 15 | 16 | count = 0 17 | global time_count 18 | 19 | def __init__(self, req_floor=0, arr_floor=0, req_time=0): 20 | Passenger.count += 1 21 | self.id = self.count 22 | self.req_time = req_time 23 | self.ser_time = 0 24 | self.arr_time = 0 25 | self.req_floor = req_floor 26 | self.arr_floor = arr_floor 27 | self.direction = self.set_direction() 28 | 29 | def get_total_time(self): 30 | return self.arr_time - self.req_time 31 | 32 | def get_serve_time(self): 33 | return self.arr_time - self.ser_time 34 | 35 | def set_direction(self): 36 | if self.req_floor > self.arr_floor: 37 | return -1 38 | else: 39 | return 1 40 | 41 | 42 | class Elevator: 43 | def __init__(self): 44 | self.max_floor = max_floor 45 | self.req_que = [] 46 | self.cur_floor = 1.0 47 | # 速度为每时间步上升/下降多少层 48 | self.speed = 0.2 49 | self.wait = 0 50 | self.height_per_floor = 3 51 | self.door = True 52 | self.avail = True 53 | self.tar_floor = None 54 | self.direction = 0 55 | self.carried = [] 56 | 57 | def down(self): 58 | self.cur_floor = round(self.cur_floor - self.speed, 10) 59 | print('电梯下行...') 60 | 61 | def up(self): 62 | self.cur_floor = round(self.cur_floor + self.speed, 10) 63 | print('电梯上行...') 64 | 65 | def arr(self): 66 | print('电梯到达...') 67 | self.open_door() 68 | # 更新tar_floor 69 | self.check_passengers() 70 | if self.set_target_floor(): 71 | if self.set_direction(): 72 | self.check_passengers() 73 | self.set_target_floor() 74 | self.wait += 1 75 | 76 | def open_door(self): 77 | print('开门') 78 | if self.door: 79 | self.door = False 80 | 81 | def close_door(self): 82 | print('关门') 83 | self.door = True 84 | self.wait = 0 85 | 86 | # 检查电梯需求列表,有没有人上,有没有人下 87 | def check_passengers(self): 88 | global sum_time 89 | global finished_p 90 | global time_count 91 | print('检查乘客状态') 92 | print('请求列表:', [p.id for p in self.req_que]) 93 | print('电梯乘客:', [p.id for p in self.carried]) 94 | for p in self.carried[:]: 95 | if p.arr_floor == self.cur_floor: 96 | print('{0}号乘客下电梯'.format(p.id)) 97 | p.arr_time = time_count 98 | sum_time = sum_time + p.arr_time - p.req_time 99 | finished_p += 1 100 | self.carried.remove(p) 101 | for p in self.req_que[:]: 102 | if p.req_floor == self.cur_floor: 103 | if self.direction != 0: 104 | if p.direction == self.direction: 105 | print('{0}号乘客上电梯'.format(p.id)) 106 | self.req_que.remove(p) 107 | self.carried.append(p) 108 | elif not self.carried: 109 | print('{0}号乘客上电梯'.format(p.id)) 110 | self.req_que.remove(p) 111 | self.carried.append(p) 112 | else: 113 | print('{0}号乘客上电梯'.format(p.id)) 114 | self.req_que.remove(p) 115 | self.carried.append(p) 116 | 117 | # 检查需求,设定目标楼层 118 | def set_target_floor(self): 119 | print('寻找目标楼层,当前电梯方向:{}'.format(self.direction)) 120 | self.tar_floor = None 121 | min_req_dis = None 122 | min_arr_dis = None 123 | 124 | # 检查电梯外部请求 125 | req_dis_list = [] 126 | req_que_select = [] 127 | if self.req_que[:]: 128 | for p in self.req_que: 129 | if p.req_floor == self.cur_floor and self.carried: 130 | continue 131 | else: 132 | req_que_select.append(p) 133 | req_dis_list.append(self.alloc_cost(p.req_floor, p.direction)) 134 | min_req_dis = min(req_dis_list) 135 | # 检查电梯内部请求 136 | if self.carried: 137 | arr_dis_list = [self.get_distance(p.arr_floor) for p in self.carried] 138 | min_arr_dis = min(arr_dis_list) 139 | 140 | # 获取最近的需求层 141 | if min_arr_dis is not None and min_req_dis is not None: 142 | if min_arr_dis < min_req_dis: 143 | self.tar_floor = self.carried[arr_dis_list.index(min_arr_dis)].arr_floor 144 | print('目标楼层:{}'.format(self.tar_floor)) 145 | else: 146 | self.tar_floor = req_que_select[req_dis_list.index(min_req_dis)].req_floor 147 | print('目标楼层:{}'.format(self.tar_floor)) 148 | return True 149 | elif min_arr_dis is not None: 150 | self.tar_floor = self.carried[arr_dis_list.index(min_arr_dis)].arr_floor 151 | print('目标楼层:{}'.format(self.tar_floor)) 152 | return True 153 | elif min_req_dis is not None: 154 | self.tar_floor = req_que_select[req_dis_list.index(min_req_dis)].req_floor 155 | print('目标楼层:{}'.format(self.tar_floor)) 156 | return True 157 | else: 158 | print('no target') 159 | return False 160 | 161 | # 计算请求楼层和当前楼层的距离 162 | def get_distance(self, req_floor): 163 | if self.direction == 1: 164 | if self.cur_floor > req_floor: 165 | return self.max_floor - self.cur_floor + self.max_floor - req_floor 166 | else: 167 | return req_floor - self.cur_floor 168 | elif self.direction == -1: 169 | if self.cur_floor > req_floor: 170 | return self.cur_floor - req_floor 171 | else: 172 | return self.cur_floor - 1 + req_floor - 1 173 | else: 174 | return abs(self.cur_floor - req_floor) 175 | 176 | def set_direction(self): 177 | old_direction = self.direction 178 | if self.tar_floor: 179 | if self.tar_floor > self.cur_floor: 180 | self.direction = 1 181 | elif self.tar_floor < self.cur_floor: 182 | self.direction = -1 183 | elif not self.carried: 184 | self.direction = 0 185 | else: 186 | self.direction = 0 187 | if self.direction != old_direction: 188 | print('方向改变:{}'.format(self.direction)) 189 | return True 190 | else: 191 | return False 192 | 193 | # 计算电梯分配成本 194 | def alloc_cost(self, req_floor, direction): 195 | if self.direction == 1: 196 | if self.cur_floor > req_floor or direction == -1: 197 | return self.max_floor - self.cur_floor + self.max_floor - req_floor 198 | else: 199 | return req_floor - self.cur_floor 200 | elif self.direction == -1: 201 | if self.cur_floor < req_floor or direction == 1: 202 | return self.cur_floor - 1 + req_floor - 1 203 | else: 204 | return self.cur_floor - req_floor 205 | else: 206 | return abs(self.cur_floor - req_floor) 207 | 208 | 209 | # 检查电梯执行动作 210 | def check_elevator(elevator): 211 | # 开门 212 | if elevator.wait == 0: 213 | if elevator.tar_floor > elevator.cur_floor: 214 | elevator.up() 215 | elif elevator.tar_floor < elevator.cur_floor: 216 | elevator.down() 217 | else: 218 | elevator.arr() 219 | else: 220 | elevator.wait += 1 221 | if elevator.wait == 4: 222 | elevator.close_door() 223 | 224 | 225 | # 产生乘客需求 226 | def make_request(elevators, clock, pro): 227 | if np.random.rand() < pro: 228 | req_floor = np.random.randint(1, max_floor + 1) 229 | arr_floor = np.random.randint(1, max_floor + 1) 230 | # 当如果请求到达楼层和当前所在楼层相同时,重新产生乘客请求 231 | while arr_floor == req_floor: 232 | arr_floor = np.random.randint(1, max_floor + 1) 233 | p = Passenger(req_floor, arr_floor, clock) 234 | 235 | dis_list = [ele.alloc_cost(p.req_floor, p.direction) for ele in elevators] 236 | print(dis_list) 237 | print('控制台信息:{0}楼用户请求使用{1}号电梯到达{2}楼,ID: {3}'.format(req_floor, dis_list.index(min(dis_list)), arr_floor, p.id)) 238 | elevators[dis_list.index(min(dis_list))].req_que.append(p) 239 | print("-----------------------") 240 | print("{}号电梯重新搜索请求".format(dis_list.index(min(dis_list)))) 241 | elevators[dis_list.index(min(dis_list))].set_target_floor() 242 | elevators[dis_list.index(min(dis_list))].set_direction() 243 | 244 | 245 | def draw_elevator(elevators, ax): 246 | ax.axis('off') 247 | ax.add_patch(plt.Rectangle((0, elevators[0].cur_floor / max_floor - 1.0 / max_floor), 0.1, 0.1, fill=elevators[0].door, edgecolor='blue')) 248 | ax.add_patch(plt.Rectangle((0.1, elevators[0].cur_floor / max_floor - 1.0 / max_floor), 0.1, 0.1, fill=elevators[0].door, edgecolor='blue')) 249 | 250 | ax.add_patch(plt.Rectangle((0.4, elevators[1].cur_floor / max_floor - 1.0 / max_floor), 0.1, 0.1, fill=elevators[1].door, edgecolor='blue')) 251 | ax.add_patch(plt.Rectangle((0.5, elevators[1].cur_floor / max_floor - 1.0 / max_floor), 0.1, 0.1, fill=elevators[1].door, edgecolor='blue')) 252 | 253 | ax.add_patch(plt.Rectangle((0.8, elevators[2].cur_floor / max_floor - 1.0 / max_floor), 0.1, 0.1, fill=elevators[2].door, edgecolor='blue')) 254 | ax.add_patch(plt.Rectangle((0.9, elevators[2].cur_floor / max_floor - 1.0 / max_floor), 0.1, 0.1, fill=elevators[2].door, edgecolor='blue')) 255 | 256 | plt.savefig("elevator.png") 257 | # plt.show() 258 | plt.cla() 259 | 260 | 261 | class WorkThread(threading.Thread): 262 | def __init__(self): 263 | threading.Thread.__init__(self) 264 | self._stop_event = threading.Event() 265 | 266 | def stop(self): 267 | self._stop_event.set() 268 | 269 | def run(self): 270 | # ele = Elevator() 271 | elevators = [Elevator(), Elevator(), Elevator()] 272 | global time_count 273 | _fig, ax = plt.subplots(figsize=(5, 5)) 274 | 275 | for i in range(1000): 276 | 277 | if self._stop_event.isSet(): 278 | break 279 | 280 | time.sleep(0.4) 281 | 282 | # 统计时间 283 | time_count += 0.4 284 | 285 | make_request(elevators, time_count, 0.1) 286 | 287 | for j, ele in enumerate(elevators): 288 | print("@@@@@@@@@@@@@@@@@@@@@@") 289 | print("{}号电梯".format(j + 1)) 290 | print("**********************") 291 | print('当前楼层:{}'.format(ele.cur_floor)) 292 | 293 | if ele.tar_floor is None: 294 | ele.set_target_floor() 295 | ele.set_direction() 296 | if ele.tar_floor is None: 297 | print('电梯空闲') 298 | if ele.wait != 0: 299 | ele.wait += 1 300 | if ele.wait == 4: 301 | ele.close_door() 302 | else: 303 | check_elevator(ele) 304 | else: 305 | check_elevator(ele) 306 | draw_elevator(elevators, ax) 307 | wx.CallAfter(pub.sendMessage, 'change_elevator', elevators=elevators) 308 | 309 | 310 | class ElevatorFrame(wx.Frame): 311 | """ 312 | A Frame that says Hello World 313 | """ 314 | 315 | def __init__(self, *args, **kw): 316 | # ensure the parent's __init__ is called 317 | super(ElevatorFrame, self).__init__(*args, **kw) 318 | 319 | self.thread = None 320 | self.floor = 1 321 | 322 | self.max_floor = 10 323 | self.r_time = 0 324 | 325 | pub.subscribe(self.update_elevator, 'change_elevator') 326 | 327 | _fig, ax = plt.subplots(figsize=(5, 5)) 328 | ax.axis('off') 329 | ax.add_patch(plt.Rectangle((0, 0), 0.1, 0.1)) 330 | ax.add_patch(plt.Rectangle((0.1, 0), 0.1, 0.1)) 331 | 332 | ax.add_patch(plt.Rectangle((0.4, 0), 0.1, 0.1)) 333 | ax.add_patch(plt.Rectangle((0.5, 0), 0.1, 0.1)) 334 | 335 | ax.add_patch(plt.Rectangle((0.8, 0), 0.1, 0.1)) 336 | ax.add_patch(plt.Rectangle((0.9, 0), 0.1, 0.1)) 337 | 338 | plt.savefig("elevator.png") 339 | # plt.show() 340 | plt.cla() 341 | 342 | # create a panel in the frame 343 | self.pnl = wx.Panel(self) 344 | bt_start = wx.Button(self.pnl, label="Start") 345 | self.Bind(wx.EVT_BUTTON, self.onStartButton, bt_start) 346 | bt_stop = wx.Button(self.pnl, label="Stop") 347 | self.Bind(wx.EVT_BUTTON, self.onStopButton, bt_stop) 348 | buttonSizer = wx.BoxSizer(wx.HORIZONTAL) 349 | buttonSizer.Add(bt_start) 350 | buttonSizer.Add(bt_stop) 351 | bodySizer = wx.BoxSizer(wx.HORIZONTAL) 352 | image = wx.Image('elevator.png', wx.BITMAP_TYPE_PNG) 353 | temp = image.ConvertToBitmap() 354 | self.bmp = wx.StaticBitmap(parent=self.pnl, bitmap=temp) 355 | self.split_line = "****************************" 356 | status_label = self.split_line + "\n当前楼层\n" + self.split_line + "\n当前电梯上乘客\n" + self.split_line + "\n当前乘客需求\n" 357 | self.time_text = wx.StaticText(self.pnl, label="当前平均周转时间: 无") 358 | self.status1 = wx.StaticText(self.pnl, label="{}号电梯\n".format(1) + status_label) 359 | self.status2 = wx.StaticText(self.pnl, label="{}号电梯\n".format(2) + status_label) 360 | self.status3 = wx.StaticText(self.pnl, label="{}号电梯\n".format(3) + status_label) 361 | self.status_list = [self.status1, self.status2, self.status3] 362 | font = self.status1.GetFont() 363 | font.PointSize += 2 364 | self.time_text.SetFont(font) 365 | self.status1.SetFont(font) 366 | self.status2.SetFont(font) 367 | self.status3.SetFont(font) 368 | bodySizer.Add(self.bmp) 369 | bodySizer.Add(self.status1, 0, wx.LEFT, 10) 370 | bodySizer.Add(self.status2, 0, wx.LEFT, 10) 371 | bodySizer.Add(self.status3, 0, wx.LEFT, 10) 372 | mainSizer = wx.BoxSizer(wx.VERTICAL) 373 | mainSizer.Add(buttonSizer, 0, wx.ALIGN_CENTER | wx.RIGHT, 100) 374 | mainSizer.Add(self.time_text, 0, wx.ALIGN_CENTER | wx.RIGHT, 100) 375 | mainSizer.Add(bodySizer, 0, wx.RIGHT, 100) 376 | self.pnl.SetSizer(mainSizer) 377 | self.pnl.Fit() 378 | 379 | def update_elevator(self, elevators): 380 | global sum_time 381 | global finished_p 382 | image = wx.Image('elevator.png', wx.BITMAP_TYPE_PNG) 383 | temp = image.ConvertToBitmap() 384 | self.bmp.SetBitmap(temp) 385 | if finished_p > 0: 386 | self.time_text.SetLabel("当前平均周转时间: {}".format(round(sum_time/finished_p, 10))) 387 | for i, elevator in enumerate(elevators): 388 | status_label = self.split_line + "\n当前楼层\n{}\n".format(round(elevator.cur_floor)) 389 | status_label += self.split_line + "\n当前电梯上乘客\n" 390 | if elevator.carried: 391 | for p in elevator.carried: 392 | status_label += "ID:{}, R_floor:{}, A_floor:{}\n".format(p.id, p.req_floor, p.arr_floor) 393 | status_label += self.split_line + "\n当前乘客需求\n" 394 | if elevator.req_que: 395 | for p in elevator.req_que: 396 | status_label += "ID:{}, R_floor:{}, A_floor:{}\n".format(p.id, p.req_floor, p.arr_floor) 397 | self.status_list[i].SetLabel("{}号电梯\n".format(i + 1) + status_label) 398 | 399 | def onStartButton(self, evt): 400 | self.thread = WorkThread() 401 | self.thread.start() 402 | 403 | def onStopButton(self, evt): 404 | self.thread.stop() 405 | 406 | if __name__ == '__main__': 407 | app = wx.App() 408 | frm = ElevatorFrame(None, title='电梯调度模拟') 409 | frm.Fit() 410 | frm.Show() 411 | app.MainLoop() 412 | -------------------------------------------------------------------------------- /elevator_demo.mp4: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Bink98/Elevator_simulation/5c961c11722fe2ff5523574e880b7318f5d425fe/elevator_demo.mp4 -------------------------------------------------------------------------------- /requirements.txt: -------------------------------------------------------------------------------- 1 | # This file may be used to create an environment using: 2 | # $ conda create --name --file 3 | # platform: win-64 4 | blas=1.0=mkl 5 | ca-certificates=2019.11.27=0 6 | certifi=2019.11.28=py36_0 7 | cycler=0.10.0=py36h009560c_0 8 | emoji=0.5.4=pypi_0 9 | freetype=2.9.1=ha9979f8_1 10 | icc_rt=2019.0.0=h0cc432a_1 11 | icu=58.2=ha66f8fd_1 12 | intel-openmp=2019.4=245 13 | jpeg=9b=hb83a4c4_2 14 | kiwisolver=1.1.0=py36ha925a31_0 15 | libpng=1.6.37=h2a8f88b_0 16 | matplotlib=3.1.1=py36hc8f65d3_0 17 | mkl=2019.4=245 18 | mkl-service=2.3.0=py36hb782905_0 19 | mkl_fft=1.0.15=py36h14836fe_0 20 | mkl_random=1.1.0=py36h675688f_0 21 | numpy=1.17.4=py36h4320e6b_0 22 | numpy-base=1.17.4=py36hc3f5095_0 23 | openssl=1.1.1d=he774522_3 24 | pip=19.3.1=py36_0 25 | pyparsing=2.4.6=py_0 26 | pypubsub=4.0.3=pypi_0 27 | pyqt=5.9.2=py36h6538335_2 28 | python=3.6.9=h5500b2f_0 29 | python-dateutil=2.8.1=py_0 30 | pytz=2019.3=py_0 31 | qt=5.9.7=vc14h73c81de_0 32 | setuptools=44.0.0=py36_0 33 | sip=4.19.8=py36h6538335_0 34 | six=1.13.0=py36_0 35 | sqlite=3.30.1=he774522_0 36 | tornado=6.0.3=py36he774522_0 37 | vc=14.1=h0510ff6_4 38 | vs2015_runtime=14.16.27012=hf0eaf9b_1 39 | wheel=0.33.6=py36_0 40 | wincertstore=0.2=py36h7fe50ca_0 41 | wxpython=4.0.4=py36ha925a31_0 42 | zlib=1.2.11=h62dcd97_3 43 | --------------------------------------------------------------------------------