diff --git a/docs/index.rst b/docs/index.rst index 3b1a13ec..9745232d 100644 --- a/docs/index.rst +++ b/docs/index.rst @@ -44,6 +44,7 @@ Head over to the :ref:`getting-started` page to install and setup PrimAITE! source/config source/primaite_session source/custom_agent + source/simulation PrimAITE API PrimAITE Tests source/dependencies diff --git a/docs/source/custom_agent.rst b/docs/source/custom_agent.rst index 8a95d3ae..040b4b3d 100644 --- a/docs/source/custom_agent.rst +++ b/docs/source/custom_agent.rst @@ -13,7 +13,7 @@ Integrating a user defined blue agent If you are planning to implement custom RL agents into PrimAITE, you must use the project as a repository. If you install PrimAITE as a python package from wheel, custom agents are not supported. -PrimAITE has integration with Ray RLLib and StableBaselines3 agents. All agents interface with PrimAITE through an :py:class:`primaite.agents.agent.AgentSessionABC` which provides Input/Output of agent savefiles, as well as capturing and plotting performance metrics during training and evaluation. If you wish to integrate a custom blue agent, it is recommended to create a subclass of the :py:class:`primaite.agents.agent.AgentSessionABC` and implement the ``__init__()``, ``_setup()``, ``_save_checkpoint()``, ``learn()``, ``evaluate()``, ``_get_latest_checkpoint``, ``load()``, and ``save()`` methods. +PrimAITE has integration with Ray RLLib and StableBaselines3 agents. All agents interface with PrimAITE through an :py:class:`primaite.agents.agent_abc.AgentSessionABC` which provides Input/Output of agent savefiles, as well as capturing and plotting performance metrics during training and evaluation. If you wish to integrate a custom blue agent, it is recommended to create a subclass of the :py:class:`primaite.agents.agent_abc.AgentSessionABC` and implement the ``__init__()``, ``_setup()``, ``_save_checkpoint()``, ``learn()``, ``evaluate()``, ``_get_latest_checkpoint``, ``load()``, and ``save()`` methods. Below is a barebones example of a custom agent implementation: @@ -21,7 +21,7 @@ Below is a barebones example of a custom agent implementation: # src/primaite/agents/my_custom_agent.py - from primaite.agents.agent import AgentSessionABC + from primaite.agents.agent_abc import AgentSessionABC from primaite.common.enums import AgentFramework, AgentIdentifier class CustomAgent(AgentSessionABC): diff --git a/docs/source/simulation.rst b/docs/source/simulation.rst new file mode 100644 index 00000000..1620f6ba --- /dev/null +++ b/docs/source/simulation.rst @@ -0,0 +1,10 @@ +.. only:: comment + + © Crown-owned copyright 2023, Defence Science and Technology Laboratory UK + +Simulation Strucutre +==================== + +The simulation is made up of many smaller components which are related to each other in a tree-like structure. At the top level, there is an object called the ``SimulationController`` _(doesn't exist yet)_, which has a physical network and a software controller for managing software and users. + +Each node of the simulation 'tree' has responsibility for creating, deleting, and updating its direct descendants. diff --git a/pyproject.toml b/pyproject.toml index b66b0168..0bae6e86 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -37,7 +37,8 @@ dependencies = [ "ray[rllib]==2.2.0", "stable-baselines3==1.6.2", "tensorflow==2.12.0", - "typer[all]==0.9.0" + "typer[all]==0.9.0", + "pydantic" ] [tool.setuptools.dynamic] diff --git a/src/primaite/simulator/__init__.py b/src/primaite/simulator/__init__.py new file mode 100644 index 00000000..e69de29b diff --git a/src/primaite/simulator/core.py b/src/primaite/simulator/core.py new file mode 100644 index 00000000..d540daf3 --- /dev/null +++ b/src/primaite/simulator/core.py @@ -0,0 +1,38 @@ +"""Core of the PrimAITE Simulator.""" +from abc import abstractmethod +from typing import Dict, List + +from pydantic import BaseModel + + +class SimComponent(BaseModel): + """Extension of pydantic BaseModel with additional methods that must be defined by all classes in the simulator.""" + + @abstractmethod + def describe_state(self) -> Dict: + """ + Return a dictionary describing the state of this object and any objects managed by it. + + This is similar to pydantic ``model_dump()``, but it only outputs information about the objects owned by this + object. If there are objects referenced by this object that are owned by something else, it is not included in + this output. + """ + return {} + + @abstractmethod + def apply_action(self, action: List[str]) -> None: + """ + Apply an action to a simulation component. Action data is passed in as a 'namespaced' list of strings. + + If the list only has one element, the action is intended to be applied directly to this object. If the list has + multiple entries, the action is passed to the child of this object specified by the first one or two entries. + This is essentially a namespace. + + For example, ["turn_on",] is meant to apply an action of 'turn on' to this component. + + However, ["services", "email_client", "turn_on"] is meant to 'turn on' this component's email client service. + + :param action: List describing the action to apply to this object. + :type action: List[str] + """ + return diff --git a/tests/unit_tests/__init__.py b/tests/unit_tests/__init__.py new file mode 100644 index 00000000..e69de29b diff --git a/tests/unit_tests/primaite/__init__.py b/tests/unit_tests/primaite/__init__.py new file mode 100644 index 00000000..e69de29b diff --git a/tests/unit_tests/primaite/simulator/__init__.py b/tests/unit_tests/primaite/simulator/__init__.py new file mode 100644 index 00000000..e69de29b diff --git a/tests/unit_tests/primaite/simulator/test_core.py b/tests/unit_tests/primaite/simulator/test_core.py new file mode 100644 index 00000000..ea593a0b --- /dev/null +++ b/tests/unit_tests/primaite/simulator/test_core.py @@ -0,0 +1,87 @@ +from typing import Dict, List, Literal, Tuple + +import pytest +from pydantic import ValidationError + +from primaite.simulator.core import SimComponent + + +class TestIsolatedSimComponent: + """Test the SimComponent class in isolation.""" + + def test_data_validation(self): + """ + Test that our derived class does not interfere with pydantic data validation. + + This test may seem like it's simply validating pydantic data validation, but + actually it is here to give us assurance that any custom functionality we add + to the SimComponent does not interfere with pydantic. + """ + + class TestComponent(SimComponent): + name: str + size: Tuple[float, float] + + def describe_state(self) -> Dict: + return {} + + def apply_action(self, action: List[str]) -> None: + pass + + comp = TestComponent(name="computer", size=(5, 10)) + assert isinstance(comp, TestComponent) + + with pytest.raises(ValidationError): + invalid_comp = TestComponent(name="computer", size="small") # noqa + + def test_serialisation(self): + """Validate that our added functionality does not interfere with pydantic.""" + + class TestComponent(SimComponent): + name: str + size: Tuple[float, float] + + def describe_state(self) -> Dict: + return {} + + def apply_action(self, action: List[str]) -> None: + pass + + comp = TestComponent(name="computer", size=(5, 10)) + dump = comp.model_dump() + assert dump == {"name": "computer", "size": (5, 10)} + + def test_apply_action(self): + """Validate that we can override apply_action behaviour and it updates the state of the component.""" + + class TestComponent(SimComponent): + name: str + status: Literal["on", "off"] = "off" + + def describe_state(self) -> Dict: + return {} + + def apply_action(self, action: List[str]) -> None: + possible_actions = { + "turn_off": self._turn_off, + "turn_on": self._turn_on, + } + if action[0] in possible_actions: + possible_actions[action[0]](action[1:]) + else: + raise ValueError(f"{self} received invalid action {action}") + + def _turn_off(self): + self.status = "off" + + def _turn_on(self): + self.status = "on" + + comp = TestComponent(name="computer", status="off") + + assert comp.status == "off" + comp.apply_action(["turn_on"]) + assert comp.status == "on" + + with pytest.raises(ValueError): + comp.apply_action(["do_nothing"])