Module likelihood.models.environments
Functions
def flatten_chain(matrix: List[List[Any]]) ‑> List[Any]-
Expand source code
def flatten_chain(matrix: List[List[Any]]) -> List[Any]: return list(chain.from_iterable(matrix))
Classes
class ActionSpace (num_actions: int)-
Expand source code
class ActionSpace: def __init__(self, num_actions: int) -> None: self._num_actions = num_actions @property def n(self) -> int: return self._num_actionsInstance variables
prop n : int-
Expand source code
@property def n(self) -> int: return self._num_actions
class OptionCriticEnv (episodes: Dict[int, Dict[str, List]])-
Expand source code
class OptionCriticEnv: """ An environment for Option Critic reinforcement learning that processes a dataset of episodes. Attributes ---------- episodes : `Dict[str, Dict]` Dataset of episodes with state, action, selected_option, reward, next_state, and done information. observation_space : `np.ndarray` Initial observation space shape (from first episode's state) done : `bool` Whether the current episode has terminated num_options : `int` Number of distinct options available in the dataset actions_by_option : `defaultdict(set)` Maps selected options to sets of actions that were taken with them unique_actions_count : `List[int]` Count of unique actions per option index (used for action space definition) action_space : `ActionSpace` Custom action space defined by unique actions per option idx_episode : `int` Current episode index being processed current_state : `np.ndarray` Current state observation in the environment epsilon : `float` Probability of choosing the reward from the nearest state. By default it is set to `0.1`. """ def __init__( self, episodes: Dict[int, Dict[str, List]], ) -> None: """ Initializes the OptionCriticEnv with a dataset of episodes. Parameters ---------- episodes : `Dict[int, Dict]` Dataset of episodes where keys are episode identifiers and values are episode data. Each episode must contain at least: - *state*: `List` of state observations - *selected_option*: `List[int]` or `List[List[int]]` of selected options - *action*: `List[int]` or `List[List[int]]` of actions taken - *reward*: `List` of rewards - *next_state*: `List` of next states - *done*: `List` of termination flags Raises ------ ValueError If required fields ("state" or "selected_option") are missing from episode data """ self.episodes = episodes self.multiple_option = False required_keys = ["state", "action", "selected_option", "reward", "next_state", "done"] for episode_id, data in episodes.items(): if not all(k in data for k in required_keys): raise ValueError( f"Episode {episode_id} missing keys: {set(required_keys) - set(data.keys())}" ) self.observation_space = np.array(episodes[0]["state"][0]) self.done = False self.idx_episode = 0 self.current_state = None self.num_options = len( set(flatten_chain(episodes[0]["selected_option"])) if isinstance(episodes[0]["selected_option"][0], list) else episodes[0]["selected_option"] ) self.actions_by_option = defaultdict(set) # Build fast lookup for transitions self.state_action_option_to_transition: Dict[Tuple, Dict[str, Any]] = {} for episode_id, data in episodes.items(): states = data["state"] actions = data["action"] options = data["selected_option"] next_states = data["next_state"] rewards = data["reward"] dones = data["done"] for i in range(len(states)): key = self._make_transition_key( states[i], options[i], actions[i], ) self.state_action_option_to_transition[key] = { "next_state": next_states[i], "reward": rewards[i], "done": dones[i], } for i, selected in enumerate(options): self.actions_by_option[ tuple(selected) if isinstance(options[0], list) else selected ].add(tuple(actions[i]) if isinstance(actions[i], list) else actions[i]) check_type = list(set([key for key in self.actions_by_option.keys()])) keys_actions_by_option = list(sorted(self.actions_by_option.keys(), reverse=True)) actions = self.actions_by_option[keys_actions_by_option[0]] self.unique_actions_count = [ len( list( OrderedDict.fromkeys( flatten_chain( [ list(action) for action in self.actions_by_option.get( keys_actions_by_option[i], set() ) ] ) ) ) if isinstance(keys_actions_by_option[i], tuple) else self.actions_by_option.get(keys_actions_by_option[i], set()) ) for i in range( max(self.actions_by_option.keys()) + 1 if not isinstance(check_type[0], tuple) else len(set(keys_actions_by_option)) ) ] self.action_space = ActionSpace(self.unique_actions_count) next_states = np.array( [trans["next_state"] for trans in self.state_action_option_to_transition.values()] ) self.kdtree = KDTree(next_states) self.trans_list = list(self.state_action_option_to_transition.values()) self.stats = 0.0 self.count_step_found = 0 self.count_step_nearest = 0 self.threshold = 0.5 self.rate = 0.1 self.epsilon = 0.1 self.states = np.array( [ self.state_action_option_to_transition[k]["next_state"] for k in self.state_action_option_to_transition ] ) self.std_per_dim = np.std(states, axis=0) def _make_transition_key( self, state: List[float], option: Union[int, List[int]], action: Union[int, List[int]], decimals: int = 6, ) -> Tuple: """ Builds a canonical transition key: ((state_tuple), option(s)..., action(s)...) """ state_key = tuple(round(float(x), decimals) for x in state) if isinstance(action, (list, tuple)): self.multiple_option = True return (state_key,) + tuple(int(o) for o in option) + tuple(int(a) for a in action) else: return (state_key, int(option), int(action)) def _check_condition(self, value: float, other: float) -> float: if other < self.threshold: increment = self.rate * (self.threshold - other) value += increment value = min(value, 1.0) return value def reset(self) -> tuple[np.ndarray, dict]: """ Resets the environment to a random episode and returns the initial state. Returns ------- observation : `np.ndarray` Initial state observation info : `Dict` Empty dictionary (no additional information) """ self.count_step_found = 0 self.count_step_nearest = 0 episode_id = np.random.choice(list(self.episodes.keys())) self.idx_episode = episode_id self.current_state = self.episodes[episode_id]["state"][0] return self.current_state, {} def step( self, action: int | List[int], option: int | List[int] ) -> tuple[np.ndarray, float, bool, bool, dict]: """ Takes an action with a specific option and returns the next state, reward, and termination status. Parameters ---------- action : `int` | `List[int]` Action index to execute option : `int` | `List[int]` Selected option index Returns ------- next_state : `np.ndarray` State after taking the action reward : `float` Immediate reward for the transition done : `bool` Whether the episode has terminated (from episode data) terminated : `bool` Whether the action-option pair was found in the dataset info : `Dict` Empty dictionary (no additional information) Notes ----- - Uses a KD-tree for efficient nearest-neighbor lookup of next states. """ key = self._make_transition_key( self.current_state, option, action, ) self.stats = round( self.count_step_found / ( (self.count_step_found + self.count_step_nearest) if (self.count_step_found + self.count_step_nearest) > 0.0 else 1.0 ), 6, ) if key in self.state_action_option_to_transition: self.count_step_found += 1 trans = self.state_action_option_to_transition[key] self.current_state = trans["next_state"] return trans["next_state"].copy(), trans["reward"], trans["done"], True, {} else: # Query KD-tree for nearest neighbor self.count_step_nearest += 1 distance, idx = self.kdtree.query(self.current_state) closest_trans = self.trans_list[idx] closest_state = closest_trans["next_state"] closest_reward = closest_trans["reward"] closest_reward_prob = random.random() self.epsilon = self._check_condition(self.epsilon, self.stats) if np.array_equal(self.current_state, closest_state): if self.count_step_nearest > 1: done = True reward = -1.0 else: done = False reward = 0.0 closest_state = closest_state + np.random.normal( 0, self.std_per_dim, size=closest_state.shape ) else: done = False reward = 0.0 self.current_state = closest_state.copy() return ( closest_state.copy(), reward if closest_reward_prob < self.epsilon else closest_reward, done if done else closest_trans.get("done", False), False, {}, )An environment for Option Critic reinforcement learning that processes a dataset of episodes.
Attributes
episodes:Dict[str, Dict]- Dataset of episodes with state, action, selected_option, reward, next_state, and done information.
observation_space:np.ndarray- Initial observation space shape (from first episode's state)
done:bool- Whether the current episode has terminated
num_options:int- Number of distinct options available in the dataset
actions_by_option:defaultdict(set)- Maps selected options to sets of actions that were taken with them
unique_actions_count:List[int]- Count of unique actions per option index (used for action space definition)
action_space:ActionSpace- Custom action space defined by unique actions per option
idx_episode:int- Current episode index being processed
current_state:np.ndarray- Current state observation in the environment
epsilon:float- Probability of choosing the reward from the nearest state. By default it is set to
0.1.
Initializes the OptionCriticEnv with a dataset of episodes.
Parameters
episodes:Dict[int, Dict]- Dataset of episodes where keys are episode identifiers and values are episode data.
Each episode must contain at least:
- state:
Listof state observations - selected_option:List[int]orList[List[int]]of selected options - action:List[int]orList[List[int]]of actions taken - reward:Listof rewards - next_state:Listof next states - done:Listof termination flags
Raises
ValueError- If required fields ("state" or "selected_option") are missing from episode data
Methods
def reset(self) ‑> tuple[numpy.ndarray, dict]-
Expand source code
def reset(self) -> tuple[np.ndarray, dict]: """ Resets the environment to a random episode and returns the initial state. Returns ------- observation : `np.ndarray` Initial state observation info : `Dict` Empty dictionary (no additional information) """ self.count_step_found = 0 self.count_step_nearest = 0 episode_id = np.random.choice(list(self.episodes.keys())) self.idx_episode = episode_id self.current_state = self.episodes[episode_id]["state"][0] return self.current_state, {}Resets the environment to a random episode and returns the initial state.
Returns
observation:np.ndarray- Initial state observation
info:Dict- Empty dictionary (no additional information)
def step(self, action: int | List[int], option: int | List[int]) ‑> tuple[numpy.ndarray, float, bool, bool, dict]-
Expand source code
def step( self, action: int | List[int], option: int | List[int] ) -> tuple[np.ndarray, float, bool, bool, dict]: """ Takes an action with a specific option and returns the next state, reward, and termination status. Parameters ---------- action : `int` | `List[int]` Action index to execute option : `int` | `List[int]` Selected option index Returns ------- next_state : `np.ndarray` State after taking the action reward : `float` Immediate reward for the transition done : `bool` Whether the episode has terminated (from episode data) terminated : `bool` Whether the action-option pair was found in the dataset info : `Dict` Empty dictionary (no additional information) Notes ----- - Uses a KD-tree for efficient nearest-neighbor lookup of next states. """ key = self._make_transition_key( self.current_state, option, action, ) self.stats = round( self.count_step_found / ( (self.count_step_found + self.count_step_nearest) if (self.count_step_found + self.count_step_nearest) > 0.0 else 1.0 ), 6, ) if key in self.state_action_option_to_transition: self.count_step_found += 1 trans = self.state_action_option_to_transition[key] self.current_state = trans["next_state"] return trans["next_state"].copy(), trans["reward"], trans["done"], True, {} else: # Query KD-tree for nearest neighbor self.count_step_nearest += 1 distance, idx = self.kdtree.query(self.current_state) closest_trans = self.trans_list[idx] closest_state = closest_trans["next_state"] closest_reward = closest_trans["reward"] closest_reward_prob = random.random() self.epsilon = self._check_condition(self.epsilon, self.stats) if np.array_equal(self.current_state, closest_state): if self.count_step_nearest > 1: done = True reward = -1.0 else: done = False reward = 0.0 closest_state = closest_state + np.random.normal( 0, self.std_per_dim, size=closest_state.shape ) else: done = False reward = 0.0 self.current_state = closest_state.copy() return ( closest_state.copy(), reward if closest_reward_prob < self.epsilon else closest_reward, done if done else closest_trans.get("done", False), False, {}, )Takes an action with a specific option and returns the next state, reward, and termination status.
Parameters
action:int` | `List[int]- Action index to execute
option:int` | `List[int]- Selected option index
Returns
next_state:np.ndarray- State after taking the action
reward:float- Immediate reward for the transition
done:bool- Whether the episode has terminated (from episode data)
terminated:bool- Whether the action-option pair was found in the dataset
info:Dict- Empty dictionary (no additional information)
Notes
- Uses a KD-tree for efficient nearest-neighbor lookup of next states.