Skip to content

Commit

Permalink
New docs for new features. Updated other docs for new features.
Browse files Browse the repository at this point in the history
  • Loading branch information
JamesArruda committed Dec 21, 2024
1 parent c7f35c0 commit 88ce8b1
Show file tree
Hide file tree
Showing 9 changed files with 412 additions and 107 deletions.
12 changes: 8 additions & 4 deletions docs/source/user_guide/how_tos/active_states.rst
Original file line number Diff line number Diff line change
Expand Up @@ -2,12 +2,15 @@
Active States
=============

Active States are an UPSTAGE feature where states are told how to update themselves when requested, while not having to modify or alter the timeout they are changing during.
Active States are an UPSTAGE feature where states are told how to update themselves when requested,
while not having to modify or alter the timeout they are changing during.

For example, a fuel depot may dispense fuel at a given rate for some amount of time. An employee may monitor that level at certain times. UPSTAGE allows the state to hold its own
For example, a fuel depot may dispense fuel at a given rate for some amount of time.
An employee may monitor that level at certain times. UPSTAGE allows the state to hold its own
update logic, rather than the employee code needing to know when the fuel started changing, at what rate, etc.

Active states are stopped and started with :py:meth:`~upstage_des.actor.Actor.activate_state` and :py:meth:`~upstage_des.actor.Actor.deactivate_state`.
Active states are stopped and started with :py:meth:`~upstage_des.actor.Actor.activate_state` and
:py:meth:`~upstage_des.actor.Actor.deactivate_state`.

Active states are automatically stopped when a Task is interrupted.

Expand Down Expand Up @@ -248,4 +251,5 @@ Another option is to make a subclass that hints for you:
>>> 220.0
Note that state activation doesn't require a task. It's just the best place to do it, because task interrupts automatically deactivate all states.
Note that state activation doesn't require a task. It's just the best place to do it,
because task interrupts automatically deactivate all states.
19 changes: 13 additions & 6 deletions docs/source/user_guide/how_tos/entity_naming.rst
Original file line number Diff line number Diff line change
Expand Up @@ -2,12 +2,18 @@
Named Entities
==============

Named entities are an :py:class:`~upstage_des.base.EnvironmentContext` and :py:class:`~upstage_des.base.NamedUpstageEntity` enabled feature where you can store instances in particular "entity groups" to gather
them from later. UPSTAGE's :py:class:`~upstage_des.actor.Actor` inherits from :py:class:`~upstage_des.base.NamedUpstageEntity`, giving all Actors the feature.
Named entities are an :py:class:`~upstage_des.base.EnvironmentContext` and
:py:class:`~upstage_des.base.NamedUpstageEntity` enabled feature where you
can store instances in particular "entity groups" to gather them from later.
UPSTAGE's :py:class:`~upstage_des.actor.Actor` inherits from :py:class:`~upstage_des.base.NamedUpstageEntity`,
giving all Actors the feature. Similarly, the ``SelfMonitoring<>`` resources
do the same to enable quick access to recorded simulation data.

All Actors are retrievable with the :py:meth:`~upstage_des.base.UpstageBase.get_actors` method if they inherit from Actor.
All Actors are retrievable with the :py:meth:`~upstage_des.base.UpstageBase.get_actors`
method if they inherit from Actor.

Entities are retrievable with :py:meth:`~upstage_des.base.UpstageBase.get_all_entity_groups` and :py:meth:`~upstage_des.base.UpstageBase.get_entity_group`.
Entities are retrievable with :py:meth:`~upstage_des.base.UpstageBase.get_all_entity_groups`
and :py:meth:`~upstage_des.base.UpstageBase.get_entity_group`.

Defining a named entity is done in the class definition:

Expand All @@ -29,7 +35,7 @@ Defining a named entity is done in the class definition:
...
Once you are in an environment context you get the actual instances.
Once you are in an environment context you can get the actual instances.

.. code-block:: python
Expand Down Expand Up @@ -59,5 +65,6 @@ Once you are in an environment context you get the actual instances.
print(different)
>>> [<__main__.Different object at 0x000001FFAB28BE10>]
Note that entity groups are inheritable, that you can inherit from ``NamedUpstageEntity`` and retrieve the instance without needing an Actor, and that it's simple to create an instance of
Note that entity groups are inheritable and that you can inherit from ``NamedUpstageEntity``
and retrieve the instance without needing an Actor. You may also create an instance of
``UpstageBase`` to get access to the required methods.
53 changes: 53 additions & 0 deletions docs/source/user_guide/how_tos/environment.rst
Original file line number Diff line number Diff line change
@@ -0,0 +1,53 @@
===================
Environment Context
===================

UPSTAGE uses Python's [context variable](https://docs.python.org/3/library/contextvars.html)
capabilities to safely manage "global" state information while not polluting the module
level data with run-specific information.

The context manager accepts three arguments:

1. Simulation start time (passes through to ``simpy.Environment``)
2. A random seed for ``random.Random``
3. A random number generator object, if different than ``random.Random``

For more about the random numbers, see :doc:`Random Numbers </user_guide/how_tos/random_numbers>`.

