Store

Stores are a powerful way of process interaction.

A store is actually a queue where items (components) can be stored. The store can have a limited or unlimited capacity.

Processes can try and get an item (component) from a store. If there’s one available, it will receive that.

The item received can be retrieved with self.from_store_item(). But if no item (component) is available now, the process will wait with the status requesting.
Once the from_store request can be honoured, the process resumes and the item can be retrieved.

A very simple example:

# demo store
import salabim as sim


class Consumer(sim.Component):
   def process(self):
      while True:
            product = yield self.from_store(products)
            yield self.hold(sim.Uniform(0, 2))


class Producer(sim.Component):
   def process(self):
      while True:
            yield self.hold(1)
            product = sim.Component("product.")
            yield self.to_store(products, product)


env = sim.Environment(trace=False)

products = sim.Store("products")

consumer = Consumer()
producer = Producer()

env.run(100)

producer.status.print_histogram(values=True)
consumer.status.print_histogram(values=True)
products.length.print_histogram()

In this example, there’s a producer which can produces 1 product per time unit, but only as long as it can place a product in the products store. As the store is not limited here, it will indeed produce a product every time unit. a product is generated ever 1 time unit. This product places itself in the products store, which has unlimited capacity.

The Consumer process tries constantly to get products out of the products store. But, when nothing is available it will wait (requesting).

That the producer never has to wait can be seen here

Histogram of producer.0.status
duration           100

value                     duration     %
scheduled                  100     100   ******************************

But the consumer has to wait for products (state=requesting) sometimes

Histogram of consumer.0.status
duration           100

value                     duration     %
requesting                   1.770   1.8
scheduled                   98.230  98.2 *****************************

The number of products in the products store is

Histogram of Length of products
                        all    excl.zero         zero
-------------- ------------ ------------ ------------
duration            100           86.265       13.735
mean                  2.106        2.441
std.deviation         1.435        1.253

minimum               0            1
median                2            2
90% percentile        4            4
95% percentile        5            5
maximum               6            6

         <=      duration     %  cum%
      0            13.735  13.7  13.7 ****|
      1            25.236  25.2  39.0 *******    |
      2            23.399  23.4  62.4 *******           |
      3            17.671  17.7  80.0 *****                   |
      4            14.792  14.8  94.8 ****                        |
      5             4.644   4.6  99.5 *                            |
      6             0.523   0.5 100.0                              |
        inf         0       0   100.0                              |

If we make give the products store a limited capacity of 3 by saying

products = sim.Store("products", capacity=3)

We see different results

Histogram of producer.0.status
duration           100

value                     duration     %
requesting                   2.523   2.5
scheduled                   97.477  97.5 *****************************

Histogram of consumer.0.status
duration           100

value                     duration     %
requesting                   4.293   4.3 *
scheduled                   95.707  95.7 ****************************

Histogram of Length of products
                        all    excl.zero         zero
-------------- ------------ ------------ ------------
duration            100           77.784       22.216
mean                  1.419        1.825
std.deviation         1.032        0.793

minimum               0            1
median                1            2
90% percentile        3            3
95% percentile        3            3
maximum               3            3

         <=      duration     %  cum%
      0            22.216  22.2  22.2 ******|
      1            32.485  32.5  54.7 *********       |
      2            26.441  26.4  81.1 *******                 |
      3            18.858  18.9 100   *****                         |
        inf         0       0   100                                 |

This is maybe better illustrated with an animation (with slightly different parameters)

_images/store1.gif

yield self.from_store and yield self.to_store explained

When we ask for an item (component) from a store, we can say

item = yield self.from_store(store0)

It is also possible to ‘connect’ to several stores, like

item = yield self.from_store((store0, store1))

We then can also ask from which store the item came from

store = self.from_store_store()

And it always possible to get the ‘from’ item later with

item = self.from_store_item()

For to_store, we can say

yield self.to_store(store0, item)

and for multiple stores

yield self.to_store((store0, store1), item)

If we would like to know to which store the item went, we can say

