Merge updated files
This commit is contained in:
@@ -160,3 +160,6 @@ class ReparamPolicy(ReparamModule):
|
||||
|
||||
def predict(self, *args, **kwargs):
|
||||
return self.module.predict(*args, **kwargs)
|
||||
|
||||
def unsafe_probability_mass(self, *args, **kwargs):
|
||||
return self.module.unsafe_probability_mass(*args, **kwargs)
|
||||
|
||||
111
src/options/envs.py
Normal file
111
src/options/envs.py
Normal file
@@ -0,0 +1,111 @@
|
||||
import gym
|
||||
import numpy as np
|
||||
from src.gail2.wrappers import Wrapper, Setobs, TransformObservation
|
||||
from intersim.envs import IntersimpleLidarFlatIncrementingAgent
|
||||
|
||||
obs_min = np.array([
|
||||
[-1000, -1000, 0, -np.pi, -1e-1, 0.],
|
||||
[0, -np.pi, -20, -20, -np.pi, -1e-1],
|
||||
[0, -np.pi, -20, -20, -np.pi, -1e-1],
|
||||
[0, -np.pi, -20, -20, -np.pi, -1e-1],
|
||||
[0, -np.pi, -20, -20, -np.pi, -1e-1],
|
||||
[0, -np.pi, -20, -20, -np.pi, -1e-1],
|
||||
]).reshape(-1)
|
||||
|
||||
obs_max = np.array([
|
||||
[1000, 1000, 20, np.pi, 1e-1, 0.],
|
||||
[50, np.pi, 20, 20, np.pi, 1e-1],
|
||||
[50, np.pi, 20, 20, np.pi, 1e-1],
|
||||
[50, np.pi, 20, 20, np.pi, 1e-1],
|
||||
[50, np.pi, 20, 20, np.pi, 1e-1],
|
||||
[50, np.pi, 20, 20, np.pi, 1e-1],
|
||||
]).reshape(-1)
|
||||
|
||||
def NormalizedOptionsEvalEnv(**kwargs):
|
||||
return OptionsEnv(Setobs(
|
||||
TransformObservation(IntersimpleLidarFlatIncrementingAgent(
|
||||
n_rays=5,
|
||||
**kwargs,
|
||||
), lambda obs: (obs - obs_min) / (obs_max - obs_min + 1e-10))
|
||||
), options=[(0, 5), (1, 5), (2, 5), (4, 5), (6, 5), (8, 5)])
|
||||
|
||||
def NormalizedContinuousEvalEnv(**kwargs):
|
||||
return Setobs(
|
||||
TransformObservation(IntersimpleLidarFlatIncrementingAgent(
|
||||
n_rays=5,
|
||||
**kwargs,
|
||||
), lambda obs: (obs - obs_min) / (obs_max - obs_min + 1e-10))
|
||||
)
|
||||
|
||||
class OptionsEnv(Wrapper):
|
||||
|
||||
def __init__(self, env, options):
|
||||
super().__init__(env)
|
||||
self.ll_action_space = env.action_space
|
||||
self.options = options
|
||||
self.action_space = gym.spaces.Discrete(len(options))
|
||||
self.max_plan_length = max(t for _, t in options)
|
||||
|
||||
def plan(self, option):
|
||||
target_v, t = option
|
||||
current_v = self.env._env.state[self.env._agent, 1].item()
|
||||
dt = self.env._env._dt
|
||||
a = (target_v - current_v) / (t * dt)
|
||||
a = self.env._normalize(a)
|
||||
a = a * np.ones((t,))
|
||||
a += 0.01 * np.random.randn(*a.shape)
|
||||
a = np.clip(a, self.ll_action_space.low, self.ll_action_space.high)
|
||||
return a
|
||||
|
||||
def execute_plan(self, obs, option, render_mode=None):
|
||||
observations = np.zeros((self.max_plan_length + 1, *self.env.observation_space.shape))
|
||||
actions = np.zeros((self.max_plan_length + 1, *self.ll_action_space.shape))
|
||||
rewards = np.zeros((self.max_plan_length + 1,))
|
||||
env_done = np.ones((self.max_plan_length + 1,), dtype=bool)
|
||||
plan_done = np.ones((self.max_plan_length + 1,), dtype=bool)
|
||||
infos = []
|
||||
|
||||
observations[0] = obs
|
||||
env_done[0] = False
|
||||
for k, u in enumerate(self.plan(option)):
|
||||
plan_done[k] = False
|
||||
o, r, d, i = super().step(u)
|
||||
actions[k] = u
|
||||
rewards[k] = r
|
||||
env_done[k] = d
|
||||
infos.append(i)
|
||||
observations[k+1] = o
|
||||
|
||||
if render_mode is not None:
|
||||
self.env.render(render_mode)
|
||||
|
||||
if d:
|
||||
break
|
||||
|
||||
n_steps = k + 1
|
||||
return observations, actions, rewards, env_done, plan_done, infos, n_steps
|
||||
|
||||
def step(self, action, render_mode=None):
|
||||
a = int(action)
|
||||
assert a == action
|
||||
ll_obs, ll_actions, ll_rewards, ll_env_done, ll_plan_done, ll_infos, ll_steps = self.execute_plan(self.last_obs, self.options[a], render_mode)
|
||||
hl_obs = ll_obs[ll_steps]
|
||||
hl_reward = (ll_rewards * ~ll_plan_done).sum().item()
|
||||
hl_done = ll_env_done[ll_steps-1].item()
|
||||
hl_infos = {
|
||||
'll': {
|
||||
'observations': ll_obs,
|
||||
'actions': ll_actions,
|
||||
'rewards': ll_rewards,
|
||||
'env_done': ll_env_done,
|
||||
'plan_done': ll_plan_done,
|
||||
'infos': ll_infos,
|
||||
'steps': ll_steps,
|
||||
}
|
||||
}
|
||||
self.last_obs = hl_obs
|
||||
return hl_obs, hl_reward, hl_done, hl_infos
|
||||
|
||||
def reset(self, *args, **kwargs):
|
||||
self.last_obs = super().reset(*args, **kwargs)
|
||||
return self.last_obs
|
||||
206
src/options/options.py
Normal file
206
src/options/options.py
Normal file
@@ -0,0 +1,206 @@
|
||||
import gym
|
||||
import numpy as np
|
||||
import torch
|
||||
from stable_baselines3.common.vec_env import DummyVecEnv as VecEnv
|
||||
|
||||
from core.reparam_module import ReparamPolicy
|
||||
from tqdm import tqdm
|
||||
from core.gail import Buffer, train_discriminator, roll_buffer, TerminalLogger
|
||||
from dataclasses import dataclass
|
||||
from core.trpo import trpo_step
|
||||
from core.ppo import ppo_step
|
||||
import torch.nn.functional as F
|
||||
|
||||
@dataclass
|
||||
class OptionsRollout:
|
||||
hl: Buffer
|
||||
ll: Buffer
|
||||
|
||||
def gail(env_fn, expert_data, discriminator, disc_opt, disc_iters, policy, value,
|
||||
v_opt, v_iters, epochs, rollout_episodes, rollout_steps, gamma,
|
||||
gae_lambda, delta, backtrack_coeff, backtrack_iters, cg_iters=10, cg_damping=0.1, wasserstein=False, wasserstein_c=None, logger=TerminalLogger(), callback=None):
|
||||
|
||||
policy(torch.zeros(env_fn(0).observation_space.shape))
|
||||
policy = ReparamPolicy(policy)
|
||||
|
||||
logger.add_scalar('expert/mean_episode_length', (~expert_data.dones).sum() / expert_data.states.shape[0])
|
||||
logger.add_scalar('expert/mean_reward_per_episode', expert_data.rewards[~expert_data.dones].sum() / expert_data.states.shape[0])
|
||||
|
||||
for epoch in tqdm(range(epochs)):
|
||||
hl_data, ll_data = rollout(env_fn, policy, rollout_episodes, rollout_steps)
|
||||
generator_data = OptionsRollout(Buffer(*hl_data), Buffer(*ll_data))
|
||||
|
||||
generator_data.ll.actions += 0.1 * torch.randn_like(generator_data.ll.actions)
|
||||
|
||||
logger.add_scalar('gen/mean_episode_length', (~generator_data.ll.dones).sum() / generator_data.ll.states.shape[0], epoch)
|
||||
logger.add_scalar('gen/mean_reward_per_episode', generator_data.hl.rewards[~generator_data.hl.dones].sum() / generator_data.hl.states.shape[0], epoch)
|
||||
|
||||
discriminator, loss = train_discriminator(expert_data, generator_data.ll, discriminator, disc_opt, disc_iters, wasserstein, wasserstein_c)
|
||||
if wasserstein:
|
||||
generator_data.ll.rewards = discriminator(generator_data.ll.states, generator_data.ll.actions)
|
||||
else:
|
||||
generator_data.ll.rewards = -F.logsigmoid(discriminator(generator_data.ll.states, generator_data.ll.actions))
|
||||
logger.add_scalar('disc/final_loss', loss, epoch)
|
||||
logger.add_scalar('disc/mean_reward_per_episode', generator_data.ll.rewards[~generator_data.ll.dones].sum() / generator_data.ll.states.shape[0], epoch)
|
||||
|
||||
#assert generator_data.ll.rewards.shape == generator_data.ll.dones.shape
|
||||
generator_data.hl.rewards = torch.where(~generator_data.ll.dones, generator_data.ll.rewards, torch.tensor(0.)).sum(-1)
|
||||
|
||||
value, policy = trpo_step(value, policy, generator_data.hl.states, generator_data.hl.actions, generator_data.hl.rewards, generator_data.hl.dones, gamma, gae_lambda, delta, backtrack_coeff, backtrack_iters, v_opt, v_iters, cg_iters, cg_damping)
|
||||
expert_data = roll_buffer(expert_data, shifts=-3, dims=0)
|
||||
|
||||
if callback is not None:
|
||||
callback(epoch, value, policy)
|
||||
|
||||
return value, policy
|
||||
|
||||
def gail_ppo(env_fn, expert_data, discriminator, disc_opt, disc_iters, policy, value,
|
||||
v_opt, v_iters, epochs, rollout_episodes, rollout_steps, gamma,
|
||||
gae_lambda, clip_ratio, pi_opt, pi_iters, target_kl=None, max_grad_norm=None, wasserstein=False, wasserstein_c=None, logger=TerminalLogger(), callback=None):
|
||||
|
||||
logger.add_scalar('expert/mean_episode_length', (~expert_data.dones).sum() / expert_data.states.shape[0])
|
||||
logger.add_scalar('expert/mean_reward_per_episode', expert_data.rewards[~expert_data.dones].sum() / expert_data.states.shape[0])
|
||||
|
||||
for epoch in range(epochs):
|
||||
hl_data, ll_data = rollout(env_fn, policy, rollout_episodes, rollout_steps)
|
||||
generator_data = OptionsRollout(Buffer(*hl_data), Buffer(*ll_data))
|
||||
|
||||
generator_data.ll.actions += 0.1 * torch.randn_like(generator_data.ll.actions)
|
||||
|
||||
logger.add_scalar('gen/mean_episode_length', (~generator_data.ll.dones).sum() / generator_data.ll.states.shape[0], epoch)
|
||||
logger.add_scalar('gen/mean_reward_per_episode', generator_data.hl.rewards[~generator_data.hl.dones].sum() / generator_data.hl.states.shape[0], epoch)
|
||||
|
||||
discriminator, loss = train_discriminator(expert_data, generator_data.ll, discriminator, disc_opt, disc_iters, wasserstein, wasserstein_c)
|
||||
if wasserstein:
|
||||
generator_data.ll.rewards = discriminator(generator_data.ll.states, generator_data.ll.actions)
|
||||
else:
|
||||
generator_data.ll.rewards = -F.logsigmoid(discriminator(generator_data.ll.states, generator_data.ll.actions))
|
||||
logger.add_scalar('disc/final_loss', loss, epoch)
|
||||
logger.add_scalar('disc/mean_reward_per_episode', generator_data.ll.rewards[~generator_data.ll.dones].sum() / generator_data.ll.states.shape[0], epoch)
|
||||
|
||||
#assert generator_data.ll.rewards.shape == generator_data.ll.dones.shape
|
||||
generator_data.hl.rewards = torch.where(~generator_data.ll.dones, generator_data.ll.rewards, torch.tensor(0.)).sum(-1)
|
||||
|
||||
value, policy = ppo_step(value, policy, generator_data.hl.states, generator_data.hl.actions, generator_data.hl.rewards, generator_data.hl.dones, clip_ratio, gamma, gae_lambda, pi_opt, pi_iters, v_opt, v_iters, target_kl, max_grad_norm)
|
||||
expert_data = roll_buffer(expert_data, shifts=-3, dims=0)
|
||||
|
||||
if callback is not None:
|
||||
callback(epoch, value, policy)
|
||||
|
||||
return value, policy
|
||||
|
||||
def rollout(env_fn, policy, n_episodes, max_steps_per_episode):
|
||||
env = env_fn(0)
|
||||
|
||||
states = torch.zeros(n_episodes, max_steps_per_episode + 1, *env.observation_space.shape)
|
||||
actions = torch.zeros(n_episodes, max_steps_per_episode + 1, *env.action_space.shape)
|
||||
rewards = torch.zeros(n_episodes, max_steps_per_episode + 1)
|
||||
dones = torch.ones(n_episodes, max_steps_per_episode + 1, dtype=bool)
|
||||
|
||||
ll_states = torch.zeros(n_episodes, max_steps_per_episode, env.max_plan_length + 1, *env.observation_space.shape)
|
||||
ll_actions = torch.zeros(n_episodes, max_steps_per_episode, env.max_plan_length + 1, *env.ll_action_space.shape)
|
||||
ll_rewards = torch.zeros(n_episodes, max_steps_per_episode, env.max_plan_length + 1)
|
||||
ll_dones = torch.ones(n_episodes, max_steps_per_episode, env.max_plan_length + 1, dtype=bool)
|
||||
|
||||
env = VecEnv(list(map(lambda i: (lambda: env_fn(i)), range(n_episodes))))
|
||||
|
||||
states[:, 0] = torch.tensor(env.reset()).clone().detach()
|
||||
|
||||
for s in tqdm(range(max_steps_per_episode), 'Rollout'):
|
||||
actions[:, s] = policy.sample(policy(states[:, s])).clone().detach()
|
||||
|
||||
clipped_actions = actions[:, s]
|
||||
if isinstance(env.action_space, gym.spaces.Box):
|
||||
clipped_actions = torch.clamp(clipped_actions, torch.from_numpy(env.action_space.low), torch.from_numpy(env.action_space.high))
|
||||
|
||||
o, r, d, info = env.step(clipped_actions)
|
||||
states[:, s + 1] = torch.tensor(o).clone().detach()
|
||||
rewards[:, s] = torch.tensor(r).clone().detach()
|
||||
dones[:, s] = torch.tensor(d).clone().detach()
|
||||
|
||||
ll_states[:, s] = torch.from_numpy(np.stack([i['ll']['observations'] for i in info])).clone().detach()
|
||||
ll_actions[:, s] = torch.from_numpy(np.stack([i['ll']['actions'] for i in info])).clone().detach()
|
||||
ll_rewards[:, s] = torch.from_numpy(np.stack([i['ll']['rewards'] for i in info])).clone().detach()
|
||||
ll_dones[:, s] = torch.from_numpy(np.stack([i['ll']['plan_done'] for i in info])).clone().detach()
|
||||
|
||||
dones = dones.cumsum(1) > 0
|
||||
|
||||
states = states[:, :max_steps_per_episode]
|
||||
actions = actions[:, :max_steps_per_episode]
|
||||
rewards = rewards[:, :max_steps_per_episode]
|
||||
dones = dones[:, :max_steps_per_episode]
|
||||
|
||||
return (states, actions, rewards, dones), (ll_states, ll_actions, ll_rewards, ll_dones)
|
||||
|
||||
class OptionsEnv(gym.Wrapper):
|
||||
|
||||
def __init__(self, env, options):
|
||||
super().__init__(env)
|
||||
self.ll_action_space = env.action_space
|
||||
self.options = options
|
||||
self.action_space = gym.spaces.Discrete(len(options))
|
||||
self.max_plan_length = max(t for _, t in options)
|
||||
|
||||
def plan(self, option):
|
||||
target_v, t = option
|
||||
current_v = self.env._env.state[self.env._agent, 1].item()
|
||||
dt = self.env._env._dt
|
||||
a = (target_v - current_v) / (t * dt)
|
||||
a = self.env._normalize(a)
|
||||
a = a * np.ones((t,))
|
||||
a += 0.01 * np.random.randn(*a.shape)
|
||||
a = np.clip(a, self.ll_action_space.low, self.ll_action_space.high)
|
||||
return a
|
||||
|
||||
def execute_plan(self, obs, option, render_mode=None):
|
||||
observations = np.zeros((self.max_plan_length + 1, *self.env.observation_space.shape))
|
||||
actions = np.zeros((self.max_plan_length + 1, *self.ll_action_space.shape))
|
||||
rewards = np.zeros((self.max_plan_length + 1,))
|
||||
env_done = np.ones((self.max_plan_length + 1,), dtype=bool)
|
||||
plan_done = np.ones((self.max_plan_length + 1,), dtype=bool)
|
||||
infos = []
|
||||
|
||||
observations[0] = obs
|
||||
env_done[0] = False
|
||||
for k, u in enumerate(self.plan(option)):
|
||||
plan_done[k] = False
|
||||
o, r, d, i = super().step(u)
|
||||
actions[k] = u
|
||||
rewards[k] = r
|
||||
env_done[k] = d
|
||||
infos.append(i)
|
||||
observations[k+1] = o
|
||||
|
||||
if render_mode is not None:
|
||||
self.env.render(render_mode)
|
||||
|
||||
if d:
|
||||
break
|
||||
|
||||
n_steps = k + 1
|
||||
return observations, actions, rewards, env_done, plan_done, infos, n_steps
|
||||
|
||||
def step(self, action, render_mode=None):
|
||||
a = int(action)
|
||||
assert a == action
|
||||
ll_obs, ll_actions, ll_rewards, ll_env_done, ll_plan_done, ll_infos, ll_steps = self.execute_plan(self.last_obs, self.options[a], render_mode)
|
||||
hl_obs = ll_obs[ll_steps]
|
||||
hl_reward = (ll_rewards * ~ll_plan_done).sum().item()
|
||||
hl_done = ll_env_done[ll_steps-1].item()
|
||||
hl_infos = {
|
||||
'll': {
|
||||
'observations': ll_obs,
|
||||
'actions': ll_actions,
|
||||
'rewards': ll_rewards,
|
||||
'env_done': ll_env_done,
|
||||
'plan_done': ll_plan_done,
|
||||
'infos': ll_infos,
|
||||
'steps': ll_steps,
|
||||
}
|
||||
}
|
||||
self.last_obs = hl_obs
|
||||
return hl_obs, hl_reward, hl_done, hl_infos
|
||||
|
||||
def reset(self, *args, **kwargs):
|
||||
self.last_obs = super().reset(*args, **kwargs)
|
||||
return self.last_obs
|
||||
54
src/options/test_options.py
Normal file
54
src/options/test_options.py
Normal file
@@ -0,0 +1,54 @@
|
||||
from intersim.envs import IntersimpleLidarFlat
|
||||
from options import OptionsEnv
|
||||
import gym
|
||||
import numpy as np
|
||||
|
||||
def test_obs_shape():
|
||||
options = [(0, 5), (5, 5), (10, 5)]
|
||||
env = OptionsEnv(IntersimpleLidarFlat(n_rays=5), options)
|
||||
assert env.reset().shape == (36,)
|
||||
|
||||
def test_act_space():
|
||||
options = [(0, 5), (5, 5), (10, 5)]
|
||||
env = OptionsEnv(IntersimpleLidarFlat(n_rays=5), options)
|
||||
assert env.action_space == gym.spaces.Discrete(3)
|
||||
|
||||
def test_plan():
|
||||
options = [(0, 5), (5, 5), (10, 5)]
|
||||
env = OptionsEnv(IntersimpleLidarFlat(n_rays=5), options)
|
||||
env.reset()
|
||||
plan = env.plan(options[0])
|
||||
assert np.allclose(plan, -13.998268127441406 * np.ones((5,)))
|
||||
|
||||
def test_plan2():
|
||||
options = [(0, 5), (5, 5), (10, 5)]
|
||||
env = OptionsEnv(IntersimpleLidarFlat(n_rays=5), options)
|
||||
obs = env.reset()
|
||||
states, actions, rewards, dones, plan_done, infos, n_steps = env.execute_plan(obs, options[0])
|
||||
assert states.shape == (6, 36)
|
||||
assert rewards.shape == (6,)
|
||||
assert dones.shape == (6,)
|
||||
assert len(infos) == 5
|
||||
|
||||
def test_step():
|
||||
options = [(0, 5), (5, 5), (10, 5)]
|
||||
env = OptionsEnv(IntersimpleLidarFlat(n_rays=5), options)
|
||||
env.reset()
|
||||
obs, reward, done, _ = env.step(0)
|
||||
assert obs.shape == (36,)
|
||||
assert reward == 5.0
|
||||
assert done == False
|
||||
|
||||
def test_ll_step():
|
||||
options = [(0, 5), (5, 5), (10, 5)]
|
||||
env = OptionsEnv(IntersimpleLidarFlat(n_rays=5), options)
|
||||
env.reset()
|
||||
_, _, _, info = env.step(0)
|
||||
assert info['ll']['observations'].shape == (6, 36)
|
||||
assert info['ll']['actions'].shape == (6, 1)
|
||||
assert info['ll']['rewards'].shape == (6,)
|
||||
assert info['ll']['env_done'].shape == (6,)
|
||||
assert info['ll']['plan_done'].shape == (6,)
|
||||
assert info['ll']['plan_done'][5] == True
|
||||
assert info['ll']['steps'] == 5
|
||||
assert len(info['ll']['infos']) == 5
|
||||
185
src/safe_options/collisions.py
Normal file
185
src/safe_options/collisions.py
Normal file
@@ -0,0 +1,185 @@
|
||||
import torch
|
||||
import numpy as np
|
||||
from intersim.collisions import state_to_polygon
|
||||
|
||||
def safety_plan(env, plan):
|
||||
return np.concatenate((plan, np.array(5 * [env._env._min_acc])), axis=0)
|
||||
|
||||
def available_actions(env, options):
|
||||
"""Return mask of available actions given current `env` state."""
|
||||
plans = [generate_plan(env, i, options) for i, _ in enumerate(options)]
|
||||
# is emergency braking still possible?
|
||||
plans = list(map(lambda p: safety_plan(env, p), plans))
|
||||
|
||||
T = max(len(p) for p in plans)
|
||||
plans = [np.pad(p, ((0, T-len(p)),), constant_values=np.nan) for p in plans]
|
||||
plans = np.stack(plans, axis=0)
|
||||
|
||||
valid = feasible(env, plans)
|
||||
return valid
|
||||
|
||||
def target_velocity_plan(current_v: float, target_v: float, t: int, dt: float):
|
||||
"""Smoothly target a velocity in a given number of steps"""
|
||||
# for now, constant acceleration
|
||||
a = (target_v - current_v) / (t * dt)
|
||||
return a*np.ones((t,))
|
||||
|
||||
def generate_plan(env, i, options):
|
||||
"""Generate input profile for high-level action `i`."""
|
||||
assert i < len(options), "Invalid option index {i}"
|
||||
target_v, t = options[i]
|
||||
current_v = env._env.state[env._agent, 1].item() # extract from env
|
||||
plan = target_velocity_plan(current_v, target_v, t, env._env._dt)
|
||||
assert len(plan) == t, "incorrect plan length"
|
||||
return plan
|
||||
|
||||
def feasible(env, plan, method='exact'):
|
||||
"""Check if input profile is feasible given current `env` state."""
|
||||
# zero pad plan - Take (B, T) or (T,) np plan and convert it to (B, T, nv, 1) torch.Tensor
|
||||
plan = torch.tensor(plan)
|
||||
plan = plan.reshape(-1, plan.shape[-1])
|
||||
full_plan = torch.zeros(*plan.shape, env._env._nv, 1)
|
||||
full_plan[:, :, env._agent, 0] = plan
|
||||
|
||||
# check_future_collisions_fast takes in B-list and outputs (B,) bool tensor
|
||||
if method=='circle':
|
||||
valid = check_future_collisions_fast(env, full_plan)
|
||||
elif method=='ncircles':
|
||||
valid = check_future_collisions_ncircles(env, full_plan)
|
||||
elif method=='exact':
|
||||
valid = check_future_collisions_exact(env, full_plan)
|
||||
else:
|
||||
raise NotImplementedError('Invalid collision-checking method')
|
||||
|
||||
return valid
|
||||
|
||||
def check_future_collisions_ncircles(env, actions, n_circles:int=2):
|
||||
"""Checks whether `env._agent` would collide with other agents assuming `actions` as input.
|
||||
|
||||
Vehicles are (over-)approximated by multiple circles.
|
||||
|
||||
Args:
|
||||
env (gym.Env): current environment state
|
||||
actions (list of torch.Tensor): list of B (T, nv, adims) T-length action profiles
|
||||
Returns:
|
||||
feasible (torch.Tensor): tensor of shape (B,) indicating whether the respective action profiles are collision-free
|
||||
"""
|
||||
assert n_circles >= 2
|
||||
B, (T, nv, _) = len(actions), actions[0].shape
|
||||
|
||||
states = env._env.propagate_action_profile_vectorized(actions)
|
||||
assert states.shape == (B, T, nv, 5)
|
||||
centers = states[:, :, :, :2]
|
||||
psi = states[:, :, :, 3]
|
||||
lon = torch.stack([psi.cos(), psi.sin()],dim=-1) # (B, T, nv, 2)
|
||||
|
||||
# offset between [-env._env.lengths+env._env.widths/2, env._env.lengths/2-env._env.widths/2]
|
||||
back = (-env._env._lengths/2+env._env._widths/2).unsqueeze(-1) # (nv, 1)
|
||||
length = (env._env._lengths-env._env._widths).unsqueeze(-1) # (nv, 1)
|
||||
diff_d = back + length*(torch.arange(n_circles)/(n_circles-1)).unsqueeze(0) # (nv, n_circles)
|
||||
assert diff_d.shape == (nv, n_circles)
|
||||
|
||||
offsets = diff_d[None, None, :, :, None] * lon[:, :, :, None, :]
|
||||
assert offsets.shape == (B, T, nv, n_circles, 2)
|
||||
|
||||
expanded_centers=centers.unsqueeze(-2) + offsets #(B, T, nv, n_circles, 2)
|
||||
assert expanded_centers.shape == (B, T, nv, n_circles, 2)
|
||||
agent_centers = expanded_centers[:,:,env._agent:env._agent+1,:,:] #(B, T, 1, n_circles, 2)
|
||||
ds = expanded_centers.reshape((B, T, nv*n_circles, 1, 2)) - agent_centers #(B, T, nv*nc,1, 2) - (B, T, 1, nc, 2) = (B, T, nv*nc, nc, 2)
|
||||
|
||||
distance = (ds**2).sum(-1).sqrt().reshape((B, T, nv, n_circles, n_circles)) # (B, T, nv, nc, nc)
|
||||
distance = torch.where(distance.isnan(), np.inf*torch.ones_like(distance), distance) # only collide with spawned agents
|
||||
distance[:, :, env._agent] = np.inf # cannot collide with itself
|
||||
assert distance.shape == (B, T, nv, n_circles, n_circles)
|
||||
|
||||
radius = env._env._widths*np.sqrt(2) / 2
|
||||
min_distance = radius[env._agent] + radius
|
||||
min_distance = min_distance[None, None, :, None, None]
|
||||
assert min_distance.shape == (1, 1, nv, 1, 1)
|
||||
|
||||
return (distance > min_distance).all(-1).all(-1).all(-1).all(-1)
|
||||
|
||||
def check_future_collisions_circle(env, actions):
|
||||
"""Compute collision information for circular vehicle approximations
|
||||
|
||||
Args:
|
||||
env (gym.Env): current environment state
|
||||
actions (list of torch.Tensor): list of B (T, nv, adims) T-length action profiles
|
||||
Returns:
|
||||
states (torch.Tensor): tensor of shape (B, T, nv, 5) of future states based on the action profiles
|
||||
collision_tensor (torch.Tensor): tensor of shape (B, T, nv) of bools indicating which plan collides with which vehicles in which time frame
|
||||
false: colliding, true: not colliding
|
||||
"""
|
||||
B, (T, nv, _) = len(actions), actions[0].shape
|
||||
|
||||
states = env._env.propagate_action_profile_vectorized(actions)
|
||||
assert states.shape == (B, T, nv, 5)
|
||||
|
||||
distance = ((states[:, :, :, :2] - states[:, :, env._agent:env._agent+1, :2])**2).sum(-1).sqrt()
|
||||
distance = torch.where(distance.isnan(), np.inf*torch.ones_like(distance), distance) # only collide with spawned agents
|
||||
distance[:, :, env._agent] = np.inf # cannot collide with itself
|
||||
assert distance.shape == (B, T, nv)
|
||||
|
||||
radius = (env._env._lengths**2 + env._env._widths**2).sqrt() / 2
|
||||
min_distance = radius[env._agent] + radius
|
||||
min_distance = min_distance.unsqueeze(0).unsqueeze(0)
|
||||
assert min_distance.shape == (1, 1, nv)
|
||||
|
||||
collision_tensor = distance > min_distance
|
||||
assert collision_tensor.shape == (B, T, nv)
|
||||
return states, collision_tensor
|
||||
|
||||
def check_future_collisions_fast(env, actions):
|
||||
"""Checks whether `env._agent` would collide with other agents assuming `actions` as input.
|
||||
|
||||
Vehicles are (over-)approximated by single circles.
|
||||
|
||||
Args:
|
||||
env (gym.Env): current environment state
|
||||
actions (list of torch.Tensor): list of B (T, nv, adims) T-length action profiles
|
||||
Returns:
|
||||
feasible (torch.Tensor): tensor of shape (B,) indicating whether the respective action profiles are collision-free
|
||||
"""
|
||||
_, collision_tensor = check_future_collisions_circle(env, actions)
|
||||
return collision_tensor.all(-1).all(-1)
|
||||
|
||||
def check_future_collisions_exact(env, actions):
|
||||
"""
|
||||
Checks whether `env._agent` would collide with other agents assuming `actions` as input.
|
||||
|
||||
Args:
|
||||
env (gym.Env): current environment state
|
||||
actions (list of torch.Tensor): list of B (T, nv, adims) T-length action profiles
|
||||
Returns:
|
||||
feasible (torch.Tensor): tensor of shape (B,) indicating whether the respective action profiles are collision-free
|
||||
"""
|
||||
# First check with simple circle collision check
|
||||
states, collision_tensor = check_future_collisions_circle(env, actions)
|
||||
(B, T, nv, _) = states.shape
|
||||
# For those that have colliding circles, check exactly
|
||||
colliding_mask = ~collision_tensor
|
||||
|
||||
ego_states = states[:, :, env._agent:env._agent+1, :].expand(states.shape)
|
||||
assert ego_states.shape == states.shape
|
||||
|
||||
# get dimensions
|
||||
lengths = env._env._lengths.expand(states.shape[:3])
|
||||
widths = env._env._widths.expand(states.shape[:3])
|
||||
ego_lengths = lengths[:, :, env._agent:env._agent+1].expand(lengths.shape)
|
||||
ego_widths = widths[:, :, env._agent:env._agent+1].expand(widths.shape)
|
||||
assert lengths.shape == widths.shape == ego_lengths.shape == ego_widths.shape == (B, T, nv)
|
||||
|
||||
# For every collision instance between ego and other vehicle, check whether rectangles intersect
|
||||
exact_collisions = torch.zeros_like(collision_tensor[colliding_mask])
|
||||
for i, (ego_state, ego_length, ego_width, other_state, other_length, other_width) in enumerate(zip(
|
||||
ego_states[colliding_mask], ego_lengths[colliding_mask], ego_widths[colliding_mask],
|
||||
states[colliding_mask], lengths[colliding_mask], widths[colliding_mask]
|
||||
)):
|
||||
assert ego_state.shape == other_state.shape == (5,)
|
||||
assert ego_length.shape == ego_width.shape == other_length.shape == other_width.shape == ()
|
||||
p_ego = state_to_polygon(ego_state, ego_length, ego_width)
|
||||
p_other = state_to_polygon(other_state, other_length, other_width)
|
||||
exact_collisions[i] = p_ego.intersects(p_other)
|
||||
|
||||
collision_tensor[colliding_mask] = ~exact_collisions
|
||||
return collision_tensor.all(-1).all(-1)
|
||||
305
src/safe_options/options.py
Normal file
305
src/safe_options/options.py
Normal file
@@ -0,0 +1,305 @@
|
||||
import gym
|
||||
import numpy as np
|
||||
import torch
|
||||
from stable_baselines3.common.vec_env import DummyVecEnv as VecEnv
|
||||
|
||||
from core.reparam_module import ReparamPolicy
|
||||
from tqdm import tqdm
|
||||
from core.gail import train_discriminator, roll_buffer, TerminalLogger
|
||||
from dataclasses import dataclass
|
||||
from safe_options.policy_gradient import trpo_step, ppo_step
|
||||
import torch.nn.functional as F
|
||||
|
||||
from safe_options.collisions import feasible
|
||||
|
||||
@dataclass
|
||||
class Buffer:
|
||||
states: torch.Tensor
|
||||
actions: torch.Tensor
|
||||
rewards: torch.Tensor
|
||||
dones: torch.Tensor
|
||||
|
||||
@dataclass
|
||||
class HLBuffer:
|
||||
states: torch.Tensor
|
||||
safe_actions: torch.Tensor
|
||||
actions: torch.Tensor
|
||||
rewards: torch.Tensor
|
||||
dones: torch.Tensor
|
||||
|
||||
@dataclass
|
||||
class OptionsRollout:
|
||||
hl: HLBuffer
|
||||
ll: Buffer
|
||||
|
||||
def gail(env_fn, expert_data, discriminator, disc_opt, disc_iters, policy, value,
|
||||
v_opt, v_iters, epochs, rollout_episodes, rollout_steps, gamma,
|
||||
gae_lambda, delta, backtrack_coeff, backtrack_iters, cg_iters=10, cg_damping=0.1, wasserstein=False, wasserstein_c=None, logger=TerminalLogger(), callback=None):
|
||||
|
||||
policy(torch.zeros(env_fn(0).observation_space['observation'].shape), torch.zeros(env_fn(0).observation_space['safe_actions'].shape))
|
||||
policy = ReparamPolicy(policy)
|
||||
|
||||
logger.add_scalar('expert/mean_episode_length', (~expert_data.dones).sum() / expert_data.states.shape[0])
|
||||
logger.add_scalar('expert/mean_reward_per_episode', expert_data.rewards[~expert_data.dones].sum() / expert_data.states.shape[0])
|
||||
|
||||
for epoch in tqdm(range(epochs)):
|
||||
hl_data, ll_data = rollout(env_fn, policy, rollout_episodes, rollout_steps)
|
||||
generator_data = OptionsRollout(HLBuffer(*hl_data), Buffer(*ll_data))
|
||||
|
||||
generator_data.ll.actions += 0.1 * torch.randn_like(generator_data.ll.actions)
|
||||
|
||||
logger.add_scalar('gen/mean_episode_length', (~generator_data.ll.dones).sum() / generator_data.ll.states.shape[0], epoch)
|
||||
logger.add_scalar('gen/mean_reward_per_episode', generator_data.hl.rewards[~generator_data.hl.dones].sum() / generator_data.hl.states.shape[0], epoch)
|
||||
logger.add_scalar('gen/unsafe_probability_mass', policy.unsafe_probability_mass(policy(generator_data.hl.states[~generator_data.hl.dones], generator_data.hl.safe_actions[~generator_data.hl.dones])).mean(), epoch)
|
||||
|
||||
discriminator, loss = train_discriminator(expert_data, generator_data.ll, discriminator, disc_opt, disc_iters, wasserstein, wasserstein_c)
|
||||
if wasserstein:
|
||||
generator_data.ll.rewards = discriminator(generator_data.ll.states, generator_data.ll.actions)
|
||||
else:
|
||||
generator_data.ll.rewards = -F.logsigmoid(discriminator(generator_data.ll.states, generator_data.ll.actions))
|
||||
logger.add_scalar('disc/final_loss', loss, epoch)
|
||||
logger.add_scalar('disc/mean_reward_per_episode', generator_data.ll.rewards[~generator_data.ll.dones].sum() / generator_data.ll.states.shape[0], epoch)
|
||||
|
||||
#assert generator_data.ll.rewards.shape == generator_data.ll.dones.shape
|
||||
generator_data.hl.rewards = torch.where(~generator_data.ll.dones, generator_data.ll.rewards, torch.tensor(0.)).sum(-1)
|
||||
|
||||
value, policy = trpo_step(value, policy, generator_data.hl.states, generator_data.hl.safe_actions, generator_data.hl.actions, generator_data.hl.rewards, generator_data.hl.dones, gamma, gae_lambda, delta, backtrack_coeff, backtrack_iters, v_opt, v_iters, cg_iters, cg_damping)
|
||||
expert_data = roll_buffer(expert_data, shifts=-3, dims=0)
|
||||
|
||||
if callback is not None:
|
||||
callback(epoch, value, policy)
|
||||
|
||||
return value, policy
|
||||
|
||||
def gail_ppo(env_fn, expert_data, discriminator, disc_opt, disc_iters, policy, value,
|
||||
v_opt, v_iters, epochs, rollout_episodes, rollout_steps, gamma,
|
||||
gae_lambda, clip_ratio, pi_opt, pi_iters, target_kl=None, max_grad_norm=None, wasserstein=False, wasserstein_c=None, logger=TerminalLogger(), callback=None):
|
||||
|
||||
logger.add_scalar('expert/mean_episode_length', (~expert_data.dones).sum() / expert_data.states.shape[0])
|
||||
logger.add_scalar('expert/mean_reward_per_episode', expert_data.rewards[~expert_data.dones].sum() / expert_data.states.shape[0])
|
||||
|
||||
for epoch in range(epochs):
|
||||
hl_data, ll_data = rollout(env_fn, policy, rollout_episodes, rollout_steps)
|
||||
generator_data = OptionsRollout(HLBuffer(*hl_data), Buffer(*ll_data))
|
||||
|
||||
generator_data.ll.actions += 0.1 * torch.randn_like(generator_data.ll.actions)
|
||||
|
||||
logger.add_scalar('gen/mean_episode_length', (~generator_data.ll.dones).sum() / generator_data.ll.states.shape[0], epoch)
|
||||
logger.add_scalar('gen/mean_reward_per_episode', generator_data.hl.rewards[~generator_data.hl.dones].sum() / generator_data.hl.states.shape[0], epoch)
|
||||
logger.add_scalar('gen/unsafe_probability_mass', policy.unsafe_probability_mass(policy(generator_data.hl.states[~generator_data.hl.dones], generator_data.hl.safe_actions[~generator_data.hl.dones])).mean(), epoch)
|
||||
|
||||
discriminator, loss = train_discriminator(expert_data, generator_data.ll, discriminator, disc_opt, disc_iters, wasserstein, wasserstein_c)
|
||||
if wasserstein:
|
||||
generator_data.ll.rewards = discriminator(generator_data.ll.states, generator_data.ll.actions)
|
||||
else:
|
||||
generator_data.ll.rewards = -F.logsigmoid(discriminator(generator_data.ll.states, generator_data.ll.actions))
|
||||
logger.add_scalar('disc/final_loss', loss, epoch)
|
||||
logger.add_scalar('disc/mean_reward_per_episode', generator_data.ll.rewards[~generator_data.ll.dones].sum() / generator_data.ll.states.shape[0], epoch)
|
||||
|
||||
#assert generator_data.ll.rewards.shape == generator_data.ll.dones.shape
|
||||
generator_data.hl.rewards = torch.where(~generator_data.ll.dones, generator_data.ll.rewards, torch.tensor(0.)).sum(-1)
|
||||
|
||||
value, policy = ppo_step(value, policy, generator_data.hl.states, generator_data.hl.safe_actions, generator_data.hl.actions, generator_data.hl.rewards, generator_data.hl.dones, clip_ratio, gamma, gae_lambda, pi_opt, pi_iters, v_opt, v_iters, target_kl, max_grad_norm)
|
||||
expert_data = roll_buffer(expert_data, shifts=-3, dims=0)
|
||||
|
||||
if callback is not None:
|
||||
callback(epoch, value, policy)
|
||||
|
||||
return value, policy
|
||||
|
||||
def rollout(env_fn, policy, n_episodes, max_steps_per_episode):
|
||||
env = env_fn(0)
|
||||
|
||||
states = torch.zeros(n_episodes, max_steps_per_episode + 1, *env.observation_space['observation'].shape)
|
||||
safe_actions = torch.zeros(n_episodes, max_steps_per_episode + 1, *env.observation_space['safe_actions'].shape)
|
||||
actions = torch.zeros(n_episodes, max_steps_per_episode + 1, *env.action_space.shape)
|
||||
rewards = torch.zeros(n_episodes, max_steps_per_episode + 1)
|
||||
dones = torch.ones(n_episodes, max_steps_per_episode + 1, dtype=bool)
|
||||
|
||||
ll_states = torch.zeros(n_episodes, max_steps_per_episode, env.max_plan_length + 1, *env.observation_space['observation'].shape)
|
||||
ll_actions = torch.zeros(n_episodes, max_steps_per_episode, env.max_plan_length + 1, *env.ll_action_space.shape)
|
||||
ll_rewards = torch.zeros(n_episodes, max_steps_per_episode, env.max_plan_length + 1)
|
||||
ll_dones = torch.ones(n_episodes, max_steps_per_episode, env.max_plan_length + 1, dtype=bool)
|
||||
|
||||
env = VecEnv(list(map(lambda i: (lambda: env_fn(i)), range(n_episodes))))
|
||||
|
||||
obs = env.reset()
|
||||
states[:, 0] = torch.tensor(obs['observation']).clone().detach()
|
||||
safe_actions[:, 0] = torch.tensor(obs['safe_actions']).clone().detach()
|
||||
dones[:, 0] = False
|
||||
|
||||
for s in tqdm(range(max_steps_per_episode), 'Rollout'):
|
||||
actions[:, s] = policy.sample(policy(states[:, s], safe_actions[:, s])).clone().detach()
|
||||
|
||||
clipped_actions = actions[:, s]
|
||||
if isinstance(env.action_space, gym.spaces.Box):
|
||||
clipped_actions = torch.clamp(clipped_actions, torch.from_numpy(env.action_space.low), torch.from_numpy(env.action_space.high))
|
||||
|
||||
o, r, d, info = env.step(clipped_actions)
|
||||
states[:, s + 1] = torch.tensor(o['observation']).clone().detach()
|
||||
safe_actions[:, s + 1] = torch.tensor(o['safe_actions']).clone().detach()
|
||||
rewards[:, s] = torch.tensor(r).clone().detach()
|
||||
dones[:, s + 1] = torch.tensor(d).clone().detach()
|
||||
|
||||
ll_states[:, s] = torch.from_numpy(np.stack([i['ll']['observations'] for i in info])).clone().detach()
|
||||
ll_actions[:, s] = torch.from_numpy(np.stack([i['ll']['actions'] for i in info])).clone().detach()
|
||||
ll_rewards[:, s] = torch.from_numpy(np.stack([i['ll']['rewards'] for i in info])).clone().detach()
|
||||
ll_dones[:, s] = torch.from_numpy(np.stack([i['ll']['plan_done'] for i in info])).clone().detach()
|
||||
|
||||
dones = dones.cumsum(1) > 0
|
||||
|
||||
states = states[:, :max_steps_per_episode]
|
||||
safe_actions = safe_actions[:, :max_steps_per_episode]
|
||||
actions = actions[:, :max_steps_per_episode]
|
||||
rewards = rewards[:, :max_steps_per_episode]
|
||||
dones = dones[:, :max_steps_per_episode]
|
||||
|
||||
return (states, safe_actions, actions, rewards, dones), (ll_states, ll_actions, ll_rewards, ll_dones)
|
||||
|
||||
class OptionsEnv(gym.Wrapper):
|
||||
|
||||
def __init__(self, env, options):
|
||||
super().__init__(env)
|
||||
self.ll_action_space = env.action_space
|
||||
self.options = options
|
||||
self.action_space = gym.spaces.Discrete(len(options))
|
||||
self.max_plan_length = max(t for _, t in options)
|
||||
|
||||
def plan(self, option):
|
||||
target_v, t = option
|
||||
current_v = self.env._env.state[self.env._agent, 1].item()
|
||||
dt = self.env._env._dt
|
||||
a = (target_v - current_v) / (t * dt)
|
||||
a = self.env._normalize(a)
|
||||
a = a * np.ones((t,))
|
||||
a += 0.01 * np.random.randn(*a.shape)
|
||||
a = np.clip(a, self.ll_action_space.low, self.ll_action_space.high)
|
||||
return a
|
||||
|
||||
def execute_plan(self, obs, option, render_mode=None):
|
||||
observations = np.zeros((self.max_plan_length + 1, *self.env.observation_space.shape))
|
||||
actions = np.zeros((self.max_plan_length + 1, *self.ll_action_space.shape))
|
||||
rewards = np.zeros((self.max_plan_length + 1,))
|
||||
env_done = np.ones((self.max_plan_length + 1,), dtype=bool)
|
||||
plan_done = np.ones((self.max_plan_length + 1,), dtype=bool)
|
||||
infos = []
|
||||
|
||||
plan = self.plan(option)
|
||||
observations[0] = obs
|
||||
env_done[0] = False
|
||||
for k, u in enumerate(plan):
|
||||
plan_done[k] = False
|
||||
o, r, d, i = self.env.step(u)
|
||||
actions[k] = u
|
||||
rewards[k] = r
|
||||
env_done[k+1] = d
|
||||
infos.append(i)
|
||||
observations[k+1] = o
|
||||
|
||||
if render_mode is not None:
|
||||
self.env.render(render_mode)
|
||||
|
||||
if d:
|
||||
break
|
||||
|
||||
n_steps = k + 1
|
||||
return observations, actions, rewards, env_done, plan_done, infos, n_steps
|
||||
|
||||
def step(self, action, render_mode=None):
|
||||
a = int(action)
|
||||
assert a == action
|
||||
ll_obs, ll_actions, ll_rewards, ll_env_done, ll_plan_done, ll_infos, ll_steps = self.execute_plan(self.last_obs, self.options[a], render_mode)
|
||||
hl_obs = ll_obs[ll_steps]
|
||||
hl_reward = (ll_rewards * ~ll_plan_done).sum().item()
|
||||
hl_done = ll_env_done[ll_steps].item()
|
||||
hl_infos = {
|
||||
'll': {
|
||||
'observations': ll_obs,
|
||||
'actions': ll_actions,
|
||||
'rewards': ll_rewards,
|
||||
'env_done': ll_env_done,
|
||||
'plan_done': ll_plan_done,
|
||||
'infos': ll_infos,
|
||||
'steps': ll_steps,
|
||||
}
|
||||
}
|
||||
self.last_obs = hl_obs
|
||||
return hl_obs, hl_reward, hl_done, hl_infos
|
||||
|
||||
def reset(self, *args, **kwargs):
|
||||
self.last_obs = super().reset(*args, **kwargs)
|
||||
return self.last_obs
|
||||
|
||||
|
||||
class SafeOptionsEnv(OptionsEnv):
|
||||
|
||||
def __init__(self, env, options, safe_actions_collision_method=None, abort_unsafe_collision_method=None):
|
||||
super().__init__(env, options)
|
||||
self.safe_actions_collision_method = safe_actions_collision_method
|
||||
self.abort_unsafe_collision_method = abort_unsafe_collision_method
|
||||
self.observation_space = gym.spaces.Dict({
|
||||
'observation': self.observation_space,
|
||||
'safe_actions': gym.spaces.Box(low=0., high=1., shape=(self.action_space.n,)),
|
||||
})
|
||||
|
||||
def safe_actions(self):
|
||||
if self.safe_actions_collision_method is None:
|
||||
return np.ones(len(self.options), dtype=bool)
|
||||
|
||||
plans = [self.plan(o) for o in self.options]
|
||||
plans = np.stack(plans)
|
||||
safe = feasible(self.env, plans, method=self.safe_actions_collision_method)
|
||||
if not safe.any():
|
||||
# action 0 is considered safe fallback
|
||||
safe[0] = True
|
||||
|
||||
return safe
|
||||
|
||||
def reset(self, *args, **kwargs):
|
||||
obs = super().reset(*args, **kwargs)
|
||||
obs = {
|
||||
'observation': obs,
|
||||
'safe_actions': self.safe_actions(),
|
||||
}
|
||||
return obs
|
||||
|
||||
def step(self, action, render_mode=None):
|
||||
obs, reward, done, info = super().step(action, render_mode)
|
||||
obs = {
|
||||
'observation': obs,
|
||||
'safe_actions': self.safe_actions(),
|
||||
}
|
||||
return obs, reward, done, info
|
||||
|
||||
def execute_plan(self, obs, option, render_mode=None):
|
||||
observations = np.zeros((self.max_plan_length + 1, *self.env.observation_space.shape))
|
||||
actions = np.zeros((self.max_plan_length + 1, *self.ll_action_space.shape))
|
||||
rewards = np.zeros((self.max_plan_length + 1,))
|
||||
env_done = np.ones((self.max_plan_length + 1,), dtype=bool)
|
||||
plan_done = np.ones((self.max_plan_length + 1,), dtype=bool)
|
||||
infos = []
|
||||
|
||||
plan = self.plan(option)
|
||||
observations[0] = obs
|
||||
env_done[0] = False
|
||||
for k, u in enumerate(plan):
|
||||
plan_done[k] = False
|
||||
o, r, d, i = self.env.step(u)
|
||||
actions[k] = u
|
||||
rewards[k] = r
|
||||
env_done[k+1] = d
|
||||
infos.append(i)
|
||||
observations[k+1] = o
|
||||
|
||||
if render_mode is not None:
|
||||
self.env.render(render_mode)
|
||||
|
||||
if d:
|
||||
break
|
||||
|
||||
if self.abort_unsafe_collision_method is not None and \
|
||||
not feasible(self.env, plan[k:], method=self.abort_unsafe_collision_method):
|
||||
break
|
||||
|
||||
n_steps = k + 1
|
||||
return observations, actions, rewards, env_done, plan_done, infos, n_steps
|
||||
35
src/safe_options/policy.py
Normal file
35
src/safe_options/policy.py
Normal file
@@ -0,0 +1,35 @@
|
||||
import torch
|
||||
import torch.nn as nn
|
||||
from torch.distributions import Categorical
|
||||
from torch.distributions.kl import kl_divergence
|
||||
from core.policy import SetDiscretePolicy
|
||||
|
||||
class SetMaskedDiscretePolicy(SetDiscretePolicy):
|
||||
|
||||
def forward(self, observation, safe_actions):
|
||||
return torch.cat((super().forward(observation), safe_actions), -1)
|
||||
|
||||
def torch_dist(self, dist):
|
||||
logits = dist[..., :self.action_dim]
|
||||
z = dist[..., self.action_dim:]
|
||||
a = super().torch_dist(logits).probs
|
||||
return Categorical(probs=a*z)
|
||||
|
||||
def unsafe_probability_mass(self, dist):
|
||||
logits = dist[..., :self.action_dim]
|
||||
z = dist[..., self.action_dim:]
|
||||
a = super().torch_dist(logits).probs
|
||||
return (a * (1 - z)).sum(-1)
|
||||
|
||||
# def torch_dist_nomask(self, dist):
|
||||
# print('no mask logprob')
|
||||
# logits = dist[..., :self.action_dim]
|
||||
# return super().torch_dist(logits)
|
||||
|
||||
# def log_prob(self, dist, actions):
|
||||
# return self.torch_dist_nomask(dist).log_prob(actions)
|
||||
|
||||
# def kl_divergence(self, dist1, dist2):
|
||||
# d1 = self.torch_dist_nomask(dist1)
|
||||
# d2 = self.torch_dist_nomask(dist2)
|
||||
# return kl_divergence(d1, d2)
|
||||
107
src/safe_options/policy_gradient.py
Normal file
107
src/safe_options/policy_gradient.py
Normal file
@@ -0,0 +1,107 @@
|
||||
import torch
|
||||
from core.value_estimation import gae
|
||||
from core.optimization import conjugate_gradient, line_search
|
||||
|
||||
def trpo_step(value, policy, states, safe_actions, actions, rewards, dones, gamma, gae_lambda, delta, backtrack_coeff, backtrack_iters, v_opt, v_iters, cg_iters=10, cg_damping=0.1):
|
||||
|
||||
states = states.detach()
|
||||
actions = actions.detach()
|
||||
rewards = rewards.detach()
|
||||
dones = dones.detach()
|
||||
|
||||
advantages, returns, valid = gae(states, rewards, value(states), dones, gamma, gae_lambda)
|
||||
advantages = advantages.detach()
|
||||
returns = returns.detach()
|
||||
|
||||
# update value function
|
||||
|
||||
for _ in range(v_iters):
|
||||
v_opt.zero_grad()
|
||||
value_loss = (value(states) - returns).pow(2)[valid].mean()
|
||||
value_loss.backward()
|
||||
v_opt.step()
|
||||
|
||||
# compute policy gradient
|
||||
|
||||
plogprob = policy.log_prob(policy(states, safe_actions), actions)
|
||||
surrogate_advantage = (plogprob * advantages)[valid].sum() / states.shape[0]
|
||||
g = torch.cat(torch.autograd.grad(surrogate_advantage, policy.flat_param)).detach()
|
||||
|
||||
def Hx(x):
|
||||
kl = policy.kl_divergence(policy(states, safe_actions), policy(states, safe_actions).detach())[valid].mean()
|
||||
dKL = torch.cat(torch.autograd.grad(kl, policy.flat_param, create_graph=True))
|
||||
H_x = torch.cat(torch.autograd.grad(dKL.T @ x, policy.flat_param)).detach()
|
||||
return H_x + cg_damping * x
|
||||
|
||||
x = conjugate_gradient(Hx, g, cg_iters)
|
||||
npg = torch.sqrt(2 * delta / (x.T @ Hx(x))) * x
|
||||
|
||||
# perform line search
|
||||
|
||||
def L(theta):
|
||||
rplogprob = policy.log_prob(policy(states, safe_actions, flat_param=theta), actions)
|
||||
return ((rplogprob - plogprob.detach()).exp() * advantages)[valid].sum() / advantages.shape[0]
|
||||
|
||||
condition = lambda theta: policy.kl_divergence(policy(states, safe_actions, flat_param=theta), policy(states, safe_actions))[valid].mean() < delta
|
||||
|
||||
x0 = policy.flat_param
|
||||
g0 = torch.cat(torch.autograd.grad(L(x0), x0))
|
||||
theta = line_search(L, x0, npg, g0, backtrack_coeff, condition, max_steps=backtrack_iters)
|
||||
|
||||
# update policy parameters
|
||||
|
||||
with torch.no_grad():
|
||||
policy.flat_param.copy_(theta)
|
||||
|
||||
return value, policy
|
||||
|
||||
def ppo_step(value, policy, states, safe_actions, actions, rewards, dones, clip_ratio, gamma, gae_lambda, pi_opt, pi_iters, v_opt, v_iters, target_kl, max_grad_norm):
|
||||
|
||||
states = states.detach()
|
||||
actions = actions.detach()
|
||||
rewards = rewards.detach()
|
||||
dones = dones.detach()
|
||||
|
||||
advantages, returns, valid = gae(states, rewards, value(states), dones, gamma, gae_lambda)
|
||||
advantages = advantages.detach()
|
||||
returns = returns.detach()
|
||||
|
||||
# update value function
|
||||
|
||||
for _ in range(v_iters):
|
||||
v_opt.zero_grad()
|
||||
value_loss = (value(states) - returns).pow(2)[valid].mean()
|
||||
value_loss.backward()
|
||||
v_opt.step()
|
||||
|
||||
# update policy
|
||||
|
||||
old_dist = policy(states, safe_actions).detach()
|
||||
old_logprob = policy.log_prob(old_dist, actions).detach()
|
||||
|
||||
def g(advantages, clip_ratio):
|
||||
return torch.where(advantages >= 0, (1 + clip_ratio) * advantages, (1 - clip_ratio) * advantages)
|
||||
|
||||
def L(states, actions, advantages, clip_ratio):
|
||||
return torch.minimum(
|
||||
(policy.log_prob(policy(states, safe_actions), actions) - old_logprob).exp() * advantages,
|
||||
g(advantages, clip_ratio)
|
||||
)[valid].mean()
|
||||
|
||||
for _ in range(pi_iters):
|
||||
pi_opt.zero_grad()
|
||||
ppo_loss = -L(states, actions, advantages, clip_ratio)
|
||||
ppo_loss.backward()
|
||||
|
||||
if max_grad_norm:
|
||||
torch.nn.utils.clip_grad_norm(policy.parameters(), max_grad_norm)
|
||||
|
||||
pi_opt.step()
|
||||
|
||||
kl = policy.kl_divergence(policy(states, safe_actions), old_dist)[valid].mean()
|
||||
if target_kl and kl > target_kl:
|
||||
break
|
||||
|
||||
print('KL', kl.item())
|
||||
|
||||
return value, policy
|
||||
54
src/safe_options/test_options.py
Normal file
54
src/safe_options/test_options.py
Normal file
@@ -0,0 +1,54 @@
|
||||
from intersim.envs import IntersimpleLidarFlat
|
||||
from options import OptionsEnv
|
||||
import gym
|
||||
import numpy as np
|
||||
|
||||
def test_obs_shape():
|
||||
options = [(0, 5), (5, 5), (10, 5)]
|
||||
env = OptionsEnv(IntersimpleLidarFlat(n_rays=5), options)
|
||||
assert env.reset().shape == (36,)
|
||||
|
||||
def test_act_space():
|
||||
options = [(0, 5), (5, 5), (10, 5)]
|
||||
env = OptionsEnv(IntersimpleLidarFlat(n_rays=5), options)
|
||||
assert env.action_space == gym.spaces.Discrete(3)
|
||||
|
||||
def test_plan():
|
||||
options = [(0, 5), (5, 5), (10, 5)]
|
||||
env = OptionsEnv(IntersimpleLidarFlat(n_rays=5), options)
|
||||
env.reset()
|
||||
plan = env.plan(options[0])
|
||||
assert np.allclose(plan, -13.998268127441406 * np.ones((5,)))
|
||||
|
||||
def test_plan2():
|
||||
options = [(0, 5), (5, 5), (10, 5)]
|
||||
env = OptionsEnv(IntersimpleLidarFlat(n_rays=5), options)
|
||||
obs = env.reset()
|
||||
states, actions, rewards, dones, plan_done, infos, n_steps = env.execute_plan(obs, options[0])
|
||||
assert states.shape == (6, 36)
|
||||
assert rewards.shape == (6,)
|
||||
assert dones.shape == (6,)
|
||||
assert len(infos) == 5
|
||||
|
||||
def test_step():
|
||||
options = [(0, 5), (5, 5), (10, 5)]
|
||||
env = OptionsEnv(IntersimpleLidarFlat(n_rays=5), options)
|
||||
env.reset()
|
||||
obs, reward, done, _ = env.step(0)
|
||||
assert obs.shape == (36,)
|
||||
assert reward == 5.0
|
||||
assert done == False
|
||||
|
||||
def test_ll_step():
|
||||
options = [(0, 5), (5, 5), (10, 5)]
|
||||
env = OptionsEnv(IntersimpleLidarFlat(n_rays=5), options)
|
||||
env.reset()
|
||||
_, _, _, info = env.step(0)
|
||||
assert info['ll']['observations'].shape == (6, 36)
|
||||
assert info['ll']['actions'].shape == (6, 1)
|
||||
assert info['ll']['rewards'].shape == (6,)
|
||||
assert info['ll']['env_done'].shape == (6,)
|
||||
assert info['ll']['plan_done'].shape == (6,)
|
||||
assert info['ll']['plan_done'][5] == True
|
||||
assert info['ll']['steps'] == 5
|
||||
assert len(info['ll']['infos']) == 5
|
||||
74
src/util/wrappers.py
Normal file
74
src/util/wrappers.py
Normal file
@@ -0,0 +1,74 @@
|
||||
import numpy as np
|
||||
import gym
|
||||
|
||||
class Wrapper(gym.Wrapper):
|
||||
def __getattr__(self, name):
|
||||
return getattr(self.env, name)
|
||||
|
||||
class TransformObservation(gym.wrappers.TransformObservation):
|
||||
def __getattr__(self, name):
|
||||
return getattr(self.env, name)
|
||||
|
||||
class CollisionPenaltyWrapper(Wrapper):
|
||||
|
||||
def __init__(self, env, collision_distance, collision_penalty, *args, **kwargs):
|
||||
super().__init__(env, *args, **kwargs)
|
||||
self.penalty = collision_penalty
|
||||
self.distance = collision_distance
|
||||
|
||||
def step(self, action):
|
||||
obs, reward, done, info = super().step(action)
|
||||
reward = -self.penalty if (obs.reshape(-1, 6)[1:, 0] < self.distance).any() else reward
|
||||
|
||||
self.env._rewards.pop()
|
||||
self.env._rewards.append(reward)
|
||||
|
||||
return obs, reward, done, info
|
||||
|
||||
class Minobs(Wrapper):
|
||||
""" Meant to be used as wrapper around LidarObservation """
|
||||
|
||||
def __init__(self, env, *args, **kwargs):
|
||||
super().__init__(env, *args, **kwargs)
|
||||
n_rays = int(self.observation_space.shape[0] / 6) - 1
|
||||
self.observation_space = gym.spaces.Box(low=-np.inf, high=np.inf, shape=((1 + n_rays) * 2,))
|
||||
|
||||
def minobs(self, obs):
|
||||
""" ego v, psidot ; (for each ray,) rel. distance, rel. velocity in ego forward direction """
|
||||
obs = obs.reshape(-1, 6)
|
||||
obs = np.concatenate((obs[:1, [2, 4]], obs[1:, [0, 2]]), axis=0)
|
||||
return obs.reshape(-1)
|
||||
|
||||
def reset(self):
|
||||
return self.minobs(super().reset())
|
||||
|
||||
def step(self, action):
|
||||
obs, reward, done, info = super().step(action)
|
||||
return self.minobs(obs), reward, done, info
|
||||
|
||||
class Setobs(Wrapper):
|
||||
""" Meant to be used as wrapper around LidarObservation """
|
||||
|
||||
def __init__(self, env, *args, **kwargs):
|
||||
super().__init__(env, *args, **kwargs)
|
||||
self.n_rays = int(self.observation_space.shape[0] / 6) - 1
|
||||
self.observation_space = gym.spaces.Box(low=-np.inf, high=np.inf, shape=(self.n_rays, 6))
|
||||
|
||||
def obs(self, obs):
|
||||
obs = obs.reshape(-1, 6)
|
||||
|
||||
ego = obs[:1, [2, 4]] # v, psidot
|
||||
ego = np.tile(ego, (self.n_rays, 1))
|
||||
|
||||
other = obs[1:, [0, 1, 2]] # distance, angle, velocity component in ego forward direction
|
||||
other = np.stack((other[:, 0], np.cos(other[:, 1]), np.sin(other[:, 1]), other[:, 2]), axis=-1)
|
||||
|
||||
obs = np.concatenate((ego, other), axis=-1)
|
||||
return obs
|
||||
|
||||
def reset(self):
|
||||
return self.obs(super().reset())
|
||||
|
||||
def step(self, action):
|
||||
obs, reward, done, info = super().step(action)
|
||||
return self.obs(obs), reward, done, info
|
||||
Reference in New Issue
Block a user