.. note::

If you get a warning or error about not finding an environment, you have likely
tried to instantiate an actor, task, or other UPSTAGE object outside of an
environment context.


Creating Contexts
=================

Use the ``EnvironmentContext`` context manager:

.. code:: python
impoprt upstage_des.api as UP
with UP.EnvironmentContext() as env:
...
# everything in here can find that environment
...
env.run()
Or, create a context at the current scope:

.. code:: python
from upstage_des.base import create_top_context, clear_top_context
ctx = create_top_context()
env = ctx.env_ctx.get()
...
env.run()
clear_top_context(ctx)
This way is friendlier to Jupyter notebooks, where you might run a simulation and want to
explore the data without needing to remain in the context manager.
12 changes: 8 additions & 4 deletions docs/source/user_guide/how_tos/random_numbers.rst
Original file line number Diff line number Diff line change
Expand Up @@ -4,9 +4,11 @@ Random Numbers

Random numbers are not supplied by UPSTAGE, you are responsible for rolling dice on your own.

However, UPSTAGE does use them in one area, which is in :py:class:`~upstage_des.events.Wait`, in the :py:meth:`~upstage_des.events.Wait.from_random_uniform` method.
However, UPSTAGE does use them in one area, which is in :py:class:`~upstage_des.events.Wait`,
in the :py:meth:`~upstage_des.events.Wait.from_random_uniform` method.

The built-in python ``random`` module is used by default, and you can find it on ``stage.random``. It can be instantiated in a few ways:
The built-in python ``random`` module is used by default, and you can find it on
``stage.random``. It can be instantiated in a few ways:

.. code-block:: python
Expand All @@ -31,6 +33,8 @@ The built-in python ``random`` module is used by default, and you can find it on
print(num)
>>> 2.348057489610457
If you want to use your own random number generator, just supply it to the ``random_gen`` input, or as its own variable with ``UP.add_stage_variable``.
If you want to use your own random number generator, just supply it to the ``random_gen``
input, or as its own variable with ``UP.add_stage_variable``.

If you supply it as ``random_gen``, ensure that it has a ``uniform`` method so that the Wait event can use it.
If you supply it as ``random_gen``, ensure that it has a ``uniform`` method so that the
Wait event can use it.
4 changes: 3 additions & 1 deletion docs/source/user_guide/index.md
Original file line number Diff line number Diff line change
Expand Up @@ -25,7 +25,7 @@ tutorials/interrupts
tutorials/rehearsal
tutorials/best_practices
tutorials/data
tutorials/simpy_compare.rst
tutorials/simpy_compare
```

It is also recommended that you familiarize yourself with how [SimPy runs by itself](https://simpy.readthedocs.io/en/latest/), since
Expand All @@ -42,6 +42,7 @@ These are complete examples for some of the above tutorials.
tutorials/first_sim_full.rst
tutorials/rehearsal_sim.rst
tutorials/complex_cashier.rst
tutorials/data_creation_example.rst
```

## How-to Guides
Expand All @@ -52,6 +53,7 @@ These pages detail the specific activities that can be accomplished using UPSTAG
:caption: How-Tos
:maxdepth: 1
how_tos/environment.rst
how_tos/resources.rst
how_tos/resource_states.rst
how_tos/active_states.rst
Expand Down
78 changes: 48 additions & 30 deletions docs/source/user_guide/tutorials/best_practices.rst
Original file line number Diff line number Diff line change
Expand Up @@ -5,75 +5,91 @@ Best Practices
Actors
======

Use knowledge when you want to add built-in enforcement/overwrite checking. States don't have that by default, so you'd have to write more validation rules in tasks
rather than mostly business logic.
Use knowledge when you want to add built-in enforcement/overwrite checking.
States don't have that by default, so you'd have to write more validation
rules in tasks rather than mostly business logic.

There's built-in signature building for Actors based on the states, but it only works in the interpreter.
There's built-in signature building for Actors based on the states, but
it only works in the interpreter.


Tasks
=====

Keep tasks as small as possible. This makes handling interrupts much easier. Use the Task Networks to compose smaller tasks, and use decision tasks to navigate the network.
Keep tasks as small as possible. This makes handling interrupts much easier.
Use the Task Networks to compose smaller tasks, and use decision tasks to navigate the network.

When doing interrupts, don't be afraid to throw exceptions everywhere. It's hard to predict what might cause an interrupt (depending), so always give yourself as much information
as you can.
When doing interrupts, don't be afraid to throw exceptions everywhere.
It's hard to predict what might cause an interrupt (depending), so always
give yourself as much information as you can.

Mixing nucleus and ``set_knowledge_event`` for task running might get confusing. Choose nucleus features for task networks that have multiple sources of interrupts. For simpler
holding events (waiting for a job to do, e.g.) that single entity will command to start, knowledge events are better.
Mixing nucleus and ``set_knowledge_event`` for task running might get confusing.
Choose nucleus features for task networks that have multiple sources of interrupts. For
simpler holding events (waiting for a job to do, e.g.) that single entity will command
to start, knowledge events are better.


Testing
=======

