Add application file
Browse files- TwoDimEnv.py +94 -0
- app.py +181 -0
- longModel.zip +3 -0
- params.py +33 -0
- requirements.txt +60 -0
TwoDimEnv.py
ADDED
@@ -0,0 +1,94 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
1 |
+
import gymnasium as gym
|
2 |
+
from params import *
|
3 |
+
|
4 |
+
|
5 |
+
class TwoDimEnv(gym.Env):
|
6 |
+
"""
|
7 |
+
Custom 2D-Environment that follows gym interface.
|
8 |
+
This is a simple 2D-env where the agent must go to the 0,0 point,
|
9 |
+
it can go right or straight in a continuous action space, but cannot change speed
|
10 |
+
"""
|
11 |
+
def __init__(self):
|
12 |
+
super(TwoDimEnv, self).__init__()
|
13 |
+
|
14 |
+
# agent cannot be further away from target than space_limits
|
15 |
+
self.space_limits = SPACE_LIMITS
|
16 |
+
|
17 |
+
# Initialize the agent position
|
18 |
+
rho_init = np.random.random() * SPACE_LIMITS
|
19 |
+
theta_init = np.random.random() * 2 * PI - PI
|
20 |
+
self.rho_init, self.theta_init, self.z_init = rho_init, theta_init, Z_INIT
|
21 |
+
self.agent_pos = rho_init * np.exp(1j * theta_init)
|
22 |
+
self.agent_z = Z_INIT
|
23 |
+
|
24 |
+
# agent abs speed is constant, may change of direction on the plane, but doesnt change in the z-axis
|
25 |
+
self.agent_speed = SPEED_RHO * np.exp(1j*SPEED_ANGLE)
|
26 |
+
self.agent_speed_z = SPEED_Z
|
27 |
+
self.agent_max_angle = MAX_ANGLE
|
28 |
+
|
29 |
+
#
|
30 |
+
self.agent_previous_pos = self.agent_pos
|
31 |
+
|
32 |
+
|
33 |
+
def reset(self, training=True, rho_init=RHO_INIT, theta_init=THETA_INIT, z_init=Z_INIT):
|
34 |
+
"""
|
35 |
+
:input: TwoDimEnv
|
36 |
+
:return: TwoDimEnv
|
37 |
+
"""
|
38 |
+
# Initialize the agent position and speed
|
39 |
+
self.agent_pos = rho_init * np.exp(1j * theta_init)
|
40 |
+
self.agent_speed = SPEED_RHO * np.exp(1j * SPEED_ANGLE)
|
41 |
+
self.agent_z = z_init
|
42 |
+
|
43 |
+
# init step index
|
44 |
+
self.step_index = 0
|
45 |
+
|
46 |
+
return self.get_obs()
|
47 |
+
|
48 |
+
def step(self, action):
|
49 |
+
'''
|
50 |
+
the Gym step
|
51 |
+
:param action:
|
52 |
+
:return:
|
53 |
+
'''
|
54 |
+
|
55 |
+
self.step_index += 1
|
56 |
+
|
57 |
+
# Agent changes of direction according to its action: action[0] in [-1; 1]
|
58 |
+
self.agent_speed *= np.exp(1j * self.agent_max_angle * action[0], dtype=complex)
|
59 |
+
self.agent_previous_pos = self.agent_pos
|
60 |
+
self.agent_pos += self.agent_speed
|
61 |
+
self.agent_z -= self.agent_speed_z
|
62 |
+
|
63 |
+
# Are we done?
|
64 |
+
done = bool(self.agent_z <= 0)
|
65 |
+
|
66 |
+
# get normalized obs
|
67 |
+
obs = self.get_obs()
|
68 |
+
|
69 |
+
return obs, 0., done, {}
|
70 |
+
|
71 |
+
def render(self, **kwargs):
|
72 |
+
pass
|
73 |
+
|
74 |
+
def close(self):
|
75 |
+
pass
|
76 |
+
|
77 |
+
# calculates normalized obs
|
78 |
+
def get_obs(self):
|
79 |
+
'''
|
80 |
+
normalises the observation
|
81 |
+
:return: normalised observation
|
82 |
+
'''
|
83 |
+
agent_dist = np.abs(self.agent_pos) / self.space_limits
|
84 |
+
agent_angle = np.angle(self.agent_pos) / (2 * PI)
|
85 |
+
if agent_angle < 0:
|
86 |
+
agent_angle += 1
|
87 |
+
agent_speed = np.angle(self.agent_speed) / (2 * PI)
|
88 |
+
if agent_speed < 0:
|
89 |
+
agent_speed += 1
|
90 |
+
agent_z = (Z_INIT-self.agent_z) / Z_INIT
|
91 |
+
obs = np.array([agent_dist, agent_angle, agent_speed, agent_z]).astype(np.float32)
|
92 |
+
return obs
|
93 |
+
|
94 |
+
|
app.py
ADDED
@@ -0,0 +1,181 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
1 |
+
import time
|
2 |
+
import streamlit as st
|
3 |
+
from params import *
|
4 |
+
from stable_baselines3 import SAC
|
5 |
+
import pandas as pd
|
6 |
+
import pydeck as pdk
|
7 |
+
from TwoDimEnv import TwoDimEnv
|
8 |
+
|
9 |
+
import gymnasium as gym
|
10 |
+
|
11 |
+
def run_episode(env, model, lat_tg, lon_tg, rho_init=RHO_INIT, theta_init=THETA_INIT, zed=Z_INIT):
|
12 |
+
'''
|
13 |
+
runs the episode given an observation init ... and an env and a model
|
14 |
+
:param env: TwoDimEnv
|
15 |
+
:param model:
|
16 |
+
:param lat_tg: lattitude de la target (0,0)
|
17 |
+
:param lon_tg: longitude de la target (0,0)
|
18 |
+
:param rho_init:
|
19 |
+
:param theta_init:
|
20 |
+
:param zed:
|
21 |
+
:return:
|
22 |
+
'''
|
23 |
+
obs = env.reset(training=False, rho_init=rho_init, theta_init=theta_init, z_init=zed)
|
24 |
+
step = 0
|
25 |
+
lat, lon = rhotheta_to_latlon(MOVE_TO_METERS * rho_init, theta_init, lat_tg, lon_tg)
|
26 |
+
path = [[lon, lat, MOVE_TO_METERS * zed]]
|
27 |
+
traj = [lat, lon, MOVE_TO_METERS * zed]
|
28 |
+
while step < zed:
|
29 |
+
step += 1
|
30 |
+
action, _ = model.predict(obs, deterministic=True)
|
31 |
+
obs, _, _, _ = env.step(action)
|
32 |
+
rho = obs[0] * env.space_limits
|
33 |
+
theta = obs[1] * 2 * PI
|
34 |
+
lat, lon = rhotheta_to_latlon(MOVE_TO_METERS*rho, theta, lat_tg, lon_tg)
|
35 |
+
traj = np.vstack((traj, [lat, lon, MOVE_TO_METERS*(zed - step)]))
|
36 |
+
path.append([lon, lat, MOVE_TO_METERS*(zed - step)])
|
37 |
+
df_col = pd.DataFrame(traj, columns=['lat', 'lon', 'zed'])
|
38 |
+
df_path = pd.DataFrame([{
|
39 |
+
'color': [0, 0, 200, 120],
|
40 |
+
'path': path
|
41 |
+
}])
|
42 |
+
return df_path, df_col
|
43 |
+
|
44 |
+
def rhotheta_to_latlon(rho, theta, lat_tg, lon_tg):
|
45 |
+
'''
|
46 |
+
transforms polar coordinates into lat, lon
|
47 |
+
:param rho:
|
48 |
+
:param theta:
|
49 |
+
:param lat_tg: latitude de la target (0,0)
|
50 |
+
:param lon_tg: longitude de la target (0,0)
|
51 |
+
:return:
|
52 |
+
'''
|
53 |
+
z = rho * np.exp(1j * theta)
|
54 |
+
lat = np.imag(z)*360/(40075*1000) + lat_tg
|
55 |
+
lon = np.real(z)*360/(40075*1000*np.cos(PI/180*lat)) + lon_tg
|
56 |
+
return lat, lon
|
57 |
+
|
58 |
+
|
59 |
+
def get_layers(df, df_past, df_target, df_path, df_col):
|
60 |
+
'''
|
61 |
+
renders the layers to be displayed with df in different formats as entries
|
62 |
+
:param df:
|
63 |
+
:param df_past:
|
64 |
+
:param df_target:
|
65 |
+
:param df_path:
|
66 |
+
:param df_col:
|
67 |
+
:return:
|
68 |
+
'''
|
69 |
+
return [
|
70 |
+
pdk.Layer(
|
71 |
+
'ScatterplotLayer',
|
72 |
+
data=df,
|
73 |
+
get_position='[lon, lat]',
|
74 |
+
get_color='[200, 30, 0, 160, 40]',
|
75 |
+
get_radius=10,
|
76 |
+
),
|
77 |
+
pdk.Layer(
|
78 |
+
'ScatterplotLayer',
|
79 |
+
data=df_target,
|
80 |
+
get_position='[lon, lat]',
|
81 |
+
get_color='[200, 30, 0]',
|
82 |
+
get_radius=65,
|
83 |
+
),
|
84 |
+
pdk.Layer(
|
85 |
+
'ScatterplotLayer',
|
86 |
+
data=df_target,
|
87 |
+
get_position='[lon, lat]',
|
88 |
+
get_color='[255, 255, 255]',
|
89 |
+
get_radius=45,
|
90 |
+
),
|
91 |
+
pdk.Layer(
|
92 |
+
'ScatterplotLayer',
|
93 |
+
data=df_target,
|
94 |
+
get_position='[lon, lat]',
|
95 |
+
get_color='[0, 0, 200]',
|
96 |
+
get_radius=25,
|
97 |
+
),
|
98 |
+
pdk.Layer(
|
99 |
+
'ScatterplotLayer',
|
100 |
+
data=df_past,
|
101 |
+
get_position='[lon, lat]',
|
102 |
+
get_color='[200, 30, 0, 40]',
|
103 |
+
get_radius=10,
|
104 |
+
),
|
105 |
+
pdk.Layer(
|
106 |
+
type="PathLayer",
|
107 |
+
data=df_path,
|
108 |
+
pickable=True,
|
109 |
+
get_color="color",
|
110 |
+
width_scale=20,
|
111 |
+
width_min_pixels=1,
|
112 |
+
get_path="path",
|
113 |
+
get_width=1,
|
114 |
+
),
|
115 |
+
pdk.Layer(
|
116 |
+
type="ColumnLayer",
|
117 |
+
data=df_col,
|
118 |
+
get_position=['lon', 'lat'],
|
119 |
+
get_elevation="zed",
|
120 |
+
elevation_scale=1,
|
121 |
+
radius=10,
|
122 |
+
get_fill_color=[160, 20, 0, 10],
|
123 |
+
pickable=True,
|
124 |
+
auto_highlight=True
|
125 |
+
),
|
126 |
+
]
|
127 |
+
|
128 |
+
|
129 |
+
def show():
|
130 |
+
'''
|
131 |
+
shows the i-PADS in Streamlit
|
132 |
+
:return:
|
133 |
+
'''
|
134 |
+
env = TwoDimEnv()
|
135 |
+
model = SAC.load("longModel")
|
136 |
+
st.title('Intelligent PADS by hexamind')
|
137 |
+
st.write('This is a quick demo of an autonomous Parachute (Precision Air Delivery System) controlled by Reinforcement learning. ')
|
138 |
+
st.text('<- Set the starting point')
|
139 |
+
|
140 |
+
st.sidebar.write("Where do you want the parachute to start from?")
|
141 |
+
rho = st.sidebar.slider('What distance? (in m)', 0, 3000, 1500) / MOVE_TO_METERS
|
142 |
+
theta = 2*PI/360 * st.sidebar.slider('What angle?', 0, 360, 90)
|
143 |
+
zed = int(st.sidebar.slider('What elevation? (in m)', 0, 1200, 600) / MOVE_TO_METERS)
|
144 |
+
|
145 |
+
location = st.sidebar.radio("Location", ['San Francisco', 'Paris', 'Puilaurens'])
|
146 |
+
lat_tg = LOC[location]['lat']
|
147 |
+
lon_tg = LOC[location]['lon']
|
148 |
+
df_path, df_col = run_episode(env, model, lat_tg, lon_tg, rho_init=rho, theta_init=theta, zed=zed)
|
149 |
+
st.sidebar.write(
|
150 |
+
'If you like to play, you will probably find some starting points where the parachute is out of control :) '
|
151 |
+
'No worries, we have plenty more efficient models at www.hexamind.com ')
|
152 |
+
|
153 |
+
df_target = pd.DataFrame({'lat': [lat_tg], 'lon': [lon_tg]})
|
154 |
+
deck_map = st.empty()
|
155 |
+
pitch = st.slider('pitch', 0, 100, 50)
|
156 |
+
initial_view_state = pdk.ViewState(
|
157 |
+
latitude=lat_tg,
|
158 |
+
longitude=lon_tg,
|
159 |
+
zoom=12,
|
160 |
+
pitch=pitch
|
161 |
+
)
|
162 |
+
deck_map.pydeck_chart(pdk.Deck(
|
163 |
+
map_style='mapbox://styles/mapbox/light-v9',
|
164 |
+
initial_view_state=initial_view_state
|
165 |
+
))
|
166 |
+
df_pathi = df_path.copy()
|
167 |
+
for i in range(zed):
|
168 |
+
df_pathi['path'][0] = df_path['path'][0][0:i+1]
|
169 |
+
layers = get_layers(df_col[i:i+1], df_col[0:i], df_target, df_pathi, df_col[0:i+1])
|
170 |
+
deck_map.pydeck_chart(pdk.Deck(
|
171 |
+
map_style='mapbox://styles/mapbox/light-v9',
|
172 |
+
initial_view_state=initial_view_state,
|
173 |
+
layers=layers
|
174 |
+
))
|
175 |
+
time.sleep(TIMESLEEP)
|
176 |
+
|
177 |
+
|
178 |
+
show()
|
179 |
+
|
180 |
+
# to uncomment for debug
|
181 |
+
#show_print()
|
longModel.zip
ADDED
@@ -0,0 +1,3 @@
|
|
|
|
|
|
|
|
|
1 |
+
version https://git-lfs.github.com/spec/v1
|
2 |
+
oid sha256:70dc33275fa1df891ec4b106e71b63c2b8ad3f9ebd7728352d7ddfff1e0e895d
|
3 |
+
size 3018346
|
params.py
ADDED
@@ -0,0 +1,33 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
1 |
+
import numpy as np
|
2 |
+
|
3 |
+
PI = np.float32(np.pi)
|
4 |
+
|
5 |
+
EPISODE_LENGTH = 100 # number of steps per episode
|
6 |
+
|
7 |
+
SPACE_LIMITS = 250 # playground limits for the PADS
|
8 |
+
LANDING_LIMITS = 10 # error tolerated for landing
|
9 |
+
LANDING_TARGET = 50 # error tolerated for the target when landing
|
10 |
+
|
11 |
+
RHO_INIT = 10 # default para's 2D position
|
12 |
+
THETA_INIT = PI / 2
|
13 |
+
Z_INIT = 100 #
|
14 |
+
|
15 |
+
SPEED_RHO = 5 # units forward per step
|
16 |
+
SPEED_ANGLE = 0
|
17 |
+
SPEED_Z = 1 # units down per step
|
18 |
+
|
19 |
+
MOVE_TO_METERS = 8 # how many meters (down or horizontally) per unit in a step
|
20 |
+
|
21 |
+
MAX_ANGLE = PI / 3
|
22 |
+
|
23 |
+
LOC = {'Paris':
|
24 |
+
{'lat': 48.865879, 'lon': 2.319827},
|
25 |
+
'Fonsorbes':
|
26 |
+
{'lat': 43.54, 'lon': 1.25},
|
27 |
+
'San Francisco':
|
28 |
+
{'lat': 37.7737283, 'lon': -122.4342383},
|
29 |
+
'Puilaurens':
|
30 |
+
{'lat': 42.8119596, 'lon': 2.2590985},
|
31 |
+
}
|
32 |
+
|
33 |
+
TIMESLEEP = 0.2
|
requirements.txt
ADDED
@@ -0,0 +1,60 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
1 |
+
altair==4.2.2
|
2 |
+
attrs==23.1.0
|
3 |
+
blinker==1.6.2
|
4 |
+
cachetools==5.3.1
|
5 |
+
certifi==2023.5.7
|
6 |
+
charset-normalizer==3.1.0
|
7 |
+
click==8.1.3
|
8 |
+
cloudpickle==2.2.1
|
9 |
+
contourpy==1.0.6
|
10 |
+
cycler==0.11.0
|
11 |
+
decorator==5.1.1
|
12 |
+
entrypoints==0.4
|
13 |
+
fonttools==4.38.0
|
14 |
+
gitdb==4.0.10
|
15 |
+
GitPython==3.1.31
|
16 |
+
gym==0.21.0
|
17 |
+
gymnasium==0.28.1
|
18 |
+
gym-notices==0.0.8
|
19 |
+
idna==3.4
|
20 |
+
importlib-metadata==4.13.0
|
21 |
+
Jinja2==3.1.2
|
22 |
+
jsonschema==4.17.3
|
23 |
+
kiwisolver==1.4.4
|
24 |
+
markdown-it-py==2.1.0
|
25 |
+
MarkupSafe==2.1.2
|
26 |
+
matplotlib==3.5.3
|
27 |
+
mdurl==0.1.2
|
28 |
+
numpy==1.21.6
|
29 |
+
packaging==23.0
|
30 |
+
pandas==1.3.5
|
31 |
+
Pillow==9.5.0
|
32 |
+
protobuf==3.20.3
|
33 |
+
pyarrow==12.0.0
|
34 |
+
pydeck==0.8.1b0
|
35 |
+
Pygments==2.15.1
|
36 |
+
Pympler==1.0.1
|
37 |
+
pyparsing==3.0.9
|
38 |
+
pyrsistent==0.19.3
|
39 |
+
python-dateutil==2.8.2
|
40 |
+
pytz==2022.7.1
|
41 |
+
pytz-deprecation-shim==0.1.0.post0
|
42 |
+
requests==2.28.2
|
43 |
+
rich==13.2.0
|
44 |
+
semver==2.13.0
|
45 |
+
six==1.16.0
|
46 |
+
smmap==5.0.0
|
47 |
+
setuptools==66.0.0
|
48 |
+
stable-baselines3==1.7.0
|
49 |
+
streamlit==1.22.0
|
50 |
+
tenacity==8.2.2
|
51 |
+
toml==0.10.2
|
52 |
+
toolz==0.12.0
|
53 |
+
torch==1.13.1
|
54 |
+
tornado==6.2
|
55 |
+
typing_extensions==4.4.0
|
56 |
+
tzdata==2022.7
|
57 |
+
tzlocal==4.2
|
58 |
+
urllib3==1.26.16
|
59 |
+
validators==0.20.0
|
60 |
+
zipp==3.11.0
|