From 0b1d86139d703d453f1f69c284f973f6e3ec4830 Mon Sep 17 00:00:00 2001 From: James Arruda <31418520+JamesArruda@users.noreply.github.com> Date: Mon, 10 Feb 2025 20:20:51 -0500 Subject: [PATCH] 30 knowledge utilities (#32) #30 Convenience methods for bulk knowledge actions --- docs/source/user_guide/how_tos/events.rst | 3 + docs/source/user_guide/how_tos/knowledge.rst | 133 +++++++++++++++++++ docs/source/user_guide/index.md | 1 + src/upstage_des/actor.py | 68 ++++++++++ src/upstage_des/task.py | 70 +++++++++- src/upstage_des/test/test_actor.py | 56 ++++++++ 6 files changed, 330 insertions(+), 1 deletion(-) create mode 100644 docs/source/user_guide/how_tos/knowledge.rst diff --git a/docs/source/user_guide/how_tos/events.rst b/docs/source/user_guide/how_tos/events.rst index a2b871c..a77dc38 100644 --- a/docs/source/user_guide/how_tos/events.rst +++ b/docs/source/user_guide/how_tos/events.rst @@ -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` ------------------------------------ diff --git a/docs/source/user_guide/how_tos/knowledge.rst b/docs/source/user_guide/how_tos/knowledge.rst new file mode 100644 index 0000000..0463dfc --- /dev/null +++ b/docs/source/user_guide/how_tos/knowledge.rst @@ -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 `, 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 `) + +.. 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. diff --git a/docs/source/user_guide/index.md b/docs/source/user_guide/index.md index e789cb4..089d38f 100644 --- a/docs/source/user_guide/index.md +++ b/docs/source/user_guide/index.md @@ -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 diff --git a/src/upstage_des/actor.py b/src/upstage_des/actor.py index 1d8147b..c691a7f 100644 --- a/src/upstage_des/actor.py +++ b/src/upstage_des/actor.py @@ -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 = "", @@ -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. diff --git a/src/upstage_des/task.py b/src/upstage_des/task.py index 68d6506..4f54ac0 100644 --- a/src/upstage_des/task.py +++ b/src/upstage_des/task.py @@ -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 @@ -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. diff --git a/src/upstage_des/test/test_actor.py b/src/upstage_des/test/test_actor.py index 9b76434..7c1881e 100644 --- a/src/upstage_des/test/test_actor.py +++ b/src/upstage_des/test/test_actor.py @@ -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")