From 022f21d22a76b88d7bf7fa86cd68e021fe201cc5 Mon Sep 17 00:00:00 2001 From: Wouter de Winter Date: Mon, 21 Jan 2019 11:38:10 +0100 Subject: [PATCH 01/15] fix conda deps / add gym --- environment.yml | 24 ++++++++++++------------ 1 file changed, 12 insertions(+), 12 deletions(-) diff --git a/environment.yml b/environment.yml index b7e7337..fb04c60 100644 --- a/environment.yml +++ b/environment.yml @@ -3,16 +3,16 @@ channels: - cogsci - defaults dependencies: - - python=3.6 - - pandas - - ipykernel - - nb_conda - - jupyter - - matplotlib - - scikit-learn - - scikit-image - - tensorflow - - keras + - python=3.6.8 + - pandas=0.23.4 + - ipykernel=5.1.0 + - nb_conda=2.2.1 + - jupyter=1.0.0 + - matplotlib=3.0.2 + - scikit-learn=0.20.2 + - scikit-image=0.14.1 + - keras=2.2.4 - pip: - - pygame - - PyGeodesy \ No newline at end of file + - pygame==1.9.4 + - PyGeodesy==18.10.29 + - gym==0.10.9 \ No newline at end of file From dccdd77434ce54cbf43b91b3754943727df1e5a0 Mon Sep 17 00:00:00 2001 From: Wouter de Winter Date: Tue, 22 Jan 2019 10:50:26 +0100 Subject: [PATCH 02/15] fix default graph error for different thread --- src/race_simulator.py | 11 ++++++++--- 1 file changed, 8 insertions(+), 3 deletions(-) diff --git a/src/race_simulator.py b/src/race_simulator.py index 87e744e..55a2e36 100644 --- a/src/race_simulator.py +++ b/src/race_simulator.py @@ -1,6 +1,7 @@ import pygame import pandas as pd import threading +import tensorflow as tf from drawers.race_drawer import RaceDrawer from settings import Settings @@ -9,16 +10,20 @@ class RaceUpdateThread (threading.Thread): """Thread for updating the steering strategy""" - def __init__(self, strategies): + def __init__(self, strategies, graph): threading.Thread.__init__(self) self._strategies = strategies self._clock = pygame.time.Clock() + self._graph = graph def run(self): while 1: for strategy in self._strategies: + # update steering strategy - strategy.update() + # need to set default graph to enable keras models to run in a different thread + with self._graph.as_default(): + strategy.update() # update strategy with current fps fps = self._clock.get_fps() @@ -57,7 +62,7 @@ def run(self): self._drawer.autoscale(self._env.get_buoys()) # start thread for steering strategy - thread = RaceUpdateThread(self._strategies) + thread = RaceUpdateThread(self._strategies, tf.get_default_graph()) thread.daemon = True thread.start() From 4a4818bf9fe3e7539c0f65750149a78ddbc16526 Mon Sep 17 00:00:00 2001 From: Wouter de Winter Date: Tue, 22 Jan 2019 19:41:34 +0100 Subject: [PATCH 03/15] first attempt on RL --- environment.yml | 3 +- src/boat.py | 4 ++ src/gym_sail/__init__.py | 6 ++ src/gym_sail/envs/__init__.py | 1 + src/gym_sail/envs/sail_env.py | 75 +++++++++++++++++++++ src/keras_rl.py | 56 ++++++++++++++++ src/simulators/rl.py | 122 ++++++++++++++++++++++++++++++++++ src/start_rl.py | 24 +++++++ 8 files changed, 290 insertions(+), 1 deletion(-) create mode 100644 src/gym_sail/__init__.py create mode 100644 src/gym_sail/envs/__init__.py create mode 100644 src/gym_sail/envs/sail_env.py create mode 100644 src/keras_rl.py create mode 100644 src/simulators/rl.py create mode 100644 src/start_rl.py diff --git a/environment.yml b/environment.yml index fb04c60..0af3a93 100644 --- a/environment.yml +++ b/environment.yml @@ -15,4 +15,5 @@ dependencies: - pip: - pygame==1.9.4 - PyGeodesy==18.10.29 - - gym==0.10.9 \ No newline at end of file + - gym==0.10.9 + - keras-rl==0.4.2 \ No newline at end of file diff --git a/src/boat.py b/src/boat.py index 7780276..03b590d 100644 --- a/src/boat.py +++ b/src/boat.py @@ -97,6 +97,10 @@ def calculate_speed(self): speed += 1 return speed + def reset_rudder(self): + self.target_rudder_angle = 0 + self.rudder_angle = 0 + def move(self): """Simulates or fetches movement of boat""" diff --git a/src/gym_sail/__init__.py b/src/gym_sail/__init__.py new file mode 100644 index 0000000..2155497 --- /dev/null +++ b/src/gym_sail/__init__.py @@ -0,0 +1,6 @@ +from gym.envs.registration import register + +register( + id='sail-v0', + entry_point='gym_sail.envs:SailEnv', +) \ No newline at end of file diff --git a/src/gym_sail/envs/__init__.py b/src/gym_sail/envs/__init__.py new file mode 100644 index 0000000..f8c0532 --- /dev/null +++ b/src/gym_sail/envs/__init__.py @@ -0,0 +1 @@ +from gym_sail.envs.sail_env import SailEnv \ No newline at end of file diff --git a/src/gym_sail/envs/sail_env.py b/src/gym_sail/envs/sail_env.py new file mode 100644 index 0000000..a752b85 --- /dev/null +++ b/src/gym_sail/envs/sail_env.py @@ -0,0 +1,75 @@ +import gym +from gym import error, spaces, utils +from gym.utils import seeding +import numpy as np +import os + +from simulators.rl import Simulator +from boat import * +from environment import Environment +from polar import Polar + + +class SailEnv(gym.Env): + metadata = {'render.modes': ['human']} + + def __init__(self): + self.action_space = spaces.Discrete(3) + + self.observation_space = spaces.Box(low=-180, high=180, shape=(1,)) + + self.seed() + self.observation = None + + # start simulator + polar = Polar(os.path.join('..', 'data', 'polars', 'first-27.csv')) + self._env = Environment() + self._boat = SimBoat(self._env, polar=polar) + self._sim = Simulator(self._boat, self._env) + + self.reset() + + self._step = 0 + + def render(self, mode='human', close=False): + self._sim.render() + + def seed(self, seed=None): + self.np_random, seed = seeding.np_random(seed) + return [seed] + + def step(self, action): + assert self.action_space.contains(action) + print("action " + str(action)) + if action == 0: + # do nothing + pass + + elif action == 1: + # steer left + self._boat.steer(-1) + + elif action == 2: + # steer right + self._boat.steer(1) + + reward = 180 - abs(self._boat.get_course_error()) + + self.observation = self._boat.get_course_error() + + self._step += 1 + done = False + if self._step > 100: + self._step = 0 + done = True + + print(self._step) + + return self.observation, reward, done, {"debug": 123} + + def reset(self): + # self.observation = 0 + # return self.observation + print("resetting") + self._boat.reset_rudder() + return self._boat.get_course_error() diff --git a/src/keras_rl.py b/src/keras_rl.py new file mode 100644 index 0000000..a5acd47 --- /dev/null +++ b/src/keras_rl.py @@ -0,0 +1,56 @@ +import numpy as np +import gym + +from keras.models import Sequential +from keras.layers import Dense, Activation, Flatten +from keras.optimizers import Adam + +from rl.agents.dqn import DQNAgent +from rl.policy import BoltzmannQPolicy +from rl.memory import SequentialMemory + +import gym_sail + +#ENV_NAME = 'CartPole-v0' +ENV_NAME = 'sail-v0' + + +# Get the environment and extract the number of actions. +env = gym.make(ENV_NAME) +np.random.seed(123) +env.seed(123) +nb_actions = env.action_space.n + +# Next, we build a very simple model. +model = Sequential() +#model.add(Flatten(input_shape=(1,) + env.observation_space.shape)) +#model.add(Flatten(input_shape=(1,))) +model.add(Dense(16, input_shape=(1,))) +# model.add(Dense(16)) +model.add(Activation('relu')) +model.add(Dense(16)) +# model.add(Activation('relu')) +# model.add(Dense(16)) +model.add(Activation('relu')) +model.add(Dense(nb_actions)) +model.add(Activation('softmax')) +print(model.summary()) + +# Finally, we configure and compile our agent. You can use every built-in Keras optimizer and +# even the metrics! +memory = SequentialMemory(limit=50000, window_length=1) +policy = BoltzmannQPolicy() +dqn = DQNAgent(model=model, nb_actions=nb_actions, memory=memory, nb_steps_warmup=10, + target_model_update=1e-2, policy=policy) +dqn.compile(Adam(lr=1e-3), metrics=['mae']) + +# Okay, now it's time to learn something! We visualize the training here for show, but this +# slows down training quite a lot. You can always safely abort the training prematurely using +# Ctrl + C. +dqn.fit(env, nb_steps=50000, visualize=True, verbose=2) + +# After training is done, we save the final weights. +dqn.save_weights('dqn_{}_weights.h5f'.format(ENV_NAME), overwrite=True) + +# Finally, evaluate our algorithm for 5 episodes. +dqn.test(env, nb_episodes=5, visualize=True) \ No newline at end of file diff --git a/src/simulators/rl.py b/src/simulators/rl.py new file mode 100644 index 0000000..0041ec8 --- /dev/null +++ b/src/simulators/rl.py @@ -0,0 +1,122 @@ +import os, pygame +import datetime +import time + +from boat import Boat +from environment import Environment +from settings import Settings +from drawers.sim_drawer import SimDrawer + + +class Simulator: + """Single boat simulator""" + + SIZE = 800, 600 + BG_COLOR = 0, 0, 255 + TEXT_COLOR = 255, 255, 255 + + def __init__(self, boat: Boat, env: Environment, shuffle_interval=10): + self._boat = boat + self._env = env + self._shuffle_interval = shuffle_interval + + pygame.init() + pygame.font.init() + + self._font = pygame.font.SysFont('Arial', 30) + self._smallfont = pygame.font.SysFont('Arial', 20) + self._screen = pygame.display.set_mode(self.SIZE) + self._drawer = SimDrawer(self._screen) + self._clock = pygame.time.Clock() + + self._shuffle_time = time.time() + self._shuffle_interval + + # initial shuffle + self._env.shuffle() + self._boat.shuffle() + + def write_text(self, text, row): + pos = 500, 30 + (row * 30) + textsurface = self._font.render(text, True, self.TEXT_COLOR) + self._screen.blit(textsurface, pos) + + def render(self): + # update boat and environment + self._env.update() + self._boat.update() + + # redraw objects + self._screen.fill(self.BG_COLOR) + self._drawer.draw_boat(self._boat) + self._drawer.draw_env(self._env) + + # calculate mean of absolute course error + if self._boat.history.shape[0] > 0: + mae = self._boat.history.course_error.abs().mean() + else: + mae = 0 + + self.write_text("Boat angle: %.1f°" % self._boat.boat_angle, 0) + self.write_text("Target angle: %.1f°" % self._boat.target_angle, 1) + self.write_text("Current deviation: %.1f°" % self._boat.get_course_error(), 2) + self.write_text("Boat heel: %.1f°" % self._boat.boat_heel, 3) + self.write_text("Rudder angle: %.1f°" % self._boat.rudder_angle, 4) + self.write_text("Boat speed: %.1f knots" % self._boat.speed, 5) + self.write_text("Angle of attack: %.1f°" % self._boat.get_angle_of_attack(), 6) + self.write_text("Wind direction: %.1f°" % self._env.wind_direction, 8) + self.write_text("Wind speed: %.1f knots" % self._env.wind_speed, 9) + self.write_text("MAE: %.1f°" % mae, 11) + + textsurface = self._smallfont.render( + "Press keys to change: 1/2 for target angle, 3/4 for wind direction, 5/6 for wind speed, s to change strategy, q to quit", True, self.TEXT_COLOR) + self._screen.blit(textsurface, (20, 565)) + + # display new frame + pygame.display.flip() + + # shuffle once in a while + if self._shuffle_interval and time.time() > self._shuffle_time: + self._env.shuffle() + self._boat.shuffle() + self._shuffle_time += self._shuffle_interval + + # check key events + for event in pygame.event.get(): + if event.type == pygame.KEYDOWN: + + # save log and quit + if event.key == pygame.K_q: + date = datetime.datetime.strftime(datetime.datetime.now(), '%Y%m%d_%H%M') + filename = os.path.join('data', 'logs', 'history_%s.csv' % date) + self._boat.history.to_csv(filename) + print("Wrote datalog to %s" % filename) + exit() + + # check for pressed keys (not the same as key events) + pressed = pygame.key.get_pressed() + + # change wind direction + if pressed[pygame.K_3]: + self._env.change_wind_direction(-1) + if pressed[pygame.K_4]: + self._env.change_wind_direction(1) + + # change wind direction + if pressed[pygame.K_5]: + self._env.change_wind_speed(-1) + if pressed[pygame.K_6]: + self._env.change_wind_speed(1) + + # change target angle + if pressed[pygame.K_1]: + self._boat.set_target_angle(self._boat.target_angle - 3) + if pressed[pygame.K_2]: + self._boat.set_target_angle(self._boat.target_angle + 3) + + # sleep for the remainder of this frame + self._clock.tick(Settings.DRAW_FPS) + fps = self._clock.get_fps() + + # update boat with current fps + if fps > 0: + self._boat.set_draw_fps(fps) diff --git a/src/start_rl.py b/src/start_rl.py new file mode 100644 index 0000000..3056e0f --- /dev/null +++ b/src/start_rl.py @@ -0,0 +1,24 @@ +import gym +import time + +# # Will be supported in future releases +# from gym.envs import mujoco +# +# mujoco.AntEnv + +import gym_sail + +env = gym.make('sail-v0') + +for i_episode in range(20): + observation = env.reset() + for t in range(100): + env.render() + action = env.action_space.sample() + observation, reward, done, info = env.step(action) + print(observation, reward, action) + if done: + print("Episode finished after {} timesteps".format(t+1)) + break + + #time.sleep(1) From d9d203362bf0edf67da4a2bb8bba63528e587ef8 Mon Sep 17 00:00:00 2001 From: Wouter de Winter Date: Tue, 22 Jan 2019 20:24:08 +0100 Subject: [PATCH 04/15] multi var input --- src/gym_sail/envs/sail_env.py | 14 +++++++------- src/keras_rl.py | 7 ++++--- src/simulators/rl.py | 20 ++++++++++---------- 3 files changed, 21 insertions(+), 20 deletions(-) diff --git a/src/gym_sail/envs/sail_env.py b/src/gym_sail/envs/sail_env.py index a752b85..13fcc66 100644 --- a/src/gym_sail/envs/sail_env.py +++ b/src/gym_sail/envs/sail_env.py @@ -16,7 +16,8 @@ class SailEnv(gym.Env): def __init__(self): self.action_space = spaces.Discrete(3) - self.observation_space = spaces.Box(low=-180, high=180, shape=(1,)) + high = np.array([-180, -30]) + self.observation_space = spaces.Box(low=-high, high=high, dtype=np.float32) self.seed() self.observation = None @@ -40,7 +41,6 @@ def seed(self, seed=None): def step(self, action): assert self.action_space.contains(action) - print("action " + str(action)) if action == 0: # do nothing pass @@ -55,16 +55,14 @@ def step(self, action): reward = 180 - abs(self._boat.get_course_error()) - self.observation = self._boat.get_course_error() + self.observation = self._boat.get_course_error(), self._boat.rudder_angle self._step += 1 done = False - if self._step > 100: + if self._step > 300: self._step = 0 done = True - print(self._step) - return self.observation, reward, done, {"debug": 123} def reset(self): @@ -72,4 +70,6 @@ def reset(self): # return self.observation print("resetting") self._boat.reset_rudder() - return self._boat.get_course_error() + self._env.shuffle() + self._boat.shuffle() + return self._boat.get_course_error(), self._boat.rudder_angle diff --git a/src/keras_rl.py b/src/keras_rl.py index a5acd47..a77db24 100644 --- a/src/keras_rl.py +++ b/src/keras_rl.py @@ -23,10 +23,11 @@ # Next, we build a very simple model. model = Sequential() -#model.add(Flatten(input_shape=(1,) + env.observation_space.shape)) +model.add(Flatten(input_shape=(1,) + env.observation_space.shape)) #model.add(Flatten(input_shape=(1,))) -model.add(Dense(16, input_shape=(1,))) -# model.add(Dense(16)) +#model.add(Dense(16, input_shape=(1,) + env.observation_space.shape)) + +model.add(Dense(16)) model.add(Activation('relu')) model.add(Dense(16)) # model.add(Activation('relu')) diff --git a/src/simulators/rl.py b/src/simulators/rl.py index 0041ec8..0c7ce0a 100644 --- a/src/simulators/rl.py +++ b/src/simulators/rl.py @@ -75,10 +75,10 @@ def render(self): pygame.display.flip() # shuffle once in a while - if self._shuffle_interval and time.time() > self._shuffle_time: - self._env.shuffle() - self._boat.shuffle() - self._shuffle_time += self._shuffle_interval + # if self._shuffle_interval and time.time() > self._shuffle_time: + # self._env.shuffle() + # self._boat.shuffle() + # self._shuffle_time += self._shuffle_interval # check key events for event in pygame.event.get(): @@ -114,9 +114,9 @@ def render(self): self._boat.set_target_angle(self._boat.target_angle + 3) # sleep for the remainder of this frame - self._clock.tick(Settings.DRAW_FPS) - fps = self._clock.get_fps() - - # update boat with current fps - if fps > 0: - self._boat.set_draw_fps(fps) + # self._clock.tick(Settings.DRAW_FPS) + # fps = self._clock.get_fps() + # + # # update boat with current fps + # if fps > 0: + # self._boat.set_draw_fps(fps) From d5b2465138ac3295c5bf6a56fa05f10c4aa20839 Mon Sep 17 00:00:00 2001 From: Wouter de Winter Date: Tue, 22 Jan 2019 21:21:22 +0100 Subject: [PATCH 05/15] experiments --- src/gym_sail/envs/sail_env.py | 16 ++++++++++++---- src/keras_rl.py | 4 ++++ 2 files changed, 16 insertions(+), 4 deletions(-) diff --git a/src/gym_sail/envs/sail_env.py b/src/gym_sail/envs/sail_env.py index 13fcc66..52fc037 100644 --- a/src/gym_sail/envs/sail_env.py +++ b/src/gym_sail/envs/sail_env.py @@ -32,11 +32,13 @@ def __init__(self): self._step = 0 + self._last_course_error = 0 + def render(self, mode='human', close=False): self._sim.render() def seed(self, seed=None): - self.np_random, seed = seeding.np_random(seed) + seed = seeding.np_random(seed) return [seed] def step(self, action): @@ -53,10 +55,18 @@ def step(self, action): # steer right self._boat.steer(1) - reward = 180 - abs(self._boat.get_course_error()) + # change in course error + # delta = abs(self._last_course_error - self._boat.get_course_error()) + # print (delta) + + reward = ((180 - abs(self._boat.get_course_error())) / 180) + print(reward) self.observation = self._boat.get_course_error(), self._boat.rudder_angle + # save couse error + self._last_course_error = self._boat.get_course_error() + self._step += 1 done = False if self._step > 300: @@ -66,8 +76,6 @@ def step(self, action): return self.observation, reward, done, {"debug": 123} def reset(self): - # self.observation = 0 - # return self.observation print("resetting") self._boat.reset_rudder() self._env.shuffle() diff --git a/src/keras_rl.py b/src/keras_rl.py index a77db24..e7da3d2 100644 --- a/src/keras_rl.py +++ b/src/keras_rl.py @@ -1,5 +1,6 @@ import numpy as np import gym +import os from keras.models import Sequential from keras.layers import Dense, Activation, Flatten @@ -9,6 +10,9 @@ from rl.policy import BoltzmannQPolicy from rl.memory import SequentialMemory +# fixed crash that occures after a while, see: https://github.com/openai/spinningup/issues/16 +os.environ['KMP_DUPLICATE_LIB_OK'] = 'True' + import gym_sail #ENV_NAME = 'CartPole-v0' From 73342a9bf2ed84e4bd9422467d607ef4da08e91a Mon Sep 17 00:00:00 2001 From: Wouter de Winter Date: Wed, 23 Jan 2019 19:53:13 +0100 Subject: [PATCH 06/15] experiments --- src/gym_sail/envs/sail_env.py | 19 ++++++++++++------- src/keras_rl.py | 14 +++++--------- src/simulators/rl.py | 3 --- 3 files changed, 17 insertions(+), 19 deletions(-) diff --git a/src/gym_sail/envs/sail_env.py b/src/gym_sail/envs/sail_env.py index 52fc037..5ab0783 100644 --- a/src/gym_sail/envs/sail_env.py +++ b/src/gym_sail/envs/sail_env.py @@ -16,7 +16,7 @@ class SailEnv(gym.Env): def __init__(self): self.action_space = spaces.Discrete(3) - high = np.array([-180, -30]) + high = np.array([180, 30]) self.observation_space = spaces.Box(low=-high, high=high, dtype=np.float32) self.seed() @@ -43,25 +43,30 @@ def seed(self, seed=None): def step(self, action): assert self.action_space.contains(action) - if action == 0: - # do nothing - pass - elif action == 1: + if action == 0: # steer left self._boat.steer(-1) + elif action == 1: + # do nothing + pass + elif action == 2: # steer right self._boat.steer(1) + # update boat and environment + self._env.update() + self._boat.update() + # change in course error # delta = abs(self._last_course_error - self._boat.get_course_error()) # print (delta) - reward = ((180 - abs(self._boat.get_course_error())) / 180) + reward = -abs(self._boat.get_course_error() / 180) - print(reward) + # print(reward) self.observation = self._boat.get_course_error(), self._boat.rudder_angle # save couse error diff --git a/src/keras_rl.py b/src/keras_rl.py index e7da3d2..235afa0 100644 --- a/src/keras_rl.py +++ b/src/keras_rl.py @@ -15,7 +15,6 @@ import gym_sail -#ENV_NAME = 'CartPole-v0' ENV_NAME = 'sail-v0' @@ -28,17 +27,14 @@ # Next, we build a very simple model. model = Sequential() model.add(Flatten(input_shape=(1,) + env.observation_space.shape)) -#model.add(Flatten(input_shape=(1,))) -#model.add(Dense(16, input_shape=(1,) + env.observation_space.shape)) - model.add(Dense(16)) model.add(Activation('relu')) model.add(Dense(16)) -# model.add(Activation('relu')) -# model.add(Dense(16)) +model.add(Activation('relu')) +model.add(Dense(16)) model.add(Activation('relu')) model.add(Dense(nb_actions)) -model.add(Activation('softmax')) +model.add(Activation('linear')) print(model.summary()) # Finally, we configure and compile our agent. You can use every built-in Keras optimizer and @@ -52,10 +48,10 @@ # Okay, now it's time to learn something! We visualize the training here for show, but this # slows down training quite a lot. You can always safely abort the training prematurely using # Ctrl + C. -dqn.fit(env, nb_steps=50000, visualize=True, verbose=2) +dqn.fit(env, nb_steps=50000, visualize=False, verbose=2) # After training is done, we save the final weights. dqn.save_weights('dqn_{}_weights.h5f'.format(ENV_NAME), overwrite=True) # Finally, evaluate our algorithm for 5 episodes. -dqn.test(env, nb_episodes=5, visualize=True) \ No newline at end of file +dqn.test(env, nb_episodes=500, visualize=True) \ No newline at end of file diff --git a/src/simulators/rl.py b/src/simulators/rl.py index 0c7ce0a..7f33cd8 100644 --- a/src/simulators/rl.py +++ b/src/simulators/rl.py @@ -41,9 +41,6 @@ def write_text(self, text, row): self._screen.blit(textsurface, pos) def render(self): - # update boat and environment - self._env.update() - self._boat.update() # redraw objects self._screen.fill(self.BG_COLOR) From 03902497f467ada4116f360acb171252ac782fd4 Mon Sep 17 00:00:00 2001 From: Wouter de Winter Date: Thu, 24 Jan 2019 20:20:02 +0100 Subject: [PATCH 07/15] continuous env + model --- src/gym_sail/__init__.py | 7 ++- src/gym_sail/envs/__init__.py | 3 +- src/gym_sail/envs/sail_env.py | 20 +++--- src/gym_sail/envs/sail_env_continuous.py | 80 ++++++++++++++++++++++++ src/keras_rl.py | 10 +-- src/keras_rl_cont.py | 70 +++++++++++++++++++++ 6 files changed, 175 insertions(+), 15 deletions(-) create mode 100644 src/gym_sail/envs/sail_env_continuous.py create mode 100644 src/keras_rl_cont.py diff --git a/src/gym_sail/__init__.py b/src/gym_sail/__init__.py index 2155497..ec32a9f 100644 --- a/src/gym_sail/__init__.py +++ b/src/gym_sail/__init__.py @@ -3,4 +3,9 @@ register( id='sail-v0', entry_point='gym_sail.envs:SailEnv', -) \ No newline at end of file +) + +register( + id='sail-continuous-v0', + entry_point='gym_sail.envs:SailEnvContinuous', +) diff --git a/src/gym_sail/envs/__init__.py b/src/gym_sail/envs/__init__.py index f8c0532..41392ad 100644 --- a/src/gym_sail/envs/__init__.py +++ b/src/gym_sail/envs/__init__.py @@ -1 +1,2 @@ -from gym_sail.envs.sail_env import SailEnv \ No newline at end of file +from gym_sail.envs.sail_env import SailEnv +from gym_sail.envs.sail_env_continuous import SailEnvContinuous \ No newline at end of file diff --git a/src/gym_sail/envs/sail_env.py b/src/gym_sail/envs/sail_env.py index 5ab0783..7e4d3f4 100644 --- a/src/gym_sail/envs/sail_env.py +++ b/src/gym_sail/envs/sail_env.py @@ -16,8 +16,8 @@ class SailEnv(gym.Env): def __init__(self): self.action_space = spaces.Discrete(3) - high = np.array([180, 30]) - self.observation_space = spaces.Box(low=-high, high=high, dtype=np.float32) + #high = np.array([180, 30, 180]) + self.observation_space = spaces.Box(low=-1, high=1, shape=(2,), dtype=np.float32) self.seed() self.observation = None @@ -66,11 +66,8 @@ def step(self, action): reward = -abs(self._boat.get_course_error() / 180) - # print(reward) - self.observation = self._boat.get_course_error(), self._boat.rudder_angle - # save couse error - self._last_course_error = self._boat.get_course_error() + #self._last_course_error = self._boat.get_course_error() self._step += 1 done = False @@ -78,11 +75,18 @@ def step(self, action): self._step = 0 done = True - return self.observation, reward, done, {"debug": 123} + return self.get_observation(), reward, done, {"debug": 123} + + def get_observation(self): + return ( + self._boat.get_course_error() / 180, + self._boat.rudder_angle / 30, + #self._boat.get_angle_of_attack() + ) def reset(self): print("resetting") self._boat.reset_rudder() self._env.shuffle() self._boat.shuffle() - return self._boat.get_course_error(), self._boat.rudder_angle + return self.get_observation() diff --git a/src/gym_sail/envs/sail_env_continuous.py b/src/gym_sail/envs/sail_env_continuous.py new file mode 100644 index 0000000..5db8a0e --- /dev/null +++ b/src/gym_sail/envs/sail_env_continuous.py @@ -0,0 +1,80 @@ +import gym +from gym import error, spaces, utils +from gym.utils import seeding +import numpy as np +import os + +from simulators.rl import Simulator +from boat import * +from environment import Environment +from polar import Polar + + +class SailEnvContinuous(gym.Env): + metadata = {'render.modes': ['human']} + + def __init__(self): + self.action_space = spaces.Box(low=-30, high=30, shape=(1,), dtype=np.float32) + self.observation_space = spaces.Box(low=-1, high=1, shape=(2,), dtype=np.float32) + + self.seed() + self.observation = None + + # start simulator + polar = Polar(os.path.join('..', 'data', 'polars', 'first-27.csv')) + self._env = Environment() + self._boat = SimBoat(self._env, polar=polar) + self._sim = Simulator(self._boat, self._env) + + self.reset() + + self._step = 0 + + self._last_course_error = 0 + + def render(self, mode='human', close=False): + self._sim.render() + + def seed(self, seed=None): + seed = seeding.np_random(seed) + return [seed] + + def step(self, action): + assert self.action_space.contains(action) + action = action * 30 + self._boat.set_target_rudder_angle(int(action)) + + # update boat and environment + self._env.update() + self._boat.update() + + # change in course error + # delta = abs(self._last_course_error - self._boat.get_course_error()) + # print (delta) + + reward = -abs(self._boat.get_course_error() / 180) + + # save couse error + #self._last_course_error = self._boat.get_course_error() + + self._step += 1 + done = False + if self._step > 300: + self._step = 0 + done = True + + return self.get_observation(), reward, done, {"debug": 123} + + def get_observation(self): + return ( + self._boat.get_course_error() / 180, + self._boat.rudder_angle / 30, + #self._boat.get_angle_of_attack() + ) + + def reset(self): + print("resetting") + self._boat.reset_rudder() + self._env.shuffle() + self._boat.shuffle() + return self.get_observation() diff --git a/src/keras_rl.py b/src/keras_rl.py index 235afa0..4480db5 100644 --- a/src/keras_rl.py +++ b/src/keras_rl.py @@ -26,7 +26,7 @@ # Next, we build a very simple model. model = Sequential() -model.add(Flatten(input_shape=(1,) + env.observation_space.shape)) +model.add(Flatten(input_shape=(3,) + env.observation_space.shape)) model.add(Dense(16)) model.add(Activation('relu')) model.add(Dense(16)) @@ -39,16 +39,16 @@ # Finally, we configure and compile our agent. You can use every built-in Keras optimizer and # even the metrics! -memory = SequentialMemory(limit=50000, window_length=1) +memory = SequentialMemory(limit=50000, window_length=3) policy = BoltzmannQPolicy() -dqn = DQNAgent(model=model, nb_actions=nb_actions, memory=memory, nb_steps_warmup=10, +dqn = DQNAgent(model=model, nb_actions=nb_actions, memory=memory, nb_steps_warmup=50, target_model_update=1e-2, policy=policy) -dqn.compile(Adam(lr=1e-3), metrics=['mae']) +dqn.compile(Adam(lr=1e-4), metrics=['mae']) # Okay, now it's time to learn something! We visualize the training here for show, but this # slows down training quite a lot. You can always safely abort the training prematurely using # Ctrl + C. -dqn.fit(env, nb_steps=50000, visualize=False, verbose=2) +dqn.fit(env, nb_steps=1000000, visualize=False, verbose=2) # After training is done, we save the final weights. dqn.save_weights('dqn_{}_weights.h5f'.format(ENV_NAME), overwrite=True) diff --git a/src/keras_rl_cont.py b/src/keras_rl_cont.py new file mode 100644 index 0000000..930ed8b --- /dev/null +++ b/src/keras_rl_cont.py @@ -0,0 +1,70 @@ +import numpy as np +import gym + +from keras.models import Sequential, Model +from keras.layers import Dense, Activation, Flatten, Input, Concatenate +from keras.optimizers import Adam + +from rl.agents import DDPGAgent +from rl.memory import SequentialMemory +from rl.random import OrnsteinUhlenbeckProcess + +import gym_sail + +ENV_NAME = 'sail-continuous-v0' + + +# Get the environment and extract the number of actions. +env = gym.make(ENV_NAME) +np.random.seed(123) +env.seed(123) +assert len(env.action_space.shape) == 1 +nb_actions = env.action_space.shape[0] + +# Next, we build a very simple model. +actor = Sequential() +actor.add(Flatten(input_shape=(1,) + env.observation_space.shape)) +actor.add(Dense(16)) +actor.add(Activation('relu')) +actor.add(Dense(16)) +actor.add(Activation('relu')) +actor.add(Dense(16)) +actor.add(Activation('relu')) +actor.add(Dense(nb_actions)) +actor.add(Activation('linear')) +print(actor.summary()) + +action_input = Input(shape=(nb_actions,), name='action_input') +observation_input = Input(shape=(1,) + env.observation_space.shape, name='observation_input') +flattened_observation = Flatten()(observation_input) +x = Concatenate()([action_input, flattened_observation]) +x = Dense(32)(x) +x = Activation('relu')(x) +x = Dense(32)(x) +x = Activation('relu')(x) +x = Dense(32)(x) +x = Activation('relu')(x) +x = Dense(1)(x) +x = Activation('linear')(x) +critic = Model(inputs=[action_input, observation_input], outputs=x) +print(critic.summary()) + +# Finally, we configure and compile our agent. You can use every built-in Keras optimizer and +# even the metrics! +memory = SequentialMemory(limit=100000, window_length=1) +random_process = OrnsteinUhlenbeckProcess(size=nb_actions, theta=.15, mu=0., sigma=.3) +agent = DDPGAgent(nb_actions=nb_actions, actor=actor, critic=critic, critic_action_input=action_input, + memory=memory, nb_steps_warmup_critic=100, nb_steps_warmup_actor=100, + random_process=random_process, gamma=.99, target_model_update=1e-3) +agent.compile(Adam(lr=.001, clipnorm=1.), metrics=['mae']) + +# Okay, now it's time to learn something! We visualize the training here for show, but this +# slows down training quite a lot. You can always safely abort the training prematurely using +# Ctrl + C. +agent.fit(env, nb_steps=50000, visualize=False, verbose=1, nb_max_episode_steps=200) + +# After training is done, we save the final weights. +agent.save_weights('ddpg_{}_weights.h5f'.format(ENV_NAME), overwrite=True) + +# Finally, evaluate our algorithm for 5 episodes. +agent.test(env, nb_episodes=5, visualize=True, nb_max_episode_steps=200) \ No newline at end of file From 13b1d08bb693b54089dbec7bcf1a056007be0936 Mon Sep 17 00:00:00 2001 From: Wouter de Winter Date: Fri, 25 Jan 2019 09:39:45 +0100 Subject: [PATCH 08/15] refactored drawers --- src/boat.py | 31 +++--- src/drawers/sim_drawer.py | 54 +++++++++- src/gym_sail/envs/sail_env.py | 24 ++--- src/gym_sail/envs/sail_env_continuous.py | 23 ++--- src/simulator.py | 56 ++--------- src/simulators/rl.py | 119 ----------------------- 6 files changed, 98 insertions(+), 209 deletions(-) delete mode 100644 src/simulators/rl.py diff --git a/src/boat.py b/src/boat.py index 03b590d..cee0384 100644 --- a/src/boat.py +++ b/src/boat.py @@ -28,7 +28,10 @@ class Boat: # distance in meters from waypoint to skip to next waypoint DIST_NEXT_WAYPOINT = 2 - def __init__(self, env, random_color=False, name='no-name'): + def __init__(self, env, random_color=False, name='no-name', keep_log=True): + self._keep_log = keep_log + self._name = name + self.rudder_angle = 0. self.target_rudder_angle = 0. self.boat_angle = 0. @@ -40,7 +43,6 @@ def __init__(self, env, random_color=False, name='no-name'): self._env = env self.history = pd.DataFrame() self.windspeed_shuffle = True - self._name = name self._position = (52.3721693, 5.0750607) self._waypoint = None self._bearing = 0. @@ -146,18 +148,19 @@ def update(self): self.nav() # save history - self.history = self.history.append([{ - 'datetime': dt.now(), - 'boat_angle': self.boat_angle + np.random.normal(0, 1), - 'boat_heel': self.boat_heel if np.random.uniform(0, 1) < 0.99 else np.nan, - 'boat_speed': self.speed + np.random.normal(0, 0.25), - 'target_angle': self.target_angle if np.random.uniform(0, 1) < 0.99 else np.nan, - 'course_error': self.get_course_error(), - 'rudder_angle': self.rudder_angle, - 'wind_direction': self._env.wind_direction, - 'wind_speed': self._env.wind_speed if np.random.uniform(0, 1) < 0.99 else np.random.randint(100, 150), - 'angle_of_attack': self.get_angle_of_attack() - }]) + if self._keep_log: + self.history = self.history.append([{ + 'datetime': dt.now(), + 'boat_angle': self.boat_angle + np.random.normal(0, 1), + 'boat_heel': self.boat_heel if np.random.uniform(0, 1) < 0.99 else np.nan, + 'boat_speed': self.speed + np.random.normal(0, 0.25), + 'target_angle': self.target_angle if np.random.uniform(0, 1) < 0.99 else np.nan, + 'course_error': self.get_course_error(), + 'rudder_angle': self.rudder_angle, + 'wind_direction': self._env.wind_direction, + 'wind_speed': self._env.wind_speed if np.random.uniform(0, 1) < 0.99 else np.random.randint(100, 150), + 'angle_of_attack': self.get_angle_of_attack() + }]) def nav(self): """ Update navigation variables and determine new course """ diff --git a/src/drawers/sim_drawer.py b/src/drawers/sim_drawer.py index bbe1a65..7bfaafb 100644 --- a/src/drawers/sim_drawer.py +++ b/src/drawers/sim_drawer.py @@ -1,4 +1,7 @@ import pygame + +from boat import Boat +from environment import Environment from tools import rotate_point, add_vector, rotate_vectors @@ -19,11 +22,21 @@ class SimDrawer: CENTER = (250, 250) - def __init__(self, screen): - self._screen = screen + TEXT_COLOR = 255, 255, 255 + SIZE = 800, 600 + BG_COLOR = 0, 0, 255 + + def __init__(self): self._offset = (0, 0) self._scale = 0 + pygame.init() + pygame.font.init() + + self._font = pygame.font.SysFont('Arial', 30) + self._smallfont = pygame.font.SysFont('Arial', 20) + self._screen = pygame.display.set_mode(self.SIZE) + def draw_boat(self, boat): # draw boat vectors = self.BOAT_SHAPE.copy() @@ -64,3 +77,40 @@ def draw_env(self, env): pygame.draw.polygon(self._screen, self.ARROW_COLOR, vectors) pygame.draw.circle(self._screen, (100, 100, 100), self.CENTER, 200, 5) + + def write_text(self, text, row): + pos = 500, 30 + (row * 30) + textsurface = self._font.render(text, True, self.TEXT_COLOR) + self._screen.blit(textsurface, pos) + + def draw_stats(self, boat, env): + # calculate mean of absolute course error + if boat.history.shape[0] > 0: + mae = boat.history.course_error.abs().mean() + else: + mae = 0 + + self.write_text("Boat angle: %.1f°" % boat.boat_angle, 0) + self.write_text("Target angle: %.1f°" % boat.target_angle, 1) + self.write_text("Current deviation: %.1f°" % boat.get_course_error(), 2) + self.write_text("Boat heel: %.1f°" % boat.boat_heel, 3) + self.write_text("Rudder angle: %.1f°" % boat.rudder_angle, 4) + self.write_text("Boat speed: %.1f knots" % boat.speed, 5) + self.write_text("Angle of attack: %.1f°" % boat.get_angle_of_attack(), 6) + self.write_text("Wind direction: %.1f°" % env.wind_direction, 8) + self.write_text("Wind speed: %.1f knots" % env.wind_speed, 9) + self.write_text("MAE: %.1f°" % mae, 11) + + textsurface = self._smallfont.render( + "Press keys to change: 1/2 for target angle, 3/4 for wind direction, 5/6 for wind speed, s to change strategy, q to quit", True, self.TEXT_COLOR) + self._screen.blit(textsurface, (20, 565)) + + def draw(self, boat: Boat, env: Environment): + # redraw objects + self._screen.fill(self.BG_COLOR) + self.draw_boat(boat) + self.draw_env(env) + self.draw_stats(boat, env) + + # display new frame + pygame.display.flip() diff --git a/src/gym_sail/envs/sail_env.py b/src/gym_sail/envs/sail_env.py index 7e4d3f4..ec4cb85 100644 --- a/src/gym_sail/envs/sail_env.py +++ b/src/gym_sail/envs/sail_env.py @@ -8,6 +8,7 @@ from boat import * from environment import Environment from polar import Polar +from drawers.sim_drawer import SimDrawer class SailEnv(gym.Env): @@ -16,7 +17,6 @@ class SailEnv(gym.Env): def __init__(self): self.action_space = spaces.Discrete(3) - #high = np.array([180, 30, 180]) self.observation_space = spaces.Box(low=-1, high=1, shape=(2,), dtype=np.float32) self.seed() @@ -25,17 +25,20 @@ def __init__(self): # start simulator polar = Polar(os.path.join('..', 'data', 'polars', 'first-27.csv')) self._env = Environment() - self._boat = SimBoat(self._env, polar=polar) - self._sim = Simulator(self._boat, self._env) + self._boat = SimBoat(self._env, polar=polar, keep_log=False) + self._drawer = SimDrawer() self.reset() - self._step = 0 - self._last_course_error = 0 - def render(self, mode='human', close=False): - self._sim.render() + self._drawer.draw(self._boat, self._env) + + # should we quit? + for event in pygame.event.get(): + if event.type == pygame.KEYDOWN: + if event.key == pygame.K_q: + exit() def seed(self, seed=None): seed = seeding.np_random(seed) @@ -60,15 +63,8 @@ def step(self, action): self._env.update() self._boat.update() - # change in course error - # delta = abs(self._last_course_error - self._boat.get_course_error()) - # print (delta) - reward = -abs(self._boat.get_course_error() / 180) - # save couse error - #self._last_course_error = self._boat.get_course_error() - self._step += 1 done = False if self._step > 300: diff --git a/src/gym_sail/envs/sail_env_continuous.py b/src/gym_sail/envs/sail_env_continuous.py index 5db8a0e..c2fe075 100644 --- a/src/gym_sail/envs/sail_env_continuous.py +++ b/src/gym_sail/envs/sail_env_continuous.py @@ -8,6 +8,7 @@ from boat import * from environment import Environment from polar import Polar +from drawers.sim_drawer import SimDrawer class SailEnvContinuous(gym.Env): @@ -23,17 +24,20 @@ def __init__(self): # start simulator polar = Polar(os.path.join('..', 'data', 'polars', 'first-27.csv')) self._env = Environment() - self._boat = SimBoat(self._env, polar=polar) - self._sim = Simulator(self._boat, self._env) + self._boat = SimBoat(self._env, polar=polar, keep_log=False) + self._drawer = SimDrawer() self.reset() - self._step = 0 - self._last_course_error = 0 - def render(self, mode='human', close=False): - self._sim.render() + self._drawer.draw(self._boat, self._env) + + # should we quit? + for event in pygame.event.get(): + if event.type == pygame.KEYDOWN: + if event.key == pygame.K_q: + exit() def seed(self, seed=None): seed = seeding.np_random(seed) @@ -48,15 +52,8 @@ def step(self, action): self._env.update() self._boat.update() - # change in course error - # delta = abs(self._last_course_error - self._boat.get_course_error()) - # print (delta) - reward = -abs(self._boat.get_course_error() / 180) - # save couse error - #self._last_course_error = self._boat.get_course_error() - self._step += 1 done = False if self._step > 300: diff --git a/src/simulator.py b/src/simulator.py index 20b47c2..1c17675 100644 --- a/src/simulator.py +++ b/src/simulator.py @@ -2,6 +2,7 @@ import datetime import time import threading +import tensorflow as tf from boat import Boat from environment import Environment @@ -13,17 +14,20 @@ class UpdateThread (threading.Thread): """Thread for updating the steering strategy""" - def __init__(self, boat: Boat, env: Environment, strategy: Base): + def __init__(self, boat: Boat, env: Environment, strategy: Base, graph): threading.Thread.__init__(self) self._boat = boat self._env = env self._strategy = strategy self._clock = pygame.time.Clock() + self._graph = graph def run(self): while 1: # update steering strategy - self._strategy.update() + # need to set default graph to enable keras models to run in a different thread + with self._graph.as_default(): + self._strategy.update() # sleep remainder of frame self._clock.tick(Settings.UPDATE_FPS) @@ -37,10 +41,6 @@ def run(self): class Simulator: """Single boat simulator""" - SIZE = 800, 600 - BG_COLOR = 0, 0, 255 - TEXT_COLOR = 255, 255, 255 - def __init__(self, boat: Boat, env: Environment, strategies: list, shuffle_interval=10): self._boat = boat self._env = env @@ -51,20 +51,9 @@ def __init__(self, boat: Boat, env: Environment, strategies: list, shuffle_inter self._strategy_id = 0 self._strategy = strategies[self._strategy_id] - pygame.init() - pygame.font.init() - - self._font = pygame.font.SysFont('Arial', 30) - self._smallfont = pygame.font.SysFont('Arial', 20) - self._screen = pygame.display.set_mode(self.SIZE) - self._drawer = SimDrawer(self._screen) + self._drawer = SimDrawer() self._clock = pygame.time.Clock() - def write_text(self, text, row): - pos = 500, 30 + (row * 30) - textsurface = self._font.render(text, True, self.TEXT_COLOR) - self._screen.blit(textsurface, pos) - def run(self): shuffle_time = time.time() + self._shuffle_interval @@ -73,7 +62,7 @@ def run(self): self._boat.shuffle() # start thread for steering strategy - thread = UpdateThread(self._boat, self._env, self._strategy) + thread = UpdateThread(self._boat, self._env, self._strategy, tf.get_default_graph()) thread.daemon = True thread.start() @@ -83,34 +72,7 @@ def run(self): self._boat.update() # redraw objects - self._screen.fill(self.BG_COLOR) - self._drawer.draw_boat(self._boat) - self._drawer.draw_env(self._env) - - # calculate mean of absolute course error - if self._boat.history.shape[0] > 0: - mae = self._boat.history.course_error.abs().mean() - else: - mae = 0 - - self.write_text("Boat angle: %.1f°" % self._boat.boat_angle, 0) - self.write_text("Target angle: %.1f°" % self._boat.target_angle, 1) - self.write_text("Current deviation: %.1f°" % self._boat.get_course_error(), 2) - self.write_text("Boat heel: %.1f°" % self._boat.boat_heel, 3) - self.write_text("Rudder angle: %.1f°" % self._boat.rudder_angle, 4) - self.write_text("Boat speed: %.1f knots" % self._boat.speed, 5) - self.write_text("Angle of attack: %.1f°" % self._boat.get_angle_of_attack(), 6) - self.write_text("Wind direction: %.1f°" % self._env.wind_direction, 8) - self.write_text("Wind speed: %.1f knots" % self._env.wind_speed, 9) - self.write_text("MAE: %.1f°" % mae, 11) - self.write_text("Strategy: %s" % type(self._strategy).__name__, 13) - - textsurface = self._smallfont.render( - "Press keys to change: 1/2 for target angle, 3/4 for wind direction, 5/6 for wind speed, s to change strategy, q to quit", True, self.TEXT_COLOR) - self._screen.blit(textsurface, (20, 565)) - - # display new frame - pygame.display.flip() + self._drawer.draw(self._boat, self._env) # shuffle once in a while if self._shuffle_interval and time.time() > shuffle_time: diff --git a/src/simulators/rl.py b/src/simulators/rl.py deleted file mode 100644 index 7f33cd8..0000000 --- a/src/simulators/rl.py +++ /dev/null @@ -1,119 +0,0 @@ -import os, pygame -import datetime -import time - -from boat import Boat -from environment import Environment -from settings import Settings -from drawers.sim_drawer import SimDrawer - - -class Simulator: - """Single boat simulator""" - - SIZE = 800, 600 - BG_COLOR = 0, 0, 255 - TEXT_COLOR = 255, 255, 255 - - def __init__(self, boat: Boat, env: Environment, shuffle_interval=10): - self._boat = boat - self._env = env - self._shuffle_interval = shuffle_interval - - pygame.init() - pygame.font.init() - - self._font = pygame.font.SysFont('Arial', 30) - self._smallfont = pygame.font.SysFont('Arial', 20) - self._screen = pygame.display.set_mode(self.SIZE) - self._drawer = SimDrawer(self._screen) - self._clock = pygame.time.Clock() - - self._shuffle_time = time.time() + self._shuffle_interval - - # initial shuffle - self._env.shuffle() - self._boat.shuffle() - - def write_text(self, text, row): - pos = 500, 30 + (row * 30) - textsurface = self._font.render(text, True, self.TEXT_COLOR) - self._screen.blit(textsurface, pos) - - def render(self): - - # redraw objects - self._screen.fill(self.BG_COLOR) - self._drawer.draw_boat(self._boat) - self._drawer.draw_env(self._env) - - # calculate mean of absolute course error - if self._boat.history.shape[0] > 0: - mae = self._boat.history.course_error.abs().mean() - else: - mae = 0 - - self.write_text("Boat angle: %.1f°" % self._boat.boat_angle, 0) - self.write_text("Target angle: %.1f°" % self._boat.target_angle, 1) - self.write_text("Current deviation: %.1f°" % self._boat.get_course_error(), 2) - self.write_text("Boat heel: %.1f°" % self._boat.boat_heel, 3) - self.write_text("Rudder angle: %.1f°" % self._boat.rudder_angle, 4) - self.write_text("Boat speed: %.1f knots" % self._boat.speed, 5) - self.write_text("Angle of attack: %.1f°" % self._boat.get_angle_of_attack(), 6) - self.write_text("Wind direction: %.1f°" % self._env.wind_direction, 8) - self.write_text("Wind speed: %.1f knots" % self._env.wind_speed, 9) - self.write_text("MAE: %.1f°" % mae, 11) - - textsurface = self._smallfont.render( - "Press keys to change: 1/2 for target angle, 3/4 for wind direction, 5/6 for wind speed, s to change strategy, q to quit", True, self.TEXT_COLOR) - self._screen.blit(textsurface, (20, 565)) - - # display new frame - pygame.display.flip() - - # shuffle once in a while - # if self._shuffle_interval and time.time() > self._shuffle_time: - # self._env.shuffle() - # self._boat.shuffle() - # self._shuffle_time += self._shuffle_interval - - # check key events - for event in pygame.event.get(): - if event.type == pygame.KEYDOWN: - - # save log and quit - if event.key == pygame.K_q: - date = datetime.datetime.strftime(datetime.datetime.now(), '%Y%m%d_%H%M') - filename = os.path.join('data', 'logs', 'history_%s.csv' % date) - self._boat.history.to_csv(filename) - print("Wrote datalog to %s" % filename) - exit() - - # check for pressed keys (not the same as key events) - pressed = pygame.key.get_pressed() - - # change wind direction - if pressed[pygame.K_3]: - self._env.change_wind_direction(-1) - if pressed[pygame.K_4]: - self._env.change_wind_direction(1) - - # change wind direction - if pressed[pygame.K_5]: - self._env.change_wind_speed(-1) - if pressed[pygame.K_6]: - self._env.change_wind_speed(1) - - # change target angle - if pressed[pygame.K_1]: - self._boat.set_target_angle(self._boat.target_angle - 3) - if pressed[pygame.K_2]: - self._boat.set_target_angle(self._boat.target_angle + 3) - - # sleep for the remainder of this frame - # self._clock.tick(Settings.DRAW_FPS) - # fps = self._clock.get_fps() - # - # # update boat with current fps - # if fps > 0: - # self._boat.set_draw_fps(fps) From a2b27379ab8a94201d1ccc44f749cd33adfbed7b Mon Sep 17 00:00:00 2001 From: Wouter de Winter Date: Fri, 25 Jan 2019 11:35:33 +0100 Subject: [PATCH 09/15] refactored race --- race.py | 2 +- src/drawers/race_drawer.py | 55 ++++++++++++++++++++++-- src/gym_sail/envs/sail_env.py | 1 - src/gym_sail/envs/sail_env_continuous.py | 1 - src/{ => simulators}/race_simulator.py | 47 ++------------------ 5 files changed, 57 insertions(+), 49 deletions(-) rename src/{ => simulators}/race_simulator.py (57%) diff --git a/race.py b/race.py index 1099088..3c911bc 100644 --- a/race.py +++ b/race.py @@ -6,7 +6,7 @@ # import project code from environment import Environment from polar import Polar -from race_simulator import RaceSimulator +from simulators.race_simulator import RaceSimulator from boat import SimBoat # import configuration diff --git a/src/drawers/race_drawer.py b/src/drawers/race_drawer.py index 1e8fc5a..a215c1c 100644 --- a/src/drawers/race_drawer.py +++ b/src/drawers/race_drawer.py @@ -1,5 +1,7 @@ import pygame from tools import rotate_point +from environment import Environment +import pandas as pd class RaceDrawer: @@ -19,11 +21,23 @@ class RaceDrawer: BOAT_COLOR = (255, 255, 255) BOAT_SCALE = 0.1 - def __init__(self, screen): - self._screen = screen + SIZE = 1024, 768 + BG_COLOR = 0, 0, 0 + TEXT_COLOR = 255, 255, 255 + + def __init__(self, strategies: list, env: Environment): + self._strategies = strategies + self._env = env self._offset = (0, 0) self._scale = 0 + pygame.init() + pygame.font.init() + + self._font = pygame.font.SysFont('Arial', 30) + self._smallfont = pygame.font.SysFont('Arial', 20) + self._screen = pygame.display.set_mode(self.SIZE) + def autoscale(self, buoys): """ Scale race canvas to fit all buoys """ lat, lon = zip(*buoys) @@ -98,4 +112,39 @@ def draw_buoys(self, buoys): x, y = self.translate_pos(position) pygame.draw.circle(self._screen, self.BUOY_COLOR, (x, y), 5) - + def write_text(self, text, row, color=(255, 255, 255)): + pos = 740, 30 + (row * 30) + textsurface = self._font.render(text, True, color) + self._screen.blit(textsurface, pos) + + def draw(self): + self._screen.fill(self.BG_COLOR) + self.draw_env(self._env) + self.draw_buoys(self._env.get_buoys()) + + # draw all boats + scoreboard = [] + for i, strategy in enumerate(self._strategies): + + # update boat and draw + boat = strategy.get_boat() + self.draw_boat(boat) + + # update scoreboard + scoreboard.append({ + 'name': strategy.get_name(), + 'color': boat.get_boat_color(), + 'marks_passed': boat.get_marks_passed(), + 'dtw': boat.get_distance_to_waypoint() + }) + + # show scoreboard + scoreboard = pd.DataFrame(scoreboard).sort_values(by=['marks_passed', 'dtw'], ascending=[False, True]) + i = 0 + for _, row in scoreboard.iterrows(): + text = "%s (DTW: %dm)" % (row['name'], row.dtw) + self.write_text(text, i, row.color) + i += 1 + + # display new frame + pygame.display.flip() diff --git a/src/gym_sail/envs/sail_env.py b/src/gym_sail/envs/sail_env.py index ec4cb85..6a4d43a 100644 --- a/src/gym_sail/envs/sail_env.py +++ b/src/gym_sail/envs/sail_env.py @@ -4,7 +4,6 @@ import numpy as np import os -from simulators.rl import Simulator from boat import * from environment import Environment from polar import Polar diff --git a/src/gym_sail/envs/sail_env_continuous.py b/src/gym_sail/envs/sail_env_continuous.py index c2fe075..f4dd0c5 100644 --- a/src/gym_sail/envs/sail_env_continuous.py +++ b/src/gym_sail/envs/sail_env_continuous.py @@ -4,7 +4,6 @@ import numpy as np import os -from simulators.rl import Simulator from boat import * from environment import Environment from polar import Polar diff --git a/src/race_simulator.py b/src/simulators/race_simulator.py similarity index 57% rename from src/race_simulator.py rename to src/simulators/race_simulator.py index 55a2e36..9e4c5cc 100644 --- a/src/race_simulator.py +++ b/src/simulators/race_simulator.py @@ -35,28 +35,13 @@ def run(self): class RaceSimulator: - SIZE = 1024, 768 - BG_COLOR = 0, 0, 0 - TEXT_COLOR = 255, 255, 255 def __init__(self, env, strategies): self._env = env self._strategies = strategies - - pygame.init() - pygame.font.init() - - self._font = pygame.font.SysFont('Arial', 30) - self._smallfont = pygame.font.SysFont('Arial', 20) - self._screen = pygame.display.set_mode(self.SIZE) - self._drawer = RaceDrawer(self._screen) + self._drawer = RaceDrawer(strategies, env) self._clock = pygame.time.Clock() - def write_text(self, text, row, color=(255, 255, 255)): - pos = 740, 30 + (row * 30) - textsurface = self._font.render(text, True, color) - self._screen.blit(textsurface, pos) - def run(self): # scale the race canvas self._drawer.autoscale(self._env.get_buoys()) @@ -67,44 +52,20 @@ def run(self): thread.start() while 1: - self._screen.fill(self.BG_COLOR) self._env.update() - self._drawer.draw_env(self._env) - self._drawer.draw_buoys(self._env.get_buoys()) - # update all boats - scoreboard = [] - for i, strategy in enumerate(self._strategies): - - # update boat and draw + for strategy in self._strategies: boat = strategy.get_boat() boat.update() - self._drawer.draw_boat(boat) - - # update scoreboard - scoreboard.append({ - 'name': strategy.get_name(), - 'color': boat.get_boat_color(), - 'marks_passed': boat.get_marks_passed(), - 'dtw': boat.get_distance_to_waypoint() - }) # update boat with current fps fps = self._clock.get_fps() if fps > 0: boat.set_draw_fps(fps) - # show scoreboard - scoreboard = pd.DataFrame(scoreboard).sort_values(by=['marks_passed', 'dtw'], ascending=[False, True]) - i = 0 - for _, row in scoreboard.iterrows(): - text = "%s (DTW: %dm)" % (row['name'], row.dtw) - self.write_text(text, i, row.color) - i += 1 - - # display new frame - pygame.display.flip() + # draw objects + self._drawer.draw() # check key events for event in pygame.event.get(): From a8748422617c780324326bf0b8d9c53837c6590b Mon Sep 17 00:00:00 2001 From: Wouter de Winter Date: Fri, 25 Jan 2019 16:57:57 +0100 Subject: [PATCH 10/15] race rl working --- race.py | 14 +--- src/boat.py | 57 ++++++++++------- src/drawers/race_drawer.py | 12 ++-- src/gym_sail/__init__.py | 5 ++ src/gym_sail/envs/__init__.py | 3 +- src/gym_sail/envs/race_env_continuous.py | 81 ++++++++++++++++++++++++ src/keras_rl_race_cont.py | 81 ++++++++++++++++++++++++ src/settings.py | 12 ++++ src/simulators/race_simulator.py | 7 +- src/strategies/base.py | 4 +- 10 files changed, 231 insertions(+), 45 deletions(-) create mode 100644 src/gym_sail/envs/race_env_continuous.py create mode 100644 src/keras_rl_race_cont.py diff --git a/race.py b/race.py index 3c911bc..b372202 100644 --- a/race.py +++ b/race.py @@ -8,6 +8,7 @@ from polar import Polar from simulators.race_simulator import RaceSimulator from boat import SimBoat +from settings import Settings # import configuration if os.path.isfile('config.py'): @@ -19,22 +20,13 @@ # load polar polar = Polar(os.path.join('data', 'polars', 'first-27.csv')) -# create some buoys -scale = 0.05 -buoys = [ - (52.3721693, 5.0750607), - (52.3721693 + 0.01 * scale, 5.0750607), - (52.3721693 + 0.008 * scale, 5.0750607 + 0.005 * scale), - (52.3721693 + 0.002 * scale, 5.0750607 + 0.005 * scale), -] - # create environment -env = Environment(buoys=buoys) +env = Environment(buoys=Settings.BUOYS) # instantiate all strategies strategies = [] for strategy in strategy_list: - boat = SimBoat(env, polar=polar, random_color=True).set_waypoint(1) + boat = SimBoat(env, polar=polar, random_color=True, name=strategy.__name__).set_waypoint(1) strategies.append(strategy(boat, env)) # start the simulator diff --git a/src/boat.py b/src/boat.py index cee0384..45e241f 100644 --- a/src/boat.py +++ b/src/boat.py @@ -31,7 +31,6 @@ class Boat: def __init__(self, env, random_color=False, name='no-name', keep_log=True): self._keep_log = keep_log self._name = name - self.rudder_angle = 0. self.target_rudder_angle = 0. self.boat_angle = 0. @@ -43,10 +42,7 @@ def __init__(self, env, random_color=False, name='no-name', keep_log=True): self._env = env self.history = pd.DataFrame() self.windspeed_shuffle = True - self._position = (52.3721693, 5.0750607) self._waypoint = None - self._bearing = 0. - self._distance = 0. self._marks_passed = 0 # set a boat color @@ -59,6 +55,19 @@ def __init__(self, env, random_color=False, name='no-name', keep_log=True): self._draw_fps = Settings.DRAW_FPS self._strategy = None + self._position = None + + # set initial position of boat + self.reset_boat_position() + + def reset_boat_position(self): + self._position = (52.3721693, 5.0750607) + + def set_heading(self, heading): + self.boat_angle = heading + + def get_heading(self): + return self.boat_angle def set_strategy(self, strategy): self._strategy = strategy @@ -144,8 +153,16 @@ def update(self): # simulate or fetch boat movements self.move() - # run navigation - self.nav() + # run navigation when steering strategy is active + if self._strategy is not None: + self.nav() + + # skip to next waypoint if we're there + if self._waypoint: + if self.get_distance_to_waypoint() < self.DIST_NEXT_WAYPOINT: + print("hit waypoint!") + self._marks_passed += 1 + self._waypoint = self._waypoint + 1 if self._waypoint < len(self._env.get_buoys()) - 1 else 0 # save history if self._keep_log: @@ -168,19 +185,11 @@ def nav(self): if self._waypoint is None: return - buoys = self._env.get_buoys() - - # get target position from waypoint - target_pos = buoys[self._waypoint] - # determine bearing to waypoint - self._bearing = geo.bearing(self._position[0], self._position[1], target_pos[0], target_pos[1]) - - # distance to waypoint - self._distance = geo.haversine(self._position[0], self._position[1], target_pos[0], target_pos[1]) + bearing = self.get_bearing_to_waypoint() # calculate new true wind angle to steer - new_twa = calc_angle(self._env.wind_direction, self._bearing) + new_twa = calc_angle(self._env.wind_direction, bearing) # get angles for optimal vmg upwind_twa = self._strategy.get_upwind_twa() @@ -196,12 +205,7 @@ def nav(self): # otherwise, steer directly to waypoint else: - self.set_target_angle(self._bearing) - - # skip to next waypoint if we're there - if self._distance < self.DIST_NEXT_WAYPOINT: - self._marks_passed += 1 - self._waypoint = self._waypoint + 1 if self._waypoint < len(buoys)-1 else 0 + self.set_target_angle(bearing) def set_twa(self, twa, tack=False): """ steer a true wind angle on the current (target) tack """ @@ -237,7 +241,14 @@ def get_marks_passed(self): return self._marks_passed def get_distance_to_waypoint(self): - return self._distance + buoys = self._env.get_buoys() + target_pos = buoys[self._waypoint] + return geo.haversine(self._position[0], self._position[1], target_pos[0], target_pos[1]) + + def get_bearing_to_waypoint(self): + buoys = self._env.get_buoys() + target_pos = buoys[self._waypoint] + return geo.bearing(self._position[0], self._position[1], target_pos[0], target_pos[1]) def get_name(self): return self._name diff --git a/src/drawers/race_drawer.py b/src/drawers/race_drawer.py index a215c1c..dcac09a 100644 --- a/src/drawers/race_drawer.py +++ b/src/drawers/race_drawer.py @@ -25,8 +25,8 @@ class RaceDrawer: BG_COLOR = 0, 0, 0 TEXT_COLOR = 255, 255, 255 - def __init__(self, strategies: list, env: Environment): - self._strategies = strategies + def __init__(self, boats: list, env: Environment): + self._boats = boats self._env = env self._offset = (0, 0) self._scale = 0 @@ -38,6 +38,9 @@ def __init__(self, strategies: list, env: Environment): self._smallfont = pygame.font.SysFont('Arial', 20) self._screen = pygame.display.set_mode(self.SIZE) + # scale the race canvas + self.autoscale(self._env.get_buoys()) + def autoscale(self, buoys): """ Scale race canvas to fit all buoys """ lat, lon = zip(*buoys) @@ -124,15 +127,14 @@ def draw(self): # draw all boats scoreboard = [] - for i, strategy in enumerate(self._strategies): + for boat in self._boats: # update boat and draw - boat = strategy.get_boat() self.draw_boat(boat) # update scoreboard scoreboard.append({ - 'name': strategy.get_name(), + 'name': boat.get_name(), 'color': boat.get_boat_color(), 'marks_passed': boat.get_marks_passed(), 'dtw': boat.get_distance_to_waypoint() diff --git a/src/gym_sail/__init__.py b/src/gym_sail/__init__.py index ec32a9f..2bee3b7 100644 --- a/src/gym_sail/__init__.py +++ b/src/gym_sail/__init__.py @@ -9,3 +9,8 @@ id='sail-continuous-v0', entry_point='gym_sail.envs:SailEnvContinuous', ) + +register( + id='race-continuous-v0', + entry_point='gym_sail.envs:RaceEnvContinuous', +) \ No newline at end of file diff --git a/src/gym_sail/envs/__init__.py b/src/gym_sail/envs/__init__.py index 41392ad..443f4a4 100644 --- a/src/gym_sail/envs/__init__.py +++ b/src/gym_sail/envs/__init__.py @@ -1,2 +1,3 @@ from gym_sail.envs.sail_env import SailEnv -from gym_sail.envs.sail_env_continuous import SailEnvContinuous \ No newline at end of file +from gym_sail.envs.sail_env_continuous import SailEnvContinuous +from gym_sail.envs.race_env_continuous import RaceEnvContinuous diff --git a/src/gym_sail/envs/race_env_continuous.py b/src/gym_sail/envs/race_env_continuous.py new file mode 100644 index 0000000..41dc741 --- /dev/null +++ b/src/gym_sail/envs/race_env_continuous.py @@ -0,0 +1,81 @@ +import gym +from gym import error, spaces, utils +from gym.utils import seeding +import numpy as np +import os +import random + +from boat import * +from environment import Environment +from polar import Polar +from drawers.race_drawer import RaceDrawer +from settings import Settings + + +class RaceEnvContinuous(gym.Env): + metadata = {'render.modes': ['human']} + + def __init__(self): + self.action_space = spaces.Box(low=-1, high=1, shape=(1,), dtype=np.float32) + self.observation_space = spaces.Box(low=-1, high=1, shape=(3,), dtype=np.float32) + + self.seed() + self.observation = None + + # start simulator + polar = Polar(os.path.join('..', 'data', 'polars', 'first-27.csv')) + self._env = Environment(buoys=Settings.BUOYS) + self._boat = SimBoat(self._env, polar=polar, keep_log=False).set_waypoint(1) + self._drawer = RaceDrawer([self._boat], self._env) + + self.reset() + self._step = 0 + + def render(self, mode='human', close=False): + self._drawer.draw() + + # should we quit? + for event in pygame.event.get(): + if event.type == pygame.KEYDOWN: + if event.key == pygame.K_q: + exit() + + def seed(self, seed=None): + seed = seeding.np_random(seed) + return [seed] + + def step(self, action): + #assert self.action_space.contains(action) + rudder_angle = float(action) * 30 + self._boat.set_target_rudder_angle(rudder_angle) + + # update boat and environment + self._env.update() + self._boat.update() + + mark_reward = self._boat.get_marks_passed() * 100 + reward = mark_reward - self._boat.get_distance_to_waypoint() + + # self._step += 1 + done = False + # if self._step > 300: + # self._step = 0 + # done = True + # print("done")q + + return self.get_observation(), reward, done, {"debug": 123} + + def get_observation(self): + delta = self._boat.get_heading() - self._boat.get_bearing_to_waypoint() + delta = (delta + 180) % 360 - 180 + return ( + delta / 180, + self._boat.rudder_angle / 30, + self._boat.get_angle_of_attack() / 180 + ) + + def reset(self): + self._boat.reset_rudder() + self._boat.reset_boat_position() + self._boat.set_heading(random.randint(-90, 90)) + return self.get_observation() diff --git a/src/keras_rl_race_cont.py b/src/keras_rl_race_cont.py new file mode 100644 index 0000000..ca969b3 --- /dev/null +++ b/src/keras_rl_race_cont.py @@ -0,0 +1,81 @@ +import time + +import numpy as np +import gym +import os.path + +from keras.models import Sequential, Model +from keras.layers import Dense, Activation, Flatten, Input, Concatenate +from keras.optimizers import Adam + +from rl.agents import DDPGAgent +from rl.memory import SequentialMemory +from rl.random import OrnsteinUhlenbeckProcess + +import gym_sail + +ENV_NAME = 'race-continuous-v0' + +LOAD = False + +# Get the environment and extract the number of actions. +env = gym.make(ENV_NAME) +np.random.seed(123) +env.seed(123) + +assert len(env.action_space.shape) == 1 +nb_actions = env.action_space.shape[0] + +# Next, we build a very simple model. +# todo back to linear activation? +actor = Sequential() +actor.add(Flatten(input_shape=(1,) + env.observation_space.shape)) +actor.add(Dense(16)) +actor.add(Activation('relu')) +actor.add(Dense(16)) +actor.add(Activation('relu')) +actor.add(Dense(16)) +actor.add(Activation('relu')) +actor.add(Dense(nb_actions)) +actor.add(Activation('tanh')) +print(actor.summary()) + +action_input = Input(shape=(nb_actions,), name='action_input') +observation_input = Input(shape=(1,) + env.observation_space.shape, name='observation_input') +flattened_observation = Flatten()(observation_input) +x = Concatenate()([action_input, flattened_observation]) +x = Dense(32)(x) +x = Activation('relu')(x) +x = Dense(32)(x) +x = Activation('relu')(x) +x = Dense(32)(x) +x = Activation('relu')(x) +x = Dense(1)(x) +x = Activation('linear')(x) +critic = Model(inputs=[action_input, observation_input], outputs=x) +print(critic.summary()) + +# Finally, we configure and compile our agent. You can use every built-in Keras optimizer and +# even the metrics! +memory = SequentialMemory(limit=100000, window_length=1) +random_process = OrnsteinUhlenbeckProcess(size=nb_actions, theta=.15, mu=0., sigma=.3) +agent = DDPGAgent(nb_actions=nb_actions, actor=actor, critic=critic, critic_action_input=action_input, + memory=memory, nb_steps_warmup_critic=100, nb_steps_warmup_actor=100, + random_process=random_process, gamma=.99, target_model_update=1e-3) +agent.compile(Adam(lr=.001, clipnorm=1.), metrics=['mae']) + +# file to save or load weights +model_filename = os.path.join('..', 'data', 'models', 'ddpg_%s_%d_weights.h5f' % (ENV_NAME, int(time.time()))) + +if LOAD: + # load weights + agent.load_weights(model_filename) +else: + # train + agent.fit(env, nb_steps=1000000, visualize=False, verbose=1, nb_max_episode_steps=3000) + + # After training is done, we save the final weights. + agent.save_weights(model_filename, overwrite=True) + +# Finally, evaluate our algorithm for 5 episodes. +agent.test(env, nb_episodes=100, visualize=True, nb_max_episode_steps=3000) \ No newline at end of file diff --git a/src/settings.py b/src/settings.py index 6bfe62c..936b0dd 100644 --- a/src/settings.py +++ b/src/settings.py @@ -6,3 +6,15 @@ class Settings: # number of graphical frames per second DRAW_FPS = 20 + + # scale the race area + BUOY_SCALE = 0.05 + + # definition of buoy positions + BUOYS = [ + (52.3721693, 5.0750607), + (52.3721693 + 0.01 * BUOY_SCALE, 5.0750607), + (52.3721693 + 0.008 * BUOY_SCALE, 5.0750607 + 0.005 * BUOY_SCALE), + (52.3721693 + 0.002 * BUOY_SCALE, 5.0750607 + 0.005 * BUOY_SCALE), + ] + diff --git a/src/simulators/race_simulator.py b/src/simulators/race_simulator.py index 9e4c5cc..d84ad77 100644 --- a/src/simulators/race_simulator.py +++ b/src/simulators/race_simulator.py @@ -39,12 +39,13 @@ class RaceSimulator: def __init__(self, env, strategies): self._env = env self._strategies = strategies - self._drawer = RaceDrawer(strategies, env) + + boats = [strategy.get_boat() for strategy in strategies] + + self._drawer = RaceDrawer(boats, env) self._clock = pygame.time.Clock() def run(self): - # scale the race canvas - self._drawer.autoscale(self._env.get_buoys()) # start thread for steering strategy thread = RaceUpdateThread(self._strategies, tf.get_default_graph()) diff --git a/src/strategies/base.py b/src/strategies/base.py index 06d18e0..4124c50 100644 --- a/src/strategies/base.py +++ b/src/strategies/base.py @@ -29,12 +29,12 @@ def get_upwind_twa(self): def need_to_tack(self) -> bool: """ Do we need to tack? """ - diff = calc_angle(self._boat.target_angle, self._boat._bearing) + diff = calc_angle(self._boat.target_angle, self._boat.get_bearing_to_waypoint()) return abs(diff) > self.get_upwind_twa() * 1.5 def need_to_gybe(self) -> bool: """ Do we need to gybe? """ - diff = calc_angle(self._boat.target_angle, self._boat._bearing) + diff = calc_angle(self._boat.target_angle, self._boat.get_bearing_to_waypoint()) return abs(diff) > (180 - self.get_downwind_twa() * 1.5) def set_update_fps(self, fps): From c9ce43d4caa02fd98d61afe8a5c7af73fb5efb17 Mon Sep 17 00:00:00 2001 From: Wouter de Winter Date: Sat, 26 Jan 2019 11:51:41 +0100 Subject: [PATCH 11/15] working race rl model --- src/boat.py | 5 ++++- src/gym_sail/envs/race_env_continuous.py | 1 + src/gym_sail/envs/sail_env.py | 2 +- .../race_continuous.py} | 18 +++++++++--------- .../steer_continuous.py} | 10 +++++++--- src/{keras_rl.py => rl/steer_discrete.py} | 0 src/{start_rl.py => rl/steer_test.py} | 0 7 files changed, 22 insertions(+), 14 deletions(-) rename src/{keras_rl_race_cont.py => rl/race_continuous.py} (84%) rename src/{keras_rl_cont.py => rl/steer_continuous.py} (86%) rename src/{keras_rl.py => rl/steer_discrete.py} (100%) rename src/{start_rl.py => rl/steer_test.py} (100%) diff --git a/src/boat.py b/src/boat.py index 45e241f..1e2b860 100644 --- a/src/boat.py +++ b/src/boat.py @@ -158,7 +158,7 @@ def update(self): self.nav() # skip to next waypoint if we're there - if self._waypoint: + if self._waypoint is not None: if self.get_distance_to_waypoint() < self.DIST_NEXT_WAYPOINT: print("hit waypoint!") self._marks_passed += 1 @@ -231,7 +231,10 @@ def get_position(self): return self._position def set_waypoint(self, waypoint): + """set new waypoint, also resets number of marks passed""" + self._waypoint = waypoint + self._marks_passed = 0 return self def get_boat_color(self): diff --git a/src/gym_sail/envs/race_env_continuous.py b/src/gym_sail/envs/race_env_continuous.py index 41dc741..1f00073 100644 --- a/src/gym_sail/envs/race_env_continuous.py +++ b/src/gym_sail/envs/race_env_continuous.py @@ -78,4 +78,5 @@ def reset(self): self._boat.reset_rudder() self._boat.reset_boat_position() self._boat.set_heading(random.randint(-90, 90)) + self._boat.set_waypoint(1) return self.get_observation() diff --git a/src/gym_sail/envs/sail_env.py b/src/gym_sail/envs/sail_env.py index 6a4d43a..a3e89e1 100644 --- a/src/gym_sail/envs/sail_env.py +++ b/src/gym_sail/envs/sail_env.py @@ -22,7 +22,7 @@ def __init__(self): self.observation = None # start simulator - polar = Polar(os.path.join('..', 'data', 'polars', 'first-27.csv')) + polar = Polar(os.path.join('..', '..', 'data', 'polars', 'first-27.csv')) self._env = Environment() self._boat = SimBoat(self._env, polar=polar, keep_log=False) self._drawer = SimDrawer() diff --git a/src/keras_rl_race_cont.py b/src/rl/race_continuous.py similarity index 84% rename from src/keras_rl_race_cont.py rename to src/rl/race_continuous.py index ca969b3..9001eac 100644 --- a/src/keras_rl_race_cont.py +++ b/src/rl/race_continuous.py @@ -16,7 +16,7 @@ ENV_NAME = 'race-continuous-v0' -LOAD = False +LOAD = True # Get the environment and extract the number of actions. env = gym.make(ENV_NAME) @@ -30,11 +30,11 @@ # todo back to linear activation? actor = Sequential() actor.add(Flatten(input_shape=(1,) + env.observation_space.shape)) -actor.add(Dense(16)) +actor.add(Dense(32)) actor.add(Activation('relu')) -actor.add(Dense(16)) +actor.add(Dense(32)) actor.add(Activation('relu')) -actor.add(Dense(16)) +actor.add(Dense(32)) actor.add(Activation('relu')) actor.add(Dense(nb_actions)) actor.add(Activation('tanh')) @@ -64,18 +64,18 @@ random_process=random_process, gamma=.99, target_model_update=1e-3) agent.compile(Adam(lr=.001, clipnorm=1.), metrics=['mae']) -# file to save or load weights -model_filename = os.path.join('..', 'data', 'models', 'ddpg_%s_%d_weights.h5f' % (ENV_NAME, int(time.time()))) - if LOAD: # load weights + t = 1548451946 + model_filename = os.path.join('..', 'data', 'models', 'ddpg_%s_%d_weights.h5f' % (ENV_NAME, t)) agent.load_weights(model_filename) else: # train - agent.fit(env, nb_steps=1000000, visualize=False, verbose=1, nb_max_episode_steps=3000) + agent.fit(env, nb_steps=10000000, visualize=False, verbose=1, nb_max_episode_steps=4000) # After training is done, we save the final weights. + model_filename = os.path.join('..', '..', 'data', 'models', 'ddpg_%s_%d_weights.h5f' % (ENV_NAME, int(time.time()))) agent.save_weights(model_filename, overwrite=True) # Finally, evaluate our algorithm for 5 episodes. -agent.test(env, nb_episodes=100, visualize=True, nb_max_episode_steps=3000) \ No newline at end of file +agent.test(env, nb_episodes=100, visualize=True, nb_max_episode_steps=4000) \ No newline at end of file diff --git a/src/keras_rl_cont.py b/src/rl/steer_continuous.py similarity index 86% rename from src/keras_rl_cont.py rename to src/rl/steer_continuous.py index 930ed8b..5a6b164 100644 --- a/src/keras_rl_cont.py +++ b/src/rl/steer_continuous.py @@ -1,5 +1,6 @@ import numpy as np import gym +import os.path from keras.models import Sequential, Model from keras.layers import Dense, Activation, Flatten, Input, Concatenate @@ -61,10 +62,13 @@ # Okay, now it's time to learn something! We visualize the training here for show, but this # slows down training quite a lot. You can always safely abort the training prematurely using # Ctrl + C. -agent.fit(env, nb_steps=50000, visualize=False, verbose=1, nb_max_episode_steps=200) +#agent.fit(env, nb_steps=50000, visualize=False, verbose=1, nb_max_episode_steps=200) + +model_filename = os.path.join('..', 'data', 'models', 'ddpg_{}_weights.h5f'.format(ENV_NAME)) +agent.load_weights(model_filename) # After training is done, we save the final weights. -agent.save_weights('ddpg_{}_weights.h5f'.format(ENV_NAME), overwrite=True) +#agent.save_weights(model_filename, overwrite=True) # Finally, evaluate our algorithm for 5 episodes. -agent.test(env, nb_episodes=5, visualize=True, nb_max_episode_steps=200) \ No newline at end of file +agent.test(env, nb_episodes=100, visualize=True, nb_max_episode_steps=200) \ No newline at end of file diff --git a/src/keras_rl.py b/src/rl/steer_discrete.py similarity index 100% rename from src/keras_rl.py rename to src/rl/steer_discrete.py diff --git a/src/start_rl.py b/src/rl/steer_test.py similarity index 100% rename from src/start_rl.py rename to src/rl/steer_test.py From 2f486e142a1a021844e682ac323a430c8c8b13cc Mon Sep 17 00:00:00 2001 From: Wouter de Winter Date: Sat, 26 Jan 2019 12:02:18 +0100 Subject: [PATCH 12/15] path fixes --- src/boat.py | 2 +- src/gym_sail/envs/race_env_continuous.py | 2 +- src/gym_sail/envs/sail_env_continuous.py | 2 +- src/rl/race_continuous.py | 4 ++-- src/rl/steer_continuous.py | 2 +- 5 files changed, 6 insertions(+), 6 deletions(-) diff --git a/src/boat.py b/src/boat.py index 1e2b860..910da3b 100644 --- a/src/boat.py +++ b/src/boat.py @@ -160,7 +160,7 @@ def update(self): # skip to next waypoint if we're there if self._waypoint is not None: if self.get_distance_to_waypoint() < self.DIST_NEXT_WAYPOINT: - print("hit waypoint!") + logging.info("hit waypoint") self._marks_passed += 1 self._waypoint = self._waypoint + 1 if self._waypoint < len(self._env.get_buoys()) - 1 else 0 diff --git a/src/gym_sail/envs/race_env_continuous.py b/src/gym_sail/envs/race_env_continuous.py index 1f00073..4b5ce6c 100644 --- a/src/gym_sail/envs/race_env_continuous.py +++ b/src/gym_sail/envs/race_env_continuous.py @@ -23,7 +23,7 @@ def __init__(self): self.observation = None # start simulator - polar = Polar(os.path.join('..', 'data', 'polars', 'first-27.csv')) + polar = Polar(os.path.join('..', '..', 'data', 'polars', 'first-27.csv')) self._env = Environment(buoys=Settings.BUOYS) self._boat = SimBoat(self._env, polar=polar, keep_log=False).set_waypoint(1) self._drawer = RaceDrawer([self._boat], self._env) diff --git a/src/gym_sail/envs/sail_env_continuous.py b/src/gym_sail/envs/sail_env_continuous.py index f4dd0c5..c4a53c0 100644 --- a/src/gym_sail/envs/sail_env_continuous.py +++ b/src/gym_sail/envs/sail_env_continuous.py @@ -21,7 +21,7 @@ def __init__(self): self.observation = None # start simulator - polar = Polar(os.path.join('..', 'data', 'polars', 'first-27.csv')) + polar = Polar(os.path.join('..', '..', 'data', 'polars', 'first-27.csv')) self._env = Environment() self._boat = SimBoat(self._env, polar=polar, keep_log=False) self._drawer = SimDrawer() diff --git a/src/rl/race_continuous.py b/src/rl/race_continuous.py index 9001eac..9f229eb 100644 --- a/src/rl/race_continuous.py +++ b/src/rl/race_continuous.py @@ -67,8 +67,8 @@ if LOAD: # load weights t = 1548451946 - model_filename = os.path.join('..', 'data', 'models', 'ddpg_%s_%d_weights.h5f' % (ENV_NAME, t)) - agent.load_weights(model_filename) + model_filename = os.path.join('..', '..', 'data', 'models', 'ddpg_%s_%d_weights.h5f' % (ENV_NAME, t)) + agent.load_weights(model_filename), else: # train agent.fit(env, nb_steps=10000000, visualize=False, verbose=1, nb_max_episode_steps=4000) diff --git a/src/rl/steer_continuous.py b/src/rl/steer_continuous.py index 5a6b164..eada254 100644 --- a/src/rl/steer_continuous.py +++ b/src/rl/steer_continuous.py @@ -64,7 +64,7 @@ # Ctrl + C. #agent.fit(env, nb_steps=50000, visualize=False, verbose=1, nb_max_episode_steps=200) -model_filename = os.path.join('..', 'data', 'models', 'ddpg_{}_weights.h5f'.format(ENV_NAME)) +model_filename = os.path.join('..', '..', 'data', 'models', 'ddpg_{}_weights.h5f'.format(ENV_NAME)) agent.load_weights(model_filename) # After training is done, we save the final weights. From 58bd333fe57799b9ec382270859b521db48e83b6 Mon Sep 17 00:00:00 2001 From: Wouter de Winter Date: Sat, 26 Jan 2019 12:32:09 +0100 Subject: [PATCH 13/15] path fixes --- fonts/B612-Regular.ttf | Bin 0 -> 181460 bytes src/drawers/race_drawer.py | 12 +++++++----- src/gym_sail/envs/race_env_continuous.py | 2 +- src/rl/race_continuous.py | 4 ++-- 4 files changed, 10 insertions(+), 8 deletions(-) create mode 100755 fonts/B612-Regular.ttf diff --git a/fonts/B612-Regular.ttf b/fonts/B612-Regular.ttf new file mode 100755 index 0000000000000000000000000000000000000000..c685a237f703b8b18dba431e47b2a4f26883de84 GIT binary patch literal 181460 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�sl$N4DNtY3AtOgkw{s`c#4JYx$`yidi*9ayrTn zz+7+`vtkgbOGY<{qNJgdH8_z)7= zR2<8$c705{c~z5FpAdeXxum>Z{~n8F`GM9%h)X zGjDf-D0gdlubp6sohs&GA3CV6`w~g8V^6e{o|WxrZURAYayp*E{4QfVTV1T@SpR3b zPDj;KNWBusO!LqVWq9l~hE~VBg_GcI$Imotrl9$Ic&QsESe+nm+ugvKS*`DDs#uJq z*`|j^c1QAxt?U?6R#`pl;?^}9JtvtxLN_=NYhXMUhkjc!ny=N|)M$!myYU1pTw`rO z_l>ooRp3~P)`Vk?h(a7okeARSMn>5cOh@B_=~!Ie1*S$*G^S$-g6U|2U^*5T6+D(w zU^-exV>+53m>${3>Y$};K^1bh%V}nIyFNm|P~e#}tDmx@V$SLuaXbs9N#V1XOHCzB>PYlQ@5~`xQuvFFb6WOgnwvJB@ zB5AC^x}-69V%9bXpa{FIE`hSKAnWs$LZQaiD;^I`1L z+(eIISK|!Mcn@F3m-7esR=$((yKh96^Q~WePV^+*5bEUb;Ji&aWxyD>;o?)&x zH=BWZmH8F(G4qGka{NYWV$E4^x87}CX?@tb#=6$}oOP3Ri*=iIx3%AT#CqI%!g|Vj z8voakD3p2y9gSfdqm-7omv~ECtzpSSQ94>$ zEXOA`jf-)7u70HEICr0Kf23(F?*)y*%ldGv#4c6FOKeh0UJ0!-Hl$0HwWS2>F_m(R zme*=t3RKh|g~V>NkPXQ#a&``lhA7>@(*&n*)_J|$k*xIW+6DZDQ^ z`oiHujrzj)KVPU=6N-^vkb!Y%9N$6?3fvdq7sfTJ6f5vjoY<^2p2lgyMtTPO!B3jk zSSMPWt-#uCJ!*Z=dJ=2nGRUX+5{eJutF#r%I`DEmf_RvSjgokgh!2VQ@rcjJE(Gz{ z5x*+&s(KfKcvQUyL9F@2`$l}I#DnU31o57_0YR+0Z$c2uZn5jW8A1HUzJzcKf_UJG z|J|(!UqcWNsoN3mK)4g(E`+-g#M@4M?e0YoKf4DJ9!99)Y{|vBhiiyWop{uVKb?5f zi7%aa(lxbxj#w1!MffO!co1EUAl^g!5X3s^0R*v+KS^^#N*{_N!j1on6VlrM7dM0p zf;FsJa6c4%LfaZmmy{Z%>u^W;P(2(Dy)R_6g@SU#U!iFgDi+IXDW`{v<&Q!>ON@^j z@_iKB64T2-#XAt*iLiJ^9g2$QgYBVajbQcMvhv&|xUfP*Ys-hLMkz}|+rei2;f%YC zwW%%nWHyO}GtQ1(ZB;p*Ue%ryk|&-M;x{2)6XG)=9utQ*_n97&cUYX4J*3ah4fK16 zzaMqZv)3-Z1d)UG>Mo$TIDc`xEGcc5@c6SxFLMXw7*+%DvPa36;v(1x;CHmKQl11` zgHvD;P}LRz6R-$44YmgBa81DyU=wT(;8_D}gRMC4KT4nXAB%JS%b|JRi?jXXjlDSO zf0?ll*U32P|1>nwbvUIy1?T8@z^>p*;cM`*GRGSqVFiW6`c)V{1HU1dl4LGd7~mW?Y!V zxJ=BeX-NBpxat_AUFh#JU}$NaY>ktxv7r$>s`$#g8SQ)#wiK*!GQpvH(89gA8W^?v aaTm;N%)^J#zThX{kj3|D3*QR*?|%U>@cn%N literal 0 HcmV?d00001 diff --git a/src/drawers/race_drawer.py b/src/drawers/race_drawer.py index dcac09a..edddd12 100644 --- a/src/drawers/race_drawer.py +++ b/src/drawers/race_drawer.py @@ -1,7 +1,9 @@ import pygame +import pandas as pd +import os.path + from tools import rotate_point from environment import Environment -import pandas as pd class RaceDrawer: @@ -11,7 +13,7 @@ class RaceDrawer: RACE_CANVAS_COLOR = (33, 66, 99) ARROW_SHAPE = [(0, 100), (0, 200), (200, 200), (200, 300), (300, 150), (200, 0), (200, 100)] - ARROW_COLOR = (0, 255, 0) + ARROW_COLOR = (9, 209, 97) ARROW_POS = [350, 250] ARROW_ORIGIN = [150, 100] ARROW_SCALE = 0.2 @@ -21,7 +23,7 @@ class RaceDrawer: BOAT_COLOR = (255, 255, 255) BOAT_SCALE = 0.1 - SIZE = 1024, 768 + SIZE = 1100, 730 BG_COLOR = 0, 0, 0 TEXT_COLOR = 255, 255, 255 @@ -34,8 +36,8 @@ def __init__(self, boats: list, env: Environment): pygame.init() pygame.font.init() - self._font = pygame.font.SysFont('Arial', 30) - self._smallfont = pygame.font.SysFont('Arial', 20) + self._font = pygame.font.Font(os.path.join('fonts', 'B612-Regular.ttf'), 20) + self._smallfont = pygame.font.Font(os.path.join('fonts', 'B612-Regular.ttf'), 15) self._screen = pygame.display.set_mode(self.SIZE) # scale the race canvas diff --git a/src/gym_sail/envs/race_env_continuous.py b/src/gym_sail/envs/race_env_continuous.py index 4b5ce6c..be4caf4 100644 --- a/src/gym_sail/envs/race_env_continuous.py +++ b/src/gym_sail/envs/race_env_continuous.py @@ -23,7 +23,7 @@ def __init__(self): self.observation = None # start simulator - polar = Polar(os.path.join('..', '..', 'data', 'polars', 'first-27.csv')) + polar = Polar(os.path.join('data', 'polars', 'first-27.csv')) self._env = Environment(buoys=Settings.BUOYS) self._boat = SimBoat(self._env, polar=polar, keep_log=False).set_waypoint(1) self._drawer = RaceDrawer([self._boat], self._env) diff --git a/src/rl/race_continuous.py b/src/rl/race_continuous.py index 9f229eb..c056cf9 100644 --- a/src/rl/race_continuous.py +++ b/src/rl/race_continuous.py @@ -67,14 +67,14 @@ if LOAD: # load weights t = 1548451946 - model_filename = os.path.join('..', '..', 'data', 'models', 'ddpg_%s_%d_weights.h5f' % (ENV_NAME, t)) + model_filename = os.path.join('data', 'models', 'ddpg_%s_%d_weights.h5f' % (ENV_NAME, t)) agent.load_weights(model_filename), else: # train agent.fit(env, nb_steps=10000000, visualize=False, verbose=1, nb_max_episode_steps=4000) # After training is done, we save the final weights. - model_filename = os.path.join('..', '..', 'data', 'models', 'ddpg_%s_%d_weights.h5f' % (ENV_NAME, int(time.time()))) + model_filename = os.path.join('data', 'models', 'ddpg_%s_%d_weights.h5f' % (ENV_NAME, int(time.time()))) agent.save_weights(model_filename, overwrite=True) # Finally, evaluate our algorithm for 5 episodes. From df2324d8ebbdd2d4e345ed36408c6fedf4549a07 Mon Sep 17 00:00:00 2001 From: Wouter de Winter Date: Sat, 6 Apr 2019 19:05:32 +0200 Subject: [PATCH 14/15] repaired switch strategy --- src/drawers/sim_drawer.py | 8 +++++--- src/simulator.py | 6 +++++- src/strategies/default/manual.py | 7 +++++-- 3 files changed, 15 insertions(+), 6 deletions(-) diff --git a/src/drawers/sim_drawer.py b/src/drawers/sim_drawer.py index 7bfaafb..67e3262 100644 --- a/src/drawers/sim_drawer.py +++ b/src/drawers/sim_drawer.py @@ -3,6 +3,7 @@ from boat import Boat from environment import Environment from tools import rotate_point, add_vector, rotate_vectors +from strategies.base import Base class SimDrawer: @@ -83,7 +84,7 @@ def write_text(self, text, row): textsurface = self._font.render(text, True, self.TEXT_COLOR) self._screen.blit(textsurface, pos) - def draw_stats(self, boat, env): + def draw_stats(self, boat, env, strategy: Base): # calculate mean of absolute course error if boat.history.shape[0] > 0: mae = boat.history.course_error.abs().mean() @@ -100,17 +101,18 @@ def draw_stats(self, boat, env): self.write_text("Wind direction: %.1f°" % env.wind_direction, 8) self.write_text("Wind speed: %.1f knots" % env.wind_speed, 9) self.write_text("MAE: %.1f°" % mae, 11) + self.write_text("Strategy: %s" % strategy.get_name(), 12) textsurface = self._smallfont.render( "Press keys to change: 1/2 for target angle, 3/4 for wind direction, 5/6 for wind speed, s to change strategy, q to quit", True, self.TEXT_COLOR) self._screen.blit(textsurface, (20, 565)) - def draw(self, boat: Boat, env: Environment): + def draw(self, boat: Boat, env: Environment, strategy: Base): # redraw objects self._screen.fill(self.BG_COLOR) self.draw_boat(boat) self.draw_env(env) - self.draw_stats(boat, env) + self.draw_stats(boat, env, strategy) # display new frame pygame.display.flip() diff --git a/src/simulator.py b/src/simulator.py index 1c17675..ed36379 100644 --- a/src/simulator.py +++ b/src/simulator.py @@ -22,6 +22,9 @@ def __init__(self, boat: Boat, env: Environment, strategy: Base, graph): self._clock = pygame.time.Clock() self._graph = graph + def set_strategy(self, strategy: Base): + self._strategy = strategy + def run(self): while 1: # update steering strategy @@ -72,7 +75,7 @@ def run(self): self._boat.update() # redraw objects - self._drawer.draw(self._boat, self._env) + self._drawer.draw(self._boat, self._env, self._strategy) # shuffle once in a while if self._shuffle_interval and time.time() > shuffle_time: @@ -88,6 +91,7 @@ def run(self): if event.key == pygame.K_s: self._strategy_id = self._strategy_id + 1 if self._strategy_id < len(self._strategies) - 1 else 0 self._strategy = self._strategies[self._strategy_id] + thread.set_strategy(self._strategy) # save log and quit if event.key == pygame.K_q: diff --git a/src/strategies/default/manual.py b/src/strategies/default/manual.py index bfb494a..61a7071 100644 --- a/src/strategies/default/manual.py +++ b/src/strategies/default/manual.py @@ -1,12 +1,15 @@ import pygame from strategies.base import Base + class Manual(Base): - STEERING_FORCE = 1 + STEERING_FORCE = 5 def update(self): pressed = pygame.key.get_pressed() if pressed[pygame.K_LEFT]: self._boat.steer(self.STEERING_FORCE) - if pressed[pygame.K_RIGHT]: + elif pressed[pygame.K_RIGHT]: self._boat.steer(-self.STEERING_FORCE) + else: + self._boat.set_target_rudder_angle(0) From b4fcad9a0d2b0a00eaae49249d31088f629bdb54 Mon Sep 17 00:00:00 2001 From: Wouter de Winter Date: Mon, 8 Apr 2019 16:01:27 +0200 Subject: [PATCH 15/15] updated sail discrete --- src/drawers/sim_drawer.py | 9 ++++----- src/gym_sail/envs/sail_env.py | 4 ++-- src/rl/steer_discrete.py | 14 ++++++++++++-- src/simulator.py | 2 +- src/strategies/default/binary.py | 1 + 5 files changed, 20 insertions(+), 10 deletions(-) diff --git a/src/drawers/sim_drawer.py b/src/drawers/sim_drawer.py index 67e3262..a0c3905 100644 --- a/src/drawers/sim_drawer.py +++ b/src/drawers/sim_drawer.py @@ -3,7 +3,6 @@ from boat import Boat from environment import Environment from tools import rotate_point, add_vector, rotate_vectors -from strategies.base import Base class SimDrawer: @@ -84,7 +83,7 @@ def write_text(self, text, row): textsurface = self._font.render(text, True, self.TEXT_COLOR) self._screen.blit(textsurface, pos) - def draw_stats(self, boat, env, strategy: Base): + def draw_stats(self, boat, env, strategy_name): # calculate mean of absolute course error if boat.history.shape[0] > 0: mae = boat.history.course_error.abs().mean() @@ -101,18 +100,18 @@ def draw_stats(self, boat, env, strategy: Base): self.write_text("Wind direction: %.1f°" % env.wind_direction, 8) self.write_text("Wind speed: %.1f knots" % env.wind_speed, 9) self.write_text("MAE: %.1f°" % mae, 11) - self.write_text("Strategy: %s" % strategy.get_name(), 12) + self.write_text("Strategy: %s" % strategy_name, 12) textsurface = self._smallfont.render( "Press keys to change: 1/2 for target angle, 3/4 for wind direction, 5/6 for wind speed, s to change strategy, q to quit", True, self.TEXT_COLOR) self._screen.blit(textsurface, (20, 565)) - def draw(self, boat: Boat, env: Environment, strategy: Base): + def draw(self, boat: Boat, env: Environment, strategy_name='Undefined'): # redraw objects self._screen.fill(self.BG_COLOR) self.draw_boat(boat) self.draw_env(env) - self.draw_stats(boat, env, strategy) + self.draw_stats(boat, env, strategy_name) # display new frame pygame.display.flip() diff --git a/src/gym_sail/envs/sail_env.py b/src/gym_sail/envs/sail_env.py index a3e89e1..dd200f6 100644 --- a/src/gym_sail/envs/sail_env.py +++ b/src/gym_sail/envs/sail_env.py @@ -22,7 +22,7 @@ def __init__(self): self.observation = None # start simulator - polar = Polar(os.path.join('..', '..', 'data', 'polars', 'first-27.csv')) + polar = Polar(os.path.join('data', 'polars', 'first-27.csv')) self._env = Environment() self._boat = SimBoat(self._env, polar=polar, keep_log=False) self._drawer = SimDrawer() @@ -31,7 +31,7 @@ def __init__(self): self._step = 0 def render(self, mode='human', close=False): - self._drawer.draw(self._boat, self._env) + self._drawer.draw(self._boat, self._env, 'SailEnv') # should we quit? for event in pygame.event.get(): diff --git a/src/rl/steer_discrete.py b/src/rl/steer_discrete.py index 4480db5..80e2776 100644 --- a/src/rl/steer_discrete.py +++ b/src/rl/steer_discrete.py @@ -1,6 +1,7 @@ import numpy as np import gym import os +import time from keras.models import Sequential from keras.layers import Dense, Activation, Flatten @@ -16,7 +17,7 @@ import gym_sail ENV_NAME = 'sail-v0' - +LOAD = True # Get the environment and extract the number of actions. env = gym.make(ENV_NAME) @@ -45,13 +46,22 @@ target_model_update=1e-2, policy=policy) dqn.compile(Adam(lr=1e-4), metrics=['mae']) +# load weights +model_filename = os.path.join('data', 'models', 'ddpg_%s_%d_weights.h5f') +if LOAD: + dqn.load_weights(model_filename % (ENV_NAME, 1554729826)), # 15 minutes of training (6h real time) + # dqn.load_weights(model_filename % (ENV_NAME, 1554731807)), # 1 hour of training (24h real time) + # Okay, now it's time to learn something! We visualize the training here for show, but this # slows down training quite a lot. You can always safely abort the training prematurely using # Ctrl + C. dqn.fit(env, nb_steps=1000000, visualize=False, verbose=2) # After training is done, we save the final weights. -dqn.save_weights('dqn_{}_weights.h5f'.format(ENV_NAME), overwrite=True) +filename = model_filename % (ENV_NAME, time.time()) +dqn.save_weights(filename, overwrite=True) +print("saved weights to %s" % filename) + # Finally, evaluate our algorithm for 5 episodes. dqn.test(env, nb_episodes=500, visualize=True) \ No newline at end of file diff --git a/src/simulator.py b/src/simulator.py index ed36379..059ae6d 100644 --- a/src/simulator.py +++ b/src/simulator.py @@ -75,7 +75,7 @@ def run(self): self._boat.update() # redraw objects - self._drawer.draw(self._boat, self._env, self._strategy) + self._drawer.draw(self._boat, self._env, self._strategy.get_name()) # shuffle once in a while if self._shuffle_interval and time.time() > shuffle_time: diff --git a/src/strategies/default/binary.py b/src/strategies/default/binary.py index 17a0063..87ec439 100644 --- a/src/strategies/default/binary.py +++ b/src/strategies/default/binary.py @@ -1,5 +1,6 @@ from strategies.base import Base + class Binary(Base): def update(self):