store =  self.to_store_store()

Alternative (faster) way to do to_store

If we now in advance that there’s enough room in a store to accomodate a to_store request, we can also just say

item.enter(store0)

, but (unless we have an unlimited capacity store), we have to check whether that’s possible at all.

This can be done with

try:
    item.enter(store0)
except sim.QueueFullError:
    yield self.to_store(store0, item)

Or

if store.available_quantity() > 0:
   item.enter(store0)
else:
   yield self.to_store(store, item)

Working with multiple stores

Both Component.to_store and Component.from_store support multiple stores.

Multiple stores with to_store

If we specify more than one store in the call to Component.to_store, like

yield self.to_store((store0, store1), item)

, salabim always tries to honour the request starting with the first store, then the second, etc.

So, if store0 is already at its capacity, store1 will be tried here.

After the request is honoured, the store where the item (component) went can be queried with self.to_store_store()

Multiple stores with from_store

If we specify more than one store in the call to Component.from_store, like

yield from.to_store((store0, store1))

, salabim always tries to get an item (component) from the first store, then the second, etc.

So, if store0 is empty, store1 will be tried here.

After the request is honoured, the item (component) may be retrieved with self.store_item() and the store where the item (component) came from can be queried with self.from_store_store()

Multiple components requesting from to the same store

When there is more than one component requesting from a store with yield self.from_store() or yield self.to_store the order with which the yields were given determines the order with which the requests will be honoured.

Filtering items

It is possible to retrieve only certain items (components) with Component.from_store.

This is done by specifying a filter function that will be applied to the components in the store’s contents queue. The filter function should take one parameter, item (component), and return either True if the item may be used or False, if not.

So,

item = yield self.from_store(store0, filter=lambda item: 10 <= item.id <=20)

Now, a component with an id of 5 will never be selected, whereas a component with an id of 15 is a candidate.

It is possible to change the filter dynamically, with Component.filter.
Note that this can never be done from within the process, because the component is requesting!

So if we have something like

class Sink(sim.Component):
   def process(self):
      while True:
         item = yield self.from_store(store0, filter=lambda item: item.color == "red")
         ...

sink = Sink()

, another process can change the filter

sink.filter = lambda item.color in ("red, "blue")

, which will now also let blue items in.

If the filter function relies on something external, like temperature

item = yield self.from_store(store0, filter=lambda item: env.temperature >= item.threshold)

and the temperature changes, this might not lead to immediate action.

In that case, after the temperature change, the store has to be rescanned

store0.rescan()

Using an alternative order for from_store

Normally, from_store requests are handled in the order in which they entered the store (taken into account the the priority).

Sometimes, it might be useful to search in another order. Therefore, the key parameter of from_store can be used.

This parameter whould be a function (most likely a lambda) that gets one parameter (the component) and should return a key value (usually a number or string). Requesting an item from the store will then return the item with the lowest key value (observing the filter, if any).

Notice that this requires traversing the whole store and therefore might not be less efficient.

Priorities within the contents of a store

Normally, when you request to put an item (component) into a store, the priority will be 0. But with the priority parameter you can force an item (component) to be placed according to the given priority

yield self.to_store(store0, item, priority=-1)

will place the item at the front of the store0’s contents, provided there are no other items with a priority <= -1.

It is possible and allowed to change the priority of items in the contents of a store. E.g.

item.priority(store, -1)

Changing the capacity of a store

The method Store.set_capacity()` can be used to set the capacity of a store

store0 = sim.Store("store0", capacity=5)
print(f"Capacity of store0 before={store0.capacity()")
store0.set_capacity(6)
print(f"Capacity of store0 after={store0.capacity()")

This will print

Capacity of store0 before=5
Capacity of store0 before=6

Salabim will automaticaly rescan the store upon a change in the capacity, if required.

Note

If the capacity is decreased, it is possible that the length of the contents queue exceeds the capacity. That’s not handled as an exception! This is similar to Queue behaviour.

The current capacity of a store can be retrieved with store.capacity.value.