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,63 +1,67 @@
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
class Customer:
arrived_at: float
started_at: float | None = None
handling_time: float | None = None
finished_at: float | None = None
customers_unhandled: list[Customer] = []
customers_in_progress: list[Customer] = []
customers_handled: list[Customer] = []
class Server(Process):
def run(self):
while True:
if len(customers_unhandled) > 0:
customer = customers_unhandled[0]
customers_unhandled.remove(customer)
customers_in_progress.append(customer)
customer.started_at = self.simulation.clock
handling_time = float(max(0, np.random.normal(5, 2.5, size=1)[0]))
yield from self.hold(handling_time)
customer.handling_time = handling_time
customer.finished_at = self.simulation.clock
customers_in_progress.remove(customer)
customers_handled.append(customer)
self.simulation.log(f"unhandled customers: {len(customers_unhandled)}")
else:
yield from self.suspend()
class CustomerGenerator(Process):
def __init__(self, lam=40, servers=[]):
self.lam = lam
self.servers = servers
super().__init__()
def run(self):
while True:
wait_for = float(max(0, (np.random.poisson(lam=self.lam, size=1) / 10)[0]))
customer = Customer(arrived_at=self.simulation.clock)
customers_unhandled.append(customer)
for server in self.servers:
server.resume()
yield from self.hold(wait_for)
def run_simulation(n_servers=2, lam=40): def run_simulation(n_servers=2, lam=40):
@dataclass
class Customer:
arrived_at: float
started_at: float | None = None
handling_time: float | None = None
finished_at: float | None = None
customers_unhandled: list[Customer] = []
customers_in_progress: list[Customer] = []
customers_handled: list[Customer] = []
class Server(Process):
def run(self):
while True:
if len(customers_unhandled) > 0:
customer = customers_unhandled[0]
customers_unhandled.remove(customer)
customers_in_progress.append(customer)
customer.started_at = self.simulation.clock
handling_time = float(max(0, np.random.normal(5, 2.5, size=1)[0]))
yield from self.hold(handling_time)
customer.handling_time = handling_time
customer.finished_at = self.simulation.clock
customers_in_progress.remove(customer)
customers_handled.append(customer)
self.simulation.log(
f"unhandled customers: {len(customers_unhandled)}"
)
else:
yield from self.suspend()
class CustomerGenerator(Process):
def __init__(self, lam=40, servers=[]):
self.lam = lam
self.servers = servers
super().__init__()
def run(self):
while True:
wait_for = float(
max(0, (np.random.poisson(lam=self.lam, size=1) / 10)[0])
)
customer = Customer(arrived_at=self.simulation.clock)
customers_unhandled.append(customer)
for server in self.servers:
server.resume()
yield from self.hold(wait_for)
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):
print(f"{'{:.2f}'.format(self.clock)}: {msg}") if self.enable_logging:
pass print(f"{'{:.2f}'.format(self.clock)}: {msg}")
class BaseProcess(Protocol): class BaseProcess(Protocol):
@@ -42,7 +43,8 @@ class Process(BaseProcess):
self.simulation.clock - self.started_at self.simulation.clock - self.started_at
) )
except Exception as e: except Exception as e:
print(e) if self.simulation.enable_logging:
print(e)
def suspend(self): def suspend(self):
self.suspended = True self.suspended = True
@@ -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)