├── in ├── .gitkeep ├── twoexit.txt ├── singleexit.txt ├── twoexitbottleneck.txt ├── exitbecomesblocked.txt ├── standardhallway.txt ├── gym1Exit.txt ├── gym2Exit.txt ├── gym3Exit.txt └── gym1ExitClustered.txt ├── requirements.txt ├── LICENSE.txt ├── bottleneck.py ├── .gitignore ├── floorparse.py ├── person.py ├── viz.py ├── README.md └── evacuate.py /in/.gitkeep: -------------------------------------------------------------------------------- 1 | 2 | -------------------------------------------------------------------------------- /requirements.txt: -------------------------------------------------------------------------------- 1 | randomgen>=1.16.6 2 | simulus>=1.2.1 3 | PySimpleGUI>=4.8.0 4 | matplotlib 5 | -------------------------------------------------------------------------------- /in/twoexit.txt: -------------------------------------------------------------------------------- 1 | S; S; S; S; S; S; S; S 2 | S; W; W; W; W; W; W; S 3 | S; W; N; N; N; P; W; S 4 | S; N; N; F; N; P; W; S 5 | S; W; N; F; N; P; W; S 6 | S; N; N; N; N; P; W; S 7 | S; W; W; W; W; W; W; S 8 | S; S; S; S; S; S; S; S 9 | 10 | -------------------------------------------------------------------------------- /in/singleexit.txt: -------------------------------------------------------------------------------- 1 | S; S; S; S; S; S; S; S 2 | S; W; W; W; W; W; W; S 3 | S; W; N; N; N; P; W; S 4 | S; N; N; F; N; P; W; S 5 | S; W; N; F; N; P; W; S 6 | S; W; N; N; N; P; W; S 7 | S; W; W; W; W; W; W; S 8 | S; S; S; S; S; S; S; S 9 | 10 | -------------------------------------------------------------------------------- /in/twoexitbottleneck.txt: -------------------------------------------------------------------------------- 1 | S; S; S; S; S; S; S; S; S; S 2 | S; W; W; W; W; W; W; W; W; S 3 | S; W; F; P; F; P; P; F; W; S 4 | S; W; P; P; N; P; P; N; W; S 5 | S; W; N; W; W; W; W; N; W; S 6 | S; W; N; N; N; F; N; N; W; S 7 | S; W; W; N; N; N; N; W; W; S 8 | S; B; N; N; N; N; N; N; B; S 9 | S; W; W; W; W; W; W; W; W; S 10 | S; S; S; S; S; S; S; S; S; S 11 | 12 | -------------------------------------------------------------------------------- /in/exitbecomesblocked.txt: -------------------------------------------------------------------------------- 1 | S; S; S; S; S; S; S; S; S; S 2 | S; W; N; W; W; W; W; N; W; S 3 | S; W; N; F; N; N; N; N; W; S 4 | S; W; N; N; N; N; N; N; W; S 5 | S; W; N; F; N; N; N; N; W; S 6 | S; W; N; W; W; W; W; N; W; S 7 | S; W; N; N; N; W; N; N; W; S 8 | S; W; P; N; N; W; N; W; W; S 9 | S; W; N; N; N; W; N; N; W; S 10 | S; W; N; P; N; W; W; N; W; S 11 | S; W; P; N; N; W; N; N; W; S 12 | S; W; N; N; N; N; N; W; W; S 13 | S; W; N; P; N; N; N; W; W; S 14 | S; W; N; N; N; N; N; W; W; S 15 | S; W; P; N; N; N; N; N; W; S 16 | S; W; N; N; N; N; N; N; W; S 17 | S; W; N; N; N; N; N; N; W; S 18 | S; W; P; P; P; P; P; N; W; S 19 | S; W; W; W; W; W; W; W; W; S 20 | S; S; S; S; S; S; S; S; S; S 21 | 22 | -------------------------------------------------------------------------------- /LICENSE.txt: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2020 Aalok Sathe, Matthew Johnson, Nick Biffis 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /in/standardhallway.txt: -------------------------------------------------------------------------------- 1 | S; S; S; S; S; S; S; S; S; S; S; S; S; S; S 2 | S; W; W; W; W; W; N; N; N; W; W; W; W; W; S 3 | S; W; N; N; N; W; N; N; N; B; N; N; N; W; S 4 | S; W; N; P; N; W; N; N; N; W; N; N; N; W; S 5 | S; W; N; N; N; B; N; N; N; W; N; P; N; W; S 6 | S; W; W; W; W; W; N; N; N; W; N; N; N; W; S 7 | S; W; N; N; N; W; N; N; N; W; W; W; W; W; S 8 | S; W; F; N; N; B; N; N; N; B; N; N; N; W; S 9 | S; W; F; F; N; W; N; N; N; W; N; N; P; W; S 10 | S; W; W; W; W; W; N; N; N; W; W; W; W; W; S 11 | S; W; N; N; N; W; N; N; N; W; N; N; N; W; S 12 | S; W; N; P; N; W; N; N; N; B; N; N; P; W; S 13 | S; W; N; N; N; B; N; N; N; W; N; N; N; W; S 14 | S; W; W; W; W; W; N; N; N; W; W; W; W; W; S 15 | S; S; S; S; S; S; S; S; S; S; S; S; S; S; S 16 | 17 | -------------------------------------------------------------------------------- /bottleneck.py: -------------------------------------------------------------------------------- 1 | ''' 2 | This file accompanies other files in the evacuation simulation project. 3 | people: Nick B., Matthew J., Aalok S. 4 | 5 | In this file we define a useful class to model bottlenecks, 'Bottleneck' 6 | ''' 7 | 8 | from collections import deque 9 | 10 | # bottleneck object, represents an area where people must queue to leave, given 11 | # a set rate at which they can pass through, one by one 12 | class Bottleneck(): 13 | loc = None, None 14 | queue = None 15 | numInQueue = 0 16 | 17 | # takes a person, and inserts them into the queue of the bottleneck 18 | def enterBottleNeck(self, person, throughput=1): 19 | self.queue.append(person) 20 | self.numInQueue = self.numInQueue + throughput 21 | 22 | # removes a person from the queue 23 | def exitBottleNeck(self, throughput=1): 24 | if(len(self.queue) > 0): 25 | personLeaving = self.queue.pop() 26 | self.numInQueue = self.numInQueue - throughput 27 | return personLeaving 28 | else: 29 | return None 30 | 31 | def __init__(self, loc): 32 | ''' 33 | constructor method 34 | --- 35 | loc (tuple xy): location (coordinates) of this bottleneck 36 | ''' 37 | self.loc = loc # coordinates of the bottleneck 38 | self.queue = deque() # queue to represents the bottleneck 39 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | floorplan/ 2 | 3 | # Byte-compiled / optimized / DLL files 4 | __pycache__/ 5 | *.py[cod] 6 | *$py.class 7 | 8 | # C extensions 9 | *.so 10 | 11 | # Distribution / packaging 12 | .Python 13 | build/ 14 | develop-eggs/ 15 | dist/ 16 | downloads/ 17 | eggs/ 18 | .eggs/ 19 | lib/ 20 | lib64/ 21 | parts/ 22 | sdist/ 23 | var/ 24 | wheels/ 25 | *.egg-info/ 26 | .installed.cfg 27 | *.egg 28 | MANIFEST 29 | 30 | # PyInstaller 31 | # Usually these files are written by a python script from a template 32 | # before PyInstaller builds the exe, so as to inject date/other infos into it. 33 | *.manifest 34 | *.spec 35 | 36 | # Installer logs 37 | pip-log.txt 38 | pip-delete-this-directory.txt 39 | 40 | # Unit test / coverage reports 41 | htmlcov/ 42 | .tox/ 43 | .coverage 44 | .coverage.* 45 | .cache 46 | nosetests.xml 47 | coverage.xml 48 | *.cover 49 | .hypothesis/ 50 | .pytest_cache/ 51 | 52 | # Translations 53 | *.mo 54 | *.pot 55 | 56 | # Django stuff: 57 | *.log 58 | local_settings.py 59 | db.sqlite3 60 | 61 | # Flask stuff: 62 | instance/ 63 | .webassets-cache 64 | 65 | # Scrapy stuff: 66 | .scrapy 67 | 68 | # Sphinx documentation 69 | docs/_build/ 70 | 71 | # PyBuilder 72 | target/ 73 | 74 | # Jupyter Notebook 75 | .ipynb_checkpoints 76 | 77 | # pyenv 78 | .python-version 79 | 80 | # celery beat schedule file 81 | celerybeat-schedule 82 | 83 | # SageMath parsed files 84 | *.sage.py 85 | 86 | # Environments 87 | .env 88 | .venv 89 | env/ 90 | venv/ 91 | ENV/ 92 | env.bak/ 93 | venv.bak/ 94 | 95 | # Spyder project settings 96 | .spyderproject 97 | .spyproject 98 | 99 | # Rope project settings 100 | .ropeproject 101 | 102 | # mkdocs documentation 103 | /site 104 | 105 | # mypy 106 | .mypy_cache/ 107 | -------------------------------------------------------------------------------- /floorparse.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python3 2 | 3 | from collections import defaultdict 4 | 5 | class FloorParser: 6 | 7 | def __init__(self): 8 | pass 9 | 10 | def parse(self, floor): 11 | ''' 12 | parses a txt floor 13 | ''' 14 | grid = [] 15 | 16 | for row in floor.split('\n'): 17 | if not row: continue 18 | sqs = row.split(';') 19 | rowattrs = [set(sq.strip().split(',')) for sq in sqs] 20 | print(' '.join([list(row)[0] for row in rowattrs])) 21 | grid += [rowattrs] 22 | 23 | 24 | graph = defaultdict(lambda: {'nbrs': set()}) 25 | 26 | R, C = len(grid), len(grid[0]) 27 | 28 | for i in range(R): 29 | for j in range(C): 30 | attrs = grid[i][j] 31 | graph[(i,j)].update({att:int(att in attrs) for att in 'WSBFNP'}) 32 | 33 | for off in {-1, 1}: 34 | if 0 <= i+off < R: 35 | graph[(i,j)]['nbrs'].add((i+off, j)) 36 | 37 | if 0 <= j+off < C: 38 | graph[(i,j)]['nbrs'].add((i, j+off)) 39 | 40 | self.graph = dict(graph.items()) 41 | return self.graph 42 | 43 | 44 | def tostr(self, graph): 45 | ''' 46 | ''' 47 | r, c = 0, 0 48 | for loc, attrs in graph.items(): 49 | r = max(r, loc[0]) 50 | c = max(c, loc[1]) 51 | r, c = r+1, c+1 52 | 53 | s = '' 54 | for r_ in range(r): 55 | for c_ in range(c): 56 | sq = graph[(r_, c_)] 57 | # this = 58 | att = ','.join([a for a in sq if a in 'BNSFWP' and sq[a]]) 59 | s += '{:>4}'.format(att) 60 | s += '\n' 61 | 62 | return s 63 | -------------------------------------------------------------------------------- /in/gym1Exit.txt: -------------------------------------------------------------------------------- 1 | S; S; S; S; S; S; S; S; S; S; S; S; S; S; S; S; S; S; S; S 2 | S; W; W; W; W; W; W; W; W; W; W; W; W; W; W; W; B; B; W; S 3 | S; W; N; N; N; N; N; N; N; N; N; N; N; N; W; N; N; N; W; S 4 | S; W; N; P; N; N; P; N; N; N; N; P; N; N; W; N; P; W; W; S 5 | S; W; N; N; N; N; N; N; N; N; N; N; N; N; W; W; P; N; W; S 6 | S; W; N; N; N; N; N; N; N; N; N; N; N; N; W; N; N; N; W; S 7 | S; W; W; W; W; W; W; W; B; B; W; W; W; W; W; N; N; W; W; S 8 | S; W; N; N; W; W; N; N; N; N; N; B; N; N; N; P; N; W; W; S 9 | S; W; N; N; P; N; N; N; W; W; W; W; W; W; W; B; W; W; W; S 10 | S; W; N; N; N; N; N; N; W; N; F; F; F; N; W; N; N; W; W; S 11 | S; W; W; N; W; W; N; N; W; W; W; W; W; W; W; N; P; N; W; S 12 | S; W; W; N; W; W; P; N; W,N; N; N; N; N; N; W; N; N; N; W; S 13 | S; W; W; N; B; N; N; N; W; N; N; N; N; N; W; N; N; N; W; S 14 | S; W; N; N; W; W; N; N; W; W; W; W; W; W; W; B; W; W; W; S 15 | S; W; W; N; W; W; N; N; N; N; N; W; W; N; N; N; W; W; W; S 16 | S; W; W; P; N; B; P; N; P; N; N; B; N; P; N; N; W; W; W; S 17 | S; W; N; W; W; W; N; N; W; W; W; N; N; W; W; N; N; N; W; S 18 | S; W; N; W; W; W; N; N; W; W; W; N; W; W; W; N; N; N; W; S 19 | S; W; W; W; W; W; W; W; W; W; W; W; W; W; W; W; W; W; W; S 20 | S; S; S; S; S; S; S; S; S; S; S; S; S; S; S; S; S; S; S; S 21 | 22 | -------------------------------------------------------------------------------- /in/gym2Exit.txt: -------------------------------------------------------------------------------- 1 | S; S; S; S; S; S; S; S; S; S; S; S; S; S; S; S; S; S; S; S 2 | S; W; W; W; W; W; W; W; W; W; W; W; W; W; W; W; B; B; W; S 3 | S; W; N; N; N; N; N; N; N; N; N; N; N; N; W; N; N; N; W; S 4 | S; W; N; P; N; N; P; N; N; N; N; P; N; N; W; N; P; W; W; S 5 | S; W; N; N; N; N; N; N; N; N; N; N; N; N; W; W; P; N; W; S 6 | S; W; N; N; N; N; N; N; N; N; N; N; N; N; W; N; N; N; W; S 7 | S; W; W; W; W; W; W; W; B; B; W; W; W; W; W; N; N; W; W; S 8 | S; B; N; N; W; W; N; N; N; N; N; B; N; N; N; P; N; W; W; S 9 | S; W; N; N; P; N; N; N; W; W; W; W; W; W; W; B; W; W; W; S 10 | S; W; N; N; N; N; N; N; W; N; F; F; F; N; W; N; N; W; W; S 11 | S; W; W; N; W; W; N; N; W; W; W; W; W; W; W; N; P; N; W; S 12 | S; W; W; N; W; W; P; N; W,N; N; N; N; N; N; W; N; N; N; W; S 13 | S; W; W; N; B; N; N; N; W; N; N; N; N; N; W; N; N; N; W; S 14 | S; W; N; N; W; W; N; N; W; W; W; W; W; W; W; B; W; W; W; S 15 | S; W; W; N; W; W; N; N; N; N; N; W; W; N; N; N; W; W; W; S 16 | S; W; W; P; N; B; P; N; P; N; N; B; N; P; N; N; W; W; W; S 17 | S; W; N; W; W; W; N; N; W; W; W; N; N; W; W; N; N; N; W; S 18 | S; W; N; W; W; W; N; N; W; W; W; N; W; W; W; N; N; N; W; S 19 | S; W; W; W; W; W; W; W; W; W; W; W; W; W; W; W; W; W; W; S 20 | S; S; S; S; S; S; S; S; S; S; S; S; S; S; S; S; S; S; S; S 21 | 22 | -------------------------------------------------------------------------------- /in/gym3Exit.txt: -------------------------------------------------------------------------------- 1 | S; S; S; S; S; S; S; S; S; S; S; S; S; S; S; S; S; S; S; S 2 | S; W; W; W; W; W; W; W; W; W; W; W; W; W; W; W; B; B; W; S 3 | S; W; N; N; N; N; N; N; N; N; N; N; N; N; W; N; N; N; W; S 4 | S; W; N; P; N; N; P; N; N; N; N; P; N; N; W; N; P; W; W; S 5 | S; W; N; N; N; N; N; N; N; N; N; N; N; N; W; W; P; N; W; S 6 | S; W; N; N; N; N; N; N; N; N; N; N; N; N; W; N; N; N; W; S 7 | S; W; W; W; W; W; W; W; B; B; W; W; W; W; W; N; N; W; W; S 8 | S; B; N; N; W; W; N; N; N; N; N; B; N; N; N; P; N; W; W; S 9 | S; W; N; N; P; N; N; N; W; W; W; W; W; W; W; B; W; W; W; S 10 | S; W; N; N; N; N; N; N; W; N; F; F; F; N; W; N; N; W; W; S 11 | S; W; W; N; W; W; N; N; W; W; W; W; W; W; W; N; P; N; W; S 12 | S; W; W; N; W; W; P; N; W,N; N; N; N; N; N; W; N; N; N; W; S 13 | S; W; W; N; B; N; N; N; W; N; N; N; N; N; W; N; N; N; W; S 14 | S; W; N; N; W; W; N; N; W; W; W; W; W; W; W; B; W; W; W; S 15 | S; W; W; N; W; W; N; N; N; N; N; W; W; N; N; N; W; W; W; S 16 | S; W; W; P; N; B; P; N; P; N; N; B; N; P; N; N; W; W; W; S 17 | S; B; N; N; W; W; N; N; W; W; W; N; N; W; W; N; N; N; W; S 18 | S; W; N; W; W; W; N; N; W; W; W; N; W; W; W; N; N; N; W; S 19 | S; W; W; W; W; W; W; W; W; W; W; W; W; W; W; W; W; W; W; S 20 | S; S; S; S; S; S; S; S; S; S; S; S; S; S; S; S; S; S; S; S 21 | 22 | -------------------------------------------------------------------------------- /in/gym1ExitClustered.txt: -------------------------------------------------------------------------------- 1 | S; S; S; S; S; S; S; S; S; S; S; S; S; S; S; S; S; S; S; S 2 | S; W; W; W; W; W; W; W; W; W; W; W; W; W; W; W; B; B; W; S 3 | S; W; N; N; N; N; N; N; N; N; N; N; N; N; W; N; N; N; W; S 4 | S; W; N; P; P; P; P; P; P; N; N; N; N; N; W; N; N; W; W; S 5 | S; W; N; N; N; N; N; N; N; N; N; N; N; N; W; W; N; N; W; S 6 | S; W; N; N; N; N; N; N; N; N; N; N; N; N; W; N; N; N; W; S 7 | S; W; W; W; W; W; W; W; B; B; W; W; W; W; W; N; N; W; W; S 8 | S; W; N; N; W; W; N; N; N; N; N; B; N; N; N; N; N; W; W; S 9 | S; W; N; N; N; N; N; N; W; W; W; W; W; W; W; B; W; W; W; S 10 | S; W; N; N; N; N; N; N; W; N; F; F; F; N; W; N; N; W; W; S 11 | S; W; W; N; W; W; N; N; W; W; W; W; W; W; W; N; N; N; W; S 12 | S; W; W; N; W; W; N; N; W,N; N; N; N; N; N; W; N; N; N; W; S 13 | S; W; W; N; B; N; N; N; W; N; N; N; N; N; W; N; N; N; W; S 14 | S; W; N; N; W; W; N; N; W; W; W; W; W; W; W; B; W; W; W; S 15 | S; W; W; N; W; W; N; N; N; N; N; W; W; N; N; N; W; W; W; S 16 | S; W; W; N; N; B; N; N; N; N; N; B; N; N; N; N; W; W; W; S 17 | S; W; N; N; W; W; N; N; W; W; W; N; N; W; W; N; N; N; W; S 18 | S; W; N; W; W; W; N; N; W; W; W; N; W; W; W; N; N; N; W; S 19 | S; W; W; W; W; W; W; W; W; W; W; W; W; W; W; W; W; W; W; S 20 | S; S; S; S; S; S; S; S; S; S; S; S; S; S; S; S; S; S; S; S 21 | 22 | -------------------------------------------------------------------------------- /person.py: -------------------------------------------------------------------------------- 1 | ''' 2 | This file accompanies other files in the evacuation simulation project. 3 | people: Nick B., Matthew J., Aalok S. 4 | 5 | In this file we define a useful class for the agent, 'Person' 6 | ''' 7 | 8 | import random 9 | 10 | class Person: 11 | id = None 12 | rate = None # how long it takes to move one unit distance 13 | strategy = None # probability with which agent picks closes exit 14 | loc = None, None # variable tracking this agent's location (xy coordinates) 15 | 16 | alive = True # TODO get rid of this variable? we don't really need this 17 | safe = False # mark safe once successfully exited. helps track how many 18 | # people still need to finish 19 | 20 | exit_time = 0 # time it took this agent to get to the safe zone from its 21 | # starting point 22 | 23 | 24 | def __init__(self, id, rate:float=1.0, strategy:float=.7, loc:tuple=None): 25 | ''' 26 | constructor method 27 | --- 28 | rate 29 | strategy 30 | loc 31 | ''' 32 | self.id = id 33 | self.rate = rate 34 | self.strategy = strategy 35 | self.loc = tuple(loc) 36 | 37 | 38 | def move(self, nbrs, rv=None): 39 | ''' 40 | when this person has finished their current movement, we must schedule 41 | the next one 42 | --- 43 | graph (dict): a dictionary-like graph storing the floor plan according 44 | to our specification 45 | 46 | return: tuple, location the agent decided to move to 47 | ''' 48 | nbrs = [(loc, attrs) for loc, attrs in nbrs 49 | if not(attrs['F'] or attrs['W'])] 50 | if not nbrs: return None 51 | loc, attrs = min(nbrs, key=lambda tup: tup[1]['distS']) 52 | # print('Person {} at {} is moving to {}'.format(self.id, self.loc, loc)) 53 | # print('Person {} is {} away from safe'.format(self.id, attrs['distS'])) 54 | self.loc = loc 55 | if attrs['S']: 56 | self.safe = True 57 | elif attrs['F']: 58 | self.alive = False 59 | 60 | return loc 61 | -------------------------------------------------------------------------------- /viz.py: -------------------------------------------------------------------------------- 1 | import matplotlib.pyplot as plt 2 | from matplotlib.colors import ListedColormap, BoundaryNorm 3 | from matplotlib import colors, colorbar 4 | import numpy as np 5 | from random import Random 6 | 7 | 8 | class Plotter: 9 | 10 | def __init__(self): 11 | ''' 12 | ''' 13 | plt.ion() 14 | 15 | def draw_grid(self, gdata): 16 | ''' 17 | ''' 18 | r, c = len(gdata), len(gdata[0]) 19 | 20 | # create discrete colormap 21 | cmap = colors.ListedColormap(['lightblue', 'black', 'red', 22 | 'lightgreen', 'darkblue', '#520000']) 23 | bounds = [-.5, .5, 1.5, 2.5, 3.5, 4.5, 5.5] 24 | norm = colors.BoundaryNorm(bounds, cmap.N) 25 | 26 | plt.imshow(gdata, cmap=cmap, norm=norm) 27 | 28 | # draw gridlines 29 | plt.grid(which='major', axis='both', linestyle='-', color='k', linewidth=2) 30 | plt.xticks(np.arange(-.5, r, 1)) 31 | plt.yticks(np.arange(-.5, c, 1)) 32 | plt.axis('off') 33 | 34 | 35 | def draw_people(self, x=[], y=[], c=[]): 36 | ''' 37 | ''' 38 | # alive ded safe unknown 39 | cmap = colors.ListedColormap(['blue', '#2b0000', 'darkgreen', 'yellow']) 40 | bounds = [-.5, .5, 1.5, 2.5, 3.5] 41 | norm = colors.BoundaryNorm(bounds, cmap.N) 42 | 43 | plt.scatter(x, y, c=c, cmap=cmap, norm=norm) 44 | 45 | 46 | def visualize(self, graph={(3,4): {'F': 1}}, people=[], delay=.01): 47 | ''' 48 | ''' 49 | 50 | # an arbitrary assignment of integers for each of the attributes for our 51 | # colormap 52 | attrmap = {'N': 0, 'W': 1, 'F': 2, 'S': 3, 'B': 4} 53 | 54 | # detect rows and columns 55 | r, c = 0, 0 56 | for loc, attrs in graph.items(): 57 | r = max(r, loc[0]+1) 58 | c = max(c, loc[1]+1) 59 | 60 | # start with a blank grid and fill into attributes 61 | gdata = np.zeros(shape=(r, c)) 62 | 63 | for loc, attrs in graph.items(): 64 | for att in 'SWBF': 65 | if att not in attrs: continue 66 | if attrs[att]: 67 | gdata[loc] = attrmap[att] 68 | if att == 'W' and attrs['F']: 69 | gdata[loc] = 5 70 | break 71 | 72 | # use the accumulated data to draw the grid 73 | self.draw_grid(gdata) 74 | 75 | X, Y, C = [], [], [] 76 | for p in people: 77 | row, col = p.loc 78 | R = Random(p.id) 79 | x, y = col-.5 + R.random(), row-.5 + R.random() 80 | if p.safe: c = 2 81 | elif not p.alive: c = 1 82 | elif p.alive: c = 0 83 | else: c = 3 # unknown state?? 84 | 85 | X += [x] 86 | Y += [y] 87 | C += [c] 88 | 89 | self.draw_people(X, Y, C) 90 | 91 | # matplotlib housekeeping 92 | plt.draw() 93 | plt.pause(delay) 94 | plt.clf() 95 | 96 | 97 | 98 | 99 | for i in range(10): 100 | break 101 | x = np.random.random([2, 10]) 102 | print(x) 103 | plt.scatter(*x) 104 | 105 | plt.draw() 106 | plt.pause(0.0001) 107 | 108 | plt.clf() 109 | 110 | 111 | 112 | 113 | if __name__ == '__main__': 114 | grid = Plotter() 115 | grid.visualize() 116 | 117 | raise 118 | # create discrete colormap 119 | cmap = colors.ListedColormap(['red', 'blue']) 120 | bounds = range() 121 | norm = colors.BoundaryNorm(bounds, cmap.N) 122 | 123 | for i in range(50): 124 | 125 | data = np.zeros(shape=(10, 10))# * 20 126 | 127 | #fig, ax = plt.subplots() 128 | plt.imshow(data, cmap=cmap, norm=norm) 129 | 130 | # draw gridlines 131 | plt.grid(which='major', axis='both', linestyle='-', color='k', linewidth=2) 132 | plt.xticks(np.arange(-.5, 10, 1)); 133 | plt.yticks(np.arange(-.5, 10, 1)); 134 | 135 | plt.draw() 136 | plt.pause(.0001) 137 | 138 | plt.clf() 139 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # agent-based building evacuation simulation 2 | 3 | this simple simulation program simulates a moving danger situation (e.g., fire) 4 | and people in a confined space trying to escape away from danger to get to safe zone(s). 5 | additional constraints are modeled as queues, where a limited number of people can pass at once. 6 | 7 | final project for [cs326: 'simulation'](http://cs.richmond.edu/courses/index.html) 8 | 9 | see it in action! 10 | 11 | ![action](https://i.imgur.com/JsQBlWi.png) 12 | 13 | Usage 14 | --- 15 | ``` 16 | usage: evacuate.py [-h] [-i INPUT] [-n NUMPEOPLE] [-r RANDOM_STATE] 17 | [-t MAX_TIME] [-f] [-g] [-v] [-d FIRE_RATE] 18 | [-b BOTTLENECK_DELAY] [-a ANIMATION_DELAY] 19 | 20 | optional arguments: 21 | -h, --help show this help message and exit 22 | 23 | -i INPUT, --input INPUT 24 | input floor plan file (default: in/twoexitbottleneck.py) 25 | 26 | -n NUMPEOPLE, --numpeople NUMPEOPLE 27 | number of people in the simulation (default: 10) 28 | 29 | -r RANDOM_STATE, --random_state RANDOM_STATE 30 | aka. seed (default: 8675309) 31 | 32 | -t MAX_TIME, --max_time MAX_TIME 33 | the building collapses at this clock tick. people 34 | beginning movement before this will be assumed to have 35 | moved away sufficiently (no default argument) 36 | 37 | -d FIRE_RATE, --fire_rate FIRE_RATE 38 | exponent of spread of fire rate function exponentiator 39 | fire grows exponentially. d determines how exponentially. 40 | 41 | -b BOTTLENECK_DELAY, --bottleneck_delay BOTTLENECK_DELAY 42 | how long until the next person may leave the B 43 | 44 | -a ANIMATION_DELAY, --animation_delay ANIMATION_DELAY 45 | delay per frame of animated visualization (s, default: 1) 46 | 47 | -f, --no_spread_fire disallow fire to spread around? (default: false) 48 | 49 | -g, --no_graphical_output 50 | disallow graphics? (default: false) 51 | 52 | -v, --verbose show excessive output? (default: false) 53 | 54 | ``` 55 | 56 | ### Sample output 57 | The goal of the program is to run simulations and output useful statistics 58 | about the simulations, in a manner that helps understand the effects of 59 | various parameters the simulation is called with, on the outcome. 60 | ``` 61 | STATS 62 | total # people .......................................... 32 63 | # people safe ........................................... 32 64 | # people dead ............................................ 0 65 | # people gravely injured ................................. 0 66 | 67 | average time to safe ................................ 10.088 68 | ``` 69 | 70 | 71 | Model 72 | --- 73 | We model a floor plan as a 2D grid. A cell neighbors four other cells (top, bottom, left, right). 74 | Each cell has attributes: it can be normal (N), wall (W), bottleneck (B), fire (F), safe zone (S), or people (P). 75 | You can use a GUI program to design and generate input per our specification; 76 | visit [this repository](https://github.com/aalok-sathe/egress-floorplan-design). Some sample inputs are available in `in/`. 77 | 78 | The goal is for as many people to get to the safe zones, away from danger's reach. 79 | To solve this problem, we represent this 2D grid using a graph with nodes and edges between adjacent nodes (neighbors). 80 | People may move between adjacent nodes. Each person has their own movement strategy, and implements a move() method that 81 | returns a location. The actual simulation is agnostic of the implementation of this method, allowing for agent-based 82 | modeling experiments. In our case, we consider some baseline strategies: a person will choose to move towards the closest safe 83 | zone with a certain probability, and away from it with the remaining chance. Additionally, agents move at different rates, 84 | congruent to real-world scenario. 85 | A bottleneck is any constricted pathway allowing the passage of only a finite amount of persons in unit time. 86 | For simplicity, we will consider this limit to be 1. There may be multiple bottlenecks. Exits are ideally bottlenecks, 87 | otherwise the simulation would not be meaningful or purposeful. 88 | 89 | The simulation may run for a max time T, allowing questions such as survival rate; percent people out of danger, 90 | or until everyone escapes, allowing for studying mean escape time, and time after which most people escape after individual 91 | variability. 92 | 93 | 94 | 95 | People 96 | --- 97 | - Aalok S 98 | - Nick B 99 | - Matthew J 100 | 101 | -------------------------------------------------------------------------------- /evacuate.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python3 2 | 3 | ''' 4 | This is the main file in the evacuation simulation project. 5 | people: Nick B., Matthew J., Aalok S. 6 | 7 | In this file we define a useful class to model the building floor, 'Floor' 8 | 9 | Also in this file, we proceed to provide a main method so that this file is 10 | meaningfully callable to run a simulation experiment 11 | ''' 12 | 13 | # stdlib imports 14 | import simulus 15 | import sys 16 | import pickle 17 | import random 18 | import pprint 19 | from argparse import ArgumentParser 20 | try: 21 | from randomgen import PCG64, RandomGenerator as Generator 22 | except ImportError: 23 | from randomgen import PCG64, Generator 24 | 25 | # local project imports 26 | from person import Person 27 | from bottleneck import Bottleneck 28 | from floorparse import FloorParser 29 | 30 | pp = pprint.PrettyPrinter(indent=4).pprint 31 | 32 | class FireSim: 33 | sim = None 34 | graph = None # dictionary (x,y) --> attributes 35 | gui = False 36 | r = None 37 | c = None 38 | 39 | numpeople = 0 40 | numdead = 0 41 | numsafe = 0 42 | nummoving = 0 43 | 44 | bottlenecks = dict() 45 | fires = set() 46 | people = [] 47 | 48 | exit_times = [] 49 | avg_exit = 0 # tracks sum first, then we divide 50 | 51 | def __init__(self, input, n, location_sampler=random.sample, 52 | strategy_generator=lambda: random.uniform(.5, 1.), 53 | rate_generator=lambda: abs(random.normalvariate(1, .5)), 54 | person_mover=random.uniform, fire_mover=random.sample, 55 | fire_rate=2, bottleneck_delay=1, animation_delay=.1, 56 | verbose=False, 57 | **kwargs): 58 | ''' 59 | constructor method 60 | --- 61 | graph (dict): a representation of the floor plan as per our 62 | specification 63 | n (int): number of people in the simulation 64 | ''' 65 | self.sim = simulus.simulator() 66 | self.parser = FloorParser() 67 | self.animation_delay = animation_delay 68 | self.verbose = verbose 69 | 70 | with open(input, 'r') as f: 71 | self.graph = self.parser.parse(f.read()) 72 | self.numpeople = n 73 | 74 | self.location_sampler = location_sampler 75 | self.strategy_generator = strategy_generator 76 | self.rate_generator = rate_generator 77 | self.person_mover = person_mover 78 | self.fire_mover = fire_mover 79 | 80 | self.fire_rate = fire_rate 81 | self.bottleneck_delay = bottleneck_delay 82 | self.kwargs = kwargs 83 | 84 | self.setup() 85 | 86 | 87 | def precompute(self): 88 | ''' 89 | precompute stats on the graph, e.g. nearest safe zone, nearest fire 90 | ''' 91 | graph = self.graph 92 | 93 | def bfs(target, pos): 94 | if graph[pos]['W']: return float('inf') 95 | q = [(pos, 0)] 96 | visited = set() 97 | while q: 98 | node, dist = q.pop() 99 | if node in visited: continue 100 | visited.add(node) 101 | 102 | node = graph[node] 103 | if node['W'] or node['F']: continue 104 | if node[target]: return dist 105 | 106 | for n in node['nbrs']: 107 | if n in visited: continue 108 | q = [(n, dist+1)] + q 109 | 110 | # unreachable 111 | return float('inf') 112 | 113 | for loc in graph: 114 | graph[loc]['distF'] = bfs('F', loc) 115 | graph[loc]['distS'] = bfs('S', loc) 116 | 117 | self.graph = dict(graph.items()) 118 | 119 | return self.graph 120 | 121 | 122 | def setup(self): 123 | ''' 124 | once we have the parameters and random variate generation methods from 125 | __init__, we can proceed to create instances of: people and bottlenecks 126 | ''' 127 | self.precompute() 128 | 129 | av_locs = [] 130 | bottleneck_locs = [] 131 | fire_locs = [] 132 | 133 | r, c = 0, 0 134 | for loc, attrs in self.graph.items(): 135 | r = max(r, loc[0]) 136 | c = max(c, loc[1]) 137 | if attrs['P']: av_locs += [loc] 138 | elif attrs['B']: bottleneck_locs += [loc] 139 | elif attrs['F']: fire_locs += [loc] 140 | 141 | assert len(av_locs) > 0, 'ERR: no people placement locations in input' 142 | for i in range(self.numpeople): 143 | p = Person(i, self.rate_generator(), 144 | self.strategy_generator(), 145 | self.location_sampler(av_locs)) 146 | self.people += [p] 147 | 148 | for loc in bottleneck_locs: 149 | b = Bottleneck(loc) 150 | self.bottlenecks[loc] = b 151 | self.fires.update(set(fire_locs)) 152 | 153 | self.r, self.c = r+1, c+1 154 | 155 | print( 156 | '='*79, 157 | 'initialized a {}x{} floor with {} people in {} locations'.format( 158 | self.r, self.c, len(self.people), len(av_locs) 159 | ), 160 | 'initialized {} bottleneck(s)'.format(len(self.bottlenecks)), 161 | 'detected {} fire zone(s)'.format(len([loc for loc in self.graph 162 | if self.graph[loc]['F']])), 163 | '\ngood luck escaping!', '='*79, 'LOGS', sep='\n' 164 | ) 165 | 166 | 167 | def visualize(self, t): 168 | ''' 169 | ''' 170 | if self.gui: 171 | self.plotter.visualize(self.graph, self.people, t) 172 | 173 | 174 | def update_bottlenecks(self): 175 | ''' 176 | handles the bottleneck zones on the grid, where people cannot all pass 177 | at once. for simplicity, bottlenecks are treated as queues 178 | ''' 179 | 180 | for key in self.bottlenecks: 181 | #print(key, self.bottlenecks[key]) 182 | personLeaving = self.bottlenecks[key].exitBottleNeck() 183 | if(personLeaving != None): 184 | self.sim.sched(self.update_person, personLeaving.id, offset=0) 185 | 186 | if self.numsafe + self.numdead >= self.numpeople: 187 | return 188 | 189 | if self.maxtime and self.sim.now >= self.maxtime: 190 | return 191 | else: 192 | self.sim.sched(self.update_bottlenecks, 193 | offset=self.bottleneck_delay) 194 | 195 | 196 | 197 | def update_fire(self): 198 | ''' 199 | method that controls the spread of fire. we use a rudimentary real-world 200 | model that spreads fire exponentially faster proportional to the amount 201 | of fire already on the floor. empty nodes are more likely to get set 202 | on fire the more lit neighbors they have. empty plain nodes are more 203 | likely to burn than walls. 204 | fire spreads proportional to (grid_area)/(fire_area)**exp 205 | the method uses the 'fire_rate' instance variable as the 'exp' in the 206 | expression above. 207 | fire stops spreading once it's everywhere, or when all the people have 208 | stopped moving (when all are dead or safe, not moving) 209 | ''' 210 | if self.numsafe + self.numdead >= self.numpeople: 211 | print('INFO:', 'people no longer moving, so stopping fire spread') 212 | return 213 | if self.maxtime and self.sim.now >= self.maxtime: 214 | return 215 | 216 | no_fire_nbrs = [] # list, not set because more neighbors = more likely 217 | for loc in self.fires: 218 | # gets the square at the computed location 219 | square = self.graph[loc] 220 | 221 | # returns the full list of nbrs of the square 222 | nbrs = [(coords, self.graph[coords]) for coords in square['nbrs']] 223 | 224 | # updates nbrs to exclude safe zones and spaces already on fire 225 | no_fire_nbrs += [(loc, attrs) for loc, attrs in nbrs 226 | if attrs['S'] == attrs['F'] == 0] 227 | # more likely (twice) to spread to non-wall empty zone 228 | no_fire_nbrs += [(loc, attrs) for loc, attrs in nbrs 229 | if attrs['W'] == attrs['S'] == attrs['F'] == 0] 230 | 231 | try: 232 | (choice, _) = self.fire_mover(no_fire_nbrs) 233 | except ValueError as e: 234 | print('INFO:', 'fire is everywhere, so stopping fire spread') 235 | return 236 | 237 | self.graph[choice]['F'] = 1 238 | self.fires.add(choice) 239 | 240 | self.precompute() 241 | rt = self.fire_rate 242 | self.sim.sched(self.update_fire, 243 | offset=len(self.graph)/max(1, len(self.fires))**rt) 244 | 245 | self.visualize(self.animation_delay/max(1, len(self.fires))**rt) 246 | 247 | return choice 248 | 249 | 250 | def update_person(self, person_ix): 251 | ''' 252 | handles scheduling an update for each person, by calling move() on them. 253 | move will return a location decided by the person, and this method will 254 | handle the simulus scheduling part to keep it clean 255 | ''' 256 | if self.maxtime and self.sim.now >= self.maxtime: 257 | return 258 | 259 | p = self.people[person_ix] 260 | if self.graph[p.loc]['F'] or not p.alive: 261 | p.alive = False 262 | self.numdead += 1 263 | if self.verbose: 264 | print('{:>6.2f}\tPerson {:>3} at {} could not make it'.format( 265 | self.sim.now, 266 | p.id, p.loc)) 267 | return 268 | if p.safe: 269 | self.numsafe += 1 270 | p.exit_time = self.sim.now 271 | self.exit_times += [p.exit_time] 272 | self.avg_exit += p.exit_time 273 | if self.verbose: 274 | print('{:>6.2f}\tPerson {:>3} is now SAFE!'.format(self.sim.now, 275 | p.id)) 276 | return 277 | 278 | loc = p.loc 279 | square = self.graph[loc] 280 | nbrs = [(coords, self.graph[coords]) for coords in square['nbrs']] 281 | 282 | target = p.move(nbrs) 283 | if not target: 284 | p.alive = False 285 | self.numdead += 1 286 | if self.verbose: 287 | print('{:>6.2f}\tPerson {:>3} at {} got trapped in fire'.format( 288 | self.sim.now, 289 | p.id, p.loc)) 290 | return 291 | square = self.graph[target] 292 | if square['B']: 293 | b = self.bottlenecks[target] 294 | b.enterBottleNeck(p) 295 | elif square['F']: 296 | p.alive = False 297 | self.numdead += 1 298 | return 299 | else: 300 | t = 1/p.rate 301 | if self.sim.now + t >= (self.maxtime or float('inf')): 302 | if square['S']: 303 | self.nummoving += 1 304 | else: 305 | self.numdead += 1 306 | else: 307 | self.sim.sched(self.update_person, person_ix, offset=1/p.rate) 308 | 309 | if (1+person_ix) % int(self.numpeople**.5) == 0: 310 | self.visualize(t=self.animation_delay/len(self.people)/2) 311 | 312 | # self.sim.show_calendar() 313 | 314 | 315 | def simulate(self, maxtime=None, spread_fire=False, gui=False): 316 | ''' 317 | sets up initial scheduling and calls the sim.run() method in simulus 318 | ''' 319 | self.gui = gui 320 | if self.gui: 321 | from viz import Plotter 322 | self.plotter = Plotter() 323 | 324 | # set initial movements of all the people 325 | for i, p in enumerate(self.people): 326 | loc = tuple(p.loc) 327 | square = self.graph[loc] 328 | nbrs = square['nbrs'] 329 | self.sim.sched(self.update_person, i, offset=1/p.rate) 330 | 331 | #updates fire initially 332 | if spread_fire: 333 | self.sim.sched(self.update_fire, 334 | offset=1)#len(self.graph)/max(1, len(self.fires))) 335 | else: 336 | print('INFO\t', 'fire won\'t spread around!') 337 | self.sim.sched(self.update_bottlenecks, offset=self.bottleneck_delay) 338 | 339 | self.maxtime = maxtime 340 | self.sim.run() 341 | 342 | self.avg_exit /= max(self.numsafe, 1) 343 | 344 | 345 | def stats(self): 346 | ''' 347 | computes and outputs useful stats about the simulation for nice output 348 | ''' 349 | print('\n\n', '='*79, sep='') 350 | print('STATS') 351 | 352 | def printstats(desc, obj): 353 | print('\t', 354 | (desc+' ').ljust(30, '.') + (' '+str(obj)).rjust(30, '.')) 355 | 356 | printstats('total # people', self.numpeople) 357 | printstats('# people safe', self.numsafe) 358 | printstats('# people dead', self.numpeople-self.numsafe-self.nummoving) 359 | printstats('# people gravely injured', self.nummoving) 360 | print() 361 | # printstats('total simulation time', '{:.3f}'.format(self.sim.now)) 362 | if self.avg_exit: 363 | printstats('average time to safe', '{:.3f}'.format(self.avg_exit)) 364 | else: 365 | printstats('average time to safe', 'NA') 366 | print() 367 | 368 | # print(self.parser.tostr(self.graph)) 369 | self.visualize(4) 370 | 371 | 372 | def main(): 373 | ''' 374 | driver method for this file. the firesim class can be used via imports as 375 | well, but this driver file provides a comprehensive standalone interface 376 | to the simulation 377 | ''' 378 | # set up and parse commandline arguments 379 | parser = ArgumentParser() 380 | parser.add_argument('-i', '--input', type=str, 381 | default='in/twoexitbottleneck.txt', 382 | help='input floor plan file (default: ' 383 | 'in/twoexitbottleneck.py)') 384 | parser.add_argument('-n', '--numpeople', type=int, default=10, 385 | help='number of people in the simulation (default:10)') 386 | parser.add_argument('-r', '--random_state', type=int, default=8675309, 387 | help='aka. seed (default:8675309)') 388 | parser.add_argument('-t', '--max_time', type=float, default=None, 389 | help='the building collapses at this clock tick. people' 390 | ' beginning movement before this will be assumed' 391 | ' to have moved away sufficiently (safe)') 392 | parser.add_argument('-f', '--no_spread_fire', action='store_true', 393 | help='disallow fire to spread around?') 394 | parser.add_argument('-g', '--no_graphical_output', action='store_true', 395 | help='disallow graphics?') 396 | parser.add_argument('-o', '--output', action='store_true', 397 | help='show excessive output?') 398 | parser.add_argument('-d', '--fire_rate', type=float, default=2, 399 | help='rate of spread of fire (this is the exponent)') 400 | parser.add_argument('-b', '--bottleneck_delay', type=float, default=1, 401 | help='how long until the next person may leave the B') 402 | parser.add_argument('-a', '--animation_delay', type=float, default=1, 403 | help='delay per frame of animated visualization (s)') 404 | args = parser.parse_args() 405 | # output them as a make-sure-this-is-what-you-meant 406 | print('commandline arguments:', args, '\n') 407 | 408 | # set up random streams 409 | streams = [Generator(PCG64(args.random_state, i)) for i in range(5)] 410 | loc_strm, strat_strm, rate_strm, pax_strm, fire_strm = streams 411 | 412 | location_sampler = loc_strm.choice # used to make initial placement of pax 413 | strategy_generator = lambda: strat_strm.uniform(.5, 1) # used to pick move 414 | rate_generator = lambda: max(.1, abs(rate_strm.normal(1, .1))) # used to 415 | # decide 416 | # strategies 417 | person_mover = lambda: pax_strm.uniform() # 418 | fire_mover = lambda a: fire_strm.choice(a) # 419 | 420 | # create an instance of Floor 421 | floor = FireSim(args.input, args.numpeople, location_sampler, 422 | strategy_generator, rate_generator, person_mover, 423 | fire_mover, fire_rate=args.fire_rate, 424 | bottleneck_delay=args.bottleneck_delay, 425 | animation_delay=args.animation_delay, verbose=args.output) 426 | 427 | # floor.visualize(t=5000) 428 | # call the simulate method to run the actual simulation 429 | floor.simulate(maxtime=args.max_time, spread_fire=not args.no_spread_fire, 430 | gui=not args.no_graphical_output) 431 | 432 | floor.stats() 433 | 434 | if __name__ == '__main__': 435 | main() 436 | --------------------------------------------------------------------------------