run multiple simulations for different sets of parameters

This commit is contained in:
2024-08-31 22:12:33 +02:00
parent 4a5ced1172
commit 6ea763f914
4 changed files with 144 additions and 58 deletions

33
poetry.lock generated
View File

@@ -1,5 +1,16 @@
# This file is automatically @generated by Poetry 1.8.3 and should not be changed by hand. # This file is automatically @generated by Poetry 1.8.3 and should not be changed by hand.
[[package]]
name = "colorama"
version = "0.4.6"
description = "Cross-platform colored terminal text."
optional = false
python-versions = "!=3.0.*,!=3.1.*,!=3.2.*,!=3.3.*,!=3.4.*,!=3.5.*,!=3.6.*,>=2.7"
files = [
{file = "colorama-0.4.6-py2.py3-none-any.whl", hash = "sha256:4f1d9991f5acc0ca119f9d443620b77f9d6b33703e51011c16baf57afb285fc6"},
{file = "colorama-0.4.6.tar.gz", hash = "sha256:08695f5cb7ed6e0531a20572697297273c47b8cae5a63ffc6d6ed5c201be6e44"},
]
[[package]] [[package]]
name = "numpy" name = "numpy"
version = "2.0.1" version = "2.0.1"
@@ -95,7 +106,27 @@ timezone = ["backports-zoneinfo", "tzdata"]
xlsx2csv = ["xlsx2csv (>=0.8.0)"] xlsx2csv = ["xlsx2csv (>=0.8.0)"]
xlsxwriter = ["xlsxwriter"] xlsxwriter = ["xlsxwriter"]
[[package]]
name = "tqdm"
version = "4.66.5"
description = "Fast, Extensible Progress Meter"
optional = false
python-versions = ">=3.7"
files = [
{file = "tqdm-4.66.5-py3-none-any.whl", hash = "sha256:90279a3770753eafc9194a0364852159802111925aa30eb3f9d85b0e805ac7cd"},
{file = "tqdm-4.66.5.tar.gz", hash = "sha256:e1020aef2e5096702d8a025ac7d16b1577279c9d63f8375b63083e9a5f0fcbad"},
]
[package.dependencies]
colorama = {version = "*", markers = "platform_system == \"Windows\""}
[package.extras]
dev = ["pytest (>=6)", "pytest-cov", "pytest-timeout", "pytest-xdist"]
notebook = ["ipywidgets (>=6)"]
slack = ["slack-sdk"]
telegram = ["requests"]
[metadata] [metadata]
lock-version = "2.0" lock-version = "2.0"
python-versions = "^3.10" python-versions = "^3.10"
content-hash = "3b5f1c763a56f03ec547b2513ce2ef1a9935ac6df8a8caca8288795aee44cbbf" content-hash = "e518e9e57f22023c75b734bc9fdfb85e25b6b937f87bbf5a26c1b31ff370b15f"

View File

@@ -9,6 +9,7 @@ readme = "README.md"
python = "^3.10" python = "^3.10"
numpy = "^2.0.1" numpy = "^2.0.1"
polars = "^1.4.1" polars = "^1.4.1"
tqdm = "^4.66.5"
[build-system] [build-system]

View File

