From 0c195463222ff29867bb1365031efc132725f21c Mon Sep 17 00:00:00 2001 From: Marek Wolan Date: Wed, 29 May 2024 14:54:45 +0100 Subject: [PATCH 01/15] #2588 optional RL deps --- .azure/azure-build-deploy-docs-pipeline.yml | 2 +- .azure/azure-ci-build-pipeline.yaml | 4 +- .github/workflows/build-sphinx.yml | 2 +- .github/workflows/python-package.yml | 2 +- README.md | 9 +- docs/source/getting_started.rst | 6 +- pyproject.toml | 8 +- .../notebooks/Training-an-RLLib-Agent.ipynb | 4 +- src/primaite/session/environment.py | 162 ---------------- src/primaite/session/ray_envs.py | 174 ++++++++++++++++++ .../test_rllib_multi_agent_environment.py | 2 +- .../test_rllib_single_agent_environment.py | 2 +- .../e2e_integration_tests/test_environment.py | 3 +- .../test_episode_scheduler.py | 3 +- 14 files changed, 201 insertions(+), 182 deletions(-) create mode 100644 src/primaite/session/ray_envs.py diff --git a/.azure/azure-build-deploy-docs-pipeline.yml b/.azure/azure-build-deploy-docs-pipeline.yml index 01adce6d..8ebfe4d6 100644 --- a/.azure/azure-build-deploy-docs-pipeline.yml +++ b/.azure/azure-build-deploy-docs-pipeline.yml @@ -26,7 +26,7 @@ jobs: displayName: 'Install build dependencies' - script: | - pip install -e .[dev] + pip install -e .[dev,rl] displayName: 'Install PrimAITE for docs autosummary' - script: | diff --git a/.azure/azure-ci-build-pipeline.yaml b/.azure/azure-ci-build-pipeline.yaml index f0a1793e..a32ae20f 100644 --- a/.azure/azure-ci-build-pipeline.yaml +++ b/.azure/azure-ci-build-pipeline.yaml @@ -82,12 +82,12 @@ stages: - script: | PRIMAITE_WHEEL=$(ls ./dist/primaite*.whl) - python -m pip install $PRIMAITE_WHEEL[dev] + python -m pip install $PRIMAITE_WHEEL[dev,rl] displayName: 'Install PrimAITE' condition: or(eq( variables['Agent.OS'], 'Linux' ), eq( variables['Agent.OS'], 'Darwin' )) - script: | - forfiles /p dist\ /m *.whl /c "cmd /c python -m pip install @file[dev]" + forfiles /p dist\ /m *.whl /c "cmd /c python -m pip install @file[dev,rl]" displayName: 'Install PrimAITE' condition: eq( variables['Agent.OS'], 'Windows_NT' ) diff --git a/.github/workflows/build-sphinx.yml b/.github/workflows/build-sphinx.yml index 82da1c6b..da20fbd3 100644 --- a/.github/workflows/build-sphinx.yml +++ b/.github/workflows/build-sphinx.yml @@ -49,7 +49,7 @@ jobs: - name: Install PrimAITE for docs autosummary run: | set -x - python -m pip install -e .[dev] + python -m pip install -e .[dev,rl] - name: Run build script for Sphinx pages env: diff --git a/.github/workflows/python-package.yml b/.github/workflows/python-package.yml index ed94ad97..1b85f4be 100644 --- a/.github/workflows/python-package.yml +++ b/.github/workflows/python-package.yml @@ -48,7 +48,7 @@ jobs: - name: Install PrimAITE run: | PRIMAITE_WHEEL=$(ls ./dist/primaite*.whl) - python -m pip install $PRIMAITE_WHEEL[dev] + python -m pip install $PRIMAITE_WHEEL[dev,rl] - name: Perform PrimAITE Setup run: | diff --git a/README.md b/README.md index 3fd73b53..68a8488b 100644 --- a/README.md +++ b/README.md @@ -43,7 +43,7 @@ cd ~\primaite python3 -m venv .venv attrib +h .venv /s /d # Hides the .venv directory .\.venv\Scripts\activate -pip install https://github.com/Autonomous-Resilient-Cyber-Defence/PrimAITE/releases/download/v2.0.0/primaite-2.0.0-py3-none-any.whl +pip install primaite-3.0.0-py3-none-any.whl[rl] primaite setup ``` @@ -66,7 +66,7 @@ mkdir ~/primaite cd ~/primaite python3 -m venv .venv source .venv/bin/activate -pip install https://github.com/Autonomous-Resilient-Cyber-Defence/PrimAITE/releases/download/v2.0.0/primaite-2.0.0-py3-none-any.whl +pip install primaite-3.0.0-py3-none-any.whl[rl] primaite setup ``` @@ -105,7 +105,7 @@ source venv/bin/activate #### 5. Install `primaite` with the dev extra into the venv along with all of it's dependencies ```bash -python3 -m pip install -e .[dev] +python3 -m pip install -e .[dev,rl] ``` #### 6. Perform the PrimAITE setup: @@ -114,6 +114,9 @@ python3 -m pip install -e .[dev] primaite setup ``` +#### Note +*It is possible to install PrimAITE without Ray RLLib, StableBaselines3, or any deep learning libraries by omitting the `rl` flag in the pip install command.* + ### Running PrimAITE Use the provided jupyter notebooks as a starting point to try running PrimAITE. They are automatically copied to your PrimAITE notebook folder when you run `primaite setup`. diff --git a/docs/source/getting_started.rst b/docs/source/getting_started.rst index 7c91498c..6e6fc3e4 100644 --- a/docs/source/getting_started.rst +++ b/docs/source/getting_started.rst @@ -82,7 +82,7 @@ Install PrimAITE .. code-block:: bash :caption: Unix - pip install path/to/your/primaite.whl + pip install path/to/your/primaite.whl[rl] .. code-block:: powershell :caption: Windows (Powershell) @@ -133,12 +133,12 @@ of your choice: .. code-block:: bash :caption: Unix - pip install -e .[dev] + pip install -e .[dev,rl] .. code-block:: powershell :caption: Windows (Powershell) - pip install -e .[dev] + pip install -e .[dev,rl] To view the complete list of packages installed during PrimAITE installation, go to the dependencies page (:ref:`Dependencies`). diff --git a/pyproject.toml b/pyproject.toml index 5d913e1a..008f7c9c 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -36,11 +36,8 @@ dependencies = [ "polars==0.18.4", "prettytable==3.8.0", "PyYAML==6.0", - "stable-baselines3[extra]==2.1.0", - "tensorflow==2.12.0", "typer[all]==0.9.0", "pydantic==2.7.0", - "ray[rllib] >= 2.9, < 3", "ipywidgets" ] @@ -55,6 +52,11 @@ license-files = ["LICENSE"] [project.optional-dependencies] +rl = [ + "ray[rllib] >= 2.9, < 3", + "tensorflow==2.12.0", + "stable-baselines3[extra]==2.1.0", +] dev = [ "build==0.10.0", "flake8==6.0.0", diff --git a/src/primaite/notebooks/Training-an-RLLib-Agent.ipynb b/src/primaite/notebooks/Training-an-RLLib-Agent.ipynb index 1fb66405..9d458426 100644 --- a/src/primaite/notebooks/Training-an-RLLib-Agent.ipynb +++ b/src/primaite/notebooks/Training-an-RLLib-Agent.ipynb @@ -18,7 +18,7 @@ "import yaml\n", "from primaite.config.load import data_manipulation_config_path\n", "\n", - "from primaite.session.environment import PrimaiteRayEnv\n", + "from primaite.session.ray_envs import PrimaiteRayEnv\n", "from ray.rllib.algorithms import ppo\n", "from ray import air, tune\n", "import ray\n", @@ -97,7 +97,7 @@ "name": "python", "nbconvert_exporter": "python", "pygments_lexer": "ipython3", - "version": "3.10.11" + "version": "3.10.12" } }, "nbformat": 4, diff --git a/src/primaite/session/environment.py b/src/primaite/session/environment.py index 6c42c701..1e9faded 100644 --- a/src/primaite/session/environment.py +++ b/src/primaite/session/environment.py @@ -4,7 +4,6 @@ from typing import Any, Dict, Optional, SupportsFloat, Tuple, Union import gymnasium from gymnasium.core import ActType, ObsType -from ray.rllib.env.multi_agent_env import MultiAgentEnv from primaite import getLogger from primaite.game.agent.interface import ProxyAgent @@ -128,164 +127,3 @@ class PrimaiteGymEnv(gymnasium.Env): if self.io.settings.save_agent_actions: all_agent_actions = {name: agent.action_history for name, agent in self.game.agents.items()} self.io.write_agent_actions(agent_actions=all_agent_actions, episode=self.episode_counter) - - -class PrimaiteRayEnv(gymnasium.Env): - """Ray wrapper that accepts a single `env_config` parameter in init function for compatibility with Ray.""" - - def __init__(self, env_config: Dict) -> None: - """Initialise the environment. - - :param env_config: A dictionary containing the environment configuration. - :type env_config: Dict - """ - self.env = PrimaiteGymEnv(env_config=env_config) - # self.env.episode_counter -= 1 - self.action_space = self.env.action_space - self.observation_space = self.env.observation_space - - def reset(self, *, seed: int = None, options: dict = None) -> Tuple[ObsType, Dict]: - """Reset the environment.""" - return self.env.reset(seed=seed) - - def step(self, action: ActType) -> Tuple[ObsType, SupportsFloat, bool, bool, Dict]: - """Perform a step in the environment.""" - return self.env.step(action) - - def close(self): - """Close the simulation.""" - self.env.close() - - @property - def game(self) -> PrimaiteGame: - """Pass through game from env.""" - return self.env.game - - -class PrimaiteRayMARLEnv(MultiAgentEnv): - """Ray Environment that inherits from MultiAgentEnv to allow training MARL systems.""" - - def __init__(self, env_config: Dict) -> None: - """Initialise the environment. - - :param env_config: A dictionary containing the environment configuration. It must contain a single key, `game` - which is the PrimaiteGame instance. - :type env_config: Dict - """ - self.episode_counter: int = 0 - """Current episode number.""" - self.episode_scheduler: EpisodeScheduler = build_scheduler(env_config) - """Object that returns a config corresponding to the current episode.""" - self.io = PrimaiteIO.from_config(self.episode_scheduler(0).get("io_settings", {})) - """Handles IO for the environment. This produces sys logs, agent logs, etc.""" - self.game: PrimaiteGame = PrimaiteGame.from_config(self.episode_scheduler(self.episode_counter)) - """Reference to the primaite game""" - self._agent_ids = list(self.game.rl_agents.keys()) - """Agent ids. This is a list of strings of agent names.""" - - self.terminateds = set() - self.truncateds = set() - self.observation_space = gymnasium.spaces.Dict( - { - name: gymnasium.spaces.flatten_space(agent.observation_manager.space) - for name, agent in self.agents.items() - } - ) - self.action_space = gymnasium.spaces.Dict( - {name: agent.action_manager.space for name, agent in self.agents.items()} - ) - - super().__init__() - - @property - def agents(self) -> Dict[str, ProxyAgent]: - """Grab a fresh reference to the agents from this episode's game object.""" - return {name: self.game.rl_agents[name] for name in self._agent_ids} - - def reset(self, *, seed: int = None, options: dict = None) -> Tuple[ObsType, Dict]: - """Reset the environment.""" - rewards = {name: agent.reward_function.total_reward for name, agent in self.agents.items()} - _LOGGER.info(f"Resetting environment, episode {self.episode_counter}, " f"avg. reward: {rewards}") - - if self.io.settings.save_agent_actions: - all_agent_actions = {name: agent.action_history for name, agent in self.game.agents.items()} - self.io.write_agent_actions(agent_actions=all_agent_actions, episode=self.episode_counter) - - self.episode_counter += 1 - self.game: PrimaiteGame = PrimaiteGame.from_config(self.episode_scheduler(self.episode_counter)) - self.game.setup_for_episode(episode=self.episode_counter) - state = self.game.get_sim_state() - self.game.update_agents(state) - next_obs = self._get_obs() - info = {} - return next_obs, info - - def step( - self, actions: Dict[str, ActType] - ) -> Tuple[Dict[str, ObsType], Dict[str, SupportsFloat], Dict[str, bool], Dict[str, bool], Dict]: - """Perform a step in the environment. Adherent to Ray MultiAgentEnv step API. - - :param actions: Dict of actions. The key is agent identifier and the value is a gymnasium action instance. - :type actions: Dict[str, ActType] - :return: Observations, rewards, terminateds, truncateds, and info. Each one is a dictionary keyed by agent - identifier. - :rtype: Tuple[Dict[str,ObsType], Dict[str, SupportsFloat], Dict[str,bool], Dict[str,bool], Dict] - """ - step = self.game.step_counter - # 1. Perform actions - for agent_name, action in actions.items(): - self.agents[agent_name].store_action(action) - self.game.pre_timestep() - self.game.apply_agent_actions() - - # 2. Advance timestep - self.game.advance_timestep() - - # 3. Get next observations - state = self.game.get_sim_state() - self.game.update_agents(state) - next_obs = self._get_obs() - - # 4. Get rewards - rewards = {name: agent.reward_function.current_reward for name, agent in self.agents.items()} - _LOGGER.info(f"step: {self.game.step_counter}, Rewards: {rewards}") - terminateds = {name: False for name, _ in self.agents.items()} - truncateds = {name: self.game.calculate_truncated() for name, _ in self.agents.items()} - infos = {name: {} for name, _ in self.agents.items()} - terminateds["__all__"] = len(self.terminateds) == len(self.agents) - truncateds["__all__"] = self.game.calculate_truncated() - if self.game.save_step_metadata: - self._write_step_metadata_json(step, actions, state, rewards) - return next_obs, rewards, terminateds, truncateds, infos - - def _write_step_metadata_json(self, step: int, actions: Dict, state: Dict, rewards: Dict): - output_dir = SIM_OUTPUT.path / f"episode_{self.episode_counter}" / "step_metadata" - - output_dir.mkdir(parents=True, exist_ok=True) - path = output_dir / f"step_{step}.json" - - data = { - "episode": self.episode_counter, - "step": step, - "actions": {agent_name: int(action) for agent_name, action in actions.items()}, - "reward": rewards, - "state": state, - } - with open(path, "w") as file: - json.dump(data, file) - - def _get_obs(self) -> Dict[str, ObsType]: - """Return the current observation.""" - obs = {} - for agent_name in self._agent_ids: - agent = self.game.rl_agents[agent_name] - unflat_space = agent.observation_manager.space - unflat_obs = agent.observation_manager.current_observation - obs[agent_name] = gymnasium.spaces.flatten(unflat_space, unflat_obs) - return obs - - def close(self): - """Close the simulation.""" - if self.io.settings.save_agent_actions: - all_agent_actions = {name: agent.action_history for name, agent in self.game.agents.items()} - self.io.write_agent_actions(agent_actions=all_agent_actions, episode=self.episode_counter) diff --git a/src/primaite/session/ray_envs.py b/src/primaite/session/ray_envs.py new file mode 100644 index 00000000..5149a225 --- /dev/null +++ b/src/primaite/session/ray_envs.py @@ -0,0 +1,174 @@ +import json +from typing import Dict, SupportsFloat, Tuple + +import gymnasium +from gymnasium.core import ActType, ObsType +from ray.rllib.env.multi_agent_env import MultiAgentEnv + +from primaite.game.agent.interface import ProxyAgent +from primaite.game.game import PrimaiteGame +from primaite.session.environment import _LOGGER, PrimaiteGymEnv +from primaite.session.episode_schedule import build_scheduler, EpisodeScheduler +from primaite.session.io import PrimaiteIO +from primaite.simulator import SIM_OUTPUT + + +class PrimaiteRayMARLEnv(MultiAgentEnv): + """Ray Environment that inherits from MultiAgentEnv to allow training MARL systems.""" + + def __init__(self, env_config: Dict) -> None: + """Initialise the environment. + + :param env_config: A dictionary containing the environment configuration. It must contain a single key, `game` + which is the PrimaiteGame instance. + :type env_config: Dict + """ + self.episode_counter: int = 0 + """Current episode number.""" + self.episode_scheduler: EpisodeScheduler = build_scheduler(env_config) + """Object that returns a config corresponding to the current episode.""" + self.io = PrimaiteIO.from_config(self.episode_scheduler(0).get("io_settings", {})) + """Handles IO for the environment. This produces sys logs, agent logs, etc.""" + self.game: PrimaiteGame = PrimaiteGame.from_config(self.episode_scheduler(self.episode_counter)) + """Reference to the primaite game""" + self._agent_ids = list(self.game.rl_agents.keys()) + """Agent ids. This is a list of strings of agent names.""" + + self.terminateds = set() + self.truncateds = set() + self.observation_space = gymnasium.spaces.Dict( + { + name: gymnasium.spaces.flatten_space(agent.observation_manager.space) + for name, agent in self.agents.items() + } + ) + self.action_space = gymnasium.spaces.Dict( + {name: agent.action_manager.space for name, agent in self.agents.items()} + ) + + super().__init__() + + @property + def agents(self) -> Dict[str, ProxyAgent]: + """Grab a fresh reference to the agents from this episode's game object.""" + return {name: self.game.rl_agents[name] for name in self._agent_ids} + + def reset(self, *, seed: int = None, options: dict = None) -> Tuple[ObsType, Dict]: + """Reset the environment.""" + rewards = {name: agent.reward_function.total_reward for name, agent in self.agents.items()} + _LOGGER.info(f"Resetting environment, episode {self.episode_counter}, " f"avg. reward: {rewards}") + + if self.io.settings.save_agent_actions: + all_agent_actions = {name: agent.action_history for name, agent in self.game.agents.items()} + self.io.write_agent_actions(agent_actions=all_agent_actions, episode=self.episode_counter) + + self.episode_counter += 1 + self.game: PrimaiteGame = PrimaiteGame.from_config(self.episode_scheduler(self.episode_counter)) + self.game.setup_for_episode(episode=self.episode_counter) + state = self.game.get_sim_state() + self.game.update_agents(state) + next_obs = self._get_obs() + info = {} + return next_obs, info + + def step( + self, actions: Dict[str, ActType] + ) -> Tuple[Dict[str, ObsType], Dict[str, SupportsFloat], Dict[str, bool], Dict[str, bool], Dict]: + """Perform a step in the environment. Adherent to Ray MultiAgentEnv step API. + + :param actions: Dict of actions. The key is agent identifier and the value is a gymnasium action instance. + :type actions: Dict[str, ActType] + :return: Observations, rewards, terminateds, truncateds, and info. Each one is a dictionary keyed by agent + identifier. + :rtype: Tuple[Dict[str,ObsType], Dict[str, SupportsFloat], Dict[str,bool], Dict[str,bool], Dict] + """ + step = self.game.step_counter + # 1. Perform actions + for agent_name, action in actions.items(): + self.agents[agent_name].store_action(action) + self.game.pre_timestep() + self.game.apply_agent_actions() + + # 2. Advance timestep + self.game.advance_timestep() + + # 3. Get next observations + state = self.game.get_sim_state() + self.game.update_agents(state) + next_obs = self._get_obs() + + # 4. Get rewards + rewards = {name: agent.reward_function.current_reward for name, agent in self.agents.items()} + _LOGGER.info(f"step: {self.game.step_counter}, Rewards: {rewards}") + terminateds = {name: False for name, _ in self.agents.items()} + truncateds = {name: self.game.calculate_truncated() for name, _ in self.agents.items()} + infos = {name: {} for name, _ in self.agents.items()} + terminateds["__all__"] = len(self.terminateds) == len(self.agents) + truncateds["__all__"] = self.game.calculate_truncated() + if self.game.save_step_metadata: + self._write_step_metadata_json(step, actions, state, rewards) + return next_obs, rewards, terminateds, truncateds, infos + + def _write_step_metadata_json(self, step: int, actions: Dict, state: Dict, rewards: Dict): + output_dir = SIM_OUTPUT.path / f"episode_{self.episode_counter}" / "step_metadata" + + output_dir.mkdir(parents=True, exist_ok=True) + path = output_dir / f"step_{step}.json" + + data = { + "episode": self.episode_counter, + "step": step, + "actions": {agent_name: int(action) for agent_name, action in actions.items()}, + "reward": rewards, + "state": state, + } + with open(path, "w") as file: + json.dump(data, file) + + def _get_obs(self) -> Dict[str, ObsType]: + """Return the current observation.""" + obs = {} + for agent_name in self._agent_ids: + agent = self.game.rl_agents[agent_name] + unflat_space = agent.observation_manager.space + unflat_obs = agent.observation_manager.current_observation + obs[agent_name] = gymnasium.spaces.flatten(unflat_space, unflat_obs) + return obs + + def close(self): + """Close the simulation.""" + if self.io.settings.save_agent_actions: + all_agent_actions = {name: agent.action_history for name, agent in self.game.agents.items()} + self.io.write_agent_actions(agent_actions=all_agent_actions, episode=self.episode_counter) + + +class PrimaiteRayEnv(gymnasium.Env): + """Ray wrapper that accepts a single `env_config` parameter in init function for compatibility with Ray.""" + + def __init__(self, env_config: Dict) -> None: + """Initialise the environment. + + :param env_config: A dictionary containing the environment configuration. + :type env_config: Dict + """ + self.env = PrimaiteGymEnv(env_config=env_config) + # self.env.episode_counter -= 1 + self.action_space = self.env.action_space + self.observation_space = self.env.observation_space + + def reset(self, *, seed: int = None, options: dict = None) -> Tuple[ObsType, Dict]: + """Reset the environment.""" + return self.env.reset(seed=seed) + + def step(self, action: ActType) -> Tuple[ObsType, SupportsFloat, bool, bool, Dict]: + """Perform a step in the environment.""" + return self.env.step(action) + + def close(self): + """Close the simulation.""" + self.env.close() + + @property + def game(self) -> PrimaiteGame: + """Pass through game from env.""" + return self.env.game diff --git a/tests/e2e_integration_tests/environments/test_rllib_multi_agent_environment.py b/tests/e2e_integration_tests/environments/test_rllib_multi_agent_environment.py index 712a16c4..9b550dd2 100644 --- a/tests/e2e_integration_tests/environments/test_rllib_multi_agent_environment.py +++ b/tests/e2e_integration_tests/environments/test_rllib_multi_agent_environment.py @@ -3,7 +3,7 @@ import yaml from ray import air, tune from ray.rllib.algorithms.ppo import PPOConfig -from primaite.session.environment import PrimaiteRayMARLEnv +from primaite.session.ray_envs import PrimaiteRayMARLEnv from tests import TEST_ASSETS_ROOT MULTI_AGENT_PATH = TEST_ASSETS_ROOT / "configs/multi_agent_session.yaml" diff --git a/tests/e2e_integration_tests/environments/test_rllib_single_agent_environment.py b/tests/e2e_integration_tests/environments/test_rllib_single_agent_environment.py index d9057fef..f56f0f85 100644 --- a/tests/e2e_integration_tests/environments/test_rllib_single_agent_environment.py +++ b/tests/e2e_integration_tests/environments/test_rllib_single_agent_environment.py @@ -8,7 +8,7 @@ from ray.rllib.algorithms import ppo from primaite.config.load import data_manipulation_config_path from primaite.game.game import PrimaiteGame -from primaite.session.environment import PrimaiteRayEnv +from primaite.session.ray_envs import PrimaiteRayEnv @pytest.mark.skip(reason="Slow, reenable later") diff --git a/tests/e2e_integration_tests/test_environment.py b/tests/e2e_integration_tests/test_environment.py index accfad50..0a2c6add 100644 --- a/tests/e2e_integration_tests/test_environment.py +++ b/tests/e2e_integration_tests/test_environment.py @@ -4,7 +4,8 @@ import yaml from gymnasium.core import ObsType from numpy import ndarray -from primaite.session.environment import PrimaiteGymEnv, PrimaiteRayMARLEnv +from primaite.session.environment import PrimaiteGymEnv +from primaite.session.ray_envs import PrimaiteRayMARLEnv from primaite.simulator.network.hardware.nodes.host.server import Printer from primaite.simulator.network.hardware.nodes.network.wireless_router import WirelessRouter from tests import TEST_ASSETS_ROOT diff --git a/tests/integration_tests/configuration_file_parsing/test_episode_scheduler.py b/tests/integration_tests/configuration_file_parsing/test_episode_scheduler.py index 6b40fb1a..c6fd1a2f 100644 --- a/tests/integration_tests/configuration_file_parsing/test_episode_scheduler.py +++ b/tests/integration_tests/configuration_file_parsing/test_episode_scheduler.py @@ -1,7 +1,8 @@ import pytest import yaml -from primaite.session.environment import PrimaiteGymEnv, PrimaiteRayEnv, PrimaiteRayMARLEnv +from primaite.session.environment import PrimaiteGymEnv +from primaite.session.ray_envs import PrimaiteRayEnv, PrimaiteRayMARLEnv from tests.conftest import TEST_ASSETS_ROOT folder_path = TEST_ASSETS_ROOT / "configs" / "scenario_with_placeholders" From 1c86948d614ba01a548990e541bb5ee0dee771c8 Mon Sep 17 00:00:00 2001 From: Marek Wolan Date: Fri, 31 May 2024 12:12:35 +0100 Subject: [PATCH 02/15] #2588 fix import in notebook --- src/primaite/notebooks/Training-an-RLLIB-MARL-System.ipynb | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/primaite/notebooks/Training-an-RLLIB-MARL-System.ipynb b/src/primaite/notebooks/Training-an-RLLIB-MARL-System.ipynb index df688146..65b1595f 100644 --- a/src/primaite/notebooks/Training-an-RLLIB-MARL-System.ipynb +++ b/src/primaite/notebooks/Training-an-RLLIB-MARL-System.ipynb @@ -31,7 +31,7 @@ "import ray\n", "from ray import air, tune\n", "from ray.rllib.algorithms.ppo import PPOConfig\n", - "from primaite.session.environment import PrimaiteRayMARLEnv\n", + "from primaite.session.ray_envs import PrimaiteRayMARLEnv\n", "\n", "# If you get an error saying this config file doesn't exist, you may need to run `primaite setup` in your command line\n", "# to copy the files to your user data path.\n", From f8336d07bdfcc276d165541716d258bdf0670e21 Mon Sep 17 00:00:00 2001 From: Marek Wolan Date: Fri, 31 May 2024 13:28:56 +0100 Subject: [PATCH 03/15] #2626 fix too many open files bug --- src/primaite/session/environment.py | 2 ++ src/primaite/session/ray_envs.py | 2 ++ .../simulator/system/core/packet_capture.py | 14 ++++++++++++++ 3 files changed, 18 insertions(+) diff --git a/src/primaite/session/environment.py b/src/primaite/session/environment.py index edb8a476..6bcadab6 100644 --- a/src/primaite/session/environment.py +++ b/src/primaite/session/environment.py @@ -11,6 +11,7 @@ from primaite.game.game import PrimaiteGame from primaite.session.episode_schedule import build_scheduler, EpisodeScheduler from primaite.session.io import PrimaiteIO from primaite.simulator import SIM_OUTPUT +from primaite.simulator.system.core.packet_capture import PacketCapture _LOGGER = getLogger(__name__) @@ -92,6 +93,7 @@ class PrimaiteGymEnv(gymnasium.Env): all_agent_actions = {name: agent.action_history for name, agent in self.game.agents.items()} self.io.write_agent_actions(agent_actions=all_agent_actions, episode=self.episode_counter) self.episode_counter += 1 + PacketCapture.clear() self.game: PrimaiteGame = PrimaiteGame.from_config(cfg=self.episode_scheduler(self.episode_counter)) self.game.setup_for_episode(episode=self.episode_counter) state = self.game.get_sim_state() diff --git a/src/primaite/session/ray_envs.py b/src/primaite/session/ray_envs.py index 5149a225..f4691155 100644 --- a/src/primaite/session/ray_envs.py +++ b/src/primaite/session/ray_envs.py @@ -11,6 +11,7 @@ from primaite.session.environment import _LOGGER, PrimaiteGymEnv from primaite.session.episode_schedule import build_scheduler, EpisodeScheduler from primaite.session.io import PrimaiteIO from primaite.simulator import SIM_OUTPUT +from primaite.simulator.system.core.packet_capture import PacketCapture class PrimaiteRayMARLEnv(MultiAgentEnv): @@ -63,6 +64,7 @@ class PrimaiteRayMARLEnv(MultiAgentEnv): self.io.write_agent_actions(agent_actions=all_agent_actions, episode=self.episode_counter) self.episode_counter += 1 + PacketCapture.clear() self.game: PrimaiteGame = PrimaiteGame.from_config(self.episode_scheduler(self.episode_counter)) self.game.setup_for_episode(episode=self.episode_counter) state = self.game.get_sim_state() diff --git a/src/primaite/simulator/system/core/packet_capture.py b/src/primaite/simulator/system/core/packet_capture.py index cf38e94b..bc8a0584 100644 --- a/src/primaite/simulator/system/core/packet_capture.py +++ b/src/primaite/simulator/system/core/packet_capture.py @@ -21,6 +21,8 @@ class PacketCapture: The PCAPs are logged to: //__pcap.log """ + _logger_instances: List[logging.Logger] = [] + def __init__( self, hostname: str, @@ -65,10 +67,12 @@ class PacketCapture: if outbound: self.outbound_logger = logging.getLogger(self._get_logger_name(outbound)) + PacketCapture._logger_instances.append(self.outbound_logger) logger = self.outbound_logger else: self.inbound_logger = logging.getLogger(self._get_logger_name(outbound)) logger = self.inbound_logger + PacketCapture._logger_instances.append(self.inbound_logger) logger.setLevel(60) # Custom log level > CRITICAL to prevent any unwanted standard DEBUG-CRITICAL logs logger.addHandler(file_handler) @@ -122,3 +126,13 @@ class PacketCapture: if SIM_OUTPUT.save_pcap_logs: msg = frame.model_dump_json() self.outbound_logger.log(level=60, msg=msg) # Log at custom log level > CRITICAL + + @staticmethod + def clear(): + """Close all open PCAP file handlers.""" + for logger in PacketCapture._logger_instances: + handlers = logger.handlers[:] + for handler in handlers: + logger.removeHandler(handler) + handler.close() + PacketCapture._logger_instances = [] From efc4e3e9b088054a4bf0f59b8675291b9e0d5f33 Mon Sep 17 00:00:00 2001 From: "Archer.Bowen" Date: Fri, 31 May 2024 13:55:20 +0100 Subject: [PATCH 04/15] Core User Guide Feeedback Implemented. Feedback Issues left: 1.Issues with red-agent image not embeded correctly in data_manipulation_e2e notebook 2._autosummary/tests.unit_tests.html is still completely blank. 3. _index.html is not updated with new 2-pager 4. _dependencies is not updated to just include tier-1 and primary for v3. 5. definiton of user_app_home is not confirmed --- docs/index.rst | 30 +----------- docs/source/getting_started.rst | 4 +- docs/source/glossary.rst | 16 +----- docs/source/varying_config_files.rst | 49 +++++++++++++++++++ .../Data-Manipulation-E2E-Demonstration.ipynb | 20 ++++---- .../notebooks/Using-Episode-Schedules.ipynb | 44 ----------------- src/primaite/notebooks/multi-processing.ipynb | 3 +- 7 files changed, 67 insertions(+), 99 deletions(-) create mode 100644 docs/source/varying_config_files.rst diff --git a/docs/index.rst b/docs/index.rst index a0f302e9..af6eff86 100644 --- a/docs/index.rst +++ b/docs/index.rst @@ -55,23 +55,6 @@ PrimAITE provides a training and evaluation capability to AI agents in the conte Use of PrimAITE default scenarios within ARCD is supported by a “Use Case Profile” tailored to the scenario. -AI Assessment Capability -^^^^^^^^^^^^^^^^^^^^^^^^ - -PrimAITE includes the capability to support in-depth assessment of cyber defence AI by outputting logs of the environment state and AI behaviour throughout both training and evaluation sessions. These logs include the following data: - -- Timestamp; -- Episode and step number; -- Agent identifier; -- Observation space; -- Action taken (by defensive AI); -- Reward value. - -Logs are available in CSV format and provide coverage of the above data for every step of every episode. - - - - What is PrimAITE built with --------------------------- @@ -109,6 +92,7 @@ Head over to the :ref:`getting-started` page to install and setup PrimAITE! source/config source/environment source/customising_scenarios + source/varying_config_files .. toctree:: :caption: Notebooks: @@ -125,14 +109,4 @@ Head over to the :ref:`getting-started` page to install and setup PrimAITE! source/state_system source/request_system PrimAITE API - PrimAITE Tests - - -.. toctree:: - :caption: Project Links: - :hidden: - - Code - Issues - Pull Requests - Discussions + PrimAITE Tests \ No newline at end of file diff --git a/docs/source/getting_started.rst b/docs/source/getting_started.rst index 7c91498c..6b7d7542 100644 --- a/docs/source/getting_started.rst +++ b/docs/source/getting_started.rst @@ -107,7 +107,9 @@ Clone & Install PrimAITE for Development To be able to extend PrimAITE further, or to build wheels manually before install, clone the repository to a location of your choice: -1. Clone the repository +1. Clone the repository. + +For example: .. code-block:: bash diff --git a/docs/source/glossary.rst b/docs/source/glossary.rst index 67fd7aaa..00b2dc79 100644 --- a/docs/source/glossary.rst +++ b/docs/source/glossary.rst @@ -38,14 +38,11 @@ Glossary Blue Agent A defensive agent that protects the network from Red Agent attacks to minimise disruption to green agents and protect data. - Information Exchange Requirement (IER) - Simulates network traffic by sending data from one network node to another via links for a specified amount of time. IERs can be part of green agent behaviour or red agent behaviour. PrimAITE can be configured to apply a penalty for green agents' IERs being blocked and a reward for red agents' IERs being blocked. - Pattern-of-Life (PoL) PoLs allow agents to change the current hardware, OS, file system, or service statuses of nodes during the course of an episode. For example, a green agent may restart a server node to represent scheduled maintainance. A red agent's Pattern-of-Life can be used to attack nodes by changing their states to CORRUPTED or COMPROMISED. Reward - The reward is a single number used by the blue agent to understand whether it's performing well or poorly. RL agents change their behaviour in an attempt to increase the expected reward each episode. The reward is generated based on the current states of the environment / :term:`reference environment` and is impacted positively by things like green IERS running successfully and negatively by things like nodes being compromised. + The reward is a single number used by the blue agent to understand whether it's performing well or poorly. RL agents change their behaviour in an attempt to increase the expected reward each episode. The reward is generated based on the current states of the environment and is impacted positively by things like green PoL running successfully and negatively by things like nodes being compromised. Observation An observation is a representation of the current state of the environment that is given to the learning agent so it can decide on which action to perform. If the environment is 'fully observable', the observation contains information about every possible aspect of the environment. More commonly, the environment is 'partially observable' which means the learning agent has to make decisions without knowing every detail of the current environment state. @@ -65,17 +62,8 @@ Glossary Episode When an episode starts, the network simulation is reset to an initial state. The agents take actions on each step of the episode until it reaches a terminal state, which usually happens after a predetermined number of steps. After the terminal state is reached, a new episode starts and the RL agent has another opportunity to protect the network. - Reference environment - While the network simulation is unfolding, a parallel simulation takes place which is identical to the main one except that blue and red agent actions are not applied. This reference environment essentially shows what would be happening to the network if there had been no cyberattack or defense. The reference environment is used to calculate rewards. - - Transaction - PrimAITE records the decisions of the learning agent by saving its observation, action, and reward at every time step. During each session, this data is saved to disk to allow for full inspection. - Laydown The laydown is a file which defines the training scenario. It contains the network topology, firewall rules, services, protocols, and details about green and red agent behaviours. Gymnasium - PrimAITE uses the Gymnasium reinforcement learning framework API to create a training environment and interface with RL agents. Gymnasium defines a common way of creating observations, actions, and rewards. - - User app home - PrimAITE supports upgrading software version while retaining user data. The user data directory is where configs, notebooks, and results are stored, this location is `~/primaite` on linux/darwin and `C:\\Users\\\\primaite\\` on Windows. + PrimAITE uses the Gymnasium reinforcement learning framework API to create a training environment and interface with RL agents. Gymnasium defines a common way of creating observations, actions, and rewards. \ No newline at end of file diff --git a/docs/source/varying_config_files.rst b/docs/source/varying_config_files.rst new file mode 100644 index 00000000..34b83895 --- /dev/null +++ b/docs/source/varying_config_files.rst @@ -0,0 +1,49 @@ +.. only:: comment + + © Crown-owned copyright 2023, Defence Science and Technology Laboratory UK + +Defining variations in the config files +================ + +PrimAITE supports the ability to use different variations on a scenario at different episodes. This can be used to increase domain randomisation to prevent overfitting, or to set up curriculum learning to train agents to perform more complicated tasks. + +When using a fixed scenario, a single yaml config file is used. However, to use episode schedules, PrimAITE uses a directory with several config files that work together. +Defining variations in the config file. + +Base scenario +************* + +The base scenario is essentially the same as a fixed YAML configuration, but it can contain placeholders that are populated with episode-specific data at runtime. The base scenario contains any network, agent, or settings that remain fixed for the entire training/evaluation session. + +The placeholders are defined as YAML Aliases and they are denoted by an asterisk (*placeholder). + +Variations +********** + +For each variation that could be used in a placeholder, there is a separate yaml file that contains the data that should populate the placeholder. + +The data that fills the placeholder is defined as a YAML Anchor in a separate file, denoted by an ampersand ``&anchor``. + +Learn more about YAML Aliases and Anchors here. + +Schedule +******** + +Users must define which combination of scenario variations should be loaded in each episode. This takes the form of a YAML file with a relative path to the base scenario and a list of paths to be loaded in during each episode. + +It takes the following format: + +.. code-block:: yaml + + base_scenario: base.yaml + schedule: + 0: # list of variations to load in at episode 0 (before the first call to env.reset() happens) + - laydown_1.yaml + - attack_1.yaml + 1: # list of variations to load in at episode 1 (after the first env.reset() call) + - laydown_2.yaml + - attack_2.yaml + +For more information please refer to the ``Using Episode Schedules`` notebook in either :ref:`Executed Notebooks` or run the notebook interactively in ``notebooks/example_notebooks/``. + +For further information around notebooks in general refer to the :ref:`Example Jupyter Notebooks`. \ No newline at end of file diff --git a/src/primaite/notebooks/Data-Manipulation-E2E-Demonstration.ipynb b/src/primaite/notebooks/Data-Manipulation-E2E-Demonstration.ipynb index 8104149e..80a46fb3 100644 --- a/src/primaite/notebooks/Data-Manipulation-E2E-Demonstration.ipynb +++ b/src/primaite/notebooks/Data-Manipulation-E2E-Demonstration.ipynb @@ -180,15 +180,15 @@ "| link_id | endpoint_a | endpoint_b |\n", "|---------|------------------|-------------------|\n", "| 1 | router_1 | switch_1 |\n", - "| 1 | router_1 | switch_2 |\n", - "| 1 | switch_1 | domain_controller |\n", - "| 1 | switch_1 | web_server |\n", - "| 1 | switch_1 | database_server |\n", - "| 1 | switch_1 | backup_server |\n", - "| 1 | switch_1 | security_suite |\n", - "| 1 | switch_2 | client_1 |\n", - "| 1 | switch_2 | client_2 |\n", - "| 1 | switch_2 | security_suite |\n", + "| 2 | router_1 | switch_2 |\n", + "| 3 | switch_1 | domain_controller |\n", + "| 4 | switch_1 | web_server |\n", + "| 5 | switch_1 | database_server |\n", + "| 6 | switch_1 | backup_server |\n", + "| 7 | switch_1 | security_suite |\n", + "| 8 | switch_2 | client_1 |\n", + "| 9 | switch_2 | client_2 |\n", + "| 10 | switch_2 | security_suite |\n", "\n", "\n", "The ACL rules in the observation space appear in the same order that they do in the actual ACL. Though, only the first 10 rules are shown, there are default rules lower down that cannot be changed by the agent. The extra rules just allow the network to function normally, by allowing pings, ARP traffic, etc.\n", @@ -705,7 +705,7 @@ "name": "python", "nbconvert_exporter": "python", "pygments_lexer": "ipython3", - "version": "3.10.11" + "version": "3.10.8" } }, "nbformat": 4, diff --git a/src/primaite/notebooks/Using-Episode-Schedules.ipynb b/src/primaite/notebooks/Using-Episode-Schedules.ipynb index b0669472..c44339ae 100644 --- a/src/primaite/notebooks/Using-Episode-Schedules.ipynb +++ b/src/primaite/notebooks/Using-Episode-Schedules.ipynb @@ -13,50 +13,6 @@ "directory with several config files that work together." ] }, - { - "cell_type": "markdown", - "metadata": {}, - "source": [ - "## Defining variations in the config file.\n", - "\n", - "### Base scenario\n", - "The base scenario is essentially the same as a fixed YAML configuration, but it can contain placeholders that are \n", - "populated with episode-specific data at runtime. The base scenario contains any network, agent, or settings that\n", - "remain fixed for the entire training/evaluation session.\n", - "\n", - "The placeholders are defined as YAML Aliases and they are denoted by an asterisk (`*placeholder`).\n", - "\n", - "### Variations\n", - "For each variation that could be used in a placeholder, there is a separate yaml file that contains the data that should populate the placeholder.\n", - "\n", - "The data that fills the placeholder is defined as a YAML Anchor in a separate file, denoted by an ampersand (`&anchor`).\n", - "\n", - "[Learn more about YAML Aliases and Anchors here.](https://www.educative.io/blog/advanced-yaml-syntax-cheatsheet#:~:text=YAML%20Anchors%20and%20Alias)\n", - "\n", - "### Schedule\n", - "Users must define which combination of scenario variations should be loaded in each episode. This takes the form of a\n", - "YAML file with a relative path to the base scenario and a list of paths to be loaded in during each episode.\n", - "\n", - "It takes the following format:\n", - "\n" - ] - }, - { - "cell_type": "markdown", - "metadata": {}, - "source": [ - "```yaml\n", - "base_scenario: base.yaml\n", - "schedule:\n", - " 0: # list of variations to load in at episode 0 (before the first call to env.reset() happens)\n", - " - laydown_1.yaml\n", - " - attack_1.yaml\n", - " 1: # list of variations to load in at episode 1 (after the first env.reset() call)\n", - " - laydown_2.yaml\n", - " - attack_2.yaml\n", - "```\n" - ] - }, { "cell_type": "markdown", "metadata": {}, diff --git a/src/primaite/notebooks/multi-processing.ipynb b/src/primaite/notebooks/multi-processing.ipynb index 71addce6..2b806e7c 100644 --- a/src/primaite/notebooks/multi-processing.ipynb +++ b/src/primaite/notebooks/multi-processing.ipynb @@ -4,8 +4,7 @@ "cell_type": "markdown", "metadata": {}, "source": [ - "## Simple multi-processing demo using SubprocVecEnv from SB3\n", - "Based on a code example provided by Rachael Proctor." + "## Simple multi-processing demo using SubprocVecEnv from SB3" ] }, { From 71190a41c2dbca7a3548e2c46d434da3fda72e4c Mon Sep 17 00:00:00 2001 From: "Archer.Bowen" Date: Fri, 31 May 2024 14:10:01 +0100 Subject: [PATCH 05/15] Fix to red agent image not loading --- .../notebooks/Data-Manipulation-E2E-Demonstration.ipynb | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/primaite/notebooks/Data-Manipulation-E2E-Demonstration.ipynb b/src/primaite/notebooks/Data-Manipulation-E2E-Demonstration.ipynb index 80a46fb3..c6939afd 100644 --- a/src/primaite/notebooks/Data-Manipulation-E2E-Demonstration.ipynb +++ b/src/primaite/notebooks/Data-Manipulation-E2E-Demonstration.ipynb @@ -59,7 +59,7 @@ "\n", "At the start of every episode, the red agent randomly chooses either client 1 or client 2 to login to. It waits a bit then sends a DELETE query to the database from its chosen client. If the delete is successful, the database file is flagged as compromised to signal that data is not available.\n", "\n", - "[](_package_data/uc2_attack.png)\n", + "![uc2_attack](./_package_data/uc2_attack.png)\n", "\n", "_(click image to enlarge)_" ] From c5f131ece59eef137efaee89a141584aca4ae78a Mon Sep 17 00:00:00 2001 From: Marek Wolan Date: Fri, 31 May 2024 15:00:18 +0100 Subject: [PATCH 06/15] fix reward logging --- src/primaite/game/agent/interface.py | 16 +++++++++++----- src/primaite/game/agent/rewards.py | 18 +++++++++--------- src/primaite/game/game.py | 1 + ...ta-Manipulation-Customising-Red-Agent.ipynb | 4 ++-- .../Data-Manipulation-E2E-Demonstration.ipynb | 6 +++--- .../Training-an-RLLIB-MARL-System.ipynb | 2 +- .../notebooks/Using-Episode-Schedules.ipynb | 8 ++++---- src/primaite/session/environment.py | 10 +++++----- src/primaite/session/io.py | 2 +- src/primaite/session/ray_envs.py | 8 ++++---- .../game_layer/test_rewards.py | 6 +++--- 11 files changed, 44 insertions(+), 37 deletions(-) diff --git a/src/primaite/game/agent/interface.py b/src/primaite/game/agent/interface.py index cd4a1c29..444aa4f7 100644 --- a/src/primaite/game/agent/interface.py +++ b/src/primaite/game/agent/interface.py @@ -14,7 +14,7 @@ if TYPE_CHECKING: pass -class AgentActionHistoryItem(BaseModel): +class AgentHistoryItem(BaseModel): """One entry of an agent's action log - what the agent did and how the simulator responded in 1 step.""" timestep: int @@ -32,6 +32,8 @@ class AgentActionHistoryItem(BaseModel): response: RequestResponse """The response sent back by the simulator for this action.""" + reward: Optional[float] = None + class AgentStartSettings(BaseModel): """Configuration values for when an agent starts performing actions.""" @@ -110,7 +112,7 @@ class AbstractAgent(ABC): self.observation_manager: Optional[ObservationManager] = observation_space self.reward_function: Optional[RewardFunction] = reward_function self.agent_settings = agent_settings or AgentSettings() - self.action_history: List[AgentActionHistoryItem] = [] + self.history: List[AgentHistoryItem] = [] def update_observation(self, state: Dict) -> ObsType: """ @@ -130,7 +132,7 @@ class AbstractAgent(ABC): :return: Reward from the state. :rtype: float """ - return self.reward_function.update(state=state, last_action_response=self.action_history[-1]) + return self.reward_function.update(state=state, last_action_response=self.history[-1]) @abstractmethod def get_action(self, obs: ObsType, timestep: int = 0) -> Tuple[str, Dict]: @@ -161,12 +163,16 @@ class AbstractAgent(ABC): self, timestep: int, action: str, parameters: Dict[str, Any], request: RequestFormat, response: RequestResponse ) -> None: """Process the response from the most recent action.""" - self.action_history.append( - AgentActionHistoryItem( + self.history.append( + AgentHistoryItem( timestep=timestep, action=action, parameters=parameters, request=request, response=response ) ) + def save_reward_to_history(self) -> None: + """Update the most recent history item with the reward value.""" + self.history[-1].reward = self.reward_function.current_reward + class AbstractScriptedAgent(AbstractAgent): """Base class for actors which generate their own behaviour.""" diff --git a/src/primaite/game/agent/rewards.py b/src/primaite/game/agent/rewards.py index 0222bfcc..d77640d1 100644 --- a/src/primaite/game/agent/rewards.py +++ b/src/primaite/game/agent/rewards.py @@ -34,7 +34,7 @@ from primaite import getLogger from primaite.game.agent.utils import access_from_nested_dict, NOT_PRESENT_IN_STATE if TYPE_CHECKING: - from primaite.game.agent.interface import AgentActionHistoryItem + from primaite.game.agent.interface import AgentHistoryItem _LOGGER = getLogger(__name__) WhereType = Optional[Iterable[Union[str, int]]] @@ -44,7 +44,7 @@ class AbstractReward: """Base class for reward function components.""" @abstractmethod - def calculate(self, state: Dict, last_action_response: "AgentActionHistoryItem") -> float: + def calculate(self, state: Dict, last_action_response: "AgentHistoryItem") -> float: """Calculate the reward for the current state.""" return 0.0 @@ -64,7 +64,7 @@ class AbstractReward: class DummyReward(AbstractReward): """Dummy reward function component which always returns 0.""" - def calculate(self, state: Dict, last_action_response: "AgentActionHistoryItem") -> float: + def calculate(self, state: Dict, last_action_response: "AgentHistoryItem") -> float: """Calculate the reward for the current state.""" return 0.0 @@ -104,7 +104,7 @@ class DatabaseFileIntegrity(AbstractReward): file_name, ] - def calculate(self, state: Dict, last_action_response: "AgentActionHistoryItem") -> float: + def calculate(self, state: Dict, last_action_response: "AgentHistoryItem") -> float: """Calculate the reward for the current state. :param state: The current state of the simulation. @@ -159,7 +159,7 @@ class WebServer404Penalty(AbstractReward): """ self.location_in_state = ["network", "nodes", node_hostname, "services", service_name] - def calculate(self, state: Dict, last_action_response: "AgentActionHistoryItem") -> float: + def calculate(self, state: Dict, last_action_response: "AgentHistoryItem") -> float: """Calculate the reward for the current state. :param state: The current state of the simulation. @@ -213,7 +213,7 @@ class WebpageUnavailablePenalty(AbstractReward): self.location_in_state: List[str] = ["network", "nodes", node_hostname, "applications", "WebBrowser"] self._last_request_failed: bool = False - def calculate(self, state: Dict, last_action_response: "AgentActionHistoryItem") -> float: + def calculate(self, state: Dict, last_action_response: "AgentHistoryItem") -> float: """ Calculate the reward based on current simulation state, and the recent agent action. @@ -273,7 +273,7 @@ class GreenAdminDatabaseUnreachablePenalty(AbstractReward): self.location_in_state: List[str] = ["network", "nodes", node_hostname, "applications", "DatabaseClient"] self._last_request_failed: bool = False - def calculate(self, state: Dict, last_action_response: "AgentActionHistoryItem") -> float: + def calculate(self, state: Dict, last_action_response: "AgentHistoryItem") -> float: """ Calculate the reward based on current simulation state, and the recent agent action. @@ -343,7 +343,7 @@ class SharedReward(AbstractReward): self.callback: Callable[[str], float] = default_callback """Method that retrieves an agent's current reward given the agent's name.""" - def calculate(self, state: Dict, last_action_response: "AgentActionHistoryItem") -> float: + def calculate(self, state: Dict, last_action_response: "AgentHistoryItem") -> float: """Simply access the other agent's reward and return it.""" return self.callback(self.agent_name) @@ -389,7 +389,7 @@ class RewardFunction: """ self.reward_components.append((component, weight)) - def update(self, state: Dict, last_action_response: "AgentActionHistoryItem") -> float: + def update(self, state: Dict, last_action_response: "AgentHistoryItem") -> float: """Calculate the overall reward for the current state. :param state: The current state of the simulation. diff --git a/src/primaite/game/game.py b/src/primaite/game/game.py index ea5b3831..772ab5aa 100644 --- a/src/primaite/game/game.py +++ b/src/primaite/game/game.py @@ -160,6 +160,7 @@ class PrimaiteGame: agent = self.agents[agent_name] if self.step_counter > 0: # can't get reward before first action agent.update_reward(state=state) + agent.save_reward_to_history() agent.update_observation(state=state) # order of this doesn't matter so just use reward order agent.reward_function.total_reward += agent.reward_function.current_reward diff --git a/src/primaite/notebooks/Data-Manipulation-Customising-Red-Agent.ipynb b/src/primaite/notebooks/Data-Manipulation-Customising-Red-Agent.ipynb index 1b016bb8..21d67bab 100644 --- a/src/primaite/notebooks/Data-Manipulation-Customising-Red-Agent.ipynb +++ b/src/primaite/notebooks/Data-Manipulation-Customising-Red-Agent.ipynb @@ -22,7 +22,7 @@ "# Imports\n", "\n", "from primaite.config.load import data_manipulation_config_path\n", - "from primaite.game.agent.interface import AgentActionHistoryItem\n", + "from primaite.game.agent.interface import AgentHistoryItem\n", "from primaite.session.environment import PrimaiteGymEnv\n", "import yaml\n", "from pprint import pprint" @@ -63,7 +63,7 @@ "source": [ "def friendly_output_red_action(info):\n", " # parse the info dict form step output and write out what the red agent is doing\n", - " red_info : AgentActionHistoryItem = info['agent_actions']['data_manipulation_attacker']\n", + " red_info : AgentHistoryItem = info['agent_actions']['data_manipulation_attacker']\n", " red_action = red_info.action\n", " if red_action == 'DONOTHING':\n", " red_str = 'DO NOTHING'\n", diff --git a/src/primaite/notebooks/Data-Manipulation-E2E-Demonstration.ipynb b/src/primaite/notebooks/Data-Manipulation-E2E-Demonstration.ipynb index 8104149e..376b7f28 100644 --- a/src/primaite/notebooks/Data-Manipulation-E2E-Demonstration.ipynb +++ b/src/primaite/notebooks/Data-Manipulation-E2E-Demonstration.ipynb @@ -392,7 +392,7 @@ "# Imports\n", "from primaite.config.load import data_manipulation_config_path\n", "from primaite.session.environment import PrimaiteGymEnv\n", - "from primaite.game.agent.interface import AgentActionHistoryItem\n", + "from primaite.game.agent.interface import AgentHistoryItem\n", "import yaml\n", "from pprint import pprint\n" ] @@ -444,7 +444,7 @@ "source": [ "def friendly_output_red_action(info):\n", " # parse the info dict form step output and write out what the red agent is doing\n", - " red_info : AgentActionHistoryItem = info['agent_actions']['data_manipulation_attacker']\n", + " red_info : AgentHistoryItem = info['agent_actions']['data_manipulation_attacker']\n", " red_action = red_info.action\n", " if red_action == 'DONOTHING':\n", " red_str = 'DO NOTHING'\n", @@ -705,7 +705,7 @@ "name": "python", "nbconvert_exporter": "python", "pygments_lexer": "ipython3", - "version": "3.10.11" + "version": "3.10.12" } }, "nbformat": 4, diff --git a/src/primaite/notebooks/Training-an-RLLIB-MARL-System.ipynb b/src/primaite/notebooks/Training-an-RLLIB-MARL-System.ipynb index 65b1595f..61b988c6 100644 --- a/src/primaite/notebooks/Training-an-RLLIB-MARL-System.ipynb +++ b/src/primaite/notebooks/Training-an-RLLIB-MARL-System.ipynb @@ -25,7 +25,7 @@ "from primaite.game.game import PrimaiteGame\n", "import yaml\n", "\n", - "from primaite.session.environment import PrimaiteRayEnv\n", + "from primaite.session.ray_envs import PrimaiteRayEnv\n", "from primaite import PRIMAITE_PATHS\n", "\n", "import ray\n", diff --git a/src/primaite/notebooks/Using-Episode-Schedules.ipynb b/src/primaite/notebooks/Using-Episode-Schedules.ipynb index b0669472..062c7135 100644 --- a/src/primaite/notebooks/Using-Episode-Schedules.ipynb +++ b/src/primaite/notebooks/Using-Episode-Schedules.ipynb @@ -298,8 +298,8 @@ "table = PrettyTable()\n", "table.field_names = [\"step\", \"Green Action\", \"Red Action\"]\n", "for i in range(21):\n", - " green_action = env.game.agents['green_A'].action_history[i].action\n", - " red_action = env.game.agents['red_A'].action_history[i].action\n", + " green_action = env.game.agents['green_A'].history[i].action\n", + " red_action = env.game.agents['red_A'].history[i].action\n", " table.add_row([i, green_action, red_action])\n", "print(table)" ] @@ -329,8 +329,8 @@ "table = PrettyTable()\n", "table.field_names = [\"step\", \"Green Action\", \"Red Action\"]\n", "for i in range(21):\n", - " green_action = env.game.agents['green_B'].action_history[i].action\n", - " red_action = env.game.agents['red_B'].action_history[i].action\n", + " green_action = env.game.agents['green_B'].history[i].action\n", + " red_action = env.game.agents['red_B'].history[i].action\n", " table.add_row([i, green_action, red_action])\n", "print(table)" ] diff --git a/src/primaite/session/environment.py b/src/primaite/session/environment.py index edb8a476..52edbbb8 100644 --- a/src/primaite/session/environment.py +++ b/src/primaite/session/environment.py @@ -60,7 +60,7 @@ class PrimaiteGymEnv(gymnasium.Env): terminated = False truncated = self.game.calculate_truncated() info = { - "agent_actions": {name: agent.action_history[-1] for name, agent in self.game.agents.items()} + "agent_actions": {name: agent.history[-1] for name, agent in self.game.agents.items()} } # tell us what all the agents did for convenience. if self.game.save_step_metadata: self._write_step_metadata_json(step, action, state, reward) @@ -89,8 +89,8 @@ class PrimaiteGymEnv(gymnasium.Env): f"avg. reward: {self.agent.reward_function.total_reward}" ) if self.io.settings.save_agent_actions: - all_agent_actions = {name: agent.action_history for name, agent in self.game.agents.items()} - self.io.write_agent_actions(agent_actions=all_agent_actions, episode=self.episode_counter) + all_agent_actions = {name: agent.history for name, agent in self.game.agents.items()} + self.io.write_agent_log(agent_actions=all_agent_actions, episode=self.episode_counter) self.episode_counter += 1 self.game: PrimaiteGame = PrimaiteGame.from_config(cfg=self.episode_scheduler(self.episode_counter)) self.game.setup_for_episode(episode=self.episode_counter) @@ -125,5 +125,5 @@ class PrimaiteGymEnv(gymnasium.Env): def close(self): """Close the simulation.""" if self.io.settings.save_agent_actions: - all_agent_actions = {name: agent.action_history for name, agent in self.game.agents.items()} - self.io.write_agent_actions(agent_actions=all_agent_actions, episode=self.episode_counter) + all_agent_actions = {name: agent.history for name, agent in self.game.agents.items()} + self.io.write_agent_log(agent_actions=all_agent_actions, episode=self.episode_counter) diff --git a/src/primaite/session/io.py b/src/primaite/session/io.py index 8bbc1b07..2901457f 100644 --- a/src/primaite/session/io.py +++ b/src/primaite/session/io.py @@ -87,7 +87,7 @@ class PrimaiteIO: """Return the path where agent actions will be saved.""" return self.session_path / "agent_actions" / f"episode_{episode}.json" - def write_agent_actions(self, agent_actions: Dict[str, List], episode: int) -> None: + def write_agent_log(self, agent_actions: Dict[str, List], episode: int) -> None: """Take the contents of the agent action log and write it to a file. :param episode: Episode number diff --git a/src/primaite/session/ray_envs.py b/src/primaite/session/ray_envs.py index 5149a225..6dddde51 100644 --- a/src/primaite/session/ray_envs.py +++ b/src/primaite/session/ray_envs.py @@ -59,8 +59,8 @@ class PrimaiteRayMARLEnv(MultiAgentEnv): _LOGGER.info(f"Resetting environment, episode {self.episode_counter}, " f"avg. reward: {rewards}") if self.io.settings.save_agent_actions: - all_agent_actions = {name: agent.action_history for name, agent in self.game.agents.items()} - self.io.write_agent_actions(agent_actions=all_agent_actions, episode=self.episode_counter) + all_agent_actions = {name: agent.history for name, agent in self.game.agents.items()} + self.io.write_agent_log(agent_actions=all_agent_actions, episode=self.episode_counter) self.episode_counter += 1 self.game: PrimaiteGame = PrimaiteGame.from_config(self.episode_scheduler(self.episode_counter)) @@ -138,8 +138,8 @@ class PrimaiteRayMARLEnv(MultiAgentEnv): def close(self): """Close the simulation.""" if self.io.settings.save_agent_actions: - all_agent_actions = {name: agent.action_history for name, agent in self.game.agents.items()} - self.io.write_agent_actions(agent_actions=all_agent_actions, episode=self.episode_counter) + all_agent_actions = {name: agent.history for name, agent in self.game.agents.items()} + self.io.write_agent_log(agent_actions=all_agent_actions, episode=self.episode_counter) class PrimaiteRayEnv(gymnasium.Env): diff --git a/tests/integration_tests/game_layer/test_rewards.py b/tests/integration_tests/game_layer/test_rewards.py index 7c38057e..dff536de 100644 --- a/tests/integration_tests/game_layer/test_rewards.py +++ b/tests/integration_tests/game_layer/test_rewards.py @@ -1,6 +1,6 @@ import yaml -from primaite.game.agent.interface import AgentActionHistoryItem +from primaite.game.agent.interface import AgentHistoryItem from primaite.game.agent.rewards import GreenAdminDatabaseUnreachablePenalty, WebpageUnavailablePenalty from primaite.game.game import PrimaiteGame from primaite.session.environment import PrimaiteGymEnv @@ -75,7 +75,7 @@ def test_uc2_rewards(game_and_agent): state = game.get_sim_state() reward_value = comp.calculate( state, - last_action_response=AgentActionHistoryItem( + last_action_response=AgentHistoryItem( timestep=0, action="NODE_APPLICATION_EXECUTE", parameters={}, request=["execute"], response=response ), ) @@ -91,7 +91,7 @@ def test_uc2_rewards(game_and_agent): state = game.get_sim_state() reward_value = comp.calculate( state, - last_action_response=AgentActionHistoryItem( + last_action_response=AgentHistoryItem( timestep=0, action="NODE_APPLICATION_EXECUTE", parameters={}, request=["execute"], response=response ), ) From e48b71ea1a1627009c8259b1f44ce570aecf113d Mon Sep 17 00:00:00 2001 From: Marek Wolan Date: Fri, 31 May 2024 15:25:08 +0100 Subject: [PATCH 07/15] get ray to stop crashing --- pyproject.toml | 2 +- src/primaite/notebooks/Training-an-RLLIB-MARL-System.ipynb | 4 ++-- src/primaite/notebooks/Training-an-RLLib-Agent.ipynb | 7 +++---- src/primaite/notebooks/Training-an-SB3-Agent.ipynb | 7 +++++-- src/primaite/session/ray_envs.py | 3 ++- 5 files changed, 13 insertions(+), 10 deletions(-) diff --git a/pyproject.toml b/pyproject.toml index 9c94a388..d01299be 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -54,7 +54,7 @@ license-files = ["LICENSE"] [project.optional-dependencies] rl = [ - "ray[rllib] >= 2.9, < 3", + "ray[rllib] >= 2.20.0, < 3", "tensorflow==2.12.0", "stable-baselines3[extra]==2.1.0", ] diff --git a/src/primaite/notebooks/Training-an-RLLIB-MARL-System.ipynb b/src/primaite/notebooks/Training-an-RLLIB-MARL-System.ipynb index 61b988c6..5ffb19ad 100644 --- a/src/primaite/notebooks/Training-an-RLLIB-MARL-System.ipynb +++ b/src/primaite/notebooks/Training-an-RLLIB-MARL-System.ipynb @@ -60,8 +60,8 @@ " policies={'defender_1','defender_2'}, # These names are the same as the agents defined in the example config.\n", " policy_mapping_fn=lambda agent_id, episode, worker, **kw: agent_id,\n", " )\n", - " .environment(env=PrimaiteRayMARLEnv, env_config=cfg)#, disable_env_checking=True)\n", - " .rollouts(num_rollout_workers=0)\n", + " .environment(env=PrimaiteRayMARLEnv, env_config=cfg)\n", + " .env_runners(num_env_runners=0)\n", " .training(train_batch_size=128)\n", " )\n" ] diff --git a/src/primaite/notebooks/Training-an-RLLib-Agent.ipynb b/src/primaite/notebooks/Training-an-RLLib-Agent.ipynb index 9d458426..fbc5f4c6 100644 --- a/src/primaite/notebooks/Training-an-RLLib-Agent.ipynb +++ b/src/primaite/notebooks/Training-an-RLLib-Agent.ipynb @@ -19,7 +19,6 @@ "from primaite.config.load import data_manipulation_config_path\n", "\n", "from primaite.session.ray_envs import PrimaiteRayEnv\n", - "from ray.rllib.algorithms import ppo\n", "from ray import air, tune\n", "import ray\n", "from ray.rllib.algorithms.ppo import PPOConfig\n", @@ -52,8 +51,8 @@ "\n", "config = (\n", " PPOConfig()\n", - " .environment(env=PrimaiteRayEnv, env_config=env_config, disable_env_checking=True)\n", - " .rollouts(num_rollout_workers=0)\n", + " .environment(env=PrimaiteRayEnv, env_config=env_config)\n", + " .env_runners(num_env_runners=0)\n", " .training(train_batch_size=128)\n", ")\n" ] @@ -74,7 +73,7 @@ "tune.Tuner(\n", " \"PPO\",\n", " run_config=air.RunConfig(\n", - " stop={\"timesteps_total\": 5 * 128}\n", + " stop={\"timesteps_total\": 512}\n", " ),\n", " param_space=config\n", ").fit()\n" diff --git a/src/primaite/notebooks/Training-an-SB3-Agent.ipynb b/src/primaite/notebooks/Training-an-SB3-Agent.ipynb index 9faf5820..1e247e81 100644 --- a/src/primaite/notebooks/Training-an-SB3-Agent.ipynb +++ b/src/primaite/notebooks/Training-an-SB3-Agent.ipynb @@ -43,7 +43,10 @@ "outputs": [], "source": [ "with open(data_manipulation_config_path(), 'r') as f:\n", - " cfg = yaml.safe_load(f)" + " cfg = yaml.safe_load(f)\n", + "for agent in cfg['agents']:\n", + " if agent['ref'] == 'defender':\n", + " agent['agent_settings']['flatten_obs']=True" ] }, { @@ -177,7 +180,7 @@ "name": "python", "nbconvert_exporter": "python", "pygments_lexer": "ipython3", - "version": "3.8.10" + "version": "3.10.12" } }, "nbformat": 4, diff --git a/src/primaite/session/ray_envs.py b/src/primaite/session/ray_envs.py index 6dddde51..111baf84 100644 --- a/src/primaite/session/ray_envs.py +++ b/src/primaite/session/ray_envs.py @@ -45,7 +45,8 @@ class PrimaiteRayMARLEnv(MultiAgentEnv): self.action_space = gymnasium.spaces.Dict( {name: agent.action_manager.space for name, agent in self.agents.items()} ) - + self._obs_space_in_preferred_format = True + self._action_space_in_preferred_format = True super().__init__() @property From 4ee29d129dcb4fb1aaf0cdd6513730131e170653 Mon Sep 17 00:00:00 2001 From: Marek Wolan Date: Fri, 31 May 2024 15:41:40 +0100 Subject: [PATCH 08/15] Fix firewall diagram --- docs/_static/firewall_acl.png | Bin 36036 -> 23963 bytes 1 file changed, 0 insertions(+), 0 deletions(-) diff --git a/docs/_static/firewall_acl.png b/docs/_static/firewall_acl.png index 1cdd25263cf0817dde59ae02124ab17879197237..1e59657519d0a68acfee06bfd129fae99800acc2 100644 GIT binary patch literal 23963 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 literal 36036 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 Date: Fri, 31 May 2024 15:42:41 +0100 Subject: [PATCH 09/15] dropped the automatic creation of primaite-dependencies.rst and made it manual as we now only need immediate dependencies --- docs/Makefile | 1 - docs/make.bat | 5 ----- docs/source/primaite-dependencies.rst | 21 +++++++++++++++++++++ 3 files changed, 21 insertions(+), 6 deletions(-) create mode 100644 docs/source/primaite-dependencies.rst diff --git a/docs/Makefile b/docs/Makefile index 82719283..fcf64a6a 100644 --- a/docs/Makefile +++ b/docs/Makefile @@ -29,6 +29,5 @@ clean: # Catch-all target: route all unknown targets to Sphinx using the new # "make mode" option. $(O) is meant as a shortcut for $(SPHINXOPTS). %: Makefile | clean - pip-licenses --format=rst --with-urls --output-file=source/primaite-dependencies.rst @$(SPHINXBUILD) -M $@ "$(SOURCEDIR)" "$(BUILDDIR)" $(SPHINXOPTS) $(O) diff --git a/docs/make.bat b/docs/make.bat index 399e9150..a341af57 100644 --- a/docs/make.bat +++ b/docs/make.bat @@ -36,11 +36,6 @@ IF EXIST %AUTOSUMMARYDIR% ( RMDIR %AUTOSUMMARYDIR% /s /q ) -REM print the YT licenses -set LICENSEBUILD=pip-licenses --format=rst --with-urls -set DEPS="%cd%\source\primaite-dependencies.rst" - -%LICENSEBUILD% --output-file=%DEPS% %SPHINXBUILD% -M %1 %SOURCEDIR% %BUILDDIR% %SPHINXOPTS% %O% goto end diff --git a/docs/source/primaite-dependencies.rst b/docs/source/primaite-dependencies.rst new file mode 100644 index 00000000..61f2c400 --- /dev/null +++ b/docs/source/primaite-dependencies.rst @@ -0,0 +1,21 @@ ++-------------------+---------+------------------------------------+-------------------------------------------------------------------------------------------------------+----------------------------------------------+ +| Name | Version | License | Description | URL | ++===================+=========+====================================+=======================================================================================================+==============================================+ +| gymnasium | 0.28.1 | MIT License | A standard API for reinforcement learning and a diverse set of reference environments (formerly Gym). | https://farama.org | +| ipywidgets | 8.1.3 | BSD License | Jupyter interactive widgets | http://jupyter.org | +| jupyterlab | 3.6.1 | BSD License | JupyterLab computational environment | https://jupyter.org | +| kaleido | 0.2.1 | MIT | Static image export for web-based visualization libraries with zero dependencies | https://github.com/plotly/Kaleido | +| matplotlib | 3.7.1 | Python Software Foundation License | Python plotting package | https://matplotlib.org | +| networkx | 3.1 | BSD License | Python package for creating and manipulating graphs and networks | https://networkx.org/ | +| numpy | 1.23.5 | BSD License | NumPy is the fundamental package for array computing with Python. | https://www.numpy.org | +| platformdirs | 3.5.1 | MIT License | A small Python package for determining appropriate platform-specific dirs, e.g. a "user data dir". | https://github.com/platformdirs/platformdirs | +| plotly | 5.15.0 | MIT License | An open-source, interactive data visualization library for Python | https://plotly.com/python/ | +| polars | 0.18.4 | MIT License | Blazingly fast DataFrame library | https://www.pola.rs/ | +| prettytable | 3.8.0 | BSD License (BSD (3 clause)) | A simple Python library for easily displaying tabular data in a visually appealing ASCII table format | https://github.com/jazzband/prettytable | +| pydantic | 2.7.0 | MIT License | Data validation using Python type hints | https://github.com/pydantic/pydantic | +| PyYAML | 6.0 | MIT License | YAML parser and emitter for Python | https://pyyaml.org/ | +| ray | 2.23.0 | Apache 2.0 | Ray provides a simple, universal API for building distributed applications. | https://github.com/ray-project/ray | +| stable-baselines3 | 2.1.0 | MIT | Pytorch version of Stable Baselines, implementations of reinforcement learning algorithms. | https://github.com/DLR-RM/stable-baselines3 | +| tensorflow | 2.12.0 | Apache Software License | TensorFlow is an open source machine learning framework for everyone. | https://www.tensorflow.org/ | +| typer | 0.9.0 | MIT License | Typer, build great CLIs. Easy to code. Based on Python type hints. | https://github.com/tiangolo/typer | ++-------------------+---------+------------------------------------+-------------------------------------------------------------------------------------------------------+----------------------------------------------+ From 97d4c382983618b26dfd204560b6bc439f93956b Mon Sep 17 00:00:00 2001 From: Marek Wolan Date: Fri, 31 May 2024 15:56:27 +0100 Subject: [PATCH 10/15] remove tests from api docs --- docs/api.rst | 1 - docs/index.rst | 1 - 2 files changed, 2 deletions(-) diff --git a/docs/api.rst b/docs/api.rst index 13f3a1ec..e74be627 100644 --- a/docs/api.rst +++ b/docs/api.rst @@ -19,4 +19,3 @@ :recursive: primaite - tests diff --git a/docs/index.rst b/docs/index.rst index a0f302e9..8e7defb1 100644 --- a/docs/index.rst +++ b/docs/index.rst @@ -125,7 +125,6 @@ Head over to the :ref:`getting-started` page to install and setup PrimAITE! source/state_system source/request_system PrimAITE API - PrimAITE Tests .. toctree:: From f9cff4285676063c1f6457056fcb7f783a734f85 Mon Sep 17 00:00:00 2001 From: Marek Wolan Date: Fri, 31 May 2024 16:03:47 +0100 Subject: [PATCH 11/15] glossary fact check --- docs/source/glossary.rst | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/source/glossary.rst b/docs/source/glossary.rst index 67fd7aaa..c322caac 100644 --- a/docs/source/glossary.rst +++ b/docs/source/glossary.rst @@ -78,4 +78,4 @@ Glossary PrimAITE uses the Gymnasium reinforcement learning framework API to create a training environment and interface with RL agents. Gymnasium defines a common way of creating observations, actions, and rewards. User app home - PrimAITE supports upgrading software version while retaining user data. The user data directory is where configs, notebooks, and results are stored, this location is `~/primaite` on linux/darwin and `C:\\Users\\\\primaite\\` on Windows. + PrimAITE supports upgrading software version while retaining user data. The user data directory is where configs, notebooks, and results are stored, this location is `~/primaite/` on linux/darwin and `C:\\Users\\\\primaite` on Windows. From 0ac57147cd08a170e39faa756de89f0448c46a3b Mon Sep 17 00:00:00 2001 From: "Archer.Bowen" Date: Fri, 31 May 2024 17:05:30 +0100 Subject: [PATCH 12/15] Updated index following new primAITE 2 Pager --- docs/_static/primAITE_architecture.png | Bin 0 -> 108754 bytes docs/index.rst | 74 +++++++++++++++++-------- 2 files changed, 52 insertions(+), 22 deletions(-) create mode 100644 docs/_static/primAITE_architecture.png diff --git a/docs/_static/primAITE_architecture.png b/docs/_static/primAITE_architecture.png new file mode 100644 index 0000000000000000000000000000000000000000..45425e4254d098ec51c5944d954e30d9b9b1182b GIT binary patch literal 108754 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 iWE2u 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 T;|~dN4Myggg2bn*x{v-FWlJ(^ literal 0 HcmV?d00001 diff --git a/docs/index.rst b/docs/index.rst index af6eff86..ad0167d9 100644 --- a/docs/index.rst +++ b/docs/index.rst @@ -11,49 +11,79 @@ What is PrimAITE? Overview ^^^^^^^^ -The ARCD Primary-level AI Training Environment (**PrimAITE**) provides an effective simulation capability for the purposes of training and evaluating AI in a cyber-defensive role. It incorporates the functionality required of a primary-level ARCD environment, which includes: +The ARCD Primary-level AI Training Environment (**PrimAITE**) provides an effective simulation capability for training and evaluating AI in a cyber-defensive role. It incorporates the functionality required of a primary-level ARCD environment: -- The ability to model a relevant platform / system context; +- The ability to model a relevant system context; - Modelling an adversarial agent that the defensive agent can be trained and evaluated against; -- The ability to model key characteristics of a platform / system by representing connections, IP addresses, ports, operating systems, services and traffic loading on links; -- Modelling background pattern-of-life; -- Operates at machine-speed to enable fast training cycles. +- The ability to model key characteristics of a system by representing hosts, servers, network devices, IP addresses, ports, operating systems, folders / files, applications, services and links; +- Modelling background (green) pattern-of-life; +- Operates at machine-speed to enable fast training cycles via Reinforcement Learning (RL). Features ^^^^^^^^ PrimAITE incorporates the following features: -- Highly configurable (via YAML files) to provide the means to model a variety of platform / system laydowns and adversarial attack scenarios; -- A Reinforcement Learning (RL) reward function based on (a) the ability to counter the modelled adversarial cyber-attack, and (b) the ability to ensure success; -- Provision of logging to support AI performance / effectiveness assessment; -- Uses the concept of Information Exchange Requirements (IERs) to model background pattern of life and adversarial behaviour; -- An Access Control List (ACL) function, mimicking the behaviour of a network firewall, is applied across the model, following standard ACL rule format (e.g. DENY/ALLOW, source IP address, destination IP address, protocol and port); -- Application of traffic to the links of the platform / system laydown adheres to the ACL ruleset; -- Presents both a Gymnasium and Ray RLLib interface to the environment, allowing integration with any compliant defensive agents; -- Allows for the saving and loading of trained defensive agents; -- Stochastic adversarial agent behaviour; -- Full capture of discrete logs relating to agent training or evaluation (system state, agent actions taken, instantaneous and average reward for every step of every episode); -- Distinct control over running a training and / or evaluation session; -- NetworkX provides laydown visualisation capability. +- Architected with a separate Simulation layer and Game layer. This separation of concerns defines a clear path towards transfer learning with environments of differing fidelity; +- Ability to reconfigure an RL reward function based on (a) the ability to counter the modelled adversarial cyber-attack, and (b) the ability to ensure success for green agents; +- Access Control List (ACL) functions for network devices (routers and firewalls), following standard ACL rule format (e.g., DENY / ALLOW, source / destination IP addresses, protocol and port); +- Application of traffic to the links of the system laydown adheres to the ACL rulesets and routing tables contained within each network device; +- Provides RL environments adherent to the Farama Foundation Gymnasium (Previously OpenAI Gym) API, allowing integration with any compliant RL Agent frameworks; +- Provides RL environments adherent to Ray RLlib environment specifications for single-agent and multi-agent scenarios; +- Assessed for compatibility with Stable-Baselines3 (SB3), Ray RLlib, and bespoke agents; +- Persona-based adversarial (Red) agent behaviour; several out-the-box personas are provided, and more can be developed to suit the needs of the task. Stochastic variations in Red agent behaviour are also included as required; +- A robust system logging tool, automatically enabled at the node level and featuring various log levels and terminal output options, enables PrimAITE users to conduct in-depth network simulations; +- A PCAP service is seamlessly integrated within the simulation, automatically capturing and logging frames for both + inbound and outbound traffic at the network interface level. This automatic functionality, combined with the ability + to separate traffic directions, significantly enhances network analysis and troubleshooting capabilities; +- Agent action logs provide a description of every action taken by each agent during the episode. This includes timestep, action, parameters, request and response, for all Blue agent activity, which is aligned with the Track 2 Common Action / Observation Space (CAOS) format. Action logs also details of all scripted / stochastic red / green agent actions; +- Environment ground truth is provided at every timestep, providing a full description of the environment’s true state; +- Alignment with CAOS provides the ability to transfer agents between CAOS compliant environments. Architecture ^^^^^^^^^^^^ -PrimAITE is a Python application and is therefore Operating System agnostic. The Gymnasium and Ray RLLib frameworks are employed to provide an interface and source for AI agents. Configuration of PrimAITE is achieved via included YAML files which support full control over the platform / system laydown being modelled, background pattern of life, adversarial (red agent) behaviour, and step and episode count. NetworkX based nodes and links host Python classes to present attributes and methods, and hence a more representative platform / system can be modelled within the simulation. +PrimAITE is a Python application and will operate on multiple Operating Systems (Windows, Linux and Mac); +a comprehensive installation and user guide is provided with each release to support its usage. +Configuration of PrimAITE is achieved via included YAML files which support full control over the network / system laydown being modelled, background pattern of life, adversarial (red agent) behaviour, and step and episode count. +A Simulation Controller layer manages the overall running of the simulation, keeping track of all low-level objects. +It is agnostic to the number of agents, their action / observation spaces, and the RL library being used. +It presents a public API providing a method for describing the current state of the simulation, a method that accepts action requests and provides responses, and a method that triggers a timestep advancement. +The Game Layer converts the simulation into a playable game for the agent(s). + +it translates between simulation state and Gymnasium.Spaces to pass action / observation data between the agent(s) and the simulation. It is responsible for calculating rewards, managing Multi-Agent RL (MARL) action turns, and via a single agent interface can interact with Blue, Red and Green agents. + +Agents can either generate their own scripted behaviour or accept input behaviour from an RL agent. + +Finally, a Gymnasium / Ray RLlib Environment Layer forwards requests to the Game Layer as the agent sends them. This layer also manages most of the I/O, such as reading in the configuration files and saving agent logs. + +.. image:: ../../_static/primAITE_architecture.png + :width: 500 + :align: center + + Training & Evaluation Capability ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ -PrimAITE provides a training and evaluation capability to AI agents in the context of cyber-attack, via its Gymnasium and RLLib compliant interface. Scenarios can be constructed to reflect platform / system laydowns consisting of any configuration of nodes (e.g. PCs, servers, switches etc.) and network links between them. All nodes can be configured to model services (and their status) and the traffic loading between them over the network links. Traffic loading is broken down into a per service granularity, relating directly to a protocol (e.g. Service A would be configured as a TCP service, and TCP traffic then flows between instances of Service A under the direction of a tailored IER). Highlights of PrimAITE’s training and evaluation capability are: +PrimAITE provides a training and evaluation capability to AI agents in the context of cyber-attack, via its Gymnasium / Ray RLlib compliant interface. + +Scenarios can be constructed to reflect network / system laydowns consisting of any configuration of nodes (e.g., PCs, servers etc.) and the networking equipment and links between them. + +All nodes can be configured to contain applications, services, folders and files (and their status). + +Traffic flows between services and applications as directed by an ‘execution definition,’ with the traffic flow on the network governed by the network equipment (switches, routers and firewalls) and the ACL rules and routing tables they employ. + +Highlights of PrimAITE’s training and evaluation capability are: - The scenario is not bound to a representation of any platform, system, or technology; -- Fully configurable (network / system laydown, IERs, node pattern-of-life, ACL, number of episodes, steps per episode) and repeatable to suit the requirements of AI agents; -- Can integrate with any Gymnasium or RLLib compliant AI agent. +- Fully configurable (network / system laydown, green pattern-of-life, red personas, reward function, ACL rules for each device, number of episodes / steps, action / observation space) and repeatable to suit the requirements of AI agents; +- Can integrate with any Gymnasium / Ray RLlib compliant AI agent . -Use of PrimAITE default scenarios within ARCD is supported by a “Use Case Profile” tailored to the scenario. + +PrimAITE provides a number of use cases (network and red/green action configurations) by default which the user is able to extend and modify as required. What is PrimAITE built with --------------------------- From 4a9b92eab133aa8f59a1f4c1f3930279e8094641 Mon Sep 17 00:00:00 2001 From: "Archer.Bowen" Date: Fri, 31 May 2024 17:17:07 +0100 Subject: [PATCH 13/15] Removing data-manp notebook outputs --- .../notebooks/Data-Manipulation-E2E-Demonstration.ipynb | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/src/primaite/notebooks/Data-Manipulation-E2E-Demonstration.ipynb b/src/primaite/notebooks/Data-Manipulation-E2E-Demonstration.ipynb index a3dab962..e0f79795 100644 --- a/src/primaite/notebooks/Data-Manipulation-E2E-Demonstration.ipynb +++ b/src/primaite/notebooks/Data-Manipulation-E2E-Demonstration.ipynb @@ -401,7 +401,8 @@ "cell_type": "markdown", "metadata": {}, "source": [ - "Instantiate the environment. We also disable the agent observation flattening.\n", + "Instantiate the environment. \n", + "We will also disable the agent observation flattening.\n", "\n", "This cell will print the observation when the network is healthy. You should be able to verify Node file and service statuses against the description above." ] From 9f0c6ddbb482911060805563c2bf66659036eb24 Mon Sep 17 00:00:00 2001 From: "Archer.Bowen" Date: Fri, 31 May 2024 18:18:40 +0100 Subject: [PATCH 14/15] Fix primaite dependencies formatting issue --- docs/source/primaite-dependencies.rst | 16 ++++++++++++++++ 1 file changed, 16 insertions(+) diff --git a/docs/source/primaite-dependencies.rst b/docs/source/primaite-dependencies.rst index 61f2c400..c70a299d 100644 --- a/docs/source/primaite-dependencies.rst +++ b/docs/source/primaite-dependencies.rst @@ -2,20 +2,36 @@ | Name | Version | License | Description | URL | +===================+=========+====================================+=======================================================================================================+==============================================+ | gymnasium | 0.28.1 | MIT License | A standard API for reinforcement learning and a diverse set of reference environments (formerly Gym). | https://farama.org | ++-------------------+---------+------------------------------------+-------------------------------------------------------------------------------------------------------+----------------------------------------------+ | ipywidgets | 8.1.3 | BSD License | Jupyter interactive widgets | http://jupyter.org | ++-------------------+---------+------------------------------------+-------------------------------------------------------------------------------------------------------+----------------------------------------------+ | jupyterlab | 3.6.1 | BSD License | JupyterLab computational environment | https://jupyter.org | ++-------------------+---------+------------------------------------+-------------------------------------------------------------------------------------------------------+----------------------------------------------+ | kaleido | 0.2.1 | MIT | Static image export for web-based visualization libraries with zero dependencies | https://github.com/plotly/Kaleido | ++-------------------+---------+------------------------------------+-------------------------------------------------------------------------------------------------------+----------------------------------------------+ | matplotlib | 3.7.1 | Python Software Foundation License | Python plotting package | https://matplotlib.org | ++-------------------+---------+------------------------------------+-------------------------------------------------------------------------------------------------------+----------------------------------------------+ | networkx | 3.1 | BSD License | Python package for creating and manipulating graphs and networks | https://networkx.org/ | ++-------------------+---------+------------------------------------+-------------------------------------------------------------------------------------------------------+----------------------------------------------+ | numpy | 1.23.5 | BSD License | NumPy is the fundamental package for array computing with Python. | https://www.numpy.org | ++-------------------+---------+------------------------------------+-------------------------------------------------------------------------------------------------------+----------------------------------------------+ | platformdirs | 3.5.1 | MIT License | A small Python package for determining appropriate platform-specific dirs, e.g. a "user data dir". | https://github.com/platformdirs/platformdirs | ++-------------------+---------+------------------------------------+-------------------------------------------------------------------------------------------------------+----------------------------------------------+ | plotly | 5.15.0 | MIT License | An open-source, interactive data visualization library for Python | https://plotly.com/python/ | ++-------------------+---------+------------------------------------+-------------------------------------------------------------------------------------------------------+----------------------------------------------+ | polars | 0.18.4 | MIT License | Blazingly fast DataFrame library | https://www.pola.rs/ | ++-------------------+---------+------------------------------------+-------------------------------------------------------------------------------------------------------+----------------------------------------------+ | prettytable | 3.8.0 | BSD License (BSD (3 clause)) | A simple Python library for easily displaying tabular data in a visually appealing ASCII table format | https://github.com/jazzband/prettytable | ++-------------------+---------+------------------------------------+-------------------------------------------------------------------------------------------------------+----------------------------------------------+ | pydantic | 2.7.0 | MIT License | Data validation using Python type hints | https://github.com/pydantic/pydantic | ++-------------------+---------+------------------------------------+-------------------------------------------------------------------------------------------------------+----------------------------------------------+ | PyYAML | 6.0 | MIT License | YAML parser and emitter for Python | https://pyyaml.org/ | ++-------------------+---------+------------------------------------+-------------------------------------------------------------------------------------------------------+----------------------------------------------+ | ray | 2.23.0 | Apache 2.0 | Ray provides a simple, universal API for building distributed applications. | https://github.com/ray-project/ray | ++-------------------+---------+------------------------------------+-------------------------------------------------------------------------------------------------------+----------------------------------------------+ | stable-baselines3 | 2.1.0 | MIT | Pytorch version of Stable Baselines, implementations of reinforcement learning algorithms. | https://github.com/DLR-RM/stable-baselines3 | ++-------------------+---------+------------------------------------+-------------------------------------------------------------------------------------------------------+----------------------------------------------+ | tensorflow | 2.12.0 | Apache Software License | TensorFlow is an open source machine learning framework for everyone. | https://www.tensorflow.org/ | ++-------------------+---------+------------------------------------+-------------------------------------------------------------------------------------------------------+----------------------------------------------+ | typer | 0.9.0 | MIT License | Typer, build great CLIs. Easy to code. Based on Python type hints. | https://github.com/tiangolo/typer | +-------------------+---------+------------------------------------+-------------------------------------------------------------------------------------------------------+----------------------------------------------+ From 2406579c782ef12c40536e107542e2ceef324d95 Mon Sep 17 00:00:00 2001 From: Marek Wolan Date: Fri, 31 May 2024 18:50:56 +0100 Subject: [PATCH 15/15] format using precommit --- docs/index.rst | 36 ++++++++++++++-------------- docs/source/getting_started.rst | 2 +- docs/source/varying_config_files.rst | 4 ++-- 3 files changed, 21 insertions(+), 21 deletions(-) diff --git a/docs/index.rst b/docs/index.rst index ad0167d9..729cdc17 100644 --- a/docs/index.rst +++ b/docs/index.rst @@ -24,11 +24,11 @@ Features PrimAITE incorporates the following features: -- Architected with a separate Simulation layer and Game layer. This separation of concerns defines a clear path towards transfer learning with environments of differing fidelity; +- Architected with a separate Simulation layer and Game layer. This separation of concerns defines a clear path towards transfer learning with environments of differing fidelity; - Ability to reconfigure an RL reward function based on (a) the ability to counter the modelled adversarial cyber-attack, and (b) the ability to ensure success for green agents; -- Access Control List (ACL) functions for network devices (routers and firewalls), following standard ACL rule format (e.g., DENY / ALLOW, source / destination IP addresses, protocol and port); +- Access Control List (ACL) functions for network devices (routers and firewalls), following standard ACL rule format (e.g., DENY / ALLOW, source / destination IP addresses, protocol and port); - Application of traffic to the links of the system laydown adheres to the ACL rulesets and routing tables contained within each network device; -- Provides RL environments adherent to the Farama Foundation Gymnasium (Previously OpenAI Gym) API, allowing integration with any compliant RL Agent frameworks; +- Provides RL environments adherent to the Farama Foundation Gymnasium (Previously OpenAI Gym) API, allowing integration with any compliant RL Agent frameworks; - Provides RL environments adherent to Ray RLlib environment specifications for single-agent and multi-agent scenarios; - Assessed for compatibility with Stable-Baselines3 (SB3), Ray RLlib, and bespoke agents; - Persona-based adversarial (Red) agent behaviour; several out-the-box personas are provided, and more can be developed to suit the needs of the task. Stochastic variations in Red agent behaviour are also included as required; @@ -38,23 +38,23 @@ PrimAITE incorporates the following features: to separate traffic directions, significantly enhances network analysis and troubleshooting capabilities; - Agent action logs provide a description of every action taken by each agent during the episode. This includes timestep, action, parameters, request and response, for all Blue agent activity, which is aligned with the Track 2 Common Action / Observation Space (CAOS) format. Action logs also details of all scripted / stochastic red / green agent actions; - Environment ground truth is provided at every timestep, providing a full description of the environment’s true state; -- Alignment with CAOS provides the ability to transfer agents between CAOS compliant environments. +- Alignment with CAOS provides the ability to transfer agents between CAOS compliant environments. Architecture ^^^^^^^^^^^^ -PrimAITE is a Python application and will operate on multiple Operating Systems (Windows, Linux and Mac); -a comprehensive installation and user guide is provided with each release to support its usage. +PrimAITE is a Python application and will operate on multiple Operating Systems (Windows, Linux and Mac); +a comprehensive installation and user guide is provided with each release to support its usage. -Configuration of PrimAITE is achieved via included YAML files which support full control over the network / system laydown being modelled, background pattern of life, adversarial (red agent) behaviour, and step and episode count. -A Simulation Controller layer manages the overall running of the simulation, keeping track of all low-level objects. +Configuration of PrimAITE is achieved via included YAML files which support full control over the network / system laydown being modelled, background pattern of life, adversarial (red agent) behaviour, and step and episode count. +A Simulation Controller layer manages the overall running of the simulation, keeping track of all low-level objects. -It is agnostic to the number of agents, their action / observation spaces, and the RL library being used. +It is agnostic to the number of agents, their action / observation spaces, and the RL library being used. It presents a public API providing a method for describing the current state of the simulation, a method that accepts action requests and provides responses, and a method that triggers a timestep advancement. -The Game Layer converts the simulation into a playable game for the agent(s). +The Game Layer converts the simulation into a playable game for the agent(s). -it translates between simulation state and Gymnasium.Spaces to pass action / observation data between the agent(s) and the simulation. It is responsible for calculating rewards, managing Multi-Agent RL (MARL) action turns, and via a single agent interface can interact with Blue, Red and Green agents. +it translates between simulation state and Gymnasium.Spaces to pass action / observation data between the agent(s) and the simulation. It is responsible for calculating rewards, managing Multi-Agent RL (MARL) action turns, and via a single agent interface can interact with Blue, Red and Green agents. Agents can either generate their own scripted behaviour or accept input behaviour from an RL agent. @@ -64,23 +64,23 @@ Finally, a Gymnasium / Ray RLlib Environment Layer forwards requests to the Game :width: 500 :align: center - + Training & Evaluation Capability ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ -PrimAITE provides a training and evaluation capability to AI agents in the context of cyber-attack, via its Gymnasium / Ray RLlib compliant interface. +PrimAITE provides a training and evaluation capability to AI agents in the context of cyber-attack, via its Gymnasium / Ray RLlib compliant interface. -Scenarios can be constructed to reflect network / system laydowns consisting of any configuration of nodes (e.g., PCs, servers etc.) and the networking equipment and links between them. +Scenarios can be constructed to reflect network / system laydowns consisting of any configuration of nodes (e.g., PCs, servers etc.) and the networking equipment and links between them. -All nodes can be configured to contain applications, services, folders and files (and their status). +All nodes can be configured to contain applications, services, folders and files (and their status). -Traffic flows between services and applications as directed by an ‘execution definition,’ with the traffic flow on the network governed by the network equipment (switches, routers and firewalls) and the ACL rules and routing tables they employ. +Traffic flows between services and applications as directed by an ‘execution definition,’ with the traffic flow on the network governed by the network equipment (switches, routers and firewalls) and the ACL rules and routing tables they employ. Highlights of PrimAITE’s training and evaluation capability are: - The scenario is not bound to a representation of any platform, system, or technology; - Fully configurable (network / system laydown, green pattern-of-life, red personas, reward function, ACL rules for each device, number of episodes / steps, action / observation space) and repeatable to suit the requirements of AI agents; -- Can integrate with any Gymnasium / Ray RLlib compliant AI agent . +- Can integrate with any Gymnasium / Ray RLlib compliant AI agent . PrimAITE provides a number of use cases (network and red/green action configurations) by default which the user is able to extend and modify as required. @@ -139,4 +139,4 @@ Head over to the :ref:`getting-started` page to install and setup PrimAITE! source/state_system source/request_system PrimAITE API - PrimAITE Tests \ No newline at end of file + PrimAITE Tests diff --git a/docs/source/getting_started.rst b/docs/source/getting_started.rst index 9283f3f4..150c3a1d 100644 --- a/docs/source/getting_started.rst +++ b/docs/source/getting_started.rst @@ -107,7 +107,7 @@ Clone & Install PrimAITE for Development To be able to extend PrimAITE further, or to build wheels manually before install, clone the repository to a location of your choice: -1. Clone the repository. +1. Clone the repository. For example: diff --git a/docs/source/varying_config_files.rst b/docs/source/varying_config_files.rst index 34b83895..74e18216 100644 --- a/docs/source/varying_config_files.rst +++ b/docs/source/varying_config_files.rst @@ -44,6 +44,6 @@ It takes the following format: - laydown_2.yaml - attack_2.yaml -For more information please refer to the ``Using Episode Schedules`` notebook in either :ref:`Executed Notebooks` or run the notebook interactively in ``notebooks/example_notebooks/``. +For more information please refer to the ``Using Episode Schedules`` notebook in either :ref:`Executed Notebooks` or run the notebook interactively in ``notebooks/example_notebooks/``. -For further information around notebooks in general refer to the :ref:`Example Jupyter Notebooks`. \ No newline at end of file +For further information around notebooks in general refer to the :ref:`Example Jupyter Notebooks`.