VeriGym - Comparing abstraction policies¶
Imports¶
import os
# checks if env var is set, defaults to True
QUICK = os.environ.get("QUICK", "1") != "0"
# uncomment line below for full run
# QUICK = False
import logging
from pprint import pprint
import gymnasium as gym
import numpy as np
import stormpy
import stable_baselines3 as sb3
from gymnasium.wrappers import TimeLimit
import os
import verigym
from verigym.abstraction.gym_utils.transform_observation import ReplaceInfObservation
from verigym.abstraction.abstractionmapper import linspace_mapper
from verigym.frameworks.stormpy.stormpy_utils import build_stormpy_mdp, build_stormpy_imdp
from verigym.policy.randomized import RandomizedPolicy
from verigym.policy.qvalue import EntropyLearningPolicy
from verigym.frameworks.stable_baselines3.policy import SB3Policy
from workflow_utils.utils import OUTPATH_WORKFLOWS as OUTPATH, train_with_sb3_ppo, train_with_sb3_sac, plot_logged_data, plot_compare_policy_eval, get_mean_reward_from_trajectories
from workflow_utils import plot_style
logging.getLogger().setLevel(logging.ERROR)
SEED = 0
GYM_STRING = "MountainCarContinuous-v0"
SUBPATH = "compare_files"
dir = f"{OUTPATH}/{SUBPATH}"
PLOT_PATH = f"{OUTPATH}/figures/{SUBPATH}"
RL_OUTPATH = f"{OUTPATH}/{SUBPATH}/ppo/{GYM_STRING}"
os.makedirs(PLOT_PATH, exist_ok=True)
def get_average_episode_length(trajectories):
return np.mean([len(traj) for traj in trajectories])
def get_mean_reward_from_trajectories(trajectories):
rewards = []
for trajectory in trajectories:
trajectory_rewards = list(map(lambda tup: tup[2], trajectory))
rewards.append(np.sum(trajectory_rewards))
return float(np.mean(rewards))
1. Environment¶
We use the OpenAI gymnasium Cart Pole environment for this demo:
# 1. Load in gym
gym_env = gym.make(GYM_STRING)
gamma = 0.995 # discount factor
gym_env.reset(seed=SEED)
print("Environment type: ", type(gym_env))
# 2. For discretization, replace (-inf, inf) observation bounds
gym_env = ReplaceInfObservation(gym_env, neg_inf=-5, pos_inf=5)
print("Observation space shape: ", gym_env.observation_space.shape)
print("Observation upper bounds: ", gym_env.observation_space.high)
print("Observation lower bounds: ", gym_env.observation_space.low)
print("Type of observation space: ", type(gym_env.observation_space))
Now, we load the gym_env into our framework, turning it into a (gym-like) VeriGymEnv:
# Create a VeriGymEnv from gym env
generative_model = verigym.GenerativeEnv.from_gymnasium(gym_env)
print("Environment type: ", type(generative_model))
print("Observation space shape: ", generative_model.observation_space.shape)
print("Observation upper bounds: ", generative_model.observation_space.high)
print("Observation lower bounds: ", generative_model.observation_space.low)
print("Type of observation space: ", type(generative_model.observation_space))
The observation space is the same as before, but now we can apply learn the underlying model and perform any abstraction!
2. Abstraction¶
Now we:
Apply a user-specified discretization to the state space
Learn the transition and reward functions through random search
n_bins_states = 25
n_bins_action = 10
abstraction_mapper = linspace_mapper(
env=generative_model,
n_bins_states=n_bins_states,
n_bins_actions=n_bins_action
)
abstracted_model = verigym.create_abstraction(
original_env=generative_model,
abstraction_mapper=abstraction_mapper,
exploration_policy=EntropyLearningPolicy(generative_model, abstraction_mapper, seed=SEED),
num_steps=(10 if QUICK else int(1e3)),
n_iterations=50,
multithreading=False,
seed=SEED
)
print("Type of observation space: ", type(abstracted_model.observation_space))
print(f"The abstract observation space has {abstracted_model.observation_space.n} states!")
print("Type of environment: ", type(abstracted_model))
The abstract model allows us to explicitly access the model’s transition and reward functions. Note that not all states are always reachable.
state = 120
print(f"Transitions from state {state}:")
pprint(abstracted_model.transition_function[state])
print(f"\nState-action rewards of state {state}:")
pprint(abstracted_model.reward_function[state])
3. Model checking¶
We now want to model check the abstraction we learned in stormpy w.r.t maximal cumulative reward.
Therefore, we
Export the model to a
stormpy.storage.SparseMdpModel check the MDP in
stormpyand receive a scheduler (policy)
stormpy_mdp = build_stormpy_mdp(abstracted_model)
print(stormpy_mdp)
prop = stormpy.parse_properties(f"Rmax=?[Cdiscount={gamma}]")[0] # checking for cumulative discounted reward
# Run the model checking:
result = stormpy.check_model_sparse(stormpy_mdp, prop, extract_scheduler=True)
scheduler = result.scheduler
4. Policy evaluation¶
Finally, we evaluate our model checked policy on the original and the abstract model.
We convert the
stormpypolicy to averigym.StormpyPolicyWe apply it to both the original and the abstract model.
# policy mapped to the original model
verigym_policy = verigym.StormpyPolicy(scheduler, abstracted_model.abstraction_map, stormpy_mdp)
# policy on the abstract model
identity_mapper = verigym.AbstractionMapper.initialize_identity_mapper(abstracted_model.observation_space, abstracted_model.action_space)
verigym_policy_on_abstracted = verigym.StormpyPolicy(
policy=scheduler,
abstraction_mapper=identity_mapper,
mdp=stormpy_mdp
)
n_simulation_steps = int(10e4)
# simulation in the original (continuous) model
trajectories_original = generative_model.simulate(
policy=verigym_policy,
n_steps=n_simulation_steps,
seed = SEED
)
mean_rewards_original = get_mean_reward_from_trajectories(trajectories_original)
# verify the policy: (2) policy performance on abstracted model
trajectories_abstracted = abstracted_model.simulate(
policy=verigym_policy_on_abstracted,
n_steps=n_simulation_steps,
seed = SEED
)
mean_rewards_abstracted = get_mean_reward_from_trajectories(trajectories_abstracted)
print("Comparing the mean cumulative reward of the abstract policy:")
print("\tOriginal environment: ", mean_rewards_original)
print("\tAbstract environment: ", mean_rewards_abstracted)
Visualize abstract-policy on the original environment¶
visualize = False # set to True for visualization
if visualize:
import matplotlib.pyplot as plt
from matplotlib import animation
from IPython.display import HTML
# Roll out one episode of verigym_policy, capturing frames via gym's rgb_array rendering
render_env = gym.make(GYM_STRING, render_mode="rgb_array")
obs, info = render_env.reset()
frames = [render_env.render()]
terminated = truncated = False
while not (terminated or truncated):
action = verigym_policy.get_action(obs)
obs, reward, terminated, truncated, info = render_env.step(action)
frames.append(render_env.render())
render_env.close()
print(f"Episode length: {len(frames)} steps")
fig, ax = plt.subplots()
ax.axis("off")
img = ax.imshow(frames[0])
def _update(i):
img.set_data(frames[i])
return (img,)
anim = animation.FuncAnimation(fig, _update, frames=len(frames), interval=40, blit=True)
plt.close(fig)
HTML(anim.to_jshtml())
Extension: IMDP learning instead of MDP learning¶
explicit_interval_model = verigym.create_abstraction(
original_env=generative_model,
abstraction_mapper=abstraction_mapper,
exploration_policy=EntropyLearningPolicy(generative_model, abstraction_mapper, seed=SEED),
num_steps=(10 if QUICK else int(1e3)),
n_iterations=50,
multithreading=False,
intervals=True,
assume_iid=False,
seed = SEED
)
stormpy_imdp = build_stormpy_imdp(explicit_interval_model,
use_reward_uncertainty=False)
print(stormpy_imdp)
Extension: Comparing to RL¶
To determine if our abstraction is any good, we compare it’s performance against that of an RL agent trained on the original environment, as well as on the abstraction. We use a PPO agent from stable-baselines, with minor alterations to the default settings to push the agent towards more exploration.
outpath_original = f"{RL_OUTPATH}/original/"
os.makedirs(outpath_original, exist_ok=True)
TRAIN_STEPS = int(2e4)
train_with_sb3_ppo(generative_model, generative_model, outpath_original, n_steps=(10 if QUICK else TRAIN_STEPS), eval_freq=2000, use_sde=True, discount=gamma, seed=SEED)
outpath_abstracted = f"{RL_OUTPATH}/abstracted/"
os.makedirs(outpath_abstracted, exist_ok=True)
abstracted_model_timelimit = TimeLimit(abstracted_model, max_episode_steps=999)
train_with_sb3_ppo(abstracted_model_timelimit, abstracted_model_timelimit, outpath_abstracted, n_steps=(10 if QUICK else TRAIN_STEPS), eval_freq=2000, discount=gamma, seed=SEED)
plot_logged_data([outpath_original + "log/evaluations.npz", outpath_abstracted + "log/evaluations.npz",
], labels=["PPO (orig.)", "PPO (abstr.)",
], key="results",
title="PPO training development", path_output=f"{PLOT_PATH}/ppo_training_curve.pdf")
rl_model_original = sb3.PPO.load(outpath_original + "ppo_model")
rl_model_abstracted = sb3.PPO.load(outpath_abstracted + "ppo_model")
rl_policy_original = SB3Policy(rl_model_original, generative_model, deterministic=True)
trajectories_rl_original = generative_model.simulate(policy=rl_policy_original, n_steps=(10 if QUICK else n_simulation_steps), seed=SEED)
mean_rewards_rl_original = get_mean_reward_from_trajectories(trajectories_rl_original)
rl_policy_abstracted = SB3Policy(rl_model_abstracted, generative_model, deterministic=True)
trajectories_rl_abstracted = generative_model.simulate(policy=rl_policy_abstracted, n_steps=(10 if QUICK else n_simulation_steps), seed=SEED)
mean_rewards_rl_abstracted = get_mean_reward_from_trajectories(trajectories_rl_abstracted)
episode_returns = lambda trajs: [float(np.sum([t[2] for t in traj])) for traj in trajs]
print("Mean cumulative reward on the original environment:")
print("\tAbstraction policy (model checked): ", mean_rewards_original)
print("\tPPO policy: ", mean_rewards_rl_original)
print("\tPPO policy (abstracted): ", mean_rewards_rl_abstracted)
plot_compare_policy_eval([episode_returns(trajectories_original), episode_returns(trajectories_rl_original), episode_returns(trajectories_rl_abstracted)],
labels=["Abstraction + PMC", "PPO", "Abstraction + PPO", ], title="Policy performance of PMC vs. (abstracted) DRL",
path_output=f"{PLOT_PATH}/comparing_abstraction_policies_with_abstracted_ppo.pdf")
plot_compare_policy_eval([episode_returns(trajectories_original), episode_returns(trajectories_rl_original)],
labels=["Abstraction + PMC", "PPO"], title="Policy performance of PMC vs. DRL", path_output=f"{PLOT_PATH}/comparing_abstraction_policies.pdf")