From 88ce8b120e98edbfcb980c5c8dfc6280b204e3d0 Mon Sep 17 00:00:00 2001 From: James Arruda Date: Fri, 20 Dec 2024 22:32:07 -0500 Subject: [PATCH] New docs for new features. Updated other docs for new features. --- .../user_guide/how_tos/active_states.rst | 12 +- .../user_guide/how_tos/entity_naming.rst | 19 +- .../source/user_guide/how_tos/environment.rst | 53 +++++ .../user_guide/how_tos/random_numbers.rst | 12 +- docs/source/user_guide/index.md | 4 +- .../user_guide/tutorials/best_practices.rst | 78 +++++--- docs/source/user_guide/tutorials/data.rst | 186 ++++++++++++++++-- .../tutorials/data_creation_example.rst | 8 + .../user_guide/tutorials/first_simulation.rst | 147 +++++++++----- 9 files changed, 412 insertions(+), 107 deletions(-) create mode 100644 docs/source/user_guide/how_tos/environment.rst create mode 100644 docs/source/user_guide/tutorials/data_creation_example.rst diff --git a/docs/source/user_guide/how_tos/active_states.rst b/docs/source/user_guide/how_tos/active_states.rst index 2e551c8..58f8d4b 100644 --- a/docs/source/user_guide/how_tos/active_states.rst +++ b/docs/source/user_guide/how_tos/active_states.rst @@ -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. @@ -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. diff --git a/docs/source/user_guide/how_tos/entity_naming.rst b/docs/source/user_guide/how_tos/entity_naming.rst index 4cac60a..5e149ea 100644 --- a/docs/source/user_guide/how_tos/entity_naming.rst +++ b/docs/source/user_guide/how_tos/entity_naming.rst @@ -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: @@ -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 @@ -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. diff --git a/docs/source/user_guide/how_tos/environment.rst b/docs/source/user_guide/how_tos/environment.rst new file mode 100644 index 0000000..889c65e --- /dev/null +++ b/docs/source/user_guide/how_tos/environment.rst @@ -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 `. + +.. 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. diff --git a/docs/source/user_guide/how_tos/random_numbers.rst b/docs/source/user_guide/how_tos/random_numbers.rst index e09483a..303f36f 100644 --- a/docs/source/user_guide/how_tos/random_numbers.rst +++ b/docs/source/user_guide/how_tos/random_numbers.rst @@ -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 @@ -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. diff --git a/docs/source/user_guide/index.md b/docs/source/user_guide/index.md index 51f2e95..d26e9b3 100644 --- a/docs/source/user_guide/index.md +++ b/docs/source/user_guide/index.md @@ -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 @@ -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 @@ -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 diff --git a/docs/source/user_guide/tutorials/best_practices.rst b/docs/source/user_guide/tutorials/best_practices.rst index 8211621..4edb6a0 100644 --- a/docs/source/user_guide/tutorials/best_practices.rst +++ b/docs/source/user_guide/tutorials/best_practices.rst @@ -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 @@ -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. diff --git a/docs/source/user_guide/tutorials/data.rst b/docs/source/user_guide/tutorials/data.rst index 08a437b..fe80204 100644 --- a/docs/source/user_guide/tutorials/data.rst +++ b/docs/source/user_guide/tutorials/data.rst @@ -1,22 +1,29 @@ -=============== -Simulation Data -=============== +======================================== +Simulation Data Gathering and Processing +======================================== UPSTAGE has three features for data recording: -1. Use `Actor.log()` to log a string message at a given time. +1. Use ``Actor.log()`` to log a string message at a given time. * Note that on Actor creation, ``debug_log=True`` must be given. 2. Use ``a_state = UP.State(recording=True)``. * Access the data with ``actor._state_histories["a_state"]`` + * The data will be in the form ``tuple[time, value]`` + * For :doc:`ActiveStates `, the ``value`` may be + a special ``Enum`` saying if the state is being activated, deactivated, + or is active/inactive. 3. Use a ``SelfMonitoring<>`` Store or Container. * Access the data with ``a_store._quantities`` + * The data will be in the form ``tuple[time, value]`` + +UPSTAGE also has utility methods for pulling all of the available data into a +tabular format, along with providing column headers. -Each feature has a list of tuples of (time, value) pairs. Actor Logging ============= @@ -73,9 +80,9 @@ On a per-actor level, you can set ``debug_log_time`` as well, and that value wil State Recording =============== -Nearly every state is recordable in UPSTAGE. The :py:class:`~upstage_des.states.ResourceState` is an exception covered -in the next section. To enable state recording, set ``recording=True``. After running the sim, use the ``_state_histories`` -attribute on the actor to get the data. +Nearly every state is recordable in UPSTAGE. The :py:class:`~upstage_des.states.ResourceState` +is an exception covered in the next section. To enable state recording, set ``recording=True``. +After running the sim, use the ``_state_histories`` attribute on the actor to get the data. .. code:: python @@ -95,7 +102,8 @@ attribute on the actor to get the data. print(cash._state_histories["items_scanned"]) >>> [(0.0, 0), (0.0, 1), (1.0, 3), (2.0, 4), (3.0, -1)] -That returns a list of (time, value) tuples. This works for simple data types, but not mutable types: +That returns a list of (time, value) tuples. This works for simple data types, +but not mutable types: .. code:: python @@ -120,8 +128,8 @@ Note that the string State of ``people_seen`` acts as a way to record data, even the moment the name of the last scanned person. This lets states behave as carriers of current or past information, depending on your needs. -The ``items`` value doesn't record, because the state doesn't see ``cash.items = ...``. For objects like that, -you should: +The ``items`` value doesn't record, because the state doesn't see ``cash.items = ...``. +For objects like that, you should: .. code:: python @@ -156,6 +164,59 @@ State recording of the built-in geographic states (cartesian and geodetic) is co with the data objects. This for both the active state versions and the typical ``UP.State[CartesianLocation]()`` ways of creating the state. +It's recommended, since UPSTAGE does not store much data about the motion of geographic states, to poll or ensure you +get the state value whenever you want to know where it is. While activating and deactivating will record the value, +if an actor is moving along waypoints, each waypoint doesn't record itself unless asked. + +Active State Recording +====================== + +Active states record in the same way, but extra information is given to tell the user if the state +was activated or not and if it was switching to/from active or inactive. + +The state history will still be ``(time, value)`` pairs, but on activation and deactivation an ``Enum`` +value is placed in the history to indicated which has taken place. The state value isn't recorded in +that row of the history because it will have been calculated immediately prior and recorded. + +.. code:: python + + class Cashier(UP.Actor): + time_worked = UP.LinearChangingState(default=0.0, recording=True) + + with UP.EnvironmentContext() as env: + cash = Cashier(name="Ertha") + + cash.activate_linear_state( + state="time_worked", + rate=1.0, + task=None, # this is fine to do outside of a task. + ) + + env.run(until=1) + cash.time_worked + env.run(until=3) + cash.time_worked + cash.deactivate_state(state="time_worked", task=None) + env.run(until=4) + cash.time_worked = 5.0 + + print(cash._state_histories["time_worked"]) + >>> [ + (0.0, 0.0), + (0.0, ), + (1.0, 1.0), + (3.0, 3.0), + (3.0, ), + (4.0, 5.0), + ] + +The built-in data gathering will account for this for you, but if you are manually processing +the active state histories, the (de)activation signal in the history should always come +after a recording at the same time value. + +Remember that if you never ask for the value of ``time_worked``, it will only report it on +activation and deactivation. + Resource Recording ================== @@ -231,3 +292,106 @@ Or use the actor init to pass the item function: name = "Lane 2", belt = {"item_func":lambda x: Counter(x)}, ) + + +Data Gathering +============== + +There are two functions for gathering data from UPSTAGE: + +1. :py:func:`upstage_des.data_utils.create_table` + + * Finds all actors and their recording states + * Finds all ``SelfMonitoring<>`` resources that are not attached + to actors. + * Ignores location states by default + * Reports actor name, actor type, state name, state value, and + if the state has an active status. + * If ``skip_locations`` is set to ``False``, then location objects + will go into the state value column. + * Data are in long-form, meaning rows may share a timestamp. + +2. :py:func:`upstage_des.data_utils.create_location_table` + + * Finds all location states on Actors + * Reports location data as individual columns for the dimensions + of the location (XYZ or LLA). + * Reports on active/inactive state data. + * Data are not completely in long-form. XYZ are on a single row, but + rows can have the same timestamp if they are different states. + +Using the example in :doc:`Data Gathering Example `, the +following table (a partial amount shown) would be obtained from the ``create_table`` function: + +.. table:: + + +-----------+-------------------------+-------------+----+-----+-----------------+ + |Entity Name| Entity Type | State Name |Time|Value|Activation Status| + +===========+=========================+=============+====+=====+=================+ + |Ertha |Cashier |items_scanned| 0| 0.0| | + +-----------+-------------------------+-------------+----+-----+-----------------+ + |Ertha |Cashier |items_scanned| 3| -1.0| | + +-----------+-------------------------+-------------+----+-----+-----------------+ + |Ertha |Cashier |cue | 3| 1.0| | + +-----------+-------------------------+-------------+----+-----+-----------------+ + |Ertha |Cashier |cue2 | 3| 11.0| | + +-----------+-------------------------+-------------+----+-----+-----------------+ + |Ertha |Cashier |time_working | 3| 2.9|active | + +-----------+-------------------------+-------------+----+-----+-----------------+ + |Bertha |Cashier |cue | 0| 0.0| | + +-----------+-------------------------+-------------+----+-----+-----------------+ + |Bertha |Cashier |cue2 | 0| 0.0| | + +-----------+-------------------------+-------------+----+-----+-----------------+ + |Bertha |Cashier |time_working | 0| 0.0|inactive | + +-----------+-------------------------+-------------+----+-----+-----------------+ + |Store Test |SelfMonitoringFilterStore|Resource | 0| 0.0| | + +-----------+-------------------------+-------------+----+-----+-----------------+ + +The location table will look like the following table. Now how the active states can be +"activating", "active", or "deactivating". Not shown is the "inactive" value, which +is used for when an active state value is changed, but not because it has been set +to change automatically. + +.. table:: + + +------------+-----------+------------+----+-------+-------+-+-----------------+ + |Entity Name |Entity Type| State Name |Time| X | Y |Z|Activation Status| + +============+===========+============+====+=======+=======+=+=================+ + |Wobbly Wheel|Cart |location | 0| 1.0000| 1.0000|0|activating | + +------------+-----------+------------+----+-------+-------+-+-----------------+ + |Wobbly Wheel|Cart |location | 1| 2.5364| 2.2803|0|active | + +------------+-----------+------------+----+-------+-------+-+-----------------+ + |Wobbly Wheel|Cart |location | 2| 4.0728| 3.5607|0|active | + +------------+-----------+------------+----+-------+-------+-+-----------------+ + |Wobbly Wheel|Cart |location | 3| 5.6093| 4.8411|0|deactivating | + +------------+-----------+------------+----+-------+-------+-+-----------------+ + |Wobbly Wheel|Cart |location_two| 0| 1.0000| 1.0000|0|activating | + +------------+-----------+------------+----+-------+-------+-+-----------------+ + |Wobbly Wheel|Cart |location_two| 1|-0.5051|-0.3170|0|active | + +------------+-----------+------------+----+-------+-------+-+-----------------+ + |Wobbly Wheel|Cart |location_two| 3|-3.5154|-2.9510|0|deactivating | + +------------+-----------+------------+----+-------+-------+-+-----------------+ + +If you were to have ``pandas`` installed, a dataframe could be created with: + +.. code:: python + + import pandas as pd + import upstage_des.api as UP + from upstage_des.data_utils import create_table + + with UP.EnvironmentContext() as env: + ... + env.run() + + table, header = create_table() + df = pd.DataFrame(table, columns=header) + +.. note:: + + The table creation methods must be called within the context, but + the resulting data does not need to stay in the context. + + The exception is that if a state has a value that uses the environment + or the stage, you may see a warning if you try to access attributes or + methods on that object. diff --git a/docs/source/user_guide/tutorials/data_creation_example.rst b/docs/source/user_guide/tutorials/data_creation_example.rst new file mode 100644 index 0000000..d45c514 --- /dev/null +++ b/docs/source/user_guide/tutorials/data_creation_example.rst @@ -0,0 +1,8 @@ +================================== +Data Gathering Example Full Source +================================== + +.. literalinclude:: ../../../../src/upstage_des/test/test_data_reporting.py + + +This file is auto-generated. diff --git a/docs/source/user_guide/tutorials/first_simulation.rst b/docs/source/user_guide/tutorials/first_simulation.rst index 001ca0c..036fb96 100644 --- a/docs/source/user_guide/tutorials/first_simulation.rst +++ b/docs/source/user_guide/tutorials/first_simulation.rst @@ -3,14 +3,16 @@ First UPSTAGE Simulation ======================== .. include:: ../../class_refs.txt -This simulation will demonstrate the primary features of UPSTAGE in a very simple scenario. The goal is demonstrate not just the core UPSTAGE features, but the +This simulation will demonstrate the primary features of UPSTAGE in a very +simple scenario. The goal is demonstrate not just the core UPSTAGE features, but the interaction of UPSTAGE with SimPy. -------- Scenario -------- -A single cashier works at grocery store. They go to the checkout line, scan groceries, take breaks, and come back to the line. +A single cashier works at grocery store. They go to the checkout line, +scan groceries, take breaks, and come back to the line. The code for the full example can be :doc:`found here `. @@ -32,9 +34,11 @@ We prefer this syntax for importing UPSTAGE and SimPy: Define an Actor with State -------------------------- -An UPSTAGE Actor is a container for State, along with methods for modifying the states, for changing tasks, and recording data. +An UPSTAGE Actor is a container for State, along with methods for modifying the states, +for changing tasks, and recording data. -Let's imagine our Cashier has the ability to scan items at a certain speed, and some time until they get a break. We begin by subclassing |Actor| and including two |State| class variables: +Let's imagine our Cashier has the ability to scan items at a certain speed, and some +time until they get a break. We begin by subclassing |Actor| and including two |State| class variables: .. code-block:: python @@ -52,10 +56,12 @@ Let's imagine our Cashier has the ability to scan items at a certain speed, and ) -Our Cashier is very simple, it contains two states that are primarily data containers for attributes of the cashier. This is typical for an UPSTAGE Actor. +Our Cashier is very simple, it contains two states that are primarily data containers +for attributes of the cashier. This is typical for an UPSTAGE Actor. -The ``scan_speed`` state is defined to require a ``float`` type (UPSTAGE will throw an error otherwise), and is ``frozen``, meaning that it cannot be changed once defined. The ``time_until_break`` -state is similar, except that a default value of 120 minutes is supplied. +The ``scan_speed`` state is defined to require a ``float`` type (UPSTAGE will throw +an error otherwise), and is ``frozen``, meaning that it cannot be changed once defined. +The ``time_until_break`` state is similar, except that a default value of 120 minutes is supplied. .. note:: There is no explicit time dimension in upstage_des. The clock units are up to the user, @@ -74,13 +80,17 @@ Then you will later instantiate a cashier with [#f1]_: debug_log=True, ) -Note that the `name` attribute is required for all UPSTAGE Actors. Also, all inputs are keyword-argument only for an Actor. The ``debug_log`` input is ``False`` by default, -and when ``True``, you can call ``cashier.log()`` to retrieve an UPSTAGE-generated log of what the actor has been doing. The same method, when -given a string, will record the message into the log, along with the default logging that UPSTAGE does. +Note that the `name` attribute is required for all UPSTAGE Actors. Also, all inputs are +keyword-argument only for an Actor. The ``debug_log`` input is ``False`` by default, +and when ``True``, you can call ``cashier.log()`` to retrieve an UPSTAGE-generated log +of what the actor has been doing. The same method, when given a string, will record +the message into the log, along with the default logging that UPSTAGE does. -States are just Python descriptors, so you may access them the same as you would any instance attribute: ``cashier.scan_speed```, e.g. +States are just Python descriptors, so you may access them the same as you would any +instance attribute: ``cashier.scan_speed```, e.g. -We want to keep track of the number of items scanned, so let's add a state that records the time at which items are scanned. +We want to keep track of the number of items scanned, so let's add a state that records +the time at which items are scanned. .. code-block:: python @@ -108,7 +118,8 @@ We want to keep track of the number of items scanned, so let's add a state that ) -Note that the keyword-argument ``recording`` has been set to ``True``. Now, whenever that state is modified, the time and value will be recorded. +Note that the keyword-argument ``recording`` has been set to ``True``. Now, +whenever that state is modified, the time and value will be recorded. .. code-block:: python @@ -123,13 +134,18 @@ Note that the keyword-argument ``recording`` has been set to ``True``. Now, when >>> [(0.0, 1), (1.2, 4)] -UPSTAGE creates the recording attribute on the instance with ``__history`` to store the tuples of ``(time, value)`` for the state on all recorded states. This is compatible with +UPSTAGE creates the recording attribute on the instance with ``_state_histories[]`` +to store the tuples of ``(time, value)`` for the state on all recorded states. This is compatible with all states, including Locations, Resources, and states that are lists, tuples, or dicts (UPSTAGE makes deep copies). -Note that now we have created a SimPy ``Environment`` in ``env`` using the |EnvironmentContext| context manager. This gives Actor instances access to the simulation clock (``env.now``). The -environment context and features will be covered more in depth later. +For more information on UPSTAGE's data recording, see :doc:`/user_guide/tutorials/data` -When we run the environment forward and change the ``items_scanned`` state, the value is recorded at the current simulation time. +Note that now we have created a SimPy ``Environment`` in ``env`` using the |EnvironmentContext| +context manager. This gives Actor instances access to the simulation clock (``env.now``). The +environment context and features are covered :doc:`here `. + +When we run the environment forward and change the ``items_scanned`` state, the value is +recorded at the current simulation time. Let's also make an Actor for the checkout lane, so we have a simple location to store customer queueing: @@ -147,12 +163,17 @@ Let's also make an Actor for the checkout lane, so we have a simple location to } ) -Here we use the built-in |ResourceState| to use a |SelfMonitoringStore| as an Actor state. The self-monitoring store is a subclass of the SimPy ``Store`` that records the number of items -in the store whenever there is a get or put. The ``ResourceState`` could accept a default and not require a definition in the instantiation, but here we are demonstrating how to instantiate -a ``ResourceState`` in a way that lets you parameterize the store's values (in this case, the kind and the capacity). Other resources, such as containers, will have capacities and initial values. +Here we use the built-in |ResourceState| to use a |SelfMonitoringStore| as an Actor state. The +self-monitoring store is a subclass of the SimPy ``Store`` that records the number of items +in the store whenever there is a get or put. The ``ResourceState`` could accept a default and +not require a definition in the instantiation, but here we are demonstrating how to instantiate +a ``ResourceState`` in a way that lets you parameterize the store's values (in this case, the +kind and the capacity). Other resources, such as containers, will have capacities and initial values. -Actors also have ``knowledge``, which is a simple dictionary attached to the actor that has an interface through the actor and tasks. This allows actors to hold runtime-dependent information -that isn't tied to a state. Knowledge can be set and accessed with error-throwing checks for its existence, or for checks that it doesn't already have a value. An example is given later. +Actors also have ``knowledge``, which is a simple dictionary attached to the actor that has an +interface through the actor and tasks. This allows actors to hold runtime-dependent information +that isn't tied to a state. Knowledge can be set and accessed with error-throwing checks for +its existence, or for checks that it doesn't already have a value. An example is given later. ---------------------------- Define Tasks for the Cashier @@ -234,23 +255,28 @@ Let's step through the task definitions line-by-line. * Line 5: Task subclasses must implement ``task`` that takes a single keyword argument: ``actor``. -* Line 7-11: Assume the cashier has some "knowledge" about the checkout lane they are going to (the store manager will give this to them). +* Line 7-11: Assume the cashier has some "knowledge" about the checkout lane + they are going to (the store manager will give this to them). * The knowledge has the name "checkout_lane", and we assume it must exist, or else throw an error. -* Line 12: Create a ``Get`` event that waits to get a customer from the lane's ResourceState. Note that we aren't yielding on this event yet. +* Line 12: Create a ``Get`` event that waits to get a customer from the lane's ResourceState. + Note that we aren't yielding on this event yet. * Line 14-18: Get information about the actor's break time. - * We could use ``actor.get_knowledge``, but using the task's method puts extra information into the actor's log, if you have it enabled. + * We could use ``actor.get_knowledge``, but using the task's method puts extra information + into the actor's log, if you have it enabled. -* Line 19-21: Get the time left in the sim until it's a break, and create a simple ``Wait`` event to succeed at that time. +* Line 19-21: Get the time left in the sim until it's a break, and create a simple ``Wait`` + event to succeed at that time. * Line 23: Yield an ``Any`` event, which succeeds when the first of its sub-events succeeds. * Line 25: Test if the customer event succeeded first with the ``Event`` method ``is_complete``. -* Line 26-27: If it did succeed, call ``get_value`` on the ``Get`` event to get customer information and add it to our knowledge. +* Line 26-27: If it did succeed, call ``get_value`` on the ``Get`` event to get customer information + and add it to our knowledge. * Here we just treat the customer information as an integer number of items. It could be anything. @@ -268,7 +294,8 @@ Let's step through the task definitions line-by-line. * Line 37-41: Retrieve the knowledge we set in the previous task. - * Notice how knowledge lets us be flexible about what our Actors can do, and how ``must_exist`` will help us ensure our tasks are doing the right thing. + * Notice how knowledge lets us be flexible about what our Actors can do, and how ``must_exist`` will + help us ensure our tasks are doing the right thing. * Line 43-47: Activate a linear changing state, which increases its value according to ``rate`` as the simulation runs. @@ -280,9 +307,11 @@ Let's step through the task definitions line-by-line. * Line 53: Assume some follow-on wait for customer payment. -This is the foundation of how UPSTAGE manages behaviors. The simulation designer creates ``Tasks`` that can be chained together to perform actions, modify data, and make decisions. +This is the foundation of how UPSTAGE manages behaviors. The simulation designer creates ``Tasks`` that +can be chained together to perform actions, modify data, and make decisions. -There is one other kind of Task, a |DecisionTask|, which does not consume the environment clock, and will not yield any events [#f2]_. +There is one other kind of Task, a |DecisionTask|, which does not consume the environment clock, +and will not yield any events [#f2]_. .. code-block:: python @@ -298,16 +327,19 @@ There is one other kind of Task, a |DecisionTask|, which does not consume the en self.set_actor_task_queue(actor, ["ShortBreak"]) -That task has the ``make_decision`` method that needs to be sublcassed. The purpose of a `DecisionTask` is to set and clear actor knowledge, and modify the task queue without consuming the clock. -It has additional benefits for rehearsal, which will be covered later. +That task has the ``make_decision`` method that needs to be sublcassed. The purpose of a +`DecisionTask` is to set and clear actor knowledge, and modify the task queue without +consuming the clock. It has additional benefits for rehearsal, which will be covered later. A note on UPSTAGE Events ------------------------ -UPSTAGE Events are custom wrappers around SimPy events that allow for accessing data about that event, handling the ``Task`` internal event loop, and for rehearsal. +UPSTAGE Events are custom wrappers around SimPy events that allow for accessing data about +that event, handling the ``Task`` internal event loop, and for rehearsal. -All ``Task`` s should yield UPSTAGE events, with one exception. A SimPy ``Process`` can be yielded out as well, but this will warn the user, and is generally not recommended. +All ``Task`` s should yield UPSTAGE events, with one exception. A SimPy ``Process`` can be +yielded out as well, but this will warn the user, and is generally not recommended. The event types are: @@ -334,7 +366,8 @@ The event types are: Define a TaskNetwork for the Cashier ------------------------------------ -The flow of Tasks is controlled by a TaskNetwork, and the setting of the queue within tasks. A Task Network is defined by the nodes and the links: +The flow of Tasks is controlled by a TaskNetwork, and the setting of the queue within +tasks. A Task Network is defined by the nodes and the links: .. code-block:: python @@ -364,13 +397,17 @@ The flow of Tasks is controlled by a TaskNetwork, and the setting of the queue w task_links=task_links, ) -The task classes are given names, and those strings are used to define the default and allowable task ordering. The task ordering need to know the default task (can be None) and the allowed tasks. -Allowed tasks must be supplied. If no default is given, an error will be thrown if no task ordering is given when a new task is selected. If the default or the set task queue violates the -allowed rule, an error will be thrown. +The task classes are given names, and those strings are used to define the default and +allowable task ordering. The task ordering need to know the default task (can be None) +and the allowed tasks. Allowed tasks must be supplied. If no default is given, an error +will be thrown if no task ordering is given when a new task is selected. If the default +or the set task queue violates the allowed rule, an error will be thrown. -The task network forms the backbone of flexible behavior definitions, while a ``DecisionTask`` helps control the path through the network. +The task network forms the backbone of flexible behavior definitions, while a ``DecisionTask`` +helps control the path through the network. -The ``cashier_task_network`` is a factory that creates network instances from the definition that actors can use (one per actor/per network). +The ``cashier_task_network`` is a factory that creates network instances from the definition +that actors can use (one per actor/per network). To start a task network on an actor with the factory: @@ -393,24 +430,30 @@ You can either start a loop on a single task, or define an initial queue through A note on TaskNetworkFactory ---------------------------- -The :py:class:`~upstage_des.task_network.TaskNetworkFactory` class has some convience methods for creating factories from typical use cases: - -#. :py:meth:`~upstage_des.task_network.TaskNetworkFactory.from_single_looping`: From a single task, make a network that loops itself. +The :py:class:`~upstage_des.task_network.TaskNetworkFactory` class has some convience methods +for creating factories from typical use cases: - * Useful for a Singleton task that, for example, receives communications and farms them out or manages other task networks. +#. :py:meth:`~upstage_des.task_network.TaskNetworkFactory.from_single_looping`: From a single + task, make a network that loops itself. -#. :py:meth:`~upstage_des.task_network.TaskNetworkFactory.from_single_terminating`: A network that does one task, then freezes for the rest of the simulation. + * Useful for a Singleton task that, for example, receives communications and farms them out + or manages other task networks. -#. :py:meth:`~upstage_des.task_network.TaskNetworkFactory.from_ordered_looping`: A series of tasks with no branching that loops. +#. :py:meth:`~upstage_des.task_network.TaskNetworkFactory.from_single_terminating`: A network + that does one task, then freezes for the rest of the simulation. -#. :py:meth:`~upstage_des.task_network.TaskNetworkFactory.from_single_looping`: A series of tasks with no branching that terminates at the end. +#. :py:meth:`~upstage_des.task_network.TaskNetworkFactory.from_ordered_looping`: A series of + tasks with no branching that loops. +#. :py:meth:`~upstage_des.task_network.TaskNetworkFactory.from_single_looping`: A series of tasks + with no branching that terminates at the end. -------------------- Setting up Customers -------------------- -To complete the simulation, we need to make customers arrive at the checkout lanes. This can be done using a standard SimPy process: +To complete the simulation, we need to make customers arrive at the checkout lanes. This can +be done using a standard SimPy process: .. code-block:: python @@ -518,7 +561,7 @@ Since only one cashier is assigned, you can examine the backlog on the lanes (an >>> (1136.5736387094469, 8), >>> (1188.3694502822516, 9)] - print(cashier._items_scanned_history) + print(cashier._state_histories["items_scanned"]) >>> ... >>> (683.5134932373091, 15), >>> (683.6134932373092, 16), @@ -532,9 +575,11 @@ Since only one cashier is assigned, you can examine the backlog on the lanes (an >>> ... -Your run may be different, due to the calls to ``stage.random`` (a passthrough for ``random.Random()``). See :doc:`Random Numbers ` for more. +Your run may be different, due to the calls to ``stage.random`` (a passthrough for ``random.Random()``). +See :doc:`Random Numbers ` for more. -Notice how lane 1 takes customers right away, but lane 2 stacks up. Also notice how the ``SelfMonitoringStore`` creates the ``._quantities`` datatype that shows the time history of number of +Notice how lane 1 takes customers right away, but lane 2 stacks up. Also notice how the +``SelfMonitoringStore`` creates the ``._quantities`` datatype that shows the time history of number of items in the store. If it was a Container, instead of a Store, it would record the level. .. [#f1] You can run this now and ignore the warning about an environment.