/
police.py
155 lines (134 loc) · 5.83 KB
/
police.py
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
import geopandas as gpd # geographic data manipulation
import random # pseudorandom numbers
import pandas as pd # data manipulation
from typing import List
class Police:
def __init__(self, bounds: gpd.GeoDataFrame):
"""Initial stat and variables for the police agents.
Takes the bounds input which determines where agents may be created.
Agents may be spawned within the extent of bounds, but to determine
whether they fall within a bounds polygon this must be checked with a
geographic function gpd.within().
Args:
bounds (gpd.GeoDataFrame): GeoDataFrame with the input polygon.
"""
# takes bounds from main.py
self.bounds = bounds
# find extent of bounds
x_min, y_min, x_max, y_max = self.bounds.total_bounds
while True:
# random xy from extent of bounds (square)
self.x = random.uniform(x_min, x_max)
self.y = random.uniform(y_min, y_max)
# convert to geodataframe
df = pd.DataFrame({'x': [self.x], 'y': [self.y]})
geom = gpd.points_from_xy(df.x, df.y)
gdf = gpd.GeoDataFrame(df, geometry=geom)
# check whether point falls within polygon
within = int(gdf.within(self.bounds))
# only keep point if within poly, otherwise repeat random coords
if within == 1:
self.x = gdf['x']
self.y = gdf['y']
self.geom = gdf['geometry']
break
def distance_between(self, agent) -> int:
"""Euclidean distance between two geographic points.
The output represents the distance referring to the geographic
unit of the projection used.
Args:
agent (Crime): A crime object with the attributes x and y.
Must be the same projection as self.
Returns:
int: Returns a float value of distance using the projection values.
"""
distance = ((self.x - agent.x)**2 +
(self.y - agent.y)**2)**0.5
distance = float(distance)
return distance
def random_movement(self, cur_dist: float, crime_list: List[int]):
"""
Create random movement parameters for each agent.
:param cur_dist: Distance of a police agent from an active crime.
:type cur_dist: float
:param crime_list: List containing all active crime agents
:type crime_list: List["Crime"]
"""
dist = []
if random.random() < 0.5:
# add 1000 to police x value
self.x = (self.x + 1000)
for c in crime_list:
# find new distance from police to all crimes
dist.append(self.distance_between(c))
# move in opposite direction if distance from nearest min(dist)
# crime to police further than original police x position
if min(dist) > cur_dist:
self.x = (self.x - 2000)
else:
# repeat the above but take - 1000 x value
self.x = (self.x - 1000)
for c in crime_list:
dist.append(self.distance_between(c))
if min(dist) > cur_dist:
self.x = (self.x + 2000)
dist = []
if random.random() < 0.5:
# repeat with + 1000 to y value
self.y = (self.y + 1000)
for c in crime_list:
dist.append(self.distance_between(c))
if min(dist) > cur_dist:
self.y = (self.y - 2000)
else:
# repeat with - 1000 to y value
self.y = (self.y - 1000)
for c in crime_list:
dist.append(self.distance_between(c))
if min(dist) > cur_dist:
self.y = (self.y + 2000)
def move(self, crime):
"""Move the police semi randomly, head in direction of crimes.
Police agents will move in a random direction each iteration.
For each solved crime, if the police moves to an area that is further
away than its previous position to a crime it will not move.
Args:
crime (List[crime.Crime]): List of crime 'agents' with x and y
coordinates
"""
# keep only unsolved crimes
crime_list = [c for c in crime if c.solved == 0]
i = 0 # for numbering loops
# while loop to attempt movement of police within bounds polygon
while True:
cur_dist = []
if len(crime_list) > 1:
for c in crime_list:
# find distance from police to crimes
cur_dist.append(self.distance_between(c))
# find min distance
cur_dist = min(cur_dist)
# call the random movement function on police
# which takes current min distance from a crime point to
# determine whether or not to move the police in the random
# direction
self.random_movement(cur_dist, crime_list)
# create xy dataframe
df = pd.DataFrame({'x': [self.x], 'y': [self.y]})
geom = gpd.points_from_xy(df.x, df.y)
# convert df to gdf
gdf = gpd.GeoDataFrame(df, geometry=geom)
# find whether new xy points are within polygon bounds
within = int(gdf.within(self.bounds))
# only allow loop to break if new xy are within bounds
# allow up to 10 times, if over 10 times the police officer is
# lost outside the bounds
i += 1
if i > 10:
print("Police officer has left the bounds.")
break
# if within gives true, allow new xy values to the police
if within == 1:
self.x = gdf['x']
self.y = gdf['y']
break