Skip to content

Commit

Permalink
29 allow tasks to yield regular simpy events (#36)
Browse files Browse the repository at this point in the history
* #29 - Feature allowed, documents added.
  • Loading branch information
JamesArruda authored Feb 11, 2025
1 parent 0b1d861 commit 2529ac0
Show file tree
Hide file tree
Showing 4 changed files with 82 additions and 8 deletions.
12 changes: 8 additions & 4 deletions docs/source/user_guide/how_tos/decision_tasks.rst
Original file line number Diff line number Diff line change
Expand Up @@ -2,8 +2,10 @@
Decision Tasks
==============

Decision tasks are :py:class:`~upstage_des.task.Task`s that take zero time and were briefly demonstrated in :doc:`Rehearsal </tutorials/rehearsal>`. The purpose of a
Decision task is to allow decision making and :py:class:`~upstage_des.task_networks.TaskNetwork` routing without moving the simulation clock and do so inside of a Task Network.
Decision tasks are :py:class:`~upstage_des.task.Task` s that take zero time and were briefly demonstrated in
:doc:`Rehearsal </user_guide/tutorials/rehearsal>`. The purpose of a Decision task is to allow decision making and
:py:class:`~upstage_des.task_networks.TaskNetwork` routing without moving the simulation clock and do so
inside of a Task Network.

A decision task must implement two methods:

Expand All @@ -16,5 +18,7 @@ Neither method outputs anything. The expectation is that inside these methods yo
* :py:meth:`upstage_des.actor.Actor.set_task_queue`: Add tasks to an empty queue (by string name) - you must empty the queue first.
* :py:meth:`upstage_des.actor.Actor.set_knowledge`: Modify knowledge

The difference between making and rehearsing the decision is covered in the tutorial. The former method is called during normal operations of UPSTAGE, and the latter is called during a
rehearsal of the task or network. It is up the user to ensure that no side-effects occur during the rehearsal that would touch non-rehearsing state, actors, or other data.
The difference between making and rehearsing the decision is covered in the tutorial. The former method
is called during normal operations of UPSTAGE, and the latter is called during a
rehearsal of the task or network. It is up the user to ensure that no side-effects occur during the
rehearsal that would touch non-rehearsing state, actors, or other data.
68 changes: 68 additions & 0 deletions docs/source/user_guide/how_tos/task.rst
Original file line number Diff line number Diff line change
@@ -0,0 +1,68 @@
=====
Tasks
=====

The :py:class:`~upstage_des.task.Task` class is one of the fundamental blocks of an UPSTAGE
simulation. It controls the changes to ``Actor`` states and coordinates with the underlying
SimPy event queue.

Tasks are defined by subclassing from ``Task`` and creating the ``task`` method. In the
example below, the task is properly typed. UPSTAGE provides a type hint for
the generator object that ``task()`` is. Not also that ``actor`` is a required named
argument.

.. code-block:: python
import upstage_des.api as UP
from upstage_des.type_help import TASK_GEN
class Lathe(UP.Actor):
outgoing_bin = UP.ResourceState[UP.SelfMonitoringStore]()
class UseLathe(UP.Task):
def task(self, *, actor: Lathe) -> TASK_GEN:
"""Run the lathe task."""
piece = self.get_actor_knowledge("work_piece", must_exist=True)
time = actor.estimate_work_time(piece)
yield UP.Wait(time)
piece.status = "Done"
actor.number_worked += 1
yield UP.Put(actor.outgoing_bin, piece)
In that example, the task yields out UPSTAGE :doc:`Events </user_guide/how_tos/events>` only, which is the
typical usage. UPSTAGE will also let you yield a simpy process, but this will raise a warning and is
discouraged as interrupt handling and other services won't work. If a process is yielded on, and your
task is interrupted, the yielded process will receive an interrupt as well.

Tasks only allow one actor, so use :doc:`Knowledge </user_guide/how_tos/knowledge>` to help
manage interactions or other information. For more complex interactions, see :doc:`State Sharing </user_guide/how_tos/state_sharing>`.

Interrupts
----------

Task interruption, by default, will raise the usual SimPy exception. The user can add the ``on_interrupt`` method
to their task subclass to handle interruption. That method must accept an actor, a cause object, and must return
a enumerator that tells UPSTAGE how to handle the interruption.

See :doc:`Interrupts </user_guide/tutorials/interrupts>` for more.


Decision Tasks
--------------

Decision tasks are a special form of Task that does not touch the simulation queue, except for a zero time wait.
The zero-time wait exists so the user knows that other decision tasks may run before the Actor using the task
proceeds on to the next yield statement.

Subclass and implement ``make_decision`` to use the class. The ``rehearse_decision`` method can also be implemented
to provide rehearsal decision making. That is useful for separating planning and action code when the simpy clock
will not be advancing.

See :doc:`Decision Tasks </user_guide/how_tos/decision_tasks>` for more.

Rehearsal
---------

Rehearsal is a feature for estimating the results of a Task on a "cloned" Actor to examine actor state
for planning purposes. See :doc:`Rehearsal </user_guide/tutorials/rehearsal>` for more.
1 change: 1 addition & 0 deletions docs/source/user_guide/index.md
Original file line number Diff line number Diff line change
Expand Up @@ -56,6 +56,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/task.rst
how_tos/resources.rst
how_tos/resource_states.rst
how_tos/active_states.rst
Expand Down
9 changes: 5 additions & 4 deletions src/upstage_des/task.py
Original file line number Diff line number Diff line change
Expand Up @@ -406,9 +406,10 @@ def rehearse(
next_event = generator.send(returned_item)
returned_item = None
if not issubclass(next_event.__class__, BaseEvent):
raise SimulationError(
f"Task {self} event {next_event} must be a subclass of BaseEvent!"
)
msg = f"Task {self} event {next_event}"
if isinstance(next_event, Process):
raise SimulationError(msg + " cannot be a process during rehearsal.")
raise SimulationError(msg + " must be a subclass of BaseEvent!")
time_advance, returned_item = next_event.rehearse()
mocked_env.now += time_advance

Expand Down Expand Up @@ -453,7 +454,7 @@ def _handle_interruption(
if isinstance(next_event, BaseEvent):
next_event.cancel()
elif isinstance(next_event, Process):
next_event.interrupt(cause="Interrupt from task")
next_event.interrupt(cause=interrupt.cause)
else:
raise SimulationError(f"Bad event passed: {next_event}")
stop_run = True
Expand Down

0 comments on commit 2529ac0

Please sign in to comment.