Merged PR 248: Make observation flattening optional for agents.

## Summary
Same as other PR but this time we are merging into DEV.

The Proxy agent has a new config option, `flatten_obs`. If true, we use gymnasium flatten.

## Test process
Unit tests and inspection.

Related work items: #2143, #2154, #2161
This commit is contained in:
Marek Wolan
2024-01-09 14:28:36 +00:00
12 changed files with 92 additions and 21 deletions

View File

@@ -6,7 +6,8 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/),
and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html). and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
## [Unreleased] ## [Unreleased]
- Made packet capture and system logging optional (off by default). To turn on, change the io_settings.save_pcap_logs and io_settings.save_sys_logs settings in the config.
- Made observation space flattening optional (on by default). To turn off for an agent, change the agent_settings.flatten_obs setting in the config.
### Added ### Added

View File

@@ -13,7 +13,25 @@ This section allows selecting which training framework and algorithm to use, and
``io_settings`` ``io_settings``
--------------- ---------------
This section configures how the ``PrimaiteSession`` saves data. This section configures how PrimAITE saves data during simulation and training.
**save_final_model**: Only used if training with PrimaiteSession, if true, the policy will be saved after the final training iteration.
**save_checkpoints**: Only used if training with PrimaiteSession, if true, the policy will be saved periodically during training.
**checkpoint_interval**: Only used if training with PrimaiteSession and if ``save_checkpoints`` is true. Defines how often to save the policy during training.
**save_logs**: *currently unused*.
**save_transactions**: *currently unused*.
**save_tensorboard_logs**: *currently unused*.
**save_step_metadata**: Whether to save the RL agents' action, environment state, and other data at every single step.
**save_pcap_logs**: Whether to save pcap files of all network traffic during the simulation.
**save_sys_logs**: Whether to save system logs from all nodes during the simulation.
``game`` ``game``
-------- --------
@@ -56,6 +74,10 @@ Description of configurable items:
**agent_settings**: **agent_settings**:
Settings passed to the agent during initialisation. These depend on the agent class. Settings passed to the agent during initialisation. These depend on the agent class.
Reinforcement learning agents use the ``ProxyAgent`` class, they accept these agent settings:
**flatten_obs**: If true, gymnasium flattening will be performed on the observation space before sending to the agent. Set this to true if your agent does not support nested observation spaces.
``simulation`` ``simulation``
-------------- --------------
In this section the network layout is defined. This part of the config follows a hierarchical structure. Almost every component defines a ``ref`` field which acts as a human-readable unique identifier, used by other parts of the config, such as agents. In this section the network layout is defined. This part of the config follows a hierarchical structure. Almost every component defines a ``ref`` field which acts as a human-readable unique identifier, used by other parts of the config, such as agents.

View File

@@ -14,6 +14,8 @@ io_settings:
save_checkpoints: true save_checkpoints: true
checkpoint_interval: 5 checkpoint_interval: 5
save_step_metadata: false save_step_metadata: false
save_pcap_logs: true
save_sys_logs: true
game: game:
@@ -523,7 +525,7 @@ agents:
agent_settings: agent_settings:
# ... flatten_obs: true

View File

@@ -44,6 +44,8 @@ class AgentSettings(BaseModel):
start_settings: Optional[AgentStartSettings] = None start_settings: Optional[AgentStartSettings] = None
"Configuration for when an agent begins performing it's actions" "Configuration for when an agent begins performing it's actions"
flatten_obs: bool = True
"Whether to flatten the observation space before passing it to the agent. True by default."
@classmethod @classmethod
def from_config(cls, config: Optional[Dict]) -> "AgentSettings": def from_config(cls, config: Optional[Dict]) -> "AgentSettings":
@@ -166,6 +168,7 @@ class ProxyAgent(AbstractAgent):
action_space: Optional[ActionManager], action_space: Optional[ActionManager],
observation_space: Optional[ObservationManager], observation_space: Optional[ObservationManager],
reward_function: Optional[RewardFunction], reward_function: Optional[RewardFunction],
agent_settings: Optional[AgentSettings] = None,
) -> None: ) -> None:
super().__init__( super().__init__(
agent_name=agent_name, agent_name=agent_name,
@@ -174,6 +177,7 @@ class ProxyAgent(AbstractAgent):
reward_function=reward_function, reward_function=reward_function,
) )
self.most_recent_action: ActType self.most_recent_action: ActType
self.flatten_obs: bool = agent_settings.flatten_obs if agent_settings else False
def get_action(self, obs: ObsType, reward: float = 0.0) -> Tuple[str, Dict]: def get_action(self, obs: ObsType, reward: float = 0.0) -> Tuple[str, Dict]:
""" """

View File

