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 = self.from_store(products)
self.hold(sim.Uniform(0, 2))
class Producer(sim.Component):
def process(self):
while True:
self.hold(1)
product = sim.Component("product.")
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 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)
self.from_store and self.to_store explained
When we ask for an item (component) from a store, we can say
item = self.from_store(store0)
It is also possible to ‘connect’ to several stores, like
item = 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
self.to_store(store0, item)
and for multiple stores
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:
self.to_store(store0, item)
Or
if store.available_quantity() > 0:
item.enter(store0)
else:
self.to_store(store, item)
Working with multiple stores
Multiple stores with to_store
If we specify more than one store in the call to Component.to_store
, like
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
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 self.from_store()
or
self.to_store
the order with which the calls 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 = 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 = 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 = 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
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
.