One counter serves five customers, and a customer who waits longer than their patience goes away. This is SimPy's Bank Renege example, written in desflow.
Before You Start
SimPy's example has one counter and 5 customers. The first customer arrives at time 0 and the others at exponential gaps with a mean of 10. Each customer has a patience drawn between 1 and 3, and goes away (reneges) if the counter is not free by then. A customer who reaches the counter is served for an exponential time with a mean of 12. SimPy counts no unit, and desflow reads each unit as one second.
The example needs only desflow, which each Quick Start in Run It installs, and no other service. desflow examples get simpy-bank-renege --api yaml downloads it, as Get an Example shows. SimPy Examples says how these pages read SimPy's times and numbers. SimPy's own page for the example is on its documentation site.
Steps
Customers Arrive
SimPy's source starts a customer at time 0 and then one after each gap.
The counter is a resource with one unit. The arrival customers has first_at: 0, so the first customer comes at time 0, and max_events: 5, so only five come. Its key names them Customer00 to Customer04.
sampler=Sampler(seed=42)gap=DistributionConfig(dist="exponential",rate=0.1)gap.stream="arrival.customers"# the sequence the blueprint's arrival draws frompatiences=sampler.generator("tasks.customer.patience")services=sampler.generator("tasks.customer.service_time")counter=simpy.Resource(env,capacity=1)
defcustomer(env,name,counter,time_in_bank):"""Customer arrives, is served and leaves."""arrive=env.nowprint(f'{arrive:7.4f}{name}: Here I am')withcounter.request()asreq:patience=random.uniform(MIN_PATIENCE,MAX_PATIENCE)# Wait for the counter or abort at the end of our tetherresults=yieldreq|env.timeout(patience)wait=env.now-arriveifreqinresults:# We got to the counterprint(f'{env.now:7.4f}{name}: Waited {wait:6.3f}')tib=random.expovariate(1.0/time_in_bank)yieldenv.timeout(tib)print(f'{env.now:7.4f}{name}: Finished')else:# We renegedprint(f'{env.now:7.4f}{name}: RENEGED after {wait:6.3f}')
patience is the longest wait for the counter, drawn uniform from 1 to 3 seconds for each customer. A customer whose patience ends first publishes reneged and leaves. The service time is exponential with a mean of 12 seconds, written as the rate 1/12.
@app.task(name="customer",resource_id="counter",# Waits for the counter at most this long, then goes awaypatience=generator({"gen":"uniform","low":1,"high":3}),service_time=generator({"gen":"sample","dist":"exponential","rate":0.08333333333333333}),)defcustomer(task_id):"""Is served at the counter."""return{"event_type":"finished"}
The customer function is SimPy's, with req | env.timeout(patience). It publishes reneged when req is not in the result.
defcustomer(name,number):defpublish(value):env.publish_event(name,value,task="customer",instance=number)withcounter.request()asreq:publish({"status":"queued"})patience=float(patiences.uniform(1,3))# Waits for the counter at most `patience`, then goes awayresults=yieldreq|env.timeout(patience)ifreqinresults:publish({"status":"started"})yieldenv.timeout(float(services.exponential(12.0)))publish({"event_type":"finished"})else:publish({"status":"reneged"})
Run It
desflow run loads the blueprint and runs it unpaced, because it sets factor: 0. Each record is printed to the terminal.
Quick Start
desflow examples get downloads the example at the release you install, into a folder of its own. Pick uv or pip:
# 1. Installpipinstalldesflow
# 2. Download the exampledesflowexamplesgetsimpy-bank-renege--apilow-level
cdsimpy-bank-renege/low-level
# 3. Run itpythonbank_renege_example.py
Check the Result
Three of the five customers renege: Customer01, Customer02 and Customer04. Customer00 is served from time 0 and finishes at 24.981 seconds. Customer03 reaches the counter at 29.312 seconds and finishes at 37.245 seconds. The run has no end time, so it stops when the last customer leaves.
SimPy's own output, below, has customers who are served and customers who renege too. desflow draws each random value from its own stream, named by the place where the value is used, such as Bank.service.patience. A seed of 42 therefore gives different numbers from SimPy's random.seed(42). The system is the same. The figures are not.
Bank renege
0.0000 Customer00: Here I am
0.0000 Customer00: Waited 0.000
3.8595 Customer00: Finished
10.2006 Customer01: Here I am
10.2006 Customer01: Waited 0.000
12.7265 Customer02: Here I am
13.9003 Customer02: RENEGED after 1.174
23.7507 Customer01: Finished
34.9993 Customer03: Here I am
34.9993 Customer03: Waited 0.000
37.9599 Customer03: Finished
40.4798 Customer04: Here I am
40.4798 Customer04: Waited 0.000
43.1401 Customer04: Finished
The three versions in desflow print the same records, which a test checks by running each version unpaced. Each record has sim_ts, the simulation time, and a key for the entity it belongs to. The statistics and the parameters that a live run can change are in the same stream, which SimPy's file does not have.
# Bank Renege: SimPy's bank_renege.py example, in YAML.## The YAML version of bank_renege_example.py, which --api builder downloads. One# counter serves five customers. The first arrives at 0 and the others about 10# seconds apart (exponential gaps). Each customer waits for the counter for a patience# drawn between 1 and 3 seconds, and goes away (reneged) if the counter is not free by# then. A customer at the counter is served for about 12 seconds (exponential).## SimPy's example counts seconds, as desflow does. Each value is drawn from desflow's# random sequence for its place in the file, so the numbers differ from those SimPy# prints.## Needs no containers. Runs unpaced and ends when the last customer leaves.# Run it from its folder with: desflow run bank_renege.yamlsimulation:{sim_id:Bank, factor:0, random_seed:42}egress:-{type:Console}resources:counter:{current_cap:1}arrivals:customers:dist:exponentialrate:0.1first_at:0max_events:5key:{gen:sequence, format:"Customer{:02d}"}spawn:customertasks:customer:resource:counterpatience:{gen:uniform, low:1, high:3}service_time:{gen:sample, dist:exponential, rate:0.08333333333333333}payload:{event_type:finished}
"""Bank Renege: SimPy's bank_renege.py example, builder API.One counter serves five customers. The first arrives at 0 and the others about 10seconds apart (exponential gaps). Each customer waits for the counter for a patiencedrawn between 1 and 3 seconds, and goes away (reneged) if the counter is not free bythen. A customer at the counter is served for about 12 seconds (exponential).SimPy's example counts seconds, as desflow does. Each value is drawn from desflow'srandom sequence for its place in the model, so the numbers differ from those SimPyprints.Needs no containers. Runs unpaced and ends when the last customer leaves."""importloggingfromdesflowimportConsoleEgress,SimulationContextfromdesflow.valuesimportgeneratordefbuild()->SimulationContext:"""Builds the bank."""app=SimulationContext(sim_id="Bank",factor=0,random_seed=42)app.add_egress(ConsoleEgress())app.add_resource("counter",current_cap=1)app.add_arrival("customers","exponential",rate=0.1,first_at=0,max_events=5,key=lambdan:"Customer{:02d}".format(n-1),spawn="customer",)@app.task(name="customer",resource_id="counter",# Waits for the counter at most this long, then goes awaypatience=generator({"gen":"uniform","low":1,"high":3}),service_time=generator({"gen":"sample","dist":"exponential","rate":0.08333333333333333}),)defcustomer(task_id):"""Is served at the counter."""return{"event_type":"finished"}returnappapp=build()defrun(model:SimulationContext=app)->None:"""Runs until the last customer leaves."""model.run()if__name__=="__main__":logging.basicConfig(level=logging.INFO,format="%(levelname)s%(message)s")run()
The script wires the model by hand on DynamicRealtimeEnvironment, with SimPy's own processes.
"""Bank Renege: SimPy's bank_renege.py example, low-level API.The low-level version of `builder/simpy/bank_renege_example.py`: SimPy's own processeson a `DynamicRealtimeEnvironment`. They publish with `publish_event` the records thatthe YAML and builder versions publish: each customer's `queued` and `started`, its`reneged` or `finished`, and the number of the task's instance. One counter servesfive customers. The first arrives at 0 and the others about 10 seconds apart. Eachcustomer waits for the counter for a patience between 1 and 3 seconds, and goes away(reneged) if the counter is not free by then. A customer at the counter is served forabout 12 seconds.SimPy's example counts seconds, as desflow does. Each value is drawn from desflow's`Sampler`, from the random sequence the YAML and builder versions use for the sameplace, so the three versions publish the same records.Needs no containers. Runs unpaced and ends when the last customer leaves."""importloggingimportsimpyfromdesflowimport(ConsoleEgress,DistributionConfig,DynamicRealtimeEnvironment,Sampler,)NEW_CUSTOMERS=5UNTIL=None# the run ends when no event is leftdefbuild(env:DynamicRealtimeEnvironment)->None:"""Starts the customers' source on `env`."""sampler=Sampler(seed=42)gap=DistributionConfig(dist="exponential",rate=0.1)gap.stream="arrival.customers"# the sequence the blueprint's arrival draws frompatiences=sampler.generator("tasks.customer.patience")services=sampler.generator("tasks.customer.service_time")counter=simpy.Resource(env,capacity=1)defcustomer(name,number):defpublish(value):env.publish_event(name,value,task="customer",instance=number)withcounter.request()asreq:publish({"status":"queued"})patience=float(patiences.uniform(1,3))# Waits for the counter at most `patience`, then goes awayresults=yieldreq|env.timeout(patience)ifreqinresults:publish({"status":"started"})yieldenv.timeout(float(services.exponential(12.0)))publish({"event_type":"finished"})else:publish({"status":"reneged"})defsource():foriinrange(NEW_CUSTOMERS):env.process(customer(f"Customer{i:02d}",i))yieldenv.timeout(sampler.sample(gap))env.process(source())defrun()->None:"""Runs until the last customer leaves."""env=DynamicRealtimeEnvironment(factor=0)env.setup_egress([ConsoleEgress()])build(env)try:env.run(until=UNTIL)finally:env.teardown()if__name__=="__main__":logging.basicConfig(level=logging.INFO,format="%(levelname)s%(message)s")run()
This is SimPy 4.1.2's bank_renege.py, with SimPy's licence notice added at the top and comment lines that mark the pieces shown above. Run with Python, it prints the output shown under Check the Result.
# SimPy 4.1.2, docs/examples/code/bank_renege.py, under SimPy's licence (LICENSE.rst):## The MIT License (MIT)## Copyright (c) 2013 Ontje Lünsdorf and Stefan Scherfke (also see AUTHORS.txt)## Permission is hereby granted, free of charge, to any person obtaining a copy of# this software and associated documentation files (the "Software"), to deal in# the Software without restriction, including without limitation the rights to# use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of# the Software, and to permit persons to whom the Software is furnished to do so,# subject to the following conditions:## The above copyright notice and this permission notice shall be included in all# copies or substantial portions of the Software.## THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS# FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR# COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER# IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN# CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE."""Bank renege exampleCovers:- Resources: Resource- Condition eventsScenario: A counter with a random service time and customers who renege. Based on the program bank08.py from TheBank tutorial of SimPy 2. (KGM)"""importrandomimportsimpyRANDOM_SEED=42NEW_CUSTOMERS=5# Total number of customersINTERVAL_CUSTOMERS=10.0# Generate new customers roughly every x secondsMIN_PATIENCE=1# Min. customer patienceMAX_PATIENCE=3# Max. customer patiencedefsource(env,number,interval,counter):"""Source generates customers randomly"""foriinrange(number):c=customer(env,f'Customer{i:02d}',counter,time_in_bank=12.0)env.process(c)t=random.expovariate(1.0/interval)yieldenv.timeout(t)defcustomer(env,name,counter,time_in_bank):"""Customer arrives, is served and leaves."""arrive=env.nowprint(f'{arrive:7.4f}{name}: Here I am')withcounter.request()asreq:patience=random.uniform(MIN_PATIENCE,MAX_PATIENCE)# Wait for the counter or abort at the end of our tetherresults=yieldreq|env.timeout(patience)wait=env.now-arriveifreqinresults:# We got to the counterprint(f'{env.now:7.4f}{name}: Waited {wait:6.3f}')tib=random.expovariate(1.0/time_in_bank)yieldenv.timeout(tib)print(f'{env.now:7.4f}{name}: Finished')else:# We renegedprint(f'{env.now:7.4f}{name}: RENEGED after {wait:6.3f}')# Setup and start the simulationprint('Bank renege')random.seed(RANDOM_SEED)env=simpy.Environment()# Start processes and runcounter=simpy.Resource(env,capacity=1)env.process(source(env,NEW_CUSTOMERS,INTERVAL_CUSTOMERS,counter))env.run()