@@ -432,6 +432,7 @@ class PrimaiteGame:
action_space=action_space, action_space=action_space,
observation_space=obs_space, observation_space=obs_space,
reward_function=rew_function, reward_function=rew_function,
agent_settings=agent_settings,
) )
game.agents.append(new_agent) game.agents.append(new_agent)
game.rl_agents.append(new_agent) game.rl_agents.append(new_agent)

View File

@@ -39,6 +39,15 @@
"#### Create a Ray algorithm and pass it our config." "#### Create a Ray algorithm and pass it our config."
] ]
}, },
{
"cell_type": "code",
"execution_count": null,
"metadata": {},
"outputs": [],
"source": [
"print(cfg['agents'][2]['agent_settings'])"
]
},
{ {
"cell_type": "code", "cell_type": "code",
"execution_count": null, "execution_count": null,
@@ -76,6 +85,13 @@
" param_space=config\n", " param_space=config\n",
").fit()\n" ").fit()\n"
] ]
},
{
"cell_type": "code",
"execution_count": null,
"metadata": {},
"outputs": [],
"source": []
} }
], ],
"metadata": { "metadata": {

View File

@@ -81,10 +81,16 @@ class PrimaiteGymEnv(gymnasium.Env):
@property @property
def observation_space(self) -> gymnasium.Space: def observation_space(self) -> gymnasium.Space:
"""Return the observation space of the environment.""" """Return the observation space of the environment."""
if self.agent.flatten_obs:
return gymnasium.spaces.flatten_space(self.agent.observation_manager.space) return gymnasium.spaces.flatten_space(self.agent.observation_manager.space)
else:
return self.agent.observation_manager.space
def _get_obs(self) -> ObsType: def _get_obs(self) -> ObsType:
"""Return the current observation.""" """Return the current observation."""
if not self.agent.flatten_obs:
return self.agent.observation_manager.current_observation
else:
unflat_space = self.agent.observation_manager.space unflat_space = self.agent.observation_manager.space
unflat_obs = self.agent.observation_manager.current_observation unflat_obs = self.agent.observation_manager.current_observation
return gymnasium.spaces.flatten(unflat_space, unflat_obs) return gymnasium.spaces.flatten(unflat_space, unflat_obs)

View File

@@ -24,9 +24,13 @@ class SessionIOSettings(BaseModel):
save_transactions: bool = True save_transactions: bool = True
"""Whether to save transactions, If true, the session path will have a transactions folder.""" """Whether to save transactions, If true, the session path will have a transactions folder."""
save_tensorboard_logs: bool = False save_tensorboard_logs: bool = False
"""Whether to save tensorboard logs. If true, the session path will have a tenorboard_logs folder.""" """Whether to save tensorboard logs. If true, the session path will have a tensorboard_logs folder."""
save_step_metadata: bool = False save_step_metadata: bool = False
"""Whether to save the RL agents' action, environment state, and other data at every single step.""" """Whether to save the RL agents' action, environment state, and other data at every single step."""
save_pcap_logs: bool = False
"""Whether to save PCAP logs."""
save_sys_logs: bool = False
"""Whether to save system logs."""
class SessionIO: class SessionIO:
@@ -39,9 +43,10 @@ class SessionIO:
def __init__(self, settings: SessionIOSettings = SessionIOSettings()) -> None: def __init__(self, settings: SessionIOSettings = SessionIOSettings()) -> None:
self.settings: SessionIOSettings = settings self.settings: SessionIOSettings = settings
self.session_path: Path = self.generate_session_path() self.session_path: Path = self.generate_session_path()
# set global SIM_OUTPUT path # set global SIM_OUTPUT path
SIM_OUTPUT.path = self.session_path / "simulation_output" SIM_OUTPUT.path = self.session_path / "simulation_output"
SIM_OUTPUT.save_pcap_logs = self.settings.save_pcap_logs
SIM_OUTPUT.save_sys_logs = self.settings.save_sys_logs
# warning TODO: must be careful not to re-initialise sessionIO because it will create a new path each time it's # warning TODO: must be careful not to re-initialise sessionIO because it will create a new path each time it's
# possible refactor needed # possible refactor needed

View File

@@ -54,7 +54,7 @@ class PrimaiteSession:
self.policy: PolicyABC self.policy: PolicyABC
"""The reinforcement learning policy.""" """The reinforcement learning policy."""
self.io_manager = SessionIO() self.io_manager: Optional["SessionIO"] = None
"""IO manager for the session.""" """IO manager for the session."""
self.game: PrimaiteGame = game self.game: PrimaiteGame = game
@@ -101,9 +101,9 @@ class PrimaiteSession:
# CREATE ENVIRONMENT # CREATE ENVIRONMENT
if sess.training_options.rl_framework == "RLLIB_single_agent": if sess.training_options.rl_framework == "RLLIB_single_agent":
sess.env = PrimaiteRayEnv(env_config={"game": game}) sess.env = PrimaiteRayEnv(env_config={"cfg": cfg})
elif sess.training_options.rl_framework == "RLLIB_multi_agent": elif sess.training_options.rl_framework == "RLLIB_multi_agent":
sess.env = PrimaiteRayMARLEnv(env_config={"game": game}) sess.env = PrimaiteRayMARLEnv(env_config={"cfg": cfg})
elif sess.training_options.rl_framework == "SB3": elif sess.training_options.rl_framework == "SB3":
sess.env = PrimaiteGymEnv(game=game) sess.env = PrimaiteGymEnv(game=game)

View File

@@ -7,11 +7,13 @@ from primaite import _PRIMAITE_ROOT
__all__ = ["SIM_OUTPUT"] __all__ = ["SIM_OUTPUT"]
class __SimOutput: class _SimOutput:
def __init__(self): def __init__(self):
self._path: Path = ( self._path: Path = (
_PRIMAITE_ROOT.parent.parent / "simulation_output" / datetime.now().strftime("%Y-%m-%d_%H-%M-%S") _PRIMAITE_ROOT.parent.parent / "simulation_output" / datetime.now().strftime("%Y-%m-%d_%H-%M-%S")
) )
self.save_pcap_logs: bool = False
self.save_sys_logs: bool = False
@property @property
def path(self) -> Path: def path(self) -> Path:
@@ -23,4 +25,4 @@ class __SimOutput:
self._path.mkdir(exist_ok=True, parents=True) self._path.mkdir(exist_ok=True, parents=True)
SIM_OUTPUT = __SimOutput() SIM_OUTPUT = _SimOutput()

View File

@@ -41,6 +41,9 @@ class PacketCapture:
def setup_logger(self): def setup_logger(self):
"""Set up the logger configuration.""" """Set up the logger configuration."""
if not SIM_OUTPUT.save_pcap_logs:
return
log_path = self._get_log_path() log_path = self._get_log_path()
file_handler = logging.FileHandler(filename=log_path) file_handler = logging.FileHandler(filename=log_path)
@@ -88,5 +91,6 @@ class PacketCapture:
:param frame: The PCAP frame to capture. :param frame: The PCAP frame to capture.
""" """
if SIM_OUTPUT.save_pcap_logs:
msg = frame.model_dump_json() msg = frame.model_dump_json()
self.logger.log(level=60, msg=msg) # Log at custom log level > CRITICAL self.logger.log(level=60, msg=msg) # Log at custom log level > CRITICAL

View File

@@ -41,6 +41,9 @@ class SysLog:
The logger is set to the DEBUG level, and is equipped with a handler that writes to a file and filters out The logger is set to the DEBUG level, and is equipped with a handler that writes to a file and filters out
JSON-like messages. JSON-like messages.
""" """
if not SIM_OUTPUT.save_sys_logs:
return
log_path = self._get_log_path() log_path = self._get_log_path()
file_handler = logging.FileHandler(filename=log_path) file_handler = logging.FileHandler(filename=log_path)
file_handler.setLevel(logging.DEBUG) file_handler.setLevel(logging.DEBUG)
@@ -91,6 +94,7 @@ class SysLog:
:param msg: The message to be logged. :param msg: The message to be logged.
""" """
if SIM_OUTPUT.save_sys_logs:
self.logger.debug(msg) self.logger.debug(msg)
def info(self, msg: str): def info(self, msg: str):
@@ -99,6 +103,7 @@ class SysLog:
:param msg: The message to be logged. :param msg: The message to be logged.
""" """
if SIM_OUTPUT.save_sys_logs:
self.logger.info(msg) self.logger.info(msg)
def warning(self, msg: str): def warning(self, msg: str):
@@ -107,6 +112,7 @@ class SysLog:
:param msg: The message to be logged. :param msg: The message to be logged.
""" """
if SIM_OUTPUT.save_sys_logs:
self.logger.warning(msg) self.logger.warning(msg)
def error(self, msg: str): def error(self, msg: str):
@@ -115,6 +121,7 @@ class SysLog:
:param msg: The message to be logged. :param msg: The message to be logged.
""" """
if SIM_OUTPUT.save_sys_logs:
self.logger.error(msg) self.logger.error(msg)
def critical(self, msg: str): def critical(self, msg: str):
@@ -123,4 +130,5 @@ class SysLog:
:param msg: The message to be logged. :param msg: The message to be logged.
""" """
if SIM_OUTPUT.save_sys_logs:
self.logger.critical(msg) self.logger.critical(msg)