Write tests for your individual tasks to make sure you see the expected changes. Use ``Task().run(actor=actor)`` in an EnvironmentContext to do that.
Write tests for your individual tasks to make sure you see the expected changes. Use
``Task().run(actor=actor)`` in an EnvironmentContext to do that.

The more clearly defined your stores/interfaces are, the easier it is to test.

Actor Interactions
==================

Interaction between different actors is sometimes easier to accomplish with a Task operated by a higher-level actor that waits for enough actors to say they are ready
(usually via a store). Then the higher-level actor can add knowledge, modify task queues, and send the actors on their way.
Interaction between different actors is sometimes easier to accomplish with a Task operated
by a higher-level actor that waits for enough actors to say they are ready
(usually via a store). Then the higher-level actor can add knowledge, modify task queues,
and send the actors on their way.

Even if the behavior being modeled would be decided mutually by the actors (no strict command hierarchy, e.g.), it can be much easier in DES to run that as a separate
process.
Even if the behavior being modeled would be decided mutually by the actors (no strict command
hierarchy, e.g.), it can be much easier in DES to run that as a separate process.

Yielding on a Get is nice for comms and commands, but that usually needs to be a separate task network with tasks that:
Yielding on a Get is nice for comms and commands, but that usually needs to be a separate task
network with tasks that:

1. Wait for the message
2. Get the message, decide what to do
3. Analyze the actor's current state
4. Interrupt and recommand as needed

There are edge cases when you re-command a Task Network, but the re-command/interrupt is sorted later in the event queue (even with zero-time waits). To mitigate this
problem, put very small (but non-zero) waits after a message is received to give some time for the new task networks to change, so they are ready for new interrupts if
a message immediately follows another.

There are edge cases when you re-command a Task Network, but the re-command/interrupt is sorted
later in the event queue (even with zero-time waits). To mitigate this problem, put very small
(but non-zero) waits after a message is received to give some time for the new task networks to
change, so they are ready for new interrupts if a message immediately follows another.

Geography
=========

The ``GeodeticLocationChangingState`` isn't perfectly accurate when it reaches its destination. Floating point errors and the like will make it be slightly off the destination.
The ``GeodeticLocationChangingState`` isn't perfectly accurate when it reaches its destination.
Floating point errors and the like will make it be slightly off the destination.

THe amount of difference will be very small, and practically shouldn't matter in most cases. Be aware of this, and set locations explicitly after deactivating them if you need the
precision.
THe amount of difference will be very small, and practically shouldn't matter in most cases.
Be aware of this, and set locations explicitly after deactivating them if you need the precision.


Simulation Determinism
======================

While Python 3.10+ generally guarantee that all dictionaries act in an insertion-ordered manner, that order might change from run to run, even if the random seed is the same.
If your simulations are not deterministic even with a controlled random seed, it is likely due to lack of determinism in dictionary access or sorting.
While Python 3.10+ generally guarantee that all dictionaries act in an insertion-ordered manner,
that order might change from run to run, even if the random seed is the same. If your simulations
are not deterministic even with a controlled random seed, it is likely due to lack of determinism
in dictionary access or sorting.

To mitigate this, you'll need to implement some kind of sorting or hashing that is dependent on something that isn't based on ``id``.
To mitigate this, you'll need to implement some kind of sorting or hashing that is dependent on
something that isn't based on ``id``.

This issue arises frequently in management logic, where actors are selected from lists or dictionaries to perform some task.
This issue arises frequently in management logic, where actors are selected from lists or dictionaries
to perform some task.

Rehearsal
=========

When testing for ``PLANNING_FACTOR_OBJECT``, do so in a method on the task that streamlines the business logic of the main task. For example:
When testing for ``PLANNING_FACTOR_OBJECT``, do so in a method on the task that streamlines the
business logic of the main task. For example:

.. code-block:: python
Expand All @@ -90,9 +106,11 @@ When testing for ``PLANNING_FACTOR_OBJECT``, do so in a method on the task that
time = self._get_time(item)
yield UP.Wait(time)
Rehearsals can get very complicated, and tasks that have lots of process interaction expectations may not rehearse well. Rehearsal is best done for
simpler, streamlined tasks. Make sure there is a clear code path for rehearsing, and following the advice in the Tasks section of this page will go
Rehearsals can get very complicated, and tasks that have lots of process interaction expectations
may not rehearse well. Rehearsal is best done for simpler, streamlined tasks. Make sure there
is a clear code path for rehearsing, and following the advice in the Tasks section of this page will go
a long way to making rehearsals better.

Rehearsal currently only works for one Actor at a time, and while the Actor is clone-able without affecting the rest of the sim, the ``stage`` is not cloned.
If a task references ``stage``, or looks to other actors, events, stores, etc. the rehearsal may cause side-effects in the actual sim.
Rehearsal currently only works for one Actor at a time, and while the Actor is clone-able without
affecting the rest of the sim, the ``stage`` is not cloned. If a task references ``stage``, or
looks to other actors, events, stores, etc. the rehearsal may cause side-effects in the actual sim.
Loading

0 comments on commit 88ce8b1

Please sign in to comment.