run multiple simulations for different sets of parameters
This commit is contained in:
33
poetry.lock
generated
33
poetry.lock
generated
@@ -1,5 +1,16 @@
|
||||
# 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]]
|
||||
name = "numpy"
|
||||
version = "2.0.1"
|
||||
@@ -95,7 +106,27 @@ timezone = ["backports-zoneinfo", "tzdata"]
|
||||
xlsx2csv = ["xlsx2csv (>=0.8.0)"]
|
||||
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]
|
||||
lock-version = "2.0"
|
||||
python-versions = "^3.10"
|
||||
content-hash = "3b5f1c763a56f03ec547b2513ce2ef1a9935ac6df8a8caca8288795aee44cbbf"
|
||||
content-hash = "e518e9e57f22023c75b734bc9fdfb85e25b6b937f87bbf5a26c1b31ff370b15f"
|
||||
|
||||
@@ -9,6 +9,7 @@ readme = "README.md"
|
||||
python = "^3.10"
|
||||
numpy = "^2.0.1"
|
||||
polars = "^1.4.1"
|
||||
tqdm = "^4.66.5"
|
||||
|
||||
|
||||
[build-system]
|
||||
|
||||
@@ -1,25 +1,27 @@
|
||||
from concurrent.futures import ProcessPoolExecutor, as_completed
|
||||
from dataclasses import dataclass
|
||||
|
||||
import numpy as np
|
||||
import polars as pl
|
||||
from tqdm import tqdm
|
||||
|
||||
from simulation.lib import Process, Simulation
|
||||
|
||||
|
||||
@dataclass
|
||||
class Customer:
|
||||
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] = []
|
||||
|
||||
customers_unhandled: list[Customer] = []
|
||||
customers_in_progress: list[Customer] = []
|
||||
customers_handled: list[Customer] = []
|
||||
|
||||
|
||||
class Server(Process):
|
||||
class Server(Process):
|
||||
def run(self):
|
||||
while True:
|
||||
if len(customers_unhandled) > 0:
|
||||
@@ -33,12 +35,13 @@ class Server(Process):
|
||||
customer.finished_at = self.simulation.clock
|
||||
customers_in_progress.remove(customer)
|
||||
customers_handled.append(customer)
|
||||
self.simulation.log(f"unhandled customers: {len(customers_unhandled)}")
|
||||
self.simulation.log(
|
||||
f"unhandled customers: {len(customers_unhandled)}"
|
||||
)
|
||||
else:
|
||||
yield from self.suspend()
|
||||
|
||||
|
||||
class CustomerGenerator(Process):
|
||||
class CustomerGenerator(Process):
|
||||
def __init__(self, lam=40, servers=[]):
|
||||
self.lam = lam
|
||||
self.servers = servers
|
||||
@@ -46,18 +49,19 @@ class CustomerGenerator(Process):
|
||||
|
||||
def run(self):
|
||||
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)
|
||||
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):
|
||||
servers = [Server() for _ in range(n_servers)]
|
||||
simulation = Simulation(
|
||||
processes=[CustomerGenerator(servers=servers, lam=lam), *servers]
|
||||
processes=[CustomerGenerator(servers=servers, lam=lam), *servers],
|
||||
enable_logging=False,
|
||||
)
|
||||
simulation.start()
|
||||
|
||||
@@ -67,9 +71,56 @@ def run_simulation(n_servers=2, lam=40):
|
||||
|
||||
df_servers = pl.DataFrame([server.__dict__ for server in servers])
|
||||
|
||||
# print(f"\nservers: {len(servers)}")
|
||||
print("\ncustomers", df_customers[["queue_time", "handling_time"]].mean())
|
||||
print("\nservers", df_servers[["id", "utilization"]])
|
||||
utilization = {
|
||||
f"utilization_server_{key}": value
|
||||
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"))
|
||||
|
||||
@@ -3,10 +3,11 @@ from typing import Any, Generator, Protocol
|
||||
|
||||
class BaseSimulation:
|
||||
clock: float
|
||||
enable_logging: bool
|
||||
|
||||
def log(self, msg):
|
||||
if self.enable_logging:
|
||||
print(f"{'{:.2f}'.format(self.clock)}: {msg}")
|
||||
pass
|
||||
|
||||
|
||||
class BaseProcess(Protocol):
|
||||
@@ -42,6 +43,7 @@ class Process(BaseProcess):
|
||||
self.simulation.clock - self.started_at
|
||||
)
|
||||
except Exception as e:
|
||||
if self.simulation.enable_logging:
|
||||
print(e)
|
||||
|
||||
def suspend(self):
|
||||
@@ -61,9 +63,10 @@ class Process(BaseProcess):
|
||||
|
||||
|
||||
class Simulation(BaseSimulation):
|
||||
def __init__(self, processes: list[Process]):
|
||||
def __init__(self, processes: list[Process], enable_logging=True):
|
||||
self.clock = 0
|
||||
self.processes = []
|
||||
self.enable_logging = enable_logging
|
||||
for process in processes:
|
||||
self.register_process(process)
|
||||
|
||||
|
||||
Reference in New Issue
Block a user