@@ -1,25 +1,27 @@
from concurrent.futures import ProcessPoolExecutor, as_completed
from dataclasses import dataclass from dataclasses import dataclass
import numpy as np import numpy as np
import polars as pl import polars as pl
from tqdm import tqdm
from simulation.lib import Process, Simulation from simulation.lib import Process, Simulation
@dataclass def run_simulation(n_servers=2, lam=40):
class Customer:
@dataclass
class Customer:
arrived_at: float arrived_at: float
started_at: float | None = None started_at: float | None = None
handling_time: float | None = None handling_time: float | None = None
finished_at: float | None = None finished_at: float | None = None
customers_unhandled: list[Customer] = []
customers_in_progress: list[Customer] = []
customers_handled: list[Customer] = []
customers_unhandled: list[Customer] = [] class Server(Process):
customers_in_progress: list[Customer] = []
customers_handled: list[Customer] = []
class Server(Process):
def run(self): def run(self):
while True: while True:
if len(customers_unhandled) > 0: if len(customers_unhandled) > 0:
@@ -33,12 +35,13 @@ class Server(Process):
customer.finished_at = self.simulation.clock customer.finished_at = self.simulation.clock
customers_in_progress.remove(customer) customers_in_progress.remove(customer)
customers_handled.append(customer) customers_handled.append(customer)
self.simulation.log(f"unhandled customers: {len(customers_unhandled)}") self.simulation.log(
f"unhandled customers: {len(customers_unhandled)}"
)
else: else:
yield from self.suspend() yield from self.suspend()
class CustomerGenerator(Process):
class CustomerGenerator(Process):
def __init__(self, lam=40, servers=[]): def __init__(self, lam=40, servers=[]):
self.lam = lam self.lam = lam
self.servers = servers self.servers = servers
@@ -46,18 +49,19 @@ class CustomerGenerator(Process):
def run(self): def run(self):
while True: while True:
wait_for = float(max(0, (np.random.poisson(lam=self.lam, size=1) / 10)[0])) wait_for = float(
max(0, (np.random.poisson(lam=self.lam, size=1) / 10)[0])
)
customer = Customer(arrived_at=self.simulation.clock) customer = Customer(arrived_at=self.simulation.clock)
customers_unhandled.append(customer) customers_unhandled.append(customer)
for server in self.servers: for server in self.servers:
server.resume() server.resume()
yield from self.hold(wait_for) yield from self.hold(wait_for)
def run_simulation(n_servers=2, lam=40):
servers = [Server() for _ in range(n_servers)] servers = [Server() for _ in range(n_servers)]
simulation = Simulation( simulation = Simulation(
processes=[CustomerGenerator(servers=servers, lam=lam), *servers] processes=[CustomerGenerator(servers=servers, lam=lam), *servers],
enable_logging=False,
) )
simulation.start() simulation.start()
@@ -67,9 +71,56 @@ def run_simulation(n_servers=2, lam=40):
df_servers = pl.DataFrame([server.__dict__ for server in servers]) df_servers = pl.DataFrame([server.__dict__ for server in servers])
# print(f"\nservers: {len(servers)}") utilization = {
print("\ncustomers", df_customers[["queue_time", "handling_time"]].mean()) f"utilization_server_{key}": value
print("\nservers", df_servers[["id", "utilization"]]) for key, value in dict(
zip(df_servers["id"].to_list(), df_servers["utilization"].to_list())
).items()
}
return {
"n_servers": n_servers,
"lam": lam,
"queue_time": df_customers["queue_time"].mean(),
"handling_time": df_customers["handling_time"].mean(),
**utilization,
}
run_simulation(n_servers=3) with ProcessPoolExecutor(max_workers=4) as executor:
n_servers = [n + 1 for n in range(10)]
lam = [(n + 1) * 4 for n in range(10)]
parameter_values = [
{"n_servers": n, "lam": m} for n in n_servers for m in lam for _ in range(10)
]
futures = [
executor.submit(run_simulation, **parameters) for parameters in parameter_values
]
results = []
for future in tqdm(as_completed(futures), total=len(parameter_values)):
result = future.result()
results.append(result)
df = (
pl.DataFrame(results)
.fill_null(0)
.group_by(["n_servers", "lam"])
.agg(pl.all().mean())
.sort(["n_servers", "lam"])
)
df_queue_time = df.pivot("lam", index="n_servers", values="queue_time").sort(
"n_servers"
)
def stats(column):
return (
df.pivot("lam", index="n_servers", values=column)
.sort("n_servers")
.with_columns(pl.all().round(2))
)
pl.Config.set_tbl_cols(20)
print(df)
print(stats("queue_time"))
print(stats("utilization_server_1"))

View File

@@ -3,10 +3,11 @@ from typing import Any, Generator, Protocol
class BaseSimulation: class BaseSimulation:
clock: float clock: float
enable_logging: bool
def log(self, msg): def log(self, msg):
if self.enable_logging:
print(f"{'{:.2f}'.format(self.clock)}: {msg}") print(f"{'{:.2f}'.format(self.clock)}: {msg}")
pass
class BaseProcess(Protocol): class BaseProcess(Protocol):
@@ -42,6 +43,7 @@ class Process(BaseProcess):
self.simulation.clock - self.started_at self.simulation.clock - self.started_at
) )
except Exception as e: except Exception as e:
if self.simulation.enable_logging:
print(e) print(e)
def suspend(self): def suspend(self):
@@ -61,9 +63,10 @@ class Process(BaseProcess):
class Simulation(BaseSimulation): class Simulation(BaseSimulation):
def __init__(self, processes: list[Process]): def __init__(self, processes: list[Process], enable_logging=True):
self.clock = 0 self.clock = 0
self.processes = [] self.processes = []
self.enable_logging = enable_logging
for process in processes: for process in processes:
self.register_process(process) self.register_process(process)