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.
|
# 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"
|
||||||
|
|||||||
@@ -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]
|
||||||
|
|||||||
@@ -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"))
|
||||||
|
|||||||
@@ -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)
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user