Skip to content

Commit

Permalink
30 knowledge utilities (#32)
Browse files Browse the repository at this point in the history
#30 Convenience methods for bulk knowledge actions
  • Loading branch information
JamesArruda authored Feb 11, 2025
1 parent 9ad9737 commit 0b1d861
Show file tree
Hide file tree
Showing 6 changed files with 330 additions and 1 deletion.
3 changes: 3 additions & 0 deletions docs/source/user_guide/how_tos/events.rst
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,9 @@ One use case is the knowledge event, which enables a way to publish and event to
subordinate: UP.Actor = actor.subordinates[0]
subordinate.succeed_knowledge_event(name="pause", some_data={...})
The Event also has a "payload", which is created from keyword arguments to the :py:meth:`~upstage_des.evetns.Event.succeed` method.
The payload can be retrieved using :py:meth:`~upstage_des.evetns.Event.get_payload`.


:py:class:`~upstage_des.events.Wait`
------------------------------------
Expand Down
133 changes: 133 additions & 0 deletions docs/source/user_guide/how_tos/knowledge.rst
Original file line number Diff line number Diff line change
@@ -0,0 +1,133 @@
=========
Knowledge
=========

Knowledge is a property of an :py:meth:`~upstage_des.actor.Actor` that is intended to be a
temporary space for storing information about the Actor's goals or perception. While many
actions that use knowledge could be accomplished with :doc:`States <states>`, knowledge is
created separately to include other checks and debug logging support.

While you can use knowledge for anything you want, a typical pattern is to use knowledge to support task
network flow. A knowledge entry could be a list of activities to do. A :py:class:`~upstage_des.task.DecisionTask` could
pop entries from a knowledge list and re-plan the network.

Knowledge is also used to store events that are known only to an Actor to support some process
continuation patterns, described farther below.

Knowledge is accessed and updated through:

* :py:meth:`upstage_des.actor.Actor.get_knowledge`

* ``name``: The name of the knowledge.

* ``must_exist``: Boolean for raising an exception if the knowledge does not exist.

* :py:meth:`upstage_des.task.Task.get_actor_knowledge`

* ``actor``: The actor that has the knowledge.

* ``name`` and ``must_exist``, as above.

* :py:meth:`upstage_des.actor.Actor.set_knowledge`

* ``name``: The name of the knowledge to set.

* ``value``: Any object to set as the value.

* ``overwrite``: Boolean for allowing an existing value to be changed. Defaults to False, and will raise an exception if not allowed to overwrite.

* ``caller``: Optional information - through a string - of who is calling the knowledge set method. This records to the actor debug log, if enabled.

* :py:meth:`upstage_des.task.Task.set_actor_knowledge`

* ``actor``: The actor that you want to set knowledge on.

* All other inputs as above, except that ``caller`` is filled out for you.

* :py:meth:`upstage_des.actor.Actor.clear_knowledge`

* ``name``: The name of the knowledge to delete.

* ``caller``: Same as above.

* :py:meth:`upstage_des.task.Task.clear_actor_knowledge`

* ``actor``: The actor to delete knowledge from.

* All other inputs as above, except that ``caller`` is filled out for you.


The actor knowledge can be set and retrieved from the actor itself, and the ``Task`` convenience methods are there
to provide data to the actor debug log (if ``debug_logging=True`` is set on the Actor) to help trace where an actor's
information came from.

For convenience, you can get and remove knowledge in one method using:

* :py:meth:`~upstage_des.actor.Actor.get_and_clear_knowledge` on the Actor.
* :py:meth:`~upstage_des.task.Task.get_and_clear_actor_knowledge` on the Task.


Bulk Knowledge
--------------

All the above methods can be operated on in bulk:

1. :py:meth:`~upstage_des.actor.Actor.set_bulk_knowledge`: Set knowledge using a dictionary.
2. :py:meth:`~upstage_des.actor.Actor.get_bulk_knowledge`: Get knowledge using an iterable of names.
3. :py:meth:`~upstage_des.actor.Actor.clear_bulk_knowledge`: Clear knowledge using an iterable of names.
4. :py:meth:`~upstage_des.actor.Actor.get_and_clear_bulk_knowledge`: Get a dictionary of knowledge and clear it.

The tasks contain similarly named methods:

1. :py:meth:`~upstage_des.task.Task.set_actor_bulk_knowledge`
2. :py:meth:`~upstage_des.task.Task.get_actor_bulk_knowledge`
3. :py:meth:`~upstage_des.task.Task.clear_actor_bulk_knowledge`
4. :py:meth:`~upstage_des.task.Task.get_and_clear_actor_bulk_knowledge`

This is most useful for initializing or passing large amounts of information to an actor.


Knowledge Events
----------------

It is often times useful to hold an actor in a task until an event succeeds. UPSTAGE Actors
have a :py:meth:`~upstage_des.actor.Actor.create_knowledge_event` and :py:meth:`~upstage_des.actor.Actor.succeed_knowledge_event`
method to support this activity (also described in :doc:`Events </user_guide/how_tos/events>`)

.. code-block:: python
HAIRCUT_DONE = "haircut is done"
class Chair(UP.Actor):
sitting = UP.ResourceState[UP.SelfMonitoringStore]()
class Customer(UP.Actor):
hair_length = UP.State[float](recording=True)
class Haircut(UP.Task):
def task(self, *, actor: Customer):
assigned_chair = self.get_actor_knowledge(
actor,
name="chair",
must_exist=True,
)
evt = actor.create_knowledge_event(name=HAIRCUT_DONE)
yield UP.Put(assigned_chair.sitting, actor)
yield evt
print(evt.get_payload())
class DoHaircut(UP.Task):
def task(self, *, actor: Chair):
customer = yield UP.Get(actor.sitting)
yield UP.Wait(30.0)
customer.hair_length *= 0.5
customer.succeed_knowledge_event(name=HAIRCUT_DONE, data="Have a nice day!")
The above simplified example shows how UPSTAGE tasks can work with knowledge events to
support simple releases from other tasks without adding stores or other signaling mechanisms.

The succeed event method also clears the event from the knowledge.
1 change: 1 addition & 0 deletions docs/source/user_guide/index.md
Original file line number Diff line number Diff line change
Expand Up @@ -55,6 +55,7 @@ These pages detail the specific activities that can be accomplished using UPSTAG
how_tos/environment.rst
how_tos/states.rst
how_tos/knowledge.rst
how_tos/resources.rst
how_tos/resource_states.rst
how_tos/active_states.rst
Expand Down
68 changes: 68 additions & 0 deletions src/upstage_des/actor.py
Original file line number Diff line number Diff line change
Expand Up @@ -497,6 +497,23 @@ def get_knowledge(self, name: str, must_exist: bool = False) -> Any:
raise SimulationError(f"Knowledge '{name}' does not exist in {self}")
return self._knowledge.get(name, None)

def get_and_clear_knowledge(self, name: str, caller: str | None = None) -> Any:
"""Get knowledge and clear it.
Clearing knowledge implies it must exist in the direct methods, so the
same assumption holds here.
Args:
name (str): Knowledge name.
caller (str): The name of the calling process for logging. Defaults to None.
Returns:
Any: The knowledge value.
"""
know = self.get_knowledge(name, must_exist=True)
self.clear_knowledge(name, caller)
return know

def _log_caller(
self,
method_name: str = "",
Expand Down Expand Up @@ -563,6 +580,57 @@ def clear_knowledge(self, name: str, caller: str | None = None) -> None:
else:
del self._knowledge[name]

def set_bulk_knowledge(
self, know: dict[str, Any], overwrite: bool = False, caller: str | None = None
) -> None:
"""Set multiple knowledge entries at once.
Args:
know (dict[str, Any]): Dictionary of key:value pairs of knowledge.
overwrite (bool, optional): If overwrite is allowed. Defaults to False.
caller (str | None, optional): The name of the Task that called the method.
Defaults to None.
"""
for k, v in know.items():
self.set_knowledge(k, v, overwrite, caller)

def clear_bulk_knowledge(self, names: Iterable[str], caller: str | None = None) -> None:
"""Clear a list of knowledge entries.
Args:
names (Iterable[str]): Knowledge names.
caller (str | None, optional): The name of the Task that called the method.
Defaults to None.
"""
for name in names:
self.clear_knowledge(name, caller)

def get_bulk_knowledge(self, names: Iterable[str], must_exist: bool = False) -> dict[str, Any]:
"""Get multiple knowledge items.
Args:
names (Iterable[str]): Names of the knowledge
must_exist (bool, optional): If all entires must exist. Defaults to False.
Returns:
dict[str, Any]: The knowledge values. None if not present.
"""
return {name: self.get_knowledge(name, must_exist) for name in names}

def get_and_clear_bulk_knowledge(
self, names: Iterable[str], caller: str | None = None
) -> dict[str, Any]:
"""Get and clear multiple knowledge entries.
Args:
names (Iterable[str]): The knowledge to retrieve and delete.
caller (str | None, optional): The name of the caller. Defaults to None.
Returns:
dict[str, Any]: The retrieved knowledge.
"""
return {name: self.get_and_clear_knowledge(name, caller) for name in names}

def add_task_network(self, network: TaskNetwork) -> None:
"""Add a task network to the actor.
Expand Down
70 changes: 69 additions & 1 deletion src/upstage_des/task.py
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@

"""Tasks constitute the actions that Actors can perform."""

from collections.abc import Callable, Generator
from collections.abc import Callable, Generator, Iterable
from enum import IntFlag
from functools import wraps
from typing import TYPE_CHECKING, Any, TypeVar
Expand Down Expand Up @@ -280,6 +280,74 @@ def get_actor_knowledge(actor: "Actor", name: str, must_exist: bool = False) ->
"""
return actor.get_knowledge(name, must_exist)

def get_and_clear_actor_knowledge(self, actor: "Actor", name: str) -> Any:
"""Get and clear knowledge on an actor.
The knowledge is assumed to exist.
Args:
actor (Actor): The actor to get knowledge from.
name (str): The knowledge name.
Returns:
Any: The knowledge value.
"""
cname = self.__class__.__qualname__
return actor.get_and_clear_knowledge(name, caller=cname)

def set_actor_bulk_knowledge(
self, actor: "Actor", know: dict[str, Any], overwrite: bool = False
) -> None:
"""Set multiple knowledge entries at once.
Args:
actor (Actor): The actor to operate on.
know (dict[str, Any]): Dictionary of key:value pairs of knowledge.
overwrite (bool, optional): If overwrite is allowed. Defaults to False.
"""
for k, v in know.items():
self.set_actor_knowledge(actor, k, v, overwrite)

def clear_actor_bulk_knowledge(self, actor: "Actor", names: Iterable[str]) -> None:
"""Clear a list of knowledge entries.
Args:
actor (Actor): The actor to operate on.
names (Iterable[str]): Knowledge names.
"""
for name in names:
self.clear_actor_knowledge(actor, name)

def get_actor_bulk_knowledge(
self, actor: "Actor", names: Iterable[str], must_exist: bool = False
) -> dict[str, Any]:
"""Get multiple knowledge items.
Args:
actor (Actor): The actor to operate on.
names (Iterable[str]): Names of the knowledge
must_exist (bool, optional): If all entires must exist. Defaults to False.
Returns:
dict[str, Any]: The knowledge values. None if not present.
"""
return {name: self.get_actor_knowledge(actor, name, must_exist) for name in names}

def get_and_clear_actor_bulk_knowledge(
self, actor: "Actor", names: Iterable[str], caller: str | None = None
) -> dict[str, Any]:
"""Get and clear multiple knowledge entries.
Args:
actor (Actor): The actor to operate on.
names (Iterable[str]): The knowledge to retrieve and delete.
caller (str | None, optional): The name of the caller. Defaults to None.
Returns:
dict[str, Any]: The retrieved knowledge.
"""
return {name: self.get_and_clear_actor_knowledge(actor, name) for name in names}

def _clone_actor(self, actor: REH_ACTOR, knowledge: dict[str, Any] | None) -> REH_ACTOR:
"""Create a clone of the actor.
Expand Down
56 changes: 56 additions & 0 deletions src/upstage_des/test/test_actor.py
Original file line number Diff line number Diff line change
Expand Up @@ -181,6 +181,62 @@ class TestActor(Actor):
assert know is None, "Knowledge was not cleared"


def test_get_and_clear() -> None:
class TestActor(Actor):
pass

with EnvironmentContext():
act = TestActor(name="Second test")
act.set_knowledge("thing", {3: 1})
v = act.get_and_clear_knowledge("thing")
assert v == {3: 1}
assert "thing" not in act._knowledge

with pytest.raises(UP.SimulationError):
act.get_and_clear_knowledge("thing")

t = UP.Task()
act.set_knowledge("other", {2: 3})
v = t.get_and_clear_actor_knowledge(act, "other")
assert v == {2: 3}
assert "other" not in act._knowledge


def test_bulk_knowledge() -> None:
class TestActor(Actor):
pass

with UP.EnvironmentContext():
know = {"one": 1, "two": 2}
act = TestActor(name="Example")
act.set_bulk_knowledge(know)
assert know == act._knowledge

with pytest.raises(UP.SimulationError):
act.set_bulk_knowledge({"one": 3, "three": 3})

act.set_bulk_knowledge({"one": 11, "three": 3}, overwrite=True)
v = act.get_bulk_knowledge(set(["one", "two", "three"]))
assert v == {"one": 11, "two": 2, "three": 3}

v = act.get_and_clear_bulk_knowledge(["two", "three"])
assert v == {"two": 2, "three": 3}
assert act._knowledge == {"one": 11}

t = UP.Task()
t.set_actor_bulk_knowledge(act, know, overwrite=True)
assert act._knowledge == know
v = t.get_actor_bulk_knowledge(act, ["one", "two"])
assert v == know

with pytest.raises(UP.SimulationError):
t.set_actor_bulk_knowledge(act, {"one": 3, "three": 3})

v = t.get_and_clear_actor_bulk_knowledge(act, ["one", "two"])
assert v == know
assert act._knowledge == {}


def test_knowledge_event() -> None:
with EnvironmentContext() as env:
act = Actor(name="A test actor")
Expand Down

0 comments on commit 0b1d861

Please sign in to comment.