Simple Simulations

A Regular Schedule

T_WORK = 50
T_BREAK = 10


def worker(env):
    while True:
        print(f"start work at {env.now}")
        yield env.timeout(T_WORK)
        print(f"start break at {env.now}")
        yield env.timeout(T_BREAK)
T_MORNING = 4 * 60

if __name__ == "__main__":
    env = Environment()
    proc = worker(env)
    env.process(proc)
    env.run(until=T_MORNING)
    print(f"done at {env.now}")
start work at 0
start break at 50
start work at 60
start break at 110
start work at 120
start break at 170
start work at 180
start break at 230
done at 240

Introducing Randomness

T_MIN_WORK = 10
T_MAX_WORK = 50


def rand_work():
    return random.uniform(T_MIN_WORK, T_MAX_WORK)


def worker(env):
    while True:
        print(f"start work at {env.now}")
        yield env.timeout(rand_work())          # changed
        print(f"start break at {env.now}")
        yield env.timeout(T_BREAK)
start work at 0
start break at 26.67250326533211
start work at 36.67250326533211
start break at 54.79344793494629
start work at 64.7934479349463
start break at 104.87029408376178
start work at 114.87029408376178
start break at 136.94211951464072
start work at 146.94211951464072
start break at 193.03234415437063
start work at 203.03234415437063
done at 240

Monitoring

def worker(env, log):
    while True:
        log.append({"event": "start", "time": env.rnow})
        yield env.timeout(rand_work())
        log.append({"event": "end", "time": env.rnow})
        yield env.timeout(T_BREAK)
SEED = 12345

def main():
    seed = int(sys.argv[1]) if len(sys.argv) > 1 else SEED
    random.seed(seed)
    env = Environment()
    log = []
    proc = worker(env, log)
    env.process(proc)
    env.run(until=T_MORNING)
    json.dump(log, sys.stdout, indent=2)
[
  {"event": "start", "time": 0},
  {"event": "end", "time": 26.665},
  …more events…
  {"event": "end", "time": 237.149}
]

Visualization

import json
import plotly.express as px
import polars as pl
import sys

df = pl.from_dicts(json.load(sys.stdin)).with_columns(
    pl.when(pl.col("event") == "start").then(1).otherwise(-1).alias("delta")
)
events = df.with_columns(
    pl.col("delta").cum_sum().alias("state")
)

fig = px.line(events, x="time", y="state", line_shape="hv")
fig.update_layout(margin={"l": 0, "r": 0, "t": 0, "b": 0}).update_yaxes(tickvals=[0, 1])
fig.write_image(sys.argv[1])
time-series plot of work

Managers and Programmers

T_JOB_ARRIVAL = (20, 30)
T_WORK = (10, 50)
PREC = 3

def rt(env):
    return round(env.now, PREC)

def rand_job_arrival():
    return random.uniform(*T_JOB_ARRIVAL)

def rand_work():
    return random.uniform(*T_WORK)
def manager(env, queue, log):
    job_id = count()
    while True:
        log.append({"time": rt(env), "id": "manager", "event": "create job"})
        yield queue.put(next(job_id))
        yield env.timeout(rand_job_arrival())
def programmer(env, queue, log):
    while True:
        log.append({"time": rt(env), "id": "worker", "event": "start wait"})
        job = yield queue.get()
        log.append({"time": rt(env), "id": "worker", "event": "start work"})
        yield env.timeout(rand_work())
        log.append({"time": rt(env), "id": "worker", "event": "end work"})
T_SIM = 100
SEED = 12345

def main():
    seed = int(sys.argv[1]) if len(sys.argv) > 1 else SEED
    random.seed(seed)

    env = Environment()
    queue = Store(env)
    log = []

    env.process(manager(env, queue, log))
    env.process(programmer(env, queue, log))
    env.run(until=T_SIM)

    json.dump(log, sys.stdout, indent=2)
[
  {"time": 0, "id": "manager", "event": "create job"},
  {"time": 0, "id": "worker", "event": "start wait"},
  {"time": 0, "id": "worker", "event": "start work"},
  {"time": 10.407, "id": "worker", "event": "end work"},
  {"time": 10.407, "id": "worker", "event": "start wait"},
  …more events…
  {"time": 92.57, "id": "worker", "event": "start wait"}
]

Interesting Questions

class Job:
    _id = count()
    _log = []

    def __init__(self, env):
        self.env = env
        self.id = next(Job._id)
        self.log("created")

    def log(self, message):
        Job._log.append({"time": rt(self.env), "id": self.id, "event": message})
def manager(env, queue):
    while True:
        yield queue.put(Job())
        yield env.timeout(rand_job_arrival())


def programmer(env, queue):
    while True:
        job = yield queue.get()
        job.log("start")
        yield env.timeout(rand_work())
        job.log("end")
def main():
    …as before…
    params = {
        "seed": seed,
        "t_sim": t_sim,
        "t_job_arrival": T_JOB_ARRIVAL,
        "t_work": T_WORK,
    }
    result = {
        "params": params,
        "tasks": Job._log,
    }
    json.dump(result, sys.stdout, indent=2)
id created start end
0 0.0 0.0 10.407
1 18.332 18.332 40.278
2 44.837 44.837 62.583
3 62.205 62.583 79.05
4 83.525 83.525 null
5 96.01 null null
metric value
throughput 0.040
delay 16.736
utilization 0.830
duration throughput delay utilization
100 0.040 16.736 0.830
1000 0.038 76.904 0.961
10000 0.034 1537.582 0.996
100000 0.033 16412.712 1.000