2023-06-30 09:08:13 +01:00
|
|
|
from __future__ import annotations
|
|
|
|
|
|
2023-06-19 21:53:25 +01:00
|
|
|
import json
|
|
|
|
|
from datetime import datetime
|
2023-06-13 09:42:54 +01:00
|
|
|
from pathlib import Path
|
2023-06-30 09:08:13 +01:00
|
|
|
from typing import Optional, Union
|
2023-06-13 09:42:54 +01:00
|
|
|
|
2023-06-30 09:08:13 +01:00
|
|
|
import tensorflow as tf
|
2023-06-13 09:42:54 +01:00
|
|
|
from ray.rllib.algorithms import Algorithm
|
2023-06-19 21:53:25 +01:00
|
|
|
from ray.rllib.algorithms.a2c import A2CConfig
|
2023-06-20 16:06:55 +01:00
|
|
|
from ray.rllib.algorithms.ppo import PPOConfig
|
2023-06-19 21:53:25 +01:00
|
|
|
from ray.tune.logger import UnifiedLogger
|
2023-06-13 09:42:54 +01:00
|
|
|
from ray.tune.registry import register_env
|
2023-06-30 09:08:13 +01:00
|
|
|
|
2023-06-19 21:53:25 +01:00
|
|
|
from primaite import getLogger
|
2023-06-19 20:27:08 +01:00
|
|
|
from primaite.agents.agent import AgentSessionABC
|
2023-06-30 09:08:13 +01:00
|
|
|
from primaite.common.enums import (
|
|
|
|
|
AgentFramework,
|
|
|
|
|
AgentIdentifier,
|
|
|
|
|
DeepLearningFramework,
|
|
|
|
|
)
|
2023-06-13 09:42:54 +01:00
|
|
|
from primaite.environment.primaite_env import Primaite
|
|
|
|
|
|
2023-06-19 21:53:25 +01:00
|
|
|
_LOGGER = getLogger(__name__)
|
|
|
|
|
|
2023-06-30 09:08:13 +01:00
|
|
|
|
2023-06-18 22:40:56 +01:00
|
|
|
def _env_creator(env_config):
|
|
|
|
|
return Primaite(
|
|
|
|
|
training_config_path=env_config["training_config_path"],
|
|
|
|
|
lay_down_config_path=env_config["lay_down_config_path"],
|
|
|
|
|
session_path=env_config["session_path"],
|
2023-06-30 09:08:13 +01:00
|
|
|
timestamp_str=env_config["timestamp_str"],
|
2023-06-18 22:40:56 +01:00
|
|
|
)
|
2023-06-13 09:42:54 +01:00
|
|
|
|
|
|
|
|
|
2023-06-19 21:53:25 +01:00
|
|
|
def _custom_log_creator(session_path: Path):
|
|
|
|
|
logdir = session_path / "ray_results"
|
|
|
|
|
logdir.mkdir(parents=True, exist_ok=True)
|
|
|
|
|
|
|
|
|
|
def logger_creator(config):
|
|
|
|
|
return UnifiedLogger(config, logdir, loggers=None)
|
|
|
|
|
|
|
|
|
|
return logger_creator
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
class RLlibAgent(AgentSessionABC):
|
2023-06-30 09:08:13 +01:00
|
|
|
"""An AgentSession class that implements a Ray RLlib agent."""
|
2023-06-18 22:40:56 +01:00
|
|
|
|
2023-06-30 09:08:13 +01:00
|
|
|
def __init__(self, training_config_path, lay_down_config_path):
|
2023-06-18 22:40:56 +01:00
|
|
|
super().__init__(training_config_path, lay_down_config_path)
|
2023-06-19 21:53:25 +01:00
|
|
|
if not self._training_config.agent_framework == AgentFramework.RLLIB:
|
2023-06-30 09:08:13 +01:00
|
|
|
msg = (
|
|
|
|
|
f"Expected RLLIB agent_framework, "
|
|
|
|
|
f"got {self._training_config.agent_framework}"
|
|
|
|
|
)
|
2023-06-19 21:53:25 +01:00
|
|
|
_LOGGER.error(msg)
|
|
|
|
|
raise ValueError(msg)
|
2023-06-20 16:06:55 +01:00
|
|
|
if self._training_config.agent_identifier == AgentIdentifier.PPO:
|
2023-06-19 21:53:25 +01:00
|
|
|
self._agent_config_class = PPOConfig
|
2023-06-20 16:06:55 +01:00
|
|
|
elif self._training_config.agent_identifier == AgentIdentifier.A2C:
|
2023-06-19 21:53:25 +01:00
|
|
|
self._agent_config_class = A2CConfig
|
|
|
|
|
else:
|
2023-06-30 09:08:13 +01:00
|
|
|
msg = (
|
|
|
|
|
"Expected PPO or A2C agent_identifier, "
|
|
|
|
|
f"got {self._training_config.agent_identifier.value}"
|
|
|
|
|
)
|
2023-06-19 21:53:25 +01:00
|
|
|
_LOGGER.error(msg)
|
|
|
|
|
raise ValueError(msg)
|
|
|
|
|
self._agent_config: PPOConfig
|
|
|
|
|
|
2023-06-18 22:40:56 +01:00
|
|
|
self._current_result: dict
|
|
|
|
|
self._setup()
|
2023-06-19 21:53:25 +01:00
|
|
|
_LOGGER.debug(
|
|
|
|
|
f"Created {self.__class__.__name__} using: "
|
|
|
|
|
f"agent_framework={self._training_config.agent_framework}, "
|
2023-06-20 16:06:55 +01:00
|
|
|
f"agent_identifier="
|
|
|
|
|
f"{self._training_config.agent_identifier}, "
|
2023-06-19 21:53:25 +01:00
|
|
|
f"deep_learning_framework="
|
|
|
|
|
f"{self._training_config.deep_learning_framework}"
|
|
|
|
|
)
|
|
|
|
|
|
|
|
|
|
def _update_session_metadata_file(self):
|
|
|
|
|
"""
|
|
|
|
|
Update the ``session_metadata.json`` file.
|
|
|
|
|
|
|
|
|
|
Updates the `session_metadata.json`` in the ``session_path`` directory
|
|
|
|
|
with the following key/value pairs:
|
|
|
|
|
|
|
|
|
|
- end_datetime: The date & time the session ended in iso format.
|
|
|
|
|
- total_episodes: The total number of training episodes completed.
|
|
|
|
|
- total_time_steps: The total number of training time steps completed.
|
|
|
|
|
"""
|
|
|
|
|
with open(self.session_path / "session_metadata.json", "r") as file:
|
|
|
|
|
metadata_dict = json.load(file)
|
|
|
|
|
|
|
|
|
|
metadata_dict["end_datetime"] = datetime.now().isoformat()
|
2023-06-30 09:08:13 +01:00
|
|
|
metadata_dict["total_episodes"] = self._current_result[
|
|
|
|
|
"episodes_total"
|
|
|
|
|
]
|
|
|
|
|
metadata_dict["total_time_steps"] = self._current_result[
|
|
|
|
|
"timesteps_total"
|
|
|
|
|
]
|
2023-06-19 21:53:25 +01:00
|
|
|
|
|
|
|
|
filepath = self.session_path / "session_metadata.json"
|
|
|
|
|
_LOGGER.debug(f"Updating Session Metadata file: {filepath}")
|
|
|
|
|
with open(filepath, "w") as file:
|
|
|
|
|
json.dump(metadata_dict, file)
|
|
|
|
|
_LOGGER.debug("Finished updating session metadata file")
|
2023-06-18 22:40:56 +01:00
|
|
|
|
|
|
|
|
def _setup(self):
|
2023-06-19 20:27:08 +01:00
|
|
|
super()._setup()
|
2023-06-18 22:40:56 +01:00
|
|
|
register_env("primaite", _env_creator)
|
2023-06-19 21:53:25 +01:00
|
|
|
self._agent_config = self._agent_config_class()
|
2023-06-18 22:40:56 +01:00
|
|
|
|
2023-06-19 21:53:25 +01:00
|
|
|
self._agent_config.environment(
|
2023-06-18 22:40:56 +01:00
|
|
|
env="primaite",
|
|
|
|
|
env_config=dict(
|
|
|
|
|
training_config_path=self._training_config_path,
|
|
|
|
|
lay_down_config_path=self._lay_down_config_path,
|
|
|
|
|
session_path=self.session_path,
|
2023-06-30 09:08:13 +01:00
|
|
|
timestamp_str=self.timestamp_str,
|
|
|
|
|
),
|
2023-06-13 09:42:54 +01:00
|
|
|
)
|
|
|
|
|
|
2023-06-19 21:53:25 +01:00
|
|
|
self._agent_config.training(
|
2023-06-18 22:40:56 +01:00
|
|
|
train_batch_size=self._training_config.num_steps
|
|
|
|
|
)
|
2023-06-30 09:08:13 +01:00
|
|
|
self._agent_config.framework(framework="tf")
|
2023-06-13 09:42:54 +01:00
|
|
|
|
2023-06-19 21:53:25 +01:00
|
|
|
self._agent_config.rollouts(
|
2023-06-18 22:40:56 +01:00
|
|
|
num_rollout_workers=1,
|
|
|
|
|
num_envs_per_worker=1,
|
2023-06-30 09:08:13 +01:00
|
|
|
horizon=self._training_config.num_steps,
|
2023-06-18 22:40:56 +01:00
|
|
|
)
|
2023-06-19 21:53:25 +01:00
|
|
|
self._agent: Algorithm = self._agent_config.build(
|
|
|
|
|
logger_creator=_custom_log_creator(self.session_path)
|
|
|
|
|
)
|
2023-06-18 22:40:56 +01:00
|
|
|
|
|
|
|
|
def _save_checkpoint(self):
|
|
|
|
|
checkpoint_n = self._training_config.checkpoint_every_n_episodes
|
|
|
|
|
episode_count = self._current_result["episodes_total"]
|
|
|
|
|
if checkpoint_n > 0 and episode_count > 0:
|
2023-06-30 09:08:13 +01:00
|
|
|
if (episode_count % checkpoint_n == 0) or (
|
|
|
|
|
episode_count == self._training_config.num_episodes
|
2023-06-18 22:40:56 +01:00
|
|
|
):
|
2023-06-30 09:08:13 +01:00
|
|
|
self._agent.save(str(self.checkpoints_path))
|
2023-06-18 22:40:56 +01:00
|
|
|
|
|
|
|
|
def learn(
|
2023-06-30 09:08:13 +01:00
|
|
|
self,
|
|
|
|
|
time_steps: Optional[int] = None,
|
|
|
|
|
episodes: Optional[int] = None,
|
|
|
|
|
**kwargs,
|
2023-06-18 22:40:56 +01:00
|
|
|
):
|
2023-06-30 09:08:13 +01:00
|
|
|
"""
|
|
|
|
|
Evaluate the agent.
|
|
|
|
|
|
|
|
|
|
:param time_steps: The number of steps per episode. Optional. If not
|
|
|
|
|
passed, the value from the training config will be used.
|
|
|
|
|
:param episodes: The number of episodes. Optional. If not
|
|
|
|
|
passed, the value from the training config will be used.
|
|
|
|
|
:param kwargs: Any agent-specific key-word args to be passed.
|
|
|
|
|
"""
|
2023-06-18 22:40:56 +01:00
|
|
|
# Temporarily override train_batch_size and horizon
|
|
|
|
|
if time_steps:
|
2023-06-19 21:53:25 +01:00
|
|
|
self._agent_config.train_batch_size = time_steps
|
|
|
|
|
self._agent_config.horizon = time_steps
|
2023-06-18 22:40:56 +01:00
|
|
|
|
|
|
|
|
if not episodes:
|
|
|
|
|
episodes = self._training_config.num_episodes
|
2023-06-30 09:08:13 +01:00
|
|
|
_LOGGER.info(
|
|
|
|
|
f"Beginning learning for {episodes} episodes @"
|
|
|
|
|
f" {time_steps} time steps..."
|
|
|
|
|
)
|
2023-06-18 22:40:56 +01:00
|
|
|
for i in range(episodes):
|
|
|
|
|
self._current_result = self._agent.train()
|
|
|
|
|
self._save_checkpoint()
|
2023-06-30 09:08:13 +01:00
|
|
|
if (
|
|
|
|
|
self._training_config.deep_learning_framework
|
|
|
|
|
!= DeepLearningFramework.TORCH
|
|
|
|
|
):
|
2023-06-20 22:29:46 +01:00
|
|
|
policy = self._agent.get_policy()
|
|
|
|
|
tf.compat.v1.summary.FileWriter(
|
2023-06-30 09:08:13 +01:00
|
|
|
self.session_path / "ray_results", policy.get_session().graph
|
2023-06-20 22:29:46 +01:00
|
|
|
)
|
2023-06-19 20:27:08 +01:00
|
|
|
super().learn()
|
2023-06-20 22:29:46 +01:00
|
|
|
self._agent.stop()
|
2023-06-18 22:40:56 +01:00
|
|
|
|
|
|
|
|
def evaluate(
|
2023-06-30 09:08:13 +01:00
|
|
|
self,
|
|
|
|
|
time_steps: Optional[int] = None,
|
|
|
|
|
episodes: Optional[int] = None,
|
|
|
|
|
**kwargs,
|
2023-06-18 22:40:56 +01:00
|
|
|
):
|
2023-06-30 09:08:13 +01:00
|
|
|
"""
|
|
|
|
|
Evaluate the agent.
|
|
|
|
|
|
|
|
|
|
:param time_steps: The number of steps per episode. Optional. If not
|
|
|
|
|
passed, the value from the training config will be used.
|
|
|
|
|
:param episodes: The number of episodes. Optional. If not
|
|
|
|
|
passed, the value from the training config will be used.
|
|
|
|
|
:param kwargs: Any agent-specific key-word args to be passed.
|
|
|
|
|
"""
|
2023-06-18 22:40:56 +01:00
|
|
|
raise NotImplementedError
|
|
|
|
|
|
|
|
|
|
def _get_latest_checkpoint(self):
|
|
|
|
|
raise NotImplementedError
|
|
|
|
|
|
|
|
|
|
@classmethod
|
2023-06-30 09:08:13 +01:00
|
|
|
def load(cls, path: Union[str, Path]) -> RLlibAgent:
|
|
|
|
|
"""Load an agent from file."""
|
2023-06-18 22:40:56 +01:00
|
|
|
raise NotImplementedError
|
|
|
|
|
|
|
|
|
|
def save(self):
|
2023-06-30 09:08:13 +01:00
|
|
|
"""Save the agent."""
|
2023-06-18 22:40:56 +01:00
|
|
|
raise NotImplementedError
|
|
|
|
|
|
|
|
|
|
def export(self):
|
2023-06-30 09:08:13 +01:00
|
|
|
"""Export the agent to transportable file format."""
|
2023-06-18 22:40:56 +01:00
|
|
|
raise NotImplementedError
|