Add SimComponent core class
This commit is contained in:
@@ -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
|
||||||
|
|||||||
@@ -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):
|
||||||
|
|||||||
10
docs/source/simulation.rst
Normal file
10
docs/source/simulation.rst
Normal 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.
|
||||||
@@ -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]
|
||||||
|
|||||||
0
src/primaite/simulator/__init__.py
Normal file
0
src/primaite/simulator/__init__.py
Normal file
38
src/primaite/simulator/core.py
Normal file
38
src/primaite/simulator/core.py
Normal 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
|
||||||
0
tests/unit_tests/__init__.py
Normal file
0
tests/unit_tests/__init__.py
Normal file
0
tests/unit_tests/primaite/__init__.py
Normal file
0
tests/unit_tests/primaite/__init__.py
Normal file
0
tests/unit_tests/primaite/simulator/__init__.py
Normal file
0
tests/unit_tests/primaite/simulator/__init__.py
Normal file
87
tests/unit_tests/primaite/simulator/test_core.py
Normal file
87
tests/unit_tests/primaite/simulator/test_core.py
Normal 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"])
|
||||||
Reference in New Issue
Block a user