"""This module contains the abstract class :class:`Brain` that is used
to implement the thinking part of agents.
"""
from __future__ import annotations
from typing import TYPE_CHECKING, Any, List
import uuid
import logging
import warnings
from pathlib import Path
from abc import ABC, abstractmethod
from .brain_dumper import BrainDumper
from .memory import Memory
from palaestrai.core import RuntimeConfig
LOG = logging.getLogger(__name__)
if TYPE_CHECKING:
from . import SensorInformation, ActuatorInformation, Objective
[docs]
class Brain(ABC):
"""Baseclass for all brain implementation
The brain is the central learning instance. It coordinates all
muscles (if multiple muscles are available). The brain does all
(deep) learning tasks and delivers a model to the muscles.
The brain has one abstract method :meth:`.thinking` that has to be
implemented.
Brain objects store their state and can re-load previous states by using
the infrastructure provided by the :class:`~BrainDumper` infrastructure.
For this, concrete Brain classes need to provide implementations of
:meth:`~Brain.load` and :meth:`~Brain.store`.
"""
def __init__(self):
self._seed: int = -1
self._memory = Memory()
self._sensors: List[SensorInformation] = []
self._actuators: List[ActuatorInformation] = []
# Some IO object to which we can dump ourselves for a freeze:
self._dumpers: List[BrainDumper] = list()
@property
def seed(self) -> int:
"""Returns the random seed applicable for this brain instance."""
return self._seed
@property
def sensors(self) -> List[SensorInformation]:
"""All sensors the Brain (and its Muscles) know about"""
return self._sensors
@property
def actuators(self) -> List[ActuatorInformation]:
"""All actuators a Muscle can act with."""
return self._actuators
@property
def memory(self) -> Memory:
"""The Brain's memory"""
return self._memory
[docs]
def setup(self):
"""Brain setup method
This method is called by the :class:`~AgentConductor` just before
the main loop is intered (:meth:`~Brain.run`). In the base Brain class,
it is empty and does nothing. However, any derived class may implement
it to do local setup before the main loop is entered.
Potential tasks that could be done in this method is to set the
size limit of the :class:`Memory` via ::`Memory.size_limit`,
or anything that needs to access the ::`Brain.seed`, ::`Brain.sensors`,
or ::`Brain.actuators`, as they're not yet available in the constructor.
This method is guaranteed to be called in the same process space as
the main loop method, :meth:`Brain.run`.
"""
pass
[docs]
@abstractmethod
def thinking(
self,
muscle_id: str,
data_from_muscle: Any,
) -> Any:
"""Think about a response using the provided information.
The :meth:`.thinking` method is the place for the
implementation of the agent's/brain's logic. The brain can
use the current sensor readings, review the actions of the
previous thinking and consider the reward (provided by the
objective).
Usually, this is the place where machine learning happens,
but other solutions are possible as well (like a set of rules
or even random based results).
The method receives only the name of the :class:`Muscle` that is
sending data, along with whatever data this :class:`Muscle` wants to
send to the Brain. As this is completely implementation-specific,
this method does not impose any restrictions.
Any data that is available to palaestrAI, such as the actual sensor
readings, setpoints a Muscle provided, rewards, the objective
function's value (goal/utility function), and whether the simulation is
*done* or not, is available via the Brain's :class:`Memory`
(cf. ::`Brain.memory`).
Parameters
-------
muscle_id : str
This is the ID of the muscle which requested the update
data_from_muscle: Any
Any data the :class:`Muscle` sends to the Brain
Returns
-------
Any
Any update that the :class:`Muscle`. If this value does not
evaluate to ``True`` (i.e., ``bool(update) == False``), then the
:class:`Muscle` will not be updated.
"""
pass
def try_load_brain_dump(self):
LOG.debug(
"%s tries to load a previous braindump from any of %s.",
self,
self._dumpers,
)
if any(d for d in self._dumpers if d._brain_source):
try:
self.load()
except AttributeError:
# This happens because somebody forgot to check whether loader
# returned None...
LOG.error(
"%s tried to load a brain dump, but that seemed to have "
"failed. However, instead of a sane reboot, the brain "
"never checked whether the loader returned 'None'. So, "
"the loading failed completely. Don't blame me, blame the "
"implementor. My brain will go on, but don't expect a "
"happy-end from me.",
self,
)
[docs]
def store(self):
"""Stores the current state of the model
This method is called whenever the current state of the brain should
be saved. How a particular model is serialized is up to the concrete
implementation. Also, brains may be divided into sub-models (e.g.,
actor and critic), whose separate storage is relized via tags.
Implementing this method allows for a versatile implementation of this.
It is advisable to use the storage facilities of palaestrAI. They are
available through the method
:meth:`~BrainDumper.store_brain_dump(binary_io, self._dumpers, tag)`.
This function calls all available dumpers to store the serialized
brain dump provided in the parameter ``binary_io`` and optionally
attaches a ``tag`` to it. The attribute ::`~Brain._dumpers` is
initialized to a list of available dumpers and can be used directly.
"""
try:
# Try to be backwards compatible and call the old store_model
# method as a default implementation
if self._dumpers:
locator = self._dumpers[0]._brain_destination
path = (
Path(RuntimeConfig().data_path).resolve()
/ "brains"
/ locator.experiment_run_uid
/ str(locator.experiment_run_phase)
/ str(locator.agent_name)
)
else:
path = RuntimeConfig().data_path
path.mkdir(parents=True, exist_ok=True)
self.store_model(path)
LOG.warning(
"%s uses deprecated storage API, please upgrade to "
"store()/load().",
self,
)
except Exception as e:
# This okay, sorts of. It means that the brain does not implement
# any way of storing its state, which might be okay as well...
LOG.warning(
"%s does not implement store(); its current state "
"cannot be saved. Please provide an implementation of "
"%s.store(). Providing an empty one silences this "
"warning. (Error message was: %s)",
self,
self,
e,
)
pass
[docs]
def load(self):
"""Loads the current state of the model
This method is called whenever the current state of the brain should
be restored. How a particular model is deserialized is up to the
concrete implementation. Also, brains may be divided into sub-models
(e.g., actor and critic), whose separate storage is relized via tags.
Implementing this method allows for a versatile implementation of this.
It is advisable to use the storage facilities of palaestrAI. They are
available through the method
:meth:`~BrainDumper.load_brain_dump(self._dumpers, tag)`.
This function calls all available dumpers to restore the serialized
brain dump (optionally identified via a ``tag``). It returns a
BinaryIO object that can then be used in the implementation. The
attribute ::`~Brain._dumpers` is initialized to the list of available
dumpers/loaders.
"""
try:
# Try to be backwards compatible and call the old load_model
# method as a default implementation
if self._dumpers and self._dumpers[0]._brain_source:
locator = self._dumpers[0]._brain_source
path = (
Path(RuntimeConfig().data_path).resolve()
/ "brains"
/ locator.experiment_run_uid
/ str(locator.experiment_run_phase)
)
else:
path = RuntimeConfig().data_path
self.load_model(path)
LOG.warning(
"%s uses deprecated storage API, please upgrade to "
"store()/load().",
self,
)
except Exception as e:
# This okay, sorts of. It means that the brain does not implement
# any way of storing its state, which might be okay as well...
LOG.warning(
"%s does not implement load(); its current state cannot"
" be loaded. Please provide an implementation of "
"%s.load(). Providing an empty one silences this "
"warning. (Error message was: %s)",
self,
self,
e,
)
pass
def load_model(self, path):
warnings.warn(
"Brain.load_model is deprecated and will be removed in "
"palaestrAI 4.0. Please use the new store()/load() "
"infrastructure instead.",
category=DeprecationWarning,
stacklevel=2,
)
def store_model(self, path):
warnings.warn(
"Brain.store_model is deprecated and will be removed in "
"palaestrAI 4.0. Please use the new store()/load() "
"infrastructure instead.",
category=DeprecationWarning,
stacklevel=2,
)
def __str__(self):
return "%s(id=0x%x)" % (self.__class__, id(self))