Add SimComponent core class

This commit is contained in:
Marek Wolan
2023-07-28 14:49:21 +01:00
parent 2642cc97de
commit b129c4fc97
10 changed files with 140 additions and 3 deletions

View File

@@ -44,6 +44,7 @@ Head over to the :ref:`getting-started` page to install and setup PrimAITE!
source/config source/config
source/primaite_session source/primaite_session
source/custom_agent source/custom_agent
source/simulation
PrimAITE API <source/_autosummary/primaite> PrimAITE API <source/_autosummary/primaite>
PrimAITE Tests <source/_autosummary/tests> PrimAITE Tests <source/_autosummary/tests>
source/dependencies source/dependencies

View File

@@ -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. 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<Agent Session>` 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<Agent Session>` 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: 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 # 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 from primaite.common.enums import AgentFramework, AgentIdentifier
class CustomAgent(AgentSessionABC): class CustomAgent(AgentSessionABC):

View File

@@ -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.

View File

@@ -37,7 +37,8 @@ dependencies = [
"ray[rllib]==2.2.0", "ray[rllib]==2.2.0",
"stable-baselines3==1.6.2", "stable-baselines3==1.6.2",
"tensorflow==2.12.0", "tensorflow==2.12.0",
"typer[all]==0.9.0" "typer[all]==0.9.0",
"pydantic"
] ]
[tool.setuptools.dynamic] [tool.setuptools.dynamic]

View File

View File

@@ -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

View File

View File

View File

@@ -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"])